为什么我要用Golang写一个“一周岁视频”生成工具?
说实话,当爸之前我完全没想到,一周岁365天视频这事儿能把我折腾成这样。
我家小闺女满一周岁的时候,我和媳妇翻手机相册,发现拍了三千多张照片、两百多个小视频,媳妇说:“咱们做个一周岁视频吧,把这一年365天串起来。”我一听,心想这还不简单?淘宝找个模板,拖进去就完事了。
结果——模板限制了每天只能选一张图,配乐就那么几首,字幕还不能自由排版,最要命的是,视频时长必须控制在3分钟以内,365天,平均一天不到0.5秒。那还看个啥?一闪而过连娃的脸都看不清。
我是程序员,动手能力还行,Golang这门语言我用了五年了,处理文件、并发、跨平台都很顺手,于是我说干就干,写了一个小工具,专门用来生成一周岁365天视频,今天就把整个过程,包括我踩的坑、优化的思路、最终的效果,全部分享出来。
第一步:先理清楚,一个“一周岁365天视频”到底包含什么
核心数据清单
| 维度 | 备注 | |
|---|---|---|
| 时间跨度 | 第1天到第365天 | 精确到天 |
| 素材类型 | 照片+短视频 | 照片建议横版16:9 |
| 每日呈现 | 1-3个素材+日期标签 | 日期格式:第XX天 |
| 背景音乐 | 一首完整的曲子 | 建议选节奏轻快的 |
| 片头片尾 | 宝宝姓名+寄语 | 可加父母的话 |
| 输出格式 | MP4, 1080P, 30fps | 兼容主流播放器 |
我遇到的第一个坑:素材杂乱无章
媳妇的手机里,照片命名是IMG_20240301.jpg,我的手机里是20240301_083215.jpg,还有奶奶用旧手机拍的,时间戳都不准。素材不整理好,一切免谈。
所以我第一步用Golang写了一个素材归类脚本:
// 伪代码示意,实际代码更长
func classifyPhotos(dir string) map[string][]string {
files := readDir(dir)
result := make(map[string][]string)
for _, f := range files {
date := extractDateFromExif(f) // 从EXIF读取拍摄时间
if date == "" {
date = extractDateFromFilename(f) // 从文件名猜
}
dayIndex := calcDayIndex(date, birthday) // 计算是第几天
result[dayIndex] = append(result[dayIndex], f)
}
return result
}
这个函数让我知道了:EXIF信息不是万能的。 很多旧手机拍的照片,EXIF里的时间字段是空的,这时候就得靠文件名正则匹配,实在匹配不了的,我写了个交互式命令,手动输入日期。
给你们的建议:从娃出生第一天开始,就在手机里建一个专门的相册,叫“宝贝365”,每天拍完就扔进去,别学我,事后整理真是累掉半条命。
第二步:Golang怎么处理视频?我用的是FFmpeg
操作系统级别的视频处理,Golang原生做不到,但是可以通过os/exec调用FFmpeg,FFmpeg是视频界的瑞士军刀,Golang是胶水语言,两者结合,堪称绝配。
我的核心架构
素材目录 → 归类脚本(Go) → 每日素材列表 → 生成逐日片段脚本(Go+FFmpeg) → 合并逐日片段 → 添加背景音乐 → 输出成片
关键代码片段(简化版)
func generateDailyClip(day int, photos []string, outputPath string) {
// 1. 计算每日片段的时长:总时长365秒,每天约1秒
duration := 1.0
// 2. 用FFmpeg生成图片幻灯片段
// 如果当天有多个素材,平均分配时间
perPhotoDuration := duration / float64(len(photos))
// 3. 拼接FFmpeg命令
args := []string{
"-y",
"-framerate", fmt.Sprintf("%f", 1/perPhotoDuration),
"-pattern_type", "glob",
"-i", fmt.Sprintf("%s/*.jpg", getDayDir(day)),
"-c:v", "libx264",
"-vf", fmt.Sprintf("drawtext=text='第%d天':x=10:y=10:fontsize=24:fontcolor=white:box=1:boxcolor=black@0.5", day),
"-t", fmt.Sprintf("%f", duration),
outputPath,
}
cmd := exec.Command("ffmpeg", args...)
cmd.Run()
}
这里有个细节:drawtext滤镜中,字体路径在Windows和Linux上不一样,我在家里用Mac,部署到服务器是Linux,踩了一次坑,后来改成用环境变量配置字体路径,才解决。
第二个坑:视频尺寸不一致
媳妇用手机竖屏拍的,我用相机横屏拍的,混在一起,出来的视频一会儿横一会儿竖,看着头晕。
我的解决办法:统一强制转成16:9,黑边填充。
// 在FFmpeg命令中加入scale和pad滤镜 "-vf", "scale=1920:1080:force_original_aspect_ratio=1,pad=1920:1080:(ow-iw)/2:(oh-ih)/2:black"
这个参数的意思是:先等比缩放到1080P以内,然后用黑边补齐成16:9,效果就是上下左右可能出现黑边,但所有画面大小一致,视觉上舒服多了。
第三步:让“一周岁365天视频”有灵魂——内容编排的艺术
技术搞定了,但成品好不好看,编排,我和媳妇商量了一套规则,用Golang实现成可配置的映射表: 权重表
| 天数区间 | 内容侧重 | 特效风格 | 配乐情绪 |
|---|---|---|---|
| 第1-7天 | 医院、回家、第一次洗澡 | 暖色调、柔光 | 舒缓、轻柔 |
| 第8-30天 | 睡觉、吃奶、打哈欠 | 自然光+轻微复古 | 温馨、恬静 |
| 第31-100天 | 抬头、翻身、笑出声 | 明亮、饱和度稍高 | 轻快、愉悦 |
| 第101-200天 | 学坐、抓握、吃辅食 | 活力、对比度增强 | 节奏感渐强 |
| 第201-300天 | 爬行、站立、咿呀学语 | 鲜艳、快速切换 | 欢快、跳跃 |
| 第301-365天 | 走路、叫爸妈、生日 | 璀璨、金色调 | 高潮、温馨结尾 |
这个表怎么用? 在Golang里,我写了一个ContentPlanner结构体,读入这个配置,自动给每天的片段分配特效参数。
type ContentPlanner struct {
rules []DayRangeRule
}
type DayRangeRule struct {
StartDay int
EndDay int
FilterParams string // FFmpeg滤镜参数
Mood string
Transition string // 转场效果
}
func (p *ContentPlanner) GetRuleForDay(day int) DayRangeRule {
for _, rule := range p.rules {
if day >= rule.StartDay && day <= rule.EndDay {
return rule
}
}
// 默认规则
return p.rules[len(p.rules)-1]
}
说人话就是: 第5天和第200天,画面色调、切换速度都不一样,宝宝刚出生那几天,画面要暖、要慢;后面会爬了,画面切换要快,要活泼,这样看下来,视频才有“成长感”,而不是365张照片的机械堆砌。
第四步:最折磨人的部分——并发处理365个片段
一开始我的脚本是顺序执行的,给每一天生成一个1秒的片段,然后合并,365个片段,每个片段调用一次FFmpeg,生成一个1秒的视频文件。
结果呢?跑了快两个小时。
我媳妇问我:“好了没?”我说:“快了,还有153天。”她说:“你确定你是在给一周岁做视频,不是在给考研做题目?”
我反思了一下:365个片段之间没有依赖关系,完全可以并行生成,Golang的goroutine天生就是干这个的。
用WaitGroup控制并发
func generateAllClips(days []int, outputDir string) {
var wg sync.WaitGroup
sem := make(chan struct{}, 8) // 限制最多8个并发,防止CPU爆炸
for _, day := range days {
wg.Add(1)
go func(d int) {
defer wg.Done()
sem <- struct{}{} // 获取信号量
defer func() { <-sem }() // 释放信号量
clipPath := filepath.Join(outputDir, fmt.Sprintf("day_%03d.mp4", d))
generateDailyClip(d, getPhotosForDay(d), clipPath)
}(day)
}
wg.Wait()
}
结果:8个并发跑,大概15分钟就生成完了所有片段,然后合并这些片段,加上背景音乐,再输出最终视频,总共不到20分钟。
合并片段的命令
func mergeClips(clipDir string, outputPath string) {
// 先创建一个文件列表
listFile := "clips.txt"
// 按天数排序写入
// file 'day_001.mp4'
// file 'day_002.mp4'
// ...
cmd := exec.Command("ffmpeg",
"-y",
"-f", "concat",
"-safe", "0",
"-i", listFile,
"-c", "copy", // 直接复制流,不重新编码,超快
outputPath)
cmd.Run()
}
注意:-c copy这个参数很重要,它告诉FFmpeg不要重新编码,直接把片段拼接起来,如果重新编码,一个365秒的视频可能要再花10分钟,用copy模式,几秒钟就搞定。
第五步:背景音乐和字幕——最后的调味料
音乐要怎么加?
背景音乐我选的是《宝贝》纯钢琴版,时长刚好4分20秒,比365秒(6分05秒)短,所以我把音乐做了循环拼接,然后用-shortest参数让视频随着音乐结束而结束,而不是把音乐重复到视频结束。
// 音乐循环拼接 "-stream_loop", "-1", // 无限循环 "-i", "music.mp3", "-shortest", // 以较短的流为准结束 "-c:a", "aac", "-b:a", "192k",
字幕怎么做?
我没用硬编码的字幕(就是画在画面上的那种),而是用了SRT字幕文件,让观众可以选择开或者关。
SRT文件长这样:
1
00:00:00,000 --> 00:00:01,000
第1天 | 欢迎来到这个世界
2
00:00:01,000 --> 00:00:02,000
第2天 | 睡得好香啊
...
365
00:06:04,000 --> 00:06:05,000
第365天 | 一周岁快乐!
Golang生成这个文件也很简单,就循环写字符串。

真实成品效果与一点遗憾
最终生成的一周岁365天视频,全长6分05秒,1080P,文件大小约1.2GB。
媳妇看完哭了,我也挺感慨的,毕竟这一年,365个日日夜夜,全浓缩在这6分钟里了。
不过说实话,也有遗憾:
- 部分照片质量很差:奶奶用老年机拍的,只有480P,在1080P的画面上拉伸后模糊得不行,如果重新做,我会加一个AI超分处理。
- 音画同步有点小瑕疵:有些天的片段里,宝宝笑的声音和画面差了0.1秒,在快速切换时感觉很轻微,但如果你仔细看,能察觉到,这是因为不同设备的录音延迟不一样,我偷懒没做音频对齐。
- 没有做自动去重:有好几天,媳妇和我拍了几乎一样的角度和表情,导致视频里连续出现两张差不多的照片,应该加一个感知哈希去重。
但这些遗憾,反而让这个视频更真实,就像宝宝的成长一样,不完美,但独一无二。
后来我把这个Golang工具开源了,放在GitHub上,有几个爸爸下载了用,有个哥们留言说:“我女儿也一周岁了,用你的工具做了一个视频,老婆说比外面花三千块做的还好。”
看到那条留言的时候,我正抱着闺女在沙发上看动画片,她指着电视喊“爸、爸”,虽然知道她只是无意识地叫,但我还是应了一声,然后我跟媳妇说:“要不,咱明年再做一套两周岁视频?”
媳妇白了我一眼:“先把代码优化一下,别又跑两个小时。”
我说:“放心,这次用goroutine池,8核拉满,15分钟搞定。”
她没理我,低头刷手机去了,但我看到她嘴角在笑。
本文来自作者[kyadmin]投稿,不代表be365立场,如若转载,请注明出处:http://www.uss1.cn/nengyuan/1656.html
评论列表(4条)
我是be365的签约作者“kyadmin”!
希望本篇文章《一周岁365天视频,用Golang记录孩子的第一个365天,我踩过的坑和收获的感动》能对你有所帮助!
本站[be365]内容主要涵盖:be365,be365网站,be365网址,ball365体育,Be365排名
本文概览:为什么我要用Golang写一个“一周岁视频”生成工具?说实话,当爸之前我完全没想到,一周岁365天视频这事儿能把我折腾成这样。我...