fix(asr): use content_dir for subtitle output instead of video_path
PR Test / lint (pull_request) Successful in 2s
PR Test / lint (pull_request) Successful in 2s
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -185,11 +185,13 @@ class Pipeline:
|
|||||||
# ASR needs video path from download step
|
# ASR needs video path from download step
|
||||||
download_result = results.get("download", {})
|
download_result = results.get("download", {})
|
||||||
video_path = download_result.get("content_file", "")
|
video_path = download_result.get("content_file", "")
|
||||||
|
|
||||||
step_input = ASRInput(
|
step_input = ASRInput(
|
||||||
content_id=parsed.content_id,
|
content_id=parsed.content_id,
|
||||||
platform=parsed.platform,
|
platform=parsed.platform,
|
||||||
video_path=video_path,
|
video_path=video_path,
|
||||||
content_type=parsed.content_type,
|
content_type=parsed.content_type,
|
||||||
|
content_dir=str(content_dir),
|
||||||
)
|
)
|
||||||
elif step_name == "analyze":
|
elif step_name == "analyze":
|
||||||
# Analyze needs metadata and content files
|
# Analyze needs metadata and content files
|
||||||
|
|||||||
@@ -172,9 +172,10 @@ class ASRProcessor:
|
|||||||
Note:
|
Note:
|
||||||
无字幕返回 None(不是抛异常),让流程继续到 Whisper
|
无字幕返回 None(不是抛异常),让流程继续到 Whisper
|
||||||
"""
|
"""
|
||||||
video_path = Path(input_data.video_path)
|
# 使用 content_dir 作为输出目录(而不是从 video_path 推断)
|
||||||
output_dir = video_path.parent
|
output_dir = Path(input_data.content_dir)
|
||||||
base_name = video_path.stem
|
# 使用 content_id 作为基础文件名
|
||||||
|
base_name = f"{input_data.platform}_{input_data.content_id}_subtitle"
|
||||||
|
|
||||||
# 输出文件路径
|
# 输出文件路径
|
||||||
txt_path = output_dir / f"{base_name}.txt"
|
txt_path = output_dir / f"{base_name}.txt"
|
||||||
@@ -343,11 +344,18 @@ class ASRProcessor:
|
|||||||
ASR 处理结果
|
ASR 处理结果
|
||||||
|
|
||||||
Raises:
|
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)
|
video_path = Path(input_data.video_path)
|
||||||
output_dir = video_path.parent
|
output_dir = Path(input_data.content_dir)
|
||||||
base_name = video_path.stem
|
base_name = f"{input_data.platform}_{input_data.content_id}_whisper"
|
||||||
|
|
||||||
# 获取 Whisper.cpp 路径
|
# 获取 Whisper.cpp 路径
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -198,11 +198,13 @@ class ASRInput(StepInput):
|
|||||||
继承 StepInput 的基础字段,添加视频路径和内容类型。
|
继承 StepInput 的基础字段,添加视频路径和内容类型。
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
video_path: 视频文件路径
|
video_path: 视频文件路径(可能为空,用于 Whisper 降级)
|
||||||
content_type: 内容类型,默认为 'video'
|
content_type: 内容类型,默认为 'video'
|
||||||
|
content_dir: 内容目录路径,用于输出字幕文件
|
||||||
"""
|
"""
|
||||||
video_path: str = Field(..., description="视频文件路径")
|
video_path: str = Field(default="", description="视频文件路径(可能为空)")
|
||||||
content_type: str = Field(default="video", description="内容类型")
|
content_type: str = Field(default="video", description="内容类型")
|
||||||
|
content_dir: str = Field(..., description="内容目录路径")
|
||||||
|
|
||||||
|
|
||||||
class ASROutput(StepOutput):
|
class ASROutput(StepOutput):
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ class TestASRInputSchema:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
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(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
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")
|
assert hasattr(input_data, "video_path")
|
||||||
@@ -59,7 +61,8 @@ class TestASRInputSchema:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
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"
|
assert input_data.content_type == "video"
|
||||||
@@ -222,7 +225,8 @@ class TestOfficialSubtitle:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
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)
|
result = await processor._try_official_subtitle(input_data)
|
||||||
@@ -256,7 +260,8 @@ class TestOfficialSubtitle:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
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)
|
result = await processor._try_official_subtitle(input_data)
|
||||||
@@ -283,7 +288,8 @@ class TestOfficialSubtitle:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_path=str(video_path)
|
video_path=str(video_path),
|
||||||
|
content_dir=str(tmp_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
await processor.process(input_data)
|
await processor.process(input_data)
|
||||||
@@ -325,7 +331,8 @@ class TestWhisperBasic:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
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)
|
result = await processor._run_whisper_basic(input_data)
|
||||||
@@ -368,7 +375,8 @@ class TestWhisperBasic:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_path=str(video_path)
|
video_path=str(video_path),
|
||||||
|
content_dir=str(tmp_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
await processor._run_whisper_basic(input_data)
|
await processor._run_whisper_basic(input_data)
|
||||||
@@ -392,7 +400,8 @@ class TestWhisperBasic:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_path=str(video_path)
|
video_path=str(video_path),
|
||||||
|
content_dir=str(tmp_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(RuntimeError):
|
with pytest.raises(RuntimeError):
|
||||||
@@ -435,7 +444,8 @@ class TestDiarization:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_path=str(video_path)
|
video_path=str(video_path),
|
||||||
|
content_dir=str(tmp_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await processor._run_whisper(input_data)
|
result = await processor._run_whisper(input_data)
|
||||||
@@ -470,7 +480,8 @@ class TestDiarization:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_path=str(video_path)
|
video_path=str(video_path),
|
||||||
|
content_dir=str(tmp_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
result = await processor._run_whisper(input_data)
|
result = await processor._run_whisper(input_data)
|
||||||
@@ -550,7 +561,8 @@ class TestStateRecording:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_path=str(video_path)
|
video_path=str(video_path),
|
||||||
|
content_dir=str(tmp_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
await processor.process(input_data)
|
await processor.process(input_data)
|
||||||
@@ -581,7 +593,8 @@ class TestStateRecording:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_path=str(video_path)
|
video_path=str(video_path),
|
||||||
|
content_dir=str(tmp_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
with pytest.raises(Exception, match="Whisper error"):
|
with pytest.raises(Exception, match="Whisper error"):
|
||||||
@@ -611,7 +624,8 @@ class TestStateRecording:
|
|||||||
input_data = ASRInput(
|
input_data = ASRInput(
|
||||||
content_id="BV1xx",
|
content_id="BV1xx",
|
||||||
platform="bilibili",
|
platform="bilibili",
|
||||||
video_path=str(video_path)
|
video_path=str(video_path),
|
||||||
|
content_dir=str(tmp_path)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 应该不抛异常
|
# 应该不抛异常
|
||||||
|
|||||||
Reference in New Issue
Block a user