Segments P4 (14 MP3)
|
v
FFmpeg concat avec transitions :
- Silence inter-segment: 500ms
- Fade in: 200ms (debut de segment)
- Fade out: 200ms (fin de segment)
|
v
Normalization volume (-16 LUFS)
|
v
Audiobook final: boule_de_suif_finalized.mp3
Utilise FFmpeg (disponible en local) pour la concatenation et le traitement audio.
withopen(TTS_METADATA, encoding='utf-8') as f: tts_meta = json.load(f)segments = [s for s in tts_meta['segments'] if s['status'] =='success']segments.sort(key=lambda s: s['seg_index'])print(f"Total segments: {len(segments)}")print(f"Total duration: {tts_meta['stats']['total_duration_s']:.1f}s")print()# Verify all files existmissing = []for seg in segments: seg_path = Path(seg['output_file'])ifnot seg_path.exists(): missing.append(seg_path.name)else: seg['file_size'] = seg_path.stat().st_sizeif missing:print(f"WARNING: {len(missing)} missing files: {missing}")else:print(f"All {len(segments)} segment files verified") total_size =sum(seg['file_size'] for seg in segments)print(f"Total size: {total_size /1024/1024:.1f} MB")
Total segments: 14
Total duration: 122.5s
All 14 segment files verified
Total size: 1.9 MB
Lecture du résultat : mesure du corpus segmenté
La cellule ci-dessus ouvre le manifeste produit par P4 (Kokoro TTS) pour vérifier l’intégrité du corpus de segments audio générés. Trois informations structurent cette lecture : le nombre total de segments (14, cohérent avec la segmentation de Maupassant réalisée en P1 — voir 04-8-Lecture-Analytique.ipynb), la durée totale (122.5 secondes ≈ 2 minutes) qui donne une idée immédiate de la taille de l’audiobook, et la taille disque cumulée (1.9 MB) qui permet d’anticiper les besoins de stockage et la bande passante de streaming. La vérification « All 14 segment files verified » confirme qu’aucun segment n’a été perdu entre P4 et P5 : c’est un garde-fou de robustesse essentiel, car un fichier manquant se traduirait par un silence parasite ou un crash de FFmpeg lors de la concaténation. Ce check d’intégrité est volontairement positionné en tête de pipeline pour échouer tôt — avant tout traitement long.
Concatenation FFmpeg
FFmpeg concat demuxer pour joindre les segments avec transitions : - Chaque segment recoit un fade in/out de 200ms - Un silence de 500ms est insere entre les segments - Le tout est mixe en un seul fichier MP3
SILENCE_MS =500FADE_MS =200TARGET_LUFS =-16.0def create_silence_ms(duration_ms, output_path, sample_rate=24000):"""Create a silent MP3 file of given duration.""" cmd = ['ffmpeg', '-y', '-f', 'lavfi','-i', f'anullsrc=r={sample_rate}:cl=mono','-t', str(duration_ms /1000),'-b:a', '128k',str(output_path) ] result = subprocess.run(cmd, capture_output=True, text=True)if result.returncode !=0:print(f"FFmpeg error: {result.stderr}")returnFalsereturnTrue# Generate silence filesilence_path = AUDIOBOOK_DIR /'silence_500ms.mp3'if create_silence_ms(SILENCE_MS, silence_path):print(f"Silence file created: {silence_path.name} ({silence_path.stat().st_size} bytes)")else:print("Failed to create silence file")# Build concat file listconcat_list_path = AUDIOBOOK_DIR /'concat_list.txt'withopen(concat_list_path, 'w', encoding='utf-8') as f:for i, seg inenumerate(segments): seg_path = Path(seg['output_file']).resolve() f.write(f"file '{seg_path}'\n")if i <len(segments) -1: f.write(f"file '{silence_path.resolve()}'\n")print(f"Concat list: {concat_list_path.name}")print(f"Entries: {len(segments)} segments + {len(segments)-1} silences")
Le resultat de la compilation (concatenation FFmpeg + normalisation -16 LUFS) est audible ci-dessous. Le player embarque l’audio en base64 : le fichier audiobook_final/boule_de_suif_finalized.mp3 est la sortie finale du pass P5.
from IPython.display import Audio, display# Lecture du resultat final : audiobook compile (MP3 128 kbps, -16 LUFS)display_audio_file(OUTPUT_FINAL)
Exercice : Paramètres de transition adaptes au type de segment
Duree estimee : 15 minutes
Objectif : Modifier le pipeline de concatenation pour utiliser des transitions différentes selon les types de segments adjacents. Un silence de 300 ms entre deux dialogues du même locuteur, 500 ms entre un dialogue et une narration, et 800 ms entre deux scenes différentes.
Contexte : Le silence uniforme de 500 ms ne reflete pas les variations naturelles de rythme narratif. Une transition trop courte entre deux personnages différents rend le dialogue confus, tandis qu’un silence trop long dans un monologue casse le rythme.
Étape 1 : Définir les règles de duree de silence selon les types de segments adjacents
Étape 2 : Generer des fichiers de silence de différentes durees (300ms, 500ms, 800ms)
Étape 3 : Construire la liste de concatenation avec les silences adaptes
Indice : La cle de transition = (type_seg_n, type_seg_n+1, locuteur_identique) Indice : Les segments sont tries par seg_index et contiennent le champ seg_type et speaker Indice : La liste de concatenation contient alternativement un segment et un silence
# TODO etudiant : transitions adaptees au type de segment# Etape 1 : Regles de duree de silenceTRANSITION_RULES = {} # TODO etudiant : {(type1, type2, same_speaker): duration_ms}# Etape 2 : Fonction de selection du silencedef get_transition_duration(seg_a, seg_b):"""Retourne la duree de silence adaptee entre deux segments consecutifs."""# TODO etudiant : utiliser TRANSITION_RULESreturn500# TODO etudiant# Etape 3 : Construire la liste de concatenation adapteedef build_adaptive_concat_list(segments_list, output_dir):"""Genere les fichiers de silence et la liste FFmpeg avec transitions adaptees."""# TODO etudiantreturn [] # TODO etudiant : liste de paths (segments + silences)print("Exercice a completer")
Exercice a completer
Verification qualite
Analyse du fichier final : duree, bitrate, volume RMS, detection clipping.
Pourquoi verifier apres concatenation
La concatenation FFmpeg + normalisation loudness peuvent introduire des artefacts invisibles a l’oreille lors d’un test rapide mais problematiques en production : (a) un MP3 final trop court ou trop long indique une perte ou duplication de segments au moment du concat ; (b) un bitrate ou sample-rate inattendu signale une mauvaise passe d’encodage ; (c) un volume RMS trop bas ou trop haut indique une normalisation loudness ratee ; (d) du clipping (peak > 0 dB) provoque une distorsion audibledans les passages forts. La verification ffprobe est l’etape de QA obligatoire avant diffusion.
Lecture des resultats (mesures verbatim issues de la cellule precedente)
Metrique
Valeur
Interpretation
Duree finale
128.9s (2.1 min)
Aligne avec l’estimation 129.0s (122.5s segments + ~6s silences)
Bitrate
128 kbps
Standard audiobook streaming (compromis qualite/taille)
Sample rate
48000 Hz
Upscale depuis 24 kHz Kokoro via resampling FFmpeg — preserve les harmoniques
Canaux
1 (mono)
Adapté narration voix (le stereo serait du gaspillage de bande passante)
Taille fichier
2.0 MB
Ratio ~15.5 KB/s cohérent avec MP3 128 kbps mono
Segments assembles
14/14
Aucun perdu au moment du concat
Silences inseres
13 × 500ms = 6.5s
Transitions uniformes entre chaque replique
Pourquoi -16 LUFS cible
Le standard de diffusion podcast/audiobook est -16 LUFS (Apple Podcasts, Spotify Reference, AES streaming recommendation). La normalisation loudnorm=I=-16:TP=-1.5:LRA=11 applique un double-pass qui (a) mesure l’integralite du fichier, (b) applique le gain adequat pour atteindre -16 LUFS sans depasser le True Peak -1.5 dBTP (evite clipping apres encodage), (c) preserve une Loudness Range de 11 LU (assez pour la variation entre dialogues et narration, pas trop pour eviter les sauts de volume audibles).
Suite logique
Le fichier final boule_de_suif_finalized.mp3 est valide pour distribution. Le pipeline est complet — la cellule suivante genere le manifest JSON de reference et l’Epic #1028 est close.
Lecture du résultat : caractérisation audio de l’audiobook final
La cellule ci-dessus inspecte les propriétés techniques du fichier MP3 final. La lecture de ces valeurs révèle plusieurs choix d’ingénierie : 128 kbps est le compromis standard entre qualité perceptible et taille fichier pour la voix humaine (un audiobook n’a pas besoin de 320 kbps) ; 48000 Hz est le produit d’un upsampling FFmpeg depuis les 24 kHz natifs de Kokoro — le tableau de la cellule de vérification qualité et le sample_rate=24000 du code des silences en témoignent. Ce choix mérite d’être interrogé : l’interpolation ne peut pas recréer d’information au-dessus de la limite de Nyquist de la source (12 kHz), et la justification « préserve les harmoniques » est discutable — 48 kHz reste toutefois le standard de distribution, ce qui évite un resampling côté lecteur ; mono (1 canal) est adapté à un audiobook narratif où la spatialisation n’apporte rien. Le LUFS cible -16.0 (Loudness Units Full Scale, recommandation EBU R128 pour la diffusion streaming) garantit que l’audiobook sera entendu à un volume cohérent par rapport aux autres podcasts sur la même plateforme — sans normalisation, l’auditeur subirait des sauts de volume audibles. La durée totale finale — 128.9 s ≈ 2.1 min — reste légèrement supérieure à la durée des seuls segments (122.5 s, mesurée en cell 3, cf. sorties commitées) par l’ajout des silences inter-segments (13 × 500 ms = 6.5 s), cohérent avec le design narratif.
Exercice : Calculer des metriques de qualite audio
Duree estimee : 15-20 minutes
Objectif : Implementer des fonctions qui calculent des metriques de qualite audio directement a partir du fichier MP3 : niveau RMS (volume moyen), peak (volume maximum), et dynamic range (ecart entre peak et RMS). Ces metriques permettent de verifier que l’audiobook est bien normalise.
Contexte : La normalisation FFmpeg vise -16 LUFS, mais il est utile de verifier le résultat en calculant le volume RMS reel du fichier final.
Étape 1 : Utiliser ffprobe pour extraire le volume RMS du fichier audio
Étape 2 : Calculer le dynamic range (peak - RMS) en dB
Étape 3 : Comparer les metriques avant et après normalisation (audiobook_raw vs audiobook_final)
Indice : ffmpeg -i input.mp3 -af “volumedetect” -f null /dev/null 2>&1 affiche mean_volume et max_volume Indice : Le dynamic range sain pour un audiobook est entre 10 et 20 dB Indice : Utilisez subprocess.run() pour executer ffprobe/ffmpeg depuis Python
# TODO etudiant : metriques de qualite audio# Etape 1 : Extraire le volume RMSdef measure_volume(filepath):"""Mesure le volume moyen (RMS) et peak d'un fichier audio via ffmpeg."""# TODO etudiant : utiliser ffmpeg -af volumedetectreturn {"mean_db": None, "max_db": None} # TODO etudiant# Etape 2 : Calculer le dynamic rangedef dynamic_range(mean_db, max_db):"""Calcule le dynamic range en dB."""# TODO etudiantreturnNone# TODO etudiant# Etape 3 : Comparaison avant/apres normalisation# TODO etudiant : mesurer OUTPUT_RAW et OUTPUT_FINAL, afficher le tableauprint("Exercice a completer")
Exercice a completer
Timeline des segments
Tableau chronologique avec duree cumulee.
Lecture de la timeline (mesures verbatim)
La timeline expose 14 segments ordonnes par seg_index avec cumul en secondes. Trois observations pedagogiques :
Concentration narrative sur bf_isabella : 9 segments sur 14 cumulent ~88s sur les ~122s totaux (cell 18, table timeline) — ~72% du temps total, soit ~64% du nombre de segments. Le narrateur domine le flux.
Pic de duree sur seg 4 (19.1s, narration bf_isabella) : c’est la description de la diligence qui part dans la neige — un passage descriptif dense typique de Maupassant qui sert de reveil auditif entre les dialogues.
Segments courts en queue : seg 9 (4.6s), seg 13 (7.8s) — dialogues brefs a fort pathos ou Boule de Suif repond aux voyageurs. Ces segments sont les plus sensibles a la prosodie expressive : un silence trop long casserait le rythme dramatique.
Pourquoi cette timeline sert la QA
Le cumul en secondes permet de verifier la continuite narrative : un saut abrupt entre deux segments (eg. 5s → 50s) indiquerait un segment orphelin omis du manifest ou un fichier audio vide. La timeline est donc un invariant structurel de l’audiobook — toute modification du pipeline P4 ou P5 doit la laisser monotone croissante.
L’ecart entre estimation et mesure releve de l’arrondi de l’encodeur MP3 sur les blocs de silence : FFmpeg insere un silence WAV pur (zeros), puis encode en MP3 avec un frame de duree finie, donc la duree exacte peut varier de quelques ms par silence. Sur 13 silences, l’ecart reste dans la tolerance d’un frame d’encodeur.
Garanties de la timeline cumulee
Le calcul cumul += dur utilise seg['duration_s'] (mesure WAV/MP3 parse par audio_duration_s de P4) — pas une estimation word-count. Si la generation Kokoro avait insere un silence anormal (eg. 2s entre mots), la timeline l’aurait capture avec une bosse visible. C’est un detecteur d’anomalies gratuit, sans cout computationnel.
Le manifest JSON contient toutes les metadonnees de l’audiobook final pour reference.
Pourquoi un manifest meme apres le MP3 final
Le MP3 final boule_de_suif_finalized.mp3 est le livrable mais il ne porte aucune métadonnée structurelle sur la composition : impossible de savoir depuis le MP3 seul combien de segments ont ete concatenes, combien de silences, quel pipeline a produit chaque portion. Le manifest JSON est le certificat de naissance de l’audiobook — il atteste de la composition et permet la tracabilite posterieure.
Structure du manifest (4 blocs)
epic, pass, description, source_text : meta de tracabilite (Epic #1028, Pass P5, source « Boule de Suif »). Meme format que tts_generation_metadata.json de P4 — facilite les scripts d’aggregation cross-pass.
pipeline : dictionnaire {P1: ..., P2: ..., P3: ..., P4: ..., P5: ...} listant les 5 passes du pipeline. Sert de table des matieres pour un lecteur qui ouvre le manifest sans connaitre l’Epic.
compilation : parametres de l’assemblage final (14 segments, 13 silences × 500ms, fades 200ms, -16 LUFS, format MP3 128k, fichier boule_de_suif_finalized.mp3, taille 2.0 MB). Ce bloc permet de rejouer la compilation si necessaire : on retrouve les constantes du pipeline.
segments : 14 entrees minimales {seg_index, seg_type, speaker, voice, duration_s} — sans output_file ni tags (inutiles ici, la concatenation est deja faite). C’est l’index de l’audiobook : un outil de chaptering peut iterer sur ce tableau pour generer les marqueurs de chapitres Audible.
Garanties du manifest
UTF-8 + ensure_ascii=False preserve les accents français (« Boule de Suif », « Elisabeth Rousset »).
indent=2 rend le fichier lisible dans un editeur de texte ou sur GitHub.
Le manifest exporte clot l’Epic #1028 : P1 (lecture analytique) → P2 (voice casting) → P3 (prosodie) → P4 (generation TTS) → P5 (compilation audio). La trace complete du pipeline est dans 4 manifests successifs (analytique_output/, voice_casting_output/, prosodic_output/, tts_output/tts_generation_metadata.json, audiobook_final/audiobook_manifest.json). C’est la chaine de custody du livrable audio.
Suite logique
L’Epic #1028 est close. Les notebooks P6+ (livraison, distribution, monetisation) restent a creer si l’objectif est la publication effective sur une plateforme audio.
manifest = {"epic": "1028","pass": "P5","description": "Final audiobook compilation","source_text": "Boule de Suif - Guy de Maupassant","pipeline": {"P1": "Lecture analytique","P2": "Voice casting","P3": "Annotation prosodique","P4": "Generation TTS (Kokoro)","P5": "Compilation audio (FFmpeg)", },"compilation": {"total_segments": len(segments),"silence_ms": SILENCE_MS,"fade_ms": FADE_MS,"target_lufs": TARGET_LUFS,"output_format": "mp3","output_bitrate": "128k","output_file": str(OUTPUT_FINAL.name),"output_size_mb": round(OUTPUT_FINAL.stat().st_size /1024/1024, 1), },"segments": [{"seg_index": s['seg_index'],"seg_type": s['seg_type'],"speaker": s['speaker'],"voice": s['kokoro_voice'],"duration_s": s['duration_s'], } for s in segments],}manifest_path = AUDIOBOOK_DIR /'audiobook_manifest.json'withopen(manifest_path, 'w', encoding='utf-8') as f: json.dump(manifest, f, ensure_ascii=False, indent=2)print(f"Manifest saved: {manifest_path.name}")print(f"Output file: {OUTPUT_FINAL}")print(f"Pipeline complete: P1 -> P2 -> P3 -> P4 -> P5")
Lecture du résultat : clôture du pipeline bout-en-bout
La cellule ci-dessus scelle l’assemblage en écrivant le manifest final de l’audiobook : un JSON qui catalogue le fichier produit, ses caractéristiques techniques, et trace la chaîne complète des cinq phases (P1 → P2 → P3 → P4 → P5) qui ont mené du texte brut de Maupassant au MP3 final lu par Kokoro. Cette trace est précieuse pour plusieurs raisons : (a) reproductibilité — le manifest permet de rejouer le pipeline à l’identique ; (b) débogage — si l’audiobook final sonne différemment d’une exécution à l’autre, le manifest identifie quelle phase a dérivé ; (c) audit qualité — les durées, tailles et LUFS documentées ici forment une baseline reproductible. La ligne « Pipeline complete: P1 -> P2 -> P3 -> P4 -> P5 » clôt la passe P5 de l’epic #1028 (GenAI Audiobook Agentique — Pipeline 5-pass) : l’epic reste ouverte sur ses autres axes, mais sa passe de compilation audio aboutit ici, et son manifeste JSON constitue le livrable principal consommable en aval (par un lecteur audio tiers, par une plateforme de podcast, ou par un outil d’analyse).
Exercice : Generer un manifest avec marqueurs de chapitres
Duree estimee : 15 minutes
Objectif : Enrichir le manifest JSON avec des marqueurs de chapitres calcules automatiquement. Les chapitres sont delimites par les segments de type “narration” longs (> 10 secondes) qui précédent un changement de type de segment. Le manifest enrichi inclut les timestamps de debut et fin de chaque chapitre.
Contexte : Les plateformes audio (Audible, Apple Podcasts) utilisent les marqueurs de chapitres pour la navigation. Ceux-ci doivent etre generes automatiquement a partir de la structure du texte.
Étape 1 : Identifier les frontieres de chapitres (narration longue suivie d’un dialogue)
Étape 2 : Calculer les timestamps cumules (en secondes) pour chaque frontiere
Étape 3 : Ajouter les chapitres au manifest et exporter en JSON
Indice : Un chapitre commence quand un segment narration (> 10s) est suivi d’un dialogue Indice : Les timestamps cumules = somme des durees des segments précédents + silences Indice : Le format chapter = {“title”: “Chapitre N”, “start_s”: float, “end_s”: float}
# TODO etudiant : manifest avec marqueurs de chapitresCHAPTER_THRESHOLD_S =10.0# Duree minimale d'un segment narration pour former un chapitre# Etape 1 : Identifier les frontieresdef detect_chapter_boundaries(segments_list, threshold_s=CHAPTER_THRESHOLD_S):"""Detecte les indices de debut de chapitre."""# TODO etudiantreturn [] # TODO etudiant# Etape 2 : Calculer les timestampsdef compute_chapter_timestamps(segments_list, boundaries, silence_ms=SILENCE_MS):"""Calcule les timestamps de debut/fin pour chaque chapitre."""# TODO etudiantreturn [] # TODO etudiant# Etape 3 : Enrichir le manifest# TODO etudiant : ajouter les chapitres au manifest et sauvegarderprint("Exercice a completer")
Exercice a completer
Chapitrage .m4b — le conteneur audiobook natif
Le compilateur de la chaine v4 (v4/p6_compile.py) ne produit pas seulement le .mp3 concatene ci-dessus : depuis #14224 il emet aussi un .m4b chapitre — le format conteneur natif des lecteurs d’audiobooks (Apple Books, BookPlayer, VLC), ou la navigation par chapitre est une primitive du lecteur, pas un signet pose a la main.
La mecanique, bouclee de bout en bout dans le module de production :
Positions d’actes — pendant la concatenation, la boucle enregistre act_start_ms[acte] = len(audiobook) au passage du premier segment de chaque acte (les quatre actes de Boule de Suif, issus de p3_dramatic_context.py) ;
FFMETADATA — ces timestamps deviennent un fichier FFMETADATA1 avec un bloc [CHAPTER] par acte (TIMEBASE 1/1000, START/END en ms, title) ;
Remux AAC — ffmpeg transcode le mp3 vers AAC dans le conteneur MPEG-4 avec -map_metadata 1 -map_chapters 1.
Cette section execute le vrai run() de production sur une fixture autonome — 8 segments de silence (2 par acte), meme topologie que le test d’integration du chapitrage — reproductible sur toute machine avec ffmpeg, sans aucun service GenAI. Le run complet sur le livre entier (385 paragraphes, 94,7 min, 4 chapitres reels) est documente dans #14059 et vit sur la machine de la chaine ; les outputs ci-dessous sont ceux de la fixture locale.
import jsonimport osimport subprocessimport sysimport warningsfrom pathlib import Path# Filtre cible : le schema pydantic de la v4 emet un UserWarning de shadowing dont le# prefixe contient le chemin absolu du module -- on l'exclut pour garder des sorties# relocalisables (il n'apporte rien a la demonstration du chapitrage).warnings.filterwarnings("ignore", message='Field name "register" in "VoiceReference"')# ffmpeg : build statique local (regle F -- on installe l'outil, on ne le contourne pas).# Sur une machine ou ffmpeg est deja dans le PATH, ce bloc est sans effet.FFMPEG_DIR = Path(os.environ.get("LOCALAPPDATA", "")) /"ffmpeg-local"/"ffmpeg-master-latest-win64-gpl"/"bin"if FFMPEG_DIR.exists(): os.environ["PATH"] =str(FFMPEG_DIR) + os.pathsep + os.environ["PATH"]FFMPEG ="ffmpeg"FFPROBE ="ffprobe"# Module de PRODUCTION v4/p6_compile.py (le chapitrage .m4b y vit depuis #14224).sys.path.insert(0, str(Path.cwd()))from v4 import p6_compilefrom v4.schemas import DramaticContext, DramaticContextBatch# Fixture autonome, reproductible sans aucun service GenAI : 8 segments de silence# (2 par acte), meme topologie que le test d'integration du chapitrage.FIX = Path("m4b_fixture")(FIX /"outputs").mkdir(parents=True, exist_ok=True)ACTS = ["act1_diligence_aller", "act2_auberge_jours", "act3_pressing_collectif", "act4_diligence_retour"]POSITIONS = ["exposition", "rising", "climax", "falling"]tts_results = []for i inrange(8): mp3 = FIX /f"seg_{i:02d}.mp3" subprocess.run( [FFMPEG, "-y", "-loglevel", "error", "-f", "lavfi","-i", "anullsrc=r=24000:cl=mono", "-t", "2", "-b:a", "128k", str(mp3)], check=True, ) tts_results.append({"seg_index": i, "mp3_path": str(mp3)})(FIX /"outputs"/"tts_results.json").write_text(json.dumps(tts_results), encoding="utf-8")contexts = [ DramaticContext( seg_index=i, act=ACTS[i //2], scene_label=f"Scene fixture {i //2+1}", tension_0_10=4, character_state={}, narrative_position=POSITIONS[i //2], )for i inrange(8)]batch = DramaticContextBatch(contexts=contexts)(FIX /"outputs"/"dramatic_context.json").write_text(batch.model_dump_json(), encoding="utf-8")print("Fixture prete : 8 segments (2 par acte), dramatic_context.json valide")# Execution du VRAI run() de production sur la fixture : BASE_DIR repointe vers FIX,# tout le reste (boucle act_start_ms, FFMETADATA, remux AAC) est le code de #14224.p6_compile.BASE_DIR = FIXmp3_out = p6_compile.run(force=True)print(f"Compile par le module de production : {mp3_out.name}")
Fixture prete : 8 segments (2 par acte), dramatic_context.json valide
[P6] Compiling audiobook...
Segments to compile: 8
Act boundary: act1_diligence_aller -> act2_auberge_jours at seg 2
Act boundary: act2_auberge_jours -> act3_pressing_collectif at seg 4
Act boundary: act3_pressing_collectif -> act4_diligence_retour at seg 6
[P6] m4b: m4b_fixture\outputs\boule_de_suif_v4.m4b (0.0 MB, 4 chapitres)
[P6] Done: m4b_fixture\outputs\boule_de_suif_v4.mp3
Compiled: 8, Skipped: 0
Duration: 24.2s (0.4min)
Size: 0.5 MB
Compile par le module de production : boule_de_suif_v4.mp3
Verification du chapitrage
ffprobe -show_chapters est l’organe de preuve : il lit les chapitres dans le conteneur produit — si le .m4b ne portait pas ses marqueurs, cette commande ne rendrait rien. On attend 4 chapitres, des timestamps de debut monotones, et les titres des quatre actes.
import jsonm4b = FIX /"outputs"/"boule_de_suif_v4.m4b"probe = subprocess.run( [FFPROBE, "-v", "error", "-show_chapters", "-of", "json", str(m4b)], capture_output=True, text=True, check=True,)chapters = json.loads(probe.stdout)["chapters"]print(f"{len(chapters)} chapitres dans {m4b.name} ({m4b.stat().st_size /1024:.0f} Ko) :")for ch in chapters:print(f" {ch['tags']['title']}: {float(ch['start_time']):.2f}s -> {float(ch['end_time']):.2f}s")titles = [ch["tags"]["title"] for ch in chapters]starts = [float(ch["start_time"]) for ch in chapters]ends = [float(ch["end_time"]) for ch in chapters]assertlen(chapters) ==4, f"attendu 4 actes, trouve {len(chapters)}"assert starts ==sorted(starts), "timestamps de debut non monotones"assertall(e > s for s, e inzip(starts, ends)), "chapitre a duree nulle"assert titles == ["Acte I -- La diligence de l'aller","Acte II -- L'auberge, les jours","Acte III -- Le pressing collectif","Acte IV -- La diligence du retour"], titlesprint("Verifie : les 4 actes sont des chapitres natifs du conteneur, monotones, a duree strictement positive.")
4 chapitres dans boule_de_suif_v4.m4b (6 Ko) :
Acte I -- La diligence de l'aller: 0.00s -> 5.30s
Acte II -- L'auberge, les jours: 5.30s -> 11.61s
Acte III -- Le pressing collectif: 11.61s -> 17.91s
Acte IV -- La diligence du retour: 17.91s -> 24.22s
Verifie : les 4 actes sont des chapitres natifs du conteneur, monotones, a duree strictement positive.
Lecture de la sortie
Les quatre actes sont des chapitres natifs du conteneur : un lecteur d’audiobooks proposera « Acte I — La diligence de l’aller » comme borne de navigation directe, avec le timescode exact capte pendant la concatenation (5,30 s, 11,61 s, 17,91 s — chaque frontiere d’acte herite aussi du silence d’1,5 s insere par la boucle). La duree totale du chapitre IV couvre exactement la fin de l’audiobook : le dernier bloc END est cale sur la duree compilee, pas sur une valeur arrondie.
Avec ce chapitrage, la case P5 de l’EPIC #1028 est livree de bout en bout : le script produit le conteneur (#14224), le companion le demontre, et la suspension du monitor d’idle pour les chaines longues est automatisee (#14415). Les objectifs de ce notebook couvraient « duree, clipping, niveaux » ; le conteneur ajoute la navigation — la derniere brique du « livre audio pour publics empches » que l’EPIC poursuit.
Conclusion — Epic #1028 Pipeline Complet
L’audiobook est compile. Pipeline 5-pass complet :
Pass
Description
Output
P1
Lecture analytique
analytique_output/
P2
Voice casting
voice_casting_output/
P3
Annotation prosodique
prosodic_output/
P4
Generation TTS
tts_output/
P5
Compilation audio
audiobook_final/
Production : En production, remplacer Kokoro par FishAudio S2-Pro pour une qualite superieure avec les 15000+ tags expressifs ([whisper], [shout], [cold], etc.).
Ameliorations futures : - Jingles/musique d’ambiance entre chapitres - Variation de vitesse par type de segment (narration vs dialogue) - Stereo spatial pour separer narrateur et personnages