diff --git a/.gitignore b/.gitignore index edbc828..60d7403 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ data/* !data/manifeste.md !data/manifeste_references_nouvelles.md .claude/ +apps/* +test-results/playwright-html/* +test-results/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 7cd8308..c7e8f1a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -257,12 +257,50 @@ stateDiagram-v2 ### Coordinateur - [x] Audit SOTA 2026 intelligence + affectation agents par module +- [x] lot-552: cloture documentaire lot-192 + sync QA/veille OSS ### Backend API -- [x] lot-192: ws-commands modular extraction phases 1-2 (`/comp`, `/layer`, `/mix`, `/voice`, gestion composition) -- [ ] lot-192: phase 3 extraction edition avancee (`/concat`, `/silence`, `/template`, `/marker`, `/metronome`, `/delete`, `/suggest`, `/snapshot`, `/randomize`) +- [x] lot-192: ws-commands modular extraction phases 1-3 (`/comp`, `/layer`, `/mix`, `/voice`, gestion composition, edition avancee) +- [ ] lot-549: renfort tests composition et export ## Delta Session 2026-03-24 — phase 3 ### Backend API - [x] lot-192: extraction complete du bloc compose avance (`/concat`, `/silence`, `/template`, `/marker`, `/metronome`, `/delete`, `/suggest`, `/snapshot`, `/randomize`) -- [ ] lot-193: renfort tests composition et export \ No newline at end of file +- [x] lot-552: cloture documentaire et durcissement final lot-192 +- [ ] lot-549: renfort tests composition et export +- [ ] lot-548: waveform timeline UI v1 +## Affectations Session 2026-03-24 — suite immediate + +| Lot | Agent pilote | Sous-agents / competences | Portee immediate | +|---|---|---|---| +| lot-549 | Backend API | Polyglot Test Agent, QA, context-map | Etendre `apps/api/src/ws-commands.test.ts` et `apps/api/src/composition-store.test.ts`, verifier non-regression composition | +| lot-548 | Frontend | Expert React Frontend Engineer, gem-browser-tester, context-map | Remplacer la waveform canvas par une timeline waveform plus lisible sans regression mobile | +| lot-552 | Coordinateur | Planner/Docs, gem-researcher, refactor-plan | Maintenir PLAN/TODO/AGENTS/QA/OSS alignes avec l'etat reel et tracer les prochains deltas | + +## Skills privilegies + +- context-map: cartographie des fichiers et dependances avant modification. +- refactor-plan: sequencing prudent des changements multi-fichiers. +- polyglot-test-agent: lot-549 pour la recherche, le plan et l'implementation de tests additionnels. +## Delta Session 2026-03-24 — validation compose + waveform + +### Backend API +- [x] lot-549: renfort tests composition et export +- Validation ciblee: `tsx --test src/composition-store.test.ts src/ws-commands.test.ts` => 44/44 OK + +### Frontend +- [x] lot-548: waveform timeline UI v1 +- Implementation: `wavesurfer.js` branche dans `apps/web/src/components/ComposePage.tsx` pour apercu piste + bloc timeline +- Validation: `npm run check` dans `apps/web` OK +## Delta Session 2026-03-24 — cleanup compose + +### Frontend +- [x] lot-553: suppression du doublon `apps/web/src/components/ComposePage 2.tsx` +- Validation: `npm run check` dans `apps/web` OK +- Impact: reduction dette technique UI compose et elimination d'un faux point d'entree potentiel +## Delta Session 2026-03-24 — QA visuelle compose + +### Frontend +- [x] lot-554: validation Playwright ciblee compose timeline +- Commande: `npx playwright test e2e/visual-qa.spec.ts --project=chromium --grep "ComposePage|composition timeline"` +- Resultat: 2/2 OK \ No newline at end of file diff --git a/PLAN.md b/PLAN.md index a3cf8fe..9e7210e 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1022,13 +1022,34 @@ Updated: 2026-03-20T12:00:00Z ## Delta Session 2026-03-24 -- lot-192-ws-commands-extraction reste en cours. -- Phases 1-2 executees: extraction de /comp, /layer, /mix, /voice, /tracks, /undo, /solo, /unsolo, /rename, /dup, /bpm, /clear-comp, /preview, /gain, /loop, /swap et /info vers apps/api/src/ws-commands-compose.ts. -- Validation: diagnostics TS OK, suite @kxkm/api relancee avec ws-commands.test.ts OK. -- Prochaine etape: phase 3 sur concat/silence/template/marker/metronome/delete/suggest/snapshot/randomize, puis lot-193 tests composition. +- lot-192-ws-commands-extraction est cloture. +- Phases 1-3 executees: extraction de /comp, /layer, /mix, /voice, /tracks, /undo, /solo, /unsolo, /rename, /dup, /bpm, /clear-comp, /preview, /gain, /loop, /swap, /info, /concat, /silence, /template, /marker, /metronome, /delete, /suggest, /snapshot et /randomize vers apps/api/src/ws-commands-compose.ts. +- Validation: diagnostics TS OK, suite @kxkm/api relancee avec ws-commands.test.ts OK, cloture documentaire lot-552 appliquee. +- Prochaine etape: lot-549 tests composition, puis lot-548 waveform/timeline UI. ## Delta Session 2026-03-24 — phase 3 - lot-192-ws-commands-extraction: phases 1-3 executees. - Bloc compose complet extrait vers apps/api/src/ws-commands-compose.ts, incluant maintenant concat/silence/template/marker/metronome/delete/suggest/snapshot/randomize. - Validation: diagnostics TS OK, suite @kxkm/api relancee avec ws-commands.test.ts OK. -- Prochaine etape: lot-193 tests composition, puis lot-194 waveform/timeline UI. \ No newline at end of file +- lot-552: synchronisation PLAN/TODO/AGENTS + rapport QA webdesign + veille OSS ciblee sur waveform/tests/orchestration. +- Prochaine etape: lot-549 tests composition, puis lot-194 waveform/timeline UI. +## Delta Session 2026-03-24 — validation compose + waveform + +- lot-549-composition-tests est valide. +- Couverture ajoutee sur `apps/api/src/composition-store.test.ts` et `apps/api/src/ws-commands.test.ts` pour rebind composition active, clamps timeline, `/comp load`, `/mix`, `/snapshot`, `/template` et `/delete`. +- Validation lot-549: `../../node_modules/.bin/tsx --test src/composition-store.test.ts src/ws-commands.test.ts` => 44/44 tests OK. +- lot-548-waveform-ui-v1 est implemente dans `apps/web/src/components/ComposePage.tsx` avec `wavesurfer.js` pour les apercus de pistes et blocs timeline. +- Validation lot-548: `npm run check` dans `apps/web` OK. +- Prochaine etape recommandee: capture visuelle Playwright du compose timeline + nettoyage du doublon `ComposePage 2.tsx`. +## Delta Session 2026-03-24 — cleanup compose + +- lot-553-cleanup-compose-doublon execute. +- Suppression du fichier duplique non reference `apps/web/src/components/ComposePage 2.tsx`. +- Validation: `npm run check` dans `apps/web` OK. +- Risque reduit: suppression d'une source de divergence entre UI compose legacy et UI active. +## Delta Session 2026-03-24 — QA visuelle compose + +- lot-554-qa-compose-timeline execute. +- Validation Playwright ciblee sur `apps/web/e2e/visual-qa.spec.ts` (grep `ComposePage|composition timeline`). +- Resultat: 2/2 tests OK, captures regenerees `10-compose-page.png` et `18-composition-timeline.png`. +- Conclusion: rendu Compose/timeline stable apres migration waveform + cleanup doublon. \ No newline at end of file diff --git a/TODO.md b/TODO.md index 472728d..f69db21 100644 --- a/TODO.md +++ b/TODO.md @@ -412,12 +412,28 @@ Fait sur ce lot: ## Delta Session 2026-03-24 -- [ ] lot-550 (en cours): lot-192 ws-commands extraction phases 1-2 (compose core + gestion composition) + audit SOTA 2026 -- [ ] lot-551: lot-192 phase 3 extraction edition avancee (concat/silence/template/marker/metronome/delete/suggest/snapshot/randomize) -- [ ] lot-549: composition tests renfort (store/remix/export/timeline) +- [x] lot-550: lot-192 ws-commands extraction phases 1-2 (compose core + gestion composition) + audit SOTA 2026 +- [x] lot-551: lot-192 phase 3 extraction edition avancee (concat/silence/template/marker/metronome/delete/suggest/snapshot/randomize) +- [x] lot-552: cloture documentaire et durcissement final lot-192 +- [ ] lot-549 (en cours): composition tests renfort (store/remix/export/timeline) ## Delta Session 2026-03-24 — phase 3 - [x] lot-551: lot-192 phase 3 extraction edition avancee (concat/silence/template/marker/metronome/delete/suggest/snapshot/randomize) -- [ ] lot-552: cloture documentaire et durcissement final lot-192 -- [ ] lot-549: composition tests renfort (store/remix/export/timeline) -- [ ] lot-548: waveform timeline UI v1 (desktop + mobile) \ No newline at end of file +- [x] lot-552: cloture documentaire et durcissement final lot-192 +- [ ] lot-549 (en cours): composition tests renfort (store/remix/export/timeline) +- [ ] lot-548: waveform timeline UI v1 (desktop + mobile) +## Delta Session 2026-03-24 — validation compose + waveform + +- [x] lot-549: composition tests renfort (store/remix/export/timeline) +- [x] lot-548: waveform timeline UI v1 (desktop + mobile) +- [ ] prochain lot suggere: capture QA visuelle compose timeline + nettoyage des doublons UI compose +## Delta Session 2026-03-24 — cleanup compose + +- [x] lot-553: cleanup doublon ComposePage non reference +- [x] validation frontend apres cleanup (`npm run check`) +- [ ] prochain lot suggere: passe QA visuelle Playwright sur timeline compose waveform +## Delta Session 2026-03-24 — QA visuelle compose + +- [x] lot-554: passe Playwright visuelle ciblee Compose/timeline +- [x] verification captures `10-compose-page.png` et `18-composition-timeline.png` +- [ ] prochain lot suggere: triage et purge artefacts test-results non necessaires au commit \ No newline at end of file diff --git a/apps/api/data/persona-memory/Pharmacius.json b/apps/api/data/persona-memory/Pharmacius.json index 8d28fad..dfb00a5 100644 --- a/apps/api/data/persona-memory/Pharmacius.json +++ b/apps/api/data/persona-memory/Pharmacius.json @@ -2,5 +2,5 @@ "nick": "Pharmacius", "facts": [], "summary": "", - "lastUpdated": "2026-03-24T16:42:42.979Z" + "lastUpdated": "2026-03-24T17:51:36.369Z" } \ No newline at end of file diff --git a/apps/api/src/composition-store.test.ts b/apps/api/src/composition-store.test.ts index ead7780..6b211af 100644 --- a/apps/api/src/composition-store.test.ts +++ b/apps/api/src/composition-store.test.ts @@ -229,4 +229,62 @@ describe("composition-store", () => { assert.equal(onDisk.timeline.markers.length, 1); assert.equal(onDisk.timeline.markers[0].label, "Intro"); }); + + it("setActiveComposition rebinds nick/channel while preserving timeline", async () => { + const { + createComposition, + addTrack, + setActiveComposition, + getActiveComposition, + getTimeline, + } = await storePromise; + const comp = createComposition("louis", "#old", "Rebind test"); + const track = addTrack(comp.id, { + type: "music", + prompt: "slow pulse", + duration: 6, + volume: 55, + startMs: 1200, + }); + + assert.ok(track); + + const rebound = setActiveComposition("marie", "#new", comp.id); + assert.ok(rebound); + assert.equal(rebound!.nick, "marie"); + assert.equal(rebound!.channel, "#new"); + + const active = getActiveComposition("marie", "#new"); + assert.ok(active); + assert.equal(active!.id, comp.id); + + const timeline = getTimeline(comp.id); + assert.ok(timeline); + assert.equal(timeline!.tracks.length, 1); + assert.equal(timeline!.clips.length, 1); + assert.equal(timeline!.clips[0].trackId, track!.id); + }); + + it("clamps timeline settings and sorts markers by position", async () => { + const { + createComposition, + updateTimelineSettings, + addTimelineMarker, + listTimelineMarkers, + } = await storePromise; + const comp = createComposition("nina", "#sort", "Clamp test"); + + const timeline = updateTimelineSettings(comp.id, { bpm: 999, timeSignature: [0, 0] }); + assert.ok(timeline); + assert.equal(timeline!.bpm, 300); + assert.deepEqual(timeline!.timeSignature, [1, 1]); + + addTimelineMarker(comp.id, { label: "Outro", atMs: 9000 }); + addTimelineMarker(comp.id, { label: "Intro", atMs: 1000 }); + addTimelineMarker(comp.id, { label: "Middle", atMs: 5000 }); + + const markers = listTimelineMarkers(comp.id); + assert.deepEqual(markers.map((marker) => marker.label), ["Intro", "Middle", "Outro"]); + assert.deepEqual(markers.map((marker) => marker.atMs), [1000, 5000, 9000]); + }); }); diff --git a/apps/api/src/ws-commands.test.ts b/apps/api/src/ws-commands.test.ts index 08536e3..4b416d7 100644 --- a/apps/api/src/ws-commands.test.ts +++ b/apps/api/src/ws-commands.test.ts @@ -1,7 +1,10 @@ import { describe, it, mock, beforeEach } from "node:test"; import assert from "node:assert/strict"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { createCommandHandler } from "./ws-commands.js"; -import { addTrack, createComposition } from "./composition-store.js"; +import { addTrack, createComposition, getActiveComposition } from "./composition-store.js"; import type { ClientInfo, ChatPersona, OutboundMessage } from "./chat-types.js"; import type { WebSocket } from "ws"; @@ -22,6 +25,14 @@ function makeInfo(overrides?: Partial): ClientInfo { }; } +function makeComposeInfo(prefix: string): ClientInfo { + const suffix = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + return makeInfo({ + nick: `${prefix}-${suffix}`, + channel: `#${prefix.toLowerCase()}-${suffix}`, + }); +} + function makeDeps(overrides?: Partial>) { return { send: mock.fn<(ws: WebSocket, msg: OutboundMessage) => void>(), @@ -388,13 +399,94 @@ describe("/compose", () => { }); }); +/* ------------------------------------------------------------------ */ +/* /comp and extracted composition commands */ +/* ------------------------------------------------------------------ */ + +describe("/comp", () => { + it("creates a composition and emits the load payload", async () => { + const info = makeComposeInfo("ComposerNew"); + const deps = makeDeps(); + const handle = createCommandHandler(deps); + + await handle({ ws: fakeWs, info, text: "/comp new Session test" }); + + assert.equal(deps.send.mock.callCount(), 2); + const createdMsg = deps.send.mock.calls[0].arguments[1] as { type: string; text: string }; + const loadedMsg = deps.send.mock.calls[1].arguments[1] as { type: string; text: string }; + assert.equal(createdMsg.type, "system"); + assert.ok(createdMsg.text.includes("Composition creee")); + assert.ok(createdMsg.text.includes("Session test")); + assert.ok(loadedMsg.text.startsWith("__comp_loaded__")); + + const match = createdMsg.text.match(/\((comp_[^)]+)\)/); + assert.ok(match, "should expose created composition id"); + rmSync(path.join(process.cwd(), "data", "compositions", match![1]), { recursive: true, force: true }); + }); + + it("loads an existing composition by id", async () => { + const info = makeComposeInfo("ComposerLoad"); + const comp = createComposition("OtherNick", "#other", "Existing comp"); + const deps = makeDeps(); + const handle = createCommandHandler(deps); + + try { + await handle({ ws: fakeWs, info, text: `/comp load ${comp.id}` }); + + assert.equal(deps.send.mock.callCount(), 2); + const statusMsg = deps.send.mock.calls[0].arguments[1] as { type: string; text: string }; + const loadedMsg = deps.send.mock.calls[1].arguments[1] as { type: string; text: string }; + assert.equal(statusMsg.type, "system"); + assert.ok(statusMsg.text.includes("Composition chargee")); + assert.ok(statusMsg.text.includes("Existing comp")); + assert.ok(loadedMsg.text.startsWith("__comp_loaded__")); + } finally { + rmSync(path.join(process.cwd(), "data", "compositions", comp.id), { recursive: true, force: true }); + } + }); +}); + /* ------------------------------------------------------------------ */ /* composition management routing */ /* ------------------------------------------------------------------ */ +describe("/mix", () => { + it("mixes a single available track and exposes download paths", async () => { + const info = makeComposeInfo("ComposerMix"); + const comp = createComposition(info.nick, info.channel, "Mix test"); + const tempDir = mkdtempSync(path.join(os.tmpdir(), "kxkm-mix-")); + const sourcePath = path.join(tempDir, "track.wav"); + writeFileSync(sourcePath, Buffer.from("RIFFtest-wave")); + const track = addTrack(comp.id, { type: "music", prompt: "drone test", duration: 7, volume: 80, startMs: 0 }); + assert.ok(track); + track!.filePath = sourcePath; + + const deps = makeDeps(); + const handle = createCommandHandler(deps); + + try { + await handle({ ws: fakeWs, info, text: "/mix" }); + + const broadcastMusic = deps.broadcast.mock.calls.find((call) => { + const msg = call.arguments[1] as { type?: string }; + return msg.type === "music"; + }); + assert.ok(broadcastMusic, "should broadcast mixed audio"); + + const systemMsg = deps.send.mock.calls.at(-1)?.arguments[1] as { type: string; text: string }; + assert.equal(systemMsg.type, "system"); + assert.ok(systemMsg.text.includes(`/api/v2/media/compositions/${comp.id}/mix`)); + assert.ok(existsSync(path.join(process.cwd(), "data", "compositions", comp.id, "mix.wav"))); + } finally { + rmSync(tempDir, { recursive: true, force: true }); + rmSync(path.join(process.cwd(), "data", "compositions", comp.id), { recursive: true, force: true }); + } + }); +}); + describe("/rename", () => { it("renames the active composition through the extracted compose handler", async () => { - const info = makeInfo({ nick: "ComposerRename", channel: "#compose-rename" }); + const info = makeComposeInfo("ComposerRename"); createComposition(info.nick, info.channel, "Ancien nom"); const deps = makeDeps(); @@ -409,7 +501,7 @@ describe("/rename", () => { describe("/tracks", () => { it("lists tracks through the extracted compose management handler", async () => { - const info = makeInfo({ nick: "ComposerTracks", channel: "#compose-tracks" }); + const info = makeComposeInfo("ComposerTracks"); const comp = createComposition(info.nick, info.channel, "Track test"); addTrack(comp.id, { type: "music", prompt: "drone test", duration: 12, volume: 80, startMs: 0 }); @@ -426,10 +518,12 @@ describe("/tracks", () => { describe("/delete", () => { it("deletes a track through the extracted compose advanced handler", async () => { - const info = makeInfo({ nick: "ComposerDelete", channel: "#compose-delete" }); - const comp = createComposition(info.nick, info.channel, "Delete test"); - addTrack(comp.id, { type: "music", prompt: "first track", duration: 8, volume: 100, startMs: 0 }); - addTrack(comp.id, { type: "music", prompt: "second track", duration: 9, volume: 100, startMs: 0 }); + const info = makeComposeInfo("ComposerDelete"); + createComposition(info.nick, info.channel, "Delete test"); + let activeComp = getActiveComposition(info.nick, info.channel); + assert.ok(activeComp); + addTrack(activeComp!.id, { type: "music", prompt: "first track", duration: 8, volume: 100, startMs: 0 }); + addTrack(activeComp!.id, { type: "music", prompt: "second track", duration: 9, volume: 100, startMs: 0 }); const deps = makeDeps(); const handle = createCommandHandler(deps); @@ -438,14 +532,16 @@ describe("/delete", () => { const msg = deps.send.mock.calls.at(-1)?.arguments[1] as { type: string; text: string }; assert.equal(msg.type, "system"); assert.ok(msg.text.includes("supprimee")); - assert.equal(comp.tracks.length, 1); - assert.ok(comp.tracks[0].prompt.includes("second track")); + activeComp = getActiveComposition(info.nick, info.channel); + assert.ok(activeComp); + assert.equal(activeComp!.tracks.length, 1); + assert.ok(activeComp!.tracks[0].prompt.includes("second track")); }); }); describe("/marker", () => { it("adds and lists markers through the extracted compose advanced handler", async () => { - const info = makeInfo({ nick: "ComposerMarker", channel: "#compose-marker" }); + const info = makeComposeInfo("ComposerMarker"); createComposition(info.nick, info.channel, "Marker test"); const deps = makeDeps(); @@ -459,3 +555,41 @@ describe("/marker", () => { assert.ok(lastMsg.text.includes("12s")); }); }); + +describe("/snapshot", () => { + it("writes a snapshot file for the active composition", async () => { + const info = makeComposeInfo("ComposerSnapshot"); + const comp = createComposition(info.nick, info.channel, "Snapshot test"); + addTrack(comp.id, { type: "music", prompt: "frozen pulse", duration: 11, volume: 70, startMs: 0 }); + + const deps = makeDeps(); + const handle = createCommandHandler(deps); + const snapshotPath = path.join(process.cwd(), "data", "compositions", comp.id, "snapshots", "v1.json"); + + try { + await handle({ ws: fakeWs, info, text: "/snapshot v1" }); + + const msg = deps.send.mock.calls.at(-1)?.arguments[1] as { type: string; text: string }; + assert.equal(msg.type, "system"); + assert.ok(msg.text.includes('Snapshot "v1" sauvegarde')); + assert.ok(existsSync(snapshotPath)); + } finally { + rmSync(path.join(process.cwd(), "data", "compositions", comp.id), { recursive: true, force: true }); + } + }); +}); + +describe("/template", () => { + it("lists available templates when the requested one is unknown", async () => { + const deps = makeDeps(); + const handle = createCommandHandler(deps); + + await handle({ ws: fakeWs, info: makeInfo(), text: "/template inconnu" }); + + const msg = deps.send.mock.calls.at(-1)?.arguments[1] as { type: string; text: string }; + assert.equal(msg.type, "system"); + assert.ok(msg.text.includes("Templates:")); + assert.ok(msg.text.includes("ambient-4")); + assert.ok(msg.text.includes("Usage: /template ")); + }); +}); diff --git a/apps/web/src/components/ComposePage 2.tsx b/apps/web/src/components/ComposePage 2.tsx deleted file mode 100644 index 6b216ac..0000000 --- a/apps/web/src/components/ComposePage 2.tsx +++ /dev/null @@ -1,291 +0,0 @@ -import React, { useState, useRef, useEffect, useCallback } from "react"; -import { VideotexPageHeader, VideotexSeparator } from "./VideotexMosaic"; - -interface Track { - id: number; - prompt: string; - style: string; - duration: number; - volume: number; - type: "music" | "voice" | "sfx"; - audioData?: string; - audioMime?: string; -} - -export default function ComposePage() { - const [prompt, setPrompt] = useState(""); - const [style, setStyle] = useState("experimental"); - const [duration, setDuration] = useState(30); - const [tracks, setTracks] = useState([]); - const [mixing, setMixing] = useState(false); - const [generating, setGenerating] = useState(false); - const [status, setStatus] = useState(""); - const [compName, setCompName] = useState("Ma composition"); - const wsRef = useRef(null); - - // Connect to WebSocket - useEffect(() => { - const nick = sessionStorage.getItem("kxkm-nick") || "composer"; - const wsUrl = (location.protocol === "https:" ? "wss:" : "ws:") + "//" + location.host + "/ws?nick=" + encodeURIComponent(nick); - const ws = new WebSocket(wsUrl); - wsRef.current = ws; - - ws.onmessage = (event) => { - try { - const m = JSON.parse(event.data); - - // Capture music results (from /layer, /noise, /mix) - if (m.type === "music" && m.audioData) { - setTracks(prev => [...prev, { - id: Date.now() + Math.random(), - prompt: m.text || "Sans titre", - style, - duration, - volume: 100, - type: (m.text || "").includes("Noise") ? "sfx" : (m.text || "").includes("Mix") ? "music" : "music", - audioData: m.audioData, - audioMime: m.audioMime || "audio/wav", - }]); - setGenerating(false); - setStatus(""); - } - - // Capture voice results (from /voice) - if (m.type === "audio" && m.data) { - setTracks(prev => [...prev, { - id: Date.now() + Math.random(), - prompt: m.nick ? m.nick + " (voix)" : "Voix", - style: "voice", - duration: 10, - volume: 100, - type: "voice", - audioData: m.data, - audioMime: m.mimeType || "audio/wav", - }]); - setGenerating(false); - setStatus(""); - } - - // Status messages - if (m.type === "system" && m.text) { - const t = m.text; - if (t.includes("ajoute une piste") || t.includes("Generation") || t.includes("generation") || t.includes("Mixage") || t.includes("compose")) { - setStatus(t.slice(0, 100)); - } - if (t.includes("Erreur") || t.includes("echouee")) { - setGenerating(false); - setStatus("Erreur: " + t.slice(0, 80)); - } - if (t.includes("Mix termine")) { - setMixing(false); - setStatus("Mix termine!"); - } - } - } catch {} - }; - - return () => { ws.close(); }; - }, []); - - function sendCmd(cmd: string) { - if (wsRef.current?.readyState === WebSocket.OPEN) { - wsRef.current.send(JSON.stringify({ type: "command", text: cmd })); - } - } - - function handleAddTrack(e: React.FormEvent) { - e.preventDefault(); - if (!prompt.trim() || generating) return; - setGenerating(true); - setStatus("Generation en cours..."); - sendCmd("/layer " + prompt.trim() + ", " + style + " style, " + duration + "s"); - } - - function handleAddVoice() { - if (!prompt.trim() || generating) return; - setGenerating(true); - setStatus("Voix en cours..."); - sendCmd("/voice Pharmacius \"" + prompt.trim() + "\""); - } - - function handleAddNoise(type: string) { - setGenerating(true); - setStatus("Noise " + type + "..."); - sendCmd("/noise " + type + " " + duration); - } - - function handleMix() { - setMixing(true); - setStatus("Mixage..."); - sendCmd("/mix"); - } - - function handleNewComp() { - sendCmd("/comp new " + compName); - setTracks([]); - setStatus("Nouvelle composition"); - } - - const maxDuration = Math.max(30, ...tracks.map(t => t.duration)); - - return ( -
- - - {/* Header */} -
- setCompName(e.target.value)} className="minitel-input" placeholder="Nom" style={{flex:1}} /> - - {tracks.length} piste{tracks.length !== 1 ? "s" : ""} -
- - {status &&
{status}
} - - - - {/* Generator */} -
-
{"> AJOUTER UNE PISTE"}
-