"""nodes/proper_nouns.py 的模块级测试(数据 → 测试过程 → 验证结果)。 被测模块:`nodes/proper_nouns.py`(专名/拟声/成人隐语提示词规则),供 llm-translate 注入系统提示词,纯函数可独立调用。 覆盖:未命中不注入、三类规则(专名/拟声/隐语)各自命中、批量文本输入、 规则文本包含可执行的翻译指令。 """ from __future__ import annotations import pytest from nodes.proper_nouns import ( ADULT_EUPHEMISMS, ONOMATOPOEIA, PROPER_NOUNS, build_proper_noun_rule, ) def test_returns_none_when_nothing_matches() -> None: """普通文本未命中任何规则表时不注入(避免干扰正常翻译)。""" # 数据:不含规则表词条的普通句子。 text = "今天天气真好。\n我们一起去散步吧。" # 测试过程 rule = build_proper_noun_rule(text) # 验证结果 assert rule is None def test_empty_input_returns_none() -> None: """空文本与空列表都不注入规则。""" # 数据:空字符串与空列表。 # 测试过程与验证结果 assert build_proper_noun_rule("") is None assert build_proper_noun_rule([]) is None def test_injects_proper_noun_rule_with_advice() -> None: """命中专名表时注入该词的处置建议(禁止硬译)。""" # 数据:真实字幕片段,含角色专名 ジンゴ。 text = "クモ穴にパンパンになって ジンゴがここで味わいませんか" # 测试过程 rule = build_proper_noun_rule(text) # 验证结果:规则非空,含原词与处置说明。 assert rule is not None assert "ジンゴ" in rule assert "芒果" in rule # 说明文本中包含"禁止译作芒果"的约束 def test_injects_person_name_rule() -> None: """人名类专名命中时给出音译建议(不保留日文写法)。""" # 数据:含人名 カンタくん。 text = "カンタくん、そこにいたの" # 测试过程 rule = build_proper_noun_rule(text) # 验证结果 assert rule is not None assert "カンタくん" in rule assert "音译" in rule def test_injects_onomatopoeia_rule() -> None: """拟声/拟态词命中时给出按语境翻译的建议。""" # 数据:含拟态词 ビクビク。 text = "体がビクビク震えてる" # 测试过程 rule = build_proper_noun_rule(text) # 验证结果 assert rule is not None assert "ビクビク" in rule def test_injects_adult_euphemism_with_actual_meaning() -> None: """成人语境隐语命中时注入"实际含义 + 应译词 + 禁止字面直译"。""" # 数据:含隐语 マンゴー(实际指女性性器官)。 text = "そろそろマンゴーが濡れてきました" # 测试过程 rule = build_proper_noun_rule(text) # 验证结果:规则提示这是隐语并禁止字面直译。 assert rule is not None assert "マンゴー" in rule assert "隐语" in rule assert "切勿按字面直译" in rule def test_accepts_batch_line_list() -> None: """接受原文行列表(按批翻译时传入多行),跨行命中同样注入。""" # 数据:命中词分散在不同行。 lines = ["普通的一句话", "相手がバナナをしゃぶってくれて"] # 测试过程 rule = build_proper_noun_rule(lines) # 验证结果 assert rule is not None assert "バナナ" in rule def test_rule_text_covers_all_injected_tokens() -> None: """多词同时命中时每条都出现在规则文本中(不丢词)。""" # 数据:同时含专名、拟声、隐语各一个。 text = "ジンゴとビクビクとマンゴーの話" # 测试过程 rule = build_proper_noun_rule(text) # 验证结果:三个词都在规则里。 assert rule is not None for token in ("ジンゴ", "ビクビク", "マンゴー"): assert token in rule def test_each_rule_table_entry_is_self_consistent() -> None: """规则表结构自检:每张表的每条记录字段完整,避免维护时漏字段。 这不重复业务逻辑,而是保证数据资产(表)本身可用——新增词条时若写错 结构,此处会立刻失败。 """ # 数据:三张规则表。 tables = (PROPER_NOUNS, ONOMATOPOEIA, ADULT_EUPHEMISMS) # 测试过程与验证结果 for table in tables: assert table, "规则表不应为空" for token, payload in table.items(): assert token.strip(), "词条不能为空白" assert all(str(part).strip() for part in payload), f"{token} 的说明字段不完整" @pytest.mark.integration def test_rule_matches_real_transcript_data() -> None: """真实数据校准:真实日文 transcript 中含专名的片段应命中并注入规则。 数据来源:`data/` 下真实任务的 transcript.srt(gitignored,缺失即跳过)。 """ # 数据:仓库 data/ 下真实产物的 transcript 文件。 import json from pathlib import Path from tests.shared.srt_entries import parse_srt_entries workspace = Path(__file__).resolve().parents[3] # 真实产物里的字幕(ASR 转录或过滤后字幕都可能含专名,两者都扫)。 candidates = sorted( p for pattern in ("steps/asr/transcript.srt", "steps/filter/*.srt", "steps/ocr/subtitle.srt") for p in (workspace / "data" / "storage").glob(f"runs/*/{pattern}") if p.is_file() ) if not candidates: pytest.skip("缺少真实字幕后端产物(data/ 未保留运行产物),跳过") # 测试过程:在真实字幕里找**实际出现的**专名(不写死某个词, # 数据内容随影片变化),用它的上下文构造规则。 for path in candidates: entries = parse_srt_entries(path.read_text(encoding="utf-8")) for token in PROPER_NOUNS: batch = [e["text"] for e in entries if token in e["text"]] if not batch: continue rule = build_proper_noun_rule(batch) # 验证结果:命中专名即注入规则,且规则可安全序列化进提示词。 assert rule is not None, f"真实数据含专名 {token},应注入规则" assert token in rule, f"规则应包含命中的专名 {token}" assert json.dumps(rule, ensure_ascii=False) return pytest.skip("真实字幕中未出现规则表内的专名,跳过")