角色:Controller / App Server(无数据库)

端口 8012 · 面向客户端 · 编排 agent_server / data_server / user_manager
immersive_study_server 是系统的控制器:所有文章与学习相关功能都走它。它自身不持有任何数据库,而是编排下游服务、执行权限/可见性规则、并通过 SSE 推送长任务进度。所有路由挂在 /api/* 下,与原单体逐字节一致 文章上传、视频导入和全文重新解析默认每篇文章最多同时执行 16 个解析批次ANALYZE_CONCURRENCY: 16),每批默认 10 句ANALYZE_BATCH_SIZE: 10)。可通过控制器环境变量或 YAML 配置调整。批次完成后串行保存结果并更新进度;取消任务会停止提交新批次,已发出的请求仍会完成用量记账,但不再保存解析结果。此限制按文章计算,不是服务全局限制;DEFAULT_WORKERS 是旧接口兼容参数,不控制当前批量解析。

接口 /api/*

文章

方法 / 路径鉴权用途
GET /api/articles?genre=可选列表(在此套用可见性 + 用户名补全)
GET /api/articles/{id}可选详情 + 句子 + 实际成功解析模型 analysis_models
GET /api/me/article-favorites用户当前用户可见的整篇文章收藏,按最新优先
PUT /api/articles/{id}/favorite用户幂等收藏整篇文章
DELETE /api/articles/{id}/favorite用户幂等取消整篇文章收藏
GET /api/articles/{id}/stats可选句子统计
PATCH /api/articles/{id}用户编辑元信息
PATCH /api/articles/{id}/visibility用户切换公开
DELETE /api/articles/{id}用户/root删除
POST /api/articles/{id}/diff用户预览编辑 diff(不写库)
PATCH /api/articles/{id}/content用户应用编辑(任务 + SSE)
POST /api/articles/{id}/reparse用户基于 raw_content 重跑(任务 + SSE)

句子

方法 / 路径用途
GET /api/articles/{id}/explain/{sid}单句详情
GET /api/articles/{id}/explain/{sid}/prompt存储的/即时的 prompt
GET /api/articles/{id}/explain/{sid}/audio流式音频
POST /api/articles/{id}/explain/{sid}/audio生成指定缺失语音(任务 + SSE)
POST /api/articles/{id}/audio/missing生成全部缺失/失败的非空句语音(任务 + SSE)
GET / PUT /api/articles/{id}/explain/{sid}/note读/存笔记
POST /api/articles/{id}/explain/{sid}/regenerate重新生成讲解(任务 + SSE)

处理与任务

方法 / 路径用途
GET /api/models去重模型名、有效默认、routing_ready/readiness_code(转发 agent)
POST /api/articles/upload上传并处理;可选 primary_modelexplanation_level(默认 N3,任务 + SSE)
POST /api/videos/file校验大小/扩展名,流式写入 data_server 临时存储并返回 file_handle
POST /api/videos/uploadinput_handle 转写并处理;可选 explanation_level,保留 keep_audio(任务 + SSE)
GET /api/tasks · GET /api/tasks/{id}任务列表 / 详情
GET /api/tasks/{id}/progressSSE 进度流
POST /api/tasks/{id}/cancel取消任务

其他

方法 / 路径用途
GET /api/verb-forms?locale= · GET /api/verb-forms/{form_id}?locale=四语静态动词变形资料;locale 支持 zhzh-Hantenja,默认 zh
GET /api/grammar-cards?locale=&category=&jlpt_level=无需登录的语法目录;分类为 verbadjectivepredicatejlpt,等级为 N1N5,两个筛选条件取交集
GET /api/grammar-cards/{card_id}?locale=无需登录的单卡元数据及 Markdown 正文;语言取值与动词接口一致
POST /api/feedback提交反馈
GET /api/admin/articles管理端文章列表(仅 root,含 analysis_models
整篇文章收藏由本 Controller 转发到 data_server;句子卡片收藏仍由客户端直连 collection_server(见 collection_server)。文章列表、详情和每周推荐对登录用户返回 is_favorite,访客固定为 false

静态语法卡片

以下静态语法/动词资产的编辑源是独立的 RakullDataAssetsruntime/ 子树;monorepo 不再内置它们,运行前由 scripts/fetch_data_assets.sh 钉版物化到 rakull_server/.data-assets/runtime/(下文中的相对路径即该目录)。发布到 COS/CDN 的完整流程见数据资产仓库与 COS/CDN 同步
目录保留原有 16 张基础卡和稳定 ID,增加 739 张 JLPT 卡片,ID 按原书等级及编号固定,例如 jlpt-n1-001。目录清单位于 .data-assets/runtime/grammar_cards/jlpt/catalog.json,新卡正文位于同目录下的 zh/{card_id}.md。按书分级不表示官方考试大纲;原书、提取资料和校审过程文件不随服务发布。 列表仍直接返回数组,不包含正文。列表和详情均提供 jlpt_levelsource_numbersource_groupsummaryaliasessort_orderrelated_card_ids,供客户端筛选、搜索、分组及关联跳转。基础卡按书内直接对应规则分为 N5(8 张)和 N4(8 张),原书编号仍为 null;因此全库等级计数为 N5 128、N4 138、N3 139、N2 152、N1 198,而 category=jlpt 的原书编号卡计数仍为 120、130、139、152、198。 16 张基础卡的 summary 按请求的 locale 返回四语言静态摘要,列表和详情保持一致,无需读取正文。JLPT 摘要仍取静态目录中的简体中文内容,原字段含义不变。 列表和详情新增 brief_summary: stringtopic_ids: string[]。短摘要与原 summary、正文独立维护,基础卡支持四语言,JLPT 使用简中。739 张 JLPT 展示元数据位于 .data-assets/runtime/grammar_cards/jlpt/presentation.tsv,以稳定 ID 显式映射;基础卡在 grammar_card_presentation.py 中维护。启动时严格校验 ID 全覆盖、唯一、非空短摘要及主题合法性,不做运行时关键词分类或正文截断。 主题 ID 为 timeconditioncausepurposecontrastcomparisonemphasisinferenceobligationrequestgivingpolitenessstructurevocabulary。保留全部旧 ID 和字段含义;新界面的“表达用途”只展示前 11 个主题,不展示后三个混合分类。 列表与详情还提供 reference_ids: string[],规则索引 ID 为 verb-formsadjective-formsparticlessentencehonorificsnumbersexpressions.data-assets/runtime/grammar_cards/lookup.tsv 为全部 755 个 ID 显式维护规则索引和补充别名;aliases 列使用 JSON 数组,读取时不按 CSV 引号解码。启动时检查 ID 完整且唯一、索引合法、别名非空且不重复,以及每张至少有一个表达用途或规则索引。补充别名与原目录别名稳定合并去重,不删除原别名。维护时逐卡核对真实的表记、读音或缩略形式,不将近义句型或任意例句当别名。 难度、用途和规则各选一个,交集筛选与多片段搜索均在已加载的客户端目录上完成,无新增查询参数或数据库。桌面把难度作为侧栏第三组,小屏在统一的分类筛选面板中展示三组;不再提供 ungraded。单卡浏览有独立的选择状态,也可直接浏览速查结果。详情索引链接清除旧关键词与其他筛选,进入该索引完整集合。两种模式只使用目录元数据,打开完整讲解才读取正文,不记录学习进度。 搜索要求所有片段命中同一标题、同一别名或同一摘要,统一全/半角、平/片假名和英文大小写。新增审核别名支持「なきゃ」「なくちゃ」及「得る」的两种读音,但不提供整句解析或自动汉字读音推导。索引是可重叠的查阅入口,不是新的等级或官方课程体系。 locale 始终保留请求语言;content_locale 标明实际正文语言,is_fallback 标明是否发生回退。JLPT 首批仅有简中内容,其他三种语言请求可显式回退到 zh;基础卡仍严格读取所请求的语言版本。正文缺失时列表返回 available: falsecontent_locale: nullis_fallback: false,详情返回 404;非法语言、分类或等级返回 422。打开卡片不调用 Agent,不新增数据库或生成接口。 更新静态内容后运行仓库根目录的 python3 scripts/jlpt_card_content.py validate,检查 739 张资源的数量、编号、结构和关联目标;本地存在五份大纲时还会交叉核对标题与分组。服务测试继续覆盖旧的 13 张动词接口及四语言基础卡。

编排与 SSE

本节假定你了解 SSE 的基本通信方式;入门说明见 SSE 与流式进度 长任务(上传、视频、编辑、重生成、按需 TTS)由控制器创建一个内存任务,立即返回 task_id。活动 TTS 任务会把文章详情中对应句子覆盖为 generating 并附上 tts_task_id。重复的同范围请求复用活动任务;批量逐句隔离失败并继续,且不会修改文章状态。文章处于 processing 但控制器已没有对应活动任务超过保护窗口时,会被收尾为 failed,避免服务重启后永久显示处理中。 文章与视频上传只持久化用户选择的 primary_model,不暴露 Endpoint/Binding。客户端在提交前刷新模型目录;如果已选模型失效,会要求重新选择。Controller 在任务开始时通过短期服务断言从 data_server 读取一次历史 Token 成本画像,并让任务内的分析与总结复用同一快照;读取失败回退输入/输出 1:1,不阻断任务。Agent 返回的结构化路由错误会原样进入任务 error_code,客户端据此显示“AI 模型路由尚未配置”,不再从错误字符串猜测 Key。 上传接口的 explain_mode 只接受 fullskipsplit。独立的 explanation_level 接受 N1N2N3N4N5es_lowes_highjhhs,会持久化到文章,并由编辑、重解析、Prompt 预览和重新生成沿用。上传调用方不再传并发数、模型重试数、视频润色重试数或开发者模式;Controller 与 Agent 继续使用服务端配置的重试策略。 文章自动 TTS 在管理后台 AI APIs → Speech & automation 设置。每个上传/视频/重解析任务在开始时固定一次 tts_enabled 快照,中途切换不会改变已经运行的任务;手动 TTS 每次请求都读取当前开关,关闭时返回 503。后台覆盖保存在 .data/runtime-settings.json,使用 revision、原子替换和 0600 权限;TTS_ENABLED 环境/YAML 值仅作部署回退。Controller 默认给每批 Azure 合成 180 秒;供应商整批失败或单句写入对象存储失败时,会逐句记录错误并把文章收尾为 partially_completed,不会丢掉已经完成的正文。 视频/音频上传不会在 Controller 的持久目录留下副本。Controller 把流转发给 data_server,任务转写前换取 10 分钟签名 URL,再携带短期服务断言调用 Agent。成功后按 keep_audio 把临时 handle 转入用户 sources/ 并记录文章 metadata,或立即删除;失败与取消也会执行清理。旧 input_path 只在下游明确报告 local 后端时兼容。 动词变形参考保存在 .data-assets/runtime/verb_type/{locale}/{form_id}.mdlabel 是写入 AI 讲解的稳定简中机器标签,display_label 与 Markdown content 按请求的界面语言返回;省略 locale 的旧客户端继续得到简中内容。Controller 只读取静态文件,打开卡片不会触发 Agent 调用。
SSE 进度只在控制器这一层流式输出。若前面有网关/代理,务必不要缓冲 /api/tasks/*/progress(关闭 X-Accel-BufferingCache-Control: no-cache)。

环境变量

下表变量同样是该服务根目录下的 YAML 配置键(键名一致):config.yaml 存非敏感默认值(已提交),config.local.yaml 存密钥与本机覆盖(已 gitignore,可从 config.example.yaml 复制)。优先级(高 → 低):环境变量 > config.local.yaml > config.yaml > 代码默认值。JWT_SECRET 等密钥放进 config.local.yamlASSETS_DIR 等机器相关路径默认在代码中,需要时在 config.local.yaml 覆盖。
变量说明默认
DATA_SERVER_URLdata_server 地址http://localhost:8014
AGENT_SERVER_URLagent_server 地址http://localhost:8011
USER_MANAGER_URLuser_manager 地址http://localhost:8010
JWT_SECRET校验/转发 Bearer 的共享密钥change-me-in-config-local
TTS_ENABLED自动/手动 TTS 的部署回退;后台值优先true
TTS_REQUEST_TIMEOUTController 等待一批 Agent TTS 的秒数180.0
CONTROLLER_RUNTIME_CONFIG_PATH后台运行时设置文件.data/runtime-settings.json

运行

cd rakull_server/immersive_study_server
JWT_SECRET=dev-secret \
DATA_SERVER_URL=http://localhost:8014 \
AGENT_SERVER_URL=http://localhost:8011 \
USER_MANAGER_URL=http://localhost:8010 \
uv run uvicorn app.main:app --port 8012

结构化任务进度

长任务通过 GET /api/tasks/{task_id} 和 SSE 公开同一份最新快照。客户端应使用 stagestatusprogresscompletedfailedskippedtotalerror_codeevent_idupdated_at 渲染进度,不应展示服务端日志文案。 阶段区间依次为:preparing 0-5、sentence_splitting 5-15、 summarization 15-25、translation_explanation 25-75、 tts_generation 75-95、finalizing 95-99。只有结果持久化成功后终态才能达到 100。 终态包括 completedpartially_completedfailedcancelled;其中 partially_completed 表示文本可读,但部分语音生成失败。SSE 首帧始终是当前快照, 重连可发送 Last-Event-ID,无法回放历史时仍返回最新快照。 完整处理使用批量联合 Prompt,同时生成翻译、注音和讲解,因此统一显示为 “翻译和讲解中”。默认每批最多 10 句,只有整批返回后才增加完成计数;这是减少请求量 和避免触发模型 RPM 限制的有意取舍。跳过讲解模式仍使用 translation 25-50。

Reader 语法卡片关联(#72,默认关闭)

GRAMMAR_LINKS_ENABLED=false。即使配置为 true,也必须先完成所有 755 张 .data-assets/runtime/grammar_cards/recognition.json 规则的人工审核,并提供同目录下通过门槛的 recognition-evaluation.json。当前提交包含规则草案和评测基础设施,没有人工审核或真实模型通过报告。 规则不能把 lookup.tsv 的人工速查别名直接视为已审核识别触发词。 关联仅跟随成功的完整语法生成、重解析、文章修改重分析及单句重新生成。 普通翻译、注音补全、读取文章/讲解不调用判别模型。旧文章通过现有重解析入口补充关联。 Controller 维护索引;Agent 的 POST /v1/agent/grammar/tokenize 返回完整 GiNZA catalog(包含助词和助动词),POST /v1/agent/grammar/select 负责一次候选判别。 两者沿用 JWT 鉴权,前端不访问内部服务。 每句最多 24 张候选,每张最多 16 个原文位置,候选 JSON 最多 3000 UTF-8 字节 (作为 byte-token tokenizer 的保守 token 上界)。长句上限 12000 字符,文章简介上下文 截取前 2000 字符;仅有候选才调用模型。判别使用文章选定模型的一次 provider attempt, 默认 15 秒预算,不重试、不追加 fallback。模型输出异常不撤销已有讲解。 GRAMMAR_LINKS_MAX_CANDIDATESGRAMMAR_LINKS_TOKEN_BUDGETGRAMMAR_LINKS_TIMEOUT_SECONDS 可下调,不能突破上述上限。 模型只能选择 card_id 和该候选的 occurrence_id,并给出多个 topic_ids 与简短 reason;Controller 校验后映射为 start_utf16 / end_utf16 半开区间和 matched_text。检索的 NFKC 文本保留原文映射,不把归一化字符串位置直接交给 Flutter。 相同卡片同一范围去重,不合并不同 ID 的同名卡,也不按 JLPT 等级排除候选。 句子详情和重新生成任务结果新增 grammar_matchesgrammar_status;状态为 not_generatedno_candidatesresolvedinvalidfailedstale。 Data Server 使用句子的 grammar_record JSON 存储关联和版本签名;新增 PUT /v1/data/articles/{article_id}/sentences/{idx}/grammar 接收 revisionsentencerecord,不匹配当前原文版本返回 409。 迁移兼容既有数据库;原文、简介、输出语言变化会原子清空关联并刷新随机版本。 新建/替换句子也获得新版本,防止原文改回原值后旧任务写入。 签名包含原句、实际简介上下文、输出语言、索引、parser、prompt 和实际模型/路由版本。 读取时过滤过期索引与上下文,不在读取时重算。当前版本每次主动生成都重新判别, 不跨生成复用模型结果,避免路由更新后使用旧缓存。静态规则及审核报告按部署加载,更新后重启 Controller。 grammar_selection 使用现有 completion trace 和用量记录,日志记录候选量、预算、耗时及 拒绝数量,不默认输出用户原文。

内容审核与评测

每张规则均需填写审核者、触发模式、接续/排除条件、区分说明,以及正反例。 token_posprevious_pos 使用 GiNZA universal POS,inflection 匹配词形标记; 短触发词必须带词性或词形约束。标题自动提取的草案必须由审核者检查,不能批量改成 reviewed。 留出集 JSONL 每行包含 case_idtextsplit: "holdout"review_statusreviewerexpected(卡片 ID 和 UTF-16 区间)及 negative_card_ids。 预测 JSONL 必须包含每个 case,失败也不能删行;字段包括 case_idsource_hashgrammar_retrieval.digest(text))、candidatesmatchesindex_versionprompt_versionmodel_version。候选按位置展开;两份数据分别保存,禁止从规则匹配结果反推标准答案。
# 在 immersive_study_server 目录下执行,无模型调用
PYTHONPATH=. uv run python scripts/evaluate_grammar_links.py \
  --labels /path/to/reviewed-holdout.jsonl \
  --predictions /path/to/model-predictions.jsonl \
  --output /path/to/evaluation.json
评测输出逐卡统计、候选召回率、最终准确率/召回率及负例误报率。 上线要求 755 张均有经审核正反例,留出集至少 1510 条、候选召回率 ≥95%、 最终准确率 ≥98%、最终召回率 ≥85%。没有实际标注和预测时不填造指标。 覆盖「って」「ば」「そうだ」、敬语、变形、助数词、缩略形式、普通词偶然子串、 同名卡、重复位置、多义、非法模型输出、归一化与 emoji 偏移。 collect_grammar_predictions.py 可显式采集真实预测:从环境变量 GRAMMAR_EVAL_TOKEN 读取凭证,传入 --cases--output 和可选的 --primary-model / --primary-binding-id。它会产生真实模型费用,因此不在测试或 Reader 读取流程中自动运行;逐条保留失败记录与完成用量元数据。 --allow-drafts 仅用于开发评估,不绕过上线审核门槛。 规则还支持 kind: "token":以完整 token 为候选片段,通过 token_posinflectionprevious_pos 识别命令形等基础构形和数词后的助数词,不要求列举每个动词。 未指定 kind 时沿用固定片段匹配。两种规则都需要人工审核,基础构形和助数词草案同样不例外。