Spaces:
Sleeping
Sleeping
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()
|