Spaces:
Sleeping
Sleeping
File size: 6,445 Bytes
db4f540 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
"""章节提取器"""
import json
import os
import re
from dataclasses import dataclass
import openai
from chapterbar.logger import logger
from chapterbar.parser import SubtitleEntry
@dataclass
class Chapter:
"""章节信息"""
title: str
start_time: float # 秒
end_time: float # 秒
color: tuple[int, int, int] # RGB
# 统一灰色方案(实际颜色在渲染时根据播放状态决定)
# 未播放:浅灰色 (220, 220, 220)
# 已播放:深灰色 (140, 140, 140)
BASE_COLOR = (200, 200, 200) # 默认灰色
def extract_chapters_auto(entries: list[SubtitleEntry], interval: int, total_duration: float) -> list[Chapter]:
"""自动分段模式:按时间间隔分段
Args:
entries: 字幕条目列表
interval: 分段间隔(秒)
total_duration: 视频总时长(秒)
Returns:
List[Chapter]: 章节列表
"""
chapters = []
current_time = 0
chapter_index = 0
while current_time < total_duration:
# 计算章节结束时间
end_time = min(current_time + interval, total_duration)
# 找到这个时间段内的字幕文本作为标题
title_parts = []
if entries: # 只有当有字幕时才尝试提取
for entry in entries:
if current_time <= entry.start_time < end_time:
title_parts.append(entry.text)
if len(title_parts) >= 3: # 最多取3条字幕
break
# 生成标题
title = " ".join(title_parts)[:30] if title_parts else f"章节 {chapter_index + 1}"
# 使用统一的基础颜色
color = BASE_COLOR
chapters.append(Chapter(title=title, start_time=current_time, end_time=end_time, color=color))
current_time = end_time
chapter_index += 1
return chapters
def format_time(seconds: float) -> str:
"""将秒数转换为 HH:MM:SS 格式"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
def extract_chapters_ai(
entries: list[SubtitleEntry],
total_duration: float,
api_key: str | None = None,
model: str = "moonshot-v1-8k",
) -> list[Chapter]:
"""AI 智能分段模式:使用 Moonshot AI 分析字幕内容
Args:
entries: 字幕条目列表
total_duration: 视频总时长(秒)
api_key: Moonshot API Key(可选,默认从环境变量读取)
model: 使用的模型(默认 moonshot-v1-8k)
Returns:
List[Chapter]: 章节列表
"""
if not entries:
# 如果没有字幕,回退到自动分段
return extract_chapters_auto(entries, interval=60, total_duration=total_duration)
# 获取 API Key
if api_key is None:
api_key = os.getenv("MOONSHOT_API_KEY")
if not api_key:
raise ValueError("未提供 Moonshot API Key,请设置环境变量 MOONSHOT_API_KEY 或通过参数传入")
# 初始化客户端
client = openai.Client(base_url="https://api.moonshot.cn/v1", api_key=api_key)
# 构建字幕文本(带时间戳)
subtitle_lines = []
for entry in entries:
time_str = format_time(entry.start_time)
subtitle_lines.append(f"[{time_str}] {entry.text}")
subtitle_text = "\n".join(subtitle_lines)
# 构建 prompt
prompt = f"""请分析以下视频字幕内容,识别内容的主题转换点,并生成合理的章节划分。
要求:
1. 识别 5-10 个主要章节(根据内容长度和复杂度调整)
2. 每个章节给出开始时间(格式:HH:MM:SS)和简短标题(5-15 字)
3. 章节标题要准确概括该段内容的核心主题
4. 返回 JSON 格式数组,每个元素包含 time 和 title 字段
5. 视频总时长为 {format_time(total_duration)}
示例输出格式:
[
{{"time": "00:00:00", "title": "开场与自我介绍"}},
{{"time": "00:01:23", "title": "问题背景分析"}},
{{"time": "00:03:45", "title": "解决方案详解"}}
]
字幕内容:
{subtitle_text}
请直接返回 JSON 数组,不要包含其他说明文字。"""
# 调用 API
try:
response = client.chat.completions.create(
model=model,
messages=[
{
"role": "system",
"content": "你是一个专业的视频内容分析助手,擅长识别内容结构和主题转换点。",
},
{"role": "user", "content": prompt},
],
temperature=0.3, # 较低的温度以获得更稳定的输出
max_tokens=2048,
)
# 解析响应
content = response.choices[0].message.content.strip()
# 提取 JSON(可能被包裹在代码块中)
json_match = re.search(r"```(?:json)?\s*(\[.*?\])\s*```", content, re.DOTALL)
json_str = json_match.group(1) if json_match else content
# 解析 JSON
chapter_data = json.loads(json_str)
# 转换为 Chapter 对象
chapters = []
for i, item in enumerate(chapter_data):
# 解析时间
time_str = item["time"]
time_parts = time_str.split(":")
start_time = int(time_parts[0]) * 3600 + int(time_parts[1]) * 60 + int(time_parts[2])
# 计算结束时间
if i < len(chapter_data) - 1:
next_time_str = chapter_data[i + 1]["time"]
next_time_parts = next_time_str.split(":")
end_time = int(next_time_parts[0]) * 3600 + int(next_time_parts[1]) * 60 + int(next_time_parts[2])
else:
end_time = total_duration
# 使用统一的基础颜色
color = BASE_COLOR
chapters.append(Chapter(title=item["title"], start_time=start_time, end_time=end_time, color=color))
return chapters
except json.JSONDecodeError as e:
logger.warning(f"AI 返回的内容无法解析为 JSON: {e}")
logger.debug(f"原始内容: {content}")
# 回退到自动分段
return extract_chapters_auto(entries, interval=60, total_duration=total_duration)
except Exception as e:
logger.warning(f"AI 分段失败: {e}")
# 回退到自动分段
return extract_chapters_auto(entries, interval=60, total_duration=total_duration)
|