Improve EPUB restoration, book terminology and Calibre QA workflow

This commit is contained in:
Kai Tan
2026-10-08 07:25:27 +08:00
parent 40491e6386
commit caf9d8f4bb
35 changed files with 1439 additions and 959 deletions
+30 -7
View File
@@ -44,7 +44,8 @@ class LLMClient:
def __init__(self, api_key: str, base_url: str, model: str = "gpt-3.5-turbo",
requests_per_minute: int = 60, concurrent_requests: int = 5,
extra_headers: Dict = None, chunk_dir: Path = None):
extra_headers: Dict = None, chunk_dir: Path = None,
timeout: float = 180, max_output_tokens: int = 16000, temperature=None):
# Configure proxy client to avoid SOCKS issues and ensure connectivity
import httpx
@@ -54,7 +55,7 @@ class LLMClient:
proxy_url = os.environ.get("http_proxy") or os.environ.get("https_proxy")
http_client = httpx.AsyncClient(
proxy=proxy_url,
timeout=60.0,
timeout=timeout,
follow_redirects=True
) if proxy_url else None
@@ -62,9 +63,13 @@ class LLMClient:
base_url=base_url,
api_key=api_key,
default_headers=extra_headers,
http_client=http_client
http_client=http_client,
timeout=timeout,
max_retries=0
)
self.model = model
self.max_output_tokens = max_output_tokens
self.temperature = temperature
self.rate_limiter = RateLimiter(requests_per_minute, concurrent_requests)
self.prompts = self._load_prompts()
@@ -76,7 +81,7 @@ class LLMClient:
def _load_prompts(self) -> Dict:
try:
with open("config/prompts.json", "r", encoding="utf-8") as f:
with open(Path(__file__).resolve().parents[2] / "config/prompts.json", "r", encoding="utf-8") as f:
return json.load(f)
except Exception as e:
logger.error(f"Failed to load config/prompts.json: {e}")
@@ -130,7 +135,7 @@ Output Format:
except Exception as e:
logger.error(f"Translation failed: {e}")
return {item.entry_id: f"[Error - {str(e)}]" for item in items}
raise
def _build_prompt_with_short_ids(self, items: List[ManifestEntry]) -> tuple:
"""
@@ -156,6 +161,8 @@ Output Format:
Returns: Dict[entry_id, translated_text]
"""
results = {}
seen = set()
duplicates = set()
for line in response.split("\n"):
line = line.strip()
@@ -175,16 +182,26 @@ Output Format:
if sub_match:
# Check if the inner ID matches the outer ID, or just strip it anyway
# Usually LLM repeats the same ID.
if sub_match.group(1) != match.group(1):
content = ''
break
content = sub_match.group(2).strip()
else:
break
if not content:
continue
if short_id in id_map:
full_id = id_map[short_id]
if short_id in seen:
duplicates.add(full_id)
seen.add(short_id)
results[full_id] = content
else:
logger.warning(f"Unknown short ID in response: {short_id}")
for full_id in duplicates:
results.pop(full_id, None)
return results
def _save_chunk(self, stage: str, content: str, system_prompt: str = None,
@@ -215,15 +232,21 @@ Output Format:
async def _make_request(self, system_prompt: str, user_prompt: str) -> str:
await self.rate_limiter.acquire()
try:
options = {"max_completion_tokens": self.max_output_tokens}
if self.temperature is not None:
options["temperature"] = self.temperature
resp = await self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt}
],
temperature=0.3,
**options,
)
return resp.choices[0].message.content.strip()
choice = resp.choices[0]
if choice.finish_reason != "stop" or not choice.message.content:
raise ValueError(f"Incomplete model response: {choice.finish_reason}")
return choice.message.content.strip()
finally:
self.rate_limiter.release()