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)