public/按LLC剪切.ps1

function 按LLC剪切 {
    <#
    .SYNOPSIS
        按 LosslessCut 的 LLC 项目文件,将视频剪成多个片段文件。
 
    .DESCRIPTION
        读取 version 2 的 .llc 项目,使用 mediaFileName 定位同目录的源媒体,只处理 cutSegments 中 selected 为 true 的片段。
        start 和 end 的单位为秒。两个端点分别对齐到距离最近的关键帧,而不是精确切在指定时间;对齐后若起点不早于终点,则报错并停止。
        所有轨道均不转码,只做流复制剪切。普通视频由 ffmpeg 处理,RM/RMVB 由 RealMedia Editor 原生处理,均保留源扩展名。片段默认使用源文件名、对齐后的时间戳和片段序号命名,源文件保持不变。
 
    .PARAMETER 项目文件
        要读取的 .llc 项目文件路径。支持带尾随逗号的 LLC JSONC 格式。
 
    .PARAMETER 输出目录
        存放所有片段的目录。省略时输出到源媒体所在目录;不存在时自动创建。
 
    .EXAMPLE
        按LLC剪切 -项目文件 'D:\视频-proj.llc'
 
        将项目中的所有已选片段分别保存为独立视频文件,文件名带实际使用的关键帧时间戳。
 
    .EXAMPLE
        按LLC剪切 -项目文件 'D:\视频-proj.llc' -输出目录 'D:\片段'
 
        把片段统一输出到指定目录。
 
    .NOTES
        依赖 ffmpeg 和 ffprobe;RM/RMVB 还依赖 RealProducer 自带的 rmeditor.exe,缺少时会自动安装。流复制片段以关键帧为边界,不保证恰好等于 LLC 中填写的毫秒时间。
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, HelpMessage = 'LosslessCut 的 .llc 项目文件路径')]
        [string]$项目文件,

        [Parameter(Position = 1)]
        [string]$输出目录
    )

    Set-StrictMode -Version Latest
    $ErrorActionPreference = 'Stop'
    $script:输出外部进程信息 = $false

    $项目文件 = 规范化_用户路径参数 -Path $项目文件
    if ($输出目录) { $输出目录 = 规范化_用户路径参数 -Path $输出目录 }
    if (-not (Test-Path -LiteralPath $项目文件 -PathType Leaf)) { throw "LLC 项目文件不存在:$项目文件" }
    确保_ffmpeg_ffprobe_可用

    $项目 = 读取_LLC项目 -Path $项目文件
    $源路径 = $项目.媒体路径
    $源扩展名 = [System.IO.Path]::GetExtension($源路径).ToLowerInvariant()
    $是RealMedia = $源扩展名 -in @('.rm', '.rmvb')
    $输出扩展名 = $源扩展名
    $RealMedia编辑器 = if ($是RealMedia) { 解决_RealMedia编辑器路径 } else { $null }
    if (-not $输出目录) { $输出目录 = Split-Path -Parent $源路径 }
    $输出目录 = [System.IO.Path]::GetFullPath($输出目录)
    if (-not (Test-Path -LiteralPath $输出目录 -PathType Container)) { New-Item -ItemType Directory -Path $输出目录 -Force | Out-Null }
    $基础名 = [System.IO.Path]::GetFileNameWithoutExtension($源路径)

    Write-Host "源文件:$源路径" -ForegroundColor Cyan
    Write-Host '正在读取关键帧并对齐剪切端点……' -ForegroundColor Cyan
    $关键帧列表 = @(获取_视频关键帧 -Path $源路径)
    if ($是RealMedia) { Write-Host 'RealMedia 使用 rmeditor 原生剪切:保留 RM/RMVB 容器,所有轨道均不转码。' -ForegroundColor Yellow }

    $结果列表 = New-Object 'System.Collections.Generic.List[System.IO.FileInfo]'
    foreach ($片段 in $项目.片段列表) {
        $开始关键帧 = 取_最近关键帧 -关键帧列表 $关键帧列表 -目标时间 $片段.开始
        $结束关键帧 = 取_最近关键帧 -关键帧列表 $关键帧列表 -目标时间 $片段.结束
        $开始 = $开始关键帧.时间
        $结束 = $结束关键帧.时间
        if ($结束 -le $开始) { throw "第 $($片段.序号) 个片段在关键帧对齐后区间无效:$开始 至 $结束 秒。" }
        $名称后缀 = if ([string]::IsNullOrWhiteSpace($片段.名称)) { '' } else { '-' + ($片段.名称 -replace '[<>:"/\\|?*\x00-\x1f]', '_') }
        $文件名 = '{0}-{1}-{2}-seg{3}{4}{5}' -f $基础名, (格式化_LLC时间戳 -秒 $开始), (格式化_LLC时间戳 -秒 $结束), $片段.序号, $名称后缀, $输出扩展名
        $片段路径 = Join-Path $输出目录 $文件名
        if ([System.IO.Path]::GetFullPath($片段路径) -ieq $源路径) { throw "输出文件不能覆盖源文件:$片段路径" }

        Write-Host ("片段 {0}:{1:F3} → {2:F3} 秒(原请求 {3:F3} → {4:F3} 秒)" -f $片段.序号, $开始, $结束, $片段.开始, $片段.结束) -ForegroundColor Cyan
        if ($是RealMedia) {
            剪切_RealMedia片段 -编辑器路径 $RealMedia编辑器 -源路径 $源路径 -输出路径 $片段路径 -开始 $开始关键帧.编辑器起点 -结束 $结束
        } else {
            $参数 = @('-hide_banner', '-nostdin', '-y', '-i', $源路径,
                '-ss', $开始.ToString('0.000000', [Globalization.CultureInfo]::InvariantCulture),
                '-to', $结束.ToString('0.000000', [Globalization.CultureInfo]::InvariantCulture),
                '-map', '0', '-c', 'copy', '-avoid_negative_ts', 'make_zero', $片段路径)
            $ffmpeg路径 = 解决_工具执行路径 -工具名 'ffmpeg'
            Write-Host "> $ffmpeg路径 $($参数 -join ' ')" -ForegroundColor DarkGray
            [void](调用_外部命令 -Exe $ffmpeg路径 -ArgumentList $参数 -CaptureOutput)
        }
        $输出文件 = Get-Item -LiteralPath $片段路径
        if ($输出文件.Length -le 0) { throw "未生成有效片段:$片段路径" }
        $结果列表.Add($输出文件)
        Write-Host "完成:$片段路径" -ForegroundColor Green
    }
    return $结果列表.ToArray()
}