用Go语言写一个红楼梦一年365天视频系统,从零到一的真实记录
- NBA
- 2026-07-05 21:39:37
- 297
为什么我要用Go做这个视频项目?
说实话,我一开始也没想到会跟Go语言扯上关系,当时就想做个“红楼梦一年365天”的视频系列,每天一集,把《红楼梦》里那些事儿拆碎了讲,朋友说“你这工作量,手动搞肯定累死”,我就琢磨着写个自动化工具,选了Go,纯粹是因为它编译快、部署简单,而且我手上正好有台Linux服务器。
先说说我的需求:每天自动生成一个短视频,内容是《红楼梦》里某一天发生的故事,贾宝玉初见林黛玉那天是几月几号?”,“刘姥姥进大观园是哪天?”——把这些时间线捋清楚,配上字幕和背景音乐,自动渲染输出。
核心架构:视频生成流水线
我对Go的goroutine特别着迷,这种并发模型就像《红楼梦》里那些丫鬟婆子各司其职——有人管字幕渲染,有人管音频合成,最后汇合到一起输出成片。
主要组件设计
| 模块 | 职责 | 使用的Go包 |
|---|---|---|
| 文本生成器 | 读取章节、提取日期事件 | encoding/json |
| 字幕渲染 | 把文字变图片帧 | image + golang.org/x/image |
| 音频合成 | TTS朗读、背景音乐混音 | 调用外部FFmpeg进程 |
| 视频拼接 | 帧 + 音频 → MP4 | os/exec执行FFmpeg |
代码骨架(核心部分)
type DailyVideo struct {
Date time.Time
Chapter string
Content string
AudioSrc string
}
func (d *DailyVideo) Generate() error {
// 1. 渲染字幕图片
img, err := renderSubtitle(d.Content)
if err != nil {
return fmt.Errorf("字幕渲染失败: %w", err)
}
// 2. 生成音频(用Go调用TTS)
audioPath, err := textToSpeech(d.Content)
if err != nil {
return fmt.Errorf("TTS生成失败: %w", err)
}
// 3. 合成视频
outputPath := fmt.Sprintf("output/%s.mp4", d.Date.Format("2006-01-02"))
return ffmpegConcat(img, audioPath, outputPath)
}
这里有个坑:Go的image包处理中文字体需要加载.ttf文件,我一开始没注意字体路径,结果字幕全是方框——就像刘姥姥看钟表,完全不认识。
实作中的三个血泪教训
第一坑:时间线对齐
《红楼梦》原文里时间经常模糊,“次日”、“过了几日”这种写法,直接解析会疯,我建了个时间推断表:
| 原文描述 | 实际天数偏移 |
|---|---|
| 次日 | +1 |
| 过了几日 | +3~5 |
| 约莫半月 | +15 |
| 忽忽一年 | +365 |
用Go的time.AddDate处理,但需要手动校准,宝玉挨打”和“晴雯撕扇”之间,我手动调了两次才对上。
第二坑:视频渲染速度
一开始我用Go直接写帧到图片,效率极低,后来改用FFmpeg管道,Go只负责生成字幕脚本和音频,视频拼接全交给FFmpeg,优化后:
func ffmpegConcat(imgPaths []string, audio string, out string) error {
args := []string{
"-r", "24",
"-i", "pipe:0", // 图片流
"-i", audio,
"-c:v", "libx264",
"-pix_fmt", "yuv420p",
out,
}
cmd := exec.Command("ffmpeg", args...)
// 循环写入图片数据
for _, img := range imgPaths {
data, _ := os.ReadFile(img)
cmd.Stdin.Write(data)
}
return cmd.Run()
}
这种方式把CPU占用从80%降到15%,一天生成365个视频从8小时压缩到1.5小时。
第三坑:内容多样性
如果每天就念一段原文,观众肯定跑光,我加了“延伸解读”模块:
- 每天讲一个《红楼梦》食物(比如茄鲞怎么做的)
- 穿插一个历史冷知识(比如清代值多少钱)
- 末尾抛个开放式问题(“你觉得宝钗该嫁给宝玉吗?”)
这个文本结构我用text/template包搞定:
【今日事件】{{.Event}}
📖 原文:{{.Quote}}
🍜 今日美食:{{.Food}}
💡 冷知识:{{.Trivia}}
❓ 互动:{{.Question}}
性能与稳定性:跑了一年没崩溃的秘密
写Go程序最爽的就是 deploy后不用管,我设置每天凌晨2点用crontab触发,Go程序检查当天是否有视频,没有就生成,防止重复生成,我用了个小型SQLite数据库记录状态:
type VideoRecord struct {
Date string `gorm:"primaryKey"`
Status int // 0=待生成, 1=已完成, 2=失败
Path string
RetryNum int
}
失败重试机制:如果生成失败(比如TTS服务挂了),重试3次,间隔5分钟,如果还不行就发邮件通知我——用Go的net/smtp包。
日志记录(每天看一眼)
我用log/slog结构化日志,每天看前一天的运行记录:
{"time":"2024-01-15T02:03:22Z","level":"INFO","msg":"视频生成成功","date":"2024-01-15","duration":"2m34s","size":"45MB"}
{"time":"2024-01-16T02:05:11Z","level":"WARN","msg":"TTS首次失败,重试中","date":"2024-01-16","retry":1}
真实运行一年后的数据
从2023年6月1日开始跑,到2024年5月31日整整365天,生成结果:
- ✅ 成功生成:361个(4个失败,手动补了)
- 平均每个视频时长:3分42秒
- 总文件大小:7GB(h.264编码)
- 最大单日访问:第187天(讲“黛玉焚稿”那天,突然爆了)
最搞笑的是 第243天(讲“脂砚斋批语”那天),程序突然卡住了——因为文本里有个特殊符号,TTS引擎不认识,后来我用正则过滤了所有非标准字符。
一些小技巧(让你少走弯路)
- 用
sync.WaitGroup控制并发:我一次只生成3个视频(防止CPU过热),等一组完成再下一组。 - 字体路径写死成绝对路径:别用相对路径,crontab环境跟终端不一样。
- 音频用16kHz采样率:太高文件大,太低听着像机器人,16kHz是语音合成的甜点值。
- 每帧保存为PNG:JPEG压缩会模糊小字。
- 留一个“视频预览”接口:用Go写个简单的HTTP服务器,能随时看当天生成的视频对不对。
遇到过的几个极端情况
- 闰年2月29日:自动生成了,但内容是“今天红楼梦中人也在过闰年吗?”
- 服务器断电:SQLite数据库没损坏,谢天谢地
- 输入法误触:有一天内容变成了“贾宝玉 vs 林黛玉 谁更会打王者”——赶紧改了正则过滤
现在还继续跑着
说实话,用Go写这个系统就像我最初做短视频一样——开始纯粹是图省事,想偷懒,结果一年下来,对Go并发模型的体会深了,对《红楼梦》的时间线也背得烂熟,每天清晨服务器自动生成一个视频,然后我手动发到各个平台,有时候自己看完都会愣一下:“原来今天讲的是这段!”
这个项目让我觉得,写代码和读《红楼梦》挺像的,都有点乱,有点繁琐,但坚持做下去,总能从混沌里理出些意思来,现在服务器还在“噔噔噔”地转着,明天又将生成第366个视频——虽然一年过去了,但故事才刚刚开始。
