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

services:

Browse files

comfyui:
image: ghcr.io/comfyanonymous/comfyui:latest
ports:
- "8188:8188"
volumes:
- ./comfyui:/workspace
restart: unless-stopped

rosalinda:
build: ./rosalinda-server
ports:
- "3001:3001"
environment:
- COMFY_URL=http://comfyui:8188
depends_on:
- comfyui
restart: unless-stopped
Ensuite : docker compose up -d

2) Serveur rosalinda-server (Node.js, API propre)
Crée rosalinda/rosalinda-server/package.json :

json
Copier le code
{
"name": "rosalinda-server",
"version": "1.0.0",
"type": "module",
"main": "server.js",
"scripts": { "start": "node server.js" },
"dependencies": {
"express": "^4.19.2",
"cors": "^2.8.5",
"node-fetch": "^3.3.2"
}
}
Crée rosalinda/rosalinda-server/Dockerfile :

dockerfile
Copier le code
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY server.js ./
EXPOSE 3001
CMD ["npm","start"]
Crée rosalinda/rosalinda-server/server.js :

js
Copier le code
import express from "express";
import cors from "cors";
import fetch from "node-fetch";

const app = express();
app.use(cors());
app.use(express.json({ limit: "2mb" }));

const COMFY_URL = process.env.COMFY_URL || "http://127.0.0.1:8188";

/**
* Helpers ComfyUI
*/
async function comfyPrompt(workflow) {
const r = await fetch(`${COMFY_URL}/prompt`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: workflow })
});
if (!r.ok) throw new Error(`ComfyUI /prompt error: ${r.status}`);
return r.json(); // { prompt_id: "..." }
}

async function comfyHistory(promptId) {
const r = await fetch(`${COMFY_URL}/history/${promptId}`);
if (!r.ok) throw new Error(`ComfyUI /history error: ${r.status}`);
return r.json();
}

function sleep(ms) { return new Promise(res => setTimeout(res, ms)); }

/**
* Attendre que ComfyUI finisse et récupérer une image (filename)
*/
async function waitForResultImage(promptId, timeoutMs = 180000) {
const t0 = Date.now();
while (Date.now() - t0 < timeoutMs) {
const h = await comfyHistory(promptId);
const item = h?.[promptId];
if (item?.outputs) {
// Cherche un output image
for (const nodeId of Object.keys(item.outputs)) {
const out = item.outputs[nodeId];
if (out?.images?.length) {
return out.images[0]; // {filename, subfolder, type}
}
}
}
await sleep(1200);
}
throw new Error("Timeout: génération image trop longue");
}

async function waitForResultVideo(promptId, timeoutMs = 360000) {
const t0 = Date.now();
while (Date.now() - t0 < timeoutMs) {
const h = await comfyHistory(promptId);
const item = h?.[promptId];
if (item?.outputs) {
// Cherche un output vidéo (souvent "gifs" ou "videos" selon workflow)
for (const nodeId of Object.keys(item.outputs)) {
const out = item.outputs[nodeId];
if (out?.gifs?.length) return out.gifs[0];
if (out?.videos?.length) return out.videos[0];
}
}
await sleep(1500);
}
throw new Error("Timeout: génération vidéo trop longue");
}

function comfyFileUrl(file) {
// ComfyUI : /view?filename=...&subfolder=...&type=output
const params = new URLSearchParams({
filename: file.filename,
subfolder: file.subfolder || "",
type: file.type || "output",
});
return `${COMFY_URL}/view?${params.toString()}`;
}

/**
* Workflows ultra simples (à remplacer par tes workflows ComfyUI)
* IMPORTANT: pour que ça marche, il faut un workflow valide dans ComfyUI.
* Ici on te met un "template" minimal : tu importes un workflow ComfyUI et tu remplaces ce JSON.
*/
function imageWorkflow(prompt, steps = 24) {
// 👉 Remplace ce JSON par TON workflow ComfyUI (export JSON)
// Astuce: dans ComfyUI -> Save (workflow) -> colle ici.
return {
"1": { "class_type": "CLIPTextEncode", "inputs": { "text": prompt, "clip": ["4", 1] } },
"2": { "class_type": "CLIPTextEncode", "inputs": { "text": "low quality, blurry", "clip": ["4", 1] } },
"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] } },
"4": { "class_type": "CheckpointLoaderSimple", "inputs": { "ckpt_name": "sdxl_base_1.0.safetensors" } },
"5": { "class_type": "EmptyLatentImage", "inputs": { "width": 1024, "height": 1024, "batch_size": 1 } },
"6": { "class_type": "VAEDecode", "inputs": { "samples": ["3", 0], "vae": ["4", 2] } },
"7": { "class_type": "SaveImage", "inputs": { "images": ["6", 0], "filename_prefix": "rosalinda_image" } }
};
}

function videoWorkflow(prompt) {
// 👉 Remplace par un workflow vidéo (SVD / AnimateDiff) exporté de ComfyUI
return {
// placeholder volontaire : tu colles ton workflow vidéo ici
};
}

/**
* API Rosalinda
*/
app.post("/api/image", async (req, res) => {
try {
const { prompt, steps } = req.body || {};
if (!prompt?.trim()) return res.status(400).json({ error: "prompt requis" });

const wf = imageWorkflow(prompt.trim(), Number(steps || 24));
const { prompt_id } = await comfyPrompt(wf);
const file = await waitForResultImage(prompt_id);

res.json({
ok: true,
name: "Rosalinda",
type: "image",
prompt_id,
url: comfyFileUrl(file)
});
} catch (e) {
res.status(500).json({ ok: false, error: e.message });
}
});

app.post("/api/video", async (req, res) => {
try {
const { prompt } = req.body || {};
if (!prompt?.trim()) return res.status(400).json({ error: "prompt requis" });

const wf = videoWorkflow(prompt.trim());
const { prompt_id } = await comfyPrompt(wf);
const file = await waitForResultVideo(prompt_id);

res.json({
ok: true,
name: "Rosalinda",
type: "video",
prompt_id,
url: comfyFileUrl(file)
});
} catch (e) {
res.status(500).json({ ok: false, error: e.message });
}
});

app.get("/health", (_req, res) => res.json({ ok: true, name: "Rosalinda" }));

app.listen(3001, () => console.log("✅ Rosalinda API on :3001"));
⚠️ Important : pour que l’image marche, ComfyUI doit avoir un modèle (ex: SDXL) et le workflow doit être valide.
👉 Le plus simple : tu ouvres ComfyUI, tu charges un workflow SDXL, tu fais Save (API format) et tu remplaces le JSON imageWorkflow().

3) Brancher dans ton interface (colonne 3 = preview image/vidéo)
Dans ton index.html, ta colonne 3 est vide. Ajoute juste un conteneur :

html
Copier le code
<aside class="right" id="colRight">
<div id="stage" style="padding:16px;"></div>
</aside>
Puis dans ton JS (dans ton app.js), ajoute :

js
Copier le code
const stage = document.getElementById("stage");

async function rosalindaGenerateImage(prompt) {
const r = await fetch("http://localhost:3001/api/image", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ prompt, steps: 24 })
});
const data = await r.json();
if (!data.ok) throw new Error(data.error || "Erreur Rosalinda");

stage.innerHTML = `
<div style="font-weight:800;margin-bottom:10px;">Rosalinda — Image</div>
<img src="${data.url}" style="width:100%;border-radius:14px;border:1px solid #1f2a44;" />
`;
}

async function rosalindaGenerateVideo(prompt) {
const r = await fetch("http://localhost:3001/api/video", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ prompt })
});
const data = await r.json();
if (!data.ok) throw new Error(data.error || "Erreur Rosalinda");

stage.innerHTML = `
<div style="font-weight:800;margin-bottom:10px;">Rosalinda — Vidéo</div>
<video src="${data.url}" controls style="width:100%;border-radius:14px;border:1px solid #1f2a44;"></video>
`;
}
Et quand tu cliques sur Générer, tu choisis automatiquement :

js
Copier le code
generateBtn.addEventListener("click", async () => {
const text = promptInput.value.trim();
if (!text) return;

// Exemple : si tu écris "image: ..." -> image
if (text.toLowerCase().startsWith("image:")) {
await rosalindaGenerateImage(text.slice(6).trim());
return;
}

// Exemple : si tu écris "video: ..." -> video
if (text.toLowerCase().startsWith("video:")) {
await rosalindaGenerateVideo(text.slice(6).trim());
return;
}

// sinon, garde ton générateur de code
codeOutput.textContent = fakeGenerate(text);
});

docker-compose.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ```
index.html CHANGED
@@ -88,12 +88,11 @@
88
  </div>
89
  </section>
90
  </main>
91
-
92
  <!-- COL 3 -->
93
  <aside id="colRight" class="border-l border-slate-800 bg-transparent p-4">
94
- <!-- Colonne vide par défaut -->
95
  </aside>
96
- </div>
97
 
98
  <div id="toast" class="toast" hidden></div>
99
 
 
88
  </div>
89
  </section>
90
  </main>
 
91
  <!-- COL 3 -->
92
  <aside id="colRight" class="border-l border-slate-800 bg-transparent p-4">
93
+ <div id="stage" style="padding:16px;"></div>
94
  </aside>
95
+ </div>
96
 
97
  <div id="toast" class="toast" hidden></div>
98
 
rosalinda-server/Dockerfile ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
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
+ ```
rosalinda-server/package.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ```
rosalinda-server/server.js ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ ```
script.js CHANGED
@@ -14,11 +14,11 @@ const micBtn = document.getElementById("micBtn");
14
  const codeOutput = document.getElementById("codeOutput");
15
  const copyBtn = document.getElementById("copyBtn");
16
  const clearBtn = document.getElementById("clearBtn");
17
-
18
  const colRight = document.getElementById("colRight");
19
  const appTitle = document.getElementById("appTitle");
20
  const appSubtitle = document.getElementById("appSubtitle");
21
 
 
22
  // ===== générateur de code (temporaire) =====
23
  function fakeGenerate(prompt){
24
  const p = (prompt || "").trim();
@@ -32,6 +32,36 @@ function main() {
32
  main();`;
33
  }
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  // ===== moteur de consignes UI (simple et efficace) =====
36
  function isUiCommand(text){
37
  const t = (text||"").toLowerCase();
@@ -80,7 +110,6 @@ function applyUiCommand(text){
80
  toast("ℹ️ Consigne non reconnue");
81
  return false;
82
  }
83
-
84
  // ===== actions =====
85
  generateBtn.addEventListener("click", ()=>{
86
  const text = promptInput.value;
@@ -89,9 +118,26 @@ generateBtn.addEventListener("click", ()=>{
89
  if(applyUiCommand(text)) return;
90
  }
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  codeOutput.textContent = fakeGenerate(text);
93
  });
94
-
95
  document.querySelectorAll(".chip").forEach(btn=>{
96
  btn.addEventListener("click", ()=>{
97
  const p = btn.dataset.prompt || "";
 
14
  const codeOutput = document.getElementById("codeOutput");
15
  const copyBtn = document.getElementById("copyBtn");
16
  const clearBtn = document.getElementById("clearBtn");
 
17
  const colRight = document.getElementById("colRight");
18
  const appTitle = document.getElementById("appTitle");
19
  const appSubtitle = document.getElementById("appSubtitle");
20
 
21
+ const stage = document.getElementById("stage");
22
  // ===== générateur de code (temporaire) =====
23
  function fakeGenerate(prompt){
24
  const p = (prompt || "").trim();
 
32
  main();`;
33
  }
34
 
35
+ // ===== Rosalinda helpers =====
36
+ async function rosalindaGenerateImage(prompt) {
37
+ const r = await fetch("http://localhost:3001/api/image", {
38
+ method: "POST",
39
+ headers: {"Content-Type":"application/json"},
40
+ body: JSON.stringify({ prompt, steps: 24 })
41
+ });
42
+ const data = await r.json();
43
+ if (!data.ok) throw new Error(data.error || "Erreur Rosalinda");
44
+
45
+ stage.innerHTML = `
46
+ <div style="font-weight:800;margin-bottom:10px;">Rosalinda — Image</div>
47
+ <img src="${data.url}" style="width:100%;border-radius:14px;border:1px solid #1f2a44;" />
48
+ `;
49
+ }
50
+
51
+ async function rosalindaGenerateVideo(prompt) {
52
+ const r = await fetch("http://localhost:3001/api/video", {
53
+ method: "POST",
54
+ headers: {"Content-Type":"application/json"},
55
+ body: JSON.stringify({ prompt })
56
+ });
57
+ const data = await r.json();
58
+ if (!data.ok) throw new Error(data.error || "Erreur Rosalinda");
59
+
60
+ stage.innerHTML = `
61
+ <div style="font-weight:800;margin-bottom:10px;">Rosalinda — Vidéo</div>
62
+ <video src="${data.url}" controls style="width:100%;border-radius:14px;border:1px solid #1f2a44;"></video>
63
+ `;
64
+ }
65
  // ===== moteur de consignes UI (simple et efficace) =====
66
  function isUiCommand(text){
67
  const t = (text||"").toLowerCase();
 
110
  toast("ℹ️ Consigne non reconnue");
111
  return false;
112
  }
 
113
  // ===== actions =====
114
  generateBtn.addEventListener("click", ()=>{
115
  const text = promptInput.value;
 
118
  if(applyUiCommand(text)) return;
119
  }
120
 
121
+ // Example: si tu écris "image: ..." -> image
122
+ if (text.toLowerCase().startsWith("image:")) {
123
+ rosalindaGenerateImage(text.slice(6).trim()).catch(err => {
124
+ console.error(err);
125
+ toast("❌ " + (err.message || "Erreur image"));
126
+ });
127
+ return;
128
+ }
129
+
130
+ // Example: si tu écris "video: ..." -> video
131
+ if (text.toLowerCase().startsWith("video:")) {
132
+ rosalindaGenerateVideo(text.slice(6).trim()).catch(err => {
133
+ console.error(err);
134
+ toast("❌ " + (err.message || "Erreur vidéo"));
135
+ });
136
+ return;
137
+ }
138
+
139
  codeOutput.textContent = fakeGenerate(text);
140
  });
 
141
  document.querySelectorAll(".chip").forEach(btn=>{
142
  btn.addEventListener("click", ()=>{
143
  const p = btn.dataset.prompt || "";