Files
InkFlow/backend/tests/test_incises.py

205 lines
6.4 KiB
Python

"""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)