Repository navigation
章节梗概 (3/9):ChaptersProcessor gate 安全加固 r1–r14 - #17
Merged
Merged
Conversation
SRT/XML/transcript-api 三条字幕解析路径此前都存在同一类问题:单条 cue 的时间 轴损坏时,要么整条 cue(连同文本)被静默跳过不进 segments(SRT 路径),要么 一条脏数据直接让整批 segments 降级为 None(XML、transcript-api 路径的批量 try/except)。下游若按 segments 消费文本会静默丢字。 统一修复为按条目 (per-item) 容错:时间轴/属性损坏只让该条的 start_time / end_time 置为 None,text 原样保留并留在 segments 里;只有整体没有任何可识别 cue(或全部文本为空)时 segments 才整体为 None。同步更新 SubtitleResult 的 docstring 声明这条不变式。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…成真实时间戳 _to_seconds 此前对数值型/字符串型(含 "inf" 这种 float() 能悄悄解析的字符串) 的非有限值不做过滤,会当成真实秒数往下传;_format_timestamp 对 inf/nan 调用 int() 分别抛 OverflowError/ValueError。现在两处都用 math.isfinite 兜底,双重 防御(即使调用方绕过 _to_seconds 直接调 _format_timestamp 也不崩)。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_validate_and_normalize_start_segs 之前对 title/gist 用 str(x or "").strip() 硬转成空串,LLM 漏填、返回非字符串(如数字)或空白字符串都会照样成章。现在与 start_seg 校验对齐:缺失/非 str/strip 后为空一律判定语义校验失败,拼错误提示 重试一次,仍不合法则 FAILED。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
原逻辑只在 segments 为 None/空时跳过;非空 segments 但每条 start_time 都是 None(或解析不出)也会照常调 LLM 并可能返回 GENERATED,而章节功能的核心价值 就是时间范围,完全没有时间轴生成出来的章节没有意义。新增门控:至少一条 segment 能解析出 start_time 才继续,部分 None 仍容忍(下游 _derive_times 本 就支持单条 None)。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…,不再把 None 传给 LLM 客户端 LLMConfig.from_dict() 会把未配置的 chapters_model 默认成 calibrate_model,但 直接用 LLMConfig(...) 构造(跳过 from_dict,例如某些测试或调用方)时 chapters_model 仍是 None,处理器会拿 model=None 去调 LLM 客户端。现在在 process() 选模型之后补一条运行时兜底链:chapters_model -> summary_model -> calibrate_model,与 from_dict 的默认语义对齐,同时保留"已配置值优先"。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
原结构校验只在合并之前检查章节数量在 [2, 100] 区间,LLM 返回两个相邻同名章节 时能通过该检查(数量=2),但随后的 _merge_adjacent_same_title 会把它们合并成 1 章,最终仍以 GENERATED 返回,突破了"至少 2 章"这条不变式。现在合并后补一次 下限重查(合并只会减少数量,不需要重查上限),跌破则 FAILED,error 里带上 "合并后不足两章"的中文说明方便排查。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
本分支新增的 chapters_model/chapters_reasoning_effort/min_chapters_threshold/ max_chapters_input_chars 之前插在 dataclass 字段列表中部(speaker_model 之后、 质量配置区),会挤占其后既有可选字段的位置序号——老代码若按位置传参(例如 speaker 相关配置)会静默写进这些新字段。现在把这 4 个字段整体挪到字段列表最 末尾(total_timeout 之后),from_dict 用的是关键字参数构造,逻辑不变。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_looks_like_timeline_attempt 此前只要一行含 "-->" 就当作时间轴声明,导致 正文里偶然出现的 "-->"(如 "Settings --> Privacy" 这类操作指引文本)被误判 成下一条 cue 的时间轴,把该行从当前 cue 文本中错误地切走。现在只有紧跟在 纯数字索引行之后(预期时间轴位置)的 "-->" 行才会被当作(哪怕已损坏的) 时间轴声明,正文收集过程中遇到的 "-->" 行一律当普通文本处理。R1 引入的 "损坏时间轴仍保留 cue 文本" 行为不变,parse_srt_to_text 输出逐字节不变。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fingerprint 此前直接 "".join() 拼接各 segment 文本再 sha1,["ab","c"] 与 ["a","bc"] 会拼出同一个 "abc",指纹碰撞导致上层误判"原文未变化"。改用 "\x1f"(ASCII Unit Separator)分隔各 segment 文本再取 sha1;full_text 仍 用无分隔符拼接(供门控 3 长度判断),不受影响。fingerprint 尚未在任何生产 路径持久化,改变取值方式是安全的。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
transcriber/segments.py 的 parse_time_to_seconds 与 chapters_processor.py 的 _to_seconds 都声明"绝不抛异常",但当输入是 JSON 合法的超大整数(如 10**400,反序列化后是 Python int)时,int/float 分支里的 float(value) 会 抛 OverflowError(区别于字符串路径的 float() 会静默变成 inf)。两处均补上 try/except OverflowError,按非法时间处理返回 None,行为与已有的 非有限值/负数兜底一致。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
prompt 声明章节 title 不超过 20 字,但本地此前未做任何长度约束,纯靠 LLM 自觉遵守不可靠。超长标题本身不是语义错误,不值得为它触发重试或判定 FAILED——strip 后长度 > 24 字时本地截断为前 23 字 + "…",只记 warning 日志, 不影响 GENERATED 状态。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
四段冒号时间(如 "00:00:00:41")曾被 chapters_processor 私有 _to_seconds 的 手写解析逻辑误当成合法秒数(41s),因为它没有像 transcriber/segments.py 的 parse_time_to_seconds 那样校验 split 段数必须是 2 或 3。根因级修复:删除私有 重复解析逻辑,直接委托共享实现(transcriber 包不依赖 llm 包,无循环导入), 仅保留一层"调共享函数 + 记 warning 日志"的薄包装。共享实现已具备的全部防御 (非有限值/负数/OverflowError/段数校验)随之自动生效。 同步删除处理器侧因此重复的防御性测试(负数/非有限值/OverflowError 等,已被 test_segments_adapter.py::TestParseTimeToSeconds 覆盖),保留委托行为断言; 并给共享测试补上此前遗漏的 bool 输入用例。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
segments 时间乱序或坏数据会导致某章反查/合并出的 end_time 早于 start_time, 此前仍判 GENERATED 并原样产出这个非法区间。修复:新增 _sanitize_end_time, 在合并相邻同名章节之后、构造最终 Chapter 之前统一校验——若 start_time 与 end_time 均非 None 且 end_time < start_time,丢弃 end_time(置 None)并记 warning,状态照常 GENERATED;不重排 segments,不改变 start_seg/end_seg 的 顺序语义。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- get_subtitle_result() 的有效性判定由「仅看 text」改为「text 非空 或 segments 非空」:API Server 路径的 SRT 若正文全是数字,旧的逐字节兼容 文本提取会把纯数字行误判为序号行而跳过(text=""),但新的 segments 提取用 lookahead 正确保留了这些内容;只看 text 会把整条结果(含时间轴) 一并当作"无字幕"丢弃。本地分支的同类判定点同步统一口径;get_subtitle() (旧 str 入口)逐字节行为不变,未触碰。 - 新增共享 helper subtitle_types.sanitize_time_pair(),统一负 start / 负 duration(体现为负 end 或 end<start)/ 区间倒挂 三类不合法时间戳的诚实 降级规则(一律置 None,绝不影响文本保留),并在三条字幕解析路径 (youtube-transcript-api 分段、TikHub XML、SRT)复用,避免各自重复实现。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sanitize_time_pair 的权威实现从 downloaders/subtitle_types.py 挪到
transcriber/segments.py(时间工具统一家,parse_time_to_seconds 已在此,
chapters_processor 也从这里导入),subtitle_types.py 改为 re-export 保持
既有 import 路径可用。normalize_segments 逐条应用该校验:此前
{start_time:10, end_time:5} 这类倒挂区间会原样放行给下游,而三条字幕解析
路径早已做了同款清洗,唯独中央适配器漏了。
via [HAPI](https://hapi.run)
Co-Authored-By: HAPI <noreply@hapi.run>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
resolver 返回字符串 "1e309" 时 float() 静默溢出成 inf(不像超大整数 int 会抛 OverflowError),现有 try/except 完全拦不住,VideoMetadata.duration 会携带 非有限值暴露给下游;JSON 数值 1e309 反序列化后同样已是 inf。float() 转换 成功后补一道 math.isfinite 校验,非有限值按转换失败同等处理;顺带把负 duration(时长不可能为负)也纳入无效分支。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_parse_youtube_subtitle_xml 的 _safe_start 排序 key 只捕获了 float()
的 ValueError/TypeError,但 float("nan") 不会抛异常——一旦某条 XML
条目的 start="nan",sort() 会拿到字面量 nan 作为 key。NaN 与任何数
比较恒为 False,会破坏其它合法条目之间本应正确的相对顺序(如
[5, nan, 1] 排序后仍可能是乱序),而不只是这条脏数据自己排错位置。
改为 isfinite 校验后统一按 0 处理,语义与"不可解析字符串"一致。
via [HAPI](https://hapi.run)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: HAPI <noreply@hapi.run>
_build_subtitle_result_from_snippets(youtube-transcript-api 路径)与 _parse_youtube_subtitle_xml(TikHub XML 路径)此前都是先用未清洗的 start 算出 end = start + duration,最后才统一调用 sanitize_time_pair 清洗负 start。start=-5、duration=10 这类输入会先算出 end=5(非负, 能骗过 sanitize_time_pair 的"end 为负"规则),最终泄漏 (None, 5) —— 这个 end 本身毫无意义,因为它是从一个已知非法的起点派生出来的。 改为先清洗 start(负数置 None),end_time 只从已验证合法的 start 派生;start 无效则 end 直接 None,不再计算。两条路径同步修复。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…er=0 被吞 speaker=0 是合法的说话人 ID(FunASR 说话人分离结果里第一位说话人的 标签),但 `if speaker:` 把 0 当假值处理,导致该说话人的前缀在拼进 LLM prompt 时被静默省略。改为 `is not None`,与指纹计算侧 (`_compute_fingerprint`)已经在用的判断口径保持一致;str 强转后的 空串仍按"无说话人"省略,语义不变。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
新增三个测试锁定 P2 发现:segments 列表混入 None/字符串等非 dict 条目时 process() 崩溃、以及内部异常未被兜底捕获会直接向外抛出。此提交下这些 测试处于红灯状态,下一个提交实现修复使其转绿。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
两层修复:① 入口过滤非 dict 条目(记 warning 含过滤数量),语义与 transcriber/segments.py::normalize_segments 跳过非 dict 条目一致;过滤后 为空则走既有 SKIPPED_NO_TIMELINE 门控。② process() 主体拆到 _process_impl, 外层包一层 try/except Exception 兜底返回 FAILED(error 带异常信息,日志 带堆栈),作为接线批完成前防未知异常的最后一道防线;已知分支仍各自返回 对应诚实状态,不依赖这层兜底代劳。 via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Reviewed commit: 🤖 Review 门禁 — 判定:
|
📒 Review ledger state
完整数据保存在 |
This was referenced Jul 20, 2026
zj1123581321
changed the base branch from
stack/chapters-02-chapters-processor
to
main
July 21, 2026 02:22
zj1123581321
added a commit
that referenced
this pull request
Oct 15, 2026
章节梗概 (3/9):ChaptersProcessor gate 安全加固 r1–r14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
范围
Stacked PR 系列第 3 个(依赖 #16)。ChaptersProcessor 与字幕时间轴的 gate 评审加固第 1 批(r1–r14,16 个修复提交),全部为正确性/可靠性/安全修复:
配置/Schema 触点
LLMConfig 章节字段位置调整(移至可选位置参数末尾,避免移位既有参数)。
验证证据
uv run pytest tests/unit全绿(本地边界点已预验证)Stacked 结构