fix(vision): 修正低帧率视频预览采样(by AI)

This commit is contained in:
Star 2026-07-18 19:00:27 +08:00
parent 84e72eec65
commit 31c939e210
6 changed files with 36 additions and 22 deletions

View File

@ -1,5 +1,9 @@
# CHANGELOG - apigo.cc/go/vision # CHANGELOG - apigo.cc/go/vision
## v1.5.4 (2026-07-18)
- **视频预览修复**: 采样避开低帧率视频无帧的尾部区间,测试在 FFmpeg 未提供 `libwebp` 编码器时正确跳过相关集成项。
- **依赖校验修复**: 重新整理模块图并补齐 checksum。
## v1.5.3 (2026-06-21) ## v1.5.3 (2026-06-21)
- **JS 对齐**: 重构 JS 导出为具名函数,并引入 `jsmod.MakeError` 动态包装错误以获取调用栈。 - **JS 对齐**: 重构 JS 导出为具名函数,并引入 `jsmod.MakeError` 动态包装错误以获取调用栈。
- **依赖更新**: 升级依赖 `jsmod``v1.5.3``cast``v1.5.3``rand``v1.5.3``encoding``v1.5.4``safe``v1.5.2``file``v1.5.5` - **依赖更新**: 升级依赖 `jsmod``v1.5.3``cast``v1.5.3``rand``v1.5.3``encoding``v1.5.4``safe``v1.5.2``file``v1.5.5`

View File

@ -1,5 +1,8 @@
# Testing @go/vision # Testing @go/vision
## v1.5.4 验证
- 全量测试与图像处理基准测试通过FFmpeg 能力相关用例按本机编码器能力执行。
`go/vision` 拥有完善的单元测试覆盖,确保在各种图像处理场景下的稳定性。 `go/vision` 拥有完善的单元测试覆盖,确保在各种图像处理场景下的稳定性。
## 运行测试 ## 运行测试

4
go.mod
View File

@ -17,8 +17,8 @@ require (
) )
require ( require (
apigo.cc/go/encoding v1.5.4 apigo.cc/go/encoding v1.5.4 // indirect
apigo.cc/go/safe v1.5.2 apigo.cc/go/safe v1.5.2 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
golang.org/x/crypto v0.52.0 // indirect golang.org/x/crypto v0.52.0 // indirect

26
go.sum
View File

@ -1,15 +1,15 @@
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE= apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw=
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8= apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I= apigo.cc/go/encoding v1.5.4 h1:Fk8TrveZATyy8SHukC4ZiqdTSp+QIfsRHtt55xmMK7w=
apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA= apigo.cc/go/encoding v1.5.4/go.mod h1:dShEsZ3gKqBINz7TSOYf4e7/fBCqCY9VzlenoGUQUFM=
apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw= apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs=
apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0= apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec= apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw= apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E= apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius=
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk= apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk= apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=
apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM= apigo.cc/go/safe v1.5.2/go.mod h1:2GqCCLLGex4OAhdET3iBWm1R+LIYtmTrvHP8W0iESSw=
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo= github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@ -32,10 +32,12 @@ github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8= golang.org/x/image v0.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA= golang.org/x/image v0.40.0/go.mod h1:uIc348UZMSvS5Z65CVZ7iDPaNobNFEPeJ4kbqTOszmA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=

View File

@ -83,7 +83,7 @@ func GenerateVideoPreview(videoPath, outPath string, width, height int, frameInt
times := make([]float64, frameCount) times := make([]float64, frameCount)
for i := 0; i < frameCount; i++ { for i := 0; i < frameCount; i++ {
times[i] = duration * (0.10 + 0.80*(float64(i)/float64(frameCount-1))) times[i] = duration * (0.10 + 0.65*(float64(i)/float64(frameCount-1)))
} }
// 2a. 动画模式 (.webp, .gif) // 2a. 动画模式 (.webp, .gif)
@ -152,7 +152,7 @@ func GenerateAudioPreview(mediaPath, outPath string) error {
} }
func getVideoDuration(videoPath string) (float64, error) { func getVideoDuration(videoPath string) (float64, error) {
out, err := exec.Command("ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", videoPath).Output() out, err := exec.Command("ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=duration", "-of", "default=noprint_wrappers=1:nokey=1", videoPath).Output()
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -160,4 +160,3 @@ func getVideoDuration(videoPath string) (float64, error) {
_, err = fmt.Sscanf(strings.TrimSpace(string(out)), "%f", &duration) _, err = fmt.Sscanf(strings.TrimSpace(string(out)), "%f", &duration)
return duration, err return duration, err
} }

View File

@ -1,6 +1,7 @@
package vision package vision
import ( import (
"bytes"
"fmt" "fmt"
"image/color" "image/color"
"os" "os"
@ -49,9 +50,10 @@ func TestPreviewer(t *testing.T) {
err = exec.Command("ffmpeg", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=mono", "-t", "5", "-c:a", "aac", audioPath).Run() err = exec.Command("ffmpeg", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=mono", "-t", "5", "-c:a", "aac", audioPath).Run()
if err == nil { if err == nil {
finalVideo := filepath.Join(tmpDir, "final.mp4") finalVideo := filepath.Join(tmpDir, "final.mp4")
exec.Command("ffmpeg", "-i", videoPath, "-i", audioPath, "-c", "copy", finalVideo).Run() if err = exec.Command("ffmpeg", "-i", videoPath, "-i", audioPath, "-c", "copy", finalVideo).Run(); err == nil {
videoPath = finalVideo videoPath = finalVideo
} }
}
// 3. 测试 Preview 功能 // 3. 测试 Preview 功能
t.Run("GenerateImagePreview", func(t *testing.T) { t.Run("GenerateImagePreview", func(t *testing.T) {
@ -67,7 +69,11 @@ func TestPreviewer(t *testing.T) {
}) })
t.Run("GenerateVideoPreview", func(t *testing.T) { t.Run("GenerateVideoPreview", func(t *testing.T) {
err := GenerateVideoPreview(videoPath, webPPath, 160, 120) encoders, err := exec.Command("ffmpeg", "-hide_banner", "-encoders").Output()
if err != nil || !bytes.Contains(encoders, []byte("libwebp")) {
t.Skip("FFmpeg does not provide the libwebp encoder")
}
err = GenerateVideoPreview(videoPath, webPPath, 160, 120)
if err != nil { if err != nil {
t.Errorf("GenerateVideoPreview failed: %v", err) t.Errorf("GenerateVideoPreview failed: %v", err)
} }