books/gu-zhenren/research15-probes/analyze_voice.py

96 lines
4.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()