"""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]