fixed a bug:translate maybe timeout
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import pytest
|
||||
|
||||
from sentence_api.ai_tutor import AITutor, AITutorError
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
|
||||
from sentence_api.ai_tutor import AITutor, AITutorError, _parse_translation_payload
|
||||
|
||||
|
||||
class FakeResponse:
|
||||
@@ -73,15 +77,13 @@ def test_ai_tutor_rejects_unexpected_response(monkeypatch):
|
||||
tutor.explain("Hello.")
|
||||
|
||||
|
||||
from sentence_api.ai_tutor import _parse_translation_payload
|
||||
|
||||
|
||||
def test_ai_tutor_translates_full_document(monkeypatch):
|
||||
requests_payloads = []
|
||||
|
||||
def fake_post(endpoint, headers, json, timeout):
|
||||
requests_payloads.append((endpoint, headers, json, timeout))
|
||||
return FakeResponse(body={"choices": [{"message": {"content": '["你好。", "见到你很高兴。"]'}}]})
|
||||
return FakeResponse(body={"choices": [{"message": {"content": '{"0":"你好。","1":"见到你很高兴。"}'}}]})
|
||||
|
||||
monkeypatch.setattr("sentence_api.ai_tutor.requests.post", fake_post)
|
||||
tutor = AITutor("https://ai.test/v1", "secret", "test-model", 1.5)
|
||||
@@ -94,11 +96,87 @@ def test_ai_tutor_translates_full_document(monkeypatch):
|
||||
assert translated == {0: "你好。", 1: "见到你很高兴。"}
|
||||
_endpoint, _headers, payload, timeout = requests_payloads[0]
|
||||
user_prompt = payload["messages"][1]["content"]
|
||||
assert "Hello." in user_prompt
|
||||
assert "Nice to meet you." in user_prompt
|
||||
assert timeout == 90.0
|
||||
assert "[0] Hello." in user_prompt
|
||||
assert "[1] Nice to meet you." in user_prompt
|
||||
assert timeout == 45.0
|
||||
|
||||
|
||||
def _translation_response_from_prompt(payload):
|
||||
user_prompt = payload["messages"][1]["content"]
|
||||
section = user_prompt.split("待翻译句子:", 1)[1]
|
||||
indexes = [int(match) for match in re.findall(r"^\[(\d+)\]", section, re.M)]
|
||||
body = {str(index): f"中文{index}" for index in indexes}
|
||||
return FakeResponse(
|
||||
body={"choices": [{"message": {"content": json.dumps(body, ensure_ascii=False)}}]}
|
||||
)
|
||||
|
||||
|
||||
def test_ai_tutor_translates_long_input_in_batches(monkeypatch):
|
||||
requests_payloads = []
|
||||
lock = threading.Lock()
|
||||
|
||||
def fake_post(endpoint, headers, json, timeout):
|
||||
with lock:
|
||||
requests_payloads.append(json)
|
||||
return _translation_response_from_prompt(json)
|
||||
|
||||
monkeypatch.setattr("sentence_api.ai_tutor.requests.post", fake_post)
|
||||
tutor = AITutor("https://ai.test/v1", "secret", "test-model", 1)
|
||||
sentences = [(index, f"This is sentence {index} with enough text.") for index in range(30)]
|
||||
|
||||
translated = tutor.translate_sentences(sentences, course_title="Long talk")
|
||||
|
||||
assert translated == {index: f"中文{index}" for index in range(30)}
|
||||
assert len(requests_payloads) >= 3
|
||||
first_prompt = requests_payloads[0]["messages"][1]["content"]
|
||||
assert "前文" in first_prompt or "后文" in first_prompt or "待翻译句子:" in first_prompt
|
||||
|
||||
|
||||
def test_ai_tutor_skips_cached_sentences_and_persists_windows(monkeypatch):
|
||||
persisted = []
|
||||
|
||||
def fake_post(endpoint, headers, json, timeout):
|
||||
return _translation_response_from_prompt(json)
|
||||
|
||||
monkeypatch.setattr("sentence_api.ai_tutor.requests.post", fake_post)
|
||||
tutor = AITutor("https://ai.test/v1", "secret", "test-model", 1)
|
||||
translated = tutor.translate_sentences(
|
||||
[(0, "Hello."), (1, "Thanks."), (2, "Bye.")],
|
||||
existing={0: "你好。"},
|
||||
persist=persisted.append,
|
||||
course_title="Greetings",
|
||||
)
|
||||
|
||||
assert translated[0] == "你好。"
|
||||
assert translated[1] == "中文1"
|
||||
assert translated[2] == "中文2"
|
||||
assert persisted
|
||||
assert 0 not in persisted[0]
|
||||
|
||||
|
||||
def test_ai_tutor_splits_invalid_windows_and_retries(monkeypatch):
|
||||
calls = {"count": 0}
|
||||
|
||||
def fake_post(endpoint, headers, json, timeout):
|
||||
calls["count"] += 1
|
||||
if calls["count"] == 1:
|
||||
return FakeResponse(body={"choices": [{"message": {"content": "not-json"}}]})
|
||||
return _translation_response_from_prompt(json)
|
||||
|
||||
monkeypatch.setattr("sentence_api.ai_tutor.requests.post", fake_post)
|
||||
tutor = AITutor("https://ai.test/v1", "secret", "test-model", 1)
|
||||
sentences = [(index, f"This is sentence {index} with enough text.") for index in range(12)]
|
||||
|
||||
translated = tutor.translate_sentences(sentences)
|
||||
|
||||
assert translated == {index: f"中文{index}" for index in range(12)}
|
||||
assert calls["count"] >= 3
|
||||
|
||||
|
||||
def test_parse_translation_payload_accepts_fenced_json_and_numbered_lines():
|
||||
assert _parse_translation_payload('```json\n["一", "二"]\n```', 2) == ["一", "二"]
|
||||
assert _parse_translation_payload("1. 一\n2. 二", 2) == ["一", "二"]
|
||||
assert _parse_translation_payload('```json\n["一", "二"]\n```', 2) == {0: "一", 1: "二"}
|
||||
assert _parse_translation_payload("1. 一\n2. 二", 2) == {0: "一", 1: "二"}
|
||||
assert _parse_translation_payload('prefix {"12":"甲","13":"乙"} suffix', [12, 13]) == {
|
||||
12: "甲",
|
||||
13: "乙",
|
||||
}
|
||||
|
||||
@@ -268,6 +268,53 @@ def test_subtitle_translation_uses_full_document_and_is_cached(tmp_path, monkeyp
|
||||
assert requested[0][0] == [(0, "Hello."), (1, "Nice to meet you.")]
|
||||
|
||||
|
||||
def test_subtitle_translation_is_shared_with_other_users(tmp_path, monkeypatch):
|
||||
client = make_client(tmp_path)
|
||||
_ready_video(client.app.state.video_repository)
|
||||
requested = []
|
||||
|
||||
def translate_sentences(sentences, **kwargs):
|
||||
requested.append((sentences, kwargs))
|
||||
return {index: f"中文{index}" for index, _text in sentences}
|
||||
|
||||
class StubAITutor:
|
||||
pass
|
||||
|
||||
stub = StubAITutor()
|
||||
stub.translate_sentences = translate_sentences
|
||||
monkeypatch.setattr(client.app.state, "ai_tutor", stub)
|
||||
|
||||
first = client.post(f"/api/v1/videos/{VIDEO_HASH}/subtitle-translation")
|
||||
assert first.status_code == 200, first.text
|
||||
|
||||
stored = client.get(f"/api/v1/videos/{VIDEO_HASH}/subtitle-translation")
|
||||
assert stored.status_code == 200
|
||||
assert stored.json() == first.json()
|
||||
|
||||
other = TestClient(client.app)
|
||||
signup = other.post(
|
||||
"/api/v1/auth/register",
|
||||
json={"username": "viewer2", "password": "password-123"},
|
||||
)
|
||||
assert signup.status_code == 201, signup.text
|
||||
other.headers.update({"Authorization": f"Bearer {signup.json()['token']}"})
|
||||
|
||||
shared = other.get(f"/api/v1/videos/{VIDEO_HASH}/subtitle-translation")
|
||||
assert shared.status_code == 200
|
||||
assert shared.json()["translations"] == [
|
||||
{"index": 0, "zh_text": "中文0"},
|
||||
{"index": 1, "zh_text": "中文1"},
|
||||
]
|
||||
boundaries = other.get(f"/api/v1/videos/{VIDEO_HASH}/sentence-boundaries")
|
||||
assert boundaries.status_code == 200
|
||||
assert [item["zh_text"] for item in boundaries.json()["sentences"]] == ["中文0", "中文1"]
|
||||
|
||||
reused = other.post(f"/api/v1/videos/{VIDEO_HASH}/subtitle-translation")
|
||||
assert reused.status_code == 200
|
||||
assert reused.json() == first.json()
|
||||
assert len(requested) == 1
|
||||
|
||||
|
||||
def test_oss_video_url_is_used_for_catalog_and_content(tmp_path):
|
||||
index_path = tmp_path / "boundaries.json"
|
||||
index_path.write_text("{}", encoding="utf-8")
|
||||
|
||||
Reference in New Issue
Block a user