From 44daa88a49e17d6c8ebc8cee916846a38c5164fe Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 09:39:47 +0000 Subject: [PATCH] =?UTF-8?q?Voix=20:=20plus=20d'acc=C3=A9l=C3=A9ration=20su?= =?UTF-8?q?r=20les=20SRT,=20moteur=20local=20Qwen3-TTS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SRT : la voix n'est plus jamais accélérée (atempo sans plafond, jusqu'à 2× et plus, rendait la voix incompréhensible). Un morceau plus long que son sous-titre déborde et décale les suivants ; les sous-titres suivent la voix, la vidéo s'allonge et un avertissement signale le décalage. Rythme : style par défaut « neutre », consignes Gemini sans « rythme entraînant », débit Edge des styles dynamique/promo ramené à +0 %/+3 %. Nouveau moteur « qwen » : service qwen-tts (Qwen3-TTS sur CPU, gratuit, français, ton piloté par consigne, clonage de voix depuis qwen-tts/voices). Même vérification Whisper que Gemini, généralisée ; repli qwen → gemini → edge, et une lecture sous 85 % du texte est écartée (mots sautés/faux). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Upu97wMmsRkBoj6iVM4rH6 --- .env.example | 11 ++ client/src/components/reels/voice-picker.tsx | 23 ++- docker-compose.yml | 34 ++++ ffmpeg-service/app/api.py | 2 +- ffmpeg-service/app/config.py | 7 + ffmpeg-service/app/srt_voice.py | 85 +++++----- ffmpeg-service/app/tts/__init__.py | 81 +++++++--- ffmpeg-service/app/tts/edge.py | 5 +- ffmpeg-service/app/tts/qwen.py | 56 +++++++ ffmpeg-service/app/voices.py | 56 ++++++- ffmpeg-service/tests/test_srt_voice.py | 27 ++-- ffmpeg-service/tests/test_voices.py | 15 ++ qwen-tts/Dockerfile | 33 ++++ qwen-tts/main.py | 159 +++++++++++++++++++ qwen-tts/requirements.txt | 6 + qwen-tts/voices/README.md | 17 ++ server/routes/reels.ts | 14 ++ server/services/ffmpeg.ts | 18 ++- server/services/reels/assets.ts | 7 +- shared/voices.ts | 21 ++- 20 files changed, 579 insertions(+), 98 deletions(-) create mode 100644 ffmpeg-service/app/tts/qwen.py create mode 100644 qwen-tts/Dockerfile create mode 100644 qwen-tts/main.py create mode 100644 qwen-tts/requirements.txt create mode 100644 qwen-tts/voices/README.md diff --git a/.env.example b/.env.example index fdcb0ed..91920e8 100644 --- a/.env.example +++ b/.env.example @@ -132,6 +132,17 @@ GEMINI_API_KEY= # demande de reconstruire ffmpeg-api. # WHISPER_MODEL=base # +# Voix locale Qwen3-TTS (service qwen-tts) : gratuite, française, sans quota, +# mais générée sur CPU (5 à 7 fois la durée de la voix sur 4 cœurs). +# Modèle choisi à la construction de l'image : +# - Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice (défaut) : ton et émotion pilotés ; +# - Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice : ~30 % plus rapide, moins expressif. +# QWEN_TTS_MODEL=Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice +# Clonage des voix déposées dans qwen-tts/voices (vide pour désactiver). +# QWEN_TTS_CLONE_MODEL=Qwen/Qwen3-TTS-12Hz-0.6B-Base +# QWEN_TTS_THREADS=0 +# QWEN_TTS_MEMORY=10g +# # Moteur de rendu des Reels vidéo : # - remotion (défaut) : sous-titres animés, logo et effet de fin identiques à # l'aperçu affiché dans l'application ; ~30 à 60 s pour un Reel de 30 s ; diff --git a/client/src/components/reels/voice-picker.tsx b/client/src/components/reels/voice-picker.tsx index c58f57e..d53c86c 100644 --- a/client/src/components/reels/voice-picker.tsx +++ b/client/src/components/reels/voice-picker.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { Loader2, Play, Square } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; @@ -19,6 +20,7 @@ import { voicesFor, type TtsEngine, type TtsStyle, + type VoiceOption, } from "@shared/voices"; import type { TimedWord } from "@shared/captions"; @@ -76,7 +78,13 @@ export function VoicePicker({ value, onChange, sampleText, compact = false, onPr useEffect(() => () => audioRef.current?.pause(), []); - const voices = voicesFor(value.engine); + // Voix Qwen réellement installées (voix clonées comprises) ; moteur grisé si le service local ne répond pas + const { data: qwenCatalog } = useQuery<{ qwenAvailable: boolean; qwen: VoiceOption[] }>({ + queryKey: ["/api/reels/voices"], + staleTime: 5 * 60_000, + }); + const qwenAvailable = qwenCatalog?.qwenAvailable ?? false; + const voices = value.engine === "qwen" && qwenCatalog?.qwen.length ? qwenCatalog.qwen : voicesFor(value.engine); const groups = [ { label: "Voix féminines", items: voices.filter((v) => v.gender === "female") }, { label: "Voix masculines", items: voices.filter((v) => v.gender === "male") }, @@ -133,7 +141,18 @@ export function VoicePicker({ value, onChange, sampleText, compact = false, onPr
-
+
+