mirror of
https://github.com/JefferyHcool/BiliNote.git
synced 2026-06-07 08:41:02 +08:00
feat(youtube): 使用 youtube-transcript-api 优先获取字幕,有字幕时跳过音频下载
- 新增 YouTubeSubtitleFetcher 模块,通过 youtube-transcript-api 获取字幕 - 重构笔记生成流程:缓存 → 平台字幕 → 按需下载 → 转写 fallback - 有字幕时仅提取视频元信息,不下载音视频文件 - 添加 youtube-transcript-api 依赖 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -133,8 +133,46 @@ class NoteGenerator:
|
||||
audio_cache_file = NOTE_OUTPUT_DIR / f"{task_id}_audio.json"
|
||||
transcript_cache_file = NOTE_OUTPUT_DIR / f"{task_id}_transcript.json"
|
||||
markdown_cache_file = NOTE_OUTPUT_DIR / f"{task_id}_markdown.md"
|
||||
print(audio_cache_file)
|
||||
# 1. 下载音频/视频
|
||||
# 1. 获取字幕/转写:优先缓存 → 平台字幕 → 音频转写
|
||||
transcript = None
|
||||
|
||||
# 尝试读取缓存
|
||||
if transcript_cache_file.exists():
|
||||
logger.info(f"检测到转写缓存 ({transcript_cache_file}),尝试读取")
|
||||
try:
|
||||
data = json.loads(transcript_cache_file.read_text(encoding="utf-8"))
|
||||
segments = [TranscriptSegment(**seg) for seg in data.get("segments", [])]
|
||||
transcript = TranscriptResult(
|
||||
language=data.get("language"),
|
||||
full_text=data["full_text"],
|
||||
segments=segments,
|
||||
)
|
||||
logger.info(f"已从缓存加载转写结果,共 {len(segments)} 段")
|
||||
except Exception as e:
|
||||
logger.warning(f"加载转写缓存失败: {e}")
|
||||
|
||||
# 缓存没有,尝试获取平台字幕
|
||||
if transcript is None:
|
||||
logger.info("尝试获取平台字幕(优先于音频下载)...")
|
||||
try:
|
||||
transcript = downloader.download_subtitles(video_url)
|
||||
if transcript and transcript.segments:
|
||||
logger.info(f"成功获取平台字幕,共 {len(transcript.segments)} 段")
|
||||
transcript_cache_file.write_text(
|
||||
json.dumps(asdict(transcript), ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
else:
|
||||
transcript = None
|
||||
logger.info("平台无可用字幕,将下载音频后转写")
|
||||
except Exception as e:
|
||||
logger.warning(f"获取平台字幕失败: {e},将下载音频后转写")
|
||||
transcript = None
|
||||
|
||||
# 2. 下载音频/视频
|
||||
# 有字幕时只提取元信息,不下载音视频文件(除非需要截图/视频理解)
|
||||
has_transcript = transcript is not None
|
||||
need_full_download = not has_transcript or screenshot or video_understanding
|
||||
audio_meta = self._download_media(
|
||||
downloader=downloader,
|
||||
video_url=video_url,
|
||||
@@ -147,18 +185,19 @@ class NoteGenerator:
|
||||
video_understanding=video_understanding,
|
||||
video_interval=video_interval,
|
||||
grid_size=grid_size,
|
||||
skip_download=not need_full_download,
|
||||
)
|
||||
|
||||
# 2. 获取字幕/转写文字
|
||||
# 优先尝试获取平台字幕,没有再 fallback 到音频转写
|
||||
transcript = self._get_transcript(
|
||||
downloader=downloader,
|
||||
video_url=video_url,
|
||||
audio_file=audio_meta.file_path,
|
||||
transcript_cache_file=transcript_cache_file,
|
||||
status_phase=TaskStatus.TRANSCRIBING,
|
||||
task_id=task_id,
|
||||
)
|
||||
# 3. 如果前面没拿到字幕,走转写流程
|
||||
if transcript is None:
|
||||
transcript = self._get_transcript(
|
||||
downloader=downloader,
|
||||
video_url=video_url,
|
||||
audio_file=audio_meta.file_path,
|
||||
transcript_cache_file=transcript_cache_file,
|
||||
status_phase=TaskStatus.TRANSCRIBING,
|
||||
task_id=task_id,
|
||||
)
|
||||
|
||||
# 3. GPT 总结
|
||||
markdown = self._summarize_text(
|
||||
@@ -331,6 +370,7 @@ class NoteGenerator:
|
||||
video_understanding: bool,
|
||||
video_interval: int,
|
||||
grid_size: List[int],
|
||||
skip_download: bool = False,
|
||||
) -> AudioDownloadResult | None:
|
||||
"""
|
||||
1. 检查音频缓存;若不存在,则根据需要下载音频或视频(若需截图/可视化)。
|
||||
@@ -353,7 +393,34 @@ class NoteGenerator:
|
||||
task_id = audio_cache_file.stem.split("_")[0]
|
||||
self._update_status(task_id, status_phase)
|
||||
|
||||
# 已有缓存,尝试加载
|
||||
if audio_cache_file.exists():
|
||||
logger.info(f"检测到音频缓存 ({audio_cache_file}),直接读取")
|
||||
try:
|
||||
data = json.loads(audio_cache_file.read_text(encoding="utf-8"))
|
||||
return AudioDownloadResult(**data)
|
||||
except Exception as e:
|
||||
logger.warning(f"读取音频缓存失败,将重新下载:{e}")
|
||||
|
||||
# 有字幕且不需要截图/视频理解时,只提取元信息不下载文件
|
||||
if skip_download:
|
||||
logger.info("已有字幕,仅提取视频元信息(不下载音视频)")
|
||||
try:
|
||||
audio = downloader.download(
|
||||
video_url=video_url,
|
||||
quality=quality,
|
||||
output_dir=output_path,
|
||||
need_video=False,
|
||||
skip_download=True,
|
||||
)
|
||||
audio_cache_file.write_text(
|
||||
json.dumps(asdict(audio), ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
logger.info(f"元信息提取完成 ({audio_cache_file})")
|
||||
return audio
|
||||
except Exception as exc:
|
||||
logger.warning(f"元信息提取失败,将尝试完整下载: {exc}")
|
||||
|
||||
# 判断是否需要下载视频
|
||||
need_video = screenshot or video_understanding
|
||||
@@ -368,9 +435,8 @@ class NoteGenerator:
|
||||
self.video_path = Path(video_path_str)
|
||||
logger.info(f"视频下载完成:{self.video_path}")
|
||||
|
||||
# 若指定了 grid_size,则生成缩略图
|
||||
if grid_size:
|
||||
self.video_img_urls=VideoReader(
|
||||
self.video_img_urls = VideoReader(
|
||||
video_path=str(self.video_path),
|
||||
grid_size=tuple(grid_size),
|
||||
frame_interval=frame_interval,
|
||||
@@ -382,17 +448,9 @@ class NoteGenerator:
|
||||
logger.info("未指定 grid_size,跳过缩略图生成")
|
||||
except Exception as exc:
|
||||
logger.error(f"视频下载失败:{exc}")
|
||||
|
||||
self._handle_exception(task_id, exc)
|
||||
raise
|
||||
# 已有缓存,尝试加载
|
||||
if audio_cache_file.exists():
|
||||
logger.info(f"检测到音频缓存 ({audio_cache_file}),直接读取")
|
||||
try:
|
||||
data = json.loads(audio_cache_file.read_text(encoding="utf-8"))
|
||||
return AudioDownloadResult(**data)
|
||||
except Exception as e:
|
||||
logger.warning(f"读取音频缓存失败,将重新下载:{e}")
|
||||
|
||||
# 下载音频
|
||||
try:
|
||||
logger.info("开始下载音频")
|
||||
@@ -402,7 +460,6 @@ class NoteGenerator:
|
||||
output_dir=output_path,
|
||||
need_video=need_video,
|
||||
)
|
||||
# 缓存 audio 元信息到本地 JSON
|
||||
audio_cache_file.write_text(json.dumps(asdict(audio), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
logger.info(f"音频下载并缓存成功 ({audio_cache_file})")
|
||||
return audio
|
||||
|
||||
Reference in New Issue
Block a user