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