在一起365发抖音视频,用Go语言记录我们的爱情代码
- 旅游
- 2026-06-28 16:54:08
- 747
从一条抖音开始的故事
说实话,我从来没想过自己会写这么一篇文章,但昨天,当我用Go语言写了个小工具,自动把我和女朋友在一起365天的照片做成了抖音视频,发出去之后居然有几千人点赞,评论区炸了,都在问“代码能不能分享一下”,我突然觉得,用程序员的浪漫方式记录生活,这事儿还挺有意思的。
你可能觉得,写代码和拍抖音扯不上关系,但如果你像我一样,是个每天和Go语言打交道的程序员,又恰好想给另一半一个特别的纪念,那么你一定会爱上这个点子,我想用最直白的方式,把整个思路讲给你听——不是那种故作高深的教程,而是像朋友聊天一样,一边想一边写出来。
为什么是Go语言而不是Python?
先别急着用Python,我知道很多人一提到自动化处理视频,第一反应就是Python,但说句实话,Go在处理大量文件操作、并发任务、以及最终编译成单一可执行文件这几件事上,真的比Python顺手太多了。
我一开始也想用Python写,但后来发现,女朋友发来的365张照片,每张都要读取、检查格式、压缩、生成视频帧,如果按顺序一张一张处理,得等到猴年马月,Go的goroutine天然适合干这种“一堆事情一起做”的活儿。
更要命的是,Python要部署给别人用,得让对方装Python环境,而Go编译出来就是个二进制文件,双击就能跑,我女朋友的电脑上连Python是什么都不知道,但她能双击我给的video_maker.exe——这就够了。
核心思路:把照片变成视频,再用FFmpeg加点料
说白了,我们干的事情就是:读取365张照片 → 按日期排序 → 逐帧生成视频 → 加上文案和背景音乐 → 输出成mp4 → 自动发抖音。
这里面有几个关键步骤,我踩了不少坑,现在一个个说清楚。
第一步:照片管理和命名规范
你得有365张照片,这不是废话,我女朋友的手机里存了上千张,但我们要选的是每天一张,我写了个Go程序,读取她手机备份里的照片,按照拍摄日期自动分组,然后从每一天的组里选一张——选哪张?我们用了个简单粗暴的方法:选文件大小最大的那张,因为通常拍得越认真,照片体积越大。
代码大概长这样:
type Photo struct {
Path string
Date time.Time
Size int64
}
然后按日期分组,取每组里Size最大的,这个逻辑我写在了一个叫picker.go的文件里,读取目录,处理EXIF信息——这里要用到一个叫github.com/rwcarlsen/goexif/exif的库,第一次用的时候我差点崩溃,因为有些照片EXIF信息是空的,后来我加了层fallback,读不到EXIF就用文件修改时间。
第二步:生成视频帧
这件事比我想象的复杂,一开始我直接用Go往视频文件里写二进制数据,写了半天发现,Go的标准库里没有直接生成MP4的能力,后来我妥协了——用Go生成FFmpeg命令,然后让系统调用FFmpeg干活。
但这里有个窍门:不要生成一条超长的FFmpeg命令,那样会超出命令行长度限制,我把所有照片的路径写到一个临时文件里,然后让FFmpeg读这个文件。
实现方式是:
- 创建一个
filelist.txt,格式是file '/path/to/photo1.jpg' - 每一行后面加个
duration 0.5,这样每张照片显示0.5秒 - 加上一个
crossfade效果,让转场不那么生硬
注意,duration和crossfade不能同时用,这是个坑,我查了半天文档才发现,需要先做一个无转场的视频,再用FFmpeg的xfade滤镜叠加转场,代码写起来像这样:
cmd := exec.Command("ffmpeg",
"-f", "concat",
"-safe", "0",
"-i", filelistPath,
"-vf", "fps=24",
"-c:v", "libx264",
tempVideoPath,
)
然后第二次调用FFmpeg,在头尾加上“365天”的文字水印和你想要的背景音乐。
第三步:加文字和音乐——这是灵魂
纯照片拼接的视频太单调了,我女朋友喜欢抖音上那种“小清新”风格:文字淡入淡出,音乐温柔但有点节奏。
文字我用的是FFmpeg的drawtext滤镜,这里注意中文字体问题——Windows上默认字体可能不支持中文,我手动指定了fontfile=C:/Windows/Fonts/msyh.ttc(微软雅黑),文字内容是写在Go代码里的,第1天”、“第100天”、“第365天”,我还在每张照片的右下角加了个小日期戳,用的strftime格式。
音乐方面,我用了一首免版权的钢琴曲,叫《With You》,直接在Go里读取music.mp3文件,混流进视频。
第四步:自动发抖音——这步最骚
视频做好了,怎么发到抖音?手动上传太low了,我研究了一下抖音的API,发现它没有公开的上传接口。—有个叫“抖音开放平台”的东西,申请之后可以用OAuth2.0授权,然后通过接口发布视频。
具体流程是:
- 在抖音开放平台创建应用,拿到
client_id和client_secret - 获取用户授权码(这一步需要手动扫码一次,以后可以自动刷新token)
- 调用
/video/upload/接口上传文件 - 调用
/video/create/接口填写标题和话题标签
这里有个细节:上传文件接口需要multipart/form-data格式,Go的net/http包原生支持,写起来很简单,但要注意设置Content-Type时不要手动拼字符串,用mime/multipart包生成。
我写了个tiktok_uploader.go,核心函数叫PostVideo,参数是视频文件路径、标题字符串,标题里自动加上“#我们的一周年 #365天挑战”之类的标签,这个函数里还处理了token过期自动刷新,因为抖音的access_token有效期只有7天。
第五步:定时任务——让浪漫自动化
你可能会问,难道每天都手动跑一遍程序?当然不是,我用Go写了个cron风格的调度器,每天凌晨2点执行一次(因为那时候服务器负载低,而且我女朋友肯定在睡觉,不会发现我在偷偷搞事情)。
调度器检查当天的日期,如果是365天中的某一天,就执行上述流程,如果哪一天照片没找到,它会发一条Slack消息通知我,让我手动补一张。
我用了github.com/robfig/cron/v3这个库,代码就几行:
c := cron.New()
c.AddFunc("0 2 * * *", func() {
makeDailyVideo()
uploadToTikTok()
})
c.Start()
踩坑记录与优化
没有哪段代码是一次写成的,我得坦白,这个项目我翻车了至少三次。
第一次翻车:照片顺序乱了。 因为Go读取目录文件的时候,默认是按文件名排序的,而我的照片文件名是“IMG_20230101.jpg”这种格式,问题在于,有些照片是晚上拍的,日期跨天了,但文件名还是前一天的,最后我用EXIF里的DateTimeOriginal字段重新排序,问题解决。
第二次翻车:视频太大。 365张照片,每张1920×1080,压缩之后仍然有300多MB,抖音上传限制是4GB,但手机端下载太慢了,我优化了压缩参数,把码率从15M降到5M,画质肉眼看不出来差别,体积降到了80MB。
第三次翻车:token失效。 在我出差那几天,程序突然不跑了,查日志发现是access_token过期了,刷新token的代码有个bug——刷新函数里把新token写到了全局变量,但调度器里的协程读的是旧值,后来我用sync.RWMutex加锁,问题解决。
用到的库和工具一览
| 库/工具 | 作用 | 版本 |
|---|---|---|
| FFmpeg | 视频编码、文字叠加、混流 | 0 |
| goexif | 读取照片EXIF信息 | v2.0 |
| cron | 定时调度 | v3 |
| mime/multipart | 文件上传编码 | 标准库 |
| net/http | HTTP客户端请求 | 标准库 |
| encoding/json | 解析API返回 | 标准库 |
| 抖音开放平台SDK | 获取token和发布视频 | v1.4.2 |
FFmpeg需要单独安装,其他都是Go模块,go get一下就行。
给想做这件事的人几点建议
第一,备份工作要做好,我女朋友的手机照片是自动同步到iCloud的,但程序读的是她电脑上的本地文件夹,我写了个文件同步函数,每次运行之前先检查本地和云端的差异,把缺失的照片下载下来。
第二,生成视频的过程大概需要15分钟(取决于CPU性能)。不要在手机上跑,那会烫得能煎鸡蛋,在服务器或者你的开发机上跑就好。
第三,如果女朋友问起来,别告诉她代码怎么写的,我直接跟她说“我写了个魔法”,她开心得不行,浪漫不需要解释原理。
代码之外的感悟
说实话,写这个程序花的功夫,比写业务代码多多了,但当你看到那365张照片像放电影一样闪过,想起第一张照片是她在医院实习时拍的,第一百张是在海边,第三百六十五张是我们一起做蛋糕——那一刻你会觉得,值了。
而且你知道吗?她后来又偷偷用我写的程序,把我们第二个365天的视频也做出来了,她不懂Go,但她会双击那个exe文件,这就够了。
如果你也想试试,从今天开始,每天存一张照片,一年之后,用Go写个程序,把那些碎片拼成一段视频,发到抖音上,配文就写“在一起365天,这是代码写的,也是真心写的。”
你的代码可以编译、分享、改进,就像你们的感情一样。
