Files
john 05e173b293 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>
2026-06-08 15:13:59 +08:00

524 lines
11 KiB
Python

from typing import Optional
from pydantic import BaseModel, Field, computed_field
# Auth
class UserRegister(BaseModel):
username: str = Field(min_length=2, max_length=50)
password: str = Field(min_length=6, max_length=100)
class UserLogin(BaseModel):
username: str
password: str
remember: bool = True
class TokenResponse(BaseModel):
access_token: str
token_type: str = "bearer"
class UserOut(BaseModel):
id: int
username: str
created_at: str
class Config:
from_attributes = True
class UserUpdate(BaseModel):
username: Optional[str] = Field(None, min_length=2, max_length=50)
current_password: Optional[str] = Field(None, min_length=6, max_length=100)
new_password: Optional[str] = Field(None, min_length=6, max_length=100)
# Translate
class TranslateRequest(BaseModel):
text: str = Field(min_length=1)
class TranslateResponse(BaseModel):
source_text: str
target_text: str
source_lang: str
target_lang: str
phonetic: Optional[str] = None
example_en: Optional[str] = None
example_cn: Optional[str] = None
found: bool = True
# Words
class WordCreate(BaseModel):
source_text: str
target_text: str
source_lang: str
target_lang: str
phonetic: Optional[str] = None
example_en: Optional[str] = None
example_cn: Optional[str] = None
class WordScanTextRequest(BaseModel):
text: str = Field(min_length=1)
class WordScanItem(BaseModel):
source_text: str
target_text: str
source_lang: str = "en"
target_lang: str = "zh"
class WordScanResponse(BaseModel):
items: list[WordScanItem]
class WordBatchCreateRequest(BaseModel):
items: list[WordCreate] = Field(min_length=1, max_length=200)
class WordUpdate(BaseModel):
status: Optional[str] = None
class WordOut(BaseModel):
id: int
user_id: int
book_id: int = 0
book_entry_id: Optional[int] = None
source_text: str
target_text: str
source_lang: str
target_lang: str
phonetic: Optional[str] = None
example_en: Optional[str] = None
example_cn: Optional[str] = None
status: str
correct_count: int
wrong_count: int
consecutive_correct_count: int
mastery_score: int
train_count: int = 0
error_bank_member: int = 0
error_reinforce_streak: int = 0
error_bank_entered_at: Optional[str] = None
total_train_seconds: int = 0
difficulty: float = 5.0
stability_hours: float = 24.0
next_review_at: Optional[str] = None
review_due_date: Optional[str] = None
last_reviewed_at: Optional[str] = None
created_at: str
@computed_field # type: ignore[prop-decorator]
@property
def entered_at(self) -> str:
"""词库进入时间(与 created_at 一致)。"""
return self.created_at
class Config:
from_attributes = True
class WordListPageOut(BaseModel):
items: list[WordOut]
total: int
page: int
page_size: int
class WordBatchCreateResponse(BaseModel):
created: int
skipped: int
items: list[WordOut]
class MemoryCurvePoint(BaseModel):
day_index: int
date: str
forgetting: float
mastery: float
risk: float
class MemoryFutureRiskPoint(BaseModel):
day_offset: int
date: str
risk: float
class MemoryGraphNode(BaseModel):
id: str
label: str
zh: str
status: str
mastery: int
entered_at: str
size: int
class MemoryGraphLink(BaseModel):
source: str
target: str
kind: str
strength: float
class MemoryGraph(BaseModel):
nodes: list[MemoryGraphNode]
links: list[MemoryGraphLink]
class MemoryWordSummary(BaseModel):
id: int
en: str
zh: str
status: str
mastery_score: int
correct_count: int = 0
wrong_count: int = 0
train_count: int = 0
total_train_seconds: int = 0
entered_at: str
retention_now: float
risk_7d: float
class WordMemoryCurvePoint(BaseModel):
date: str
datetime: str
forgetting: float
mastery: float
risk: float
wrong_count: int
train_count: int
train_seconds: int
is_correct: Optional[bool] = None
class WordMemoryDetailResponse(BaseModel):
word_id: int
en: str
zh: str
correct_count: int
wrong_count: int
train_count: int
total_train_seconds: int
curve_points: list[WordMemoryCurvePoint]
future_risk: list[MemoryFutureRiskPoint]
class MemoryVisualizationResponse(BaseModel):
curve_points: list[MemoryCurvePoint]
future_risk: list[MemoryFutureRiskPoint]
words: list[MemoryWordSummary]
graph: MemoryGraph
# Quiz
class QuizOption(BaseModel):
label: str
text: str
class QuizQuestion(BaseModel):
word_id: int
question_type: str
prompt: str
options: list[QuizOption] = []
correct_answer: str
phonetic: Optional[str] = None
train_count: int = 0
wrong_count: int = 0
error_reinforce_streak: int = 0
error_clear_correct_count: int = 2
error_bank_member: int = 0
class DailyQuizResponse(BaseModel):
questions: list[QuizQuestion]
total: int
track: str = "accumulation"
book_id: Optional[int] = None
pool: str = "untrained"
class QuizAnswerRequest(BaseModel):
word_id: int
pool: Optional[str] = None
question_type: str
user_answer: str
correct_answer: str
duration_seconds: Optional[int] = Field(None, ge=0, le=3600)
class QuizAnswerResponse(BaseModel):
is_correct: bool
correct_answer: str
word: WordOut
class QuizStatsResponse(BaseModel):
total_words: int
new_count: int
learning_count: int
mastered_count: int
weak_count: int
today_quiz_count: int
today_correct_count: int
today_accuracy: float
daily_target: int
today_completed: int
streak_days: int
untrained_pending: int = 0
error_pending: int = 0
error_bank_due_pending: int = 0
error_bank_total: int = 0
track: str = "accumulation"
book_id: Optional[int] = None
class PracticePlanResetResponse(BaseModel):
reset_count: int
track: str = "accumulation"
book_id: Optional[int] = None
class ErrorBankWordOut(BaseModel):
word_id: int
en: str
zh: str
wrong_count: int
train_count: int
error_reinforce_streak: int
status: str
next_review_at: Optional[str] = None
review_due_date: Optional[str] = None
class ErrorBankListResponse(BaseModel):
items: list[ErrorBankWordOut]
total: int
due_count: int
track: str = "accumulation"
book_id: Optional[int] = None
# Word books
class WordBookOut(BaseModel):
id: int
slug: str
title: str
description: Optional[str] = None
level: str
word_count: int
unit_count: int
sort_order: int
daily_target: int
master_required_count: int
weak_wrong_threshold: int
learn_mode: str
class Config:
from_attributes = True
class WordBookUnitOut(BaseModel):
unit: int
title: Optional[str] = None
word_count: int
class WordBookDetailOut(WordBookOut):
units: list[WordBookUnitOut] = []
class WordBookProgressOut(BaseModel):
book_id: int
total: int
introduced: int
mastered: int
learning: int
locked: int
class BookPracticeSettingsOut(BaseModel):
book_id: int
daily_target: int
master_required_count: int
weak_wrong_threshold: int
error_clear_correct_count: int
class ActiveBookOut(BaseModel):
book: Optional[WordBookOut] = None
progress: Optional[WordBookProgressOut] = None
settings: Optional[BookPracticeSettingsOut] = None
class SetActiveBookRequest(BaseModel):
book_id: int = Field(ge=1)
class BookPracticeSettingsUpdate(BaseModel):
book_id: Optional[int] = Field(None, ge=1)
daily_target: Optional[int] = Field(None, ge=5, le=50)
master_required_count: Optional[int] = Field(None, ge=1, le=10)
weak_wrong_threshold: Optional[int] = Field(None, ge=1, le=10)
error_clear_correct_count: Optional[int] = Field(None, ge=1, le=5)
# Settings
class SettingsOut(BaseModel):
daily_target: int
master_required_count: int
weak_wrong_threshold: int
error_clear_correct_count: int
class Config:
from_attributes = True
class SettingsUpdate(BaseModel):
daily_target: Optional[int] = Field(None, ge=1, le=100)
master_required_count: Optional[int] = Field(None, ge=1, le=20)
weak_wrong_threshold: Optional[int] = Field(None, ge=1, le=20)
error_clear_correct_count: Optional[int] = Field(None, ge=1, le=5)
# Wiki
class WikiPagePreview(BaseModel):
slug: str
title: str
category: str
summary: str
content: str
source_count: int
updated_at: str
class WikiStatusOut(BaseModel):
enabled: bool
auto_organize: bool
page_count: int
link_count: int
last_organized_at: Optional[str] = None
latest_page: Optional[WikiPagePreview] = None
llm_configured: bool = False
llm_model: Optional[str] = None
compiler_mode: str = "deterministic"
daily_llm_limit: int = 20
llm_used_today: int = 0
llm_remaining_today: int = 20
quota_exceeded: bool = False
recharge_message: Optional[str] = None
class WikiGraphNodeOut(BaseModel):
id: str
slug: str
title: str
category: str
summary: str
source_count: int
updated_at: str
degree: int = 0
class WikiGraphLinkOut(BaseModel):
source: str
target: str
relation: str
class WikiGraphOut(BaseModel):
nodes: list[WikiGraphNodeOut]
links: list[WikiGraphLinkOut]
class WikiSettingsUpdate(BaseModel):
enabled: Optional[bool] = None
auto_organize: Optional[bool] = None
# Memory coach (Q/K/V dialogue)
class MemoryToken(BaseModel):
role: str
key: str
label: str
value: str
revealed: bool = False
class CoachWordBrief(BaseModel):
word_id: int
zh: str
phonetic: Optional[str] = None
retention_now: float
mastery_score: int
status: str
retrievability: float = 0.0
next_review_at: Optional[str] = None
class CoachSessionResponse(BaseModel):
words: list[CoachWordBrief]
total: int
class CoachTurnRequest(BaseModel):
word_id: int
stage: str = "intro"
user_message: str = ""
hints_used: int = Field(0, ge=0, le=20)
duration_seconds: Optional[int] = Field(None, ge=0, le=3600)
class MemoryTransformerCurvePoint(BaseModel):
hours_ahead: float
recall_percent: float
forgetting_percent: float
class MemoryTransformerAttention(BaseModel):
index: int
label: str
weight: float
class MemoryTransformerPredictResponse(BaseModel):
word_id: int
en: str
recall_now_percent: float
formula_recall_percent: float
model_recall_percent: float
blend_weight: float
event_count: int
model_trained: bool
recommended_review_days: int
half_life_hours: Optional[float] = None
curve: list[MemoryTransformerCurvePoint]
attention_hint: list[MemoryTransformerAttention]
class CoachTurnResponse(BaseModel):
assistant_messages: list[str]
tokens: list[MemoryToken] = []
stage: str
expect_input: bool
prompt_zh: str = ""
prompt_phonetic: Optional[str] = None
input_hint: str = "请输入对应的英文单词"
is_correct: Optional[bool] = None
quiz_recorded: bool = False
word_complete: bool = False
hints_used: int = 0
target_en: Optional[str] = None
target_zh: Optional[str] = None
review_message: Optional[str] = None
retrievability: Optional[float] = None
next_review_at: Optional[str] = None