Preserve semantic note layouts and checkpoint on provider quota failures

This commit is contained in:
Kai Tan
2026-10-08 09:35:22 +08:00
parent 4f124a0335
commit cab13568df
9 changed files with 111 additions and 7 deletions
+5 -1
View File
@@ -20,6 +20,8 @@ def fixture(tmp_path):
chapter.content = '''<html><head></head><body><h2 id="ch1"><span class="line">1</span>
<span class="subtitle">ROOTS</span></h2><p id="opening" class="noindent" style="text-indent:0 !important; color:red">Opening prose.</p>
<p id="caption" class="caption">A caption.</p><blockquote><p id="quote">Quoted text.</p></blockquote>
<div role="doc-credits"><p id="credit" style="text-indent:0 !important">1: Photo credit.</p></div>
<p role="doc-endnote" id="semantic-note" style="text-indent:0 !important">A semantic endnote.</p>
<p id="second">Second paragraph.<a epub:type="noteref" href="notes.xhtml#note">1</a></p></body></html>'''
part = epub.EpubHtml(uid='part', file_name='Text/part.xhtml', title='Part 1')
part.content = '<html><body><h1 id="part"><span>PART 1</span><br/><em>BOY GENIUS</em></h1></body></html>'
@@ -84,9 +86,11 @@ def test_headings_nav_ncx_contents_share_translation_and_keep_links(tmp_path,mod
p=ch.find(attrs={'data-translation-for':key}) if mode=='bilingual' else ch.find(id=key)
assert p['data-translation-role']=='body'
assert 'text-indent' not in p.get('style','')
for key in ['caption','quote']:
for key in ['caption','quote','credit','semantic-note']:
p=ch.find(attrs={'data-translation-for':key}) if mode=='bilingual' else ch.find(id=key)
assert p['data-translation-role']=='other'
if key in {'credit','semantic-note'}:
assert 'text-indent:0 !important' in p['style']
notes=BeautifulSoup(after.read(prefix+'Text/notes.xhtml'),'html.parser')
assert notes.find(id='note') and notes.find('a',href='ch.xhtml#second')
+48
View File
@@ -119,11 +119,13 @@ def test_automatic_glossary_does_not_force_generic_senses_or_implicit_surnames()
def test_auto_aliases_require_evidence_but_explicit_nicknames_remain():
from src.translation.glossary import sanitize_automatic_terms
terms, _ = sanitize_automatic_terms([
{'source': 'The Wall Street Journal', 'target': '《华尔街日报》', 'aliases': ['the Journal']},
{'source': 'Theresa May', 'target': '特蕾莎·梅', 'aliases': ['May']},
{'source': 'University of California, Berkeley', 'target': '加州大学伯克利分校', 'aliases': ['Berkeley', 'UC Berkeley']},
{'source': 'Masayoshi Son', 'target': '孙正义', 'aliases': ['Masa'], 'notes': 'Masa is his nickname.'}], set())
glossary = BookGlossary(terms)
assert not glossary.validate('In May he stayed at the Berkeley hotel.', '五月他住在伯克利酒店。')
assert not glossary.validate('A domestic journal reported. The journal said.', '一家国内期刊报道。该期刊说。')
assert glossary.validate('Masa studied at UC Berkeley.', '他去上学。')
assert glossary.validate('Theresa May visited.', '五月来访。')
@@ -333,3 +335,49 @@ def test_output_budget_and_truncation_handling(finish_reason):
else:
with pytest.raises(ValueError,match='Incomplete'):
asyncio.run(call)
@pytest.mark.parametrize('status', [401, 402, 403])
def test_access_errors_stop_retries_and_queued_requests(status):
import httpx
from openai import APIStatusError
from types import SimpleNamespace
from src.common.exceptions import ProviderAccessError
from src.translation.llm_client import RateLimiter
client = object.__new__(LLMClient)
client.model = 'test'; client.max_output_tokens = 100; client.temperature = None
client.rate_limiter = RateLimiter(0, 1)
calls = []
async def create(**kwargs):
calls.append(kwargs)
response = httpx.Response(status, request=httpx.Request('POST', 'https://example.test'))
raise APIStatusError('provider response', response=response, body={})
client.client = SimpleNamespace(chat=SimpleNamespace(completions=SimpleNamespace(create=create)))
async def run():
for _ in range(2):
with pytest.raises(ProviderAccessError, match=f'HTTP {status}'):
await client._make_request('system', 'source')
asyncio.run(run())
assert len(calls) == 1
def test_quota_failure_preserves_completed_translation_and_checkpoint():
from src.common.exceptions import ProviderAccessError
class Client:
model = 'test'
calls = 0
async def translate_chunk(self, items, **kwargs):
self.calls += 1
if self.calls > 1:
raise ProviderAccessError('API HTTP 402: quota exhausted')
return {items[0].entry_id: '已完成。'}
entries = [entry(f'<p id="p{i}">Plain source {i}.</p>') for i in range(3)]
for i, item in enumerate(entries): item.entry_id = str(i)
snapshots = []
translator = Translator(Client(), chunk_size=1, max_concurrent=1,
on_chunk_complete=lambda: snapshots.append([e.translated_text for e in entries]))
with pytest.raises(ProviderAccessError):
asyncio.run(translator.translate(entries, BookProfile(title='Test', author='Test')))
assert entries[0].translated_text == '已完成。'
assert not any(e.translated_text for e in entries[1:])
assert snapshots and snapshots[-1][0] == '已完成。'