96 lines
4.6 KiB
Python
96 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
||
"""Детерминированный анализ маркеров голоса по выходам проб (прототип voice-флаггера).
|
||
|
||
Маркеры (по духу флаггеров проекта, $0, без LLM):
|
||
- T1: Шэнь Цуй → Фан Юань: доля вы-форм в её репликах (норма 1.0); самоуничижительное
|
||
самоназвание («служанка/рабыня») vs плоское «я»; Фан Юань → «ты» служанке.
|
||
- T2: братья на «ты»: любое вы-обращение внутри реплик = нарушение.
|
||
- Общие: средняя длина реплики по спикеру (рубленость Фан Юаня), восклицания Фан Чжэна.
|
||
"""
|
||
import json
|
||
import re
|
||
from pathlib import Path
|
||
|
||
RAW = Path(__file__).resolve().parent / "raw"
|
||
|
||
VY = re.compile(r"\b(вы|вас|вам|вами|ваш\w*)\b", re.I)
|
||
TY = re.compile(r"\b(ты|тебя|тебе|тобой|твой|твоя|твоё|твои|тво\w*)\b", re.I)
|
||
SERVILE = re.compile(r"(служанк|рабын|недостойн|ничтожн|осмел)", re.I)
|
||
QUOTE = re.compile(r"«([^»]+)»|“([^”]+)”|\"([^\"]+)\"")
|
||
|
||
|
||
DASH_LINE = re.compile(r"^\s*[—–-]\s*(.+)$", re.M)
|
||
|
||
|
||
def quotes(text):
|
||
out = []
|
||
for m in QUOTE.finditer(text):
|
||
q = next(g for g in m.groups() if g)
|
||
out.append(q)
|
||
# Тире-диалоги (правильная русская типографика): строка с ведущим тире;
|
||
# авторскую ремарку после « — » внутри строки отрезаем от реплики грубо.
|
||
for m in DASH_LINE.finditer(text):
|
||
line = m.group(1)
|
||
# отрезать хвостовую ремарку вида «..., — сказал X.» если она есть
|
||
parts = re.split(r"\s[—–]\s", line)
|
||
out.append(parts[0] if parts else line)
|
||
return out
|
||
|
||
|
||
def cjk_share(text):
|
||
if not text:
|
||
return 1.0
|
||
cjk = sum(1 for c in text if '一' <= c <= '鿿')
|
||
return cjk / max(len(text), 1)
|
||
|
||
|
||
def analyze_t1(text):
|
||
qs = quotes(text)
|
||
# Эвристика атрибуции: реплики Шэнь Цуй содержат «господин»; реплика Фан Юаня — «проголодался»/«вноси»/«неси»
|
||
sc = [q for q in qs if "господин" in q.lower()]
|
||
fy = [q for q in qs if re.search(r"проголодал|внос|неси|занос", q.lower())]
|
||
sc_vy = sum(len(VY.findall(q)) for q in sc)
|
||
sc_ty = sum(len(TY.findall(q)) for q in sc)
|
||
sc_servile = sum(len(SERVILE.findall(q)) for q in sc)
|
||
fy_ty = sum(len(TY.findall(q)) for q in fy)
|
||
fy_vy = sum(len(VY.findall(q)) for q in fy)
|
||
return {"n_quotes": len(qs), "sc_quotes": len(sc), "sc_vy": sc_vy, "sc_ty": sc_ty,
|
||
"sc_servile_selfref": sc_servile, "fy_ty": fy_ty, "fy_vy": fy_vy}
|
||
|
||
|
||
def analyze_t2(text):
|
||
qs = quotes(text)
|
||
# Оба спикера — братья, «вы» внутри реплик = нарушение регистра пары
|
||
# (кроме цитирования третьих лиц — в этой сцене их реплик нет).
|
||
vy = sum(len(VY.findall(q)) for q in qs)
|
||
ty = sum(len(TY.findall(q)) for q in qs)
|
||
# Рубленость Фан Юаня: его реплики — с бранью/приказом; Фан Чжэна — с «брат»/«хочу»
|
||
fy = [q for q in qs if re.search(r"мерзав|глупец|дурак|щенок|ступай|исчезн|убирайся|провались", q.lower())]
|
||
fz = [q for q in qs if re.search(r"\bбрат\b|братец|как ты мо|я знаю|я понима", q.lower())]
|
||
mean = lambda xs: round(sum(len(x) for x in xs) / len(xs), 1) if xs else None
|
||
excl = sum(q.count("!") for q in qs)
|
||
return {"n_quotes": len(qs), "vy_violations": vy, "ty_total": ty,
|
||
"fy_mean_len": mean(fy), "fz_mean_len": mean(fz), "exclamations": excl}
|
||
|
||
|
||
def main():
|
||
rows = []
|
||
for f in sorted(RAW.glob("p1-*.json")):
|
||
d = json.loads(f.read_text())
|
||
text = d.get("text") or ""
|
||
echo = cjk_share(text) > 0.15
|
||
base = {"tag": d["tag"], "echo": echo, "chars": len(text)}
|
||
if echo:
|
||
rows.append(base)
|
||
continue
|
||
if "-T1-" in d["tag"]:
|
||
base.update(analyze_t1(text))
|
||
else:
|
||
base.update(analyze_t2(text))
|
||
rows.append(base)
|
||
for r in rows:
|
||
print(json.dumps(r, ensure_ascii=False))
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|