"""命令行界面""" 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()