05e173b293
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>
524 lines
11 KiB
Python
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
|