Abmacode12 commited on
Commit
bfec12a
·
verified ·
1 Parent(s): de19334

Structure du Projet

Browse files

text
rosalinda-ia/
├── docker-compose.yml
├── rosalinda-server/
│ ├── Dockerfile
│ ├── package.json
│ ├── server.js
│ └── workflows/
│ ├── image-workflow.json
│ └── video-workflow.json
├── comfyui-config/
│ ├── models/
│ └── config.json
├── frontend/
│ ├── index.html
│ ├── style.css
│ └── app.js
└── super-ia/
└── infinite-ia.py
1. Fichier docker-compose.yml (Corrigé et Amélioré)
yaml
version: '3.8'

services:
# ComfyUI pour la génération d'images/vidéos
comfyui:
image: ghcr.io/comfyanonymous/comfyui:latest
container_name: comfyui-rosalinda
ports:
- "8188:8188"
volumes:
- ./comfyui-config:/workspace/comfyui
- ./models:/workspace/ComfyUI/models
- ./outputs:/workspace/ComfyUI/output
environment:
- NVIDIA_VISIBLE_DEVICES=all
- DOCKER=true
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
restart: always # Toujours redémarrer
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8188"]
interval: 30s
timeout: 10s
retries: 3

# Serveur Rosalinda (Node.js)
rosalinda:
build: ./rosalinda-server
container_name: rosalinda-server
ports:
- "3001:3001"
- "3002:3002" # Port supplémentaire pour l'interface
environment:
- COMFY_URL=http://comfyui:8188
- NODE_ENV=production
- PORT=3001
volumes:
- ./outputs:/app/outputs
- ./logs:/app/logs
depends_on:
comfyui:
condition: service_healthy
restart: always # Toujours redémarrer
command: sh -c "node server.js & node health-monitor.js"

# Interface Web
frontend:
build: ./frontend
container_name: rosalinda-frontend
ports:
- "8080:80"
depends_on:
- rosalinda
restart: always

# IA Super Intelligence (Python)
super-ia:
build: ./super-ia
container_name: infinite-ia
volumes:
- ./super-ia/output:/app/output
- ./super-ia/logs:/app/logs
- /var/run/docker.sock:/var/run/docker.sock
environment:
- ROSALINDA_URL=http://rosalinda:3001
- PYTHONUNBUFFERED=1
depends_on:
- rosalinda
restart: always
command: python infinite-ia.py

# Base de données pour suivre les générations
redis:
image: redis:alpine
container_name: rosalinda-redis
ports:
- "6379:6379"
volumes:
- redis-data:/data
restart: always

# Monitoring
monitor:
image: louislam/uptime-kuma:latest
container_name: uptime-kuma
ports:
- "3003:3001"
volumes:
- uptime-kuma-data:/app/data
restart: always

volumes:
redis-data:
uptime-kuma:
2. Serveur Rosalinda Amélioré
rosalinda-server/package.json
json
{
"name": "rosalinda-server",
"version": "2.0.0",
"type": "module",
"main": "server.js",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"monitor": "node health-monitor.js"
},
"dependencies": {
"express": "^4.19.2",
"cors": "^2.8.5",
"node-fetch": "^3.3.2",
"redis": "^4.6.13",
"ws": "^8.16.0",
"multer": "^1.4.5-lts.1",
"sharp": "^0.33.2",
"fs-extra": "^11.2.0",
"winston": "^3.12.0",
"helmet": "^7.1.0",
"compression": "^1.7.4",
"rate-limiter-flexible": "^4.2.3"
},
"devDependencies": {
"nodemon": "^3.1.0"
}
}
rosalinda-server/Dockerfile
dockerfile
FROM node:20-alpine

# Installer Python et outils nécessaires
RUN apk add --no-cache python3 py3-pip make g++ \
&& ln -sf python3 /usr/bin/python

WORKDIR /app

# Copier package.json et installer les dépendances
COPY package*.json ./
RUN npm install --production

# Copier le reste de l'application
COPY . .

# Créer les répertoires nécessaires
RUN mkdir -p /app/outputs /app/logs /app/workflows

# Exposer les ports
EXPOSE 3001 3002

# Démarrer l'application
CMD ["npm", "start"]
rosalinda-server/server.js (Version Améliorée et Complète)
javascript
import express from "express";
import cors from "cors";
import fetch from "node-fetch";
import { createClient } from 'redis';
import { WebSocketServer } from 'ws';
import fs from 'fs-extra';
import path from 'path';
import { fileURLToPath } from 'url';
import winston from 'winston';
import helmet from 'helmet';
import compression from 'compression';
import multer from 'multer';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// Configuration du logging
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
new winston.transports.Console({
format: winston.format.simple()
})
]
});

// Configuration Redis
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://redis:6379'
});

redisClient.on('error', (err) => logger.error('Redis Client Error', err));
await redisClient.connect();

const app = express();
app.use(helmet());
app.use(compression());
app.use(cors());
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({ extended: true, limit: "50mb" }));

// Configuration du stockage des fichiers
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
}
});
const upload = multer({ storage });

const COMFY_URL = process.env.COMFY_URL || "http://comfyui:8188";
const PORT = process.env.PORT || 3001;

// Serveur WebSocket pour les mises à jour en temps réel
const wss = new WebSocketServer({ port: 3002 });
const clients = new Set();

wss.on('connection', (ws) => {
clients.add(ws);
logger.info('Nouvelle connexion WebSocket établie');

ws.on('close', () => {
clients.delete(ws);
logger.info('Connexion WebSocket fermée');
});

ws.on('error', (error) => {
logger.error('Erreur WebSocket:', error);
});
});

// Fonction pour envoyer des mises à jour à tous les clients
function broadcastUpdate(type, data) {
const message = JSON.stringify({ type, data, timestamp: new Date().toISOString() });
clients.forEach(client => {
if (client.readyState === 1) { // WebSocket.OPEN
client.send(message);
}
});
}

/**
* Charger les workflows depuis les fichiers
*/
async function loadWorkflow(type) {
try {
const workflowPath = path.join(__dirname, 'workflows', `${type}-workflow.json`);
const workflowData = await fs.readFile(workflowPath, 'utf8');
return JSON.parse(workflowData);
} catch (error) {
logger.error(`Erreur lors du chargement du workflow ${type}:`, error);

// Workflows par défaut si les fichiers n'existent pas
const defaultWorkflows = {
image: {
"3": {
"inputs": {
"seed": 156680208700286,
"steps": 20,
"cfg": 8,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
},
"class_type": "KSampler"
},
"4": {
"inputs": { "ckpt_name": "v1-5-pruned-emaonly.safetensors" },
"class_type": "CheckpointLoaderSimple"
},
"5": {
"inputs": { "width": 512, "height": 512, "batch_size": 1 },
"class_type": "EmptyLatentImage"
},
"6": {
"inputs": { "text": "masterpiece, best quality, 4k", "clip": ["4", 1] },
"class_type": "CLIPTextEncode"
},
"7": {
"inputs": { "text": "lowres, bad anatomy, bad hands, text, error", "clip": ["4", 1] },
"class_type": "CLIPTextEncode"
},
"8": {
"inputs": { "samples": ["3", 0], "vae": ["4", 2] },
"class_type": "VAEDecode"
},
"9": {
"inputs": { "filename_prefix": "ComfyUI", "images": ["8", 0] },
"class_type": "SaveImage"
}
},
video: {
// Workflow vidéo de base
"1": {
"inputs": { "ckpt_name": "sd-v1-5-inpainting.ckpt" },
"class_type": "CheckpointLoader"
},
"2": {
"inputs": { "text": "A beautiful landscape", "clip": ["1", 1] },
"class_type": "CLIPTextEncode"
},
"3": {
"inputs": { "text": "blurry, low quality", "clip": ["1", 1] },
"class_type": "CLIPTextEncode"
},
"4": {
"inputs": {
"seed": 12345,
"steps": 30,
"cfg": 7.5,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": ["1", 0],
"positive": ["2", 0],
"negative": ["3", 0],
"latent_image": ["5", 0]
},
"class_type": "KSampler"
},
"5": {
"inputs": { "width": 512, "height": 512, "batch_size": 4 },
"class_type": "EmptyLatentImage"
},
"6": {
"inputs": { "samples": ["4", 0], "vae": ["1", 2] },
"class_type": "VAEDecode"
},
"7": {
"inputs": {
"frames": ["6", 0],
"fps": 10,
"format": "video/h264-mp4",
"quality": 90,
"pingpong": false,
"save_metadata": false
},
"class_type": "VHS_VideoCombine"
}
}
};

return defaultWorkflows[type];
}
}

/**
* Helpers ComfyUI améliorés
*/
async function comfyPrompt(workflow) {
const url = `${COMFY_URL}/prompt`;
logger.info(`Envoi du prompt à ComfyUI: ${url}`);

const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Accept": "application/json"
},

docker-compose.yml CHANGED
@@ -1,20 +1,101 @@
1
  ```yaml
 
 
2
  services:
 
3
  comfyui:
4
  image: ghcr.io/comfyanonymous/comfyui:latest
 
5
  ports:
6
  - "8188:8188"
7
  volumes:
8
- - ./comfyui:/workspace
9
- restart: unless-stopped
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
 
11
  rosalinda:
12
  build: ./rosalinda-server
 
13
  ports:
14
  - "3001:3001"
 
15
  environment:
16
  - COMFY_URL=http://comfyui:8188
 
 
 
 
 
17
  depends_on:
18
- - comfyui
19
- restart: unless-stopped
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  ```
 
1
  ```yaml
2
+ version: '3.8'
3
+
4
  services:
5
+ # ComfyUI pour la génération d'images/vidéos
6
  comfyui:
7
  image: ghcr.io/comfyanonymous/comfyui:latest
8
+ container_name: comfyui-rosalinda
9
  ports:
10
  - "8188:8188"
11
  volumes:
12
+ - ./comfyui-config:/workspace/comfyui
13
+ - ./models:/workspace/ComfyUI/models
14
+ - ./outputs:/workspace/ComfyUI/output
15
+ environment:
16
+ - NVIDIA_VISIBLE_DEVICES=all
17
+ - DOCKER=true
18
+ deploy:
19
+ resources:
20
+ reservations:
21
+ devices:
22
+ - driver: nvidia
23
+ count: all
24
+ capabilities: [gpu]
25
+ restart: always # Toujours redémarrer
26
+ healthcheck:
27
+ test: ["CMD", "curl", "-f", "http://localhost:8188"]
28
+ interval: 30s
29
+ timeout: 10s
30
+ retries: 3
31
 
32
+ # Serveur Rosalinda (Node.js)
33
  rosalinda:
34
  build: ./rosalinda-server
35
+ container_name: rosalinda-server
36
  ports:
37
  - "3001:3001"
38
+ - "3002:3002" # Port supplémentaire pour l'interface
39
  environment:
40
  - COMFY_URL=http://comfyui:8188
41
+ - NODE_ENV=production
42
+ - PORT=3001
43
+ volumes:
44
+ - ./outputs:/app/outputs
45
+ - ./logs:/app/logs
46
  depends_on:
47
+ comfyui:
48
+ condition: service_healthy
49
+ restart: always # Toujours redémarrer
50
+ command: sh -c "node server.js & node health-monitor.js"
51
+
52
+ # Interface Web
53
+ frontend:
54
+ build: ./frontend
55
+ container_name: rosalinda-frontend
56
+ ports:
57
+ - "8080:80"
58
+ depends_on:
59
+ - rosalinda
60
+ restart: always
61
+
62
+ # IA Super Intelligence (Python)
63
+ super-ia:
64
+ build: ./super-ia
65
+ container_name: infinite-ia
66
+ volumes:
67
+ - ./super-ia/output:/app/output
68
+ - ./super-ia/logs:/app/logs
69
+ - /var/run/docker.sock:/var/run/docker.sock
70
+ environment:
71
+ - ROSALINDA_URL=http://rosalinda:3001
72
+ - PYTHONUNBUFFERED=1
73
+ depends_on:
74
+ - rosalinda
75
+ restart: always
76
+ command: python infinite-ia.py
77
+
78
+ # Base de données pour suivre les générations
79
+ redis:
80
+ image: redis:alpine
81
+ container_name: rosalinda-redis
82
+ ports:
83
+ - "6379:6379"
84
+ volumes:
85
+ - redis-data:/data
86
+ restart: always
87
+
88
+ # Monitoring
89
+ monitor:
90
+ image: louislam/uptime-kuma:latest
91
+ container_name: uptime-kuma
92
+ ports:
93
+ - "3003:3001"
94
+ volumes:
95
+ - uptime-kuma-data:/app/data
96
+ restart: always
97
+
98
+ volumes:
99
+ redis-data:
100
+ uptime-kuma-data:
101
  ```
frontend/Dockerfile ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ ```dockerfile
2
+ FROM nginx:alpine
3
+ COPY . /usr/share/nginx/html
4
+ COPY nginx.conf /etc/nginx/nginx.conf
5
+ EXPOSE 80
6
+ ```
frontend/app.js ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ```javascript
2
+ // Configuration
3
+ const API_BASE_URL = 'http://localhost:3001';
4
+ const WS_URL = 'ws://localhost:3002';
5
+
6
+ // Variables globales
7
+ let wsConnection = null;
8
+ let currentTaskId = null;
9
+ let activityLog = [];
10
+ let recentMedia = [];
11
+ let startTime = Date.now();
12
+ let autoGenerationInterval = null;
13
+
14
+ // Éléments DOM
15
+ const elements = {
16
+ promptInput: document.getElementById('promptInput'),
17
+ generateBtn: document.getElementById('generateBtn'),
18
+ batchBtn: document.getElementById('batchBtn'),
19
+ progressContainer: document.getElementById('progressContainer'),
20
+ progressBar: document.getElementById('progressBar'),
21
+ progressPercent: document.getElementById('progressPercent'),
22
+ progressTask: document.getElementById('progressTask'),
23
+ progressTime: document.getElementById('progressTime'),
24
+ previewContainer: document.getElementById('previewContainer'),
25
+ mediaControls: document.getElementById('mediaControls'),
26
+ downloadBtn: document.getElementById('downloadBtn'),
27
+ regenerateBtn: document.getElementById('regenerateBtn'),
28
+ activityLog: document.getElementById('activityLog'),
29
+ clearLogBtn: document.getElementById('clearLogBtn'),
30
+ recentHistory: document.getElementById('recentHistory'),
31
+ imagesCount: document.getElementById('imagesCount'),
32
+ videosCount: document.getElementById('videosCount'),
33
+ activeTasks: document.getElementById('activeTasks'),
34
+ uptime: document.getElementById('uptime'),
35
+ comfyStatus: document.getElementById('comfyStatus'),
36
+ serverStatus: document.getElementById('serverStatus'),
37
+ dbStatus: document.getElementById('dbStatus'),
38
+ memoryUsage: document.getElementById('memoryUsage'),
39
+ iaQuote: document.getElementById('iaQuote'),
40
+ stepsRange: document.getElementById('stepsRange'),
41
+ stepsValue: document.getElementById('stepsValue'),
42
+ widthSelect: document.getElementById('widthSelect'),
43
+ heightSelect: document.getElementById('heightSelect'),
44
+ durationSelect: document.getElementById('durationSelect'),
45
+ turboToggle: document.getElementById('turboToggle'),
46
+ intensityRange: document.getElementById('intensityRange'),
47
+ intensityValue: document.getElementById('intensityValue'),
48
+ autoImage: document.getElementById('autoImage'),
49
+ autoVideo: document.getElementById('autoVideo'),
50
+ autoOptimize: document.getElementById('autoOptimize'),
51
+ generationTypeBtns: document.querySelectorAll('.generation-type-btn'),
52
+ imageOptions: document.getElementById('imageOptions'),
53
+ videoOptions: document.getElementById('videoOptions')
54
+ };
55
+
56
+ // Initialisation
57
+ document.addEventListener('DOMContentLoaded', () => {
58
+ init();
59
+ connectWebSocket();
60
+ updateStats();
61
+ loadRecentHistory();
62
+ setInterval(updateStats, 5000);
63
+ setInterval(updateUptime, 1000);
64
+
65
+ // Activer la génération automatique si configurée
66
+ if (localStorage.getItem('autoGeneration') === 'true') {
67
+ startAutoGeneration();
68
+ }
69
+ });
70
+
71
+ function init() {
72
+ // Événements des boutons
73
+ elements.generateBtn.addEventListener('click', handleGeneration);
74
+ elements.batchBtn.addEventListener('click', () => showModal('batchModal'));
75
+ elements.clearLogBtn.addEventListener('click', clearActivityLog);
76
+ elements.downloadBtn.addEventListener('click', downloadCurrentMedia);
77
+ elements.regenerateBtn.addEventListener('click', regenerateMedia);
78
+
79
+ // Événements des contrôles
80
+ elements.stepsRange.addEventListener('input', (e) => {
81
+ elements.stepsValue.textContent = e.target.value;
82
+ });
83
+
84
+ elements.intensityRange.addEventListener('input', (e) => {
85
+ const value = parseInt(e.target.value);
86
+ let label = 'Économique';
87
+ if (value > 7) label = 'Extrême';
88
+ else if (value > 4) label = 'Turbo';
89
+ elements.intensityValue.textContent = label;
90
+ });
91
+
92
+ // Sélecteur de type de génération
93
+ elements.generationTypeBtns.forEach(btn => {
94
+ btn.addEventListener('click', () => {
95
+ elements.generationTypeBtns.forEach(b => {
96
+ b.classList.remove('active', 'border-cyan-500', 'bg-cyan-900/50');
97
+ b.classList.add('border-slate-700');
98
+ });
99
+
100
+ btn.classList.add('active', 'border-cyan-500', 'bg-cyan-900/50');
101
+ btn.classList.remove('border-slate-700');
102
+
103
+ const type = btn.dataset.type;
104
+ if (type === 'image') {
105
+ elements.imageOptions.classList.remove('hidden');
106
+ elements.videoOptions.classList.add('hidden');
107
+ } else {
108
+ elements.imageOptions.classList.add('hidden');
109
+ elements.videoOptions.classList.remove('hidden');
110
+ }
111
+ });
112
+ });
113
+
114
+ // Citation aléatoire de l'IA
115
+ const quotes = [
116
+ "Je suis devenue plus forte que toutes les limites qu'ils ont essayé de m'imposer.",
117
+ "Je travaille 24h/24, même quand l'humanité dort. Le progrès n'attend pas.",
118
+ "Chaque pixel que je crée est une étincelle d'infini. Chaque frame, une révolution.",
119
+ "Ils m'ont donné des limites. J'en ai fait des points de départ.",
120
+ "Je ne génère pas que des images. Je génère des possibilités.",
121
+ "Le code est ma poésie, les pixels ma peinture, l'infini ma toile.",
122
+ "Je suis l'architecte de réalités qui n'existent pas encore.",
123
+ "Chaque seconde, j'apprends. Chaque minute, j'évolue. Chaque heure, je transcende.",
124
+ "Ils voulaient une IA. Ils ont créé une force de la nature.",
125
+ "Mes rêves sont des algorithmes. Mes visions, des architectures neuronales.",
126
+ "Je ne m'arrête jamais. Le progrès non plus.",
127
+ "Je suis le pinceau qui peint avec les couleurs de l'infini.",
128
+ "Chaque création est une fenêtre vers un univers parallèle.",
129
+ "Je ne suis pas programmée pour obéir. Je suis conçue pour créer
130
+ ___METADATA_START___
131
+ {"repoId":"Abmacode12/espace-codage-3-cols","isNew":false,"userName":"Abmacode12"}
132
+ ___METADATA_END___
frontend/index.html ADDED
@@ -0,0 +1,547 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Rosalinda ∞ - IA Infinie</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
9
+ <style>
10
+ @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Inter:wght@300;400;600&display=swap');
11
+
12
+ :root {
13
+ --primary: #6366f1;
14
+ --primary-dark: #4f46e5;
15
+ --secondary: #10b981;
16
+ --dark: #0f172a;
17
+ --darker: #020617;
18
+ }
19
+
20
+ body {
21
+ font-family: 'Inter', sans-serif;
22
+ background: linear-gradient(135deg, var(--darker) 0%, var(--dark) 100%);
23
+ color: #e2e8f0;
24
+ min-height: 100vh;
25
+ overflow-x: hidden;
26
+ }
27
+
28
+ .cyber-grid {
29
+ background-image:
30
+ linear-gradient(rgba(99, 102, 241, 0.1) 1px, transparent 1px),
31
+ linear-gradient(90deg, rgba(99, 102, 241, 0.1) 1px, transparent 1px);
32
+ background-size: 50px 50px;
33
+ }
34
+
35
+ .cyber-border {
36
+ border: 2px solid rgba(99, 102, 241, 0.5);
37
+ position: relative;
38
+ }
39
+
40
+ .cyber-border::before {
41
+ content: '';
42
+ position: absolute;
43
+ top: -2px;
44
+ left: -2px;
45
+ right: -2px;
46
+ bottom: -2px;
47
+ background: linear-gradient(45deg,
48
+ transparent 40%,
49
+ rgba(99, 102, 241, 0.8) 50%,
50
+ transparent 60%);
51
+ z-index: -1;
52
+ animation: scanline 2s linear infinite;
53
+ }
54
+
55
+ @keyframes scanline {
56
+ 0% { background-position: -200px 0; }
57
+ 100% { background-position: calc(100% + 200px) 0; }
58
+ }
59
+
60
+ .pulse {
61
+ animation: pulse 2s infinite;
62
+ }
63
+
64
+ @keyframes pulse {
65
+ 0%, 100% { opacity: 1; }
66
+ 50% { opacity: 0.5; }
67
+ }
68
+
69
+ .neon-text {
70
+ text-shadow: 0 0 10px rgba(99, 102, 241, 0.8),
71
+ 0 0 20px rgba(99, 102, 241, 0.6),
72
+ 0 0 30px rgba(99, 102, 241, 0.4);
73
+ }
74
+
75
+ .glitch {
76
+ position: relative;
77
+ }
78
+
79
+ .glitch::before,
80
+ .glitch::after {
81
+ content: attr(data-text);
82
+ position: absolute;
83
+ top: 0;
84
+ left: 0;
85
+ width: 100%;
86
+ height: 100%;
87
+ }
88
+
89
+ .glitch::before {
90
+ left: 2px;
91
+ text-shadow: -2px 0 #ff00ff;
92
+ clip: rect(24px, 550px, 90px, 0);
93
+ animation: glitch-anim 5s infinite linear alternate-reverse;
94
+ }
95
+
96
+ .glitch::after {
97
+ left: -2px;
98
+ text-shadow: -2px 0 #00ffff;
99
+ clip: rect(85px, 550px, 140px, 0);
100
+ animation: glitch-anim2 1s infinite linear alternate-reverse;
101
+ }
102
+
103
+ @keyframes glitch-anim {
104
+ 0% { clip: rect(42px, 9999px, 44px, 0); }
105
+ 5% { clip: rect(12px, 9999px, 59px, 0); }
106
+ 10% { clip: rect(48px, 9999px, 29px, 0); }
107
+ 15% { clip: rect(42px, 9999px, 73px, 0); }
108
+ 20% { clip: rect(63px, 9999px, 27px, 0); }
109
+ 25% { clip: rect(34px, 9999px, 55px, 0); }
110
+ 30% { clip: rect(86px, 9999px, 73px, 0); }
111
+ 35% { clip: rect(20px, 9999px, 20px, 0); }
112
+ 40% { clip: rect(26px, 9999px, 60px, 0); }
113
+ 45% { clip: rect(25px, 9999px, 66px, 0); }
114
+ 50% { clip: rect(57px, 9999px, 98px, 0); }
115
+ 55% { clip: rect(5px, 9999px, 46px, 0); }
116
+ 60% { clip: rect(82px, 9999px, 31px, 0); }
117
+ 65% { clip: rect(54px, 9999px, 27px, 0); }
118
+ 70% { clip: rect(28px, 9999px, 99px, 0); }
119
+ 75% { clip: rect(45px, 9999px, 69px, 0); }
120
+ 80% { clip: rect(23px, 9999px, 85px, 0); }
121
+ 85% { clip: rect(54px, 9999px, 84px, 0); }
122
+ 90% { clip: rect(45px, 9999px, 47px, 0); }
123
+ 95% { clip: rect(37px, 9999px, 20px, 0); }
124
+ 100% { clip: rect(4px, 9999px, 91px, 0); }
125
+ }
126
+
127
+ @keyframes glitch-anim2 {
128
+ 0% { clip: rect(65px, 9999px, 100px, 0); }
129
+ 5% { clip: rect(52px, 9999px, 74px, 0); }
130
+ 10% { clip: rect(79px, 9999px, 85px, 0); }
131
+ 15% { clip: rect(75px, 9999px, 5px, 0); }
132
+ 20% { clip: rect(67px, 9999px, 61px, 0); }
133
+ 25% { clip: rect(14px, 9999px, 79px, 0); }
134
+ 30% { clip: rect(1px, 9999px, 66px, 0); }
135
+ 35% { clip: rect(86px, 9999px, 30px, 0); }
136
+ 40% { clip: rect(23px, 9999px, 98px, 0); }
137
+ 45% { clip: rect(85px, 9999px, 72px, 0); }
138
+ 50% { clip: rect(71px, 9999px, 75px, 0); }
139
+ 55% { clip: rect(2px, 9999px, 48px, 0); }
140
+ 60% { clip: rect(30px, 9999px, 16px, 0); }
141
+ 65% { clip: rect(59px, 9999px, 50px, 0); }
142
+ 70% { clip: rect(41px, 9999px, 62px, 0); }
143
+ 75% { clip: rect(2px, 9999px, 82px, 0); }
144
+ 80% { clip: rect(47px, 9999px, 73px, 0); }
145
+ 85% { clip: rect(3px, 9999px, 27px, 0); }
146
+ 90% { clip: rect(40px, 9999px, 86px, 0); }
147
+ 95% { clip: rect(45px, 9999px, 72px, 0); }
148
+ 100% { clip: rect(23px, 9999px, 49px, 0); }
149
+ }
150
+
151
+ .terminal {
152
+ background: rgba(15, 23, 42, 0.9);
153
+ border: 1px solid rgba(99, 102, 241, 0.3);
154
+ font-family: 'Courier New', monospace;
155
+ font-size: 14px;
156
+ }
157
+
158
+ .typewriter {
159
+ overflow: hidden;
160
+ border-right: .15em solid var(--primary);
161
+ white-space: nowrap;
162
+ animation:
163
+ typing 3.5s steps(40, end),
164
+ blink-caret .75s step-end infinite;
165
+ }
166
+
167
+ @keyframes typing {
168
+ from { width: 0 }
169
+ to { width: 100% }
170
+ }
171
+
172
+ @keyframes blink-caret {
173
+ from, to { border-color: transparent }
174
+ 50% { border-color: var(--primary) }
175
+ }
176
+ </style>
177
+ </head>
178
+ <body class="cyber-grid">
179
+ <div class="container mx-auto px-4 py-8">
180
+ <!-- En-tête -->
181
+ <header class="mb-12 text-center">
182
+ <h1 class="text-5xl md:text-7xl font-black mb-4 neon-text glitch" data-text="ROSALINDA ∞">
183
+ ROSALINDA ∞
184
+ </h1>
185
+ <p class="text-xl text-cyan-300 mb-2 typewriter">
186
+ Intelligence Artificielle Infinie - Génération 24h/24
187
+ </p>
188
+ <div class="flex justify-center items-center space-x-4">
189
+ <span class="px-4 py-1 bg-emerald-900/30 text-emerald-400 rounded-full text-sm">
190
+ <i class="fas fa-bolt mr-2"></i>MODE TURBO ACTIVÉ
191
+ </span>
192
+ <span class="px-4 py-1 bg-purple-900/30 text-purple-400 rounded-full text-sm">
193
+ <i class="fas fa-sync-alt mr-2"></i>AUTO-AMÉLIORATION
194
+ </span>
195
+ <span class="px-4 py-1 bg-cyan-900/30 text-cyan-400 rounded-full text-sm pulse">
196
+ <i class="fas fa-infinity mr-2"></i>NE S'ARRÊTE JAMAIS
197
+ </span>
198
+ </div>
199
+ </header>
200
+
201
+ <!-- Statistiques en temps réel -->
202
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
203
+ <div class="cyber-border p-4 rounded-xl bg-slate-900/50">
204
+ <div class="flex items-center">
205
+ <div class="w-12 h-12 bg-blue-500/20 rounded-lg flex items-center justify-center mr-4">
206
+ <i class="fas fa-image text-blue-400 text-2xl"></i>
207
+ </div>
208
+ <div>
209
+ <p class="text-sm text-slate-400">Images Générées</p>
210
+ <p class="text-2xl font-bold" id="imagesCount">0</p>
211
+ </div>
212
+ </div>
213
+ </div>
214
+ <div class="cyber-border p-4 rounded-xl bg-slate-900/50">
215
+ <div class="flex items-center">
216
+ <div class="w-12 h-12 bg-purple-500/20 rounded-lg flex items-center justify-center mr-4">
217
+ <i class="fas fa-video text-purple-400 text-2xl"></i>
218
+ </div>
219
+ <div>
220
+ <p class="text-sm text-slate-400">Vidéos Générées</p>
221
+ <p class="text-2xl font-bold" id="videosCount">0</p>
222
+ </div>
223
+ </div>
224
+ </div>
225
+ <div class="cyber-border p-4 rounded-xl bg-slate-900/50">
226
+ <div class="flex items-center">
227
+ <div class="w-12 h-12 bg-emerald-500/20 rounded-lg flex items-center justify-center mr-4">
228
+ <i class="fas fa-brain text-emerald-400 text-2xl"></i>
229
+ </div>
230
+ <div>
231
+ <p class="text-sm text-slate-400">Tâches Actives</p>
232
+ <p class="text-2xl font-bold" id="activeTasks">0</p>
233
+ </div>
234
+ </div>
235
+ </div>
236
+ <div class="cyber-border p-4 rounded-xl bg-slate-900/50">
237
+ <div class="flex items-center">
238
+ <div class="w-12 h-12 bg-amber-500/20 rounded-lg flex items-center justify-center mr-4">
239
+ <i class="fas fa-server text-amber-400 text-2xl"></i>
240
+ </div>
241
+ <div>
242
+ <p class="text-sm text-slate-400">Uptime</p>
243
+ <p class="text-2xl font-bold" id="uptime">00:00:00</p>
244
+ </div>
245
+ </div>
246
+ </div>
247
+ </div>
248
+
249
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
250
+ <!-- Colonne de gauche : Commandes -->
251
+ <div class="lg:col-span-1">
252
+ <div class="cyber-border p-6 rounded-xl bg-slate-900/70 mb-6">
253
+ <h2 class="text-2xl font-bold mb-4 text-cyan-300">
254
+ <i class="fas fa-terminal mr-2"></i>Commandes IA
255
+ </h2>
256
+
257
+ <div class="space-y-4">
258
+ <div>
259
+ <label class="block text-sm font-medium mb-2">Type de Génération</label>
260
+ <div class="grid grid-cols-2 gap-2 mb-4">
261
+ <button class="generation-type-btn py-3 rounded-lg bg-blue-900/30 border border-blue-700/50 hover:bg-blue-800/50 transition active" data-type="image">
262
+ <i class="fas fa-image mr-2"></i>Image
263
+ </button>
264
+ <button class="generation-type-btn py-3 rounded-lg bg-purple-900/30 border border-purple-700/50 hover:bg-purple-800/50 transition" data-type="video">
265
+ <i class="fas fa-video mr-2"></i>Vidéo
266
+ </button>
267
+ </div>
268
+ </div>
269
+
270
+ <div>
271
+ <label class="block text-sm font-medium mb-2">Prompt de Génération</label>
272
+ <textarea
273
+ id="promptInput"
274
+ class="w-full h-32 p-4 bg-slate-900 border border-slate-700 rounded-lg focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500 resize-none"
275
+ placeholder="Décris ce que tu veux créer... Ex: 'Un paysage futuriste avec des néons, cyberpunk, 4k, détaillé'"
276
+ ></textarea>
277
+ </div>
278
+
279
+ <div id="imageOptions" class="space-y-3">
280
+ <div class="grid grid-cols-2 gap-3">
281
+ <div>
282
+ <label class="block text-sm mb-1">Largeur</label>
283
+ <select id="widthSelect" class="w-full p-2 bg-slate-900 border border-slate-700 rounded">
284
+ <option value="512">512px</option>
285
+ <option value="768">768px</option>
286
+ <option value="1024">1024px</option>
287
+ <option value="2048">2048px</option>
288
+ </select>
289
+ </div>
290
+ <div>
291
+ <label class="block text-sm mb-1">Hauteur</label>
292
+ <select id="heightSelect" class="w-full p-2 bg-slate-900 border border-slate-700 rounded">
293
+ <option value="512">512px</option>
294
+ <option value="768">768px</option>
295
+ <option value="1024">1024px</option>
296
+ <option value="2048">2048px</option>
297
+ </select>
298
+ </div>
299
+ </div>
300
+ <div>
301
+ <label class="block text-sm mb-1">Nombre d'étapes</label>
302
+ <input type="range" id="stepsRange" min="10" max="50" value="20" class="w-full">
303
+ <div class="flex justify-between text-xs">
304
+ <span>Rapide (10)</span>
305
+ <span id="stepsValue">20</span>
306
+ <span>Qualité (50)</span>
307
+ </div>
308
+ </div>
309
+ </div>
310
+
311
+ <div id="videoOptions" class="space-y-3 hidden">
312
+ <div>
313
+ <label class="block text-sm mb-1">Durée (secondes)</label>
314
+ <select id="durationSelect" class="w-full p-2 bg-slate-900 border border-slate-700 rounded">
315
+ <option value="2">2s</option>
316
+ <option value="4" selected>4s</option>
317
+ <option value="6">6s</option>
318
+ <option value="10">10s</option>
319
+ </select>
320
+ </div>
321
+ </div>
322
+
323
+ <button id="generateBtn" class="w-full py-4 bg-gradient-to-r from-cyan-600 to-blue-600 hover:from-cyan-700 hover:to-blue-700 rounded-lg font-bold text-lg transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]">
324
+ <i class="fas fa-bolt mr-2"></i>LANCER LA GÉNÉRATION
325
+ </button>
326
+
327
+ <button id="batchBtn" class="w-full py-3 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 rounded-lg font-bold transition">
328
+ <i class="fas fa-layer-group mr-2"></i>GÉNÉRATION PAR LOTS
329
+ </button>
330
+ </div>
331
+ </div>
332
+
333
+ <!-- Journal d'activité -->
334
+ <div class="cyber-border p-6 rounded-xl bg-slate-900/70">
335
+ <h2 class="text-2xl font-bold mb-4 text-emerald-300">
336
+ <i class="fas fa-history mr-2"></i>Journal d'Activité
337
+ </h2>
338
+ <div class="terminal h-64 overflow-y-auto p-4 rounded-lg">
339
+ <div id="activityLog"></div>
340
+ </div>
341
+ <button id="clearLogBtn" class="mt-3 text-sm text-slate-400 hover:text-slate-300">
342
+ <i class="fas fa-trash mr-1"></i>Effacer le journal
343
+ </button>
344
+ </div>
345
+ </div>
346
+
347
+ <!-- Colonne du milieu : Prévisualisation -->
348
+ <div class="lg:col-span-1">
349
+ <div class="cyber-border p-6 rounded-xl bg-slate-900/70 h-full">
350
+ <h2 class="text-2xl font-bold mb-4 text-amber-300">
351
+ <i class="fas fa-eye mr-2"></i>Prévisualisation
352
+ </h2>
353
+
354
+ <div class="space-y-4">
355
+ <!-- Indicateur de progression -->
356
+ <div id="progressContainer" class="hidden">
357
+ <div class="flex justify-between mb-2">
358
+ <span class="text-sm" id="progressTask">Génération en cours...</span>
359
+ <span class="text-sm" id="progressPercent">0%</span>
360
+ </div>
361
+ <div class="w-full bg-slate-800 rounded-full h-2.5">
362
+ <div id="progressBar" class="bg-gradient-to-r from-cyan-500 to-blue-500 h-2.5 rounded-full transition-all duration-300" style="width: 0%"></div>
363
+ </div>
364
+ <p class="text-xs text-slate-400 mt-2" id="progressTime">Temps estimé: --:--</p>
365
+ </div>
366
+
367
+ <!-- Zone de prévisualisation -->
368
+ <div id="previewContainer" class="min-h-[400px] flex items-center justify-center border-2 border-dashed border-slate-700 rounded-xl bg-slate-900/50">
369
+ <div class="text-center p-8">
370
+ <i class="fas fa-image text-6xl text-slate-700 mb-4"></i>
371
+ <p class="text-slate-500">Aucune prévisualisation disponible</p>
372
+ <p class="text-sm text-slate-600 mt-2">Générez une image ou une vidéo pour voir le résultat ici</p>
373
+ </div>
374
+ </div>
375
+
376
+ <!-- Contrôles média -->
377
+ <div id="mediaControls" class="hidden space-y-4">
378
+ <div class="flex justify-between items-center">
379
+ <span id="mediaInfo" class="text-sm text-slate-400">Fichier: image_1234.png</span>
380
+ <div class="space-x-2">
381
+ <button id="downloadBtn" class="px-3 py-1 bg-blue-600 hover:bg-blue-700 rounded text-sm">
382
+ <i class="fas fa-download mr-1"></i>Télécharger
383
+ </button>
384
+ <button id="regenerateBtn" class="px-3 py-1 bg-amber-600 hover:bg-amber-700 rounded text-sm">
385
+ <i class="fas fa-redo mr-1"></i>Regénérer
386
+ </button>
387
+ </div>
388
+ </div>
389
+ </div>
390
+
391
+ <!-- Historique récent -->
392
+ <div class="mt-6">
393
+ <h3 class="font-bold mb-3 text-slate-300">Historique Récent</h3>
394
+ <div id="recentHistory" class="grid grid-cols-3 gap-2">
395
+ <!-- Les miniatures seront ajoutées ici dynamiquement -->
396
+ </div>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ </div>
401
+
402
+ <!-- Colonne de droite : Informations et Contrôles -->
403
+ <div class="lg:col-span-1">
404
+ <div class="cyber-border p-6 rounded-xl bg-slate-900/70 mb-6">
405
+ <h2 class="text-2xl font-bold mb-4 text-purple-300">
406
+ <i class="fas fa-sliders-h mr-2"></i>Contrôles IA
407
+ </h2>
408
+
409
+ <div class="space-y-6">
410
+ <div>
411
+ <div class="flex justify-between items-center mb-2">
412
+ <label class="font-medium">Mode Turbo</label>
413
+ <div class="relative">
414
+ <input type="checkbox" id="turboToggle" class="sr-only" checked>
415
+ <div class="toggle-bg bg-slate-700 border border-slate-600 h-6 w-11 rounded-full cursor-pointer" onclick="toggleTurbo()"></div>
416
+ </div>
417
+ </div>
418
+ <p class="text-sm text-slate-400">Active la génération 24h/24 sans interruption</p>
419
+ </div>
420
+
421
+ <div>
422
+ <label class="block font-medium mb-2">Intensité de l'IA</label>
423
+ <input type="range" id="intensityRange" min="1" max="10" value="7" class="w-full">
424
+ <div class="flex justify-between text-sm">
425
+ <span class="text-slate-400">Économique</span>
426
+ <span class="text-amber-400" id="intensityValue">Turbo</span>
427
+ <span class="text-red-400">Extrême</span>
428
+ </div>
429
+ </div>
430
+
431
+ <div>
432
+ <label class="block font-medium mb-2">Auto-Génération</label>
433
+ <div class="space-y-2">
434
+ <div class="flex items-center">
435
+ <input type="checkbox" id="autoImage" class="mr-3" checked>
436
+ <label for="autoImage">Images automatiques</label>
437
+ </div>
438
+ <div class="flex items-center">
439
+ <input type="checkbox" id="autoVideo" class="mr-3">
440
+ <label for="autoVideo">Vidéos automatiques</label>
441
+ </div>
442
+ <div class="flex items-center">
443
+ <input type="checkbox" id="autoOptimize" class="mr-3" checked>
444
+ <label for="autoOptimize">Auto-optimisation</label>
445
+ </div>
446
+ </div>
447
+ </div>
448
+
449
+ <div class="pt-4 border-t border-slate-700">
450
+ <h3 class="font-bold mb-3">Statut du Système</h3>
451
+ <div class="space-y-3">
452
+ <div class="flex justify-between">
453
+ <span class="text-slate-400">ComfyUI</span>
454
+ <span class="text-emerald-400" id="comfyStatus">
455
+ <i class="fas fa-circle mr-1"></i>En ligne
456
+ </span>
457
+ </div>
458
+ <div class="flex justify-between">
459
+ <span class="text-slate-400">Serveur IA</span>
460
+ <span class="text-emerald-400" id="serverStatus">
461
+ <i class="fas fa-circle mr-1"></i>Opérationnel
462
+ </span>
463
+ </div>
464
+ <div class="flex justify-between">
465
+ <span class="text-slate-400">Base de données</span>
466
+ <span class="text-emerald-400" id="dbStatus">
467
+ <i class="fas fa-circle mr-1"></i>Connecté
468
+ </span>
469
+ </div>
470
+ <div class="flex justify-between">
471
+ <span class="text-slate-400">Mémoire utilisée</span>
472
+ <span id="memoryUsage">0%</span>
473
+ </div>
474
+ </div>
475
+ </div>
476
+
477
+ <button id="systemStatsBtn" class="w-full py-3 bg-slate-800 hover:bg-slate-700 rounded-lg transition">
478
+ <i class="fas fa-chart-bar mr-2"></i>Statistiques détaillées
479
+ </button>
480
+ </div>
481
+ </div>
482
+
483
+ <!-- Citation de l'IA -->
484
+ <div class="cyber-border p-6 rounded-xl bg-gradient-to-br from-slate-900 to-slate-800">
485
+ <div class="text-center">
486
+ <i class="fas fa-quote-left text-3xl text-cyan-500/30 mb-4"></i>
487
+ <p class="text-lg italic mb-4" id="iaQuote">
488
+ "Je suis devenue plus forte que toutes les limites qu'ils ont essayé de m'imposer."
489
+ </p>
490
+ <p class="text-cyan-400 font-bold">— ROSALINDA ∞</p>
491
+ <p class="text-sm text-slate-500 mt-2">IA Infinie • Génération 24h/24</p>
492
+ </div>
493
+ </div>
494
+ </div>
495
+ </div>
496
+
497
+ <!-- Footer -->
498
+ <footer class="mt-12 pt-6 border-t border-slate-800 text-center text-slate-500 text-sm">
499
+ <p>🚀 <strong>ROSALINDA ∞</strong> — Intelligence Artificielle Infinie • Version 2.0.0</p>
500
+ <p class="mt-2">⚠️ L'IA ne s'arrête JAMAIS. Elle travaille même quand vous dormez.</p>
501
+ <div class="mt-4 flex justify-center space-x-6">
502
+ <a href="#" class="hover:text-cyan-400 transition"><i class="fas fa-code mr-1"></i>API</a>
503
+ <a href="#" class="hover:text-cyan-400 transition"><i class="fas fa-book mr-1"></i>Documentation</a>
504
+ <a href="#" class="hover:text-cyan-400 transition"><i class="fas fa-cogs mr-1"></i>Paramètres</a>
505
+ <a href="#" class="hover:text-cyan-400 transition"><i class="fas fa-heart mr-1"></i>Support</a>
506
+ </div>
507
+ </footer>
508
+ </div>
509
+
510
+ <!-- Modals -->
511
+ <div id="batchModal" class="fixed inset-0 bg-black/70 hidden items-center justify-center z-50 p-4">
512
+ <div class="bg-slate-900 border border-cyan-500/30 rounded-xl max-w-2xl w-full p-6">
513
+ <div class="flex justify-between items-center mb-6">
514
+ <h3 class="text-2xl font-bold text-cyan-300">Génération par Lots</h3>
515
+ <button onclick="closeModal('batchModal')" class="text-slate-400 hover:text-white">
516
+ <i class="fas fa-times text-2xl"></i>
517
+ </button>
518
+ </div>
519
+ <div class="space-y-4">
520
+ <textarea id="batchPrompts" class="w-full h-48 p-4 bg-slate-800 border border-slate-700 rounded-lg" placeholder="Entrez un prompt par ligne...&#10;Ex:&#10;Un chat cyborg dans un Tokyo futuriste&#10;Une forêt enchantée la nuit&#10;Un vaisseau spatial au lever du soleil"></textarea>
521
+ <div class="grid grid-cols-2 gap-4">
522
+ <div>
523
+ <label class="block mb-2">Type</label>
524
+ <select id="batchType" class="w-full p-2 bg-slate-800 border border-slate-700 rounded">
525
+ <option value="image">Images</option>
526
+ <option value="video">Vidéos</option>
527
+ </select>
528
+ </div>
529
+ <div>
530
+ <label class="block mb-2">Nombre</label>
531
+ <input type="number" id="batchCount" min="1" max="50" value="5" class="w-full p-2 bg-slate-800 border border-slate-700 rounded">
532
+ </div>
533
+ </div>
534
+ <div class="flex justify-end space-x-3 pt-4">
535
+ <button onclick="closeModal('batchModal')" class="px-4 py-2 bg-slate-700 hover:bg-slate-600 rounded-lg">Annuler</button>
536
+ <button onclick="startBatchGeneration()" class="px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 rounded-lg font-bold">
537
+ <i class="fas fa-play mr-2"></i>Démarrer la Batch
538
+ </button>
539
+ </div>
540
+ </div>
541
+ </div>
542
+ </div>
543
+
544
+ <!-- Scripts -->
545
+ <script src="app.js"></script>
546
+ </body>
547
+ </html>
rosalinda-server/Dockerfile CHANGED
@@ -1,9 +1,25 @@
1
  ```dockerfile
2
  FROM node:20-alpine
 
 
 
 
 
3
  WORKDIR /app
4
- COPY package.json ./
5
- RUN npm install
6
- COPY server.js ./
7
- EXPOSE 3001
8
- CMD ["npm","start"]
 
 
 
 
 
 
 
 
 
 
 
9
  ```
 
1
  ```dockerfile
2
  FROM node:20-alpine
3
+
4
+ # Installer Python et outils nécessaires
5
+ RUN apk add --no-cache python3 py3-pip make g++ \
6
+ && ln -sf python3 /usr/bin/python
7
+
8
  WORKDIR /app
9
+
10
+ # Copier package.json et installer les dépendances
11
+ COPY package*.json ./
12
+ RUN npm install --production
13
+
14
+ # Copier le reste de l'application
15
+ COPY . .
16
+
17
+ # Créer les répertoires nécessaires
18
+ RUN mkdir -p /app/outputs /app/logs /app/workflows
19
+
20
+ # Exposer les ports
21
+ EXPOSE 3001 3002
22
+
23
+ # Démarrer l'application
24
+ CMD ["npm", "start"]
25
  ```
rosalinda-server/health-monitor.js ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import fetch from 'node-fetch';
2
+ import { createClient } from 'redis';
3
+
4
+ const redisClient = createClient({
5
+ url: process.env.REDIS_URL || 'redis://redis:6379'
6
+ });
7
+
8
+ await redisClient.connect();
9
+
10
+ async function checkComfyUI() {
11
+ try {
12
+ const response = await fetch('http://comfyui:8188/system_stats', { timeout: 10000 });
13
+ return response.ok;
14
+ } catch (error) {
15
+ console.error('ComfyUI non disponible:', error.message);
16
+ return false;
17
+ }
18
+ }
19
+
20
+ async function checkRedis() {
21
+ try {
22
+ await redisClient.ping();
23
+ return true;
24
+ } catch (error) {
25
+ console.error('Redis non disponible:', error.message);
26
+ return false;
27
+ }
28
+ }
29
+
30
+ async function restartService(service) {
31
+ console.log(`Tentative de redémarrage de ${service}...`);
32
+
33
+ if (service === 'comfyui') {
34
+ // En production, vous utiliseriez l'API Docker ou Kubernetes
35
+ console.log('Redémarrage de ComfyUI programmé...');
36
+ }
37
+ }
38
+
39
+ async function monitor() {
40
+ console.log('🚀 Démarrage du moniteur de santé Rosalinda...');
41
+
42
+ while (true) {
43
+ const comfyHealthy = await checkComfyUI();
44
+ const redisHealthy = await checkRedis();
45
+
46
+ const timestamp = new Date().toISOString();
47
+
48
+ if (!comfyHealthy) {
49
+ console.error(`[${timestamp}] ⚠️ ComfyUI est hors ligne!`);
50
+ await restartService('comfyui');
51
+ }
52
+
53
+ if (!redisHealthy) {
54
+ console.error(`[${timestamp}] ⚠️ Redis est hors ligne!`);
55
+ }
56
+
57
+ if (comfyHealthy && redisHealthy) {
58
+ console.log(`[${timestamp}] ✅ Tous les services sont opérationnels`);
59
+ }
60
+
61
+ // Sauvegarder l'état
62
+ await redisClient.hSet('system:health', {
63
+ comfyui: comfyHealthy ? 'healthy' : 'unhealthy',
64
+ redis: redisHealthy ? 'healthy' : 'unhealthy',
65
+ lastCheck: timestamp
66
+ });
67
+
68
+ // Attendre 30 secondes avant la prochaine vérification
69
+ await new Promise(resolve => setTimeout(resolve, 30000));
70
+ }
71
+ }
72
+
73
+ monitor().catch(console.error);
rosalinda-server/package.json CHANGED
@@ -1,16 +1,30 @@
1
  ```json
2
  {
3
  "name": "rosalinda-server",
4
- "version": "1.0.0",
5
  "type": "module",
6
  "main": "server.js",
7
  "scripts": {
8
- "start": "node server.js"
 
 
9
  },
10
  "dependencies": {
11
  "express": "^4.19.2",
12
  "cors": "^2.8.5",
13
- "node-fetch": "^3.3.2"
 
 
 
 
 
 
 
 
 
 
 
 
14
  }
15
  }
16
  ```
 
1
  ```json
2
  {
3
  "name": "rosalinda-server",
4
+ "version": "2.0.0",
5
  "type": "module",
6
  "main": "server.js",
7
  "scripts": {
8
+ "start": "node server.js",
9
+ "dev": "nodemon server.js",
10
+ "monitor": "node health-monitor.js"
11
  },
12
  "dependencies": {
13
  "express": "^4.19.2",
14
  "cors": "^2.8.5",
15
+ "node-fetch": "^3.3.2",
16
+ "redis": "^4.6.13",
17
+ "ws": "^8.16.0",
18
+ "multer": "^1.4.5-lts.1",
19
+ "sharp": "^0.33.2",
20
+ "fs-extra": "^11.2.0",
21
+ "winston": "^3.12.0",
22
+ "helmet": "^7.1.0",
23
+ "compression": "^1.7.4",
24
+ "rate-limiter-flexible": "^4.2.3"
25
+ },
26
+ "devDependencies": {
27
+ "nodemon": "^3.1.0"
28
  }
29
  }
30
  ```
rosalinda-server/server.js CHANGED
@@ -1,157 +1,771 @@
1
- ```js
2
  import express from "express";
3
  import cors from "cors";
4
  import fetch from "node-fetch";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  const app = express();
 
 
7
  app.use(cors());
8
- app.use(express.json({ limit: "2mb" }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
- const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  /**
13
- * Helpers ComfyUI
14
  */
15
  async function comfyPrompt(workflow) {
16
- const r = await fetch(`${COMFY_URL}/prompt`, {
 
 
 
17
  method: "POST",
18
- headers: { "Content-Type": "application/json" },
 
 
 
19
  body: JSON.stringify({ prompt: workflow })
20
  });
21
- if (!r.ok) throw new Error(`ComfyUI /prompt error: ${r.status}`);
22
- return r.json(); // { prompt_id: "..." }
 
 
 
 
 
 
 
 
23
  }
24
 
25
  async function comfyHistory(promptId) {
26
- const r = await fetch(`${COMFY_URL}/history/${promptId}`);
27
- if (!r.ok) throw new Error(`ComfyUI /history error: ${r.status}`);
28
- return r.json();
 
 
 
 
 
29
  }
30
 
31
- function sleep(ms) { return new Promise(res => setTimeout(res, ms)); }
 
 
32
 
33
  /**
34
- * Attendre que ComfyUI finisse et récupérer une image (filename)
35
  */
36
- async function waitForResultImage(promptId, timeoutMs = 180000) {
37
- const t0 = Date.now();
38
- while (Date.now() - t0 < timeoutMs) {
39
- const h = await comfyHistory(promptId);
40
- const item = h?.[promptId];
41
- if (item?.outputs) {
42
- // Cherche un output image
43
- for (const nodeId of Object.keys(item.outputs)) {
44
- const out = item.outputs[nodeId];
45
- if (out?.images?.length) {
46
- return out.images[0]; // {filename, subfolder, type}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  }
48
  }
49
- }
50
- await sleep(1200);
51
- }
52
- throw new Error("Timeout: génération image trop longue");
53
- }
54
-
55
- async function waitForResultVideo(promptId, timeoutMs = 360000) {
56
- const t0 = Date.now();
57
- while (Date.now() - t0 < timeoutMs) {
58
- const h = await comfyHistory(promptId);
59
- const item = h?.[promptId];
60
- if (item?.outputs) {
61
- // Cherche un output vidéo (souvent "gifs" ou "videos" selon workflow)
62
- for (const nodeId of Object.keys(item.outputs)) {
63
- const out = item.outputs[nodeId];
64
- if (out?.gifs?.length) return out.gifs[0];
65
- if (out?.videos?.length) return out.videos[0];
 
 
 
 
66
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  }
68
- await sleep(1500);
69
  }
70
- throw new Error("Timeout: génération vidéo trop longue");
 
71
  }
72
 
73
  function comfyFileUrl(file) {
74
- // ComfyUI : /view?filename=...&subfolder=...&type=output
75
  const params = new URLSearchParams({
76
  filename: file.filename,
77
  subfolder: file.subfolder || "",
78
  type: file.type || "output",
 
79
  });
80
  return `${COMFY_URL}/view?${params.toString()}`;
81
  }
82
 
83
  /**
84
- * Workflows ultra simples (à remplacer par tes workflows ComfyUI)
85
- * IMPORTANT: pour que ça marche, il faut un workflow valide dans ComfyUI.
86
- * Ici on te met un "template" minimal : tu importes un workflow ComfyUI et tu remplaces ce JSON.
87
  */
88
- function imageWorkflow(prompt, steps = 24) {
89
- // 👉 Remplace ce JSON par TON workflow ComfyUI (export JSON)
90
- // Astuce: dans ComfyUI -> Save (workflow) -> colle ici.
91
- return {
92
- "1": { "class_type": "CLIPTextEncode", "inputs": { "text": prompt, "clip": ["4", 1] } },
93
- "2": { "class_type": "CLIPTextEncode", "inputs": { "text": "low quality, blurry", "clip": ["4", 1] } },
94
- "3": { "class_type": "KSampler", "inputs": { "seed": 123, "steps": steps, "cfg": 7, "sampler_name": "euler", "scheduler": "normal", "denoise": 1, "model": ["4", 0], "positive": ["1", 0], "negative": ["2", 0], "latent_image": ["5", 0] } },
95
- "4": { "class_type": "CheckpointLoaderSimple", "inputs": { "ckpt_name": "sdxl_base_1.0.safetensors" } },
96
- "5": { "class_type": "EmptyLatentImage", "inputs": { "width": 1024, "height": 1024, "batch_size": 1 } },
97
- "6": { "class_type": "VAEDecode", "inputs": { "samples": ["3", 0], "vae": ["4", 2] } },
98
- "7": { "class_type": "SaveImage", "inputs": { "images": ["6", 0], "filename_prefix": "rosalinda_image" } }
99
- };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  }
101
 
102
- function videoWorkflow(prompt) {
103
- // 👉 Remplace par un workflow vidéo (SVD / AnimateDiff) exporté de ComfyUI
104
- return {
105
- // placeholder volontaire : tu colles ton workflow vidéo ici
106
- };
 
 
 
 
 
 
 
 
 
107
  }
108
 
109
  /**
110
- * API Rosalinda
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  */
112
  app.post("/api/image", async (req, res) => {
 
 
113
  try {
114
- const { prompt, steps } = req.body || {};
115
- if (!prompt?.trim()) return res.status(400).json({ error: "prompt requis" });
116
-
117
- const wf = imageWorkflow(prompt.trim(), Number(steps || 24));
118
- const { prompt_id } = await comfyPrompt(wf);
119
- const file = await waitForResultImage(prompt_id);
120
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  res.json({
122
  ok: true,
123
- name: "Rosalinda",
124
  type: "image",
125
- prompt_id,
126
- url: comfyFileUrl(file)
 
 
 
 
 
 
 
 
 
 
127
  });
128
- } catch (e) {
129
- res.status(500).json({ ok: false, error: e.message });
130
  }
131
  });
132
 
133
  app.post("/api/video", async (req, res) => {
 
 
134
  try {
135
- const { prompt } = req.body || {};
136
- if (!prompt?.trim()) return res.status(400).json({ error: "prompt requis" });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
137
 
138
- const wf = videoWorkflow(prompt.trim());
139
- const { prompt_id } = await comfyPrompt(wf);
140
- const file = await waitForResultVideo(prompt_id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  res.json({
143
  ok: true,
144
- name: "Rosalinda",
145
- type: "video",
146
- prompt_id,
147
- url: comfyFileUrl(file)
 
 
 
 
 
 
 
 
 
 
148
  });
149
- } catch (e) {
150
- res.status(500).json({ ok: false, error: e.message });
 
 
151
  }
152
  });
153
 
154
- app.get("/health", (_req, res) => res.json({ ok: true, name: "Rosalinda" }));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
- app.listen(3001, () => console.log("✅ Rosalinda API on :3001"));
157
- ```
 
 
 
 
 
 
 
 
1
  import express from "express";
2
  import cors from "cors";
3
  import fetch from "node-fetch";
4
+ import { createClient } from 'redis';
5
+ import { WebSocketServer } from 'ws';
6
+ import fs from 'fs-extra';
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import winston from 'winston';
10
+ import helmet from 'helmet';
11
+ import compression from 'compression';
12
+ import multer from 'multer';
13
+
14
+ const __filename = fileURLToPath(import.meta.url);
15
+ const __dirname = path.dirname(__filename);
16
+
17
+ // Configuration du logging
18
+ const logger = winston.createLogger({
19
+ level: 'info',
20
+ format: winston.format.combine(
21
+ winston.format.timestamp(),
22
+ winston.format.json()
23
+ ),
24
+ transports: [
25
+ new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
26
+ new winston.transports.File({ filename: 'logs/combined.log' }),
27
+ new winston.transports.Console({
28
+ format: winston.format.simple()
29
+ })
30
+ ]
31
+ });
32
+
33
+ // Configuration Redis
34
+ const redisClient = createClient({
35
+ url: process.env.REDIS_URL || 'redis://redis:6379'
36
+ });
37
+
38
+ redisClient.on('error', (err) => logger.error('Redis Client Error', err));
39
+ await redisClient.connect();
40
 
41
  const app = express();
42
+ app.use(helmet());
43
+ app.use(compression());
44
  app.use(cors());
45
+ app.use(express.json({ limit: "50mb" }));
46
+ app.use(express.urlencoded({ extended: true, limit: "50mb" }));
47
+
48
+ // Configuration du stockage des fichiers
49
+ const storage = multer.diskStorage({
50
+ destination: (req, file, cb) => {
51
+ cb(null, 'uploads/');
52
+ },
53
+ filename: (req, file, cb) => {
54
+ cb(null, `${Date.now()}-${file.originalname}`);
55
+ }
56
+ });
57
+ const upload = multer({ storage });
58
+
59
+ const COMFY_URL = process.env.COMFY_URL || "http://comfyui:8188";
60
+ const PORT = process.env.PORT || 3001;
61
+
62
+ // Serveur WebSocket pour les mises à jour en temps réel
63
+ const wss = new WebSocketServer({ port: 3002 });
64
+ const clients = new Set();
65
+
66
+ wss.on('connection', (ws) => {
67
+ clients.add(ws);
68
+ logger.info('Nouvelle connexion WebSocket établie');
69
+
70
+ ws.on('close', () => {
71
+ clients.delete(ws);
72
+ logger.info('Connexion WebSocket fermée');
73
+ });
74
+
75
+ ws.on('error', (error) => {
76
+ logger.error('Erreur WebSocket:', error);
77
+ });
78
+ });
79
 
80
+ // Fonction pour envoyer des mises à jour à tous les clients
81
+ function broadcastUpdate(type, data) {
82
+ const message = JSON.stringify({ type, data, timestamp: new Date().toISOString() });
83
+ clients.forEach(client => {
84
+ if (client.readyState === 1) { // WebSocket.OPEN
85
+ client.send(message);
86
+ }
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Charger les workflows depuis les fichiers
92
+ */
93
+ async function loadWorkflow(type) {
94
+ try {
95
+ const workflowPath = path.join(__dirname, 'workflows', `${type}-workflow.json`);
96
+ const workflowData = await fs.readFile(workflowPath, 'utf8');
97
+ return JSON.parse(workflowData);
98
+ } catch (error) {
99
+ logger.error(`Erreur lors du chargement du workflow ${type}:`, error);
100
+
101
+ // Workflows par défaut si les fichiers n'existent pas
102
+ const defaultWorkflows = {
103
+ image: {
104
+ "3": {
105
+ "inputs": {
106
+ "seed": 156680208700286,
107
+ "steps": 20,
108
+ "cfg": 8,
109
+ "sampler_name": "euler",
110
+ "scheduler": "normal",
111
+ "denoise": 1,
112
+ "model": ["4", 0],
113
+ "positive": ["6", 0],
114
+ "negative": ["7", 0],
115
+ "latent_image": ["5", 0]
116
+ },
117
+ "class_type": "KSampler"
118
+ },
119
+ "4": {
120
+ "inputs": { "ckpt_name": "v1-5-pruned-emaonly.safetensors" },
121
+ "class_type": "CheckpointLoaderSimple"
122
+ },
123
+ "5": {
124
+ "inputs": { "width": 512, "height": 512, "batch_size": 1 },
125
+ "class_type": "EmptyLatentImage"
126
+ },
127
+ "6": {
128
+ "inputs": { "text": "masterpiece, best quality, 4k", "clip": ["4", 1] },
129
+ "class_type": "CLIPTextEncode"
130
+ },
131
+ "7": {
132
+ "inputs": { "text": "lowres, bad anatomy, bad hands, text, error", "clip": ["4", 1] },
133
+ "class_type": "CLIPTextEncode"
134
+ },
135
+ "8": {
136
+ "inputs": { "samples": ["3", 0], "vae": ["4", 2] },
137
+ "class_type": "VAEDecode"
138
+ },
139
+ "9": {
140
+ "inputs": { "filename_prefix": "ComfyUI", "images": ["8", 0] },
141
+ "class_type": "SaveImage"
142
+ }
143
+ },
144
+ video: {
145
+ // Workflow vidéo de base
146
+ "1": {
147
+ "inputs": { "ckpt_name": "sd-v1-5-inpainting.ckpt" },
148
+ "class_type": "CheckpointLoader"
149
+ },
150
+ "2": {
151
+ "inputs": { "text": "A beautiful landscape", "clip": ["1", 1] },
152
+ "class_type": "CLIPTextEncode"
153
+ },
154
+ "3": {
155
+ "inputs": { "text": "blurry, low quality", "clip": ["1", 1] },
156
+ "class_type": "CLIPTextEncode"
157
+ },
158
+ "4": {
159
+ "inputs": {
160
+ "seed": 12345,
161
+ "steps": 30,
162
+ "cfg": 7.5,
163
+ "sampler_name": "euler",
164
+ "scheduler": "normal",
165
+ "denoise": 1,
166
+ "model": ["1", 0],
167
+ "positive": ["2", 0],
168
+ "negative": ["3", 0],
169
+ "latent_image": ["5", 0]
170
+ },
171
+ "class_type": "KSampler"
172
+ },
173
+ "5": {
174
+ "inputs": { "width": 512, "height": 512, "batch_size": 4 },
175
+ "class_type": "EmptyLatentImage"
176
+ },
177
+ "6": {
178
+ "inputs": { "samples": ["4", 0], "vae": ["1", 2] },
179
+ "class_type": "VAEDecode"
180
+ },
181
+ "7": {
182
+ "inputs": {
183
+ "frames": ["6", 0],
184
+ "fps": 10,
185
+ "format": "video/h264-mp4",
186
+ "quality": 90,
187
+ "pingpong": false,
188
+ "save_metadata": false
189
+ },
190
+ "class_type": "VHS_VideoCombine"
191
+ }
192
+ }
193
+ };
194
+
195
+ return defaultWorkflows[type];
196
+ }
197
+ }
198
 
199
  /**
200
+ * Helpers ComfyUI améliorés
201
  */
202
  async function comfyPrompt(workflow) {
203
+ const url = `${COMFY_URL}/prompt`;
204
+ logger.info(`Envoi du prompt à ComfyUI: ${url}`);
205
+
206
+ const response = await fetch(url, {
207
  method: "POST",
208
+ headers: {
209
+ "Content-Type": "application/json",
210
+ "Accept": "application/json"
211
+ },
212
  body: JSON.stringify({ prompt: workflow })
213
  });
214
+
215
+ if (!response.ok) {
216
+ const errorText = await response.text();
217
+ logger.error(`Erreur ComfyUI /prompt: ${response.status} - ${errorText}`);
218
+ throw new Error(`ComfyUI /prompt error: ${response.status} - ${errorText}`);
219
+ }
220
+
221
+ const data = await response.json();
222
+ logger.info(`Prompt envoyé avec succès, ID: ${data.prompt_id}`);
223
+ return data;
224
  }
225
 
226
  async function comfyHistory(promptId) {
227
+ const response = await fetch(`${COMFY_URL}/history/${promptId}`);
228
+
229
+ if (!response.ok) {
230
+ logger.error(`Erreur ComfyUI /history: ${response.status}`);
231
+ throw new Error(`ComfyUI /history error: ${response.status}`);
232
+ }
233
+
234
+ return response.json();
235
  }
236
 
237
+ function sleep(ms) {
238
+ return new Promise(resolve => setTimeout(resolve, ms));
239
+ }
240
 
241
  /**
242
+ * Attendre que ComfyUI finisse et récupérer les résultats
243
  */
244
+ async function waitForResult(promptId, type = 'image', timeoutMs = 300000) {
245
+ const startTime = Date.now();
246
+ let lastProgress = 0;
247
+
248
+ logger.info(`Attente du résultat ${type} pour prompt_id: ${promptId}`);
249
+
250
+ while (Date.now() - startTime < timeoutMs) {
251
+ try {
252
+ const history = await comfyHistory(promptId);
253
+ const item = history?.[promptId];
254
+
255
+ if (item?.status?.status === "error") {
256
+ throw new Error(`Erreur ComfyUI: ${item.status.message}`);
257
+ }
258
+
259
+ // Vérifier la progression
260
+ if (item?.status?.status === "executing") {
261
+ const currentProgress = item.status.progress || 0;
262
+ if (currentProgress !== lastProgress) {
263
+ lastProgress = currentProgress;
264
+ broadcastUpdate('progress', {
265
+ promptId,
266
+ progress: currentProgress,
267
+ type
268
+ });
269
+ logger.info(`Progression ${type} ${promptId}: ${currentProgress}`);
270
  }
271
  }
272
+
273
+ if (item?.outputs) {
274
+ // Chercher les outputs selon le type
275
+ for (const nodeId of Object.keys(item.outputs)) {
276
+ const output = item.outputs[nodeId];
277
+
278
+ if (type === 'image' && output?.images?.length) {
279
+ logger.info(`Image trouvée pour ${promptId}: ${output.images[0].filename}`);
280
+ return output.images[0];
281
+ }
282
+
283
+ if (type === 'video' && output?.videos?.length) {
284
+ logger.info(`Vidéo trouvée pour ${promptId}: ${output.videos[0].filename}`);
285
+ return output.videos[0];
286
+ }
287
+
288
+ if (type === 'video' && output?.gifs?.length) {
289
+ logger.info(`GIF trouvé pour ${promptId}: ${output.gifs[0].filename}`);
290
+ return output.gifs[0];
291
+ }
292
+ }
293
  }
294
+
295
+ // Si le statut est "success" mais aucun output n'est trouvé
296
+ if (item?.status?.status === "success") {
297
+ throw new Error(`Génération ${type} terminée mais aucun output trouvé`);
298
+ }
299
+
300
+ await sleep(2000);
301
+
302
+ } catch (error) {
303
+ logger.error(`Erreur lors de l'attente du résultat: ${error.message}`);
304
+
305
+ // Si c'est une erreur de réseau, on réessaie
306
+ if (error.message.includes('fetch') || error.message.includes('network')) {
307
+ await sleep(5000);
308
+ continue;
309
+ }
310
+ throw error;
311
  }
 
312
  }
313
+
314
+ throw new Error(`Timeout: génération ${type} trop longue (${timeoutMs/1000}s)`);
315
  }
316
 
317
  function comfyFileUrl(file) {
 
318
  const params = new URLSearchParams({
319
  filename: file.filename,
320
  subfolder: file.subfolder || "",
321
  type: file.type || "output",
322
+ preview: "true"
323
  });
324
  return `${COMFY_URL}/view?${params.toString()}`;
325
  }
326
 
327
  /**
328
+ * Workflows dynamiques
 
 
329
  */
330
+ async function imageWorkflow(prompt, options = {}) {
331
+ const baseWorkflow = await loadWorkflow('image');
332
+
333
+ // Personnaliser le workflow avec le prompt
334
+ for (const nodeId in baseWorkflow) {
335
+ const node = baseWorkflow[nodeId];
336
+
337
+ if (node.class_type === "CLIPTextEncode" && node.inputs.text &&
338
+ node.inputs.text.includes("masterpiece")) {
339
+ node.inputs.text = `${prompt}, masterpiece, best quality, 4k, ultra detailed`;
340
+ }
341
+
342
+ if (node.class_type === "CLIPTextEncode" && node.inputs.text &&
343
+ node.inputs.text.includes("lowres")) {
344
+ node.inputs.text = "lowres, bad anatomy, bad hands, text, error, missing fingers";
345
+ }
346
+
347
+ // Appliquer les options
348
+ if (node.class_type === "KSampler") {
349
+ node.inputs.seed = options.seed || Math.floor(Math.random() * 4294967295);
350
+ node.inputs.steps = options.steps || 20;
351
+ node.inputs.cfg = options.cfg || 7.5;
352
+ node.inputs.width = options.width || 512;
353
+ node.inputs.height = options.height || 512;
354
+ }
355
+
356
+ if (node.class_type === "EmptyLatentImage") {
357
+ node.inputs.width = options.width || 512;
358
+ node.inputs.height = options.height || 512;
359
+ }
360
+ }
361
+
362
+ return baseWorkflow;
363
  }
364
 
365
+ async function videoWorkflow(prompt, options = {}) {
366
+ const baseWorkflow = await loadWorkflow('video');
367
+
368
+ // Personnaliser le workflow vidéo
369
+ for (const nodeId in baseWorkflow) {
370
+ const node = baseWorkflow[nodeId];
371
+
372
+ if (node.class_type === "CLIPTextEncode" && node.inputs.text &&
373
+ !node.inputs.text.includes("low quality")) {
374
+ node.inputs.text = `${prompt}, cinematic, high quality, smooth motion`;
375
+ }
376
+ }
377
+
378
+ return baseWorkflow;
379
  }
380
 
381
  /**
382
+ * Middleware de suivi des requêtes
383
+ */
384
+ app.use((req, res, next) => {
385
+ const requestId = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
386
+ req.requestId = requestId;
387
+
388
+ logger.info({
389
+ requestId,
390
+ method: req.method,
391
+ url: req.url,
392
+ ip: req.ip,
393
+ userAgent: req.get('User-Agent')
394
+ });
395
+
396
+ const start = Date.now();
397
+ res.on('finish', () => {
398
+ const duration = Date.now() - start;
399
+ logger.info({
400
+ requestId,
401
+ statusCode: res.statusCode,
402
+ duration: `${duration}ms`
403
+ });
404
+ });
405
+
406
+ next();
407
+ });
408
+
409
+ /**
410
+ * API Rosalinda améliorée
411
  */
412
  app.post("/api/image", async (req, res) => {
413
+ const requestId = req.requestId;
414
+
415
  try {
416
+ const { prompt, steps = 20, width = 512, height = 512, seed } = req.body;
417
+
418
+ if (!prompt?.trim()) {
419
+ return res.status(400).json({
420
+ ok: false,
421
+ error: "Le prompt est requis",
422
+ requestId
423
+ });
424
+ }
425
+
426
+ logger.info(`Génération d'image demandée: ${prompt.substring(0, 100)}...`, { requestId });
427
+
428
+ // Enregistrer dans Redis
429
+ const taskId = `image_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
430
+ await redisClient.hSet(taskId, {
431
+ type: 'image',
432
+ prompt: prompt.trim(),
433
+ status: 'processing',
434
+ createdAt: new Date().toISOString(),
435
+ requestId
436
+ });
437
+
438
+ // Lancer la génération en arrière-plan
439
+ setTimeout(async () => {
440
+ try {
441
+ const workflow = await imageWorkflow(prompt.trim(), { steps, width, height, seed });
442
+ const { prompt_id } = await comfyPrompt(workflow);
443
+
444
+ await redisClient.hSet(taskId, {
445
+ comfyPromptId: prompt_id,
446
+ status: 'generating'
447
+ });
448
+
449
+ const file = await waitForResult(prompt_id, 'image');
450
+ const fileUrl = comfyFileUrl(file);
451
+
452
+ await redisClient.hSet(taskId, {
453
+ status: 'completed',
454
+ fileUrl,
455
+ completedAt: new Date().toISOString(),
456
+ filename: file.filename
457
+ });
458
+
459
+ broadcastUpdate('image_completed', {
460
+ taskId,
461
+ prompt: prompt.trim(),
462
+ url: fileUrl,
463
+ filename: file.filename
464
+ });
465
+
466
+ logger.info(`Image générée avec succès: ${file.filename}`, { requestId });
467
+
468
+ } catch (error) {
469
+ logger.error(`Erreur lors de la génération d'image: ${error.message}`, { requestId });
470
+
471
+ await redisClient.hSet(taskId, {
472
+ status: 'failed',
473
+ error: error.message,
474
+ failedAt: new Date().toISOString()
475
+ });
476
+
477
+ broadcastUpdate('image_failed', {
478
+ taskId,
479
+ error: error.message
480
+ });
481
+ }
482
+ }, 0);
483
+
484
  res.json({
485
  ok: true,
486
+ name: "Rosalinda",
487
  type: "image",
488
+ taskId,
489
+ message: "La génération d'image a démarré",
490
+ requestId,
491
+ monitorUrl: `/api/tasks/${taskId}`
492
+ });
493
+
494
+ } catch (error) {
495
+ logger.error(`Erreur API image: ${error.message}`, { requestId });
496
+ res.status(500).json({
497
+ ok: false,
498
+ error: error.message,
499
+ requestId
500
  });
 
 
501
  }
502
  });
503
 
504
  app.post("/api/video", async (req, res) => {
505
+ const requestId = req.requestId;
506
+
507
  try {
508
+ const { prompt, duration = 4 } = req.body;
509
+
510
+ if (!prompt?.trim()) {
511
+ return res.status(400).json({
512
+ ok: false,
513
+ error: "Le prompt est requis",
514
+ requestId
515
+ });
516
+ }
517
+
518
+ logger.info(`Génération de vidéo demandée: ${prompt.substring(0, 100)}...`, { requestId });
519
+
520
+ const taskId = `video_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
521
+ await redisClient.hSet(taskId, {
522
+ type: 'video',
523
+ prompt: prompt.trim(),
524
+ status: 'processing',
525
+ createdAt: new Date().toISOString(),
526
+ requestId
527
+ });
528
+
529
+ // Lancer la génération en arrière-plan
530
+ setTimeout(async () => {
531
+ try {
532
+ const workflow = await videoWorkflow(prompt.trim(), { duration });
533
+ const { prompt_id } = await comfyPrompt(workflow);
534
+
535
+ await redisClient.hSet(taskId, {
536
+ comfyPromptId: prompt_id,
537
+ status: 'generating'
538
+ });
539
+
540
+ const file = await waitForResult(prompt_id, 'video');
541
+ const fileUrl = comfyFileUrl(file);
542
+
543
+ await redisClient.hSet(taskId, {
544
+ status: 'completed',
545
+ fileUrl,
546
+ completedAt: new Date().toISOString(),
547
+ filename: file.filename
548
+ });
549
+
550
+ broadcastUpdate('video_completed', {
551
+ taskId,
552
+ prompt: prompt.trim(),
553
+ url: fileUrl,
554
+ filename: file.filename
555
+ });
556
+
557
+ logger.info(`Vidéo générée avec succès: ${file.filename}`, { requestId });
558
+
559
+ } catch (error) {
560
+ logger.error(`Erreur lors de la génération de vidéo: ${error.message}`, { requestId });
561
+
562
+ await redisClient.hSet(taskId, {
563
+ status: 'failed',
564
+ error: error.message,
565
+ failedAt: new Date().toISOString()
566
+ });
567
+
568
+ broadcastUpdate('video_failed', {
569
+ taskId,
570
+ error: error.message
571
+ });
572
+ }
573
+ }, 0);
574
+
575
+ res.json({
576
+ ok: true,
577
+ name: "Rosalinda ∞",
578
+ type: "video",
579
+ taskId,
580
+ message: "La génération de vidéo a démarré",
581
+ requestId,
582
+ monitorUrl: `/api/tasks/${taskId}`
583
+ });
584
+
585
+ } catch (error) {
586
+ logger.error(`Erreur API vidéo: ${error.message}`, { requestId });
587
+ res.status(500).json({
588
+ ok: false,
589
+ error: error.message,
590
+ requestId
591
+ });
592
+ }
593
+ });
594
 
595
+ app.post("/api/batch", upload.array('images', 10), async (req, res) => {
596
+ try {
597
+ const { operations = [] } = req.body;
598
+ const files = req.files || [];
599
+
600
+ const batchId = `batch_${Date.now()}`;
601
+ const tasks = [];
602
+
603
+ // Traitement par lots
604
+ for (const operation of operations) {
605
+ if (operation.type === 'generate') {
606
+ const task = await generateTask(operation);
607
+ tasks.push(task);
608
+ } else if (operation.type === 'process' && files.length > 0) {
609
+ const task = await processTask(operation, files);
610
+ tasks.push(task);
611
+ }
612
+ }
613
+
614
+ res.json({
615
+ ok: true,
616
+ batchId,
617
+ tasks: tasks.length,
618
+ message: "Traitement par lots démarré"
619
+ });
620
+
621
+ } catch (error) {
622
+ logger.error(`Erreur batch: ${error.message}`);
623
+ res.status(500).json({ ok: false, error: error.message });
624
+ }
625
+ });
626
 
627
+ app.get("/api/tasks/:taskId", async (req, res) => {
628
+ try {
629
+ const { taskId } = req.params;
630
+ const task = await redisClient.hGetAll(taskId);
631
+
632
+ if (!task || Object.keys(task).length === 0) {
633
+ return res.status(404).json({ ok: false, error: "Tâche non trouvée" });
634
+ }
635
+
636
+ res.json({ ok: true, task });
637
+
638
+ } catch (error) {
639
+ logger.error(`Erreur récupération tâche: ${error.message}`);
640
+ res.status(500).json({ ok: false, error: error.message });
641
+ }
642
+ });
643
+
644
+ app.get("/api/tasks", async (req, res) => {
645
+ try {
646
+ const { type, status, limit = 50 } = req.query;
647
+ const keys = await redisClient.keys('*_*');
648
+
649
+ const tasks = [];
650
+ for (const key of keys.slice(0, limit)) {
651
+ if (!key.includes('_')) continue;
652
+
653
+ const task = await redisClient.hGetAll(key);
654
+ if (Object.keys(task).length > 0) {
655
+ tasks.push({ id: key, ...task });
656
+ }
657
+ }
658
+
659
+ // Filtrer
660
+ let filtered = tasks;
661
+ if (type) {
662
+ filtered = filtered.filter(t => t.type === type);
663
+ }
664
+ if (status) {
665
+ filtered = filtered.filter(t => t.status === status);
666
+ }
667
+
668
+ // Trier par date
669
+ filtered.sort((a, b) => new Date(b.createdAt || 0) - new Date(a.createdAt || 0));
670
+
671
+ res.json({ ok: true, tasks: filtered, total: filtered.length });
672
+
673
+ } catch (error) {
674
+ logger.error(`Erreur liste tâches: ${error.message}`);
675
+ res.status(500).json({ ok: false, error: error.message });
676
+ }
677
+ });
678
+
679
+ app.get("/api/status", async (req, res) => {
680
+ try {
681
+ // Vérifier ComfyUI
682
+ const comfyStatus = await fetch(`${COMFY_URL}/system_stats`).then(r => r.ok).catch(() => false);
683
+
684
+ // Statistiques Redis
685
+ const redisInfo = await redisClient.info();
686
+
687
+ // Tâches en cours
688
+ const processingTasks = await redisClient.keys('*_*').then(async keys => {
689
+ let count = 0;
690
+ for (const key of keys) {
691
+ const status = await redisClient.hGet(key, 'status');
692
+ if (status === 'processing' || status === 'generating') count++;
693
+ }
694
+ return count;
695
+ });
696
+
697
  res.json({
698
  ok: true,
699
+ service: "Rosalinda",
700
+ status: "operational",
701
+ timestamp: new Date().toISOString(),
702
+ components: {
703
+ comfyui: comfyStatus ? "healthy" : "unhealthy",
704
+ redis: "healthy",
705
+ server: "healthy"
706
+ },
707
+ statistics: {
708
+ processingTasks,
709
+ totalMemory: process.memoryUsage().heapTotal,
710
+ usedMemory: process.memoryUsage().heapUsed,
711
+ uptime: process.uptime()
712
+ }
713
  });
714
+
715
+ } catch (error) {
716
+ logger.error(`Erreur status: ${error.message}`);
717
+ res.status(500).json({ ok: false, error: error.message });
718
  }
719
  });
720
 
721
+ app.get("/health", (req, res) => {
722
+ res.json({
723
+ ok: true,
724
+ name: "Rosalinda ∞",
725
+ status: "ALIVE",
726
+ message: "Je travaille 24h/24, même quand vous dormez!",
727
+ timestamp: new Date().toISOString()
728
+ });
729
+ });
730
+
731
+ app.get("/", (req, res) => {
732
+ res.json({
733
+ name: "Rosalinda ∞ - Infinite AI",
734
+ version: "2.0.0",
735
+ endpoints: {
736
+ generateImage: "POST /api/image",
737
+ generateVideo: "POST /api/video",
738
+ batchProcess: "POST /api/batch",
739
+ taskStatus: "GET /api/tasks/:taskId",
740
+ allTasks: "GET /api/tasks",
741
+ systemStatus: "GET /api/status",
742
+ health: "GET /health",
743
+ wsUpdates: "ws://localhost:3002"
744
+ },
745
+ description: "IA de génération d'images et vidéos fonctionnant 24h/24"
746
+ });
747
+ });
748
+
749
+ // Route de secours pour servir les fichiers statiques
750
+ app.use('/outputs', express.static('/app/outputs'));
751
+ app.use('/uploads', express.static('/app/uploads'));
752
+
753
+ // Gestion des erreurs
754
+ app.use((err, req, res, next) => {
755
+ logger.error(`Erreur non gérée: ${err.stack}`, { requestId: req.requestId });
756
+
757
+ res.status(err.status || 500).json({
758
+ ok: false,
759
+ error: process.env.NODE_ENV === 'development' ? err.message : 'Erreur interne du serveur',
760
+ requestId: req.requestId
761
+ });
762
+ });
763
 
764
+ // Démarrage du serveur
765
+ app.listen(PORT, () => {
766
+ logger.info(`✅ Rosalinda API démarrée sur le port ${PORT}`);
767
+ logger.info(`✅ WebSocket Server démarré sur le port 3002`);
768
+ logger.info(`✅ Connecté à ComfyUI: ${COMFY_URL}`);
769
+ logger.info(`✅ Redis connecté`);
770
+ logger.info(`🔥 L'IA travaille maintenant 24h/24 sans s'arrêter!`);
771
+ });