fix(vision): 修正低帧率视频预览采样(by AI)
This commit is contained in:
parent
84e72eec65
commit
31c939e210
@ -1,5 +1,9 @@
|
||||
# CHANGELOG - apigo.cc/go/vision
|
||||
|
||||
## v1.5.4 (2026-07-18)
|
||||
- **视频预览修复**: 采样避开低帧率视频无帧的尾部区间,测试在 FFmpeg 未提供 `libwebp` 编码器时正确跳过相关集成项。
|
||||
- **依赖校验修复**: 重新整理模块图并补齐 checksum。
|
||||
|
||||
## v1.5.3 (2026-06-21)
|
||||
- **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`。
|
||||
|
||||
3
TEST.md
3
TEST.md
@ -1,5 +1,8 @@
|
||||
# Testing @go/vision
|
||||
|
||||
## v1.5.4 验证
|
||||
- 全量测试与图像处理基准测试通过;FFmpeg 能力相关用例按本机编码器能力执行。
|
||||
|
||||
`go/vision` 拥有完善的单元测试覆盖,确保在各种图像处理场景下的稳定性。
|
||||
|
||||
## 运行测试
|
||||
|
||||
4
go.mod
4
go.mod
@ -17,8 +17,8 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
apigo.cc/go/encoding v1.5.4
|
||||
apigo.cc/go/safe v1.5.2
|
||||
apigo.cc/go/encoding v1.5.4 // indirect
|
||||
apigo.cc/go/safe v1.5.2 // indirect
|
||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.52.0 // indirect
|
||||
|
||||
26
go.sum
26
go.sum
@ -1,15 +1,15 @@
|
||||
apigo.cc/go/cast v1.5.0 h1:UBGJtFQ8eJPMQXs37cUgqd7YQo1zI9opuSDBDmn2/pE=
|
||||
apigo.cc/go/cast v1.5.0/go.mod h1:z2GW5p5WCZGEqVVIJUdhl232vRbLf2Qu4EDlEakX/D8=
|
||||
apigo.cc/go/encoding v1.5.0 h1:EJNdRVDOMoI2DAvZwQNQTbYuqB/6zsEzvg7lS5pQI+I=
|
||||
apigo.cc/go/encoding v1.5.0/go.mod h1:8++NfZj3hWig0qh2g7GQRw/4LpSvCYMWUZ+8J+x58cA=
|
||||
apigo.cc/go/file v1.5.0 h1:Fh1NSDBqaxjuXYJ71yPHPXVJ8BFEv/AGS3l+jkLi5uw=
|
||||
apigo.cc/go/file v1.5.0/go.mod h1:4YhOGgBINTpmmmgws3H8LAyXQQBGzBp44hYUoCS+kr0=
|
||||
apigo.cc/go/jsmod v1.5.0 h1:JgQtJNiJWy1NOP9AzE8NX5VXJkpO/x3GqLsCCSny5Ec=
|
||||
apigo.cc/go/jsmod v1.5.0/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||
apigo.cc/go/rand v1.5.0 h1:1o8hh8fhdBuk1/h02IvugvamuT3dkWbVJrqEJVQKB2E=
|
||||
apigo.cc/go/rand v1.5.0/go.mod h1:Lh98S2dm9UY0X+M+kNQQEKyXHG5pcCKSFPyXN0QCGdk=
|
||||
apigo.cc/go/safe v1.5.0 h1:W1NblmcU8cex1f9Y5z8mNLUJOzZTE1s6fszb3FbhGnk=
|
||||
apigo.cc/go/safe v1.5.0/go.mod h1:OfQ5d6COePSGEuPvMeOk6KagX2sezw7nvKh7exj9SeM=
|
||||
apigo.cc/go/cast v1.5.3 h1:jk6VX0rGFhjKtfPhsaV6IKYpiGmORRk9qPTtuNS53tw=
|
||||
apigo.cc/go/cast v1.5.3/go.mod h1:GMjjrYn93tWat1U409G7h1jR3ejfLLI7r0efBo9Sbd4=
|
||||
apigo.cc/go/encoding v1.5.4 h1:Fk8TrveZATyy8SHukC4ZiqdTSp+QIfsRHtt55xmMK7w=
|
||||
apigo.cc/go/encoding v1.5.4/go.mod h1:dShEsZ3gKqBINz7TSOYf4e7/fBCqCY9VzlenoGUQUFM=
|
||||
apigo.cc/go/file v1.5.5 h1:/+HmDumLu6Qk2KuQL63M9lpgzHTDL+QJ8dStOl7e9gs=
|
||||
apigo.cc/go/file v1.5.5/go.mod h1:xRVNhctvqOKeBemmcRW/BQfgkc3B+vT/UZVdSc7duUo=
|
||||
apigo.cc/go/jsmod v1.5.3 h1:S3W317bH0QV2NMeRO1E0v6ySIBOfMWYv/NuQJbvqKWU=
|
||||
apigo.cc/go/jsmod v1.5.3/go.mod h1:bmyeZtOAP/j5am+YRnaiM89smysK24K7ebk0koFtsSw=
|
||||
apigo.cc/go/rand v1.5.3 h1:O4bPIwyaOWEBCr0nL9A4G4qG48AqiGTCzfPeckm3Ius=
|
||||
apigo.cc/go/rand v1.5.3/go.mod h1:q1BTFkY/cXE229dDD5Q22lF7T0DoKPV6xAu+6bCrDH4=
|
||||
apigo.cc/go/safe v1.5.2 h1:EnuEOW/SGwf/5A0nw9LnqfKJE071+TIc6ez8HI9R9Lg=
|
||||
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/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
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/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
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.40.0 h1:Tw4GyDXMo+daZN1znreBRC3VayR1aLFUyUEOLUdW1a8=
|
||||
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/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
|
||||
@ -83,7 +83,7 @@ func GenerateVideoPreview(videoPath, outPath string, width, height int, frameInt
|
||||
|
||||
times := make([]float64, frameCount)
|
||||
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)
|
||||
@ -152,7 +152,7 @@ func GenerateAudioPreview(mediaPath, outPath string) 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 {
|
||||
return 0, err
|
||||
}
|
||||
@ -160,4 +160,3 @@ func getVideoDuration(videoPath string) (float64, error) {
|
||||
_, err = fmt.Sscanf(strings.TrimSpace(string(out)), "%f", &duration)
|
||||
return duration, err
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package vision
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"os"
|
||||
@ -49,8 +50,9 @@ func TestPreviewer(t *testing.T) {
|
||||
err = exec.Command("ffmpeg", "-f", "lavfi", "-i", "anullsrc=r=44100:cl=mono", "-t", "5", "-c:a", "aac", audioPath).Run()
|
||||
if err == nil {
|
||||
finalVideo := filepath.Join(tmpDir, "final.mp4")
|
||||
exec.Command("ffmpeg", "-i", videoPath, "-i", audioPath, "-c", "copy", finalVideo).Run()
|
||||
videoPath = finalVideo
|
||||
if err = exec.Command("ffmpeg", "-i", videoPath, "-i", audioPath, "-c", "copy", finalVideo).Run(); err == nil {
|
||||
videoPath = finalVideo
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 测试 Preview 功能
|
||||
@ -67,7 +69,11 @@ func TestPreviewer(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 {
|
||||
t.Errorf("GenerateVideoPreview failed: %v", err)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user