File size: 8,923 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
"""命令行界面"""

from pathlib import Path

import typer
from rich.console import Console

from chapterbar.chapter_extractor import extract_chapters_ai, extract_chapters_auto
from chapterbar.chapter_loader import ChapterLoader
from chapterbar.generator import generate_video
from chapterbar.interactive_editor import display_chapters_table
from chapterbar.parser import parse_srt

app = typer.Typer(help="Auto-Chapter-Bar - 视频章节进度条生成器")
console = Console()


@app.command()
def main(
    srt_file: Path | None = typer.Argument(None, help="SRT 字幕文件路径(可选,使用 --chapters 时不需要)"),
    duration: float | None = typer.Argument(None, help="视频总时长(秒),不提供则从 SRT 文件自动获取"),
    output: Path = typer.Option("chapter_bar.mov", "--output", "-o", help="输出文件路径"),
    width: int = typer.Option(1920, "--width", "-w", help="视频宽度"),
    height: int = typer.Option(60, "--height", "-h", help="进度条高度"),
    mode: str = typer.Option(
        "ai",
        "--mode",
        "-m",
        help="章节提取模式:auto(固定间隔)、ai(智能分段,默认)或 manual(手动配置)",
    ),
    interval: int = typer.Option(60, "--interval", "-i", help="自动分段间隔(秒),仅在 auto 模式下使用"),
    api_key: str | None = typer.Option(
        None,
        "--api-key",
        help="Moonshot API Key(AI 模式需要,也可通过环境变量 MOONSHOT_API_KEY 设置)",
    ),
    model: str = typer.Option("moonshot-v1-8k", "--model", help="AI 模型名称(默认 moonshot-v1-8k)"),
    auto_confirm: bool = typer.Option(False, "--yes", "-y", help="自动确认所有提示(跳过时长确认和章节编辑)"),
    chapters_file: Path | None = typer.Option(None, "--chapters", help="手动章节配置文件(YAML 格式)"),
    save_chapters: Path | None = typer.Option(None, "--save-chapters", help="保存生成的章节配置到 YAML 文件"),
):
    """生成视频章节进度条"""

    try:
        # 1. 检查是否使用手动配置文件
        if chapters_file:
            console.print(f"[cyan]📄 正在加载章节配置文件: {chapters_file}[/cyan]")
            try:
                chapters, duration, warnings = ChapterLoader.load_from_yaml(str(chapters_file))
                console.print(f"[green]✓ 配置加载成功,共 {len(chapters)} 个章节[/green]")
                console.print(f"[cyan]📏 视频时长: {duration:.2f} 秒 ({duration / 60:.2f} 分钟)[/cyan]")

                # 显示警告
                if warnings:
                    console.print(f"\n[yellow]⚠️  发现 {len(warnings)} 个警告:[/yellow]")
                    for warning in warnings:
                        console.print(f"[yellow]  - {warning.message}[/yellow]")
                    console.print()

                # 跳过 SRT 解析,直接到章节显示
                entries = None

            except (FileNotFoundError, ValueError) as e:
                console.print(f"[red]✗ 错误: {e}[/red]")
                raise typer.Exit(1) from e

        # 2. 解析 SRT 文件(如果没有使用配置文件)
        elif srt_file:
            console.print(f"[cyan]正在解析 SRT 文件: {srt_file}[/cyan]")
            entries = parse_srt(str(srt_file))
            console.print(f"[green]✓ 解析完成,共 {len(entries)} 条字幕[/green]")

            if not entries:
                console.print("[red]✗ 错误: SRT 文件为空[/red]")
                raise typer.Exit(1)
        else:
            console.print("[red]✗ 错误: 必须提供 SRT 文件或章节配置文件(--chapters)[/red]")
            raise typer.Exit(1)

        # 3. 处理视频时长(如果没有使用配置文件)
        if not chapters_file:
            srt_duration = entries[-1].end_time

            if duration is None:
                # 用户未提供时长,自动从 SRT 获取
                duration = srt_duration
                console.print(
                    f"[cyan]📏 从 SRT 文件自动获取视频时长: {duration:.2f} 秒 ({duration / 60:.2f} 分钟)[/cyan]"
                )
            else:
                # 用户提供了时长,检查是否与 SRT 一致
                console.print(f"[cyan]📏 用户指定视频时长: {duration:.2f} 秒 ({duration / 60:.2f} 分钟)[/cyan]")
                console.print(f"[cyan]📏 SRT 文件实际时长: {srt_duration:.2f} 秒 ({srt_duration / 60:.2f} 分钟)[/cyan]")

                # 如果差异超过 5 秒,显示警告
                if abs(duration - srt_duration) > 5:
                    console.print("\n[yellow]⚠️  警告: 指定时长与 SRT 实际时长不一致![/yellow]")
                    console.print(f"[yellow]   差异: {abs(duration - srt_duration):.2f} 秒[/yellow]\n")

                    if not auto_confirm:
                        # 询问用户选择
                        console.print("请选择使用哪个时长:")
                        console.print(f"  [1] 使用 SRT 时长: {srt_duration:.2f} 秒 (推荐)")
                        console.print(f"  [2] 使用指定时长: {duration:.2f} 秒")
                        console.print("  [3] 取消操作")

                        choice = typer.prompt("\n请输入选择 (1/2/3)", default="1")

                        if choice == "1":
                            duration = srt_duration
                            console.print(f"[green]✓ 使用 SRT 时长: {duration:.2f} 秒[/green]\n")
                        elif choice == "2":
                            console.print(f"[green]✓ 使用指定时长: {duration:.2f} 秒[/green]\n")
                        else:
                            console.print("[yellow]已取消操作[/yellow]")
                            raise typer.Exit(0)
                    else:
                        # 自动确认模式,使用 SRT 时长
                        duration = srt_duration
                        console.print(f"[green]✓ 自动使用 SRT 时长: {duration:.2f} 秒[/green]\n")

        # 4. 提取章节(如果没有使用配置文件)
        if chapters_file:
            # 已经从配置文件加载了章节,跳过
            pass
        elif mode == "ai":
            console.print(f"[cyan]🤖 正在使用 AI 智能分段(模型: {model})...[/cyan]")
            console.print("[yellow]这可能需要几秒钟,请稍候...[/yellow]")
            chapters = extract_chapters_ai(entries, duration, api_key, model)
            console.print(f"[green]✓ AI 分段完成,共 {len(chapters)} 个章节[/green]\n")
        else:
            console.print(f"[cyan]正在提取章节(间隔: {interval}秒)...[/cyan]")
            chapters = extract_chapters_auto(entries, interval, duration)
            console.print(f"[green]✓ 提取完成,共 {len(chapters)} 个章节[/green]\n")

        # 5. 保存章节配置(如果指定了 --save-chapters)
        if save_chapters:
            console.print(f"\n[cyan]💾 正在保存章节配置到: {save_chapters}[/cyan]")
            try:
                ChapterLoader.save_to_yaml(chapters, duration, str(save_chapters))
                console.print("[green]✓ 章节配置已保存[/green]")
                console.print(f"[cyan]💡 提示: 可以编辑 {save_chapters} 后使用 --chapters 参数重新生成[/cyan]\n")
            except Exception as e:
                console.print(f"[yellow]⚠️  保存配置失败: {e}[/yellow]\n")

        # 6. 显示章节列表
        display_chapters_table(chapters)
        console.print()

        # 7. 交互式确认(仅在 AI 或 Auto 模式下,且未使用配置文件时)
        # if not chapters_file and mode in ["ai", "auto"]:
        #     chapters = confirm_chapters(chapters, skip_confirm=auto_confirm)
        #     if chapters is None:
        #         # 用户选择退出
        #         raise typer.Exit(0)

        # 8. 生成视频
        console.print(f"[cyan]正在生成视频: {output}[/cyan]")
        console.print("[yellow]这可能需要几分钟时间,请耐心等待...[/yellow]")

        generate_video(
            chapters=chapters,
            output_path=str(output),
            width=width,
            height=height,
            duration=duration,
        )

        console.print(f"[green]✓ 视频生成完成: {output}[/green]")
        console.print("\n[bold]使用说明:[/bold]")
        console.print("1. 在剪辑软件(PR/剪映/达芬奇)中打开原视频")
        console.print("2. 将生成的章节条视频拖入最上层轨道")
        console.print("3. 调整位置和大小,导出最终视频")

    except Exception as e:
        console.print(f"[red]✗ 错误: {e}[/red]")
        raise typer.Exit(1) from e


if __name__ == "__main__":
    app()