e76fa586f1
Ship dual-track learning (daily accumulation vs textbook),沪教/商务词书 APIs and UI, native iOS wrapper with bundled H5, and production book import on deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
385 lines
8.2 KiB
Python
385 lines
8.2 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 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
|
|
total_train_seconds: int = 0
|
|
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 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
|
|
|
|
|
|
class DailyQuizResponse(BaseModel):
|
|
questions: list[QuizQuestion]
|
|
total: int
|
|
track: str = "accumulation"
|
|
book_id: Optional[int] = None
|
|
|
|
|
|
class QuizAnswerRequest(BaseModel):
|
|
word_id: int
|
|
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
|
|
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
|
|
|
|
|
|
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)
|
|
|
|
|
|
# Settings
|
|
class SettingsOut(BaseModel):
|
|
daily_target: int
|
|
master_required_count: int
|
|
weak_wrong_threshold: 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)
|
|
|
|
|
|
# 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
|
|
|
|
|
|
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
|