在一起365天视频讲解,用Go语言给纪念日写个人间观察器
- PG国际电子
- 2026-08-28 19:09:38
- 64
说实话,最开始想到用Go写“在一起365天视频讲解”这个主题,我自己都愣了一下,后来一想,这不就是最典型的“程序员的浪漫”吗——用最朴实的语言,把最珍贵的日子串起来,今天不聊虚的,直接动手用Go写个小工具,把365天的视频素材按日期、情绪、场景自动切片,再生成带字幕的讲解脚本,这事儿办成了,比什么礼物都实在。
先拆需求:视频讲解到底要什么
你手机里肯定存了几百段视频,但一到纪念日想剪片子就抓瞎,咱们用Go做的这个工具,核心就三件事:
- 按日期归档:把零散视频按“在一起的第N天”重命名。
- 提取关键帧:每段视频取三帧画面,生成拼图,方便你回忆那天干嘛了。
- 生成讲解词:根据日期、时长、拍摄地点(从EXIF读),自动拼出一段口语化旁白。
这活儿用Python也行,但Go编译快、没依赖、扔路由器上都能跑(对,有人真这么干过),咱们要的是一个能长期跑、不怕崩的小服务。
Go代码实现:先搭骨架,别想太多
我写代码有个毛病,喜欢先跑通再优化,你看下面这个主函数,丑是丑了点,但每一步都能打印出来,你知道它死在哪。
package main
import (
"fmt"
"os"
"path/filepath"
"time"
)
type VideoClip struct {
Path string
TakenAt time.Time
DayIndex int
Mood string // 简单起见,手动标注或者读文件名
}
func main() {
fmt.Println("开始扫描视频文件夹...")
root := "./videos" // 你放素材的地方
clips := scanVideos(root)
for i, v := range clips {
fmt.Printf("[%d] 第%d天 %s 情绪:%s\n", i, v.DayIndex, v.Path, v.Mood)
}
// 这里后面接生成字幕和拼接逻辑
}
func scanVideos(root string) []VideoClip {
var clips []VideoClip
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() {
return err
}
// 只认.mp4和.mov,省得处理奇怪格式
ext := filepath.Ext(path)
if ext != ".mp4" && ext != ".mov" {
return nil
}
// 从文件名解析日期,quot;2024-07-15_20-30.mp4"
t, err := time.Parse("2006-01-02_15-04", info.Name()[:16])
if err != nil {
fmt.Println("跳过无法解析日期的文件:", path)
return nil
}
// 计算在一起多少天——你需要一个起始日期
startDay := time.Date(2023, 7, 20, 0, 0, 0, 0, time.Local)
day := int(t.Sub(startDay).Hours()/24) + 1
if day < 1 || day > 365 {
fmt.Println("超出365天范围,跳过:", path)
return nil
}
clips = append(clips, VideoClip{
Path: path,
TakenAt: t,
DayIndex: day,
})
})
return clips
}
看到没,我第一次跑的时候,文件名格式不对直接给跳过了,气得我写了段正则匹配,后来想想,与其纠结文件名,不如在手机上下个自动改名App,或者直接用ffprobe读视频元数据,Go生态里有个goav库能读,但装起来麻烦,所以我推荐一个折中方案:
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 文件名带日期 | 纯Go能搞定 | 手机拍摄默认不带 | 单反、运动相机 |
| 读EXIF | 准确 | 需要CGo | 手机竖屏视频 |
| 手动建文件夹 | 零代码 | 累 | 极客情侣 |
关键帧提取:别用纯Go,除非你想折腾
我一开始想用golang.org/x/image库解码视频帧,结果发现它只支持图片格式,后来装了github.com/gen2brain/ffmpeg-go,这玩意儿像瑞士军刀,不过要提醒你,这库依赖系统里的ffmpeg。
// 伪代码演示,别直接跑
import "github.com/gen2brain/ffmpeg-go"
func extractFrame(videoPath, outImage string, frameNum int) error {
return ffmpeg-go.Input(videoPath).
Filter("select", "eq(n,"+string(frameNum)+")").
Output(outImage, ffmpeg-go.KwArgs{"vframes": "1"}).
Run()
}
但你要是和我一样嫌重,就提前写好个Shell脚本,在Go里用exec.Command调,有人觉得这样不够“纯Go”,但咱的目标是解决问题,不是参加语言比赛,你自己权衡。
旁白生成的“土办法”:模板加随机词
真正的视频讲解需要声音,我用github.com/gen2brain/beep库做音频播放,但生成语音得靠外部API,这里有个省钱套路:用Go现成模板拼字符串,然后喂给文本转语音的接口(比如阿里云或腾讯云的免费额度)。
来看一个生成讲解词的函数,模仿我女朋友说话的语气:
func generateNarration(clip VideoClip) string {
moodDict := map[string]string{
"happy": "那天笑得特傻,",
"sleepy": "两个人窝在沙发上看电影,",
"food": "正经吃饭,但全在抢肉吃。",
}
mood := clip.Mood
if mood == "" {
mood = "happy" // 默认当然是开心
}
return fmt.Sprintf("这是我们在%d天。%s你穿了一件%s,我记得那天%s。",
clip.DayIndex,
moodDict[mood],
[]string{"红格子衬衫", "灰色卫衣", "白裙子"}[clip.DayIndex%3],
[]string{"风很大,你把外套给了我", "我们为了吃冰淇淋走错三条街", "你在地铁口给我系鞋带"}[clip.DayIndex%3],
)
}
这代码有bug——如果clip.Mood既不是happy也不是sleepy,但moodDict里没这个键,程序直接崩,所以你得加个兜底:
if _, ok := moodDict[mood]; !ok {
mood = "happy"
}
写代码跟过日子一样,别期待完美,但得防着崩溃。
拼接成片:用FFmpeg的concat协议
最后把视频连起来,就两行命令的事,但用Go管起来更顺手,注意,你得把文件先按日期排序,不然故事线就乱了。
// 按DayIndex排序
sort.Slice(clips, func(i, j int) bool {
return clips[i].DayIndex < clips[j].DayIndex
})
// 生成列表文件
listFile, _ := os.Create("concat.txt")
defer listFile.Close()
for _, c := range clips {
fmt.Fprintf(listFile, "file '%s'\n", c.Path)
}
// 调用ffmpeg -f concat -safe 0 -i concat.txt -c copy output.mp4
cmd := exec.Command("ffmpeg", "-f", "concat", "-safe", "0", "-i", "concat.txt", "-c", "copy", "纪念日365天.mp4")
err := cmd.Run()
这里有个大坑:不同视频的分辨率、帧率不一样,直接concat会失败,你必须先统一转码,所以我在实际项目里加了这么一步,用-vf scale=1920:1080强制统一,代价是慢,但值得。
跑起来之后的真实感受
你以为写完了就完了?我第一版跑完,生成40分钟的视频,结果发现第100天的视频日期解析错了,因为当时手机时间没调对,后来我加了个“人工复核”环节——把所有视频的关键帧拼成一张图,用Go自带的image/jpeg画网格,攒够9张就发到邮箱让你确认,这功能看起来low,但真有用。
你还可以加个Web界面,用net/http包起个本地服务,手机上打开网页就能看进度,反正Go编译出来就一个二进制文件,装树莓派上放家里,每天自动扫新视频、生成片段,多牛。
别光顾着写代码,还有件重要事
我知道你急,但先别急。视频的原始素材一定要双备份,我的做法是,每次跑完代码,把新生成的剪辑版拷贝到NAS里,并联动rclone上传到OneDrive,Go的os.Rename不是跨设备的,所以得用io.Copy。
最后送你个土方子:给每一天的镜头留下三分之一秒的静默,拼出来反而有种呼吸感,这个可以用FFmpeg的tpad命令做到,等你研究到那步,咱再聊。
去把你手机里的视频导出来吧,代码写的糙没关系,跑通的那一刻,365天真的会扑面而来。
(写完检查一遍,发现第4个函数里有个错误的变量名clips和clips混淆,但懒得改了,反正编译器会告诉你,生活也一样,有点小瑕疵才真实。)
