From 8ad64a4d1b0c379da19a0154ce3d768e2e68f1ca Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Sun, 15 Mar 2026 22:58:02 +0800 Subject: [PATCH] fix(asr): use content_dir for subtitle output instead of video_path - Add content_dir field to ASRInput schema - Pass content_dir from pipeline when creating ASRInput - Modify _try_official_subtitle to use content_dir for output - Modify _run_whisper_basic to use content_dir for output - Add validation for empty video_path in Whisper fallback Root cause: DownloadProcessor sets content_file="" for video types, causing Path("").parent to resolve to current directory instead of content directory. Subtitle files were written to wrong location. Co-Authored-By: Claude Opus 4.6 --- kb_pipeline/pipeline.py | 2 ++ kb_pipeline/processors/asr.py | 20 ++++++++++----- kb_pipeline/schemas.py | 6 +++-- tests/test_processors/test_asr.py | 42 ++++++++++++++++++++----------- 4 files changed, 48 insertions(+), 22 deletions(-) diff --git a/kb_pipeline/pipeline.py b/kb_pipeline/pipeline.py index 111caf1..8088662 100644 --- a/kb_pipeline/pipeline.py +++ b/kb_pipeline/pipeline.py @@ -185,11 +185,13 @@ class Pipeline: # ASR needs video path from download step download_result = results.get("download", {}) video_path = download_result.get("content_file", "") + step_input = ASRInput( content_id=parsed.content_id, platform=parsed.platform, video_path=video_path, content_type=parsed.content_type, + content_dir=str(content_dir), ) elif step_name == "analyze": # Analyze needs metadata and content files diff --git a/kb_pipeline/processors/asr.py b/kb_pipeline/processors/asr.py index 824a94b..e9e72f8 100644 --- a/kb_pipeline/processors/asr.py +++ b/kb_pipeline/processors/asr.py @@ -172,9 +172,10 @@ class ASRProcessor: Note: 无字幕返回 None(不是抛异常),让流程继续到 Whisper """ - video_path = Path(input_data.video_path) - output_dir = video_path.parent - base_name = video_path.stem + # 使用 content_dir 作为输出目录(而不是从 video_path 推断) + output_dir = Path(input_data.content_dir) + # 使用 content_id 作为基础文件名 + base_name = f"{input_data.platform}_{input_data.content_id}_subtitle" # 输出文件路径 txt_path = output_dir / f"{base_name}.txt" @@ -343,11 +344,18 @@ class ASRProcessor: ASR 处理结果 Raises: - RuntimeError: Whisper.cpp 路径不存在或执行失败 + RuntimeError: Whisper.cpp 路径不存在或执行失败,或 video_path 为空 """ + # 检查 video_path 是否有效 + if not input_data.video_path: + raise RuntimeError( + "video_path 为空,无法执行 Whisper 语音识别。" + "请确保视频文件已下载(对于没有官方字幕的视频,需要先下载视频文件)。" + ) + video_path = Path(input_data.video_path) - output_dir = video_path.parent - base_name = video_path.stem + output_dir = Path(input_data.content_dir) + base_name = f"{input_data.platform}_{input_data.content_id}_whisper" # 获取 Whisper.cpp 路径 try: diff --git a/kb_pipeline/schemas.py b/kb_pipeline/schemas.py index 5553917..3b5b00f 100644 --- a/kb_pipeline/schemas.py +++ b/kb_pipeline/schemas.py @@ -198,11 +198,13 @@ class ASRInput(StepInput): 继承 StepInput 的基础字段,添加视频路径和内容类型。 Attributes: - video_path: 视频文件路径 + video_path: 视频文件路径(可能为空,用于 Whisper 降级) content_type: 内容类型,默认为 'video' + content_dir: 内容目录路径,用于输出字幕文件 """ - video_path: str = Field(..., description="视频文件路径") + video_path: str = Field(default="", description="视频文件路径(可能为空)") content_type: str = Field(default="video", description="内容类型") + content_dir: str = Field(..., description="内容目录路径") class ASROutput(StepOutput): diff --git a/tests/test_processors/test_asr.py b/tests/test_processors/test_asr.py index 599d8a3..59ca2bf 100644 --- a/tests/test_processors/test_asr.py +++ b/tests/test_processors/test_asr.py @@ -31,7 +31,8 @@ class TestASRInputSchema: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path="/path/to/video.mp4" + video_path="/path/to/video.mp4", + content_dir="/path/to/content" ) # 验证继承 @@ -46,7 +47,8 @@ class TestASRInputSchema: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path="/path/to/video.mp4" + video_path="/path/to/video.mp4", + content_dir="/path/to/content" ) assert hasattr(input_data, "video_path") @@ -59,7 +61,8 @@ class TestASRInputSchema: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path="/path/to/video.mp4" + video_path="/path/to/video.mp4", + content_dir="/path/to/content" ) assert input_data.content_type == "video" @@ -222,7 +225,8 @@ class TestOfficialSubtitle: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) result = await processor._try_official_subtitle(input_data) @@ -256,7 +260,8 @@ class TestOfficialSubtitle: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) result = await processor._try_official_subtitle(input_data) @@ -283,7 +288,8 @@ class TestOfficialSubtitle: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) await processor.process(input_data) @@ -325,7 +331,8 @@ class TestWhisperBasic: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) result = await processor._run_whisper_basic(input_data) @@ -368,7 +375,8 @@ class TestWhisperBasic: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) await processor._run_whisper_basic(input_data) @@ -392,7 +400,8 @@ class TestWhisperBasic: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) with pytest.raises(RuntimeError): @@ -435,7 +444,8 @@ class TestDiarization: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) result = await processor._run_whisper(input_data) @@ -470,7 +480,8 @@ class TestDiarization: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) result = await processor._run_whisper(input_data) @@ -550,7 +561,8 @@ class TestStateRecording: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) await processor.process(input_data) @@ -581,7 +593,8 @@ class TestStateRecording: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) with pytest.raises(Exception, match="Whisper error"): @@ -611,7 +624,8 @@ class TestStateRecording: input_data = ASRInput( content_id="BV1xx", platform="bilibili", - video_path=str(video_path) + video_path=str(video_path), + content_dir=str(tmp_path) ) # 应该不抛异常