"""Tests de la detection deterministe des incises. `detect_incises` / `incise_speaker` / `iter_incise_pieces` sont pures et testables sans Gemma. Deux passes : inversion verbe-pronom ("dit-il") et nominale consciente du casting ("compatit Holden", "informa le soldat"). """ from __future__ import annotations from inkflow.analysis.segmenter import ( detect_incises, incise_speaker, iter_incise_pieces, ) NAMES = {"Holden", "Kajri", "Camina Drummer"} def _pieces(text: str, names=NAMES) -> list[tuple[bool, str]]: return iter_incise_pieces(text, detect_incises(text, names=names)) # --- Passe inversion (verbe-pronom) ----------------------------------------- def test_inversion_au_milieu(): assert _pieces("James Holden, coupa-t-elle. Je sais qui vous êtes.") == [ (False, "James Holden,"), (True, "coupa-t-elle."), (False, "Je sais qui vous êtes."), ] def test_inversion_en_fin(): assert _pieces("C'est fini, dit-elle.") == [ (False, "C'est fini,"), (True, "dit-elle."), ] def test_inversion_reflechi_exclamation(): assert _pieces("Viens ici, s'écria-t-il !") == [ (False, "Viens ici,"), (True, "s'écria-t-il !"), ] def test_inversion_fermee_par_virgule(): assert _pieces("Pars, répondit-elle, et ne reviens pas.") == [ (False, "Pars,"), (True, "répondit-elle,"), (False, "et ne reviens pas."), ] def test_inversion_complements_apres_pronom(): assert _pieces("Trop tard, murmura-t-il en souriant. Partons.") == [ (False, "Trop tard,"), (True, "murmura-t-il en souriant."), (False, "Partons."), ] def test_double_inversion(): assert _pieces("Stop, dit-il. Non, reprit-elle.") == [ (False, "Stop,"), (True, "dit-il."), (False, "Non,"), (True, "reprit-elle."), ] # --- Incise en fin de parole : tout le reste de la replique est narration ---- def test_incise_apres_fin_de_phrase_va_jusqu_au_bout(): # Apres "…" la parole est close : "dit-il ... provisoires." est narration. text = ("Dans une minute, oui. Je voudrais juste… dit-il avec un geste vague, " "comme si tout cela n'avait plus d'importance.") assert _pieces(text) == [ (False, "Dans une minute, oui. Je voudrais juste…"), (True, "dit-il avec un geste vague, comme si tout cela n'avait plus " "d'importance."), ] def test_incise_apres_virgule_reprend_le_dialogue(): # Apres une simple virgule, le dialogue reprend (contraste avec ci-dessus). assert _pieces("Pars, répondit-elle, et ne reviens pas.") == [ (False, "Pars,"), (True, "répondit-elle,"), (False, "et ne reviens pas."), ] def test_incise_nominale_apres_point_interrogation_va_au_bout(): text = "Vraiment ? demanda-t-il en se levant. Il s'éloigna." assert _pieces(text) == [ (False, "Vraiment ?"), (True, "demanda-t-il en se levant. Il s'éloigna."), ] # --- Passe nominale (verbe + sujet connu) ----------------------------------- def test_nominale_nom_propre(): assert _pieces("Toutes mes condoléances, compatit Holden.") == [ (False, "Toutes mes condoléances,"), (True, "compatit Holden."), ] def test_nominale_alias_apres_ponctuation_forte(): # "?" comme delimiteur a gauche + sujet = alias d'un personnage connu. assert _pieces("Flippant, cet enfoiré, hein ? lança Drummer.") == [ (False, "Flippant, cet enfoiré, hein ?"), (True, "lança Drummer."), ] def test_nominale_clitic_et_nom_de_role(): assert _pieces("Vous venez, monsieur ? lui demanda un garde.") == [ (False, "Vous venez, monsieur ?"), (True, "lui demanda un garde."), ] # --- incise_speaker : seeding du locuteur explicite ------------------------- def test_seed_speaker_nom_propre(): text = "Toutes mes condoléances, compatit Holden." inc = detect_incises(text, names=NAMES)[0] assert incise_speaker(text, inc, NAMES) == "Holden" def test_seed_speaker_alias_vers_canonique(): text = "Hein ? lança Drummer." inc = detect_incises(text, names=NAMES)[0] assert incise_speaker(text, inc, NAMES) == "Camina Drummer" def test_seed_speaker_role_non_nomme_est_none(): # Un nom de role ("un garde") n'est pas un personnage du casting -> pas de seed. text = "Vous venez ? lui demanda un garde." inc = detect_incises(text, names=NAMES)[0] assert incise_speaker(text, inc, NAMES) is None def test_seed_speaker_inversion_est_none(): text = "C'est fini, dit-elle." inc = detect_incises(text, names=NAMES)[0] assert incise_speaker(text, inc, NAMES) is None def test_seed_nom_propre_absent_du_casting(): # Le nom est ecrit dans l'incise -> seede meme si l'extraction l'a rate. text = "Bonjour, lança Drummer." inc = detect_incises(text, names=set())[0] assert incise_speaker(text, inc, set()) == "Drummer" assert _pieces(text, names=set()) == [ (False, "Bonjour,"), (True, "lança Drummer."), ] # --- Faux positifs a NE PAS detecter ---------------------------------------- def test_vocatif_adresse_pas_incise(): # Le personnage est interpelle, pas une incise (aucun verbe de parole). text = "Vous n'avez pas l'air en mesure de rendre service, capitaine Holden." assert detect_incises(text, names=NAMES) == [] def test_imperatif_sans_incise(): assert detect_incises("Donne-le-moi.", names=NAMES) == [] def test_pronom_tu_exclu(): assert detect_incises("Crois-tu ?", names=NAMES) == [] def test_replique_simple_sans_incise(): assert detect_incises("Bonjour à tous.", names=NAMES) == [] def test_sans_noms_inversion_seule(): # Sans casting fourni, la passe inversion fonctionne toujours. assert _pieces("C'est fini, dit-elle.", names=set()) == [ (False, "C'est fini,"), (True, "dit-elle."), ] # --- Invariants ------------------------------------------------------------- def test_texte_preserve_modulo_espaces(): text = "James Holden, coupa-t-elle. Je sais qui vous êtes." joined = "".join(p for _, p in _pieces(text)) assert joined.replace(" ", "") == text.replace(" ", "") def test_bornes_non_chevauchantes_et_triees(): text = "Stop, dit-il. Non, reprit-elle." incs = detect_incises(text, names=NAMES) assert all(incs[i].end <= incs[i + 1].start for i in range(len(incs) - 1)) for inc in incs: assert 0 <= inc.start < inc.end <= len(text)