Files
vrsub/tests/test_translation_line_alignment.py
T

121 lines
5.3 KiB
Python

"""R05 回归:合法 SRT 多行/空 cue、稳定 ID 翻译和非法模型输出重试。
历史 run_51242078d76e 出现文本贴错时间;仅检查行数或在末尾合并/补空无法
定位中间缺失。本测试在 HTTP 边界注入 JSON 响应,调用真实翻译实现。
"""
import json
from pathlib import Path
import pytest
from nodes import llm
from wov_sdk.models import InvokeRequest
def _http(monkeypatch, answers):
"""按次序返回真实 chat.completions 结构,并记录发出的输入。"""
calls = []
iterator = iter(answers)
class Response:
def __init__(self, content):
self.content = content
def __enter__(self):
return self
def __exit__(self, *args):
return False
def read(self):
return json.dumps({"choices": [{"message": {"content": self.content}}],
"usage": {"total_tokens": 10}}).encode()
def open_request(request, **kwargs):
calls.append(json.loads(request.data))
answer = next(iterator)
return Response(answer if isinstance(answer, str) else json.dumps(answer))
monkeypatch.setattr("urllib.request.urlopen", open_request)
return calls
def test_multiline_and_empty_cues_keep_timestamps(monkeypatch, tmp_path):
"""多行正文视为一条 cue,空 cue 不发送翻译,时间轴不会进入模型输入。"""
source = tmp_path / "input.srt"
source.write_text("\ufeff7\n00:00:01,000 --> 00:00:02,000\nこんにちは\n元気ですか\n\n"
"8\n00:00:03,000 --> 00:00:04,000\n\n"
"9\n00:00:05,000 --> 00:00:06,000\nはい\n", encoding="utf-8")
calls = _http(monkeypatch, [[{"id": 3, "text": "是的"}, {"id": 1, "text": "你好\n还好吗"}]])
response = llm.invoke(InvokeRequest(run_id="r", node_instance_id="", inputs={"srt_uri": str(source)}, output_dir=str(tmp_path / "out")))
assert response.status == "completed", response.error
assert json.loads(calls[0]["messages"][1]["content"]) == [
{"id": 1, "text": "こんにちは\n元気ですか"}, {"id": 3, "text": "はい"}]
text = Path(response.outputs["cn_srt_uri"]).read_text()
assert text.count("-->") == 3
assert "00:00:01,000 --> 00:00:02,000\n你好\n还好吗" in text
assert "00:00:03,000 --> 00:00:04,000\n\n" in text
assert "00:00:05,000 --> 00:00:06,000\n是的" in text
@pytest.mark.parametrize("bad", [
[{"id": 1, "text": "一"}],
[{"id": 1, "text": "一"}, {"id": 1, "text": "重复"}],
[{"id": 1, "text": "一"}, {"id": 99, "text": "未知"}],
[{"id": True, "text": "一"}, {"id": 2, "text": "二"}],
[{"id": 1, "text": "一"}, {"id": 2, "text": ""}],
"一\n二", "{truncated", {"1": "一", "2": "二"},
])
def test_invalid_ids_retry_without_positional_repair(monkeypatch, bad):
"""缺失/重复/未知 ID 和无结构文本均重试整批,不猜测句子对应关系。"""
calls = _http(monkeypatch, [bad, [{"id": 2, "text": "二"}, {"id": 1, "text": "一"}]])
assert llm.translate_lines(["first", "second"], {}) == ["一", "二"]
assert len(calls) == 2
assert calls[0]["messages"][1] == calls[1]["messages"][1]
def test_exhausted_alignment_retries_fail_node(monkeypatch, tmp_path):
"""无法对齐时返回 failed,不生成带空占位或错位文本的成功成品。"""
source = tmp_path / "input.srt"
source.write_text("1\n00:00:01,000 --> 00:00:02,000\nhello\n", encoding="utf-8")
calls = _http(monkeypatch, ["无 ID 输出"] * llm.MAX_BATCH_RETRIES)
response = llm.invoke(InvokeRequest(run_id="r", node_instance_id="", inputs={"srt_uri": str(source)}, output_dir=str(tmp_path / "out")))
assert response.status == "failed"
assert len(calls) == llm.MAX_BATCH_RETRIES
assert not (tmp_path / "out/cn.srt").exists()
def test_batch_ids_are_global_and_order_independent(monkeypatch):
"""跨批 ID 保持全局位置,乱序输出也能按正确 cue 回填。"""
answers = [[{"id": i, "text": f"译{i}"} for i in range(20, 0, -1)],
[{"id": 22, "text": "译22"}, {"id": 21, "text": "译21"}]]
calls = _http(monkeypatch, answers)
assert llm.translate_lines([f"原{i}" for i in range(1, 23)], {}) == [f"译{i}" for i in range(1, 23)]
assert json.loads(calls[1]["messages"][1]["content"])[0]["id"] == 21
def test_malformed_srt_fails_without_llm(monkeypatch, tmp_path):
"""非空坏字幕不能被静默解析为空并成功输出。"""
source = tmp_path / "input.srt"
source.write_text("1\ninvalid timestamp\nhello\n", encoding="utf-8")
calls = _http(monkeypatch, [])
response = llm.invoke(InvokeRequest(run_id="r", node_instance_id="", inputs={"srt_uri": str(source)}, output_dir=str(tmp_path / "out")))
assert response.status == "failed"
assert calls == []
@pytest.mark.integration
def test_real_llm_structured_translation():
"""真实接口校准 JSON 协议;无 Key 时跳过,仅使用短日文句子控制调用量。"""
import os
from dotenv import load_dotenv
load_dotenv()
if not os.getenv("LLM_API_KEY"):
pytest.skip("未配置 LLM_API_KEY")
result = llm.translate_lines(["こんにちは。", "ありがとうございます。"], {})
assert len(result) == 2 and all(result)
assert any(word in result[0] for word in ("好", "嗨"))
assert "谢" in result[1]