add readming mode
This commit is contained in:
@@ -25,6 +25,8 @@ AI_API_URL=https://api.example.com/v1/chat/completions
|
||||
AI_API_KEY=replace-with-your-ai-api-key
|
||||
AI_MODEL=gpt-4o-mini
|
||||
AI_TIMEOUT_SECONDS=45
|
||||
# 后台批量翻译可使用更长时间;实际翻译超时 = max(AI_TIMEOUT_SECONDS, AI_TRANSLATION_TIMEOUT_SECONDS)
|
||||
AI_TRANSLATION_TIMEOUT_SECONDS=120
|
||||
|
||||
# 火山引擎对象存储(TOS / 火山 OSS)。桶请设置为公共读,或把下方地址配置为可公开访问的 CDN 域名。
|
||||
VOLCANO_OSS_ACCESS_KEY=replace-with-volcano-access-key
|
||||
|
||||
@@ -21,7 +21,7 @@ _TRANSLATE_BATCH_CHARS = 2_200
|
||||
_TRANSLATE_CONTEXT_BEFORE = 8
|
||||
_TRANSLATE_CONTEXT_AFTER = 4
|
||||
_TRANSLATE_WORKERS = 4
|
||||
_TRANSLATE_TIMEOUT_SECONDS = 45.0
|
||||
_TRANSLATE_TIMEOUT_SECONDS = 120.0
|
||||
_TRANSLATE_SPLIT_THRESHOLD = 4
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class AITutor:
|
||||
api_key: str
|
||||
model: str
|
||||
timeout_seconds: float
|
||||
translation_timeout_seconds: float = _TRANSLATE_TIMEOUT_SECONDS
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@@ -301,13 +302,26 @@ class AITutor:
|
||||
user_parts.append(
|
||||
"输出示例:{" + ",".join(f'"{index}":"中文"' for index in expected_indexes[:2]) + "}"
|
||||
)
|
||||
user_prompt = "\n".join(user_parts)
|
||||
content = self._complete(
|
||||
system_prompt,
|
||||
"\n".join(user_parts),
|
||||
user_prompt,
|
||||
temperature=0.2,
|
||||
timeout_seconds=max(self.timeout_seconds, _TRANSLATE_TIMEOUT_SECONDS),
|
||||
timeout_seconds=max(self.timeout_seconds, self.translation_timeout_seconds),
|
||||
)
|
||||
parsed = _parse_translation_payload(content, expected_indexes)
|
||||
try:
|
||||
parsed = _parse_translation_payload(content, expected_indexes)
|
||||
except AITutorError:
|
||||
logger.warning(
|
||||
"Translation response parse failed: endpoint=%s model=%s "
|
||||
"expected_indexes=%s prompt=%r raw_content=%r",
|
||||
self.request_url,
|
||||
self.model,
|
||||
expected_indexes,
|
||||
user_prompt[:2000],
|
||||
content[:2000],
|
||||
)
|
||||
raise
|
||||
source_by_index = {index: text for index, text in batch}
|
||||
missing = [
|
||||
index
|
||||
@@ -315,6 +329,25 @@ class AITutor:
|
||||
if translation_needs_repair(source_by_index.get(index, ""), parsed.get(index, ""))
|
||||
]
|
||||
if missing:
|
||||
invalid = [
|
||||
{
|
||||
"index": index,
|
||||
"source": source_by_index.get(index, ""),
|
||||
"translation": parsed.get(index, ""),
|
||||
}
|
||||
for index in missing
|
||||
]
|
||||
logger.warning(
|
||||
"Translation response failed validation: endpoint=%s model=%s "
|
||||
"expected_indexes=%s parsed=%r invalid=%r prompt=%r raw_content=%r",
|
||||
self.request_url,
|
||||
self.model,
|
||||
expected_indexes,
|
||||
parsed,
|
||||
invalid,
|
||||
user_prompt[:2000],
|
||||
content[:2000],
|
||||
)
|
||||
raise AITutorError("AI translation returned an unexpected response.")
|
||||
return {
|
||||
index: _apply_translation_terms(source_by_index.get(index, ""), parsed[index], glossary)
|
||||
@@ -348,6 +381,16 @@ class AITutor:
|
||||
},
|
||||
timeout=timeout,
|
||||
)
|
||||
except requests.Timeout as exc:
|
||||
logger.warning(
|
||||
"AI request timed out: endpoint=%s model=%s timeout_seconds=%s",
|
||||
self.request_url,
|
||||
self.model,
|
||||
timeout,
|
||||
)
|
||||
raise AITutorError(
|
||||
f"AI service request timed out after {timeout:g} seconds."
|
||||
) from exc
|
||||
except requests.RequestException as exc:
|
||||
logger.exception(
|
||||
"AI request failed: endpoint=%s model=%s",
|
||||
@@ -454,11 +497,13 @@ def _parse_translation_payload(content: str, expected_indexes: list[int] | int)
|
||||
else:
|
||||
expected = list(expected_indexes)
|
||||
text = _JSON_FENCE_RE.sub("", content.strip()).strip()
|
||||
parse_errors: list[str] = []
|
||||
for candidate in (text, *_json_snippets(text)):
|
||||
try:
|
||||
parsed = _translations_from_json(json.loads(candidate), expected)
|
||||
except (TypeError, ValueError):
|
||||
except (TypeError, ValueError) as exc:
|
||||
parsed = None
|
||||
parse_errors.append(str(exc))
|
||||
if parsed:
|
||||
return parsed
|
||||
|
||||
@@ -482,6 +527,14 @@ def _parse_translation_payload(content: str, expected_indexes: list[int] | int)
|
||||
}
|
||||
if len(by_order) == len(expected):
|
||||
return by_order
|
||||
logger.warning(
|
||||
"Translation payload parser exhausted all formats: expected_indexes=%s "
|
||||
"json_parse_errors=%r numbered_line_keys=%s content=%r",
|
||||
expected,
|
||||
parse_errors,
|
||||
sorted(numbered),
|
||||
text[:2000],
|
||||
)
|
||||
raise AITutorError("AI translation returned an unexpected response.")
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ class Settings:
|
||||
ai_api_key: str
|
||||
ai_model: str
|
||||
ai_timeout_seconds: float
|
||||
ai_translation_timeout_seconds: float
|
||||
pass_score: float
|
||||
volcano_oss_access_key: str
|
||||
volcano_oss_secret_key: str
|
||||
@@ -71,6 +72,9 @@ class Settings:
|
||||
ai_api_key=os.getenv("AI_API_KEY", "").strip(),
|
||||
ai_model=os.getenv("AI_MODEL", "gpt-4o-mini"),
|
||||
ai_timeout_seconds=float(os.getenv("AI_TIMEOUT_SECONDS", "45")),
|
||||
ai_translation_timeout_seconds=float(
|
||||
os.getenv("AI_TRANSLATION_TIMEOUT_SECONDS", "120")
|
||||
),
|
||||
pass_score=float(os.getenv("ASSESSMENT_PASS_SCORE", "70")),
|
||||
volcano_oss_access_key=os.getenv("VOLCANO_OSS_ACCESS_KEY", "").strip(),
|
||||
volcano_oss_secret_key=os.getenv("VOLCANO_OSS_SECRET_KEY", "").strip(),
|
||||
|
||||
@@ -117,6 +117,7 @@ def create_app(
|
||||
api_key=service_settings.ai_api_key,
|
||||
model=service_settings.ai_model,
|
||||
timeout_seconds=service_settings.ai_timeout_seconds,
|
||||
translation_timeout_seconds=service_settings.ai_translation_timeout_seconds,
|
||||
)
|
||||
oss = oss_uploader or VolcanoOSSUploader(service_settings)
|
||||
processor = VideoProcessor(service_settings, video_repository, moss, oss)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import pytest
|
||||
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import threading
|
||||
|
||||
@@ -98,7 +99,29 @@ def test_ai_tutor_translates_full_document(monkeypatch):
|
||||
user_prompt = payload["messages"][1]["content"]
|
||||
assert "[0] Hello." in user_prompt
|
||||
assert "[1] Nice to meet you." in user_prompt
|
||||
assert timeout == 45.0
|
||||
assert timeout == 120.0
|
||||
|
||||
|
||||
def test_ai_tutor_translation_timeout_can_be_configured(monkeypatch):
|
||||
request_payloads = []
|
||||
|
||||
def fake_post(endpoint, headers, json, timeout):
|
||||
request_payloads.append((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,
|
||||
translation_timeout_seconds=180,
|
||||
)
|
||||
|
||||
translated = tutor.translate_sentences([(0, "Hello.")])
|
||||
|
||||
assert translated == {0: "中文0"}
|
||||
assert request_payloads[0][1] == 180
|
||||
|
||||
|
||||
def _translation_response_from_prompt(payload):
|
||||
@@ -248,3 +271,43 @@ def test_ai_tutor_retries_when_translation_stays_english(monkeypatch):
|
||||
|
||||
assert translated == {0: "你好。"}
|
||||
assert calls["count"] >= 2
|
||||
|
||||
|
||||
def test_ai_tutor_logs_raw_response_when_translation_json_is_invalid(monkeypatch, caplog):
|
||||
def fake_post(endpoint, headers, json, timeout):
|
||||
return FakeResponse(
|
||||
body={"choices": [{"message": {"content": "definitely not translation json"}}]}
|
||||
)
|
||||
|
||||
monkeypatch.setattr("sentence_api.ai_tutor.requests.post", fake_post)
|
||||
tutor = AITutor("https://ai.test/v1", "secret", "test-model", 1)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="sentence_api.ai_tutor"):
|
||||
with pytest.raises(AITutorError):
|
||||
tutor.translate_sentences([(0, "Hello.")])
|
||||
|
||||
assert any(
|
||||
"Translation response parse failed" in record.getMessage()
|
||||
and "definitely not translation json" in record.getMessage()
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
|
||||
def test_ai_tutor_logs_invalid_translation_before_repair(monkeypatch, caplog):
|
||||
def fake_post(endpoint, headers, json, timeout):
|
||||
return FakeResponse(
|
||||
body={"choices": [{"message": {"content": '{"0":"Still English."}'}}]}
|
||||
)
|
||||
|
||||
monkeypatch.setattr("sentence_api.ai_tutor.requests.post", fake_post)
|
||||
tutor = AITutor("https://ai.test/v1", "secret", "test-model", 1)
|
||||
|
||||
with caplog.at_level(logging.WARNING, logger="sentence_api.ai_tutor"):
|
||||
with pytest.raises(AITutorError):
|
||||
tutor.translate_sentences([(0, "Hello.")])
|
||||
|
||||
assert any(
|
||||
"Translation response failed validation" in record.getMessage()
|
||||
and "Still English." in record.getMessage()
|
||||
for record in caplog.records
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user