diff --git a/translations/genai/audio.csv b/translations/genai/audio.csv index d88bb7d37d0..7da9fe51199 100644 --- a/translations/genai/audio.csv +++ b/translations/genai/audio.csv @@ -19542,9 +19542,9 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-1-Educational-Audio-Content.ipy - [ ] Créer des sous-titres synchronises - [ ] Ajouter une musique de fond discrete - [ ] Generer un PDF du cours avec transcription",,,,,,,,131728730a4e1888,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,182567ea,markdown,fr,e1e72dc6662dddfd,"# P3 - Annotation Prosodique pour TTS Agentique +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,182567ea,markdown,fr,a3780beb2b757b43,"# P3 - Annotation Prosodique pour TTS Agentique -**Navigation** : [Index](../README.md) | [<< Précédent](04-9-Voice-Casting.ipynb) | [Suivant >>](04-11-Generation-TTS.ipynb) +**Navigation** : [Index](../README.md) | [<< Précédent](../04-Voix/04-9-Voice-Casting.ipynb) | [Suivant >>](04-11-Generation-TTS.ipynb) **Epic #1028** | Pipeline 5-pass : Lecture analytique > Voice casting > **Annotation prosodique** > Generation TTS > Compilation audio @@ -19555,13 +19555,13 @@ Ce notebook enrichit les repliques identifiees en P1 avec des tags expressifs Fi 1. Charger les repliques (P1) et le voice casting (P2) 2. Analyser le contexte emotionnel de chaque replique 3. Assigner des tags expressifs FishAudio S2-Pro adaptes -4. Exporter le résultat en markdown enrichi (`book.ssml.md`) +4. Exporter les annotations : `prosodic_annotations.json` (contrat lu par P4) et `book.ssml.md` (livre de relecture) **Duree estimee** : 8-10 minutes *** -",,,,,,,,e1e72dc6662dddfd,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,2be00eeb,markdown,fr,811ee6e5c682c12e,"## 1. Configuration et imports +",,,,,,,,a3780beb2b757b43,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,2be00eeb,markdown,fr,9a434f7c22517d88,"## 1. Configuration et imports Le notebook **P3 — Annotation prosodique** prend les **résultats combinés de P1 (analyse textuelle) et P2 (voice casting)** et produit un **livre enrichi SSML** prêt à être synthétisé par P4 (TTS). C'est la passe qui transforme un texte brut en **partition d'acteur audio** : pour chaque réplique, le moteur choisit les tags expressifs FishAudio S2-Pro qui correspondent au contexte émotionnel. @@ -19587,30 +19587,142 @@ Les trois répertoires forment un **pipeline de fichiers en chaîne** : P3 ne fa Le retour `tuple` est intentionnel : P3 n'a besoin que de **4 objets ordonnés** (analysis, utterances, characters, segments), et le déballage positionnel `(analysis, utterances, characters, segments) = load_p1_data()` se lit clairement dans le code. Un `dict` ajouterait une indirection `p1[""utterances""]` sans gain de clarté. C'est le **complément exact du pattern dict utilisé en P2** : dict quand le nombre de clés est ouvert ou quand le nom sémantique aide la lecture, tuple quand le contrat est figé et ordonné. -### Mesures verbatim sur cette exécution +### Chemins affichés -L'output de cette cellule (verbatim, chemin Windows) : -- `P1 output dir: D:\Dev\CoursIA\MyIA.AI.Notebooks\GenAI\Audio\04-Applications\output_analytique` -- `P2 voice config: D:\Dev\CoursIA\MyIA.AI.Notebooks\GenAI\Audio\04-Applications\voice_casting_output\voice_casting_config.json` -- `P3 output dir: D:\Dev\CoursIA\MyIA.AI.Notebooks\GenAI\Audio\04-Applications\prosodic_output` - -Les chemins absolus dans l'output sont un **artefact d'environnement** (cwd au moment de l'exécution) — voir la règle `secrets-hygiene.md` §6 (Stop & Repair) : on **ne scrub pas** la sortie, on documente la cause (ici : exécution depuis la racine du repo `D:\Dev\CoursIA`). En pratique, les notebooks pédagogiques affichent ces paths comme **information de debug** pour le lecteur — pas comme secret.",,,,,,,,811ee6e5c682c12e,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,2171f375,code,fr,207252ef46727a77,"import json +La cellule affiche les chemins absolus des trois répertoires. Ils dépendent du dossier depuis lequel le notebook est exécuté : dans la version enregistrée, une copie locale du dépôt `jsboige/CoursIA`. Ce sont des informations de débogage, pas des secrets. +",,,,,,,,9a434f7c22517d88,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,2171f375,code,fr,a406de6c6f5d1f9d,"import json import csv import re +import os +import sys +import subprocess from pathlib import Path from dataclasses import dataclass, field, asdict from typing import Optional +# Les cles (OpenRouter/FishAudio) viennent d'un fichier .env NON versionne, comme +# dans 04-8 et 04-9 : aucun secret n'est ecrit dans ce carnet. +try: + from dotenv import load_dotenv + for _env_path in [Path(""../.env""), Path(""../../.env""), Path(""../../../.env"")]: + if _env_path.exists(): + load_dotenv(_env_path) + break +except ImportError: + pass + +# --- Choix du dossier de travail : Google Drive, ou depot GitHub jsboige/CoursIA ------------- +# On ne se contente pas de verifier qu'un dossier existe : on verifie que les FICHIERS dont le +# notebook a besoin y sont (sorties de P1 et P2, contrat P1, instrument v4). Le premier complet gagne. +DOSSIER_DRIVE = Path(""/content/drive/MyDrive/04-Applications"") # adapte le chemin a ton Drive +DEPOT_URL = ""https://github.com/jsboige/CoursIA.git"" +DEPOT_SOUS_DOSSIER = ""MyIA.AI.Notebooks/GenAI/Audio/04-Applications"" +DEPOT_COLAB = Path(""/content/CoursIA"") + +FICHIERS_REQUIS = [ + ""output_analytique/analyse_boule_de_suif.json"", # P1 (04-8) + ""output_analytique/repliques.csv"", + ""output_analytique/personnages.csv"", + ""output_analytique/segments.csv"", + ""output_analytique/p1_contract.json"", # manifeste de provenance P1 + ""output_analytique/characters.json"", # contrat P1 (compte de reference) + ""output_analytique/utterances.csv"", # contrat P1 (compte de reference) + ""voice_casting_output/voice_casting_config.json"", # P2 (04-9) + ""v4/prosody_lab/syllable_pitch.py"", # instrument de mesure (section 10) + ""v4/prosody_lab/prosody_metrics.py"", +] + +EN_COLAB = ""google.colab"" in sys.modules or bool(os.getenv(""COLAB_RELEASE_TAG"")) + +def manquants(dossier): + """"""Liste des fichiers requis absents de `dossier` (tous si le dossier n'existe pas)."""""" + return [f for f in FICHIERS_REQUIS if not (dossier / f).is_file()] + +def candidat_drive(): + if not EN_COLAB: + return None + try: + from google.colab import drive + if not Path(""/content/drive/MyDrive"").exists(): + drive.mount(""/content/drive"") + return DOSSIER_DRIVE + except Exception as e: # montage refuse ou impossible + print(f""Drive : montage impossible ({type(e).__name__}: {e})"") + return None + +def candidat_depot_local(): + """"""Notebook lance depuis un clone du depot (dossier courant ou un de ses parents)."""""" + ici = Path.cwd() + if ici.name == ""04-Applications"": + return ici + for parent in [ici, *ici.parents]: + if (parent / DEPOT_SOUS_DOSSIER).is_dir(): + return parent / DEPOT_SOUS_DOSSIER + return None + +def candidat_depot_colab(): + """"""Dans Colab, clone partiel du seul dossier 04-Applications (rapide, sans tout le depot)."""""" + if not EN_COLAB: + return None + if not (DEPOT_COLAB / "".git"").exists(): + try: + subprocess.run([""git"", ""clone"", ""-q"", ""--depth"", ""1"", ""--filter=blob:none"", ""--sparse"", + DEPOT_URL, str(DEPOT_COLAB)], check=True, timeout=300) + subprocess.run([""git"", ""-C"", str(DEPOT_COLAB), ""sparse-checkout"", ""set"", DEPOT_SOUS_DOSSIER], + check=True, timeout=300) + except Exception as e: # reseau, git absent, delai depasse + print(f""Depot GitHub : clonage impossible ({type(e).__name__})"") + return None + return DEPOT_COLAB / DEPOT_SOUS_DOSSIER + +TRAVAIL_DIR, bilan = None, [] +for nom, fabrique in [(""Google Drive"", candidat_drive), + (""dossier courant ou clone local"", candidat_depot_local), + (""depot GitHub (clone Colab)"", candidat_depot_colab)]: + dossier = fabrique() + if dossier is None: + bilan.append((nom, ""absent"", [])) + continue + absents = manquants(dossier) + bilan.append((nom, str(dossier), absents)) + if not absents: + TRAVAIL_DIR = dossier + break + +print(""Test des emplacements :"") +for nom, dossier, absents in bilan: + if dossier == ""absent"": + etat, libelle = ""absent ici"", ""-"" + else: + etat = ""OK"" if not absents else f""{len(absents)} fichier(s) manquant(s)"" + libelle = Path(dossier).name # jamais un chemin machine dans une sortie + print(f"" - {nom:32} {etat:28} {libelle}"") + for f in absents: + print(f"" manque : {f}"") + +if TRAVAIL_DIR is None: + raise FileNotFoundError( + ""Aucun emplacement complet. Le depot GitHub ne versionne ni les sorties de P1/P2 "" + ""ni le manifeste p1_contract.json : executer d'abord 04-8 (output_analytique/) puis "" + ""04-9 (voice_casting_output/) dans ce dossier, ou copier ces fichiers dans "" + f""{DOSSIER_DRIVE}."") + +os.chdir(TRAVAIL_DIR) # le reste du notebook utilise des chemins relatifs a ce dossier +print(f""\nDossier de travail retenu : {Path(TRAVAIL_DIR).name}"") + +if EN_COLAB: + subprocess.run([sys.executable, ""-m"", ""pip"", ""install"", ""-q"", ""librosa==0.11.0"", ""faster-whisper""], check=False) + BASE_DIR = Path(""output_analytique"") VOICE_CONFIG = Path(""voice_casting_output/voice_casting_config.json"") OUTPUT_DIR = Path(""prosodic_output"") OUTPUT_DIR.mkdir(exist_ok=True) -print(f""P1 output dir: {BASE_DIR.resolve()}"") -print(f""P2 voice config: {VOICE_CONFIG.resolve()}"") -print(f""P3 output dir: {OUTPUT_DIR.resolve()}"")",,,,,,,,207252ef46727a77,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,13f34b01,markdown,fr,4a245d79cfa5548a,"## 2. Tags expressifs FishAudio S2-Pro +print(f""P1 output dir: {BASE_DIR}"") +print(f""P2 voice config: {VOICE_CONFIG}"") +print(f""P3 output dir: {OUTPUT_DIR}"")",,,,,,,,a406de6c6f5d1f9d,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,13f34b01,markdown,fr,4a245d79cfa5548a,"## 2. Tags expressifs FishAudio S2-Pro FishAudio S2-Pro supporte des tags expressifs inseres directement dans le texte entre crochets. Contrairement au SSML standard (limites a ``, ``, ``), les tags FishAudio sont beaucoup plus riches : @@ -19624,7 +19736,7 @@ FishAudio S2-Pro supporte des tags expressifs inseres directement dans le texte | **Paralinguistique** | `[yawn]`, `[cough]`, `[sniff]`, `[giggle]` | Sons non-verbaux | La stratégie d'annotation combine analyse du texte (mots-clés, ponctuation) et contexte narratif (rôle du personnage, type de segment).",,,,,,,,4a245d79cfa5548a,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,944d96d0,code,fr,95e29509c6270aa8,"# FishAudio S2-Pro expressive tag taxonomy +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,944d96d0,code,fr,95e29509c6270aa8,"# FishAudio S2-Pro expressive tag taxonomy # Organized by category for programmatic annotation FISHAUDIO_TAGS = { @@ -19664,43 +19776,65 @@ FISHAUDIO_TAGS = { print(f""Taxonomie FishAudio : {sum(len(v) for v in FISHAUDIO_TAGS.values())} tags en {len(FISHAUDIO_TAGS)} categories"") for cat, tags in FISHAUDIO_TAGS.items(): print(f"" {cat}: {', '.join(tags.keys())}"")",,,,,,,,95e29509c6270aa8,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,2cb776e2,markdown,fr,13dfc3811d5de7d8,"## 3. Chargement des données P1 et P2 +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,2cb776e2,markdown,fr,20fabcaa7d9188f7,"## 3. Chargement des données P1 et P2 -Cette étape **consomme les artefacts des deux passes précédentes** (P1 analyse textuelle + P2 voice casting) pour reconstituer le **graphe narratif annoté** : 14 segments P1 dont 9 répliques de personnages + 5 segments de narration/description, et 8 personnages castés. +Cette étape **consomme les artefacts des deux passes précédentes** (P1 analyse textuelle + P2 voice casting) pour reconstituer le **graphe narratif annoté**. -### Mesures observées verbatim sur cette exécution +### Mesures de cette exécution -L'output de la cellule code ci-dessous donne : +P1 et P2 ont été régénérés en exécutant les carnets 04-8 et 04-9 du dépôt. Les dossiers `output_analytique/` et `voice_casting_output/` ne sont pas versionnés ; c'est le **manifeste `p1_contract.json`** (version 1.0, produit par 04-8) qui atteste la provenance du corpus chargé, et non la présence d'un fichier au bon nom. | Donnée | Source | Valeur mesurée | |--------|--------|----------------| -| Segments P1 | `segments.csv` | **14** segments narratifs atomiques | -| Répliques P1 | `repliques.csv` | **9** utterances de personnages | -| Personnages P1 | `personnages.csv` | **8** personnages détectés | -| Personnages P2 | `voice_casting_config.json` | **8** personnages castés (cohérent avec P1) | +| Segments P1 | `segments.csv` | **342** segments : 302 de narration, 39 de dialogue, 1 de description | +| Répliques P1 | `repliques.csv` | **39** répliques | +| Personnages P1 | `personnages.csv` | **9** personnages | +| Contrat P1 | `p1_contract.json` | version 1.0, source *Boule de Suif* (Gutenberg #10746, texte 83 097 caractères) | +| Personnages P2 | `voice_casting_config.json` | **9** personnages castés (cohérent avec P1) | -L'écart **14 segments vs 9 utterances** est structurel : sur les 14 segments P1, 9 sont des **dialogues** (un personnage parle) et 5 sont de la **narration/description** (le narrateur raconte l'action). Cette répartition sera exploitée plus loin par `annotate_segment()` qui ne traite que les 5 segments non-dialogue. +La cellule ci-dessous confronte les deux côtés : le nombre de lignes **chargées** et le nombre **annoncé** par le manifeste. Un écart lève avec les deux nombres et le remède exact, au lieu de laisser un corpus étranger passer en silence — l'incident d'origine était un lot de 14 segments d'une génération antérieure consommé sans avertissement. -### Aperçu des 5 premières répliques verbatim +**Ce qui a changé depuis la version précédente de ce notebook.** Les sorties enregistrées auparavant décrivaient un extrait de démonstration (14 segments, 9 répliques, 8 personnages) **sans accents**, dont les répliques contenaient leur incise (« murmura Boule de Suif »). Le P1 actuel analyse le conte entier **avec ses accents**, et sépare la réplique de la phrase narrative qui l'introduit (« Boule de Suif répondit sèchement : » est un segment narratif distinct). Ces deux changements expliquent une partie des résultats de la suite : +- la taxonomie, écrite sans accents, rate des mots accentués ; +- les verbes de parole ne sont plus dans le texte des répliques. -L'output affiche la tête des répliques avec speaker + texte : +L'exemple guidé de la section 4 traite ces deux points. -- **[Narrateur]** « Et ils ne font rien de mal aux gens ? demanda timidement une grosse dame, la comte... » (seg 2) -- **[Cornudet]** « Rassurez-vous, madame, dit Cornudet avec un sourire, ils sont corrects... » (seg 3) -- **[Elisabeth Rousset]** « Mon Dieu ! murmura Boule de Suif, je n'ai rien a manger. Je suis partie sans pre... » (seg 5) -- **[Comtesse de Breville]** « Prenez ceci, ma chere demoiselle, dit la comtesse en tendant un morcea... » (seg 6) -- **[Narrateur]** « Merci, madame, vous etes bien bonne, repondit Elisabeth en rougissant... » (seg 7) - -Ces 5 répliques montrent la **palette émotionnelle du texte** : la timidité de la dame (seg 2), l'ironie diplomatique de Cornudet (seg 3), la misère de Boule de Suif (seg 5), la charité glaciale de la Comtesse (seg 6), la gratitude rougissante (seg 7). Ce sont les 5 actes que le moteur d'annotation devra marquer avec des tags FishAudio. +**Attribution des répliques.** 21 des 39 répliques sont attribuées au Narrateur, c'est-à-dire que P1 n'a pas identifié le personnage qui parle, et d'autres sont attribuées au mauvais personnage. Une émotion bien détectée peut donc être portée par la mauvaise voix. La section 6 bis corrige en P3 ce que la phrase d'introduction de chaque réplique permet de corriger, signale les répliques non résolues et liste les corrections à reporter en P1. ### Pourquoi `csv.DictReader` et pas `pandas.read_csv` -P3 manipule des **données structurées simples** (4 fichiers CSV plats, ~14 lignes chacun). `pandas.DataFrame` serait overkill pour 9 répliques — overhead mémoire +50× et dépendance dure. `csv.DictReader` retourne des `OrderedDict` directement utilisables comme `utterance[""speaker""]` / `utterance[""text""]`. C'est aussi plus **lisible pour un étudiant** qui voit la structure de données sans abstraction supplémentaire. +P3 manipule des **données structurées simples** (des fichiers CSV plats). `pandas.DataFrame` serait overkill pour des dizaines de lignes — overhead mémoire et dépendance dure. `csv.DictReader` retourne des `OrderedDict` directement utilisables comme `utterance[""speaker""]` / `utterance[""text""]`. C'est aussi plus **lisible pour un étudiant** qui voit la structure de données sans abstraction supplémentaire. ### Pourquoi `tuple` de retour, pas `dict` -Le retour `(analysis, utterances, characters, segments)` est positionnel et stable : `analysis` (json brut), puis les 3 CSV. L'appelant déballe en 4 variables, et Python sait à la compilation que c'est un `tuple[dict, list[dict], list[dict], list[dict]]`. Un `dict` aurait ajouté 4 niveaux d'indirection (`p1[""segments""]`) pour 0 gain de clarté. Pattern inverse à P2 où `dict` était préféré pour sa flexibilité — voir cell[1] ci-dessus pour la justification détaillée.",,,,,,,,13dfc3811d5de7d8,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,8afb14e7,code,fr,f132c068a88d7907,"def load_p1_data() -> tuple: +Le retour `(analysis, utterances, characters, segments)` est positionnel et stable : `analysis` (json brut), puis les 3 CSV. L'appelant déballe en 4 variables, et Python sait à la compilation que c'est un `tuple[dict, list[dict], list[dict], list[dict]]`. Un `dict` aurait ajouté 4 niveaux d'indirection (`p1[""segments""]`) pour 0 gain de clarté. Pattern inverse à P2 où `dict` était préféré pour sa flexibilité — voir cell[1] ci-dessus pour la justification détaillée.",,,,,,,,20fabcaa7d9188f7,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,8afb14e7,code,fr,a2d9756a8d9ed618,"# Contrat P1 canonique v1.0 -- MEME organe que 04-9 (manifeste p1_contract.json). +# Le manifeste est la reference de provenance : il nomme le producteur, la +# version et les comptes attendus. Sans lui, rien n'atteste la generation des +# fichiers charges -- des artefacts d'une generation anterieure ont deja ete +# consommes ici en silence (14 segments au lieu des 342 annonces). +P1_CONTRACT = BASE_DIR / ""p1_contract.json"" + + +def load_p1_contract() -> dict: + if not P1_CONTRACT.exists(): + raise RuntimeError( + f""[CONTRAT P1] {P1_CONTRACT} introuvable : executer "" + f""04-8-Lecture-Analytique.ipynb dans ce dossier avant 04-10."" + ) + contract = json.loads(P1_CONTRACT.read_text(encoding=""utf-8"")) + source = contract.get(""source"", {}) + print(f""[CONTRAT P1 v{contract.get('version', '?')}] produit par "" + f""{contract.get('producer', '?')} | source : {source.get('title', '?')} "" + f""(Gutenberg #{source.get('gutenberg_id', '?')}, "" + f""texte {source.get('text_length', '?')} car.)"") + return contract + + +def load_p1_data() -> tuple: + # Charger P1 selon le contrat canonique v1 (aucun artefact de provenance inconnue). + contract = load_p1_contract() # Load P1 analysis results with open(BASE_DIR / ""analyse_boule_de_suif.json"", encoding=""utf-8"") as f: analysis = json.load(f) @@ -19726,6 +19860,52 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb, for row in reader: segments.append(row) + # Provenance : le manifeste decrit les fichiers du CONTRAT ; 04-10 charge + # les fichiers LEGACY. On mesure les deux cotes et on exige l'accord, sinon + # un corpus etranger passe en silence -- c'est ce qui s'est produit (sorties + # committees : 14 segments, 9 repliques, 8 personnages, contre 342 / 39 / 9). + def _count_rows(name: str) -> int: + with open(BASE_DIR / name, encoding=""utf-8"") as fh: + return sum(1 for _ in csv.DictReader(fh)) + + chars_path = BASE_DIR / ""characters.json"" + if not chars_path.exists(): + raise RuntimeError( + f""[CONTRAT P1] {chars_path} introuvable : executer "" + f""04-8-Lecture-Analytique.ipynb dans ce dossier avant 04-10."" + ) + contract_chars = json.loads(chars_path.read_text(encoding=""utf-8"")) + expected = contract.get(""files"", {}) + # (fichier du contrat, fichier legacy charge par ce carnet, legacy, contrat) + pairs = [ + (""characters.json"", ""personnages.csv"", len(characters), len(contract_chars)), + (""segments.csv"", ""segments.csv"", len(segments), expected.get(""segments.csv"")), + (""utterances.csv"", ""repliques.csv"", len(utterances), _count_rows(""utterances.csv"")), + ] + problems = [] + for contract_file, legacy_file, legacy_n, contract_n in pairs: + announced = expected.get(contract_file) + if announced != contract_n: + problems.append( + f""{contract_file} porte {contract_n} entree(s) sur disque "" + f""contre {announced if announced is not None else '?'} annonce(s)"" + ) + if legacy_n != contract_n: + problems.append( + f""{legacy_file} (charge par ce carnet) porte {legacy_n} entree(s) "" + f""contre {contract_file} = {contract_n}"" + ) + if problems: + raise RuntimeError( + ""[CONTRAT P1] Corpus incoherent dans "" + f""{BASE_DIR} : {' ; '.join(problems)}. "" + ""Re-executer 04-8-Lecture-Analytique.ipynb dans ce dossier."" + ) + for contract_file, legacy_file, legacy_n, contract_n in pairs: + print(f""{contract_file:16s} {contract_n:4d} entree(s) "" + f""(manifeste : {expected.get(contract_file):4d}) | "" + f""{legacy_file:16s} {legacy_n:4d} chargee(s)"") + return analysis, utterances, characters, segments @@ -19742,8 +19922,8 @@ print(f""P1 - Segments: {len(segments)}, Repliques: {len(utterances)}, Personnag print(f""P2 - Personnages castes: {len(voice_config['characters'])}"") print(f""\nPremieres repliques:"") for u in utterances[:5]: - print(f"" [{u['speaker']}] {u['text'][:70]}..."")",,,,,,,,f132c068a88d7907,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,7f85c58c,markdown,fr,8d9dbb1130edc80d,"## 4. Moteur d'annotation prosodique + print(f"" [{u['speaker']}] {u['text'][:70]}..."")",,,,,,,,a2d9756a8d9ed618,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,7f85c58c,markdown,fr,ab415c33c75ef0a9,"## 4. Moteur d'annotation prosodique Le moteur d'annotation fonctionne en trois étapes pour chaque replique : @@ -19751,10 +19931,13 @@ Le moteur d'annotation fonctionne en trois étapes pour chaque replique : 2. **Contexte narratif** : Rôle du personnage, type de segment (dialogue/narration), position dans l'intrigue 3. **Sélection des tags** : Choix des tags FishAudio les plus adapttes, avec positionnement dans le texte -Les tags sont inseres aux endroits stratégiques du texte (debut de phrase, pauses naturelies, changements emotionnels).",,,,,,,,8d9dbb1130edc80d,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,cb0e3b41,code,fr,b7427294519d1d86,"@dataclass +Les tags sont inseres aux endroits stratégiques du texte (debut de phrase, pauses naturelies, changements emotionnels). + +> **Statut de ce moteur.** Les sections 4 à 6 présentent le **moteur d'origine (v1)**. Il est conservé comme référence de comparaison, mais ce n'est **pas lui qui est exporté**. Le moteur exporté est le **moteur retenu (v2)**, défini à la section 6 bis à partir des corrections validées par les exercices. Deux corrections sont toutefois appliquées dès cette section, parce qu'elles ne changent pas les règles de détection : l'ordre des tags est stable, et la correspondance entre noms de personnages et identifiants s'appuie sur les identifiants communs à P1 et P2 au lieu d'une table codée en dur. +",,,,,,,,ab415c33c75ef0a9,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,cb0e3b41,code,fr,bb360577d57d8f21,"@dataclass class ProsodicAnnotation: - # A single utterance with prosodic annotation + # A single utterance with prosodic annotation (moteur d'origine v1, conservé pour comparaison) seg_index: int speaker: str original_text: str @@ -19765,6 +19948,10 @@ class ProsodicAnnotation: utterance_type: str = ""dialogue"" +# Ordre de référence des tags : celui de la taxonomie (émotions, intensité, ton, rythme, souffle) +ORDRE_TAXONOMIE = [tag for categorie in FISHAUDIO_TAGS.values() for tag in categorie] + + def detect_emotion_tags(text: str) -> list: # Detect FishAudio tags from text content and stage directions text_lower = text.lower() @@ -19786,50 +19973,73 @@ def detect_emotion_tags(text: str) -> list: if ""pause"" not in detected: detected.append(""pause"") - return list(set(detected)) + # Ordre STABLE : la version initiale renvoyait list(set(detected)), dont l'ordre varie d'un + # processus Python à l'autre (hachage aléatoire des chaînes), alors que le premier tag est + # celui placé en tête du texte. Les règles de détection, elles, sont inchangées. + rang = {tag: i for i, tag in enumerate(ORDRE_TAXONOMIE)} + return sorted(set(detected), key=lambda tag: rang.get(tag, len(rang))) + + +# --- Correspondance des personnages : identifiants COMMUNS à P1 et P2 --------------------- +# La version initiale utilisait une table codée en dur, qui contenait ""elisabeth_rousset"" +# (absent de P1 et de P2, dont l'identifiant est ""boule_de_suif"") et ignorait ""Mme Loiseau"" : +# ces deux personnages recevaient en silence la voix du narrateur. On part désormais des +# identifiants de personnages.csv (P1), vérifiés contre voice_casting_config.json (P2). +import unicodedata as _ud +from collections import Counter + +def _cle_nom(nom): + nom = """".join(c for c in _ud.normalize(""NFD"", nom) if _ud.category(c) != ""Mn"") + return "" "".join(nom.lower().replace(""-"", "" "").split()) + +NARRATEUR_ID = ""narrateur"" +PRESET_NARRATEUR_DEFAUT = ""narrator_male"" # même valeur par défaut que annotate_segment et que P4 +ID_PAR_NOM = {_cle_nom(c[""full_name""]): c[""id""] for c in characters} +IDS_P1 = {c[""id""] for c in characters} +IDS_P2 = set(voice_config[""characters""]) +PRESET_NARRATEUR = (voice_config[""characters""].get(NARRATEUR_ID, {}).get(""voices"", {}) + .get(""fishaudio_production"", {}).get(""preset"", PRESET_NARRATEUR_DEFAUT)) def get_speaker_profile(speaker: str, characters: list, voice_config: dict) -> dict: - # Get character profile from P1 data and P2 voice casting - # Map speaker names to character IDs - speaker_map = { - ""Elisabeth Rousset"": ""elisabeth_rousset"", - ""Boule de Suif"": ""elisabeth_rousset"", - ""Cornudet"": ""cornudet"", - ""Comtesse de Breville"": ""comtesse"", - ""Comte Hubert de Breville"": ""comte"", - ""Narrateur"": ""narrateur"", - ""Monsieur Loiseau"": ""loiseau"", - ""Loiseau"": ""loiseau"", - ""Monsieur Carre-Lamadon"": ""carre_lamadon"", - ""Les bonnes soeurs"": ""soeurs"", - ""Officier prussien"": ""officier"", - } - char_id = speaker_map.get(speaker, ""narrateur"") + """"""Profil vocal d'un locuteur à partir des identifiants communs P1/P2. + resolution : ""p1"" (nom reconnu dans P1), ""narrateur"" (P1 n'a pas identifié qui parle), + ""inconnu"" (nom absent de P1 : signalé, jamais converti en narrateur sans le dire)."""""" + cle = _cle_nom(speaker) + if cle == NARRATEUR_ID: + char_id, resolution = NARRATEUR_ID, ""narrateur"" + elif cle in ID_PAR_NOM: + char_id, resolution = ID_PAR_NOM[cle], ""p1"" + else: + char_id, resolution = None, ""inconnu"" + + if char_id == NARRATEUR_ID: + role = ""narrateur"" + else: + role = next((c[""role""] for c in characters if c[""id""] == char_id), ""figurant"") - # Get voice preset from P2 config - fish_preset = ""neutral"" if char_id in voice_config.get(""characters"", {}): - fish_info = voice_config[""characters""][char_id][""voices""][""fishaudio_production""] - fish_preset = fish_info[""preset""] - - # Get role from P1 data - role = ""figurant"" - for c in characters: - if c[""id""] == char_id: - role = c[""role""] - break + fish_preset = voice_config[""characters""][char_id][""voices""][""fishaudio_production""][""preset""] + elif char_id == NARRATEUR_ID: + fish_preset = PRESET_NARRATEUR + else: + fish_preset = ""neutral"" - return { - ""char_id"": char_id, - ""role"": role, - ""fish_preset"": fish_preset, - } + return {""char_id"": char_id, ""role"": role, ""fish_preset"": fish_preset, ""resolution"": resolution} print(""Moteur d'annotation prosodique defini."") -print(f""Taxonomie : {sum(len(v) for v in FISHAUDIO_TAGS.values())} cles emotionnelles"")",,,,,,,,b7427294519d1d86,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,727cc210,markdown,fr,681f7660c100d63b,"### Exercice : Detecteur d'emotion par ponctuation +print(f""Taxonomie : {sum(len(v) for v in FISHAUDIO_TAGS.values())} cles emotionnelles"") +print(f""\nIdentifiants P1 = P2 : {IDS_P1 == IDS_P2} ({len(IDS_P1)} personnages)"" + + ("""" if IDS_P1 == IDS_P2 else f"" | P1 seul : {sorted(IDS_P1 - IDS_P2)} | P2 seul : {sorted(IDS_P2 - IDS_P1)}"")) +print(f""Narrateur : {'présent' if NARRATEUR_ID in IDS_P2 else 'absent'} de P2 -> preset {PRESET_NARRATEUR!r}"" + + ("""" if NARRATEUR_ID in IDS_P2 else "" (valeur par défaut, la même que P4)"")) +print(""\nLocuteurs des répliques P1 :"") +for nom, n in Counter(u[""speaker""] for u in utterances).most_common(): + p = get_speaker_profile(nom, characters, voice_config) + print(f"" {nom:26} x{n:2d} -> {str(p['char_id']):14} ({p['resolution']}, preset {p['fish_preset']})"") +",,,,,,,,bb360577d57d8f21,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,727cc210,markdown,fr,672e471bdb217925,"### Exemple guidé : détecteur d'émotion par ponctuation (groupe G17, PR #23242) **Duree estimee :** 10-15 minutes @@ -19842,33 +20052,182 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb, - **Étape 3 :** Integrer dans detect_emotion_tags et tester sur les repliques existantes **Indice :** re.search(r'[A-Z]{3,}', text) detecte les suites de majuscules -**Indice :** text.count('...') > 1 indique des hesitations repetees",,,,,,,,681f7660c100d63b,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,f3a6bb19,code,fr,cfb03d44f8a34a6e,"# TODO etudiant : detecteur d'emotion par ponctuation +**Indice :** text.count('...') > 1 indique des hesitations repetees",,,,,,,,672e471bdb217925,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,f3a6bb19,code,fr,9b662cce69da1f76," # Exemple guidé — résolution du groupe G17 (Killian Barret), PR #23242, revue et intégrée. +# Exercice : détecteur d'émotion par ponctuation (et marquage robuste au français réel) import re +import unicodedata +from collections import Counter -# Etape 1 : Detection des majuscules sostenues -def detect_caps_shouting(text): - """"""Detecte si le texte contient des mots en majuscules (cris/colere)."""""" - # TODO etudiant - return False # TODO etudiant +# --- Normalisation commune --------------------------------------------------------- +# La taxonomie est écrite SANS accents (""colere"", ""stupefait"", ""indigne"") alors que le texte +# P1 actuel est accentué (""colère"", ""stupéfait"", ""indignée"") : une comparaison directe rate ces +# mots. On compare donc les deux sous une forme commune : minuscules, sans diacritiques, +# apostrophe typographique unifiée, retours à la ligne remplacés par des espaces. +# Cette forme garde la longueur du texte (une lettre accentuée précomposée reste une lettre), +# ce qui permet de reporter une position trouvée dans le texte normalisé sur le texte d'origine. +def normaliser_fr(texte): + texte = texte.replace(""’"", ""'"").replace(""\n"", "" "").replace(""\t"", "" "") + texte = unicodedata.normalize(""NFC"", texte) + texte = """".join(unicodedata.normalize(""NFD"", c)[0] for c in texte) + return texte.lower() -# Etape 2 : Detection des hesitations multiples -def detect_hesitation(text): - """"""Detecte les hesitations via les points de suspension repetes."""""" - # TODO etudiant - return False # TODO etudiant +# --- Étape 1 : majuscules soutenues ------------------------------------------------- +ACRONYMES = {""NASA"", ""SNCF"", ""URSS"", ""OTAN""} # sigles : des capitales qui ne sont pas des cris -# Etape 3 : Version etendue de detect_emotion_tags -def detect_emotion_tags_v2(text): - """"""Version etendue avec detection par ponctuation."""""" - # TODO etudiant : appeler detect_emotion_tags + ajouter les nouvelles detections - return [] # TODO etudiant +def detect_caps_shouting(text): + """"""Vrai si le texte contient un mot de plus de 3 lettres entièrement en capitales. + - Lettres Unicode : « ÉCOUTEZ » compte en entier (la regex [A-Z] n'en voit que « COUTEZ »). + - Les sigles connus ne sont pas des cris. + - Un titre (« BOULE DE SUIF ») n'a ni minuscule ni ponctuation expressive : ce n'est pas un cri."""""" + mots = [m for m in re.findall(r""[^\W\d_]+"", text) + if len(m) > 3 and m.isupper() and m not in ACRONYMES] + if not mots: + return False + est_titre = not any(c.islower() for c in text) and not re.search(r""[!?]"", text) + return not est_titre + +# --- Étape 2 : points de suspension ----------------------------------------------- +# « ... » (trois points) et « … » (caractère unique U+2026, courant en typographie française) +# sont la même marque. Quatre points ou plus, ou « …… », signalent une longue pause. +RE_SUSPENSION = re.compile(r""\.{3,}|…+"") -# Test sur les repliques existantes -# TODO etudiant : appliquer detect_emotion_tags_v2 aux utterances +def compter_suspensions(text): + """"""Renvoie (nombre de marques de suspension, nombre de longues pauses)."""""" + marques = RE_SUSPENSION.findall(text) + longues = [m for m in marques if m.count(""."") >= 4 or m.count(""…"") >= 2] + return len(marques), len(longues) -print(""Exercice a completer"")",,,,,,,,cfb03d44f8a34a6e,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,7fd27198,code,fr,c9d0867d3b6f992a,"def annotate_utterance(utterance: dict, characters: list, voice_config: dict) -> ProsodicAnnotation: +def detect_hesitation(text): + """"""Hésitation = points de suspension répétés (au moins deux marques dans la réplique). + Une marque isolée reste un simple [pause] (sous-entendu, suspense)."""""" + return compter_suspensions(text)[0] >= 2 + +# --- Étape 3 : version étendue ------------------------------------------------------- +# Ordre stable : celui de la taxonomie (émotions, intensité, ton, rythme, souffle). +# La version d'origine renvoie list(set(...)) : l'ordre, donc le tag placé en tête du texte +# annoté, change d'une exécution Python à l'autre (hachage aléatoire des chaînes). +ORDRE_TAGS = [tag for categorie in FISHAUDIO_TAGS.values() for tag in categorie] + +def _compiler_mot_cle(mot_cle): + """"""Mot-clé ancré en début de mot : « froid » trouve « froidement » mais plus « refroidie », + « rire » ne trouve plus « prirent », « vite » ne trouve plus « éviter ». + Les mots-clés de pure ponctuation (« ... », « - ») sont traités par les règles de ponctuation."""""" + forme = normaliser_fr(mot_cle).strip() + if not re.search(r""\w"", forme): + return None + return re.compile(r""(? shout ; + suspensions répétées -> hesitate ; toute suspension (dont « … » et « .... ») -> pause. + 3. contexte (facultatif) : l'incise narrative qui introduit la réplique + (« Boule de Suif répondit sèchement : »). Seuls ses mots-clés comptent, pas sa ponctuation. + 4. Ordre déterministe (ordre de la taxonomie)."""""" + trouves = tags_lexicaux(text) | (tags_lexicaux(contexte) if contexte else set()) + if detect_caps_shouting(text) or text.count(""!"") >= 2: + trouves.add(""shout"") + if detect_hesitation(text): + trouves.add(""hesitate"") + if compter_suspensions(text)[0]: + trouves.add(""pause"") + return [tag for tag in ORDRE_TAGS if tag in trouves] + +# Phrase d'introduction : dans le P1 actuel, « dit-il », « répondit-elle sèchement »… ne sont +# plus dans la réplique mais à la fin du segment narratif qui la précède (terminé par « : »). +# On ne garde que la dernière proposition : après le dernier « . ! ? ; » et après la dernière +# citation fermée (« … »), pour ne pas attribuer à la réplique les mots d'une autre phrase. +segments_par_index = {int(s[""index""]): s for s in segments} + +def incise_precedente(seg_index): + """"""Phrase qui introduit la réplique (fin du segment narratif précédent, terminé par « : »)."""""" + precedent = segments_par_index.get(seg_index - 1) + if not precedent or precedent[""seg_type""] == ""dialogue"": + return """" + texte = "" "".join(precedent[""text""].split()).rstrip(""-— "").strip() + if not texte.endswith("":""): + return """" + texte = re.split(r""[.!?;»]\s*"", texte[:-1])[-1] + return texte.lstrip(""-— "").strip() + "":"" + +# --- Tests unitaires : la ponctuation expressive est rare dans ce conte, on vérifie donc +# chaque règle sur des cas construits, avec la réponse attendue écrite à l'avance. +indice_regex = lambda t: bool(re.search(r""[A-Z]{3,}"", t)) # l'indice de l'énoncé, pour comparaison +cas_majuscules = [ + (""JAMAIS ! Vous ne m'aurez pas !"", True), + (""Hé ! ÉCOUTEZ-MOI bien."", True), + (""BOULE DE SUIF"", False), # titre + (""Il travaillait pour la NASA."", False), # sigle + (""Il dit NON !"", False), # 3 lettres : sous le seuil « plus de 3 lettres » + (""Mon Dieu ! murmura Boule de Suif."", False), +] +cas_suspensions = [ + (""Il attendit..."", (1, 0), False), + (""Il attendit…"", (1, 0), False), + (""Je... je ne sais pas... enfin."", (2, 0), True), + (""Et puis.... plus rien."", (1, 1), False), + (""Euh… c'est-à-dire…… non."", (2, 1), True), +] +print(f""{'texte':38} {'attendu':>8} {'v2':>6} {'indice [A-Z]{3,}':>17}"") +for texte, attendu in cas_majuscules: + obtenu = detect_caps_shouting(texte) + assert obtenu == attendu, texte + print(f""{texte:38} {attendu!s:>8} {obtenu!s:>6} {indice_regex(texte)!s:>17}"") +print() +for texte, compte, hesite in cas_suspensions: + assert compter_suspensions(texte) == compte and detect_hesitation(texte) == hesite, texte + print(f""{texte:38} marques/longues={compte} hésitation={hesite!s:5} "" + f""| v1 voit « ... » : {'...' in texte!s:5} | v2 : {detect_emotion_tags_v2(texte)}"") +print(""Tous les tests unitaires passent.\n"") + +# --- Application aux répliques du P1 ---------------------------------------------- +print(""Ponctuation dans les répliques : majuscules soutenues ="", + sum(detect_caps_shouting(u[""text""]) for u in utterances), + ""| suspensions ="", sum(compter_suspensions(u[""text""])[0] for u in utterances), + ""| répliques avec au moins deux « ! » ="", sum(u[""text""].count(""!"") >= 2 for u in utterances)) + +comparaison_ex1 = [] +for u in utterances: + idx = int(u[""seg_index""]) + v1 = sorted(detect_emotion_tags(u[""text""])) + v2 = detect_emotion_tags_v2(u[""text""]) + incise = incise_precedente(idx) + v2c = detect_emotion_tags_v2(u[""text""], incise) + comparaison_ex1.append(dict(seg=idx, speaker=u[""speaker""], v1=v1, v2=v2, v2_contexte=v2c, incise=incise)) + +print(f""\n{'seg':>4} {'personnage':22} {'v1':18} {'v2':18} {'v2 + intro':22} phrase d'introduction"") +for c in comparaison_ex1: + if c[""v1""] or c[""v2""] or c[""v2_contexte""]: + print(f""{c['seg']:4d} {c['speaker'][:22]:22} {','.join(c['v1']) or '-':18} "" + f""{','.join(c['v2']) or '-':18} {','.join(c['v2_contexte']) or '-':22} {c['incise'][-75:]}"") +print(f""\nRépliques taguées : v1 {sum(bool(c['v1']) for c in comparaison_ex1)} / {len(comparaison_ex1)}"" + f"" | v2 {sum(bool(c['v2']) for c in comparaison_ex1)}"" + f"" | v2 + introduction {sum(bool(c['v2_contexte']) for c in comparaison_ex1)}"" + f"" | phrases d'introduction trouvées : {sum(bool(c['incise']) for c in comparaison_ex1)}"") + +# --- Effet de la normalisation sur tout le conte (342 segments) ---------------------- +v1_total, v2_total = Counter(), Counter() +for s in segments: + v1_total.update(detect_emotion_tags(s[""text""])) + v2_total.update(detect_emotion_tags_v2(s[""text""])) +print(""\nDétections sur les 342 segments (v1 d'origine -> v2, mêmes règles de ponctuation) :"") +for tag in ORDRE_TAGS: + if v1_total[tag] or v2_total[tag]: + print(f"" {tag:10} {v1_total[tag]:3d} -> {v2_total[tag]:3d}"") +",,,,,,,,9b662cce69da1f76,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,7fd27198,code,fr,c9d0867d3b6f992a,"def annotate_utterance(utterance: dict, characters: list, voice_config: dict) -> ProsodicAnnotation: # Annotate a single utterance with FishAudio expressive tags text = utterance[""text""] speaker = utterance[""speaker""] @@ -19926,10 +20285,11 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb, print(""Fonction d'annotation definie."")",,,,,,,,c9d0867d3b6f992a,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,acd003b7,markdown,fr,a3414fb7785df2e3,"## 5. Pipeline d'annotation complete +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,acd003b7,markdown,fr,89db690f0714fc3a,"## 5. Pipeline d'annotation (moteur d'origine v1, pour comparaison) -Application du moteur d'annotation a toutes les repliques P1, avec enrichissement contextuel (didascalies, emotions detectees, profil vocal).",,,,,,,,a3414fb7785df2e3,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,eb7088ae,code,fr,f1eab074497d7873,"# Run full prosodic annotation pipeline +Application du moteur d'origine à toutes les répliques P1. Ces annotations servent de **référence de comparaison** pour les exercices et pour la section 6 bis ; elles ne sont pas exportées. +",,,,,,,,89db690f0714fc3a,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,eb7088ae,code,fr,f1eab074497d7873,"# Run full prosodic annotation pipeline annotations = [] for u in utterances: @@ -19944,10 +20304,11 @@ for ann in annotations: print(f"" Tags: {tags_str}"") print(f"" Profil: {ann.emotion_profile}"") print(f"" Texte annote: {ann.annotated_text[:90]}..."")",,,,,,,,f1eab074497d7873,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,4bed7e34,markdown,fr,6455afa694c5c739,"## 6. Annotation des segments narratifs +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,4bed7e34,markdown,fr,74325ee36563ea6d,"## 6. Annotation des segments narratifs (moteur d'origine v1, pour comparaison) -Les segments de narration et description reoivent également des tags prosodiques pour guider le narrateur (rythme, pauses, intensite).",,,,,,,,6455afa694c5c739,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,37dbdbe9,code,fr,855ca215bb2032a9,"def annotate_segment(seg: dict) -> dict: +Les segments de narration et de description reçoivent également des tags prosodiques pour guider le narrateur (rythme, pauses, intensité). Comme à la section 5, ces annotations v1 servent de référence de comparaison et ne sont pas exportées. +",,,,,,,,74325ee36563ea6d,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,37dbdbe9,code,fr,855ca215bb2032a9,"def annotate_segment(seg: dict) -> dict: # Annotate a narration/description segment for narrator text = seg[""text""] seg_type = seg[""seg_type""] @@ -20007,7 +20368,7 @@ for seg in segments: print(f""{len(segment_annotations)} segments narratifs/descriptifs annnotes"") for sa in segment_annotations: print(f"" [seg {sa['seg_index']}] ({sa['seg_type']}) tags: {', '.join(sa['tags_applied'])}"")",,,,,,,,855ca215bb2032a9,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,848f2404,markdown,fr,bed5a9ed35df9c18,"### Exercice : Enrichir les annotations des segments narratifs +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,848f2404,markdown,fr,99cdc962d1f78bd9,"### Exemple guidé : enrichir les annotations des segments narratifs (groupe G17, PR #23242) **Duree estimee :** 15 minutes @@ -20020,236 +20381,309 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb, - **Étape 3 :** Re-annoter les segments narratifs et comparer les résultats **Indice :** Ajoutez des mots comme ""brusquement"", ""soudain"", ""vivement"" pour [quick] -**Indice :** Ajoutez ""lentement"", ""doucement"", ""paisiblement"", ""calme"" pour [slow]",,,,,,,,bed5a9ed35df9c18,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,a06825dd,code,fr,fd272370919ead02,"# TODO etudiant : enrichir les annotations des segments narratifs -# Etape 1 : Mots-cles supplementaires -QUICK_KEYWORDS = [] # TODO etudiant : mots indiquant une acceleration -SLOW_KEYWORDS = [] # TODO etudiant : mots indiquant une deceleration - -# Etape 2 : Fonction amelioree -def annotate_segment_v2(seg): - """"""Version amelioree avec detection de changements de rythme."""""" - # TODO etudiant : reprendre annotate_segment et ajouter la detection de rythme - return {} # TODO etudiant - -# Etape 3 : Comparer avec la version originale -# TODO etudiant : re-annoter les segments et afficher les differences +**Indice :** Ajoutez ""lentement"", ""doucement"", ""paisiblement"", ""calme"" pour [slow]",,,,,,,,99cdc962d1f78bd9,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,a06825dd,code,fr,eaaeee3b80ae0a20," # Exemple guidé — résolution du groupe G17 (Killian Barret), PR #23242, revue et intégrée. +# Exercice : enrichir les annotations des segments narratifs (changements de rythme) +# Étape 1 : mots-clés choisis d'après leur fréquence réelle dans le conte (formes sans accents). +# « mot* » = début de mot (toutes les terminaisons) ; « mot » sans étoile = mot entier. +QUICK_KEYWORDS = [""soudain*"", ""brusquement"", ""precipit*"", ""vivement"", ""subitement"", + ""rapidement"", ""vite"", ""tout a coup"", ""tout d'un coup""] +SLOW_KEYWORDS = [""lentement"", ""doucement"", ""paisibl*"", ""calme*"", ""tranquill*"", + ""peu a peu"", ""longuement"", ""interminable*"", ""somnol*""] +# Écartés après vérification sur le texte : « aussitôt » (enchaînement, pas débit de parole), +# « longtemps » (durée de l'action), « immobile » (posture), « éclata » (souvent un rire). + +def _compiler(mots_cles): + motifs = [] + for mot in mots_cles: + forme = normaliser_fr(mot.rstrip(""*"")) + fin = """" if mot.endswith(""*"") else r""(?!\w)"" + motifs.append(re.compile(r""(? str: - # Build enriched markdown book for TTS generation - lines = [] - lines.append(""# Boule de Suif - Livre enrichi pour TTS"") - lines.append("""") - lines.append("""") - lines.append("""") - lines.append(f"""") - lines.append(f"""") - lines.append("""") - lines.append(""---"") - lines.append("""") +def _debut_de_phrase(texte, position): + """"""Position du début de la phrase qui contient `position`."""""" + debut = max(texte.rfind(p, 0, position) for p in "".!?;:"") + 1 + while debut < len(texte) and texte[debut] in "" \n«»\""'-—"": + debut += 1 + return debut - # Build a map of all annotations by seg_index - utterance_map = {} - for ann in annotations: - utterance_map[ann.seg_index] = ann +# Étape 2 : fonction améliorée +def annotate_segment_v2(seg): + """"""Reprend annotate_segment, avec texte normalisé, mots-clés ancrés, détection des + changements de rythme et placement du tag de rythme au début de la phrase où le changement + a lieu (et non plus seulement en tête du segment)."""""" + texte, seg_type = seg[""text""], seg[""seg_type""] + forme = normaliser_fr(texte) + tags, placements = [], [] + + def ajouter(*nouveaux): + for tag in nouveaux: + if tag not in tags: + tags.append(tag) + + for motifs, tags_regle in REGLES_ORIGINE: + if any(p.search(forme) for p in motifs): + ajouter(*tags_regle) + for tag, motifs in RYTHME.items(): + for motif in motifs: + for m in motif.finditer(forme): + ajouter(tag) + placements.append((_debut_de_phrase(forme, m.start()), tag, texte[m.start():m.end()])) + if compter_suspensions(texte)[0]: + ajouter(""pause"") + + par_defaut = not tags + if par_defaut: + tags = [""slow""] if seg_type == ""description"" else [""pause""] + + # Texte annoté : tag principal en tête, puis chaque tag de rythme au début de sa phrase + inserts = sorted({(pos, tag) for pos, tag, _ in placements if not (pos == 0 and tag == tags[0])}, + reverse=True) + annote = texte + for pos, tag in inserts: + annote = annote[:pos] + f""[{tag}] "" + annote[pos:] + annote = f""[{tags[0]}] {annote}"" - segment_map = {} - for sa in segment_annotations: - segment_map[sa[""seg_index""]] = sa + return { + ""seg_index"": int(seg[""index""]), ""seg_type"": seg_type, ""original_text"": texte, + ""annotated_text"": annote, ""tags_applied"": tags, ""tag_par_defaut"": par_defaut, + ""changements_de_rythme"": [(tag, mot, pos) for pos, tag, mot in sorted(placements)], + ""speaker"": ""Narrateur"", ""fish_preset"": ""narrator_male"", + } - # Process segments in order +# Étape 3 : re-annoter les segments narratifs et comparer +segment_annotations_v2 = [annotate_segment_v2(s) for s in segments + if int(s[""index""]) not in dialogue_seg_indices] +v1_par_index = {sa[""seg_index""]: sa for sa in segment_annotations} +changes = [(v1_par_index[a[""seg_index""]], a) for a in segment_annotations_v2 + if v1_par_index[a[""seg_index""]][""tags_applied""] != a[""tags_applied""]] + +compte = lambda annots, tag: sum(tag in a[""tags_applied""] for a in annots) +print(f""{len(segment_annotations_v2)} segments narratifs re-annotés ; {len(changes)} ont des tags différents de la v1"") +print(f""{'tag':8} {'v1':>4} {'v2':>4}"") +for tag in [""quick"", ""slow"", ""pause"", ""cold""]: + print(f""{tag:8} {compte(segment_annotations, tag):4d} {compte(segment_annotations_v2, tag):4d}"") +print(""Segments avec seulement le tag par défaut :"", + sum(a[""tag_par_defaut""] for a in segment_annotations_v2), ""sur"", len(segment_annotations_v2)) +melanges = [a for a in segment_annotations_v2 if ""quick"" in a[""tags_applied""] and ""slow"" in a[""tags_applied""]] +print(""Segments qui accélèrent ET ralentissent :"", len(melanges)) + +print(""\nDifférences v1 -> v2 (extrait) :"") +for v1, v2 in changes[:12]: + print(f"" seg {v2['seg_index']:3d} : {'+'.join(v1['tags_applied']):18} -> {'+'.join(v2['tags_applied']):18}"" + f"" {[(t, m) for t, m, _ in v2['changements_de_rythme']]}"") + +milieu = [a for a in segment_annotations_v2 if any(pos > 0 for _, _, pos in a[""changements_de_rythme""])] +print(f""\nSegments où un tag de rythme est placé en milieu de texte : {len(milieu)}"") +for a in milieu[:3]: + texte_annote = "" "".join(a[""annotated_text""].split()) + tags_milieu = [m.start() for m in re.finditer(r""\[(quick|slow)\]"", texte_annote) if m.start() > 0] + debut = max(0, tags_milieu[0] - 90) + print(f"" seg {a['seg_index']:3d} : …{texte_annote[debut:tags_milieu[0] + 110]}…"") +",,,,,,,,eaaeee3b80ae0a20,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,99fa27d0,markdown,fr,f57122f2bde03c12,"## 7. Export du livre enrichi en markdown + +`book.ssml.md` est un **document de relecture humaine**. Il assemble, dans l'ordre du texte, chaque segment avec son personnage, son identifiant, le statut de l'attribution, son preset, les tags **insérés** et le texte préparé pour FishAudio. **Ce n'est pas le fichier lu par P4** : le notebook 04-11 lit `prosodic_annotations.json` (section 8 et section 9 bis). Les deux fichiers sont produits par le même moteur, le moteur retenu (v2), à partir des mêmes objets. +",,,,,,,,f57122f2bde03c12,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,5883dd6d,code,fr,8be50ab347dcb2e7,"def build_book_ssml(segments, annotations, segment_annotations, voice_config) -> str: + """"""Livre de relecture, produit à partir des annotations du moteur retenu."""""" + par_index = {a.seg_index: a for a in annotations} + par_index.update({s[""seg_index""]: AnnotationRetenue(**s) for s in segment_annotations}) + lines = [""# Boule de Suif - Livre enrichi pour TTS (relecture)"", """", + """", + f"""", + f"""", + """", + """", ""---"", """"] for seg in segments: - seg_idx = int(seg[""index""]) - seg_type = seg[""seg_type""] - - if seg_idx in utterance_map: - # Dialogue utterance with prosodic annotation - ann = utterance_map[seg_idx] - speaker = ann.speaker - profile = get_speaker_profile(speaker, characters, voice_config) - char_id = profile[""char_id""] - preset = ann.fish_preset - tags = ""+"".join(ann.tags_applied) if ann.tags_applied else ""neutral"" - - lines.append(f""## Segment {seg_idx} — Dialogue"") - lines.append("""") - lines.append(f""**Personnage** : {speaker} (`{char_id}`) "") - lines.append(f""**Preset vocal** : `{preset}` "") - lines.append(f""**Profil emotionnel** : `{tags}`"") - lines.append("""") - lines.append(f""> {ann.annotated_text}"") - lines.append("""") - elif seg_idx in segment_map: - # Narration/description segment - sa = segment_map[seg_idx] - tags = ""+"".join(sa[""tags_applied""]) - - lines.append(f""## Segment {seg_idx} — {seg_type.capitalize()}"") - lines.append("""") - lines.append(f""**Narrateur** | Tags: `{tags}`"") - lines.append("""") - lines.append(f""> {sa['annotated_text']}"") - lines.append("""") + a = par_index[int(seg[""index""])] + tags = ""+"".join(a.tags_applied) or ""aucun"" + if a.seg_type == ""dialogue"": + lines += [f""## Segment {a.seg_index} — Dialogue"", """", + f""**Personnage** : {a.speaker} (`{a.character_id}`) — attribution : {a.attribution}"" + + (f"" (P1 : {a.speaker_p1})"" if a.speaker_p1 != a.speaker else """") + "" "", + f""**Preset vocal** : `{a.fish_preset}` "", + f""**Tags insérés** : `{tags}`"" + (f"" — écartés : {a.tags_ecartes}"" if a.tags_ecartes else """")] else: - # Fallback: raw segment - lines.append(f""## Segment {seg_idx} — {seg_type.capitalize()}"") - lines.append("""") - lines.append(f""> {seg['text']}"") - lines.append("""") + lines += [f""## Segment {a.seg_index} — {a.seg_type.capitalize()}"", """", + f""**Narrateur** | Tags insérés : `{tags}`"" + ("" (par défaut)"" if a.tag_par_defaut else """")] + lines += ["""", f""> {' '.join(a.annotated_text.split())}"", """", + f""FishAudio S2 : `{' '.join(a.fishaudio_text.split())}`"", """", ""---"", """"] - lines.append(""---"") - lines.append("""") - - # Append voice casting summary - lines.append(""# Casting vocal"") - lines.append("""") - lines.append(""| Personnage | Role | Preset FishAudio | Voix Kokoro | Voix OpenAI |"") - lines.append(""|---|---|---|---|---|"") + lines += [""# Casting vocal"", """", ""| Personnage | Identifiant | Rôle | Preset FishAudio | Voix Kokoro | Voix OpenAI |"", + ""|---|---|---|---|---|---|""] for char_id, info in voice_config[""characters""].items(): - role = info[""role""] - fish = info[""voices""][""fishaudio_production""][""preset""] - kokoro = info[""voices""][""kokoro""][""voice_id""] - openai_v = info[""voices""][""openai""][""voice_id""] - name = info[""name""] - lines.append(f""| {name} | {role} | `{fish}` | `{kokoro}` | `{openai_v}` |"") + v = info[""voices""] + lines.append(f""| {info['name']} | `{char_id}` | {info['role']} | `{v['fishaudio_production']['preset']}` "" + f""| `{v['kokoro']['voice_id']}` | `{v['openai']['voice_id']}` |"") + lines.append(f""| Narrateur | `{NARRATEUR_ID}` | narrateur | `{PRESET_NARRATEUR}` (absent de P2) | — | — |"") + + non_resolues = [a for a in annotations if a.attribution.startswith(""non résolue"")] + lines += ["""", ""# Attributions à vérifier en P1"", """", + f""{len(non_resolues)} répliques sans locuteur résolu (voix du narrateur par défaut) ; "" + f""{sum(a.attribution.startswith('corrigée') for a in annotations)} "" + ""attributions corrigées par la phrase d'introduction."", """"] + for a in annotations: + if a.attribution != ""P1, non vérifiée"" and not a.attribution.startswith(""confirmée""): + lines.append(f""- seg {a.seg_index} : P1 « {a.speaker_p1} » -> {a.speaker} ({a.attribution})"") lines.append("""") - return ""\n"".join(lines) -# Build and display book_ssml = build_book_ssml(segments, annotations, segment_annotations, voice_config) print(f""Livre enrichi genere : {len(book_ssml)} caracteres, {book_ssml.count(chr(10))} lignes"") print(""\n--- Premieres lignes ---"") -for line in book_ssml.split(chr(10))[:20]: - print(line)",,,,,,,,5ad94e12be41d6fc,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,869cc307,markdown,fr,caae18821b224d4c,"## 8. Sauvegarde des résultats +for line in book_ssml.split(chr(10))[:22]: + print(line) +",,,,,,,,8be50ab347dcb2e7,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,869cc307,markdown,fr,f0bd1cc78ba15216,"## 8. Sauvegarde des résultats -Cette étape **matérialise les annotations en fichiers** que P4 (Génération TTS) et la pipeline audio consommeront directement. Trois fichiers sont produits, chacun avec un consommateur différent. +Cette étape écrit les deux livrables de P3, produits par le **même moteur** (le moteur retenu) à partir des **mêmes objets** : -### Mesures verbatim observées +| Fichier | Contenu | Lecteur | +|---------|---------|---------| +| `prosodic_output/prosodic_annotations.json` | **Contrat P3 → P4, version 2.0** : 39 répliques + 303 segments narratifs | P4 (04-11) | +| `prosodic_output/book.ssml.md` | Livre de relecture : personnage, attribution, tags insérés, texte FishAudio | Relecture humaine | +| (résumé affiché) | Tags insérés par personnage | Lecture pédagogique | -L'output de la cellule code donne : +### Le contrat JSON (version 2.0) -| Fichier | Taille | Consommateur | Format | -|---------|--------|--------------|--------| -| `prosodic_output/book.ssml.md` | Livre SSML enrichi | P4 (TTS) | Markdown avec tags FishAudio insérés | -| `prosodic_output/prosodic_annotations.json` | JSON structuré | Outils audit / re-annotation | 9 utterances + 5 segments narratifs | -| (résumé stdout) | Per-character tag counts | Logique pédagogique | Counter top-5 par speaker | +Les champs historiques que P4 lit déjà sont **conservés** : `speaker`, `seg_index`, `utterance_type`, `seg_type`, `original_text`, `annotated_text`, `tags_applied`. Leur sens est désormais précis : `tags_applied` contient les tags **effectivement insérés** dans `annotated_text`. Des champs sont ajoutés pour P4 : +- `character_id` : l'identifiant commun P1/P2 de la voix, nul quand le locuteur n'est pas résolu. P4 doit l'utiliser plutôt que de deviner l'identifiant à partir du nom (voir la section 9 bis) ; +- `attribution` et `speaker_p1` : d'où vient l'attribution, et ce que P1 avait proposé ; +- `fishaudio_text` : le texte à envoyer **tel quel** à FishAudio S2-Pro, avec uniquement des tags officiels entre crochets ; +- `tags_detectes` (avec leur source), `tags_retenus`, `tags_ecartes` (avec leur raison), `tags_inseres` (avec leur position) et `tags_non_transmis` (tags supprimés à la conversion vers S2). -### Décomposition du résumé par personnage +Le bloc `contrat_p4` du fichier rappelle ces règles et le statut du contrat (préparé côté P3, intégration dans P4 à réaliser). Le bloc `stats` est calculé à partir des tags relus dans les textes : 405 détectés, 358 insérés (14 tags différents), 325 conservés après suppression des 33 `quick`, et 314 balises préparées pour FishAudio après fusion. -Le résumé par personnage (extrait verbatim de l'output) révèle un **biais d'annotation** instructif : - -``` -Narrateur: timid (1), shout (1), indignant (1), cold (1) -Cornudet: onctuous (1), laugh (1) -Elisabeth Rousset: whisper (2), indignant (1) -Comtesse de Breville: (vide) -Comte Hubert de Breville: pause (1), onctuous (1) -``` +### Le moteur d'origine (v1), conservé pour comparaison -**Observation 1 — Comtesse de Breville : zéro tag.** Sa réplique « Prenez ceci, ma chere demoiselle, dit la comtesse en tendant un morceau de pain » ne déclenche aucun mot-clé de la taxonomie (ni `tendre`, ni `cold`, ni `sarcastic`). Le moteur lexique-neural ne sait pas qu'une comtesse qui offre du pain à Boule de Suif dans *Boule de Suif* est **acte de pitié condescendante**, pas de vraie chaleur — il faut une analyse sémantique profonde (LLM GPT-4 / Claude) pour le capter. C'est une **limite documentée** du moteur actuel. +Le moteur lexical d'origine reste exécuté aux sections 5 et 6. Sur le même corpus, il annote **7 répliques sur 39** (sortie de la section 6 bis : « Répliques taguées : v1 7/39 -> moteur retenu 14/39 »), et l'exemple guidé de la section 4 montre que sa recherche de sous-chaînes sans accents ni frontières de mots produisait à la fois des ratés (« indignée » jamais vue) et des faux positifs (« prirent » pour `laugh`). Ce plafond est une **limite documentée du moteur lexical** : face aux 342 segments du conte complet, les mots-clés seuls ne retrouvent pas la granularité émotionnelle qu'ils donnaient sur l'ancien extrait de 14 segments. Capturer la prosodie du corpus complet exige une passe **sémantique** — c'est l'incrément suivant de la série, pas ce carnet. -**Observation 2 — Le Narrateur porte 4 rôles différents** sur 4 répliques distinctes (timid pour la dame, shout pour la scène de rébellion, indignant pour le climax, cold pour l'officier prussien). C'est cohérent avec la technique narrative de Maupassant : le narrateur est **caméléon émotionnel** qui prend le ton du personnage qu'il décrit. Kokoro `bf_isabella` (la voix narrateur assignée par P2) doit donc être **techniquement capable de basculer rapidement entre ces tons** — un moteur sans prosodie expressive (comme Kokoro) perd toute cette nuance. C'est l'argument principal pour FishAudio S2-Pro en production. +### Ce que montre le résumé par personnage -**Observation 3 — Elisabeth Rousset a `whisper: 2` (la fréquence la plus haute)**, cohérent avec son rôle de protagoniste souffrante (la faim, la honte, la colère rentrée). Son preset vocal `expressive_female_warm` est précisément conçu pour ces modulations chuchotées. Si P4 produit un audiobook où Boule de Suif ne chuchote jamais, c'est un **signal d'échec** de la chaîne P2→P3→P4. +- **19 répliques sont portées par « Narrateur (locuteur non résolu) ».** Elles sont signalées, et non plus confondues avec la narration. +- **Mme Loiseau a désormais 5 répliques**, contre 2 selon P1, grâce aux trois corrections d'attribution ; ses tags (`shout`, `indignant`, `whisper`…) correspondent aux verbes qui introduisent ses répliques. +- **Les bonnes sœurs n'ont aucun tag** : leur unique réplique (« C'est la faim, pas autre chose. ») n'est introduite que par « elle ajouta », sans indication de ton. ### Pourquoi `book.ssml.md` et pas un vrai fichier SSML XML -FishAudio S2-Pro accepte le format markdown-like (`# Titre` + `[tag] texte`) plutôt que le vrai SSML XML (`...`). C'est un **choix pragmatique** : FishAudio reconnaît ses propres tags `[laugh]`, `[whisper]` etc. directement dans le texte, sans couche XML superflue. Le `.md` (markdown) est donc plus simple à éditer pour un humain et plus robuste pour une génération automatique. Le SSML traditionnel serait plus standard mais rejetterait la majorité des 15000 tags FishAudio. - -### Garanties du fichier `prosodic_annotations.json` - -Le JSON exporté contient : -- `epic` / `pass` / `source_text` : métadonnées de provenance (P3 de l'Epic #1028 sur Boule de Suif) -- `stats.total_tags_applied` : **20 instances de tags** sur 14 segments = densité moyenne ~1.4 tags/segment -- `stats.unique_tags` : **9 tags uniques** sur 21 possibles dans la taxonomie = exploitation 43% du vocabulaire FishAudio -- `utterances` : 9 `ProsodicAnnotation` (dataclass dumpés via `asdict`) -- `narration_segments` : 5 segments narratifs avec leur profil - -Le JSON est **autosuffisant pour la re-annotation** : un script peut recharger `prosodic_annotations.json`, modifier les tags, et regénérer `book.ssml.md` sans ré-exécuter P1+P2. C'est le **contrat de mesure** vers P4 et les itérations futures.",,,,,,,,caae18821b224d4c,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,91af02e1,code,fr,44abefedf52c1940,"# Save book.ssml.md +Ce fichier n'est pas destiné à un moteur de synthèse : c'est une vue de relecture. Le format markdown, avec les tags `[tag]` dans le texte, se lit facilement et montre, pour chaque segment, à la fois le texte annoté et le texte exact préparé pour FishAudio. Le SSML XML standard (`…`) n'apporterait rien ici, puisque FishAudio S2 utilise ses propres tags entre crochets. +",,,,,,,,f0bd1cc78ba15216,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,91af02e1,code,fr,b4d52fc5a1adc897,"# Sauvegarde : livre de relecture + contrat JSON lu par P4, tous deux issus du moteur retenu ssml_path = OUTPUT_DIR / ""book.ssml.md"" ssml_path.write_text(book_ssml, encoding=""utf-8"") -print(f""Sauvegarde: {ssml_path.resolve()}"") +print(f""Sauvegarde: {ssml_path}"") + +# Statistiques calculées à partir des tags RÉELLEMENT présents dans les textes annotés +inseres = Counter(t for a in toutes_retenues for t in tags_dans_le_texte(a.annotated_text)) +assert sum(inseres.values()) == sum(len(a.tags_retenus) for a in toutes_retenues) -# Save annotations as JSON annotations_json = { ""epic"": ""1028"", ""pass"": ""P3"", + ""contract_version"": ""2.0"", + ""moteur"": MOTEUR_RETENU, ""description"": ""Prosodic annotation with FishAudio S2-Pro tags"", ""source_text"": ""Boule de Suif - Guy de Maupassant"", + ""contrat_p4"": { + ""lire"": ""utterances + narration_segments (book.ssml.md est un document de relecture)"", + ""voix"": ""character_id (identifiant commun P1/P2) ; ne pas re-deviner l'identifiant à partir de speaker"", + ""texte_fishaudio"": ""fishaudio_text : tags officiels S2-Pro entre crochets, à envoyer tel quel"", + ""statut"": ""contrat préparé côté P3 ; incompatibilités de P4 identifiées (section 9 bis) ; "" + ""intégration opérationnelle à réaliser dans P4 puis à valider par une synthèse réelle"", + ""texte_kokoro"": ""original_text (Kokoro ignore les tags)"", + ""tags_applied"": ""tags internes effectivement insérés dans annotated_text"", + ""a_verifier"": ""attribution commençant par 'non résolue' ou 'corrigée' : à remonter en P1"", + }, ""stats"": { ""total_utterances"": len(annotations), ""total_narration_segments"": len(segment_annotations), - ""total_tags_applied"": sum(len(a.tags_applied) for a in annotations) + sum(len(sa[""tags_applied""]) for sa in segment_annotations), - ""unique_tags"": list(set(t for a in annotations for t in a.tags_applied)), + ""tags_detectes"": sum(len(a.tags_detectes) for a in toutes_retenues), + ""tags_retenus"": sum(len(a.tags_retenus) for a in toutes_retenues), + ""total_tags_applied"": sum(inseres.values()), + ""unique_tags"": sorted(inseres), + ""tags_inseres_par_tag"": dict(inseres.most_common()), + ""tags_supprimes_conversion_s2"": dict(Counter(t for a in toutes_retenues for t in a.tags_non_transmis)), + ""tags_conserves_avant_fusion"": sum(len(a.tags_applied) - len(a.tags_non_transmis) for a in toutes_retenues), + ""balises_preparees_fishaudio"": sum(len(balises_s2(a.fishaudio_text)) for a in toutes_retenues), + ""statut_fishaudio"": ""balises préparées ; aucune synthèse exécutée dans ce notebook"", + ""attributions"": dict(Counter(a.attribution for a in annotations)), }, ""utterances"": [asdict(a) for a in annotations], ""narration_segments"": segment_annotations, } - json_path = OUTPUT_DIR / ""prosodic_annotations.json"" with open(json_path, ""w"", encoding=""utf-8"") as f: json.dump(annotations_json, f, ensure_ascii=False, indent=2) -print(f""Sauvegarde: {json_path.resolve()}"") - -# Save per-character tag summary -char_tags = {} -for ann in annotations: - speaker = ann.speaker - if speaker not in char_tags: - char_tags[speaker] = [] - char_tags[speaker].extend(ann.tags_applied) - -print(""\n--- Resume par personnage ---"") -for speaker, tags in char_tags.items(): - from collections import Counter - tag_counts = Counter(tags) - top_tags = "", "".join(f""{t} ({c})"" for t, c in tag_counts.most_common(5)) - print(f"" {speaker}: {top_tags}"")",,,,,,,,44abefedf52c1940,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,46d46da1,markdown,fr,5821b305c84b53aa,"## 9. Analyse de la distribution des tags - -Visualisation de la repartition des tags expressifs par catégorie et par personnage, pour verifier la coherence de l'annotation.",,,,,,,,5821b305c84b53aa,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,6bdbb09e,code,fr,48114d2eae3c59a5,"from collections import Counter - -# Global tag distribution -all_tags = [] -for ann in annotations: - all_tags.extend(ann.tags_applied) -for sa in segment_annotations: - all_tags.extend(sa[""tags_applied""]) - +print(f""Sauvegarde: {json_path}"") +print(f""\nTags insérés : {sum(inseres.values())} ({len(inseres)} tags différents) ; "" + f""détectés : {annotations_json['stats']['tags_detectes']}, retenus : {annotations_json['stats']['tags_retenus']}"") + +print(""\n--- Résumé par personnage (tags insérés) ---"") +par_personnage = {} +for a in annotations: + par_personnage.setdefault(a.speaker, Counter()).update(a.tags_applied) +for speaker, tags in par_personnage.items(): + n = sum(1 for a in annotations if a.speaker == speaker) + print(f"" {speaker:34} {n:2d} répliques : "" + ("", "".join(f""{t} ({c})"" for t, c in tags.most_common(5)) or ""aucun tag"")) +",,,,,,,,b4d52fc5a1adc897,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,46d46da1,markdown,fr,dbc0b7f6d35905c1,"## 9. Analyse de la distribution des tags + +Les statistiques sont calculées sur les tags **effectivement insérés** dans les textes annotés (relus dans le texte, pas déduits d'une liste), avec le parcours de chaque tag : détecté, retenu, inséré, conservé pour FishAudio, puis le total des balises préparées après fusion. Aucune synthèse n'étant exécutée, ces balises sont **préparées**, pas transmises. +",,,,,,,,dbc0b7f6d35905c1,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,6bdbb09e,code,fr,230292bca7d0d1bf,"from collections import Counter + +# Global tag distribution : tags relus dans les textes annotés du moteur retenu +all_tags = [t for a in toutes_retenues for t in tags_dans_le_texte(a.annotated_text)] tag_counter = Counter(all_tags) total_tag_instances = sum(tag_counter.values()) +par_defaut = sum(1 for a in toutes_retenues if a.tag_par_defaut) -print(f""=== Distribution globale des tags ==="") -print(f""Total instances: {total_tag_instances}"") +print(""=== Distribution globale des tags insérés ==="") +print(f""Total instances: {total_tag_instances} (dont {par_defaut} tags par défaut de la narration)"") print(f""Tags uniques: {len(tag_counter)}"") print() - for tag, count in tag_counter.most_common(): pct = count / total_tag_instances * 100 - bar = ""#"" * int(pct) - print(f"" {tag:15s} {count:3d} ({pct:5.1f}%) {bar}"") + print(f"" {tag:12s} {count:3d} ({pct:5.1f}%) {'#' * int(pct)}"") -# Per-category analysis print(""\n=== Par categorie FishAudio ==="") for cat, tag_dict in FISHAUDIO_TAGS.items(): cat_tags = [t for t in all_tags if t in tag_dict] if cat_tags: - print(f"" {cat}: {len(cat_tags)} instances ({', '.join(set(cat_tags))})"")",,,,,,,,48114d2eae3c59a5,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,85b98a4e,markdown,fr,c46d512c3d63db78,"### Exercice : Analyser la diversite des tags par segment + print(f"" {cat}: {len(cat_tags)} instances ({', '.join(sorted(set(cat_tags)))})"") +hors = [t for t in all_tags if not any(t in d for d in FISHAUDIO_TAGS.values())] +print(f"" hors taxonomie : {len(hors)}"") + +print(""\n=== Parcours de chaque tag : détecté -> retenu -> inséré -> conservé pour S2 (avant fusion) ==="") +detectes = Counter(t for a in toutes_retenues for t in a.tags_detectes) +retenus = Counter(t for a in toutes_retenues for t in a.tags_retenus) +non_transmis = Counter(t for a in toutes_retenues for t in a.tags_non_transmis) +print(f"" {'tag':12} {'détecté':>8} {'retenu':>7} {'inséré':>7} {'conservé':>9} balise S2"") +for tag in ORDRE_TAGS_MOTEUR: + if detectes[tag]: + print(f"" {tag:12} {detectes[tag]:8d} {retenus[tag]:7d} {tag_counter[tag]:7d} "" + f""{tag_counter[tag] - non_transmis[tag]:9d} {VERS_S2.get(tag) or '— (supprimé)'}"") +conserves = sum(tag_counter.values()) - sum(non_transmis.values()) +balises = sum(len(balises_s2(a.fishaudio_text)) for a in toutes_retenues) +print(f""\n Total : {sum(tag_counter.values())} insérés, {sum(non_transmis.values())} supprimés à la conversion, "" + f""{conserves} conservés, {conserves - balises} fusionnés, {balises} balises préparées pour FishAudio"") +",,,,,,,,230292bca7d0d1bf,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,85b98a4e,markdown,fr,a0635c5c859dcaa3,"### Exemple guidé : analyser la diversité des tags par segment (groupe G17, PR #23242) **Duree estimee :** 10-15 minutes @@ -20262,48 +20696,144 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb, - **Étape 3 :** Proposer des tags additionnels de catégories non representees pour ces segments **Indice :** FISHAUDIO_TAGS est organise par catégorie. Pour chaque tag d'un segment, trouver sa catégorie parent -**Indice :** Un score de 3/5 catégories = très diversifie ; 1/5 = monochrome",,,,,,,,c46d512c3d63db78,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,3bb8534d,code,fr,79b385b431ff3d4a,"# TODO etudiant : score de diversite prosodique -# Etape 1 : Trouver la categorie d'un tag +**Indice :** Un score de 3/5 catégories = très diversifie ; 1/5 = monochrome",,,,,,,,a0635c5c859dcaa3,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,3bb8534d,code,fr,f009b9471e25ed20," # Exemple guidé — résolution du groupe G17 (Killian Barret), PR #23242, revue et intégrée. +# Exercice : score de diversité prosodique +# Étape 1 : trouver la catégorie d'un tag def tag_category(tag_name, fishaudio_tags): - """"""Retourne la categorie d'un tag FishAudio (ou None si inconnu)."""""" - # TODO etudiant - return None # TODO etudiant + """"""Retourne la catégorie d'un tag FishAudio (ou None si inconnu)."""""" + for categorie, tags in fishaudio_tags.items(): + if tag_name in tags: + return categorie + return None -# Etape 2 : Score de diversite par segment +# Étape 2 : score de diversité par segment def diversity_score(tags_list, fishaudio_tags): - """"""Retourne le nombre de categories differentes couvertes par les tags."""""" - # TODO etudiant - return 0 # TODO etudiant + """"""Retourne le nombre de catégories différentes couvertes par les tags (tags inconnus ignorés)."""""" + return len({c for c in (tag_category(t, fishaudio_tags) for t in tags_list) if c is not None}) + +# Étape 3 : analyser toutes les annotations du livre (répliques + segments narratifs) +NB_CATEGORIES = len(FISHAUDIO_TAGS) +MAX_TAGS = 3 +toutes = ([dict(seg_index=a.seg_index, type=""réplique"", texte=a.original_text, tags=list(a.tags_applied)) + for a in annotations] + + [dict(seg_index=s[""seg_index""], type=s[""seg_type""], texte=s[""original_text""], tags=list(s[""tags_applied""])) + for s in segment_annotations]) +inconnus = Counter(t for a in toutes for t in a[""tags""] if tag_category(t, FISHAUDIO_TAGS) is None) +for a in toutes: + a[""score""] = diversity_score(a[""tags""], FISHAUDIO_TAGS) + a[""categories""] = sorted({tag_category(t, FISHAUDIO_TAGS) for t in a[""tags""]} - {None}) + +print(f""{len(toutes)} annotations (moteur retenu) ; tags hors taxonomie : {dict(inconnus) or 'aucun'}"") +scores_v1 = Counter(diversity_score(a.tags_applied, FISHAUDIO_TAGS) for a in annotations_v1) +scores_v1.update(diversity_score(s[""tags_applied""], FISHAUDIO_TAGS) for s in segment_annotations_v1) +scores_v2 = Counter(a[""score""] for a in toutes) +print(""Répartition des scores (catégories couvertes sur"", NB_CATEGORIES, "") : moteur d'origine v1 -> moteur retenu"") +for score in sorted(set(scores_v1) | set(scores_v2)): + print(f"" {score}/{NB_CATEGORIES} : {scores_v1[score]:3d} -> {scores_v2[score]:3d} annotations"") + +sans_tag = [a for a in toutes if not a[""tags""]] +mono_tag = [a for a in toutes if len(a[""tags""]) == 1] +monochromes = [a for a in toutes if len(a[""tags""]) >= 2 and a[""score""] == 1] +print(f""\nSans aucun tag : {len(sans_tag)} | un seul tag : {len(mono_tag)} | "" + f""monochromes (plusieurs tags, une seule catégorie) : {len(monochromes)}"") +for a in monochromes[:8]: + print(f"" seg {a['seg_index']:3d} ({a['type']}) : {'+'.join(a['tags'])} -> {a['categories']}"") + +# Propositions : uniquement des tags APPUYÉS PAR LE TEXTE (détecteurs v2 des exercices +# précédents, phrase d'introduction comprise), dans une catégorie absente, sans dépasser MAX_TAGS. +# Sans indice dans le texte, aucune proposition : ajouter un tag pour « remplir » une +# catégorie dégraderait l'annotation au lieu de l'enrichir. +# Pour la narration, seules les catégories du narrateur sont admises (rythme) : « il riait » +# ou « elle sanglotait » décrivent un personnage, le narrateur ne doit pas rire ni sangloter. +CATEGORIES_NARRATEUR = {""rhythm""} + +# Les indices viennent des détecteurs du MOTEUR RETENU (section 6 bis) : mêmes mots-clés, +# même retrait de « enfin », sans les tags par défaut ni les tags écartés pour une raison de fond. +RAISONS_DE_FOND = (""son non verbal cité dans les paroles"", ""catégorie non portée par le narrateur"") + +def indices_textuels(a): + if a[""type""] == ""réplique"": + sources, _ = detecter_replique(a[""texte""], incise_precedente(a[""seg_index""])) + else: + sources, _ = detecter_narration(segments_par_index[a[""seg_index""]]) + ecartes = selectionner(dict(sources), ""dialogue"" if a[""type""] == ""réplique"" else ""narration"")[1] + candidats = [t for t, s in sources.items() if s != ""defaut"" and ecartes.get(t) not in RAISONS_DE_FOND] + if a[""type""] != ""réplique"": + candidats = [t for t in candidats if tag_category(t, FISHAUDIO_TAGS) in CATEGORIES_NARRATEUR] + return sorted(candidats, key=ORDRE_TAGS_MOTEUR.index) + +propositions = [] +for a in mono_tag + monochromes: + places = MAX_TAGS - len(a[""tags""]) + candidats = [] + for tag in indices_textuels(a): + cat = tag_category(tag, FISHAUDIO_TAGS) + if cat and cat not in a[""categories""] and tag not in candidats and len(candidats) < places: + candidats.append(tag) + if candidats: + propositions.append((a, candidats)) + +print(f""\nAnnotations à une seule catégorie : {len(mono_tag) + len(monochromes)} ; "" + f""avec au moins une proposition appuyée par le texte : {len(propositions)} "" + f""({sum(a['type'] == 'réplique' for a, _ in propositions)} répliques, "" + f""{sum(a['type'] != 'réplique' for a, _ in propositions)} segments narratifs)"") +def preuve(tag, texte): + """"""Mot du texte qui justifie le tag (pour relire chaque proposition)."""""" + forme = normaliser_fr(texte) + motifs = list(MOTIFS_MOTEUR.get(tag, [])) + list(RYTHME.get(tag, [])) + motifs += [m for regle, tags in REGLES_ORIGINE if tag in tags for m in regle] + for motif in motifs: + m = motif.search(forme) + if m: + return texte[m.start():m.end() + 12].replace(""\n"", "" "") + return ""ponctuation"" if RE_SUSPENSION.search(texte) or ""!"" in texte else ""?"" + +for a, candidats in propositions: + source = a[""texte""] + "" "" + (incise_precedente(a[""seg_index""]) if a[""type""] == ""réplique"" else """") + preuves = ""; "".join(f""{t} <- « {preuve(t, source)} »"" for t in candidats) + print(f"" seg {a['seg_index']:3d} ({a['type']:9}) {'+'.join(a['tags']):6} -> + {preuves}"") +",,,,,,,,f009b9471e25ed20,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-10-Annotation-Prosodique.ipynb,4ce8fb9e,markdown,fr,26b7f0285422eadd,"## 11. Conclusion + +### Livrables P3 -# Etape 3 : Analyser toutes les annotations -# TODO etudiant : calculer et afficher le score pour chaque annotation +| Fichier | Description | Lecteur | +|---------|-------------|---------| +| `prosodic_output/prosodic_annotations.json` | Contrat P3 → P4 (version 2.0) : voix (`character_id`), tags détectés, retenus et insérés, texte FishAudio S2 | P4 (04-11) | +| `prosodic_output/book.ssml.md` | Livre de relecture, produit par le même moteur, avec la liste des attributions à vérifier en P1 | Relecture humaine | +| `prosodic_output/test_integration/` | Extraits synthétisés du test P3/P4 (section 9 bis), avec la transcription qui les accompagne | Audit | +| `prosodic_output/scores_mesure/partition_bourdon_vs_melodie.png` | Partition syllabique bourdon vs mélodie (section 10, instrument #1877) | Audit | -print(""Exercice a completer"")",,,,,,,,79b385b431ff3d4a,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-10-Annotation-Prosodique.ipynb,4ce8fb9e,markdown,fr,592198f726e8bbec,"## 11. Conclusion +### Moteur exporté -### Livrables P3 +Le moteur exporté est le **moteur retenu (v2)** de la section 6 bis. Il repose sur : +- des mots-clés normalisés (accents) et ancrés en début de mot ; +- la phrase qui introduit chaque réplique, qui sert à la fois à détecter l'émotion et à vérifier ou corriger le locuteur ; +- des priorités et des conflits explicites, avec un plafond de 3 tags ; +- un placement des tags là où le changement a lieu. -| Fichier | Description | -|---------|-------------| -| `prosodic_output/book.ssml.md` | Livre complet avec tags FishAudio inseres | -| `prosodic_output/prosodic_annotations.json` | Annotations structurees (par replique et segment) | -| `prosodic_output/scores_mesure/partition_bourdon_vs_melodie.png` | Partition syllabique bourdon vs melodie (section 10, instrument #1877) | +Chaque tag retenu est inséré exactement une fois, et les statistiques sont calculées sur les tags réellement insérés. Le moteur d'origine (v1) reste affiché pour comparaison, avec un ordre de tags désormais stable. + +### Passage à P4 -### Passage a P4 (Generation TTS) +**Statut : contrat préparé côté P3, incompatibilités identifiées, synthèse testée sur un échantillon (modèle gratuit).** `book.ssml.md` n'est pas le fichier de génération : P4 lit `prosodic_annotations.json`. Le test de la section 9 bis, exécuté sur le code réel de 04-11, établit trois corrections à faire en aval : +- P4 doit lire `character_id` : sinon la Comtesse reçoit la voix du comte, et deux personnages retombent sur le narrateur ; +- P4 doit envoyer `fishaudio_text` : sa conversion produit la syntaxe de l'ancien modèle S1 — audible (aucun tag n'est prononcé à voix haute, vérifié par transcription), mais pas fidèle aux tags officiels S2 ; +- P2 doit fournir des références de voix FishAudio (`reference_id`) : sans elles, tous les personnages ont la même voix par défaut. -Le fichier `book.ssml.md` est pret pour la generation TTS : -- Chaque segment contient les tags `[tag1] [tag2] texte` interpretes par FishAudio S2-Pro -- Le preset vocal de chaque personnage est documente -- Les tags de rythme, emotion et intensite sont positionnes aux endroits stratégiques +La synthèse a été exécutée avec le modèle **gratuit** `fish-audio/s2.1-pro-free:free` via OpenRouter (clé lue dans un `.env` non versionné) ; le modèle payant n'est proposé que si `FISHAUDIO_ALLOW_PAID=1`. ### Limites de cette approche -- L'annotation est basee sur des heuristiques lexicales, pas sur une analyse sémantique profonde -- Un modèle LLM (GPT-4, Claude) pourrait ameliorer la detection emotionnelle -- Les tags FishAudio sont simules ici (FishAudio S2-Pro n'est pas deploye en Docker) -- La mesure de la section 10 est demontree sur signal synthetique contrôlé (clips TTS non versions au depot) ; l'instrument s'invoque identiquement sur un clip reel",,,,,,,,592198f726e8bbec,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,ab3609d0,markdown,fr,6f7c9c8bbdae19a1,"# P4 — Generation TTS pour Audiobook +- L'annotation reste fondée sur des mots-clés et sur la ponctuation, pas sur une compréhension du texte : un rire « muet » devient un rire audible, et l'émotion d'un interlocuteur peut être attribuée à celui qui parle. +- 19 répliques sur 39 restent sans locuteur résolu : les résoudre demande de suivre les pronoms d'une phrase à l'autre, ce qui relève de P1. +- `[quick]` n'a pas d'équivalent parmi les tags officiels de FishAudio S2 et est supprimé à la conversion. `[slow]` est rendu par `[pause]` : c'est une approximation, car ralentir une lecture et marquer une pause sont deux intentions différentes ; quand `slow` accompagne un `pause`, les deux fusionnent et le ralentissement disparaît (11 segments). +- Plusieurs tags restent imposés par des règles plutôt que par le texte : le ton par défaut des rôles (`cold` pour l'officier, `warm` pour Boule de Suif) et la pause par défaut de la narration. +- La section 10 mesure la mélodie sur des signaux synthétiques : elle ne mesure pas encore l'effet des annotations sur une voix TTS réelle. +- La validation par synthèse porte sur 6 extraits (3 segments × 2 syntaxes) au modèle gratuit, sans référence de voix : elle prouve que les balises ne sont pas prononcées, pas que chaque personnage est distingué. +- Les mesures de la section 10 dépendent de la version de librosa : la 0.11.0 reproduit exactement les valeurs commentées, la 1.0.0 donne 17,4 notes effectives au lieu de 17,8 pour la mélodie (verdicts inchangés).",,,,,,,,26b7f0285422eadd,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,ab3609d0,markdown,fr,6f7c9c8bbdae19a1,"# P4 — Generation TTS pour Audiobook **Navigation** : [Index](../README.md) | [<< Précédent](04-10-Annotation-Prosodique.ipynb) | [Suivant >>](04-12-Compilation-Audio.ipynb) @@ -20316,7 +20846,7 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,ab3609d 3. Generer les fichiers audio pour les 342 segments du texte 4. Exporter les metadonnees de generation (durees, tailles, paths) ",,,,,,,,6f7c9c8bbdae19a1,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,c84ee935,code,fr,173d73d379015298,"# Import guards - availability flags for external dependencies +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,c84ee935,code,fr,173d73d379015298,"# Import guards - availability flags for external dependencies try: from IPython.display import display, Audio, Image as IPImage @@ -20371,7 +20901,7 @@ if HELPERS_PATH.exists(): sys.path.insert(0, str(HELPERS_PATH.parent)) from helpers.audio_helpers import display_audio_bundle, display_audio_file print(""Helpers audio importes"")",,,,,,,,173d73d379015298,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,f2132600,markdown,fr,7d8940c27c394099,"## Architecture TTS +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,f2132600,markdown,fr,7d8940c27c394099,"## Architecture TTS | Modèle | Statut | Qualite | Latence | Expressivite | |--------|--------|---------|---------|-------------| @@ -20380,7 +20910,7 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,f213260 | **OpenAI TTS-1** | Cloud | Bonne | *runtime machine-dep* : ~2s | 6 voix fixes | Ce notebook utilise **Kokoro** pour la demo locale. Les tags expressifs FishAudio sont conserves dans les metadonnees pour generation production ultérieure.",,,,,,,,7d8940c27c394099,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,4da363c3,code,fr,c5abedc1544bf960,"def load_voice_config(path): +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,4da363c3,code,fr,c5abedc1544bf960,"def load_voice_config(path): """"""Load P2 voice casting configuration."""""" with open(path) as f: return json.load(f) @@ -20417,7 +20947,7 @@ voice_map = build_character_voice_map(voice_config) print(f""Characters mapped: {len(voice_map)}"") for cid, info in voice_map.items(): print(f"" {cid}: {info['name']} -> Kokoro {info['kokoro_voice']}"")",,,,,,,,c5abedc1544bf960,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,44e5bede,markdown,fr,049d6e87fa52ae37,"## Kokoro TTS — API locale +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,44e5bede,markdown,fr,049d6e87fa52ae37,"## Kokoro TTS — API locale Le service Kokoro (Docker, port 8191) expose une API compatible OpenAI : @@ -20431,7 +20961,7 @@ Content-Type: application/json Reponse : audio MP3 (128 kbps, 24000 Hz, mono). Note : Les tags expressifs `[whisper]`, `[shout]` etc. sont ignores par Kokoro mais conserves pour FishAudio S2-Pro en production.",,,,,,,,049d6e87fa52ae37,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,8a6a4dc9,code,fr,c20d6efd42581b7c,"@dataclass +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,8a6a4dc9,code,fr,c20d6efd42581b7c,"@dataclass class TTSRequest: """"""Represents a TTS generation request for one segment."""""" seg_index: int @@ -20473,13 +21003,13 @@ def kokoro_tts(text, voice_id, speed=1.0): print(""TTS client ready"") print(f""Kokoro endpoint: {KOKORO_URL}"")",,,,,,,,c20d6efd42581b7c,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,7dde4239,markdown,fr,497ffd193a647546,"## Construction des segments a generer +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,7dde4239,markdown,fr,497ffd193a647546,"## Construction des segments a generer Chaque segment (dialogue ou narration) est prepare avec : - Le texte annote P3 (avec tags expressifs) - La voix Kokoro correspondante depuis P2 - Le preset FishAudio pour reference production",,,,,,,,497ffd193a647546,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,266da177,code,fr,8ed09cec82b14e6a,"def resolve_speaker_id(speaker_name, voice_map): +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,266da177,code,fr,8ed09cec82b14e6a,"def resolve_speaker_id(speaker_name, voice_map): """"""Resolve a speaker name to character_id."""""" name_lower = speaker_name.lower().replace("" "", ""_"") # Direct match @@ -20530,10 +21060,10 @@ print(f""Total segments to generate: {len(tts_requests)}"") for req in tts_requests: print(f"" Seg {req.seg_index}: {req.seg_type:12s} | "" f""{req.kokoro_voice:15s} | {req.speaker}"")",,,,,,,,8ed09cec82b14e6a,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,11894894,markdown,fr,8436e47072ba185d,"## Generation audio +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,11894894,markdown,fr,8436e47072ba185d,"## Generation audio Chaque segment est envoye a Kokoro TTS avec la voix correspondante. Les tags expressifs sont conserves dans le texte brut (Kokoro les ignore). Les fichiers audio sont sauvegardes en MP3 (format natif de Kokoro).",,,,,,,,8436e47072ba185d,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,d9c8323d,code,fr,030537a012d22239,"import struct +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,d9c8323d,code,fr,030537a012d22239,"import struct def audio_duration_s(audio_bytes): """"""Extract duration from audio bytes (WAV or MP3)."""""" @@ -20604,7 +21134,7 @@ elapsed = time.time() - t_start success_count = sum(1 for r in generated if r.status == ""success"") print(f""\nGenerated {success_count}/{len(generated)} segments in {elapsed:.1f}s "" f""({cached_count} reloaded from disk cache, {success_count - cached_count} newly generated)"")",,,,,,,,030537a012d22239,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,c9ed5f80,markdown,fr,c94e7281f4ed0300,"## Résultats de la generation +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,c9ed5f80,markdown,fr,c94e7281f4ed0300,"## Résultats de la generation **Tableau recapitulatif** des 342 segments audio generes par Kokoro TTS (texte integral de « Boule de Suif »). @@ -20630,7 +21160,7 @@ La distribution des durees (1,7 s pour le seg 26 de 7 mots, 88,9 s pour le seg 1 Les fichiers MP3 sont maintenant disponibles dans `tts_output/`, les metadonnees seront exportees dans la cellule suivante pour que P5 (Compilation audio) puisse les assembler en livre audio. ",,,,,,,,c94e7281f4ed0300,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,c57ad4ce,code,fr,eabb18b76d75bd42,"print(f""{'Seg':>4} | {'Type':12s} | {'Voice':15s} | {'Speaker':25s} | "" +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,c57ad4ce,code,fr,eabb18b76d75bd42,"print(f""{'Seg':>4} | {'Type':12s} | {'Voice':15s} | {'Speaker':25s} | "" f""{'Duration':>8s} | {'Size':>7s} | {'Status':>7s}"") print(""-"" * 100) total_duration = 0.0 @@ -20644,7 +21174,7 @@ for req in generated: print(f""\nTotal duration: {total_duration:.1f}s ({total_duration/60:.1f}min)"") print(f""Output directory: {TTS_OUTPUT_DIR.name}"")",,,,,,,,eabb18b76d75bd42,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,cb5cd451,markdown,fr,c4e26c57239c15cb,"### Exemple guidé : estimation de la durée audio à partir du texte +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,cb5cd451,markdown,fr,c4e26c57239c15cb,"### Exemple guidé : estimation de la durée audio à partir du texte *Contribution du groupe de la PR #21095 (APP3), intégrée comme exemple guidé.* @@ -20659,7 +21189,7 @@ Le débit (mots par seconde) est **commun** à tous les segments — même voix - Un **contrôle synthétique** — trente segments de débit et de biais connus — vérifie que le code d'estimation retrouve ces paramètres, indépendamment de la disponibilité du service TTS. - Les **cas limites** sont traités : texte vide, type jamais vu, balises de prosodie (qui ne sont pas prononcées), mots composés et élisions, et absence totale de segment réussi. ",,,,,,,,c4e26c57239c15cb,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,ca6a709c,code,fr,469d5de881632941,"# Exemple guide : estimation de la duree audio a partir du texte +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,ca6a709c,code,fr,469d5de881632941,"# Exemple guide : estimation de la duree audio a partir du texte # Contribution du groupe de la PR #21095, integree comme exemple guide. # # Modele retenu : duree = nb_mots / debit + biais(type) @@ -20818,7 +21348,7 @@ if _source.exists(): _debit = _co[TYPE_GLOBAL][0] _s = compter_mots(_texte) / _debit print(f""Boule de Suif complet : {compter_mots(_texte)} mots -> ~{_s / 60:.0f} min d'audio ({_nom}, {_debit:.2f} mots/s)"")",,,,,,,,469d5de881632941,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,7f303d5f,markdown,fr,2a96057093088d29,"## Export des metadonnees +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,7f303d5f,markdown,fr,2a96057093088d29,"## Export des metadonnees Les metadonnees de generation sont exportees en JSON pour P5 (Compilation audio). Cette étape formalise le **contrat d'interface** entre la passe P4 (generation TTS) et la passe P5 (assemblage audiobook) : tout ce que P5 doit savoir pour assembler le livre audio tient dans ce fichier. @@ -20842,7 +21372,7 @@ La cellule verifie que `asdict(r)` capture bien **tous** les champs du `@datacla ### Sortie attendue A l'issue de cette cellule, `tts_output/tts_generation_metadata.json` existe et contient `stats.successful = 342`, `stats.total_duration_s = 6125.3`. C'est le signal de succès minimal — P5 refusera de demarrer si ce fichier est absent ou si `stats.successful != 342`.",,,,,,,,2a96057093088d29,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,d80c4ab3,code,fr,2d6a2a6219be31bc,"tts_metadata = { +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,d80c4ab3,code,fr,2d6a2a6219be31bc,"tts_metadata = { ""epic"": ""1028"", ""pass"": ""P4"", ""description"": ""TTS generation via Kokoro for audiobook demo"", @@ -20865,7 +21395,7 @@ with open(meta_path, ""w"", encoding=""utf-8"") as f: print(f""Metadata saved: {meta_path.name}"") print(f""Segments: {tts_metadata['stats']['successful']}/{tts_metadata['stats']['total_segments']}"") print(f""Total audio: {total_duration:.1f}s"")",,,,,,,,2d6a2a6219be31bc,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,b3551146,markdown,fr,c0a0dcab73c4dfcc,"## Analyse de l'utilisation des voix +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,b3551146,markdown,fr,c0a0dcab73c4dfcc,"## Analyse de l'utilisation des voix Repartition des segments par voix et par type. Cette cellule agrege les résultats par voix Kokoro et par type de segment — deux axes complémentaires qui servent a la fois au **debug** (identifier les voix sous-utilisees ou les types surrepresentes) et a la **planification** (estimer la charge d'une prochaine generation). @@ -20896,7 +21426,7 @@ Le recours a `collections.Counter` (et non a `pandas.DataFrame.groupby`) est del L'agregation filtre `status == ""success""`. Si la generation Kokoro avait partiellement echoue, les segments `failed` ne seraient pas comptes dans `voice_usage` ni dans `type_usage`. C'est desirable : on analyse la production effective, pas la production théorique. La duree affichée correspond a `req.duration_s` (mesuree par parsing WAV/MP3), pas a une estimation word-count. ",,,,,,,,c0a0dcab73c4dfcc,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,836fd92f,code,fr,9837e18eb7d9e4dc,"from collections import Counter +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,836fd92f,code,fr,9837e18eb7d9e4dc,"from collections import Counter voice_usage = Counter(r.kokoro_voice for r in generated if r.status == ""success"") type_usage = Counter(r.seg_type for r in generated if r.status == ""success"") @@ -20909,7 +21439,7 @@ for voice, count in voice_usage.most_common(): print(f""\nSegment types:"") for stype, count in type_usage.most_common(): print(f"" {stype:12s}: {count}"")",,,,,,,,9837e18eb7d9e4dc,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,b5fc8ede,markdown,fr,2f5dacc2cf1ae306,"## Conclusion P4 — Kokoro (Demo) +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,b5fc8ede,markdown,fr,2f5dacc2cf1ae306,"## Conclusion P4 — Kokoro (Demo) Les fichiers audio MP3 ont ete generes pour chaque segment du texte via Kokoro TTS. Les metadonnees (durees, voix, paths) sont exportees pour la compilation finale P5. @@ -20920,7 +21450,7 @@ Les fichiers audio MP3 ont ete generes pour chaque segment du texte via Kokoro T ## P4b — Generation FishAudio S2-Pro (Production) FishAudio S2-Pro (4.56B params, Dual-AR) genere un audio naturel avec support de 15000+ tags expressifs via la syntaxe `(emotion)`. Le service tourne en Docker sur GPU1 (RTX 3090 24GB).",,,,,,,,2f5dacc2cf1ae306,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,9b706e7e,code,fr,72b9f8f6ac8a68e3,"FISHAUDIO_URL = os.getenv(""FISHAUDIO_URL"", ""http://localhost:8197"") +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,9b706e7e,code,fr,72b9f8f6ac8a68e3,"FISHAUDIO_URL = os.getenv(""FISHAUDIO_URL"", ""http://localhost:8197"") def fishaudio_tts(text, reference_id=None, timeout=300): """"""Generate audio via FishAudio S2-Pro API. @@ -21016,7 +21546,7 @@ if fishaudio_available: else: print(f""FishAudio S2-Pro: NON DISPONIBLE ({FISHAUDIO_URL})"") print(""Le service FishAudio est optionnel — la generation Kokoro reste disponible."")",,,,,,,,72b9f8f6ac8a68e3,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,10e99ddf,code,fr,344aa26413b5181a,"# Tag mapping: P3 bracket notation -> FishAudio parenthesis notation +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,10e99ddf,code,fr,344aa26413b5181a,"# Tag mapping: P3 bracket notation -> FishAudio parenthesis notation TAG_MAP = { ""whisper"": ""whispering"", ""shout"": ""shouting"", @@ -21059,7 +21589,7 @@ print(f""Tag mapping: {len(TAG_MAP)} tags"") print(f""Exemple: '[whisper] Bonjour' -> '{convert_tags_for_fishaudio('[whisper] Bonjour')}'"") print(f""Exemple: '[shout] Au revoir' -> '{convert_tags_for_fishaudio('[shout] Au revoir')}'"") print(f""Exemple: '[cold] Je vois' -> '{convert_tags_for_fishaudio('[cold] Je vois')}'"")",,,,,,,,344aa26413b5181a,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,c35f76c4,markdown,fr,4666fc28c954e622,"### Exercice : Etendre le convertisseur de tags pour un nouveau modèle TTS +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,c35f76c4,markdown,fr,4666fc28c954e622,"### Exercice : Etendre le convertisseur de tags pour un nouveau modèle TTS **Duree estimee :** 15 minutes @@ -21074,7 +21604,7 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,c35f76c **Indice :** SSML utilise des balises ouvrantes/fermantes : `texte` **Indice :** Les tags de pause P3 `[pause]` deviennent `` en SSML **Indice :** Testez avec xml.etree.ElementTree pour verifier que le SSML est valide",,,,,,,,4666fc28c954e622,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,d08029ca,code,fr,4e612172aa514995,"# TODO etudiant : convertisseur P3 vers SSML +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,d08029ca,code,fr,4e612172aa514995,"# TODO etudiant : convertisseur P3 vers SSML # Etape 1 : Mapping P3 -> SSML SSML_MAP = {} # TODO etudiant : {""whisper"": '', ""shout"": '', ...} @@ -21093,12 +21623,12 @@ test_texts = [ # TODO etudiant : appliquer convert_tags_to_ssml et afficher print(""Exercice a completer"")",,,,,,,,4e612172aa514995,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,60ee12ae,markdown,fr,e6c08682ee1fc179,"## Decoupage intelligent des segments (LLM) +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,60ee12ae,markdown,fr,1e736a94abdf3f1b,"## Decoupage intelligent des segments (LLM) Les segments longs (>55 mots) doivent etre decoupes pour FishAudio. Un decoupage naif par comptage de mots coupe au milieu d'une phrase ou d'une scene. Cette étape utilise un LLM pour decouper aux **frontieres naturelles** du texte : fins de phrase, changements de scene, transitions narratif/dialogue. -La fonction `llm_split_segment()` envoie le texte a `gpt-4o-mini` qui identifie les points de coupure optimaux, avec un fallback sur le decoupage par phrase si le LLM est indisponible.",,,,,,,,e6c08682ee1fc179,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,5bd5cead,code,fr,1939bc87acdd6941,"from openai import OpenAI +La fonction `llm_split_segment()` envoie le texte a `gpt-5.6-luna` qui identifie les points de coupure optimaux, avec un fallback sur le decoupage par phrase si le LLM est indisponible.",,,,,,,,1e736a94abdf3f1b,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,5bd5cead,code,fr,4e2cccffa63dcca7,"from openai import OpenAI # Cle optionnelle : sans OPENAI_API_KEY le client reste absent et le decoupage # deterministe de repli s'applique (le carnet reste executable de bout en bout). @@ -21132,7 +21662,9 @@ def llm_split_segment(text, max_words=55): ) try: resp = openai_client.chat.completions.create( - model=""gpt-4o-mini"", + # Cible chat-mini de la generation courante : table de correspondance + # docs/genai/model-mapping-openai.md (tranche 1 de #14755). + model=""gpt-5.6-luna"", messages=[{""role"": ""user"", ""content"": prompt}], temperature=0.1, max_tokens=1000, @@ -21152,10 +21684,10 @@ def llm_split_segment(text, max_words=55): test_long = ""La diligence partit dans la neige. Dans la voiture, neuf passagers se tenaient serres : le marchand de vins Loiseau et sa femme, le republican Cornudet, la comtesse de Breville, le manufacturier Carre-Lamadon et son epouse, le comte Hubert de Breville, les bonnes soeurs, et Elisabeth Rousset, dite Boule de Suif, une prostituee de grand renom."" parts = llm_split_segment(test_long) for i, p in enumerate(parts): - print(f"" Part {i+1} ({len(p.split())} mots): {p[:60]}..."")",,,,,,,,1939bc87acdd6941,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,63be896a,markdown,fr,f944c13149591892,"Le decoupage par LLM vise a respecter les frontieres narratives naturelles du texte (fins de phrase, changements de scene) plutot que de couper mecaniquement après N mots. Au moment de cette exécution, le LLM etait indisponible (erreur d'authentification du proxy, code 401) : la generation FishAudio qui suit a donc utilise le **decoupage déterministe de repli**, coupe sur la ponctuation et la longueur, sans interpretation narrative. Le mécanisme de cache evite de regenerer les segments déjà produits. Avec la cle proxy retablie, la même cellule bascule sur le decoupage LLM sans autre changement de code. + print(f"" Part {i+1} ({len(p.split())} mots): {p[:60]}..."")",,,,,,,,4e2cccffa63dcca7,,,,,,,,,,,,,,, +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,63be896a,markdown,fr,f944c13149591892,"Le decoupage par LLM vise a respecter les frontieres narratives naturelles du texte (fins de phrase, changements de scene) plutot que de couper mecaniquement après N mots. Au moment de cette exécution, le LLM etait indisponible (erreur d'authentification du proxy, code 401) : la generation FishAudio qui suit a donc utilise le **decoupage déterministe de repli**, coupe sur la ponctuation et la longueur, sans interpretation narrative. Le mécanisme de cache evite de regenerer les segments déjà produits. Avec la cle proxy retablie, la même cellule bascule sur le decoupage LLM sans autre changement de code. ",,,,,,,,f944c13149591892,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,40b77d53,code,fr,3554e89e73279910,"FISH_OUTPUT_DIR = BASE_DIR / ""tts_output_fishaudio"" +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,40b77d53,code,fr,3554e89e73279910,"FISH_OUTPUT_DIR = BASE_DIR / ""tts_output_fishaudio"" FISH_OUTPUT_DIR.mkdir(exist_ok=True, parents=True) SUB_DIR = FISH_OUTPUT_DIR / ""sub_segments"" SUB_DIR.mkdir(exist_ok=True) @@ -21270,9 +21802,9 @@ if fishaudio_available: else: print(""FishAudio S2-Pro non disponible — generation skippee"") print(""Demarrer le service: docker compose -f docker-configurations/services/tts-fishaudio/docker-compose.yml up -d"")",,,,,,,,3554e89e73279910,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,a7ffaaaf,markdown,fr,adc78a637da45dd5,"Les 342 segments ont ete generes avec succes via FishAudio S2-Pro, avec le decoupage déterministe de repli (LLM indisponible, cf. cellule précédente). Les 63 segments longs ont ete automatiquement decoupes en sous-parties (jusqu'a 3 morceaux pour le seg 1), puis reassembles par concatenation WAV. Chaque segment beneficie des tags expressifs convertis depuis la notation P3. +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,a7ffaaaf,markdown,fr,adc78a637da45dd5,"Les 342 segments ont ete generes avec succes via FishAudio S2-Pro, avec le decoupage déterministe de repli (LLM indisponible, cf. cellule précédente). Les 63 segments longs ont ete automatiquement decoupes en sous-parties (jusqu'a 3 morceaux pour le seg 1), puis reassembles par concatenation WAV. Chaque segment beneficie des tags expressifs convertis depuis la notation P3. ",,,,,,,,adc78a637da45dd5,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,eaa745da,code,fr,e9b5f28c92d35ccf,"if fish_generated: +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,eaa745da,code,fr,e9b5f28c92d35ccf,"if fish_generated: kokoro_dur = sum(r.duration_s for r in generated if r.status == ""success"") fish_dur = sum(r.duration_s for r in fish_generated if r.status == ""success"") kokoro_size = sum(r.file_size_kb for r in generated if r.status == ""success"") @@ -21289,9 +21821,9 @@ MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,eaa745d else: print(""FishAudio non disponible — comparaison impossible"") print(""Le service FishAudio S2-Pro doit tourner sur le port 8197"")",,,,,,,,e9b5f28c92d35ccf,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,a41ef228,markdown,fr,788ea70e9826e6c3,"Le tableau comparatif met en evidence un ecart significatif sur la taille des fichiers : FishAudio genere des WAV non compresses (428 353 KB au total, ~1250 KB par segment en moyenne) contre des MP3 compresses pour Kokoro (95 707 KB, ~280 KB par segment), soit un ratio de ~4,5x. La duree totale differe aussi : 4973,0 s pour FishAudio contre 6125,3 s pour Kokoro (-18,8 %) — sur ce corpus a dominante narrative, FishAudio synthetise sensiblement plus vite que Kokoro. +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,a41ef228,markdown,fr,788ea70e9826e6c3,"Le tableau comparatif met en evidence un ecart significatif sur la taille des fichiers : FishAudio genere des WAV non compresses (428 353 KB au total, ~1250 KB par segment en moyenne) contre des MP3 compresses pour Kokoro (95 707 KB, ~280 KB par segment), soit un ratio de ~4,5x. La duree totale differe aussi : 4973,0 s pour FishAudio contre 6125,3 s pour Kokoro (-18,8 %) — sur ce corpus a dominante narrative, FishAudio synthetise sensiblement plus vite que Kokoro. ",,,,,,,,788ea70e9826e6c3,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,42378a32,code,fr,a49121b7ff895a83,"# Export FishAudio generation metadata for P5 +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,42378a32,code,fr,a49121b7ff895a83,"# Export FishAudio generation metadata for P5 fish_meta = { ""epic"": ""1273"", ""pass"": ""P4b"", @@ -21319,7 +21851,7 @@ if fish_generated: print(f""Total FishAudio audio: {fish_dur:.1f}s ({fish_dur/60:.1f}min)"") else: print(""No FishAudio segments generated"")",,,,,,,,a49121b7ff895a83,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,7f55161e,markdown,fr,e243b6d83e1aceac,"## Compilation des livres audio complets +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,7f55161e,markdown,fr,e243b6d83e1aceac,"## Compilation des livres audio complets Deux assemblages bout-en-bout sont produits : @@ -21328,7 +21860,7 @@ Deux assemblages bout-en-bout sont produits : Les deux fichiers complets restent sur disque (artefacts de production). La section Comparaison ci-dessous embarque un extrait de 30 s de chacun pour l'ecoute cote a cote : les 28 players par segment embarquent déjà l'integralite des deux rendus, doubler le notebook avec deux livres entiers n'apporterait rien de plus. ",,,,,,,,e243b6d83e1aceac,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,6732e984,code,fr,a8fab163d05b1088,"if fish_generated: +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,6732e984,code,fr,a8fab163d05b1088,"if fish_generated: success_segments = sorted( [r for r in fish_generated if r.status == ""success""], key=lambda r: r.seg_index @@ -21409,7 +21941,7 @@ if _kokoro_meta_c.exists(): else: print(""Metadonnees Kokoro absentes — compilation MP3 ignoree"") ",,,,,,,,a8fab163d05b1088,,,,,,,,,,,,,,, -MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-11-Generation-TTS.ipynb,b5aff9ea,markdown,fr,bd03e3485377befb,"## Comparaison detaillee Kokoro vs FishAudio (segment par segment) +MyIA.AI.Notebooks/GenAI/Audio/04-Applications/04-Audiobook/04-11-Generation-TTS.ipynb,b5aff9ea,markdown,fr,bd03e3485377befb,"## Comparaison detaillee Kokoro vs FishAudio (segment par segment) Pour chaque segment, vous pouvez **ecouter les deux versions** et comparer la qualite, l'expressivite et le naturel de la synthese. Cette cellule produit a la fois un **tableau quantitatif** (durees, tailles, delta) et un **comparateur audio interactif** (balise HTML `