Leçon 4 — Générer : image produit, clips, voix et musique

Voix off (Gemini TTS) et musique (Lyria)

8 min de lecture

Voix off (Gemini TTS) et musique (Lyria)

Premier essai réel du pipeline, le 2026-10-15. La voix off du plan 1 devait durer 3 secondes ; elle en dure 7,5. Le pipeline s'arrête avant d'appeler Veo, avec ce message : plan p1 : voix de 7.5 s pour un plan de 4 s, raccourcir le texte. La transcription du fichier donne la cause : « Dites d'une voix chaleureuse et posée avec un léger sourire. Cannellade Cold Brew. Infusé 16 heures à froid. » Le modèle a lu la consigne de style à voix haute.

Deux enseignements, que le code applique depuis : la voix se génère avant la vidéo, parce qu'elle coûte un millième de dollar et qu'elle échoue souvent ; et la consigne de style ne s'écrit jamais dans le texte lu.

Le mécanisme

sequenceDiagram
  participant P as Pipeline
  participant T as Gemini TTS
  participant F as ffmpeg
  participant G as Gemini (transcription)
  participant V as Veo
  P->>T: texte du plan + style en annotation
  T-->>P: WAV 24 kHz mono
  P->>F: retrait des silences de tête et de queue
  F-->>P: durée nette mesurée
  alt durée > plan - 0,4 s
    P-->>P: arrêt, 0 $ de vidéo dépensé
  end
  P->>G: que dit réellement l'audio ?
  alt transcription bien plus longue que le texte
    P-->>P: arrêt : consigne prononcée
  end
  P->>V: seulement maintenant, les clips

Une prise par plan. Chaque plan a sa propre ligne de voix, générée séparément. Le début de la prise est connu (début du plan + 0,2 s), sa durée est mesurée : les sous-titres se calent sans transcription ni alignement automatique.

Le style en annotation. gemini-3.8-flash-tts traite le texte comme une transcription mot à mot (c'est écrit dans la documentation). Le style (« chaleureuse et posée, léger sourire ») passe dans une annotation speech_metadata de l'API Interactions. Avec generateContent, ce modèle refuse l'instruction système (400 Developer instruction is not enabled for this model, constaté le même jour) : il n'y a pas d'autre endroit propre pour le style.

La musique. lyria-3-clip-preview produit toujours un extrait de 30 s en MP3 44,1 kHz stéréo, pour 0,04 $. L'instrumental se demande dans le prompt (« Instrumental only, no vocals ») ; il n'y a pas de paramètre dédié. Le montage n'en garde que 8 s, avec un fondu d'entrée de 0,3 s et de sortie de 0,8 s, et l'abaisse automatiquement quand la voix parle (compression en sidechain, détaillée en leçon 5).

La mise en œuvre

files/pubcourte/generation.py (extrait)
# gemini-3.8-flash-tts lit le texte mot à mot : une consigne écrite dans le texte est
# PRONONCÉE (constaté le 2026-10-15). Le style passe en annotation séparée.
it = self.client.interactions.create(
    model=modele,
    input=[{"type": "user_input", "content": [{
        "type": "text", "text": texte,
        "annotations": [{"type": "speech_metadata", "style": style}] if style else []}]}],
    response_format={"type": "audio"},
    generation_config={"speech_config": [{"voice": voix}]})
data = base64.b64decode(it.output_audio.data)

Sur Vertex AI, le même modèle se pilote par generateContent (l'API Interactions n'y est pas utilisée ici) ; le style est alors ignoré, avec un avertissement dans le journal. Les formats de sortie diffèrent selon le modèle : WAV complet pour gemini-3.8-flash-tts, PCM 16 bits brut (audio/L16;codec=pcm;rate=24000) pour gemini-2.5-flash-tts sur Vertex. pcm_en_wav() normalise les deux.

files/pubcourte/pipeline.py (extrait)
net = m.nettoyer_voix(gen.voix(b.voix.modele, b.voix.voix, b.voix.style, p.voix), ...)
d = m.duree(net)
if d > p.duree - 2 * MARGE_VOIX_S:
    raise BriefRefuse(f"plan {p.id} : voix de {d:.1f} s pour un plan de {p.duree} s, raccourcir le texte")
if controle_voix and hasattr(gen, "transcrire"):
    entendu = gen.transcrire(net)
    if voix_suspecte(p.voix, entendu):  # transcription > 1,5 × le texte attendu
        raise BriefRefuse(f"plan {p.id} : la voix dit « {entendu} » (consigne de style prononcée ?)")

Le retrait des silences est un filtre ffmpeg appliqué deux fois, à l'endroit puis à l'envers :

files/pubcourte/montage.py (extrait)
coupe = "silenceremove=start_periods=1:start_threshold=-45dB:start_silence=0.05"
ffmpeg(["-i", str(wav), "-af", f"{coupe},areverse,{coupe},areverse", str(sortie)])

Résultat obtenu

Exécutions réelles du 2026-10-15 :

PriseTexteBrutAprès retrait des silencesCoût
Essai 1 (consigne dans le texte)« Dites d'une voix chaleureuse… : Calade Cold Brew. Infusé seize heures, à froid. »8,04 s7,47 s, refusé0,0018 $
p1« Calade Cold Brew. Seize heures à froid. »3,28 s2,70 s0,0007 $
p2« Ouvrez. C'est prêt. »1,88 s1,29 s0,0004 $
Musiqueprompt lo-fi acoustique, 92 BPM, ré majeur30,77 s8 s montées0,04 $

Contrôle de transcription consigné dans manifeste.json :

"controle_voix": {
  "p1": "Canat Cold Brew. 16h à froid.",
  "p2": "Ouvrez, c'est prêt."
}

La transcription confirme qu'aucune consigne n'est prononcée. Elle ne confirme pas la prononciation de « Calade » : sur quatre essais, elle a donné « Cannellade », « Égalade », « Gaillarde », « Canat ». Un nom de marque inventé met en échec la transcription autant que la synthèse ; seule une écoute humaine tranche. Si la prononciation est fausse, écrivez le nom phonétiquement dans le texte envoyé au TTS, et gardez l'orthographe correcte dans les sous-titres.

Ce qui casse en production

SymptômeCauseParade
La voix lit la consigne de stylestyle écrit dans le texte d'un modèle « mot à mot »annotation speech_metadata ; contrôle de longueur de la transcription
400 Developer instruction is not enabledinstruction système envoyée à un modèle TTSne pas utiliser system_instruction avec le TTS
Voix qui déborde sur le plan suivanttexte trop long, silences de queue non retirésmesure après silenceremove, refus avant Veo
Fichier WAV illisiblePCM brut écrit tel quel (Vertex, modèles 2.5)en-tête WAV ajouté par pcm_en_wav()
Nom de marque mal prononcémot absent des données d'entraînementécoute humaine, graphie phonétique côté TTS
Musique avec voix chantée« instrumental » absent du prompt Lyria« Instrumental only, no vocals » en tête de prompt ; la sortie texte de Lyria renvoie <instrumental> quand c'est respecté

La règle de décision

Voix off TTS pour tout texte publicitaire : il change souvent et se refait pour presque rien. Gardez une voix par marque (ici Sulafat, décrite comme « Warm ») et ne la changez pas d'une campagne à l'autre. N'utilisez pas de clone de voix d'une personne réelle sans contrat écrit (leçon 6). Pour la musique, Lyria Clip suffit à un format de 8 à 30 s ; au-delà, lyria-3.5 produit des morceaux de quelques minutes pour 0,08 $. Une musique du commerce exige une licence de synchronisation, que le brief référence (source: licence:<contrat>).

Pratique

Le pipeline refuse un brief : « plan p2 : voix de 3,9 s pour un plan de 4 s ». Le client tient à son texte. Que proposez-vous ?

Sur Vertex AI, votre voix off sort sans le ton chaleureux obtenu avec la clé d'API. Le journal affiche « style ignoré ». Quelle est la décision la plus raisonnable ?

Exercice. Écrivez un test sans réseau qui vérifie que la musique est abaissée sous la voix : le graphe de filtres doit contenir sidechaincompress, alimenté par la voix, et ne pas en contenir quand aucun plan n'a de voix off.

Voir la solution

Le test existe déjà dans tests/test_montage.py :

def test_mixage_duck_et_loudnorm():
    g = filtre_mixage(2, [0.2, 4.2], 8.0, 0.35)
    assert "adelay=200|200" in g and "adelay=4200|4200" in g
    assert "sidechaincompress" in g and "loudnorm=I=-14" in g
    assert "afade=t=out:st=7.2" in g
    assert "sidechaincompress" not in filtre_mixage(0, [], 8.0, 0.35)

Il teste la construction du graphe, pas le son. Le rendu sonore se vérifie à l'écoute et par la mesure de sonie de la leçon 5.

  • Je génère et je mesure la voix avant de payer la vidéo
  • Je passe le style en annotation, jamais dans le texte lu
  • Je contrôle par transcription qu'aucune consigne n'est prononcée, et j'écoute les noms propres
  • Je demande une musique instrumentale explicitement et j'en connais le coût (0,04 $ l'extrait)
Suivant