首次提交:脑晴测 H5 项目
包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、 Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。 技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。 DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
This commit is contained in:
122
brain-h5/scripts/extract-video-frames.ps1
Normal file
122
brain-h5/scripts/extract-video-frames.ps1
Normal file
@@ -0,0 +1,122 @@
|
||||
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
|
||||
Reference in New Issue
Block a user