包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、 Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。 技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。 DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
123 lines
3.5 KiB
PowerShell
123 lines
3.5 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$VideoPath,
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$OutputDirectory,
|
|
|
|
[int]$FrameCount = 12,
|
|
|
|
[double]$StartSeconds = 0,
|
|
|
|
[double]$EndSeconds = -1
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
Add-Type -AssemblyName PresentationCore
|
|
Add-Type -AssemblyName WindowsBase
|
|
|
|
function Invoke-DispatcherWait {
|
|
param([int]$Milliseconds)
|
|
|
|
$frame = [System.Windows.Threading.DispatcherFrame]::new()
|
|
$timer = [System.Windows.Threading.DispatcherTimer]::new(
|
|
[System.Windows.Threading.DispatcherPriority]::Background
|
|
)
|
|
$timer.Interval = [TimeSpan]::FromMilliseconds($Milliseconds)
|
|
$timer.Add_Tick({
|
|
$timer.Stop()
|
|
$frame.Continue = $false
|
|
})
|
|
$timer.Start()
|
|
[System.Windows.Threading.Dispatcher]::PushFrame($frame)
|
|
}
|
|
|
|
$resolvedVideo = (Resolve-Path -LiteralPath $VideoPath).Path
|
|
$resolvedOutput = [System.IO.Path]::GetFullPath($OutputDirectory)
|
|
[System.IO.Directory]::CreateDirectory($resolvedOutput) | Out-Null
|
|
|
|
$player = [System.Windows.Media.MediaPlayer]::new()
|
|
$player.ScrubbingEnabled = $true
|
|
$player.Open([Uri]::new($resolvedVideo))
|
|
$player.Play()
|
|
|
|
for ($attempt = 0; $attempt -lt 100 -and $player.NaturalVideoWidth -eq 0; $attempt++) {
|
|
Invoke-DispatcherWait -Milliseconds 100
|
|
}
|
|
|
|
if ($player.NaturalVideoWidth -eq 0 -or -not $player.NaturalDuration.HasTimeSpan) {
|
|
$player.Close()
|
|
throw "Unable to decode video: $resolvedVideo"
|
|
}
|
|
|
|
$width = $player.NaturalVideoWidth
|
|
$height = $player.NaturalVideoHeight
|
|
$duration = $player.NaturalDuration.TimeSpan.TotalSeconds
|
|
$rangeStart = [Math]::Min([Math]::Max($StartSeconds, 0), $duration)
|
|
$rangeEnd = if ($EndSeconds -lt 0) {
|
|
$duration
|
|
} else {
|
|
[Math]::Min([Math]::Max($EndSeconds, $rangeStart), $duration)
|
|
}
|
|
$metadata = [ordered]@{
|
|
path = $resolvedVideo
|
|
width = $width
|
|
height = $height
|
|
durationSeconds = [Math]::Round($duration, 3)
|
|
frames = @()
|
|
}
|
|
|
|
$player.Stop()
|
|
$player.Position = [TimeSpan]::Zero
|
|
$player.SpeedRatio = 16
|
|
$player.Play()
|
|
|
|
for ($index = 0; $index -lt $FrameCount; $index++) {
|
|
$ratio = if ($FrameCount -eq 1) { 0.5 } else { $index / ($FrameCount - 1) }
|
|
$seconds = $rangeStart + (($rangeEnd - $rangeStart) * $ratio)
|
|
$seconds = [Math]::Min([Math]::Max($seconds, 0), [Math]::Max($duration - 0.08, 0))
|
|
$deadline = [DateTime]::UtcNow.AddSeconds(45)
|
|
while ($player.Position.TotalSeconds -lt $seconds -and [DateTime]::UtcNow -lt $deadline) {
|
|
Invoke-DispatcherWait -Milliseconds 40
|
|
}
|
|
Invoke-DispatcherWait -Milliseconds 80
|
|
|
|
$visual = [System.Windows.Media.DrawingVisual]::new()
|
|
$context = $visual.RenderOpen()
|
|
$context.DrawVideo(
|
|
$player,
|
|
[Windows.Rect]::new(0, 0, $width, $height)
|
|
)
|
|
$context.Close()
|
|
|
|
$bitmap = [System.Windows.Media.Imaging.RenderTargetBitmap]::new(
|
|
$width,
|
|
$height,
|
|
96,
|
|
96,
|
|
[System.Windows.Media.PixelFormats]::Pbgra32
|
|
)
|
|
$bitmap.Render($visual)
|
|
|
|
$milliseconds = [long][Math]::Round($seconds * 1000)
|
|
$fileName = "frame-{0:D2}-{1:D6}ms.png" -f ([int]($index + 1)), $milliseconds
|
|
$filePath = Join-Path $resolvedOutput $fileName
|
|
$encoder = [System.Windows.Media.Imaging.PngBitmapEncoder]::new()
|
|
$encoder.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($bitmap))
|
|
$stream = [System.IO.File]::Open($filePath, [System.IO.FileMode]::Create)
|
|
try {
|
|
$encoder.Save($stream)
|
|
} finally {
|
|
$stream.Dispose()
|
|
}
|
|
|
|
$metadata.frames += [ordered]@{
|
|
file = $fileName
|
|
seconds = [Math]::Round($seconds, 3)
|
|
actualSeconds = [Math]::Round($player.Position.TotalSeconds, 3)
|
|
}
|
|
}
|
|
|
|
$player.Close()
|
|
$metadata | ConvertTo-Json -Depth 4
|