Voix : plus d'accélération sur les SRT, moteur local Qwen3-TTS

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Upu97wMmsRkBoj6iVM4rH6
This commit is contained in:
Claude committed 2026-09-29 09:39:47 +00:00
1 parent 4b8fbe6d8e
commit 44daa88a49
20 files changed
+579 -98

No files matched your search

+15 -12
View File
@@ -7,29 +7,32 @@ from app.quality import Envelope
from app.render import RenderPlan
from app.srt_voice import (
Cue,
Segment,
as_sentence,
atempo_chain,
cue_windows,
extract_command,
mix_command,
place_segments,
segments_from_reading,
speed_factor,
split_by_cue,
)
def test_window_stops_at_next_cue_and_tolerates_a_short_overflow():
cues = [Cue(0.0, 2.0, "a"), Cue(2.1, 4.0, "b"), Cue(6.0, 7.0, "c")]
assert cue_windows(cues) == [2.1, 2.15, 1.25]
def test_segments_start_at_their_cue_when_they_fit():
cues = [Cue(0.0, 2.0, "a"), Cue(2.5, 4.0, "b"), Cue(6.0, 7.0, "c")]
assert place_segments(cues, [2.0, 1.0, 1.0]) == [0.0, 2.5, 6.0]
def test_voice_is_sped_up_only_when_too_long():
assert speed_factor(1.5, 2.0) == 1.0
assert speed_factor(3.0, 2.0) == 1.5
def test_overflowing_segment_pushes_the_next_ones_instead_of_speeding_up():
cues = [Cue(0.0, 1.0, "a"), Cue(1.0, 2.0, "b"), Cue(5.0, 6.0, "c")]
starts = place_segments(cues, [2.0, 2.0, 1.0])
assert starts[0] == 0.0
assert abs(starts[1] - 2.05) < 1e-9 # après la fin du premier, à vitesse normale
assert starts[2] == 5.0 # rattrape le minutage dès que possible
def test_atempo_is_split_into_supported_steps():
assert atempo_chain(1.25) == "atempo=1.2500"
assert atempo_chain(3.0) == "atempo=2.0000,atempo=1.5000"
def test_segments_are_never_sped_up():
cmd = extract_command(Path("in.wav"), Segment(1.0, 4.0, []), Path("out.wav"))
assert "atempo" not in cmd[cmd.index("-af") + 1]
def test_each_cue_becomes_a_sentence_for_the_reading():
+15
View File
@@ -21,3 +21,18 @@ def test_edge_fallbacks_are_french_and_same_gender():
def test_style_prompt_precedes_text():
assert build_prompt("Salut", "neutral") == "Salut"
assert build_prompt("Salut", "dynamic").endswith(":\nSalut")
def test_qwen_voice_keeps_clones_and_matches_gender():
from app.voices import qwen_instruction, resolve_qwen_voice
assert resolve_qwen_voice("clone:camille") == "clone:camille"
assert resolve_qwen_voice("Ryan") == "ryan"
assert resolve_qwen_voice("Charon") == "aiden" # voix Gemini masculine
assert resolve_qwen_voice(None) == "serena"
assert "French" in qwen_instruction("dynamic")
def test_qwen_voice_falls_back_to_same_gender_elsewhere():
assert resolve_edge_voice("uncle_fu").gender == "male"
assert resolve_gemini_voice("uncle_fu").id == "Charon"