diff --git a/frontend-angular-ai/voice-generator/README.md b/frontend-angular-ai/voice-generator/README.md new file mode 100644 index 00000000..7c513e98 --- /dev/null +++ b/frontend-angular-ai/voice-generator/README.md @@ -0,0 +1,104 @@ +# voice-generator + +Generate a short script with an **LLM** (ChatGPT or Claude), then turn it into speech with a +**Text-to-Speech (TTS)** engine. Two TTS providers are supported and interchangeable at request time: + +| Provider | `tts` value | Auth header | Response | Notes | +|--------------|---------------|------------------------------|-------------------|-----------------------------------------| +| ElevenLabs | `elevenlabs` | `xi-api-key: ` | streamed MP3 | Default. `eleven_multilingual_v2` model | +| 60dB | `60db` | `Authorization: Bearer `| JSON (base64 MP3) | `/tts-synthesize`, decoded to disk | + +--- + +## đŸ§© Architecture + +``` +Angular frontend ──POST /api/llm/:type/:llm──â–ș LLM service ──â–ș storage/data/-.json + │ │ + └────POST /api/voice/:llm?tts=──â–ș voice route ──reads JSON──┘ + │ + â–Œ + ElevenLabs OR 60dB adapter ──â–ș storage/voices/-.mp3 + │ + ◄──── { success, data: } ──────────┘ (served from /storage) +``` + +The voice route does **not** receive the text directly — it reads the script JSON produced by the +LLM step, then hands the text to the selected TTS adapter. + +--- + +## 🔗 API + +| Method | Endpoint | Description | +|--------|---------------------------------------|--------------------------------------------------------| +| POST | `/api/llm/:type/:llm` | Generate a script (`type` = `biography`/`summary`) | +| POST | `/api/voice/:llm?tts=` | Synthesize the script to MP3. `tts` defaults to `elevenlabs` | +| GET | `/api/voice/health/tts` | ElevenLabs connectivity/key check | + +**Voice request example** + +```bash +# ElevenLabs (default — ?tts can be omitted) +curl -X POST "http://localhost:3000/api/voice/chatgpt" \ + -H "Content-Type: application/json" -d '{"name":"Ridley Scott"}' + +# 60dB +curl -X POST "http://localhost:3000/api/voice/chatgpt?tts=60db" \ + -H "Content-Type: application/json" -d '{"name":"Ridley Scott"}' +``` + +Provider selection lives in `backend-javascript/src/routes/voice.routes.js` (`getTtsProvider()`), +mirroring the LLM `getProvider()` pattern. Each adapter exposes the same +`generateVoice(text, voiceId, outputPath)` signature: + +- `src/services/voice/voice.service.js` — ElevenLabs +- `src/services/voice/sixtydb.service.js` — 60dB + +--- + +## 🛠 Configuration + +`backend-javascript/.env` (see `.env.template`): + +```env +# true => local mocks, no API calls | false => real provider APIs +USE_MOCK=true + +# ElevenLabs +ELEVENLABS_API_KEY=eleven-your-key +ELEVENLABS_VOICE_ID=eleven-voice-id-xxxxxxxx + +# 60dB (VOICE_ID optional — blank uses the 60dB system default voice) +SIXTYDB_API_KEY=sixtydb-your-key +SIXTYDB_VOICE_ID= +``` + +When `USE_MOCK=true`, the backend copies a pre-recorded sample instead of calling any provider, so +no API key is needed to demo the flow. + +--- + +## 🎚 Frontend + +In the Angular UI a **Voix (TTS)** dropdown selects the provider (`ElevenLabs` / `60dB`). The choice +is sent through as `?tts=` and the voice buttons / status messages relabel to the active provider. + +> Note: in mock mode the bundled sample audio is ElevenLabs-style; switching to 60dB relabels the UI +> but plays the same sample. Real synthesis (`USE_MOCK=false`) uses the selected provider end to end. + +--- + +## ⚙ Quick start + +```bash +# Backend +cd backend-javascript +npm install +npm start # http://localhost:3000 + +# Frontend +cd frontend-angular +npm install +npm start # http://localhost:4200 +``` diff --git a/frontend-angular-ai/voice-generator/backend-javascript/.env.template b/frontend-angular-ai/voice-generator/backend-javascript/.env.template index 120a91e9..a9a7e767 100644 --- a/frontend-angular-ai/voice-generator/backend-javascript/.env.template +++ b/frontend-angular-ai/voice-generator/backend-javascript/.env.template @@ -37,6 +37,11 @@ DEEPSEEK_API_KEY=deepseek-your-key ELEVENLABS_API_KEY=eleven-your-key ELEVENLABS_VOICE_ID=eleven-voice-id-xxxxxxxx +# 60dB – Realistic voice synthesis & cloning (multi-language) +# VOICE_ID is optional: leave blank to use the 60dB system default voice +SIXTYDB_API_KEY=sixtydb-your-key +SIXTYDB_VOICE_ID= + # -------------------------------------------------- # AVATARS / VIDEO AI – Face & Speech Animation # -------------------------------------------------- diff --git a/frontend-angular-ai/voice-generator/backend-javascript/ai-docs/scripts/02 - config_ai-services.js.md b/frontend-angular-ai/voice-generator/backend-javascript/ai-docs/scripts/02 - config_ai-services.js.md index f2c87481..028a1ba5 100644 --- a/frontend-angular-ai/voice-generator/backend-javascript/ai-docs/scripts/02 - config_ai-services.js.md +++ b/frontend-angular-ai/voice-generator/backend-javascript/ai-docs/scripts/02 - config_ai-services.js.md @@ -16,6 +16,7 @@ const aiServices = { tts: [ { type: 'elevenlabs', label: 'ElevenLabs', purpose: 'High-quality voice synthesis from text, multilingual' }, + { type: '60db', label: '60dB', purpose: 'Voice synthesis and cloning from text, multilingual' }, // autres services TTS... ], diff --git a/frontend-angular-ai/voice-generator/backend-javascript/ai-docs/source/02 - config_ai-services.js.md b/frontend-angular-ai/voice-generator/backend-javascript/ai-docs/source/02 - config_ai-services.js.md index 129c5c60..fd77bbbf 100644 --- a/frontend-angular-ai/voice-generator/backend-javascript/ai-docs/source/02 - config_ai-services.js.md +++ b/frontend-angular-ai/voice-generator/backend-javascript/ai-docs/source/02 - config_ai-services.js.md @@ -13,6 +13,7 @@ const aiServices = { tts: [ { type: 'elevenlabs', label: 'ElevenLabs', purpose: 'High-quality voice synthesis from text, multilingual' }, + { type: '60db', label: '60dB', purpose: 'Voice synthesis and cloning from text, multilingual' }, ], avatar: [ diff --git a/frontend-angular-ai/voice-generator/backend-javascript/src/config/ai-services.js b/frontend-angular-ai/voice-generator/backend-javascript/src/config/ai-services.js index 51678ed2..ea34691e 100644 --- a/frontend-angular-ai/voice-generator/backend-javascript/src/config/ai-services.js +++ b/frontend-angular-ai/voice-generator/backend-javascript/src/config/ai-services.js @@ -10,6 +10,7 @@ const aiServices = { tts: [ { type: 'elevenlabs', label: 'ElevenLabs', purpose: 'High-quality voice synthesis from text, multilingual' }, + { type: '60db', label: '60dB', purpose: 'Voice synthesis and cloning from text, multilingual' }, ], avatar: [ diff --git a/frontend-angular-ai/voice-generator/backend-javascript/src/routes/voice.routes.js b/frontend-angular-ai/voice-generator/backend-javascript/src/routes/voice.routes.js index 298fbe8d..d9874256 100644 --- a/frontend-angular-ai/voice-generator/backend-javascript/src/routes/voice.routes.js +++ b/frontend-angular-ai/voice-generator/backend-javascript/src/routes/voice.routes.js @@ -4,7 +4,8 @@ import path from 'path'; import dotenv from 'dotenv'; import testElevenLabs from '../services/voice/test-elevenlabs.js'; -import generateVoice from '../services/voice/voice.service.js'; +import generateVoiceElevenLabs from '../services/voice/voice.service.js'; +import generateVoiceSixtyDb from '../services/voice/sixtydb.service.js'; import generateVoiceMock from '../mocks/voice/voice.mock.js'; dotenv.config(); @@ -16,11 +17,28 @@ function safeFilename(name, llm) { return `${name.toLowerCase().replace(/\s+/g, '-')}-${llm}`; } +function getTtsProvider(tts) { + const providers = { + elevenlabs: { + real: generateVoiceElevenLabs, + voiceId: () => process.env.ELEVENLABS_VOICE_ID || '21m00Tcm4TlvDq8ikWAM', + }, + '60db': { + real: generateVoiceSixtyDb, + voiceId: () => process.env.SIXTYDB_VOICE_ID || '', + }, + }; + + return providers[tts] || providers.elevenlabs; +} + router.post('/:llm', async (req, res) => { const { llm } = req.params; const { name } = req.body; - const voiceId = process.env.ELEVENLABS_VOICE_ID || '21m00Tcm4TlvDq8ikWAM'; + const tts = (req.query.tts || 'elevenlabs').toLowerCase(); + const provider = getTtsProvider(tts); + const voiceId = provider.voiceId(); const fileName = safeFilename(name, llm); const audioPath = path.join(process.cwd(), 'storage', 'voices', `${fileName}.mp3`); @@ -47,8 +65,8 @@ router.post('/:llm', async (req, res) => { await generateVoiceMock(text, voiceId, audioPath); console.log('🟡 TTS MOCK -', audioPath); } else { - await generateVoice(text, voiceId, audioPath); - console.log('✅ TTS rĂ©el -', audioPath); + await provider.real(text, voiceId, audioPath); + console.log(`✅ TTS rĂ©el (${tts}) -`, audioPath); } const publicPath = `/storage/voices/${fileName}.mp3`; diff --git a/frontend-angular-ai/voice-generator/backend-javascript/src/services/voice/sixtydb.service.js b/frontend-angular-ai/voice-generator/backend-javascript/src/services/voice/sixtydb.service.js new file mode 100644 index 00000000..776ba06f --- /dev/null +++ b/frontend-angular-ai/voice-generator/backend-javascript/src/services/voice/sixtydb.service.js @@ -0,0 +1,53 @@ + +import axios from 'axios'; +import fs from 'fs'; + +async function generateVoice(text, voiceId, outputPath) { + const url = 'https://api.60db.ai/tts-synthesize'; + + try { + const body = { + text: text, + output_format: 'mp3', + }; + + if (voiceId) { + body.voice_id = voiceId; + } + + const response = await axios.post( + url, + body, + { + headers: { + Authorization: `Bearer ${process.env.SIXTYDB_API_KEY}`, + 'Content-Type': 'application/json', + }, + }, + ); + + const { success, message, audio_base64 } = response.data || {}; + + if (!success || !audio_base64) { + throw new Error(message || 'RĂ©ponse 60db invalide (audio_base64 manquant)'); + } + + fs.writeFileSync(outputPath, Buffer.from(audio_base64, 'base64')); + console.log('✅ Audio enregistrĂ© :', outputPath); + + return outputPath; + + } catch (error) { + const status = error.response?.status; + + if (status) { + console.error(`❌ Erreur 60db ${status}`); + } else { + console.error('❌ Erreur inconnue :', error.message); + } + + throw error; + } +} + +export default generateVoice; diff --git a/frontend-angular-ai/voice-generator/frontend-angular/src/app/ai-service.ts b/frontend-angular-ai/voice-generator/frontend-angular/src/app/ai-service.ts index 1d07214d..b9608282 100644 --- a/frontend-angular-ai/voice-generator/frontend-angular/src/app/ai-service.ts +++ b/frontend-angular-ai/voice-generator/frontend-angular/src/app/ai-service.ts @@ -46,7 +46,7 @@ export class AiService { ); } - generateVoice(llm: string, name: string): Observable { + generateVoice(llm: string, name: string, tts = 'elevenlabs'): Observable { if (environment.useMock) { const safeName = name.toLowerCase().replace(/\s+/g, '-'); const voiceMockPath = `assets/voices/${safeName}-${llm}.mp3`; @@ -57,7 +57,7 @@ export class AiService { }).pipe(delay(1000)); } - const url = `${this.baseUrl}/voice/${llm}`; + const url = `${this.baseUrl}/voice/${llm}?tts=${encodeURIComponent(tts)}`; const body = { name }; return this.http.post(url, body).pipe( diff --git a/frontend-angular-ai/voice-generator/frontend-angular/src/app/app.html b/frontend-angular-ai/voice-generator/frontend-angular/src/app/app.html index 967e2a5c..f18259d1 100644 --- a/frontend-angular-ai/voice-generator/frontend-angular/src/app/app.html +++ b/frontend-angular-ai/voice-generator/frontend-angular/src/app/app.html @@ -15,13 +15,13 @@

voice-generator

-
+
-
+
+
+ + +
@@ -79,7 +85,7 @@

voice-generator

- + Voix OK ✓ RĂ©ponse en {{ voiceChatgptDuration.toFixed(1) }}s @@ -96,12 +102,12 @@

voice-generator

@@ -171,7 +177,7 @@

voice-generator

- + Voix OK ✓ RĂ©ponse en {{ voiceClaudeDuration.toFixed(1) }}s @@ -188,12 +194,12 @@

voice-generator

diff --git a/frontend-angular-ai/voice-generator/frontend-angular/src/app/app.ts b/frontend-angular-ai/voice-generator/frontend-angular/src/app/app.ts index f7b1f3b4..cf024db6 100644 --- a/frontend-angular-ai/voice-generator/frontend-angular/src/app/app.ts +++ b/frontend-angular-ai/voice-generator/frontend-angular/src/app/app.ts @@ -20,6 +20,7 @@ export class App { type = 'biography'; style = 'neutral'; length = 'short'; + tts = 'elevenlabs'; contentChatgpt = ''; contentClaude = ''; @@ -68,6 +69,15 @@ export class App { { value: 'technical', label: 'Technique' }, ]; + ttsOptions = [ + { value: 'elevenlabs', label: 'ElevenLabs' }, + { value: '60db', label: '60dB' }, + ]; + + get ttsLabel(): string { + return this.ttsOptions.find((o) => o.value === this.tts)?.label || 'ElevenLabs'; + } + private aiService = inject(AiService); toggleTheme() { @@ -155,7 +165,7 @@ export class App { } this.aiService - .generateVoice(llm, this.name) + .generateVoice(llm, this.name, this.tts) .subscribe((response: VoiceGenerationResponse) => { const duration = (performance.now() - start) / 1000; clearInterval(interval); @@ -187,6 +197,16 @@ export class App { this.resetAll(); } + onTtsChange(value: string) { + this.tts = value; + this.voiceChatgpt = ''; + this.voiceClaude = ''; + this.voiceChatgptError = null; + this.voiceClaudeError = null; + this.voiceChatgptDuration = 0; + this.voiceClaudeDuration = 0; + } + onTypeChange(value: string) { this.type = value; this.name = this.useMock