Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### Fixed

- 章节笔记:章节目录产物现在记录实际锚定的数据来源(缓存结构化对话 / 原始转写分片),笔记按同一来源精确复现。此前目录标成「原始分片」却实际锚在缓存对话上,笔记必然指纹校验失败且无法生成(生产事故:BV1fcHD6cEw3)。标签声明的来源已不存在时直接报错,不再默默换成另一份数据。
- 章节目录:模型按主题弧线而非时间序返回章节时(每章时间锚点正确),章节层不再整体失败,改为按时间排序归一化(排序改变顺序时记入日志,含错位数与 LIS 统计)。此前此类输出会导致整个视频无章节目录(生产事故:BV1fcHD6cEw3)。
- 章节生成失败原因现在落盘到任务状态的 llm_status.json(chapters_error 字段),不再只出现在服务日志里;补跑成功后旧失败原因自动清除。

Expand Down
1 change: 1 addition & 0 deletions src/video_transcript_api/api/services/llm_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,7 @@ def _handle_llm_task(llm_task: dict):
speaker_count_hint=llm_task.get("cached_speaker_count"),
skip_chapters=skip_chapters_for_coordinator,
timeline_segments=timeline_segments_seed,
timeline_segments_kind=chapters_seed_kind,
)

# 适配返回格式
Expand Down
14 changes: 13 additions & 1 deletion src/video_transcript_api/llm/coordinator.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ def process(
contradiction_scan: Optional[bool] = None,
skip_chapters: bool = False,
timeline_segments: Optional[List[Dict]] = None,
timeline_segments_kind: Optional[str] = None,
) -> Dict:
"""处理文本(统一入口)

Expand Down Expand Up @@ -173,6 +174,10 @@ def process(
timeline_segments: 章节输入(dialogs 或原始 segments 列表)。由
llm_ops 按输入梯度解析后传入,保持 llm/ 包不依赖 cache 读盘。
None/空时 processor 会诚实返回 SKIPPED_NO_TIMELINE。
timeline_segments_kind: timeline_segments 的真实来源标签(调用方
已解析的事实,如 ``cached_dialogs`` / ``segments``)。标签会原样
写进 llm_chapters.json 的 source.kind,notes reader 据此精确复现
同一份锚点数据,缺失才默认按原始 segments 形态处理。

Returns:
处理结果字典:
Expand Down Expand Up @@ -314,7 +319,14 @@ def process(
else:
logger.info("Step 3/3: Chapters generation")
chapters_input = timeline_segments
chapters_source_kind = "segments" if timeline_segments else "none"
# notes_anchor_kind_truth: source.kind 必须是「实际喂给 chapters 的
# 锚点数据」的真实来源,而不是「入参长什么样」。调用方 llm_ops 按输入
# 梯度解析种子时已经知道它是缓存 dialogs 还是原始 segments,这里直接
# 采用调用方的事实;本轮 structured dialogs 覆盖种子时改写为 dialogs。
# 两者都是真实锚点,notes reader 才能据此精确复现(#227)。
chapters_source_kind = (
(timeline_segments_kind or "segments") if timeline_segments else "none"
)
structured_for_chapters = calibration_result.get("structured_data")
if isinstance(structured_for_chapters, dict):
dialogs = structured_for_chapters.get("dialogs")
Expand Down
31 changes: 21 additions & 10 deletions src/video_transcript_api/llm/processors/notes_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,28 +117,39 @@ def load_notes_source_segments(
``dialogs``/``cached_dialogs`` sources use ``llm_processed.json``. A
``segments`` source uses the shared ``load_segments`` adapter, preserving
the plain-text path's normalized list and original index order.

A declared ``source_kind`` is reproduced exactly: if the cache no longer
holds that anchor, the lookup fails as ``(None, None)`` instead of silently
substituting the other source, which would make notes hash a different
anchor than chapters did (#227). Only a product carrying no label at all
falls back to probing ``dialogs`` then ``segments``.
"""
cache_path = Path(cache_dir)
normalized_kind = (source_kind or "").lower()
wants_dialogs = normalized_kind in {

processed = _read_json_file(cache_path / "llm_processed.json")
dialogs = processed.get("dialogs") if processed else None
dialogs_source = dialogs if isinstance(dialogs, list) and dialogs else None

if normalized_kind in {
"dialogs",
"cached_dialogs",
"structured",
"plain_structured",
}
wants_segments = normalized_kind in {"segments", "timeline"}
}:
return (dialogs_source, "dialogs") if dialogs_source is not None else (None, None)

processed = _read_json_file(cache_path / "llm_processed.json")
dialogs = processed.get("dialogs") if processed else None
if (wants_dialogs or not wants_segments) and isinstance(dialogs, list) and dialogs:
return dialogs, "dialogs"
if normalized_kind in {"segments", "timeline"}:
segments = load_segments(cache_path)
if isinstance(segments, list) and segments:
return segments, "segments"
return None, None

if dialogs_source is not None:
return dialogs_source, "dialogs"
segments = load_segments(cache_path)
if isinstance(segments, list) and segments:
return segments, "segments"

if not wants_segments and isinstance(dialogs, list) and dialogs:
return dialogs, "dialogs"
return None, None


Expand Down
Loading
Loading