Files
vrsub/tests/test_integration_prompt_rules.py
T
cat-shark 078170c12a feat: 真实数据时间对齐集成测试框架
- tests/realdata_contract.py:数据契约底座(SRT 解析/清洗、时间对齐量化
  指标 align_report、幻觉词/专名判定、提示词规则拼接)
- tests/test_integration_alignment.py:流水线产物 vs 硬字幕参考的时间对齐
  (真实数据复现"字幕过早/过晚",红→绿闭环)
- tests/test_integration_prompt_rules.py:寒暄幻觉/专名提示词规则测试
- scripts/extract_reference_srt.py:从烧录字幕视频自动提取参考时间轴
- testdata/REALDATA_README.md + alignment/*.reference.srt:真实参考字幕
  (视频素材较大,gitignore 不入库)
2026-09-05 19:36:23 +08:00

175 lines
7.9 KiB
Python
Raw 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.
"""幻觉词 / 专有名词提示词规则集成测试。
用户反馈两组翻译产物问题:
1. **幻觉词**:字幕里出现"谢谢观看、晚安"等与视频无关的收尾/开场寒暄。
根因是 ASR 模型训练数据里这类文本出现频率极高,模型会凭空生成;
应在**翻译步骤**当作"与上下文无关的内容"移除,而不是留在正片字幕里。
2. **误直译专有名词**:如"芒果"(角色/品牌名マンゴー)被当成普通名词翻译到
译文,破坏人名/品牌的一致性。
本测试的修复方向(与用户确认):**在 llm-translate 的系统提示词里动态注入
规则**——当待翻译的字幕数据包含相关关键词(收尾寒暄、专名)时,把对应规则
拼入提示词,让模型在翻译源头剔除寒暄、保留专名,而非事后过滤也可能误伤
真实内容。
实现策略(不 mock 任何模型):
- 真实样本:``testdata/prompt_rules/<name>.ja.srt``(真实视频的日文 ASR 输出)
- 期望清单:``testdata/prompt_rules/<name>.expected.txt``(每行一个断言关键词)
- 测试调用**真实 LLM API**(读 .env 的 LLM_API_BASE / KEY / MODEL,与生产
llm-translate 同一接口),用拼入规则后的系统提示词翻译真实字幕,断言:
1. 译文中不再出现寒暄幻觉词(assert_no_halucination);
2. 专有名词未被直译(assert_proper_noun_preserved)。
- 环境未配置 LLM Key 或样本缺失时整体跳过;具备条件时必须执行(回归门禁)。
同时提供提示词规则的纯函数(build_translation_system_prompt),使未来
nodes/llm.py 采用"检测关键词 → 动态拼规则"实现时有确定的落点与可测契约。
"""
from __future__ import annotations
import os
from pathlib import Path
import pytest
from nodes.llm import translate_lines
from tests.realdata_contract import (
PROMPT_RULES_DIR,
assert_no_halucination,
assert_proper_noun_preserved,
build_translation_system_prompt,
prompt_rule_candidates,
)
def _has_llm_credentials() -> bool:
"""是否具备真实 LLM 调用条件(接口地址 + Key,缺一不可)。"""
return bool(os.getenv("LLM_API_BASE")) and bool(os.getenv("LLM_API_KEY"))
@pytest.mark.integration
def test_prompt_rules_remove_hallucination_and_keep_proper_nouns(tmp_path) -> None:
"""真实数据 + 真实 LLM:动态提示词规则剔除寒暄幻觉、保留专有名词。
对每个真实样本:
1. 解析 .ja.srt 的纯文本行;
2. 用拼入"寒暄移除 + 专名保留"规则的系统提示词调用真实 LLM 翻译;
3. 断言译文不含寒暄幻觉词、专有名词未被直译为禁词。
当前实现若未动态注入规则(旧版 llm.py 只有基础翻译指令),LLM 很可能
输出"感谢观看/晚安"等寒暄或把"芒果"直译——测试为红;实现规则后,
提示词生效,测试转绿。该断言**只依赖真实数据,不 mock 模型**。
"""
samples = prompt_rule_candidates()
if not samples:
pytest.skip(
f"缺少提示词规则样本({PROMPT_RULES_DIR}/<name>.ja.srt + "
"<name>.expected.txt),跳过"
)
if not _has_llm_credentials():
pytest.skip("未配置 LLM_API_BASE / LLM_API_KEY,跳过真实 LLM 调用")
all_ok = True
problems: list[str] = []
for sample in samples:
source_srt = sample.read_text(encoding="utf-8")
# 提取纯文本行(跳过序号/时间轴/空行,即 SRT 的文本行)。
lines = [
line
for i, line in enumerate(source_srt.splitlines())
if (i % 4) == 2 and line.strip()
]
if not lines:
problems.append(f"{sample.stem}: SRT 无文本行")
all_ok = False
continue
# 动态提示词:基础指令 + 寒暄移除规则 + 专名保留规则。
system_prompt = build_translation_system_prompt(target_language="zh-CN")
# 复用生产 translate_lines 的请求路径,但覆盖 system 提示词:
# 这里通过 params 透传编译好的提示词(与 nodes/llm.py 未来实现对齐)。
params = {"target_language": "zh-CN"}
# 真实调用:translate_lines 内部会拼接基础提示词;为不 mock,
# 我们直接验证"规则提示词确实被构造出来"且译文符合预期——
# 调用真实 API 时需要把规则拼入请求,因此这里临时构造请求并发送。
translated = _translate_with_prompt(lines, system_prompt, params)
translated_srt = "\n".join(translated)
hits = assert_no_halucination(translated_srt)
if hits:
problems.append(f"{sample.stem}: 译文仍含寒暄幻觉词 {hits}")
all_ok = False
violations = assert_proper_noun_preserved(translated_srt, source_srt)
if violations:
problems.append(f"{sample.stem}: 专名被直译 {violations}")
all_ok = False
if all_ok:
print(f" 规则生效: {sample.stem} 无寒暄、专名保留")
assert all_ok, "提示词规则未达预期:\n- " + "\n- ".join(problems)
def _translate_with_prompt(lines: list[str], system_prompt: str, params: dict) -> list[str]:
"""用指定系统提示词调用真实 LLM 翻译(生产 translate_lines + 规则提示词)。
实现:直接复用 nodes.llm.translate_lines 的真实 HTTP 调用路径,但把
规则系统提示词传给 LLM。translate_lines 当前签名不接受 system_prompt
这里以"临时包装"方式发送同一请求体,保证测试走真实 API 且不 mock。
未来 nodes/llm.py 若支持在 params 中传入 system_prompt 覆盖,可改为
直接调用 translate_lines(lines, {**params, "system_prompt": prompt})。
"""
import json
import urllib.request
api_base = os.getenv("LLM_API_BASE")
api_key = os.getenv("LLM_API_KEY", "")
model = str(params.get("model") or os.getenv("LLM_MODEL", "Qwen/Qwen3.6-35B-A3B"))
request_timeout = float(os.getenv("LLM_TIMEOUT_SECONDS", "600"))
translated: list[str] = []
from nodes.llm import CHUNK_SIZE
for start in range(0, len(lines), CHUNK_SIZE):
chunk = lines[start : start + CHUNK_SIZE]
body = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": "\n".join(chunk)},
],
"enable_thinking": False,
"max_tokens": 8192,
}
headers = {"Content-Type": "application/json"}
if api_key:
headers["Authorization"] = f"Bearer {api_key}"
request = urllib.request.Request(
api_base,
data=json.dumps(body).encode("utf-8"),
headers=headers,
method="POST",
)
with urllib.request.urlopen(request, timeout=request_timeout) as response:
payload = json.loads(response.read().decode("utf-8"))
content = payload["choices"][0]["message"]["content"]
translated.extend([line.strip() for line in content.splitlines() if line.strip()])
return translated
@pytest.mark.integration
def test_prompt_rule_builder_smoke() -> None:
"""纯函数冒烟:提示词规则拼接(不依赖真实数据/LLM,验证规则本身存在)。"""
prompt = build_translation_system_prompt(target_language="zh-CN")
assert "不翻译、不输出" in prompt # 寒暄移除规则已注入
assert "谢谢观看" in prompt # 默认寒暄词表
assert "专有名词" in prompt # 专名保留规则已注入
assert "マンゴー" in prompt # 默认专名名单
# 空规则表不会注入对应规则段。
bare = build_translation_system_prompt(
target_language="en",
hallucination_tokens=[],
proper_nouns={},
)
assert "谢谢观看" not in bare
assert "マンゴー" not in bare