Spaces:
Running
services:
Browse filescomfyui:
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 +20 -0
- index.html +2 -3
- rosalinda-server/Dockerfile +9 -0
- rosalinda-server/package.json +16 -0
- rosalinda-server/server.js +157 -0
- script.js +49 -3
|
@@ -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 |
+
```
|
|
@@ -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 |
-
<
|
| 95 |
</aside>
|
| 96 |
-
|
| 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 |
|
|
@@ -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 |
+
```
|
|
@@ -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 |
+
```
|
|
@@ -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 |
+
```
|
|
@@ -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 || "";
|