Preserve semantic note layouts and checkpoint on provider quota failures
This commit is contained in:
@@ -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')
|
||||
|
||||
|
||||
@@ -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] == '已完成。'
|
||||
|
||||
Reference in New Issue
Block a user