Ship native iOS app, Wiki/TTS backend, and skeleton loading UX.
Replace the WebView shell with SwiftUI screens, add account-scoped Wiki and TTS APIs with adaptive review and photo scan support, and keep web/iOS pages usable while data loads asynchronously. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
from config import get_settings
|
||||
|
||||
|
||||
class LlmServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LlmService:
|
||||
def is_configured(self) -> bool:
|
||||
return bool(get_settings().deepseek_api_key.strip())
|
||||
|
||||
def model_name(self) -> str:
|
||||
return get_settings().deepseek_model
|
||||
|
||||
def _chat_completion(
|
||||
self,
|
||||
*,
|
||||
messages: list[dict],
|
||||
temperature: float = 0.2,
|
||||
max_tokens: int = 1800,
|
||||
response_format: dict | None = None,
|
||||
timeout: int = 15,
|
||||
) -> str:
|
||||
settings = get_settings()
|
||||
api_key = settings.deepseek_api_key.strip()
|
||||
if not api_key:
|
||||
raise LlmServiceError("DeepSeek API key is not configured")
|
||||
|
||||
payload: dict = {
|
||||
"model": settings.deepseek_model,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
}
|
||||
if response_format:
|
||||
payload["response_format"] = response_format
|
||||
|
||||
request = urllib.request.Request(
|
||||
f"{settings.deepseek_base_url.rstrip('/')}/v1/chat/completions",
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=timeout) as response:
|
||||
body = json.loads(response.read().decode("utf-8"))
|
||||
except urllib.error.HTTPError as exc:
|
||||
detail = exc.read().decode("utf-8", errors="replace")
|
||||
raise LlmServiceError(f"DeepSeek HTTP {exc.code}: {detail}") from exc
|
||||
except urllib.error.URLError as exc:
|
||||
raise LlmServiceError(f"DeepSeek request failed: {exc.reason}") from exc
|
||||
|
||||
try:
|
||||
content = body["choices"][0]["message"]["content"]
|
||||
except (KeyError, IndexError, TypeError) as exc:
|
||||
raise LlmServiceError("DeepSeek response format is invalid") from exc
|
||||
return str(content).strip()
|
||||
|
||||
@staticmethod
|
||||
def _strip_code_fence(text: str) -> str:
|
||||
cleaned = text.strip()
|
||||
if not cleaned.startswith("```"):
|
||||
return cleaned
|
||||
cleaned = cleaned.strip("`").strip()
|
||||
if cleaned.lower().startswith("json"):
|
||||
cleaned = cleaned[4:].lstrip()
|
||||
elif cleaned.lower().startswith("markdown"):
|
||||
cleaned = cleaned[8:].lstrip()
|
||||
return cleaned.strip()
|
||||
|
||||
def enhance_wiki_page(self, *, title: str, draft_markdown: str) -> str:
|
||||
prompt = (
|
||||
"你是 WordLoop 个人学习 Wiki 的编译器,遵循 Karpathy Wiki 思路:"
|
||||
"把原始学习事件整理成简洁、可检索的 Markdown 知识页。\n"
|
||||
"要求:\n"
|
||||
"1. 保留草稿中的事实数据(释义、掌握度、练习记录等),不要编造。\n"
|
||||
"2. 优化结构、摘要与关联说明,便于日后复习。\n"
|
||||
"3. 只输出 Markdown 正文,不要代码块包裹,不要额外解释。\n"
|
||||
f"\n页面标题:{title}\n\n草稿:\n{draft_markdown}"
|
||||
)
|
||||
content = self._chat_completion(
|
||||
messages=[
|
||||
{"role": "system", "content": "你是严谨的学习笔记整理助手。"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.2,
|
||||
max_tokens=1800,
|
||||
)
|
||||
cleaned = self._strip_code_fence(content)
|
||||
return cleaned or draft_markdown
|
||||
|
||||
def parse_word_pairs_from_text(self, text: str) -> list[dict[str, str]]:
|
||||
prompt = (
|
||||
"你是英语学习词表整理助手。从 OCR 识别文本中提取英汉单词对。\n"
|
||||
"要求:\n"
|
||||
"1. 只提取明确的单词/短语与其中文释义,不要编造。\n"
|
||||
"2. 忽略页码、标题、序号、噪声行。\n"
|
||||
"3. 输出 JSON 对象,格式为 {\"items\":[{\"en\":\"...\",\"zh\":\"...\"}]}。\n"
|
||||
"4. en 为英文,zh 为中文;若原文是中文在前英文在后,也要正确归位。\n"
|
||||
"5. 只输出 JSON,不要解释。\n"
|
||||
f"\nOCR 文本:\n{text.strip()}"
|
||||
)
|
||||
content = self._chat_completion(
|
||||
messages=[
|
||||
{"role": "system", "content": "你只输出合法 JSON。"},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=0.1,
|
||||
max_tokens=4000,
|
||||
response_format={"type": "json_object"},
|
||||
timeout=30,
|
||||
)
|
||||
cleaned = self._strip_code_fence(content)
|
||||
try:
|
||||
payload = json.loads(cleaned)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise LlmServiceError("词表解析结果不是合法 JSON") from exc
|
||||
|
||||
raw_items = payload.get("items") if isinstance(payload, dict) else payload
|
||||
if not isinstance(raw_items, list):
|
||||
raise LlmServiceError("词表解析结果缺少 items 数组")
|
||||
|
||||
pairs: list[dict[str, str]] = []
|
||||
for item in raw_items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
en = str(item.get("en") or item.get("source_text") or "").strip()
|
||||
zh = str(item.get("zh") or item.get("target_text") or "").strip()
|
||||
if en and zh:
|
||||
pairs.append({"en": en, "zh": zh})
|
||||
return pairs
|
||||
|
||||
|
||||
llm_service = LlmService()
|
||||
Reference in New Issue
Block a user