
Video Creator
- 94 installs
- 263 repo stars
- Updated July 22, 2026
- zrt-ai-lab/opencode-skills
Helps with ai & agent building tasks during AI-assisted development.
About
video-creator is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- video-creator
- AI & Agent Building
- AI-coding skill
Video Creator by the numbers
- 94 all-time installs (skills.sh)
- Ranked #4,499 of 16,556 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zrt-ai-lab/opencode-skills --skill video-creatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 94 |
|---|---|
| repo stars | ★ 263 |
| Last updated | July 22, 2026 |
| Repository | zrt-ai-lab/opencode-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Video Creator
图片+音频合成视频工具。
---
⛔⛔⛔ 最高铁律:音画同步(违反即废片重做!)
每次生成视频,duration 必须从 narration.json 时间戳精确计算,绝对禁止凭感觉手动填!
强制流程(缺一步都不行)
1. TTS 生成配音 → 得到 narration.mp3 + narration.json(时间戳)
2. 读取 narration.json,逐句分析内容语义
3. 确定每张图对应哪些句子(按内容语义匹配,不是平均分!)
4. 每张图的 duration = 对应最后一句的 end - 对应第一句的 start
5. 校验:所有 duration 之和 ≈ 音频总时长(误差 < 1s)
6. ⛔ 运行 verify_alignment.py 校验(必须通过才能合成!)
7. 校验通过才能写 video_config.yaml,否则停止!示例(以古诗教学为例)
narration.json 时间戳:
句0 [0.1-2.6] "静夜思,唐,李白。"
句1 [2.6-5.7] "床前明月光,疑是地上霜。"
句2 [5.7-8.6] "举头望明月,低头思故乡。"
句3 [8.6-16.4] "这首诗是唐代大诗人李白..."
句4 [16.4-20.9] "短短二十个字..."
句5 [20.9-22.6] "床前明月光。"
...
图片分配:
01_title.png → 句0-2(全诗朗诵)→ duration = 8.6 - 0.1 = 8.5s
02_poet.png → 句3-4(诗人介绍)→ duration = 20.9 - 8.6 = 12.3s
03_moonlight.png → 句5-8(第一句解读)→ duration = 38.6 - 20.9 = 17.7s
...
校验:8.5 + 12.3 + 17.7 + ... = 114.4s ≈ 音频 114.5s ✅禁止行为
- ❌ 凭感觉给每张图分配 10s、15s、20s
- ❌ 平均分配(总时长 / 图片数)
- ❌ 不读 narration.json 就写 duration
- ❌ 图片总时长和音频差 5 秒以上还强行合成
- ❌ 让 video_maker.py 自动拉伸超过 1 秒
- ❌ duration: auto(已从代码中彻底删除!)
---
生图铁律(违反即重做)
0. 默认尺寸(最重要!)
默认比例:16:9(1920×1080 横版)
除非以下情况,否则一律用 16:9:
- 用户明确指定其他比例
- 与其他流程协同时,其他流程有明确要求(如小红书要 3:4,抖音要 9:16)
# ✅ 默认:不指定 -r 参数,或明确写 -r 16:9
python text_to_image.py "提示词" -o output.png
python text_to_image.py "提示词" -r 16:9 -o output.png
# ❌ 禁止:随意切换比例,一会儿竖版一会儿横版
# 同一个视频项目,所有图片必须是同一比例!1. 图片密度要求
| 视频时长 | 最少图片数 | 每张图时长 |
|---|---|---|
| 30秒 | 8张 | 3-4秒 |
| 60秒 | 15张 | 3-5秒 |
| 90秒 | 22张 | 3-5秒 |
| 120秒 | 30张 | 3-5秒 |
铁律:每张图最长 7 秒,超过必须拆分!
计算公式:图片数量 = ceil(音频时长 / 4)
2. 生图提示词语言要求
所有生图 prompt 必须用中文写!禁止英文 prompt!
# ❌ 错误:用英文写 prompt
python text_to_image.py "modern tech illustration, AI robot, blue gradient background"
# ❌ 错误:英文 prompt 里夹中文
python text_to_image.py "tech style, Chinese text '对决', blue theme"
# ✅ 正确:纯中文 prompt
python text_to_image.py "现代科技插画风格,可爱AI机器人坐在电脑前,蓝紫色渐变背景,霓虹灯光效,多个悬浮的全息UI面板,信息密度高,专业信息图风格"铁律:
- prompt 必须是纯中文
- 生成的图片里如果有文字,也必须是中文
- 禁止任何英文出现
3. 信息密度要求
信息密度 = 文字要点多 + 视觉元素丰富
文字内容要丰富:
# ❌ 错误:文字太少
图片里只写 "AI对比"
# ✅ 正确:文字要点多
图片里包含:
- 标题:QoderWork vs OpenClaw
- 副标题:桌面AI助手对比
- 要点1:开箱即用 vs 自由定制
- 要点2:$19/月 vs 免费开源
- 要点3:普通用户 vs 技术极客视觉元素要丰富:
# ❌ 错误:太空洞
只有文字,没有图标、图表、装饰
# ✅ 正确:可视化丰富
- 配合图标(对勾、叉、箭头、星星)
- 配合图表(柱状图、饼图、对比条)
- 配合插画(机器人、电脑、用户形象)
- 配合装饰(光效、渐变、边框)信息密度原则:
- 每张图要有明确的文字标题和要点
- 文字内容要和配音内容对应
- 更重要的是可视化:图标、图表、插画、装饰
- 禁止纯文字图,也禁止纯装饰图
4. 生图描述要细致具体
每张图必须有丰富、具体的视觉元素,禁止笼统空洞!
# ❌ 错误:太笼统
"一个机器人"
"科技风格的图"
"对比图"
# ✅ 正确:细致具体
"可爱的蓝色AI机器人吉祥物,圆润金属质感,坐在现代简约办公桌前,
桌上有27寸曲面显示器显示代码,旁边放着咖啡杯和多肉植物,
机器人头顶悬浮三个全息面板分别显示折线图、饼图、进度条,
深蓝色科技感背景,地面有蓝色光带,整体赛博朋克风格,柔和的体积光"prompt 必备 6 要素(缺一不可):
| 要素 | 说明 | 示例 |
|---|---|---|
| 主体 | 谁/什么东西,要具体 | "蓝色圆润金属质感的AI机器人" 而非 "机器人" |
| 动作 | 在做什么,姿态如何 | "双手放在键盘上打字,微微侧头" |
| 环境 | 在哪里,背景是什么 | "现代简约办公室,落地窗外是城市夜景" |
| 细节 | 周围有什么物品/元素 | "桌上有咖啡杯、多肉植物、便签纸" |
| 风格 | 什么画风/光效 | "赛博朋克风格,霓虹灯光,体积光效果" |
| 色彩 | 主色调是什么 | "蓝紫渐变主色调,橙色点缀" |
5. 视觉风格一致性
同一个视频的所有图片必须保持风格统一:
- 使用相同的风格前缀
- 使用相同的色彩基调
- 使用相同的比例(禁止混用横竖版!)
# 定义统一风格前缀(中文!)
STYLE="现代科技插画风格,干净矢量设计,蓝紫渐变配色,专业信息图美感,高信息密度,霓虹发光效果,深色背景"
# 所有图片都用这个前缀 + 相同比例
python text_to_image.py "$STYLE,[具体场景内容]" -r 16:9 -o scene01.png
python text_to_image.py "$STYLE,[具体场景内容]" -r 16:9 -o scene02.png6. 比例选择指南
| 场景 | 比例 | 说明 |
|---|---|---|
| 默认/通用 | 16:9 | B站、YouTube、公众号视频、PPT配图 |
| 抖音/视频号/快手 | 9:16 | 竖屏短视频平台,需用户指定或流程要求 |
| 小红书 | 3:4 | 小红书笔记配图,需用户指定或流程要求 |
| 朋友圈 | 1:1 | 正方形,需用户指定 |
铁律:不确定用什么比例时,一律用 16:9
---
核心流程(铁律)
故事类视频生成流程(套娃流程)
当用户提供故事/剧情/剧本时,必须严格按以下套娃流程执行:
┌─────────────────────────────────────────────────────────────┐
│ 第一层:故事 → 拆分场景 → 并发生成场景主图(文生图) │
│ │
│ 大闹天宫 → 场景1:弼马温受辱 │
│ 场景2:筋斗云回花果山 │
│ 场景3:玉帝派兵 │
│ ... │
│ → 并发调用 text_to_image.py 生成每个场景主图 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 第二层:每个场景主图 → 图生图拆出细镜头(保持角色一致) │
│ │
│ 场景1主图 → 细镜头1:悟空看官印疑惑 │
│ 细镜头2:悟空踢翻马槽 │
│ 场景2主图 → 细镜头1:踏筋斗云腾空 │
│ 细镜头2:花果山自封大圣 │
│ → 并发调用 image_to_image.py,以主图为参考 │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ 第三层:生成配音 + 字幕 + 校验 + 合成视频 │
│ │
│ 1. tts_generator.py 生成配音 + 时间戳 │
│ 2. 【铁律】根据时间戳精确计算每张图的duration │
│ 3. 生成 SRT 字幕 │
│ 4. 生成 video_config.yaml │
│ 5. ⛔ verify_alignment.py 校验(必须通过!) │
│ 6. video_maker.py 合成: │
│ → 图片合成(带转场) │
│ → 合并音频 │
│ → 烧录字幕(ASS格式,底部居中固定) │
│ → 自动拼接片尾(二维码+"点关注不迷路") │
│ → 添加BGM │
└─────────────────────────────────────────────────────────────┘
**铁律:所有视频必须自动拼接片尾!**目录结构规范
assets/generated/{project_name}/
├── scene1/
│ ├── main.png # 场景1主图(文生图)
│ ├── shot_01.png # 细镜头1(图生图)
│ └── shot_02.png # 细镜头2(图生图)
├── scene2/
│ ├── main.png
│ ├── shot_01.png
│ └── shot_02.png
├── ...
├── narration.mp3 # 配音
├── narration.json # 时间戳
├── subtitles.srt # 字幕
├── video_config.yaml # 视频配置
└── {project_name}.mp4 # 最终视频执行命令示例
# 第一层:并发生成场景主图(默认 16:9)
python .opencode/skills/image-service/scripts/text_to_image.py "风格描述,场景1内容" -r 16:9 -o scene1/main.png &
python .opencode/skills/image-service/scripts/text_to_image.py "风格描述,场景2内容" -r 16:9 -o scene2/main.png &
wait
# 第二层:并发图生图生成细镜头(保持相同比例)
python .opencode/skills/image-service/scripts/image_to_image.py scene1/main.png "保持角色风格,细镜头描述" -r 16:9 -o scene1/shot_01.png &
python .opencode/skills/image-service/scripts/image_to_image.py scene1/main.png "保持角色风格,细镜头描述" -r 16:9 -o scene1/shot_02.png &
wait
# 第三层:生成配音+校验+合成视频
python .opencode/skills/video-creator/scripts/tts_generator.py --text "完整旁白" --output narration.mp3 --timestamps
# ⛔ 强制校验(必须通过才能合成!)
python .opencode/skills/video-creator/scripts/verify_alignment.py video_config.yaml
# 校验通过后才能合成
python .opencode/skills/video-creator/scripts/video_maker.py video_config.yaml --srt subtitles.srt --bgm epic---
视频配置文件格式
# video_config.yaml
ratio: "16:9" # 默认横版!必须加引号避免YAML解析错误
bgm_volume: 0.12
outro: true
scenes:
- audio: narration.mp3
images:
# 按场景顺序排列所有细镜头
- file: scene1/shot_01.png
duration: 4.34
- file: scene1/shot_02.png
duration: 4.88
- file: scene2/shot_01.png
duration: 2.15
# ...注意:ratio 必须用引号包裹,如 "16:9",否则 YAML 会解析成时间格式。
---
时长分配规范(铁律!)
生成 video_config.yaml 前,必须严格按以下流程计算 duration:
步骤1:读取时间戳文件
import json
with open("narration.json", "r") as f:
timestamps = json.load(f)
audio_duration = timestamps[-1]["end"]
print(f"音频总时长: {audio_duration:.1f}s")步骤2:按内容语义划分场景
根据解说词内容,确定每张图对应的时间段:
# 示例:根据解说词内容划分
# 找到每个主题切换点的时间戳
scenes = [
("cover.png", 0, 12.5), # 开场到第一个主题切换
("scene01.png", 12.5, 26), # 第二段内容
# ...根据 narration.json 中的句子边界精确划分
]步骤3:计算每张图的 duration
for file, start, end in scenes:
duration = end - start
print(f"{file}: {duration:.1f}s")步骤4:校验总时长
total_duration = sum(duration for _, _, duration in scenes)
assert abs(total_duration - audio_duration) < 1.0, \
f"时长不匹配!图片总时长{total_duration}s vs 音频{audio_duration}s"铁律
1. 必须先读取 narration.json 时间戳,不能凭感觉估算 2. 按句子语义边界划分,不能平均分配 3. 生成配置前必须校验,确保图片总时长 = 音频总时长(误差<0.5秒) 4. 禁止让脚本自动拉伸,音画不同步的视频不合格 5. 禁止 duration=0,每张图最少 1 秒 6. 生成 yaml 后必须用 verify_alignment.py 校验再合成
校验脚本(强制执行!合成前必须通过!)
# ⛔ 合成视频前必须先运行校验脚本,不通过禁止合成!
python .opencode/skills/video-creator/scripts/verify_alignment.py video_config.yaml
# 校验内容:
# 1. 所有图片文件是否存在
# 2. duration 是否为精确数值(非数字直接拒绝!)
# 3. 图片总时长 vs 音频总时长(误差 < 1s)
# 4. 每张图时长是否在合理范围(1-7s)
# 5. 图片文件名关键词 vs 语音内容关键词 语义交叉比对
# 6. 输出完整对照表(图片 + 时长 + 语义✅/❌ + 对应语音文字)
# 退出码 0 = 通过,1 = 失败
# 失败时禁止合成,必须修复后重新校验!注意:video_maker.py 也内置了硬校验——duration 必须是精确正数浮点数,缺失或非数字直接拒绝执行并 exit(1)!duration: auto 已被彻底删除!
时长分配表模板
生成配置前,先输出分配表确认:
| 场景图 | 对应内容 | 开始 | 结束 | 时长 |
|--------|----------|------|------|------|
| cover.png | 开场引入 | 0s | 12.5s | 12.5s |
| scene01.png | AI Agent时代 | 12.5s | 26s | 13.5s |
| ... | ... | ... | ... | ... |
| **合计** | | | | **{total}s** |
音频总时长:{audio_duration}s
差值:{diff}s ✅/❌---
字幕规范
字幕使用 ASS 格式,强制底部居中固定位置:
- 位置:底部居中(Alignment=2)
- 字体:PingFang SC
- 大小:屏幕高度 / 40
- 描边:2px 黑色描边 + 1px 阴影
- 底边距:屏幕高度 / 20
禁止:字幕乱跑、大小不一、位置不固定
---
脚本参数说明
video_maker.py
python video_maker.py config.yaml [options]| 参数 | 说明 | 默认值 |
|---|---|---|
--no-outro | 不添加片尾 | 添加 |
--no-bgm | 不添加BGM | 添加 |
--fade | 转场时长(秒) | 0.5 |
--bgm-volume | BGM音量 | 0.08 |
--bgm | 自定义BGM(可选: epic) | 默认科技风 |
--ratio | 视频比例 | 16:9(会被配置文件覆盖) |
--srt | 字幕文件路径 | 无 |
verify_alignment.py
python verify_alignment.py video_config.yaml| 校验项 | 说明 |
|---|---|
| 图片存在性 | 所有图片文件必须存在 |
| duration 精确性 | 必须是正数浮点数,禁止 auto/空值/非数字 |
| 总时长匹配 | 图片总时长 vs 音频总时长,误差 < 1s |
| 单图时长范围 | 每张图 1-7 秒,超出警告 |
| 语义交叉比对 | 总结段图片文件名关键词 vs 语音内容关键词匹配 |
tts_generator.py
python tts_generator.py --text "文本" --output audio.mp3 [options]| 参数 | 说明 | 默认值 |
|---|---|---|
--voice | 音色 | zh-CN-YunxiNeural |
--rate | 语速 | +0% |
--timestamps | 输出时间戳JSON | 否 |
---
支持的视频比例
与 image-service 生图服务保持一致,支持 10 种比例:
| 比例 | 分辨率 | 适用场景 |
|---|---|---|
| 1:1 | 1024×1024 | 正方形,朋友圈 |
| 2:3 | 832×1248 | 竖版海报 |
| 3:2 | 1248×832 | 横版海报 |
| 3:4 | 1080×1440 | 小红书、朋友圈 |
| 4:3 | 1440×1080 | 传统显示器 |
| 4:5 | 864×1080 | |
| 5:4 | 1080×864 | 横版照片 |
| 9:16 | 1080×1920 | 抖音、视频号、竖屏 |
| 16:9 | 1920×1080 | B站、YouTube、横屏 |
| 21:9 | 1536×672 | 超宽屏电影 |
---
片尾规范
铁律:所有视频必须自动拼接对应尺寸的片尾!
片尾匹配顺序: 1. 精确匹配:outro_{ratio}.mp4 2. 方向匹配:竖版→outro_9x16.mp4,横版→outro_16x9.mp4 3. 兜底:outro.mp4
---
BGM 资源
按风格分类
| 风格 | 文件 | 快捷参数 | 适用场景 |
|---|---|---|---|
| 古风/中国风 | bgm_ancient_tale.mp3 | ancient | 水浒、三国、历史故事 |
bgm_asian_drums.mp3 | asian | 武侠、动作、战斗 | |
bgm_meditation.mp3 | meditation | 禅意、冥想、国学 | |
bgm_garden.mp3 | garden | 田园、悠闲、风景 | |
| 治愈/轻松 | bgm_carefree.mp3 | carefree | Vlog、日常、生活 |
bgm_dreamy.mp3 | dreamy | 梦幻、回忆、温馨 | |
bgm_happybee.mp3 | happybee | 欢快、明亮、阳光 | |
| 热血/史诗 | bgm_epic.mp3 | epic | 励志、战斗、高燃 |
bgm_heroic.mp3 | heroic | 英雄、胜利、荣耀 | |
bgm_crusade.mp3 | crusade | 战争、史诗、宏大 | |
bgm_allthis.mp3 | allthis | 电影感、叙事、情感 | |
| 科技/未来 | bgm_technology.mp3 | tech | AI、产品、教程 |
bgm_digital.mp3 | digital | 数码、网络、互联网 | |
bgm_chasm.mp3 | chasm | 科幻、太空、神秘 | |
| 悬疑/紧张 | bgm_anxiety.mp3 | anxiety | 推理、紧张、危机 |
bgm_darkfog.mp3 | darkfog | 恐怖、黑暗、悬疑 | |
| 欢快/活泼 | bgm_funky.mp3 | funky | 搞笑、轻松、节奏感 |
bgm_happyboy.mp3 | happyboy | 可爱、儿童、动画 | |
bgm_doh.mp3 | doh | 俏皮、有趣、短视频 | |
| 电子/节奏 | bgm_edm.mp3 | edm | 动感、剪辑、卡点 |
bgm_electro.mp3 | electro | 电子、现代、潮流 | |
bgm_bitshift.mp3 | bitshift | 游戏、8-bit、复古电子 | |
bgm_hiphop.mp3 | hiphop | 说唱、街头、潮酷 |
使用方式
# 方式1:快捷参数(推荐)
python video_maker.py config.yaml --bgm epic
python video_maker.py config.yaml --bgm ancient
python video_maker.py config.yaml --bgm edm
# 方式2:完整文件名
python video_maker.py config.yaml --bgm bgm_ancient_tale.mp3
# 方式3:自定义路径
python video_maker.py config.yaml --bgm /path/to/custom.mp3---
常用音色
| 音色 ID | 风格 |
|---|---|
| zh-CN-YunyangNeural | 男声,新闻播报 |
| zh-CN-YunxiNeural | 男声,阳光活泼 |
| zh-CN-XiaoxiaoNeural | 女声,温暖自然 |
| zh-CN-XiaoyiNeural | 女声,活泼可爱 |
---
目录结构
video-creator/
├── SKILL.md
├── scripts/
│ ├── video_maker.py # 主脚本:图片+音频→视频(内置duration硬卡)
│ ├── verify_alignment.py # 合成前强制校验(时长+语义交叉比对)
│ ├── tts_generator.py # TTS 语音生成
│ └── scene_splitter.py # 场景拆分器(可选)
├── assets/
│ ├── outro.mp4 # 通用片尾(16:9)
│ ├── outro_9x16.mp4 # 竖版片尾
│ ├── outro_3x4.mp4 # 3:4片尾
│ └── bgm_*.mp3 # 22首BGM(详见BGM资源表)
└── references/
└── edge_tts_voices.md---
依赖
# 系统依赖
brew install ffmpeg # Mac
# Python 依赖
pip install edge-tts pyyaml# 默认配置 - 所有项目继承此配置
# 项目配置只需写差异部分即可
# 视频基础配置
resolution: [1080, 1920] # 默认竖版
fps: 30
# 语音默认配置(项目可覆盖)
voice:
name: "zh-CN-YunxiNeural" # 默认男声
rate: "+0%"
pitch: "+0Hz"
# 可选音色参考:
# 男声: zh-CN-YunxiNeural, zh-CN-YunyangNeural, zh-CN-YunjianNeural
# 女声: zh-CN-XiaoxiaoNeural, zh-CN-XiaoyiNeural, zh-CN-XiaohanNeural
# 方言: zh-CN-liaoning-XiaobeiNeural, zh-CN-shaanxi-XiaoniNeural
# 样式默认配置
style:
font: "PingFang SC"
font_size: 42
font_color: "#FFFFFF"
stroke_color: "#000000"
stroke_width: 2
subtitle_position: "bottom" # bottom/top/center
subtitle_bg: true
subtitle_bg_color: "#000000"
subtitle_bg_opacity: 0.7
highlight_color: "#FFD700"
# 动画默认配置
animation:
ken_burns: true
default_animation: "zoom_in"
animation_intensity: 0.15 # 动画幅度 0.1~0.3
# 可选动画效果:
# 缩放: zoom_in, zoom_out
# 平移: pan_left, pan_right, pan_up, pan_down
# 组合: zoom_pan_left, zoom_pan_right, zoom_pan_up, zoom_pan_down
# 转场默认配置
transition:
type: "fade"
duration: 0.5
# 可选转场效果:
# 基础: fade, dissolve
# 滑动: slide_left, slide_right, slide_up, slide_down
# 特效: zoom_blur, wipe_left, wipe_right
# 片头默认配置
intro:
enabled: true
duration: 3
title_animation: "scale_in" # scale_in/fade_in/typewriter
background_color: "#0d1117"
title_color: "#FFFFFF"
# 片尾默认配置
outro:
enabled: true
duration: 4
logo: "assets/logo.jpg"
logo_animation: "scale_in"
background_color: "#0d1117"
text: "点点关注,学习更多的AI知识"
text_color: "#CCCCCC"
# 背景音乐(项目可配置)
# background_music: "path/to/music.mp3"
# music_volume: 0.1 # 0.0~1.0
# 视频创作器 - 示例配置
# 使用方法: python scripts/video_generator.py assets/example_config.yaml
# 视频基础配置
title: "AI 前沿速递"
output: "output/ai_news.mp4"
resolution: [1920, 1080] # 横版 16:9
# 语音配置
voice:
name: "zh-CN-YunxiNeural" # 男声新闻风
rate: "+0%"
pitch: "+0Hz"
# 样式配置
style:
font: "PingFang SC"
font_size: 48
font_color: "#FFFFFF"
stroke_color: "#000000"
stroke_width: 2
subtitle_position: "bottom"
subtitle_bg: true
subtitle_bg_color: "#000000"
subtitle_bg_opacity: 0.6
# 动画配置
animation:
ken_burns: true
default_animation: "zoom_in"
animation_intensity: 0.1
# 转场配置
transition:
type: "fade"
duration: 0.5
# 片头配置
intro:
enabled: true
duration: 3
title_animation: "fade_up"
background_color: "#1a1a2e"
title_color: "#FFFFFF"
# 片尾配置
outro:
enabled: true
duration: 3
logo: "assets/logo.jpg" # 微信公众号 Logo
logo_animation: "bounce_in"
background_color: "#1a1a2e"
text: "点点关注,学习更多的AI知识"
text_color: "#CCCCCC"
# 场景列表
scenes:
- image: "assets/scene_01.png"
text: "今天我们来聊聊 AI Agent 的最新进展"
animation: "zoom_in"
- image: "assets/scene_02.png"
text: "首先是 OpenAI 发布的 Operator"
animation: "pan_right"
highlight: ["Operator"]
- image: "assets/scene_03.png"
text: "它能自动操作浏览器完成复杂任务"
animation: "zoom_out"
- image: "assets/scene_04.png"
text: "这意味着 AI 正在从对话走向行动"
animation: "pan_left"
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1248" height="832" viewBox="0 0 1248 832">
<defs>
<g>
<g id="glyph-0-0">
<path d="M 5.722656 -10.414062 L 5.722656 -6.683594 L 2.25 -6.683594 L 2.25 -2.582031 L 10.707031 -2.582031 L 10.707031 -6.683594 L 6.632812 -6.683594 L 6.632812 -8.304688 L 11.589844 -8.304688 L 11.589844 -9.175781 L 6.632812 -9.175781 L 6.632812 -10.414062 Z M 9.789062 -3.4375 L 3.167969 -3.4375 L 3.167969 -5.8125 L 9.789062 -5.8125 Z M 2.222656 -2.019531 C 1.761719 -0.972656 1.214844 -0.0625 0.574219 0.691406 L 1.328125 1.238281 C 1.96875 0.421875 2.515625 -0.535156 3.003906 -1.648438 Z M 5.007812 -1.648438 L 4.152344 -1.46875 C 4.394531 -0.664062 4.601562 0.253906 4.777344 1.304688 L 5.699219 1.097656 C 5.496094 0.113281 5.265625 -0.804688 5.007812 -1.648438 Z M 7.679688 -1.777344 L 6.808594 -1.597656 C 7.195312 -0.765625 7.511719 0.191406 7.769531 1.277344 L 8.675781 1.074219 C 8.394531 0.0507812 8.0625 -0.90625 7.679688 -1.777344 Z M 10.296875 -2.03125 L 9.558594 -1.558594 C 10.335938 -0.511719 10.949219 0.433594 11.398438 1.304688 L 12.164062 0.765625 C 11.730469 -0.0273438 11.117188 -0.957031 10.296875 -2.03125 Z "/>
</g>
<g id="glyph-0-1">
<path d="M 3.960938 -10.386719 L 3.117188 -9.980469 C 3.640625 -9.339844 4.128906 -8.613281 4.585938 -7.792969 L 5.417969 -8.214844 C 4.957031 -9.007812 4.472656 -9.722656 3.960938 -10.386719 Z M 8.765625 -10.503906 C 8.382812 -9.429688 7.960938 -8.523438 7.476562 -7.78125 L 1.636719 -7.78125 L 1.636719 -6.886719 L 5.914062 -6.886719 L 5.914062 -6.285156 C 5.902344 -5.660156 5.839844 -5.074219 5.722656 -4.511719 L 0.90625 -4.511719 L 0.90625 -3.617188 L 5.496094 -3.617188 C 5.316406 -3.054688 5.074219 -2.554688 4.765625 -2.09375 C 4 -1.074219 2.632812 -0.242188 0.675781 0.382812 L 1.175781 1.1875 C 3.144531 0.5625 4.5625 -0.320312 5.429688 -1.445312 C 5.839844 -1.992188 6.160156 -2.632812 6.402344 -3.359375 C 7.089844 -1.417969 8.828125 0.128906 11.640625 1.265625 L 12.175781 0.410156 C 9.492188 -0.550781 7.84375 -1.890625 7.21875 -3.617188 L 11.871094 -3.617188 L 11.871094 -4.511719 L 6.683594 -4.511719 C 6.785156 -5.046875 6.835938 -5.648438 6.863281 -6.285156 L 6.863281 -6.886719 L 11.128906 -6.886719 L 11.128906 -7.78125 L 8.496094 -7.78125 C 8.957031 -8.507812 9.339844 -9.339844 9.648438 -10.261719 Z "/>
</g>
<g id="glyph-0-2">
<path d="M 1.675781 -10.246094 L 1.007812 -9.609375 C 1.953125 -8.945312 2.707031 -8.304688 3.246094 -7.703125 L 3.898438 -8.355469 C 3.296875 -8.984375 2.554688 -9.609375 1.675781 -10.246094 Z M 1.355469 -7.230469 L 0.703125 -6.605469 C 1.597656 -5.914062 2.300781 -5.277344 2.8125 -4.6875 L 3.460938 -5.339844 C 2.902344 -5.980469 2.199219 -6.605469 1.355469 -7.230469 Z M 2.964844 -3.707031 C 2.390625 -2.171875 1.710938 -0.675781 0.945312 0.78125 L 1.867188 1.175781 C 2.566406 -0.242188 3.207031 -1.761719 3.78125 -3.375 Z M 4.394531 -8.28125 L 4.394531 -7.371094 L 7.71875 -7.371094 L 7.71875 -4.304688 L 4.753906 -4.304688 L 4.753906 -3.425781 L 7.71875 -3.425781 L 7.71875 -0.0625 L 4.074219 -0.0625 L 4.074219 0.84375 L 12.191406 0.84375 L 12.191406 -0.0625 L 8.648438 -0.0625 L 8.648438 -3.425781 L 11.601562 -3.425781 L 11.601562 -4.304688 L 8.648438 -4.304688 L 8.648438 -7.371094 L 11.945312 -7.371094 L 11.945312 -8.28125 L 8.140625 -8.28125 L 8.664062 -8.484375 C 8.472656 -9.097656 8.191406 -9.761719 7.832031 -10.503906 L 6.964844 -10.261719 C 7.296875 -9.621094 7.578125 -8.96875 7.832031 -8.28125 Z "/>
</g>
<g id="glyph-0-3">
<rect x="0" y="0" width="0" height="0" mask="url(#mask-0)"/>
</g>
<g id="glyph-0-4">
<path d="M 0.640625 -5.148438 L 0.640625 -4.230469 L 12.125 -4.230469 L 12.125 -5.148438 Z "/>
</g>
<g id="glyph-0-5">
<path d="M 12.011719 -3.539062 L 11.140625 -3.832031 C 11.089844 -3.09375 11.027344 -2.59375 10.9375 -2.351562 C 10.824219 -2.082031 10.578125 -1.941406 10.222656 -1.941406 L 8.484375 -1.941406 C 8.125 -1.953125 7.949219 -2.132812 7.949219 -2.480469 L 7.949219 -5.417969 L 11.398438 -5.417969 L 11.398438 -9.621094 L 6.808594 -9.621094 L 6.808594 -8.765625 L 10.503906 -8.765625 L 10.503906 -6.300781 L 7.027344 -6.300781 L 7.027344 -2.148438 C 7.027344 -1.417969 7.386719 -1.046875 8.113281 -1.046875 L 10.386719 -1.046875 C 10.988281 -1.046875 11.382812 -1.214844 11.578125 -1.519531 C 11.78125 -1.828125 11.933594 -2.503906 12.011719 -3.539062 Z M 1.546875 -4.355469 C 1.480469 -2.503906 1.125 -0.832031 0.484375 0.652344 L 1.1875 1.277344 C 1.519531 0.535156 1.777344 -0.253906 1.980469 -1.113281 C 2.464844 -0.34375 3.039062 0.203125 3.691406 0.511719 C 4.433594 0.855469 6.3125 1.035156 9.300781 1.035156 L 12.113281 1.035156 L 12.265625 0.140625 C 11.578125 0.179688 10.515625 0.203125 9.097656 0.203125 C 6.757812 0.203125 5.1875 0.0898438 4.394531 -0.128906 L 4.394531 -2.53125 L 6.210938 -2.53125 L 6.210938 -3.375 L 4.394531 -3.375 L 4.394531 -5.175781 L 6.363281 -5.175781 L 6.363281 -6.019531 L 4.152344 -6.019531 L 4.152344 -7.894531 L 6.195312 -7.894531 L 6.195312 -8.738281 L 4.152344 -8.738281 L 4.152344 -10.464844 L 3.234375 -10.464844 L 3.234375 -8.738281 L 1.125 -8.738281 L 1.125 -7.894531 L 3.234375 -7.894531 L 3.234375 -6.019531 L 0.894531 -6.019531 L 0.894531 -5.175781 L 3.515625 -5.175781 L 3.515625 -0.5625 C 3.003906 -0.921875 2.566406 -1.457031 2.183594 -2.171875 C 2.289062 -2.835938 2.351562 -3.527344 2.375 -4.253906 Z "/>
</g>
<g id="glyph-0-6">
<path d="M 11.78125 -7.84375 L 9.316406 -7.84375 C 9.800781 -8.484375 10.261719 -9.25 10.707031 -10.171875 L 9.773438 -10.515625 C 9.367188 -9.558594 8.878906 -8.664062 8.316406 -7.84375 L 0.996094 -7.84375 L 0.996094 -4.984375 L 1.890625 -4.984375 L 1.890625 -6.964844 L 10.875 -6.964844 L 10.875 -4.984375 L 11.78125 -4.984375 Z M 2.953125 -5.878906 L 2.953125 -5.058594 L 8.523438 -5.058594 C 7.960938 -4.753906 7.15625 -4.371094 6.082031 -3.898438 L 6.082031 -2.835938 L 0.90625 -2.835938 L 0.90625 -1.96875 L 6.082031 -1.96875 L 6.082031 -0.128906 C 6.082031 0.230469 5.914062 0.410156 5.609375 0.410156 C 5.058594 0.410156 4.546875 0.394531 4.089844 0.382812 L 4.304688 1.226562 L 5.929688 1.226562 C 6.644531 1.226562 7.015625 0.882812 7.015625 0.191406 L 7.015625 -1.96875 L 11.882812 -1.96875 L 11.882812 -2.835938 L 7.015625 -2.835938 L 7.015625 -3.488281 C 8.0625 -4 8.996094 -4.523438 9.839844 -5.058594 L 9.839844 -5.878906 Z M 2.8125 -10.324219 L 1.980469 -9.914062 C 2.453125 -9.328125 2.902344 -8.664062 3.335938 -7.921875 L 4.128906 -8.316406 C 3.71875 -9.046875 3.269531 -9.710938 2.8125 -10.324219 Z M 5.902344 -10.566406 L 5.074219 -10.15625 C 5.507812 -9.542969 5.914062 -8.84375 6.300781 -8.050781 L 7.117188 -8.457031 C 6.734375 -9.226562 6.324219 -9.929688 5.902344 -10.566406 Z "/>
</g>
<g id="glyph-0-7">
<path d="M 3.589844 -9.121094 L 0 0 L 1.125 0 L 2.09375 -2.554688 L 6.285156 -2.554688 L 7.257812 0 L 8.394531 0 L 4.804688 -9.121094 Z M 2.425781 -3.4375 L 4.179688 -8 L 4.230469 -8 L 5.953125 -3.4375 Z "/>
</g>
<g id="glyph-0-8">
<path d="M 0.996094 -9.121094 L 0.996094 0 L 2.03125 0 L 2.03125 -9.121094 Z "/>
</g>
</g>
<image id="source-8" x="0" y="0" width="0" height="0"/>
<mask id="mask-0">
<use xlink:href="#source-8"/>
</mask>
</defs>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="30" y="33.543945"/>
<use xlink:href="#glyph-0-0" x="43" y="33.543945"/>
<use xlink:href="#glyph-0-1" x="56" y="33.543945"/>
<use xlink:href="#glyph-0-2" x="69" y="33.543945"/>
<use xlink:href="#glyph-0-3" x="82" y="33.543945"/>
<use xlink:href="#glyph-0-3" x="86" y="33.543945"/>
<use xlink:href="#glyph-0-4" x="90" y="33.543945"/>
<use xlink:href="#glyph-0-5" x="103" y="33.543945"/>
<use xlink:href="#glyph-0-6" x="116" y="33.543945"/>
<use xlink:href="#glyph-0-3" x="129" y="33.543945"/>
</g>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-7" x="133" y="33.543945"/>
<use xlink:href="#glyph-0-8" x="141" y="33.543945"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1536" height="672" viewBox="0 0 1536 672">
<defs>
<g>
<g id="glyph-0-0">
<path d="M 6.472656 -11.773438 L 6.472656 -7.554688 L 2.542969 -7.554688 L 2.542969 -2.917969 L 12.105469 -2.917969 L 12.105469 -7.554688 L 7.496094 -7.554688 L 7.496094 -9.390625 L 13.101562 -9.390625 L 13.101562 -10.371094 L 7.496094 -10.371094 L 7.496094 -11.773438 Z M 11.0625 -3.886719 L 3.582031 -3.886719 L 3.582031 -6.570312 L 11.0625 -6.570312 Z M 2.511719 -2.28125 C 1.992188 -1.097656 1.371094 -0.0703125 0.648438 0.78125 L 1.503906 1.402344 C 2.222656 0.476562 2.84375 -0.605469 3.394531 -1.863281 Z M 5.664062 -1.863281 L 4.695312 -1.660156 C 4.96875 -0.75 5.199219 0.289062 5.402344 1.472656 L 6.441406 1.242188 C 6.210938 0.128906 5.949219 -0.910156 5.664062 -1.863281 Z M 8.679688 -2.007812 L 7.699219 -1.804688 C 8.132812 -0.867188 8.492188 0.214844 8.78125 1.445312 L 9.808594 1.214844 C 9.488281 0.0585938 9.113281 -1.027344 8.679688 -2.007812 Z M 11.640625 -2.296875 L 10.804688 -1.761719 C 11.683594 -0.578125 12.378906 0.492188 12.882812 1.472656 L 13.75 0.867188 C 13.261719 -0.0273438 12.566406 -1.082031 11.640625 -2.296875 Z "/>
</g>
<g id="glyph-0-1">
<path d="M 4.476562 -11.742188 L 3.523438 -11.28125 C 4.117188 -10.558594 4.664062 -9.734375 5.183594 -8.8125 L 6.125 -9.289062 C 5.605469 -10.183594 5.054688 -10.992188 4.476562 -11.742188 Z M 9.910156 -11.875 C 9.476562 -10.660156 9 -9.632812 8.449219 -8.796875 L 1.847656 -8.796875 L 1.847656 -7.785156 L 6.6875 -7.785156 L 6.6875 -7.105469 C 6.671875 -6.398438 6.601562 -5.734375 6.472656 -5.097656 L 1.027344 -5.097656 L 1.027344 -4.085938 L 6.210938 -4.085938 C 6.007812 -3.453125 5.734375 -2.890625 5.386719 -2.367188 C 4.519531 -1.214844 2.976562 -0.273438 0.765625 0.433594 L 1.328125 1.34375 C 3.554688 0.636719 5.15625 -0.359375 6.140625 -1.632812 C 6.601562 -2.253906 6.960938 -2.976562 7.238281 -3.800781 C 8.015625 -1.601562 9.980469 0.144531 13.160156 1.429688 L 13.765625 0.460938 C 10.730469 -0.621094 8.867188 -2.136719 8.160156 -4.085938 L 13.417969 -4.085938 L 13.417969 -5.097656 L 7.554688 -5.097656 C 7.671875 -5.707031 7.726562 -6.382812 7.757812 -7.105469 L 7.757812 -7.785156 L 12.582031 -7.785156 L 12.582031 -8.796875 L 9.605469 -8.796875 C 10.125 -9.621094 10.558594 -10.558594 10.90625 -11.597656 Z "/>
</g>
<g id="glyph-0-2">
<path d="M 1.890625 -11.585938 L 1.140625 -10.863281 C 2.210938 -10.109375 3.0625 -9.390625 3.667969 -8.710938 L 4.40625 -9.445312 C 3.726562 -10.15625 2.890625 -10.863281 1.890625 -11.585938 Z M 1.53125 -8.175781 L 0.792969 -7.46875 C 1.804688 -6.6875 2.601562 -5.964844 3.179688 -5.300781 L 3.914062 -6.039062 C 3.277344 -6.761719 2.484375 -7.46875 1.53125 -8.175781 Z M 3.351562 -4.1875 C 2.699219 -2.457031 1.933594 -0.765625 1.070312 0.882812 L 2.109375 1.328125 C 2.902344 -0.273438 3.625 -1.992188 4.277344 -3.8125 Z M 4.96875 -9.359375 L 4.96875 -8.335938 L 8.722656 -8.335938 L 8.722656 -4.867188 L 5.375 -4.867188 L 5.375 -3.871094 L 8.722656 -3.871094 L 8.722656 -0.0703125 L 4.609375 -0.0703125 L 4.609375 0.953125 L 13.78125 0.953125 L 13.78125 -0.0703125 L 9.777344 -0.0703125 L 9.777344 -3.871094 L 13.117188 -3.871094 L 13.117188 -4.867188 L 9.777344 -4.867188 L 9.777344 -8.335938 L 13.503906 -8.335938 L 13.503906 -9.359375 L 9.199219 -9.359375 L 9.792969 -9.589844 C 9.578125 -10.285156 9.257812 -11.035156 8.855469 -11.875 L 7.871094 -11.597656 C 8.246094 -10.875 8.566406 -10.140625 8.855469 -9.359375 Z "/>
</g>
<g id="glyph-0-3">
<rect x="0" y="0" width="0" height="0" mask="url(#mask-0)"/>
</g>
<g id="glyph-0-4">
<path d="M 0.722656 -5.820312 L 0.722656 -4.78125 L 13.707031 -4.78125 L 13.707031 -5.820312 Z "/>
</g>
<g id="glyph-0-5">
<path d="M 13.578125 -4 L 12.59375 -4.332031 C 12.539062 -3.496094 12.464844 -2.933594 12.363281 -2.65625 C 12.234375 -2.355469 11.960938 -2.195312 11.554688 -2.195312 L 9.589844 -2.195312 C 9.1875 -2.210938 8.984375 -2.414062 8.984375 -2.800781 L 8.984375 -6.125 L 12.882812 -6.125 L 12.882812 -10.875 L 7.699219 -10.875 L 7.699219 -9.910156 L 11.875 -9.910156 L 11.875 -7.121094 L 7.945312 -7.121094 L 7.945312 -2.425781 C 7.945312 -1.601562 8.347656 -1.183594 9.171875 -1.183594 L 11.742188 -1.183594 C 12.421875 -1.183594 12.871094 -1.371094 13.085938 -1.71875 C 13.316406 -2.066406 13.492188 -2.832031 13.578125 -4 Z M 1.746094 -4.925781 C 1.675781 -2.832031 1.269531 -0.9375 0.550781 0.738281 L 1.34375 1.445312 C 1.71875 0.605469 2.007812 -0.289062 2.238281 -1.257812 C 2.789062 -0.390625 3.4375 0.230469 4.175781 0.578125 C 5.011719 0.96875 7.136719 1.171875 10.515625 1.171875 L 13.691406 1.171875 L 13.867188 0.160156 C 13.085938 0.203125 11.886719 0.230469 10.285156 0.230469 C 7.640625 0.230469 5.863281 0.101562 4.96875 -0.144531 L 4.96875 -2.859375 L 7.019531 -2.859375 L 7.019531 -3.8125 L 4.96875 -3.8125 L 4.96875 -5.851562 L 7.191406 -5.851562 L 7.191406 -6.804688 L 4.695312 -6.804688 L 4.695312 -8.925781 L 7.003906 -8.925781 L 7.003906 -9.878906 L 4.695312 -9.878906 L 4.695312 -11.828125 L 3.65625 -11.828125 L 3.65625 -9.878906 L 1.269531 -9.878906 L 1.269531 -8.925781 L 3.65625 -8.925781 L 3.65625 -6.804688 L 1.011719 -6.804688 L 1.011719 -5.851562 L 3.972656 -5.851562 L 3.972656 -0.636719 C 3.394531 -1.039062 2.902344 -1.648438 2.46875 -2.457031 C 2.585938 -3.207031 2.65625 -3.988281 2.6875 -4.808594 Z "/>
</g>
<g id="glyph-0-6">
<path d="M 13.316406 -8.867188 L 10.53125 -8.867188 C 11.078125 -9.589844 11.597656 -10.457031 12.105469 -11.496094 L 11.050781 -11.886719 C 10.585938 -10.804688 10.039062 -9.792969 9.402344 -8.867188 L 1.125 -8.867188 L 1.125 -5.632812 L 2.136719 -5.632812 L 2.136719 -7.871094 L 12.292969 -7.871094 L 12.292969 -5.632812 L 13.316406 -5.632812 Z M 3.335938 -6.644531 L 3.335938 -5.71875 L 9.632812 -5.71875 C 9 -5.375 8.089844 -4.941406 6.875 -4.40625 L 6.875 -3.207031 L 1.027344 -3.207031 L 1.027344 -2.222656 L 6.875 -2.222656 L 6.875 -0.144531 C 6.875 0.261719 6.6875 0.460938 6.339844 0.460938 C 5.71875 0.460938 5.140625 0.449219 4.621094 0.433594 L 4.867188 1.386719 L 6.703125 1.386719 C 7.511719 1.386719 7.929688 0.996094 7.929688 0.214844 L 7.929688 -2.222656 L 13.433594 -2.222656 L 13.433594 -3.207031 L 7.929688 -3.207031 L 7.929688 -3.941406 C 9.113281 -4.519531 10.167969 -5.113281 11.121094 -5.71875 L 11.121094 -6.644531 Z M 3.179688 -11.671875 L 2.238281 -11.207031 C 2.773438 -10.542969 3.277344 -9.792969 3.769531 -8.957031 L 4.664062 -9.402344 C 4.203125 -10.226562 3.699219 -10.976562 3.179688 -11.671875 Z M 6.671875 -11.945312 L 5.734375 -11.484375 C 6.226562 -10.789062 6.6875 -9.996094 7.121094 -9.101562 L 8.046875 -9.5625 C 7.613281 -10.429688 7.148438 -11.222656 6.671875 -11.945312 Z "/>
</g>
<g id="glyph-0-7">
<path d="M 4.058594 -10.3125 L 0 0 L 1.269531 0 L 2.367188 -2.890625 L 7.105469 -2.890625 L 8.203125 0 L 9.488281 0 L 5.429688 -10.3125 Z M 2.746094 -3.886719 L 4.722656 -9.042969 L 4.78125 -9.042969 L 6.730469 -3.886719 Z "/>
</g>
<g id="glyph-0-8">
<path d="M 1.125 -10.3125 L 1.125 0 L 2.296875 0 L 2.296875 -10.3125 Z "/>
</g>
</g>
<image id="source-8" x="0" y="0" width="0" height="0"/>
<mask id="mask-0">
<use xlink:href="#source-8"/>
</mask>
</defs>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="30" y="35.310547"/>
<use xlink:href="#glyph-0-0" x="44" y="35.310547"/>
<use xlink:href="#glyph-0-1" x="58" y="35.310547"/>
<use xlink:href="#glyph-0-2" x="72" y="35.310547"/>
<use xlink:href="#glyph-0-3" x="86" y="35.310547"/>
<use xlink:href="#glyph-0-3" x="91" y="35.310547"/>
<use xlink:href="#glyph-0-4" x="96" y="35.310547"/>
<use xlink:href="#glyph-0-5" x="110" y="35.310547"/>
<use xlink:href="#glyph-0-6" x="124" y="35.310547"/>
<use xlink:href="#glyph-0-3" x="138" y="35.310547"/>
</g>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-7" x="143" y="35.310547"/>
<use xlink:href="#glyph-0-8" x="152" y="35.310547"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="832" height="1248" viewBox="0 0 832 1248">
<defs>
<g>
<g id="glyph-0-0">
<path d="M 4.730469 -8.601562 L 4.730469 -5.519531 L 1.859375 -5.519531 L 1.859375 -2.132812 L 8.84375 -2.132812 L 8.84375 -5.519531 L 5.476562 -5.519531 L 5.476562 -6.859375 L 9.574219 -6.859375 L 9.574219 -7.578125 L 5.476562 -7.578125 L 5.476562 -8.601562 Z M 8.085938 -2.839844 L 2.617188 -2.839844 L 2.617188 -4.804688 L 8.085938 -4.804688 Z M 1.835938 -1.667969 C 1.457031 -0.800781 1.003906 -0.0546875 0.476562 0.570312 L 1.097656 1.023438 C 1.625 0.347656 2.078125 -0.441406 2.480469 -1.363281 Z M 4.136719 -1.363281 L 3.429688 -1.214844 C 3.632812 -0.550781 3.800781 0.210938 3.949219 1.078125 L 4.707031 0.90625 C 4.539062 0.09375 4.347656 -0.664062 4.136719 -1.363281 Z M 6.34375 -1.46875 L 5.625 -1.320312 C 5.941406 -0.632812 6.207031 0.160156 6.417969 1.054688 L 7.167969 0.886719 C 6.933594 0.0429688 6.660156 -0.75 6.34375 -1.46875 Z M 8.507812 -1.679688 L 7.894531 -1.289062 C 8.539062 -0.421875 9.046875 0.359375 9.414062 1.078125 L 10.050781 0.632812 C 9.691406 -0.0195312 9.183594 -0.792969 8.507812 -1.679688 Z "/>
</g>
<g id="glyph-0-1">
<path d="M 3.273438 -8.582031 L 2.574219 -8.242188 C 3.007812 -7.714844 3.410156 -7.113281 3.789062 -6.4375 L 4.476562 -6.789062 C 4.09375 -7.441406 3.695312 -8.03125 3.273438 -8.582031 Z M 7.242188 -8.675781 C 6.925781 -7.789062 6.578125 -7.039062 6.175781 -6.429688 L 1.351562 -6.429688 L 1.351562 -5.691406 L 4.886719 -5.691406 L 4.886719 -5.195312 C 4.875 -4.675781 4.824219 -4.191406 4.730469 -3.726562 L 0.75 -3.726562 L 0.75 -2.988281 L 4.539062 -2.988281 C 4.390625 -2.523438 4.191406 -2.109375 3.9375 -1.730469 C 3.304688 -0.886719 2.175781 -0.199219 0.558594 0.316406 L 0.972656 0.980469 C 2.597656 0.464844 3.769531 -0.265625 4.484375 -1.191406 C 4.824219 -1.648438 5.085938 -2.175781 5.289062 -2.777344 C 5.859375 -1.171875 7.292969 0.105469 9.617188 1.046875 L 10.058594 0.335938 C 7.84375 -0.453125 6.480469 -1.5625 5.964844 -2.988281 L 9.804688 -2.988281 L 9.804688 -3.726562 L 5.519531 -3.726562 C 5.605469 -4.167969 5.648438 -4.664062 5.667969 -5.195312 L 5.667969 -5.691406 L 9.195312 -5.691406 L 9.195312 -6.429688 L 7.019531 -6.429688 C 7.398438 -7.03125 7.714844 -7.714844 7.96875 -8.476562 Z "/>
</g>
<g id="glyph-0-2">
<path d="M 1.382812 -8.464844 L 0.832031 -7.9375 C 1.613281 -7.390625 2.238281 -6.859375 2.679688 -6.363281 L 3.21875 -6.902344 C 2.722656 -7.421875 2.109375 -7.9375 1.382812 -8.464844 Z M 1.117188 -5.972656 L 0.582031 -5.457031 C 1.320312 -4.886719 1.898438 -4.359375 2.320312 -3.875 L 2.859375 -4.414062 C 2.394531 -4.941406 1.816406 -5.457031 1.117188 -5.972656 Z M 2.449219 -3.0625 C 1.972656 -1.792969 1.414062 -0.558594 0.78125 0.644531 L 1.542969 0.972656 C 2.121094 -0.199219 2.648438 -1.457031 3.125 -2.785156 Z M 3.632812 -6.839844 L 3.632812 -6.089844 L 6.375 -6.089844 L 6.375 -3.558594 L 3.925781 -3.558594 L 3.925781 -2.828125 L 6.375 -2.828125 L 6.375 -0.0546875 L 3.367188 -0.0546875 L 3.367188 0.695312 L 10.070312 0.695312 L 10.070312 -0.0546875 L 7.144531 -0.0546875 L 7.144531 -2.828125 L 9.585938 -2.828125 L 9.585938 -3.558594 L 7.144531 -3.558594 L 7.144531 -6.089844 L 9.871094 -6.089844 L 9.871094 -6.839844 L 6.722656 -6.839844 L 7.15625 -7.007812 C 7 -7.515625 6.765625 -8.066406 6.46875 -8.675781 L 5.753906 -8.476562 C 6.027344 -7.949219 6.257812 -7.410156 6.46875 -6.839844 Z "/>
</g>
<g id="glyph-0-3">
<rect x="0" y="0" width="0" height="0" mask="url(#mask-0)"/>
</g>
<g id="glyph-0-4">
<path d="M 0.527344 -4.253906 L 0.527344 -3.492188 L 10.015625 -3.492188 L 10.015625 -4.253906 Z "/>
</g>
<g id="glyph-0-5">
<path d="M 9.921875 -2.925781 L 9.203125 -3.167969 C 9.164062 -2.554688 9.109375 -2.144531 9.035156 -1.941406 C 8.941406 -1.71875 8.738281 -1.605469 8.445312 -1.605469 L 7.007812 -1.605469 C 6.714844 -1.613281 6.566406 -1.761719 6.566406 -2.046875 L 6.566406 -4.476562 L 9.414062 -4.476562 L 9.414062 -7.949219 L 5.625 -7.949219 L 5.625 -7.242188 L 8.675781 -7.242188 L 8.675781 -5.203125 L 5.804688 -5.203125 L 5.804688 -1.773438 C 5.804688 -1.171875 6.101562 -0.867188 6.703125 -0.867188 L 8.582031 -0.867188 C 9.078125 -0.867188 9.40625 -1.003906 9.5625 -1.257812 C 9.730469 -1.507812 9.859375 -2.070312 9.921875 -2.925781 Z M 1.277344 -3.597656 C 1.222656 -2.070312 0.929688 -0.6875 0.402344 0.539062 L 0.980469 1.054688 C 1.257812 0.441406 1.46875 -0.210938 1.636719 -0.917969 C 2.039062 -0.285156 2.511719 0.167969 3.050781 0.421875 C 3.664062 0.707031 5.214844 0.855469 7.683594 0.855469 L 10.007812 0.855469 L 10.132812 0.117188 C 9.5625 0.148438 8.6875 0.167969 7.515625 0.167969 C 5.582031 0.167969 4.285156 0.0742188 3.632812 -0.105469 L 3.632812 -2.089844 L 5.128906 -2.089844 L 5.128906 -2.785156 L 3.632812 -2.785156 L 3.632812 -4.273438 L 5.257812 -4.273438 L 5.257812 -4.972656 L 3.429688 -4.972656 L 3.429688 -6.523438 L 5.121094 -6.523438 L 5.121094 -7.21875 L 3.429688 -7.21875 L 3.429688 -8.644531 L 2.671875 -8.644531 L 2.671875 -7.21875 L 0.929688 -7.21875 L 0.929688 -6.523438 L 2.671875 -6.523438 L 2.671875 -4.972656 L 0.738281 -4.972656 L 0.738281 -4.273438 L 2.902344 -4.273438 L 2.902344 -0.464844 C 2.480469 -0.761719 2.121094 -1.203125 1.804688 -1.792969 C 1.890625 -2.34375 1.941406 -2.914062 1.964844 -3.515625 Z "/>
</g>
<g id="glyph-0-6">
<path d="M 9.730469 -6.480469 L 7.695312 -6.480469 C 8.097656 -7.007812 8.476562 -7.640625 8.84375 -8.402344 L 8.074219 -8.6875 C 7.738281 -7.894531 7.335938 -7.15625 6.871094 -6.480469 L 0.824219 -6.480469 L 0.824219 -4.117188 L 1.5625 -4.117188 L 1.5625 -5.753906 L 8.984375 -5.753906 L 8.984375 -4.117188 L 9.730469 -4.117188 Z M 2.4375 -4.855469 L 2.4375 -4.179688 L 7.039062 -4.179688 C 6.578125 -3.925781 5.910156 -3.609375 5.023438 -3.21875 L 5.023438 -2.34375 L 0.75 -2.34375 L 0.75 -1.625 L 5.023438 -1.625 L 5.023438 -0.105469 C 5.023438 0.191406 4.886719 0.335938 4.632812 0.335938 C 4.179688 0.335938 3.757812 0.328125 3.378906 0.316406 L 3.558594 1.011719 L 4.898438 1.011719 C 5.488281 1.011719 5.796875 0.726562 5.796875 0.160156 L 5.796875 -1.625 L 9.816406 -1.625 L 9.816406 -2.34375 L 5.796875 -2.34375 L 5.796875 -2.882812 C 6.660156 -3.304688 7.429688 -3.738281 8.128906 -4.179688 L 8.128906 -4.855469 Z M 2.320312 -8.527344 L 1.636719 -8.191406 C 2.027344 -7.707031 2.394531 -7.15625 2.753906 -6.542969 L 3.410156 -6.871094 C 3.070312 -7.472656 2.703125 -8.023438 2.320312 -8.527344 Z M 4.875 -8.730469 L 4.191406 -8.390625 C 4.550781 -7.886719 4.886719 -7.304688 5.203125 -6.648438 L 5.878906 -6.988281 C 5.5625 -7.621094 5.226562 -8.203125 4.875 -8.730469 Z "/>
</g>
<g id="glyph-0-7">
<path d="M 2.964844 -7.535156 L 0 0 L 0.929688 0 L 1.730469 -2.109375 L 5.195312 -2.109375 L 5.996094 0 L 6.933594 0 L 3.96875 -7.535156 Z M 2.003906 -2.839844 L 3.453125 -6.609375 L 3.492188 -6.609375 L 4.917969 -2.839844 Z "/>
</g>
<g id="glyph-0-8">
<path d="M 0.824219 -7.535156 L 0.824219 0 L 1.679688 0 L 1.679688 -7.535156 Z "/>
</g>
</g>
<image id="source-8" x="0" y="0" width="0" height="0"/>
<mask id="mask-0">
<use xlink:href="#source-8"/>
</mask>
</defs>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="30" y="31.189453"/>
<use xlink:href="#glyph-0-0" x="41" y="31.189453"/>
<use xlink:href="#glyph-0-1" x="52" y="31.189453"/>
<use xlink:href="#glyph-0-2" x="63" y="31.189453"/>
<use xlink:href="#glyph-0-3" x="74" y="31.189453"/>
<use xlink:href="#glyph-0-3" x="78" y="31.189453"/>
<use xlink:href="#glyph-0-4" x="82" y="31.189453"/>
<use xlink:href="#glyph-0-5" x="93" y="31.189453"/>
<use xlink:href="#glyph-0-6" x="104" y="31.189453"/>
<use xlink:href="#glyph-0-3" x="115" y="31.189453"/>
</g>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-7" x="119" y="31.189453"/>
<use xlink:href="#glyph-0-8" x="126" y="31.189453"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1080" height="1920" viewBox="0 0 1080 1920">
<defs>
<g>
<g id="glyph-0-0">
<path d="M 4.976562 -9.054688 L 4.976562 -5.8125 L 1.957031 -5.8125 L 1.957031 -2.246094 L 9.308594 -2.246094 L 9.308594 -5.8125 L 5.765625 -5.8125 L 5.765625 -7.222656 L 10.078125 -7.222656 L 10.078125 -7.976562 L 5.765625 -7.976562 L 5.765625 -9.054688 Z M 8.511719 -2.988281 L 2.753906 -2.988281 L 2.753906 -5.054688 L 8.511719 -5.054688 Z M 1.933594 -1.753906 C 1.535156 -0.84375 1.054688 -0.0546875 0.5 0.601562 L 1.15625 1.078125 C 1.710938 0.367188 2.1875 -0.464844 2.609375 -1.433594 Z M 4.355469 -1.433594 L 3.609375 -1.277344 C 3.820312 -0.578125 4 0.222656 4.15625 1.132812 L 4.957031 0.957031 C 4.777344 0.101562 4.578125 -0.699219 4.355469 -1.433594 Z M 6.675781 -1.542969 L 5.921875 -1.390625 C 6.253906 -0.667969 6.53125 0.167969 6.753906 1.109375 L 7.542969 0.933594 C 7.300781 0.0429688 7.011719 -0.789062 6.675781 -1.542969 Z M 8.953125 -1.765625 L 8.308594 -1.355469 C 8.988281 -0.445312 9.523438 0.378906 9.910156 1.132812 L 10.578125 0.667969 C 10.199219 -0.0234375 9.664062 -0.832031 8.953125 -1.765625 Z "/>
</g>
<g id="glyph-0-1">
<path d="M 3.445312 -9.03125 L 2.710938 -8.675781 C 3.167969 -8.121094 3.589844 -7.488281 3.988281 -6.777344 L 4.710938 -7.144531 C 4.3125 -7.832031 3.886719 -8.453125 3.445312 -9.03125 Z M 7.621094 -9.132812 C 7.289062 -8.199219 6.921875 -7.410156 6.5 -6.765625 L 1.421875 -6.765625 L 1.421875 -5.988281 L 5.144531 -5.988281 L 5.144531 -5.464844 C 5.132812 -4.921875 5.078125 -4.410156 4.976562 -3.921875 L 0.789062 -3.921875 L 0.789062 -3.144531 L 4.777344 -3.144531 C 4.621094 -2.65625 4.410156 -2.222656 4.144531 -1.820312 C 3.476562 -0.933594 2.289062 -0.210938 0.589844 0.332031 L 1.023438 1.035156 C 2.734375 0.488281 3.964844 -0.277344 4.722656 -1.253906 C 5.078125 -1.734375 5.355469 -2.289062 5.566406 -2.921875 C 6.167969 -1.234375 7.675781 0.109375 10.121094 1.101562 L 10.589844 0.355469 C 8.253906 -0.476562 6.820312 -1.644531 6.277344 -3.144531 L 10.320312 -3.144531 L 10.320312 -3.921875 L 5.8125 -3.921875 C 5.898438 -4.386719 5.945312 -4.910156 5.964844 -5.464844 L 5.964844 -5.988281 L 9.675781 -5.988281 L 9.675781 -6.765625 L 7.386719 -6.765625 C 7.789062 -7.398438 8.121094 -8.121094 8.386719 -8.921875 Z "/>
</g>
<g id="glyph-0-2">
<path d="M 1.457031 -8.910156 L 0.878906 -8.355469 C 1.699219 -7.777344 2.355469 -7.222656 2.820312 -6.699219 L 3.386719 -7.265625 C 2.867188 -7.8125 2.222656 -8.355469 1.457031 -8.910156 Z M 1.175781 -6.289062 L 0.609375 -5.742188 C 1.390625 -5.144531 2 -4.589844 2.445312 -4.078125 L 3.011719 -4.644531 C 2.523438 -5.199219 1.910156 -5.742188 1.175781 -6.289062 Z M 2.578125 -3.222656 C 2.078125 -1.890625 1.488281 -0.589844 0.820312 0.675781 L 1.621094 1.023438 C 2.234375 -0.210938 2.789062 -1.535156 3.289062 -2.933594 Z M 3.820312 -7.199219 L 3.820312 -6.410156 L 6.710938 -6.410156 L 6.710938 -3.746094 L 4.132812 -3.746094 L 4.132812 -2.976562 L 6.710938 -2.976562 L 6.710938 -0.0546875 L 3.542969 -0.0546875 L 3.542969 0.734375 L 10.597656 0.734375 L 10.597656 -0.0546875 L 7.523438 -0.0546875 L 7.523438 -2.976562 L 10.089844 -2.976562 L 10.089844 -3.746094 L 7.523438 -3.746094 L 7.523438 -6.410156 L 10.386719 -6.410156 L 10.386719 -7.199219 L 7.078125 -7.199219 L 7.53125 -7.378906 C 7.367188 -7.910156 7.121094 -8.488281 6.8125 -9.132812 L 6.054688 -8.921875 C 6.34375 -8.367188 6.589844 -7.800781 6.8125 -7.199219 Z "/>
</g>
<g id="glyph-0-3">
<rect x="0" y="0" width="0" height="0" mask="url(#mask-0)"/>
</g>
<g id="glyph-0-4">
<path d="M 0.554688 -4.476562 L 0.554688 -3.675781 L 10.542969 -3.675781 L 10.542969 -4.476562 Z "/>
</g>
<g id="glyph-0-5">
<path d="M 10.445312 -3.078125 L 9.6875 -3.332031 C 9.644531 -2.6875 9.589844 -2.253906 9.511719 -2.042969 C 9.410156 -1.8125 9.199219 -1.6875 8.886719 -1.6875 L 7.378906 -1.6875 C 7.066406 -1.699219 6.910156 -1.855469 6.910156 -2.15625 L 6.910156 -4.710938 L 9.910156 -4.710938 L 9.910156 -8.367188 L 5.921875 -8.367188 L 5.921875 -7.621094 L 9.132812 -7.621094 L 9.132812 -5.476562 L 6.109375 -5.476562 L 6.109375 -1.867188 C 6.109375 -1.234375 6.421875 -0.910156 7.054688 -0.910156 L 9.03125 -0.910156 C 9.554688 -0.910156 9.898438 -1.054688 10.066406 -1.320312 C 10.242188 -1.589844 10.378906 -2.175781 10.445312 -3.078125 Z M 1.34375 -3.789062 C 1.289062 -2.175781 0.976562 -0.722656 0.421875 0.566406 L 1.035156 1.109375 C 1.320312 0.464844 1.542969 -0.222656 1.722656 -0.964844 C 2.144531 -0.300781 2.644531 0.179688 3.210938 0.445312 C 3.855469 0.746094 5.488281 0.898438 8.089844 0.898438 L 10.53125 0.898438 L 10.664062 0.121094 C 10.066406 0.15625 9.144531 0.179688 7.910156 0.179688 C 5.878906 0.179688 4.511719 0.078125 3.820312 -0.109375 L 3.820312 -2.199219 L 5.398438 -2.199219 L 5.398438 -2.933594 L 3.820312 -2.933594 L 3.820312 -4.5 L 5.53125 -4.5 L 5.53125 -5.234375 L 3.609375 -5.234375 L 3.609375 -6.867188 L 5.386719 -6.867188 L 5.386719 -7.597656 L 3.609375 -7.597656 L 3.609375 -9.097656 L 2.8125 -9.097656 L 2.8125 -7.597656 L 0.976562 -7.597656 L 0.976562 -6.867188 L 2.8125 -6.867188 L 2.8125 -5.234375 L 0.777344 -5.234375 L 0.777344 -4.5 L 3.054688 -4.5 L 3.054688 -0.488281 C 2.609375 -0.800781 2.234375 -1.265625 1.898438 -1.890625 C 1.988281 -2.464844 2.042969 -3.066406 2.066406 -3.699219 Z "/>
</g>
<g id="glyph-0-6">
<path d="M 10.242188 -6.820312 L 8.097656 -6.820312 C 8.523438 -7.378906 8.921875 -8.042969 9.308594 -8.84375 L 8.5 -9.144531 C 8.144531 -8.308594 7.722656 -7.53125 7.234375 -6.820312 L 0.867188 -6.820312 L 0.867188 -4.332031 L 1.644531 -4.332031 L 1.644531 -6.054688 L 9.453125 -6.054688 L 9.453125 -4.332031 L 10.242188 -4.332031 Z M 2.566406 -5.109375 L 2.566406 -4.398438 L 7.410156 -4.398438 C 6.921875 -4.132812 6.222656 -3.800781 5.289062 -3.386719 L 5.289062 -2.464844 L 0.789062 -2.464844 L 0.789062 -1.710938 L 5.289062 -1.710938 L 5.289062 -0.109375 C 5.289062 0.199219 5.144531 0.355469 4.878906 0.355469 C 4.398438 0.355469 3.957031 0.34375 3.554688 0.332031 L 3.746094 1.066406 L 5.15625 1.066406 C 5.777344 1.066406 6.097656 0.765625 6.097656 0.167969 L 6.097656 -1.710938 L 10.332031 -1.710938 L 10.332031 -2.464844 L 6.097656 -2.464844 L 6.097656 -3.03125 C 7.011719 -3.476562 7.820312 -3.933594 8.554688 -4.398438 L 8.554688 -5.109375 Z M 2.445312 -8.976562 L 1.722656 -8.621094 C 2.132812 -8.109375 2.523438 -7.53125 2.898438 -6.886719 L 3.589844 -7.234375 C 3.234375 -7.867188 2.84375 -8.445312 2.445312 -8.976562 Z M 5.132812 -9.1875 L 4.410156 -8.832031 C 4.789062 -8.300781 5.144531 -7.6875 5.476562 -7 L 6.1875 -7.355469 C 5.855469 -8.023438 5.5 -8.632812 5.132812 -9.1875 Z "/>
</g>
<g id="glyph-0-7">
<path d="M 3.121094 -7.933594 L 0 0 L 0.976562 0 L 1.820312 -2.222656 L 5.464844 -2.222656 L 6.3125 0 L 7.300781 0 L 4.175781 -7.933594 Z M 2.109375 -2.988281 L 3.632812 -6.957031 L 3.675781 -6.957031 L 5.175781 -2.988281 Z "/>
</g>
<g id="glyph-0-8">
<path d="M 0.867188 -7.933594 L 0.867188 0 L 1.765625 0 L 1.765625 -7.933594 Z "/>
</g>
</g>
<image id="source-8" x="0" y="0" width="0" height="0"/>
<mask id="mask-0">
<use xlink:href="#source-8"/>
</mask>
</defs>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="30" y="31.777344"/>
<use xlink:href="#glyph-0-0" x="41" y="31.777344"/>
<use xlink:href="#glyph-0-1" x="52" y="31.777344"/>
<use xlink:href="#glyph-0-2" x="63" y="31.777344"/>
<use xlink:href="#glyph-0-3" x="74" y="31.777344"/>
<use xlink:href="#glyph-0-3" x="78" y="31.777344"/>
<use xlink:href="#glyph-0-4" x="82" y="31.777344"/>
<use xlink:href="#glyph-0-5" x="93" y="31.777344"/>
<use xlink:href="#glyph-0-6" x="104" y="31.777344"/>
<use xlink:href="#glyph-0-3" x="115" y="31.777344"/>
</g>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-7" x="119" y="31.777344"/>
<use xlink:href="#glyph-0-8" x="126" y="31.777344"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="864" height="1080" viewBox="0 0 864 1080">
<defs>
<g>
<g id="glyph-0-0">
<path d="M 4.480469 -8.148438 L 4.480469 -5.230469 L 1.761719 -5.230469 L 1.761719 -2.019531 L 8.378906 -2.019531 L 8.378906 -5.230469 L 5.191406 -5.230469 L 5.191406 -6.5 L 9.070312 -6.5 L 9.070312 -7.179688 L 5.191406 -7.179688 L 5.191406 -8.148438 Z M 7.660156 -2.691406 L 2.480469 -2.691406 L 2.480469 -4.550781 L 7.660156 -4.550781 Z M 1.738281 -1.578125 C 1.378906 -0.761719 0.949219 -0.0507812 0.449219 0.539062 L 1.039062 0.96875 C 1.539062 0.328125 1.96875 -0.421875 2.351562 -1.289062 Z M 3.921875 -1.289062 L 3.25 -1.148438 C 3.441406 -0.519531 3.601562 0.199219 3.738281 1.019531 L 4.460938 0.859375 C 4.300781 0.0898438 4.121094 -0.628906 3.921875 -1.289062 Z M 6.011719 -1.390625 L 5.328125 -1.25 C 5.628906 -0.601562 5.878906 0.148438 6.078125 1 L 6.789062 0.839844 C 6.570312 0.0390625 6.308594 -0.710938 6.011719 -1.390625 Z M 8.058594 -1.589844 L 7.480469 -1.21875 C 8.089844 -0.398438 8.570312 0.339844 8.921875 1.019531 L 9.519531 0.601562 C 9.179688 -0.0195312 8.699219 -0.75 8.058594 -1.589844 Z "/>
</g>
<g id="glyph-0-1">
<path d="M 3.101562 -8.128906 L 2.441406 -7.808594 C 2.851562 -7.308594 3.230469 -6.738281 3.589844 -6.101562 L 4.238281 -6.429688 C 3.878906 -7.050781 3.5 -7.609375 3.101562 -8.128906 Z M 6.859375 -8.21875 C 6.558594 -7.378906 6.230469 -6.671875 5.851562 -6.089844 L 1.28125 -6.089844 L 1.28125 -5.390625 L 4.628906 -5.390625 L 4.628906 -4.921875 C 4.621094 -4.429688 4.570312 -3.96875 4.480469 -3.53125 L 0.710938 -3.53125 L 0.710938 -2.828125 L 4.300781 -2.828125 C 4.160156 -2.390625 3.96875 -2 3.730469 -1.640625 C 3.128906 -0.839844 2.058594 -0.191406 0.53125 0.300781 L 0.921875 0.929688 C 2.460938 0.441406 3.570312 -0.25 4.25 -1.128906 C 4.570312 -1.558594 4.820312 -2.058594 5.011719 -2.628906 C 5.550781 -1.109375 6.910156 0.101562 9.109375 0.988281 L 9.53125 0.320312 C 7.429688 -0.429688 6.140625 -1.480469 5.648438 -2.828125 L 9.289062 -2.828125 L 9.289062 -3.53125 L 5.230469 -3.53125 C 5.308594 -3.949219 5.351562 -4.421875 5.371094 -4.921875 L 5.371094 -5.390625 L 8.710938 -5.390625 L 8.710938 -6.089844 L 6.648438 -6.089844 C 7.011719 -6.660156 7.308594 -7.308594 7.550781 -8.03125 Z "/>
</g>
<g id="glyph-0-2">
<path d="M 1.308594 -8.019531 L 0.789062 -7.519531 C 1.53125 -7 2.121094 -6.5 2.539062 -6.03125 L 3.050781 -6.539062 C 2.578125 -7.03125 2 -7.519531 1.308594 -8.019531 Z M 1.058594 -5.660156 L 0.550781 -5.171875 C 1.25 -4.628906 1.800781 -4.128906 2.199219 -3.671875 L 2.710938 -4.179688 C 2.269531 -4.679688 1.71875 -5.171875 1.058594 -5.660156 Z M 2.320312 -2.898438 C 1.871094 -1.699219 1.339844 -0.53125 0.738281 0.609375 L 1.460938 0.921875 C 2.011719 -0.191406 2.511719 -1.378906 2.960938 -2.640625 Z M 3.441406 -6.480469 L 3.441406 -5.769531 L 6.039062 -5.769531 L 6.039062 -3.371094 L 3.71875 -3.371094 L 3.71875 -2.679688 L 6.039062 -2.679688 L 6.039062 -0.0507812 L 3.191406 -0.0507812 L 3.191406 0.660156 L 9.539062 0.660156 L 9.539062 -0.0507812 L 6.769531 -0.0507812 L 6.769531 -2.679688 L 9.078125 -2.679688 L 9.078125 -3.371094 L 6.769531 -3.371094 L 6.769531 -5.769531 L 9.351562 -5.769531 L 9.351562 -6.480469 L 6.371094 -6.480469 L 6.78125 -6.640625 C 6.628906 -7.121094 6.410156 -7.640625 6.128906 -8.21875 L 5.449219 -8.03125 C 5.710938 -7.53125 5.929688 -7.019531 6.128906 -6.480469 Z "/>
</g>
<g id="glyph-0-3">
<rect x="0" y="0" width="0" height="0" mask="url(#mask-0)"/>
</g>
<g id="glyph-0-4">
<path d="M 0.5 -4.03125 L 0.5 -3.308594 L 9.488281 -3.308594 L 9.488281 -4.03125 Z "/>
</g>
<g id="glyph-0-5">
<path d="M 9.398438 -2.769531 L 8.71875 -3 C 8.679688 -2.421875 8.628906 -2.03125 8.558594 -1.839844 C 8.46875 -1.628906 8.28125 -1.519531 8 -1.519531 L 6.640625 -1.519531 C 6.359375 -1.53125 6.21875 -1.671875 6.21875 -1.941406 L 6.21875 -4.238281 L 8.921875 -4.238281 L 8.921875 -7.53125 L 5.328125 -7.53125 L 5.328125 -6.859375 L 8.21875 -6.859375 L 8.21875 -4.929688 L 5.5 -4.929688 L 5.5 -1.679688 C 5.5 -1.109375 5.78125 -0.820312 6.351562 -0.820312 L 8.128906 -0.820312 C 8.601562 -0.820312 8.910156 -0.949219 9.058594 -1.191406 C 9.21875 -1.429688 9.339844 -1.960938 9.398438 -2.769531 Z M 1.210938 -3.410156 C 1.160156 -1.960938 0.878906 -0.648438 0.378906 0.511719 L 0.929688 1 C 1.191406 0.421875 1.390625 -0.199219 1.550781 -0.871094 C 1.929688 -0.269531 2.378906 0.160156 2.890625 0.398438 C 3.46875 0.671875 4.941406 0.808594 7.28125 0.808594 L 9.480469 0.808594 L 9.601562 0.109375 C 9.058594 0.140625 8.230469 0.160156 7.121094 0.160156 C 5.289062 0.160156 4.058594 0.0703125 3.441406 -0.101562 L 3.441406 -1.980469 L 4.859375 -1.980469 L 4.859375 -2.640625 L 3.441406 -2.640625 L 3.441406 -4.050781 L 4.980469 -4.050781 L 4.980469 -4.710938 L 3.25 -4.710938 L 3.25 -6.179688 L 4.851562 -6.179688 L 4.851562 -6.839844 L 3.25 -6.839844 L 3.25 -8.191406 L 2.53125 -8.191406 L 2.53125 -6.839844 L 0.878906 -6.839844 L 0.878906 -6.179688 L 2.53125 -6.179688 L 2.53125 -4.710938 L 0.699219 -4.710938 L 0.699219 -4.050781 L 2.75 -4.050781 L 2.75 -0.441406 C 2.351562 -0.71875 2.011719 -1.140625 1.710938 -1.699219 C 1.789062 -2.21875 1.839844 -2.761719 1.859375 -3.328125 Z "/>
</g>
<g id="glyph-0-6">
<path d="M 9.21875 -6.140625 L 7.289062 -6.140625 C 7.671875 -6.640625 8.03125 -7.238281 8.378906 -7.960938 L 7.648438 -8.230469 C 7.328125 -7.480469 6.949219 -6.78125 6.511719 -6.140625 L 0.78125 -6.140625 L 0.78125 -3.898438 L 1.480469 -3.898438 L 1.480469 -5.449219 L 8.511719 -5.449219 L 8.511719 -3.898438 L 9.21875 -3.898438 Z M 2.308594 -4.601562 L 2.308594 -3.960938 L 6.671875 -3.960938 C 6.230469 -3.71875 5.601562 -3.421875 4.761719 -3.050781 L 4.761719 -2.21875 L 0.710938 -2.21875 L 0.710938 -1.539062 L 4.761719 -1.539062 L 4.761719 -0.101562 C 4.761719 0.179688 4.628906 0.320312 4.390625 0.320312 C 3.960938 0.320312 3.558594 0.308594 3.199219 0.300781 L 3.371094 0.960938 L 4.640625 0.960938 C 5.199219 0.960938 5.488281 0.691406 5.488281 0.148438 L 5.488281 -1.539062 L 9.300781 -1.539062 L 9.300781 -2.21875 L 5.488281 -2.21875 L 5.488281 -2.730469 C 6.308594 -3.128906 7.039062 -3.539062 7.699219 -3.960938 L 7.699219 -4.601562 Z M 2.199219 -8.078125 L 1.550781 -7.761719 C 1.921875 -7.300781 2.269531 -6.78125 2.609375 -6.199219 L 3.230469 -6.511719 C 2.910156 -7.078125 2.558594 -7.601562 2.199219 -8.078125 Z M 4.621094 -8.269531 L 3.96875 -7.949219 C 4.308594 -7.46875 4.628906 -6.921875 4.929688 -6.300781 L 5.570312 -6.621094 C 5.269531 -7.21875 4.949219 -7.769531 4.621094 -8.269531 Z "/>
</g>
<g id="glyph-0-7">
<path d="M 2.808594 -7.140625 L 0 0 L 0.878906 0 L 1.640625 -2 L 4.921875 -2 L 5.679688 0 L 6.570312 0 L 3.761719 -7.140625 Z M 1.898438 -2.691406 L 3.269531 -6.261719 L 3.308594 -6.261719 L 4.660156 -2.691406 Z "/>
</g>
<g id="glyph-0-8">
<path d="M 0.78125 -7.140625 L 0.78125 0 L 1.589844 0 L 1.589844 -7.140625 Z "/>
</g>
</g>
<image id="source-8" x="0" y="0" width="0" height="0"/>
<mask id="mask-0">
<use xlink:href="#source-8"/>
</mask>
</defs>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="30" y="30.599609"/>
<use xlink:href="#glyph-0-0" x="40" y="30.599609"/>
<use xlink:href="#glyph-0-1" x="50" y="30.599609"/>
<use xlink:href="#glyph-0-2" x="60" y="30.599609"/>
<use xlink:href="#glyph-0-3" x="70" y="30.599609"/>
<use xlink:href="#glyph-0-3" x="73" y="30.599609"/>
<use xlink:href="#glyph-0-4" x="76" y="30.599609"/>
<use xlink:href="#glyph-0-5" x="86" y="30.599609"/>
<use xlink:href="#glyph-0-6" x="96" y="30.599609"/>
<use xlink:href="#glyph-0-3" x="106" y="30.599609"/>
</g>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-7" x="109" y="30.599609"/>
<use xlink:href="#glyph-0-8" x="116" y="30.599609"/>
</g>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
<defs>
<g>
<g id="glyph-0-0">
<path d="M 5.476562 -9.960938 L 5.476562 -6.390625 L 2.152344 -6.390625 L 2.152344 -2.46875 L 10.242188 -2.46875 L 10.242188 -6.390625 L 6.34375 -6.390625 L 6.34375 -7.945312 L 11.085938 -7.945312 L 11.085938 -8.777344 L 6.34375 -8.777344 L 6.34375 -9.960938 Z M 9.363281 -3.289062 L 3.03125 -3.289062 L 3.03125 -5.5625 L 9.363281 -5.5625 Z M 2.125 -1.929688 C 1.6875 -0.929688 1.160156 -0.0625 0.550781 0.660156 L 1.269531 1.1875 C 1.882812 0.402344 2.40625 -0.511719 2.871094 -1.578125 Z M 4.792969 -1.578125 L 3.972656 -1.40625 C 4.203125 -0.636719 4.398438 0.246094 4.570312 1.246094 L 5.453125 1.050781 C 5.253906 0.109375 5.035156 -0.769531 4.792969 -1.578125 Z M 7.347656 -1.699219 L 6.515625 -1.527344 C 6.882812 -0.734375 7.1875 0.183594 7.429688 1.222656 L 8.300781 1.027344 C 8.03125 0.0507812 7.710938 -0.867188 7.347656 -1.699219 Z M 9.851562 -1.945312 L 9.140625 -1.492188 C 9.886719 -0.488281 10.476562 0.414062 10.902344 1.246094 L 11.636719 0.734375 C 11.21875 -0.0234375 10.632812 -0.917969 9.851562 -1.945312 Z "/>
</g>
<g id="glyph-0-1">
<path d="M 3.789062 -9.9375 L 2.980469 -9.546875 C 3.484375 -8.933594 3.949219 -8.238281 4.386719 -7.457031 L 5.183594 -7.859375 C 4.742188 -8.617188 4.277344 -9.300781 3.789062 -9.9375 Z M 8.382812 -10.046875 C 8.019531 -9.019531 7.613281 -8.152344 7.148438 -7.445312 L 1.566406 -7.445312 L 1.566406 -6.589844 L 5.660156 -6.589844 L 5.660156 -6.011719 C 5.648438 -5.414062 5.585938 -4.851562 5.476562 -4.316406 L 0.867188 -4.316406 L 0.867188 -3.460938 L 5.253906 -3.460938 C 5.085938 -2.921875 4.851562 -2.445312 4.558594 -2.003906 C 3.824219 -1.027344 2.519531 -0.230469 0.648438 0.367188 L 1.125 1.136719 C 3.007812 0.539062 4.363281 -0.304688 5.195312 -1.382812 C 5.585938 -1.90625 5.890625 -2.519531 6.125 -3.214844 C 6.785156 -1.355469 8.445312 0.121094 11.136719 1.210938 L 11.648438 0.390625 C 9.082031 -0.527344 7.503906 -1.808594 6.90625 -3.460938 L 11.355469 -3.460938 L 11.355469 -4.316406 L 6.390625 -4.316406 C 6.488281 -4.828125 6.539062 -5.402344 6.5625 -6.011719 L 6.5625 -6.589844 L 10.644531 -6.589844 L 10.644531 -7.445312 L 8.128906 -7.445312 C 8.566406 -8.140625 8.933594 -8.933594 9.226562 -9.816406 Z "/>
</g>
<g id="glyph-0-2">
<path d="M 1.601562 -9.800781 L 0.964844 -9.191406 C 1.871094 -8.554688 2.589844 -7.945312 3.105469 -7.371094 L 3.726562 -7.992188 C 3.152344 -8.59375 2.445312 -9.191406 1.601562 -9.800781 Z M 1.296875 -6.917969 L 0.671875 -6.320312 C 1.527344 -5.660156 2.199219 -5.046875 2.6875 -4.484375 L 3.3125 -5.109375 C 2.773438 -5.71875 2.101562 -6.320312 1.296875 -6.917969 Z M 2.835938 -3.542969 C 2.285156 -2.078125 1.636719 -0.648438 0.90625 0.746094 L 1.785156 1.125 C 2.457031 -0.230469 3.066406 -1.6875 3.617188 -3.226562 Z M 4.203125 -7.921875 L 4.203125 -7.050781 L 7.382812 -7.050781 L 7.382812 -4.117188 L 4.546875 -4.117188 L 4.546875 -3.277344 L 7.382812 -3.277344 L 7.382812 -0.0625 L 3.898438 -0.0625 L 3.898438 0.808594 L 11.660156 0.808594 L 11.660156 -0.0625 L 8.273438 -0.0625 L 8.273438 -3.277344 L 11.097656 -3.277344 L 11.097656 -4.117188 L 8.273438 -4.117188 L 8.273438 -7.050781 L 11.429688 -7.050781 L 11.429688 -7.921875 L 7.785156 -7.921875 L 8.285156 -8.117188 C 8.105469 -8.703125 7.835938 -9.339844 7.492188 -10.046875 L 6.660156 -9.816406 C 6.980469 -9.203125 7.246094 -8.582031 7.492188 -7.921875 Z "/>
</g>
<g id="glyph-0-3">
<rect x="0" y="0" width="0" height="0" mask="url(#mask-0)"/>
</g>
<g id="glyph-0-4">
<path d="M 0.609375 -4.925781 L 0.609375 -4.046875 L 11.597656 -4.046875 L 11.597656 -4.925781 Z "/>
</g>
<g id="glyph-0-5">
<path d="M 11.488281 -3.386719 L 10.65625 -3.667969 C 10.609375 -2.957031 10.546875 -2.480469 10.460938 -2.25 C 10.351562 -1.992188 10.121094 -1.859375 9.777344 -1.859375 L 8.117188 -1.859375 C 7.773438 -1.871094 7.601562 -2.042969 7.601562 -2.371094 L 7.601562 -5.183594 L 10.902344 -5.183594 L 10.902344 -9.203125 L 6.515625 -9.203125 L 6.515625 -8.382812 L 10.046875 -8.382812 L 10.046875 -6.027344 L 6.722656 -6.027344 L 6.722656 -2.054688 C 6.722656 -1.355469 7.066406 -1.003906 7.761719 -1.003906 L 9.9375 -1.003906 C 10.511719 -1.003906 10.890625 -1.160156 11.074219 -1.453125 C 11.269531 -1.746094 11.414062 -2.394531 11.488281 -3.386719 Z M 1.480469 -4.167969 C 1.417969 -2.394531 1.074219 -0.792969 0.464844 0.625 L 1.136719 1.222656 C 1.453125 0.511719 1.699219 -0.246094 1.894531 -1.0625 C 2.359375 -0.328125 2.910156 0.195312 3.53125 0.488281 C 4.242188 0.820312 6.039062 0.988281 8.898438 0.988281 L 11.585938 0.988281 L 11.734375 0.132812 C 11.074219 0.171875 10.058594 0.195312 8.703125 0.195312 C 6.464844 0.195312 4.960938 0.0859375 4.203125 -0.121094 L 4.203125 -2.421875 L 5.941406 -2.421875 L 5.941406 -3.226562 L 4.203125 -3.226562 L 4.203125 -4.949219 L 6.085938 -4.949219 L 6.085938 -5.757812 L 3.972656 -5.757812 L 3.972656 -7.554688 L 5.929688 -7.554688 L 5.929688 -8.359375 L 3.972656 -8.359375 L 3.972656 -10.011719 L 3.09375 -10.011719 L 3.09375 -8.359375 L 1.074219 -8.359375 L 1.074219 -7.554688 L 3.09375 -7.554688 L 3.09375 -5.757812 L 0.855469 -5.757812 L 0.855469 -4.949219 L 3.359375 -4.949219 L 3.359375 -0.539062 C 2.871094 -0.878906 2.457031 -1.394531 2.089844 -2.078125 C 2.1875 -2.714844 2.25 -3.375 2.273438 -4.070312 Z "/>
</g>
<g id="glyph-0-6">
<path d="M 11.269531 -7.503906 L 8.910156 -7.503906 C 9.375 -8.117188 9.816406 -8.847656 10.242188 -9.730469 L 9.351562 -10.058594 C 8.960938 -9.140625 8.496094 -8.285156 7.957031 -7.503906 L 0.953125 -7.503906 L 0.953125 -4.765625 L 1.808594 -4.765625 L 1.808594 -6.660156 L 10.402344 -6.660156 L 10.402344 -4.765625 L 11.269531 -4.765625 Z M 2.824219 -5.621094 L 2.824219 -4.839844 L 8.152344 -4.839844 C 7.613281 -4.546875 6.84375 -4.179688 5.816406 -3.726562 L 5.816406 -2.714844 L 0.867188 -2.714844 L 0.867188 -1.882812 L 5.816406 -1.882812 L 5.816406 -0.121094 C 5.816406 0.21875 5.660156 0.390625 5.367188 0.390625 C 4.839844 0.390625 4.351562 0.378906 3.910156 0.367188 L 4.117188 1.171875 L 5.671875 1.171875 C 6.355469 1.171875 6.710938 0.84375 6.710938 0.183594 L 6.710938 -1.882812 L 11.367188 -1.882812 L 11.367188 -2.714844 L 6.710938 -2.714844 L 6.710938 -3.335938 C 7.710938 -3.824219 8.605469 -4.328125 9.410156 -4.839844 L 9.410156 -5.621094 Z M 2.6875 -9.875 L 1.894531 -9.484375 C 2.347656 -8.921875 2.773438 -8.285156 3.191406 -7.578125 L 3.949219 -7.957031 C 3.558594 -8.652344 3.128906 -9.289062 2.6875 -9.875 Z M 5.648438 -10.109375 L 4.851562 -9.71875 C 5.269531 -9.128906 5.660156 -8.457031 6.027344 -7.699219 L 6.808594 -8.089844 C 6.441406 -8.824219 6.050781 -9.496094 5.648438 -10.109375 Z "/>
</g>
<g id="glyph-0-7">
<path d="M 3.433594 -8.726562 L 0 0 L 1.074219 0 L 2.003906 -2.445312 L 6.011719 -2.445312 L 6.941406 0 L 8.03125 0 L 4.597656 -8.726562 Z M 2.324219 -3.289062 L 3.996094 -7.652344 L 4.046875 -7.652344 L 5.695312 -3.289062 Z "/>
</g>
<g id="glyph-0-8">
<path d="M 0.953125 -8.726562 L 0.953125 0 L 1.945312 0 L 1.945312 -8.726562 Z "/>
</g>
</g>
<image id="source-8" x="0" y="0" width="0" height="0"/>
<mask id="mask-0">
<use xlink:href="#source-8"/>
</mask>
</defs>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-0" x="30" y="32.956055"/>
<use xlink:href="#glyph-0-0" x="42" y="32.956055"/>
<use xlink:href="#glyph-0-1" x="54" y="32.956055"/>
<use xlink:href="#glyph-0-2" x="66" y="32.956055"/>
<use xlink:href="#glyph-0-3" x="78" y="32.956055"/>
<use xlink:href="#glyph-0-3" x="82" y="32.956055"/>
<use xlink:href="#glyph-0-4" x="86" y="32.956055"/>
<use xlink:href="#glyph-0-5" x="98" y="32.956055"/>
<use xlink:href="#glyph-0-6" x="110" y="32.956055"/>
<use xlink:href="#glyph-0-3" x="122" y="32.956055"/>
</g>
<g fill="rgb(100%, 100%, 100%)" fill-opacity="1">
<use xlink:href="#glyph-0-7" x="126" y="32.956055"/>
<use xlink:href="#glyph-0-8" x="134" y="32.956055"/>
</g>
</svg>
#!/usr/bin/env python3
"""
生成通用片尾动画
支持三种尺寸:16:9、3:4、9:16
用法:
# 16:9 横版(默认)
manim -qh --format=mp4 --fps=30 -o outro.mp4 outro_generator.py OutroAnimation
# 3:4 竖版
manim -qh --format=mp4 --fps=30 -o outro_3x4.mp4 outro_generator.py OutroAnimation3x4
# 9:16 竖版(手机全屏)
manim -qh --format=mp4 --fps=30 -o outro_9x16.mp4 outro_generator.py OutroAnimation9x16
加语音(每种尺寸都要加):
edge-tts --text "点关注,不迷路!" --voice zh-CN-YunxiNeural --rate="+10%" --write-media outro_voice.mp3
ffmpeg -y -i outro.mp4 -i outro_voice.mp3 -filter_complex "[1:a]adelay=1000|1000,apad=whole_dur=5.2[aout]" -map 0:v -map "[aout]" -c:v copy -c:a aac outro_with_voice.mp4
mv outro_with_voice.mp4 outro.mp4
"""
from manim import *
import os
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LOGO_PATH = os.path.join(SCRIPT_DIR, "logo.jpg")
class OutroAnimation(Scene):
"""16:9 横版片尾"""
def construct(self):
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(1.8)
qr_code.shift(UP * 0.8)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=48, color=WHITE)
title.next_to(qr_code, DOWN, buff=0.6)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation3x4(Scene):
"""3:4 竖版片尾"""
def construct(self):
config.pixel_width = 1080
config.pixel_height = 1440
config.frame_width = 8
config.frame_height = 10.67
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(2.2)
qr_code.shift(UP * 1.5)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=42, color=WHITE)
title.next_to(qr_code, DOWN, buff=0.8)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation9x16(Scene):
"""9:16 竖版片尾(手机全屏)"""
def construct(self):
config.pixel_width = 1080
config.pixel_height = 1920
config.frame_width = 8
config.frame_height = 14.22
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(2.5)
qr_code.shift(UP * 2)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=40, color=WHITE)
title.next_to(qr_code, DOWN, buff=1.0)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation1x1(Scene):
"""1:1 正方形片尾"""
def construct(self):
config.pixel_width = 1024
config.pixel_height = 1024
config.frame_width = 8
config.frame_height = 8
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(2.0)
qr_code.shift(UP * 0.5)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=44, color=WHITE)
title.next_to(qr_code, DOWN, buff=0.6)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation2x3(Scene):
"""2:3 竖版片尾"""
def construct(self):
config.pixel_width = 832
config.pixel_height = 1248
config.frame_width = 8
config.frame_height = 12
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(2.0)
qr_code.shift(UP * 1.2)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=38, color=WHITE)
title.next_to(qr_code, DOWN, buff=0.8)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation3x2(Scene):
"""3:2 横版片尾"""
def construct(self):
config.pixel_width = 1248
config.pixel_height = 832
config.frame_width = 12
config.frame_height = 8
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(1.6)
qr_code.shift(UP * 0.6)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=46, color=WHITE)
title.next_to(qr_code, DOWN, buff=0.5)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation4x3(Scene):
"""4:3 横版片尾"""
def construct(self):
config.pixel_width = 1440
config.pixel_height = 1080
config.frame_width = 10.67
config.frame_height = 8
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(1.8)
qr_code.shift(UP * 0.7)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=46, color=WHITE)
title.next_to(qr_code, DOWN, buff=0.6)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation4x5(Scene):
"""4:5 竖版片尾(Instagram)"""
def construct(self):
config.pixel_width = 864
config.pixel_height = 1080
config.frame_width = 8
config.frame_height = 10
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(2.0)
qr_code.shift(UP * 1.0)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=36, color=WHITE)
title.next_to(qr_code, DOWN, buff=0.7)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation5x4(Scene):
"""5:4 横版片尾"""
def construct(self):
config.pixel_width = 1080
config.pixel_height = 864
config.frame_width = 10
config.frame_height = 8
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(1.7)
qr_code.shift(UP * 0.6)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=44, color=WHITE)
title.next_to(qr_code, DOWN, buff=0.5)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
class OutroAnimation21x9(Scene):
"""21:9 超宽屏片尾"""
def construct(self):
config.pixel_width = 1536
config.pixel_height = 672
config.frame_width = 18.29
config.frame_height = 8
self.camera.background_color = "#1a1a2e"
qr_code = ImageMobject(LOGO_PATH)
qr_code.scale(1.3)
qr_code.shift(LEFT * 4)
title = Text("点点关注 一起学 AI", font="PingFang SC", font_size=52, color=WHITE)
title.shift(RIGHT * 2)
self.play(GrowFromCenter(qr_code), run_time=0.8)
self.play(Write(title), run_time=1.0)
self.play(qr_code.animate.scale(1.05), rate_func=there_and_back, run_time=0.4)
self.wait(3)
if __name__ == "__main__":
print("生成片尾动画...")
print("\n16:9 横版:")
print(" manim -qh --format=mp4 --fps=30 -o outro.mp4 outro_generator.py OutroAnimation")
print("\n3:4 竖版:")
print(" manim -qh --format=mp4 --fps=30 -o outro_3x4.mp4 outro_generator.py OutroAnimation3x4")
print("\n9:16 竖版:")
print(" manim -qh --format=mp4 --fps=30 -o outro_9x16.mp4 outro_generator.py OutroAnimation9x16")
Video Creator
视频生成技能,图片+音频合成视频。
依赖
brew install ffmpeg
pip install edge-tts pyyaml功能
- 图片序列 + 音频 → 视频
- 淡入淡出转场
- 自动拼接片尾
- 添加 BGM
- 烧录字幕
资源
- 片尾视频:支持 9 种比例(1x1/9x16/16x9 等)
- BGM:科技感/史诗感
- Logo
Edge-TTS 中文音色参考
常用音色推荐
| 音色 ID | 性别 | 风格 | 适用场景 |
|---|---|---|---|
| zh-CN-YunxiNeural | 男 | 新闻播报风 | 资讯类、技术分享 |
| zh-CN-YunyangNeural | 男 | 温和亲切 | 教程、讲解 |
| zh-CN-XiaoxiaoNeural | 女 | 活泼自然 | 日常分享、生活类 |
| zh-CN-XiaoyiNeural | 女 | 温柔知性 | 文艺、情感类 |
| zh-CN-YunjianNeural | 男 | 浑厚大气 | 宣传片、正式场合 |
| zh-CN-XiaochenNeural | 女 | 甜美可爱 | 儿童内容、轻松话题 |
全部中文音色列表
普通话(大陆)
- zh-CN-XiaoxiaoNeural (女)
- zh-CN-XiaoyiNeural (女)
- zh-CN-YunjianNeural (男)
- zh-CN-YunxiNeural (男)
- zh-CN-YunyangNeural (男)
- zh-CN-XiaochenNeural (女)
- zh-CN-XiaohanNeural (女)
- zh-CN-XiaomengNeural (女)
- zh-CN-XiaomoNeural (女)
- zh-CN-XiaoqiuNeural (女)
- zh-CN-XiaoruiNeural (女)
- zh-CN-XiaoshuangNeural (女)
- zh-CN-XiaoxuanNeural (女)
- zh-CN-XiaoyanNeural (女)
- zh-CN-XiaoyouNeural (女)
- zh-CN-XiaozhenNeural (女)
- zh-CN-YunfengNeural (男)
- zh-CN-YunhaoNeural (男)
- zh-CN-YunzeNeural (男)
粤语(香港)
- zh-HK-HiuGaaiNeural (女)
- zh-HK-HiuMaanNeural (女)
- zh-HK-WanLungNeural (男)
台湾
- zh-TW-HsiaoChenNeural (女)
- zh-TW-HsiaoYuNeural (女)
- zh-TW-YunJheNeural (男)
语速和音调调整
语速 (rate)
- 加快:
+10%,+20%,+50% - 减慢:
-10%,-20%,-30% - 默认:
+0%
音调 (pitch)
- 升高:
+5Hz,+10Hz,+20Hz - 降低:
-5Hz,-10Hz,-20Hz - 默认:
+0Hz
配置示例
voice:
name: "zh-CN-YunxiNeural"
rate: "+10%" # 稍快一点
pitch: "-5Hz" # 稍低沉一点查看所有可用音色
python scripts/tts_generator.py --list-voices#!/usr/bin/env python3
"""
场景拆分器 - 将口播文本拆分成细镜头
基于时间戳对齐图片和字幕
"""
import json
import re
import argparse
from pathlib import Path
from typing import List, Dict
def split_by_sentence_timestamps(timestamps: List[Dict]) -> List[Dict]:
"""
直接使用 TTS 的 SentenceBoundary 时间戳作为镜头分割
Args:
timestamps: TTS 输出的时间戳(包含 sentence 类型)
Returns:
每个镜头的信息:text, start, end, duration
"""
shots = []
for ts in timestamps:
if ts.get("type") == "sentence":
shots.append({
"text": ts["text"],
"start": ts["start"],
"end": ts["end"],
"duration": round(ts["end"] - ts["start"], 2)
})
if not shots and timestamps:
total_start = timestamps[0]["start"]
total_end = timestamps[-1]["end"]
full_text = "".join(ts["text"] for ts in timestamps)
shots.append({
"text": full_text,
"start": total_start,
"end": total_end,
"duration": round(total_end - total_start, 2)
})
return shots
def split_long_shots(shots: List[Dict], max_duration: float = 6.0) -> List[Dict]:
"""
将过长的镜头按标点符号进一步拆分
Args:
shots: 镜头列表
max_duration: 最大镜头时长
Returns:
拆分后的镜头列表
"""
result = []
for shot in shots:
if shot["duration"] <= max_duration:
result.append(shot)
continue
text = shot["text"]
splits = re.split(r'([,。!?,!?])', text)
sub_texts = []
current = ""
for i, part in enumerate(splits):
current += part
if i % 2 == 1 and current.strip():
sub_texts.append(current.strip())
current = ""
if current.strip():
sub_texts.append(current.strip())
if len(sub_texts) <= 1:
result.append(shot)
continue
total_chars = sum(len(t) for t in sub_texts)
current_time = shot["start"]
for sub_text in sub_texts:
ratio = len(sub_text) / total_chars
sub_duration = shot["duration"] * ratio
result.append({
"text": sub_text,
"start": round(current_time, 2),
"end": round(current_time + sub_duration, 2),
"duration": round(sub_duration, 2)
})
current_time += sub_duration
return result
def merge_short_shots(shots: List[Dict], min_duration: float = 2.5) -> List[Dict]:
"""合并过短的镜头"""
if not shots:
return shots
merged = []
current = shots[0].copy()
for shot in shots[1:]:
if current["duration"] < min_duration:
current["text"] += shot["text"]
current["end"] = shot["end"]
current["duration"] = round(current["end"] - current["start"], 2)
else:
merged.append(current)
current = shot.copy()
merged.append(current)
return merged
def generate_shot_prompts(shots: List[Dict], style: str, context: str = "") -> List[Dict]:
"""
为每个镜头生成图片提示词
Args:
shots: 镜头列表
style: 画风描述
context: 上下文(如角色描述)
Returns:
带有图片提示词的镜头列表
"""
for i, shot in enumerate(shots):
shot["image_prompt"] = f"{style},{context},画面:{shot['text']}。禁止出现任何文字"
shot["index"] = i + 1
return shots
def generate_srt(shots: List[Dict], output_path: str):
"""生成 SRT 字幕文件"""
def format_time(seconds: float) -> str:
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int((seconds % 1) * 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
with open(output_path, "w", encoding="utf-8") as f:
for i, shot in enumerate(shots, 1):
f.write(f"{i}\n")
f.write(f"{format_time(shot['start'])} --> {format_time(shot['end'])}\n")
f.write(f"{shot['text']}\n\n")
print(f" ✓ 字幕: {output_path}")
def process_scene(text: str, timestamps_path: str, style: str, context: str = "", output_dir: str = ".") -> Dict:
"""
处理单个场景,输出镜头配置
Args:
text: 场景口播文本
timestamps_path: TTS 时间戳 JSON 文件
style: 画风
context: 上下文
output_dir: 输出目录
Returns:
场景配置字典
"""
with open(timestamps_path, "r", encoding="utf-8") as f:
timestamps = json.load(f)
shots = split_by_sentence_timestamps(timestamps)
shots = split_long_shots(shots, max_duration=6.0)
shots = merge_short_shots(shots, min_duration=2.5)
shots = generate_shot_prompts(shots, style, context)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
srt_path = output_path / "subtitles.srt"
generate_srt(shots, str(srt_path))
config_path = output_path / "shots.json"
with open(config_path, "w", encoding="utf-8") as f:
json.dump(shots, f, ensure_ascii=False, indent=2)
print(f" ✓ 镜头配置: {config_path}")
return {"shots": shots, "srt_path": str(srt_path)}
def main():
parser = argparse.ArgumentParser(description='场景拆分器')
parser.add_argument('--text', type=str, required=True, help='口播文本')
parser.add_argument('--timestamps', type=str, required=True, help='TTS时间戳JSON文件')
parser.add_argument('--style', type=str, default='', help='画风描述')
parser.add_argument('--context', type=str, default='', help='上下文(角色等)')
parser.add_argument('--output-dir', type=str, default='.', help='输出目录')
args = parser.parse_args()
result = process_scene(
text=args.text,
timestamps_path=args.timestamps,
style=args.style,
context=args.context,
output_dir=args.output_dir
)
print(f"\n拆分完成,共 {len(result['shots'])} 个镜头:")
for shot in result["shots"]:
print(f" [{shot['duration']:.1f}s] {shot['text']}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
TTS 语音生成器 - 使用 edge-tts
支持时间戳输出,用于字幕同步和镜头切换
"""
import asyncio
import argparse
import os
import json
import yaml
import edge_tts
async def generate_tts(text: str, voice: str, output_path: str, rate: str = "+0%", pitch: str = "+0Hz", with_timestamps: bool = False):
"""生成单条语音,可选输出时间戳"""
communicate = edge_tts.Communicate(text, voice, rate=rate, pitch=pitch)
if with_timestamps:
timestamps = []
audio_chunks = []
async for chunk in communicate.stream():
chunk_type = chunk.get("type", "")
if chunk_type == "audio":
audio_chunks.append(chunk.get("data", b""))
elif chunk_type == "WordBoundary":
timestamps.append({
"text": chunk.get("text", ""),
"start": chunk.get("offset", 0) / 10000000,
"end": (chunk.get("offset", 0) + chunk.get("duration", 0)) / 10000000
})
elif chunk_type == "SentenceBoundary":
timestamps.append({
"text": chunk.get("text", ""),
"start": chunk.get("offset", 0) / 10000000,
"end": (chunk.get("offset", 0) + chunk.get("duration", 0)) / 10000000,
"type": "sentence"
})
with open(output_path, "wb") as f:
for data in audio_chunks:
f.write(data)
ts_path = output_path.rsplit(".", 1)[0] + ".json"
with open(ts_path, "w", encoding="utf-8") as f:
json.dump(timestamps, f, ensure_ascii=False, indent=2)
print(f" ✓ 生成: {output_path} + 时间戳")
return timestamps
else:
await communicate.save(output_path)
print(f" ✓ 生成: {output_path}")
return None
async def generate_batch(config_path: str, output_dir: str):
"""批量生成语音"""
with open(config_path, 'r', encoding='utf-8') as f:
config = yaml.safe_load(f)
os.makedirs(output_dir, exist_ok=True)
voice_config = config.get('voice', {})
voice_name = voice_config.get('name', 'zh-CN-YunxiNeural')
rate = voice_config.get('rate', '+0%')
pitch = voice_config.get('pitch', '+0Hz')
scenes = config.get('scenes', [])
tasks = []
for i, scene in enumerate(scenes):
text = scene.get('text', '')
if not text:
continue
output_path = os.path.join(output_dir, f"{i:03d}.mp3")
tasks.append(generate_tts(text, voice_name, output_path, rate, pitch))
print(f"开始生成 {len(tasks)} 条语音...")
await asyncio.gather(*tasks)
print(f"✓ 完成!语音文件保存在: {output_dir}")
async def list_voices():
"""列出所有可用音色"""
voices = await edge_tts.list_voices()
zh_voices = [v for v in voices if v['Locale'].startswith('zh')]
print("\n中文可用音色:")
print("-" * 60)
for v in zh_voices:
gender = "♂" if v['Gender'] == 'Male' else "♀"
print(f"{gender} {v['ShortName']:<30} {v['Locale']}")
print("-" * 60)
print(f"共 {len(zh_voices)} 个中文音色")
def main():
parser = argparse.ArgumentParser(description='Edge-TTS 语音生成器')
parser.add_argument('--text', type=str, help='要转换的文本')
parser.add_argument('--voice', type=str, default='zh-CN-YunxiNeural', help='音色名称')
parser.add_argument('--rate', type=str, default='+0%', help='语速调整')
parser.add_argument('--pitch', type=str, default='+0Hz', help='音调调整')
parser.add_argument('--output', type=str, help='输出文件路径')
parser.add_argument('--timestamps', action='store_true', help='输出时间戳JSON文件')
parser.add_argument('--config', type=str, help='配置文件路径(批量生成)')
parser.add_argument('--output-dir', type=str, default='temp/audio', help='批量输出目录')
parser.add_argument('--list-voices', action='store_true', help='列出可用音色')
args = parser.parse_args()
if args.list_voices:
asyncio.run(list_voices())
elif args.config:
asyncio.run(generate_batch(args.config, args.output_dir))
elif args.text and args.output:
asyncio.run(generate_tts(args.text, args.voice, args.output, args.rate, args.pitch, args.timestamps))
else:
parser.print_help()
if __name__ == '__main__':
main()
#!/usr/bin/env python3
"""
视频图文语音对齐校验器
在合成视频之前,必须运行此脚本校验:
1. 每张图片文件是否存在
2. duration 是否为精确数值(必须从 narration.json 计算)
3. 图片总时长 vs 音频总时长(误差 < 1s)
4. 每张图时长是否在合理范围(1-7s)
5. 图片文件名关键词 vs 语音内容关键词 语义交叉比对
6. 输出完整对照表
用法:
python verify_alignment.py video_config.yaml
退出码:
0 = 校验通过
1 = 校验失败,禁止合成
"""
import json
import re
import sys
import yaml
from pathlib import Path
def get_audio_duration(audio_path):
"""用 ffprobe 获取音频时长"""
import subprocess
result = subprocess.run([
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'csv=p=0', str(audio_path)
], capture_output=True, text=True)
return float(result.stdout.strip())
def load_narration_timestamps(work_dir, audio_path):
"""加载 narration.json 时间戳"""
candidates = [
work_dir / 'narration.json',
Path(audio_path).with_suffix('.json'),
work_dir / (Path(audio_path).stem + '.json'),
]
for p in candidates:
if p.exists():
with open(p, 'r') as f:
return json.load(f)
return None
# ── 语义关键词映射表 ──
# 图片文件名中的关键词 → 语音中应该出现的关键词
KEYWORD_MAP = {
# 模型名
'ds': ['deepseek', 'ds', '深度求索'],
'deepseek': ['deepseek', 'ds', '深度求索'],
'glm': ['glm', '智谱', 'zhipu'],
'opus': ['opus', 'claude', 'anthropic'],
'claude': ['opus', 'claude', 'anthropic'],
'codex': ['codex', 'openai', 'gpt'],
'gpt': ['codex', 'openai', 'gpt'],
'kimi': ['kimi', '月之暗面', 'moonshot', 'k2'],
'qwen': ['qwen', '通义', '阿里', '千问'],
'minimax': ['minimax', 'mm'],
'mm': ['minimax', 'mm', 'minimax'],
# 功能/概念
'swarm': ['swarm', '蜂群', '并行', '子任务'],
'bench': [], # bench 太通用,语音里可能说"分"、"评测"或具体数字,不强制
'bench1': [],
'bench2': [],
'cost': ['成本', '性价比', '价格', '美元', '免费'],
'price': ['价格', '贵', '成本', '质量'],
'agent': ['agent', '智能体', '任务'],
'pony': ['pony', 'alpha', '神秘', '测试版'],
'pony1': ['pony', 'alpha', '神秘', '测试版'],
'pony2': ['pony', 'alpha', '第一', '排行'],
'b2b': ['b2b', 'b2c', '商业化'],
'music': ['music', '音乐', '音频'],
'multi': [], # multi 太通用
'multi1': [],
'multi2': [],
'arch': ['架构', '参数', '注意力', '稀疏'],
'terminal': [], # terminal 只是评测名,语音可能换说法
'think': ['thinking', '推理', '能力'],
'future': ['指日可待', '即将', '蓄势', '待发'],
'landscape': ['百花齐放', '格局', '一家独大'],
'budget': ['场景', '预算', '选', '取决'],
'cta': ['关注', '下期', '再见'],
'opening': ['春节', '炸场', '2026'],
'intro': [], # intro 太通用,不强制检查
'sum': [], # sum/summary 太通用,不强制检查(但下面会检查模型名)
'core': [], # core 太通用
'full': [], # full 太通用
'top': [],
'btm': [],
'creative': [],
'models': [],
'vs': [],
'title': [],
}
def extract_keywords_from_filename(filename):
"""从文件名提取关键词"""
name = Path(filename).stem.lower()
# 去掉序号前缀 (01_, 02_ 等)
name = re.sub(r'^\d+_', '', name)
# 按 _ 分割
parts = name.split('_')
return parts
def check_semantic_match(filename, speech_text):
"""
检查图片文件名暗示的内容是否和语音文字匹配
策略:
- 模型名关键词(ds/glm/opus/codex/kimi/qwen/mm)只在总结段(文件名含 sum_)强制检查
因为正文段落里模型名只在开头提一次,后续评测细节不会重复模型名
- 功能/概念关键词(swarm/cost/price/landscape/cta 等)始终检查
返回: (is_ok, reason)
"""
if not speech_text:
return True, ""
parts = extract_keywords_from_filename(filename)
speech_lower = speech_text.lower()
# 判断是否是总结段(去掉序号后以 sum_ 开头,如 37_sum_ds.png)
name_no_num = re.sub(r'^\d+_', '', Path(filename).stem.lower())
is_summary = name_no_num.startswith('sum_')
# 模型名关键词集合
MODEL_KEYS = {'ds', 'deepseek', 'glm', 'opus', 'claude', 'codex', 'gpt', 'kimi', 'qwen', 'minimax', 'mm'}
mismatches = []
for part in parts:
if part in KEYWORD_MAP:
expected_words = KEYWORD_MAP[part]
if not expected_words:
continue # 跳过不强制检查的通用词
# 模型名关键词:只在总结段强制检查
if part in MODEL_KEYS and not is_summary:
continue
# 检查语音中是否包含至少一个期望关键词
found = any(w in speech_lower for w in expected_words)
if not found:
mismatches.append((part, expected_words))
if mismatches:
details = []
for part, expected in mismatches:
details.append(f"文件名含'{part}'但语音中没有{expected}中的任何一个")
return False, '; '.join(details)
return True, ""
def main():
if len(sys.argv) < 2:
print("用法: python verify_alignment.py video_config.yaml")
sys.exit(1)
config_path = Path(sys.argv[1])
if not config_path.exists():
print(f"❌ 配置文件不存在: {config_path}")
sys.exit(1)
with open(config_path) as f:
config = yaml.safe_load(f)
work_dir = config_path.parent
scenes = config.get('scenes', [])
if not scenes:
print("❌ 配置文件中没有 scenes")
sys.exit(1)
errors = []
warnings = []
# ── 收集所有图片和 duration ──
images = []
durations = []
audio_files = []
for si, scene in enumerate(scenes):
audio_path = work_dir / scene['audio']
if not audio_path.exists():
errors.append(f"音频不存在: {audio_path}")
audio_files.append(audio_path)
if 'images' not in scene:
errors.append(f"场景 {si+1} 缺少 images 列表")
continue
for img_cfg in scene['images']:
img_path = work_dir / img_cfg['file']
if not img_path.exists():
errors.append(f"图片不存在: {img_path}")
d = img_cfg.get('duration')
if d is None:
errors.append(f"❌ {img_cfg['file']} 缺少 duration!必须从 narration.json 精确计算!")
durations.append(0)
else:
try:
d = float(d)
except (ValueError, TypeError):
errors.append(f"❌ {img_cfg['file']} 的 duration='{d}' 不是数字!必须填写精确的秒数!")
durations.append(0)
continue
durations.append(d)
if d <= 0:
errors.append(f"❌ {img_cfg['file']} duration={d},不能 ≤ 0")
elif d < 1.0:
warnings.append(f"⚠ {img_cfg['file']} duration={d:.1f}s,太短(<1s)")
elif d > 7.0:
warnings.append(f"⚠ {img_cfg['file']} duration={d:.1f}s,超过7秒,建议拆分")
images.append(img_cfg['file'])
# ── 音频总时长 ──
total_audio = 0
for af in audio_files:
if af.exists():
total_audio += get_audio_duration(af)
total_image = sum(durations)
diff = abs(total_image - total_audio)
if diff > 1.0:
errors.append(f"❌ 图片总时长({total_image:.1f}s) vs 音频总时长({total_audio:.1f}s),差值 {diff:.1f}s 超过 1s!")
elif diff > 0.5:
warnings.append(f"⚠ 图片总时长({total_image:.1f}s) vs 音频总时长({total_audio:.1f}s),差值 {diff:.1f}s")
# ── 加载 narration.json 做语义对照 ──
timestamps = None
if audio_files:
timestamps = load_narration_timestamps(work_dir, audio_files[0])
# ── 输出对照表 + 语义校验 ──
print("\n" + "=" * 80)
print("📋 图文语音对齐校验报告")
print("=" * 80)
print(f"\n{'序号':<4} {'图片文件':<28} {'时长':>5} {'语义':>3} 对应语音内容")
print("-" * 80)
semantic_errors = []
cum_time = 0
for i, (img, dur) in enumerate(zip(images, durations)):
# 匹配 narration.json 中对应的句子
matched_text = ""
if timestamps:
start_t = cum_time
end_t = cum_time + dur
matched_sentences = []
for ts in timestamps:
if ts['end'] > start_t + 0.1 and ts['start'] < end_t - 0.1:
matched_sentences.append(ts['text'])
if matched_sentences:
matched_text = ' '.join(matched_sentences)
# 语义校验
display_text = matched_text[:45] + "..." if len(matched_text) > 45 else matched_text
sem_ok, sem_reason = check_semantic_match(img, matched_text)
sem_flag = "✅" if sem_ok else "❌"
if not sem_ok:
semantic_errors.append((i + 1, img, matched_text[:60], sem_reason))
print(f" {i+1:<3} {img:<28} {dur:>4.1f}s {sem_flag} {display_text}")
cum_time += dur
print("-" * 80)
print(f" {'合计':<33} {total_image:>4.1f}s")
print(f" {'音频总时长':<33} {total_audio:>4.1f}s")
print(f" {'差值':<33} {diff:>4.1f}s")
# ── 语义错误详情 ──
if semantic_errors:
print(f"\n🔍 语义不匹配 ({len(semantic_errors)}):")
for idx, img, text, reason in semantic_errors:
print(f" 第{idx}张 {img}")
print(f" 语音: {text}")
print(f" 问题: {reason}")
errors.append(f"❌ {len(semantic_errors)} 张图片的文件名与语音内容语义不匹配!图片画的内容和语音说的内容对不上!")
# ── 输出结果 ──
if warnings:
print(f"\n⚠ 警告 ({len(warnings)}):")
for w in warnings:
print(f" {w}")
if errors:
print(f"\n❌ 错误 ({len(errors)}):")
for e in errors:
print(f" {e}")
print(f"\n⛔ 校验失败!请修复以上问题后再合成视频。")
print(f" 禁止跳过校验直接合成!")
sys.exit(1)
else:
print(f"\n✅ 校验通过!图文语音对齐正确,可以合成视频。")
sys.exit(0)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
视频生成器 - 图片+音频合成视频
支持:淡入淡出转场、自动拼接片尾、添加BGM
用法:
python video_maker.py config.yaml
python video_maker.py config.yaml --no-outro # 不加片尾
python video_maker.py config.yaml --no-bgm # 不加BGM
"""
import argparse
import os
import subprocess
import sys
import yaml
from pathlib import Path
SCRIPT_DIR = Path(__file__).parent
SKILL_DIR = SCRIPT_DIR.parent
ASSETS_DIR = SKILL_DIR / "assets"
BGM_DEFAULT = ASSETS_DIR / "bgm_technology.mp3"
BGM_EPIC = ASSETS_DIR / "bgm_epic.mp3"
VALID_ASPECT_RATIOS = [
"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9"
]
RATIO_TO_SIZE = {
"1:1": (1024, 1024),
"2:3": (832, 1248),
"3:2": (1248, 832),
"3:4": (1080, 1440),
"4:3": (1440, 1080),
"4:5": (864, 1080),
"5:4": (1080, 864),
"9:16": (1080, 1920),
"16:9": (1920, 1080),
"21:9": (1536, 672),
}
def get_outro_path(ratio):
"""根据比例获取片尾路径,优先精确匹配,否则按方向匹配,最后兜底"""
ratio_file = ASSETS_DIR / f"outro_{ratio.replace(':', 'x')}.mp4"
if ratio_file.exists():
return ratio_file
w, h = RATIO_TO_SIZE.get(ratio, (1920, 1080))
if h > w:
candidates = ["outro_9x16.mp4", "outro_3x4.mp4"]
elif w > h:
candidates = ["outro.mp4", "outro_3x4.mp4"]
else:
candidates = ["outro_1x1.mp4", "outro.mp4"]
for name in candidates:
fallback = ASSETS_DIR / name
if fallback.exists():
return fallback
return ASSETS_DIR / "outro.mp4"
def run_cmd(cmd, desc=""):
"""执行命令并返回结果"""
if desc:
print(f" {desc}...")
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"错误: {result.stderr[-1000:]}")
sys.exit(1)
return result
def get_duration(file_path):
"""获取音视频时长"""
result = subprocess.run([
'ffprobe', '-v', 'error', '-show_entries', 'format=duration',
'-of', 'csv=p=0', str(file_path)
], capture_output=True, text=True)
return float(result.stdout.strip())
def generate_video_with_transitions(images, durations, output_path, fade_duration=0.5, ratio="16:9"):
"""生成带转场的视频"""
print(f"\n[1/4] 生成主视频 ({len(images)}张图片, {fade_duration}秒转场)")
width, height = RATIO_TO_SIZE.get(ratio, (1920, 1080))
display_durations = []
for i, dur in enumerate(durations):
if i < len(durations) - 1:
display_durations.append(dur + fade_duration)
else:
display_durations.append(dur)
inputs = []
for img, dur in zip(images, display_durations):
inputs.extend(['-loop', '1', '-t', str(dur), '-i', str(img)])
filter_parts = []
for i in range(len(images)):
filter_parts.append(
f"[{i}:v]scale={width}:{height}:force_original_aspect_ratio=decrease,"
f"pad={width}:{height}:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30[v{i}];"
)
offset = 0
for i in range(len(images) - 1):
if i == 0:
offset = display_durations[0] - fade_duration
filter_parts.append(
f"[v0][v1]xfade=transition=fade:duration={fade_duration}:offset={offset}[xf1];"
)
else:
offset += display_durations[i] - fade_duration
filter_parts.append(
f"[xf{i}][v{i+1}]xfade=transition=fade:duration={fade_duration}:offset={offset}[xf{i+1}];"
)
last_xf = f"xf{len(images)-1}"
filter_complex = ''.join(filter_parts).rstrip(';')
cmd = ['ffmpeg', '-y'] + inputs + [
'-filter_complex', filter_complex,
'-map', f'[{last_xf}]',
'-c:v', 'libx264', '-preset', 'fast', '-crf', '20', '-pix_fmt', 'yuv420p',
str(output_path)
]
run_cmd(cmd, f"合成{len(images)}张图片")
print(f" ✓ 主视频: {get_duration(output_path):.1f}秒")
def merge_audio(audio_files, output_path):
"""合并音频文件"""
print(f"\n[2/4] 合并音频 ({len(audio_files)}个文件)")
concat_file = output_path.parent / "audio_concat.txt"
with open(concat_file, 'w') as f:
for audio in audio_files:
f.write(f"file '{audio.absolute()}'\n")
cmd = [
'ffmpeg', '-y', '-f', 'concat', '-safe', '0', '-i', str(concat_file),
'-af', 'aresample=44100', '-c:a', 'aac', '-b:a', '192k', str(output_path)
]
run_cmd(cmd, "合并音频")
concat_file.unlink()
print(f" ✓ 音频: {get_duration(output_path):.1f}秒")
def combine_video_audio(video_path, audio_path, output_path):
"""合并视频和音频"""
cmd = [
'ffmpeg', '-y', '-i', str(video_path), '-i', str(audio_path),
'-c:v', 'copy', '-c:a', 'copy', '-shortest', str(output_path)
]
run_cmd(cmd, "合并视频音频")
def append_outro(video_path, output_path, fade_duration=0.5, ratio="16:9"):
"""拼接片尾,自动缩放片尾到主视频分辨率"""
print(f"\n[3/4] 拼接片尾")
outro_file = get_outro_path(ratio)
if not outro_file.exists():
print(f" ⚠ 片尾文件不存在: {outro_file}")
return video_path
width, height = RATIO_TO_SIZE.get(ratio, (1920, 1080))
outro_ready = output_path.parent / "outro_ready.mp4"
cmd = [
'ffmpeg', '-y', '-i', str(outro_file),
'-vf', f'scale={width}:{height}:force_original_aspect_ratio=decrease,pad={width}:{height}:(ow-iw)/2:(oh-ih)/2,setsar=1',
'-c:v', 'libx264', '-preset', 'fast', '-crf', '20',
'-c:a', 'aac', '-ar', '44100', str(outro_ready)
]
run_cmd(cmd, "准备片尾")
video_duration = get_duration(video_path)
fade_start = video_duration - fade_duration
cmd = [
'ffmpeg', '-y', '-i', str(video_path), '-i', str(outro_ready),
'-filter_complex',
f"[0:v]fade=t=out:st={fade_start}:d={fade_duration}[v0];"
f"[1:v]fade=t=in:st=0:d={fade_duration}[v1];"
f"[v0][v1]concat=n=2:v=1:a=0[vout];"
f"[0:a][1:a]concat=n=2:v=0:a=1[aout]",
'-map', '[vout]', '-map', '[aout]',
'-c:v', 'libx264', '-preset', 'fast', '-crf', '20',
'-c:a', 'aac', '-b:a', '192k', str(output_path)
]
run_cmd(cmd, "拼接片尾")
outro_ready.unlink()
print(f" ✓ 含片尾: {get_duration(output_path):.1f}秒")
return output_path
def burn_subtitles(video_path, srt_path, output_path, ratio="16:9"):
"""烧录字幕到视频:底部居中固定位置"""
print(f"\n[字幕] 烧录字幕")
if not Path(srt_path).exists():
print(f" ⚠ 字幕文件不存在: {srt_path}")
return video_path
width, height = RATIO_TO_SIZE.get(ratio, (1920, 1080))
# 字体大小:高度/25,16:9时约43px,9:16时约77px
font_size = max(36, int(height / 25))
margin_bottom = int(height / 15)
ass_path = Path(srt_path).with_suffix('.ass')
srt_to_ass(srt_path, ass_path, width, height, font_size, margin_bottom)
ass_escaped = str(ass_path).replace(":", r"\:").replace("'", r"\'")
cmd = [
'ffmpeg', '-y', '-i', str(video_path),
'-vf', f"ass='{ass_escaped}'",
'-c:v', 'libx264', '-preset', 'fast', '-crf', '20',
'-c:a', 'copy', str(output_path)
]
run_cmd(cmd, "烧录字幕")
print(f" ✓ 含字幕: {get_duration(output_path):.1f}秒")
return output_path
def srt_to_ass(srt_path, ass_path, width, height, font_size, margin_bottom):
"""将 SRT 转换为 ASS 格式,固定底部居中,自动换行"""
import re
with open(srt_path, 'r', encoding='utf-8') as f:
srt_content = f.read()
# 每行字数规则表(按分辨率宽度固定)
CHARS_PER_LINE_MAP = {
1024: 20, # 1:1
832: 14, # 2:3
1248: 32, # 3:2
1080: 16, # 3:4, 4:5, 5:4, 9:16 (竖版统一16字)
1440: 28, # 4:3
864: 17, # 4:5
1920: 38, # 16:9
1536: 48, # 21:9
}
# 查表,找不到则按公式计算
MAX_CHARS = CHARS_PER_LINE_MAP.get(width)
if MAX_CHARS is None:
# 兜底:按宽度和字体大小估算
MAX_CHARS = max(12, int(width / (font_size * 1.2)))
# 标点符号(不能放行首)
PUNCTUATION = ',。、:;?!,.:;?!)】」》\'\"'
def find_break_point(text, max_pos):
"""找到合适的断点位置,优先在空格处断开"""
if max_pos >= len(text):
return len(text)
# 从max_pos往前找空格断点
for i in range(max_pos, max(max_pos // 2, 1), -1):
if text[i] == ' ':
return i
# 没找到空格就直接断
return max_pos
def wrap_text_2lines(text):
"""换行:严格2行,返回单个2行字幕块"""
text = text.strip()
if len(text) <= MAX_CHARS:
return text + r'\N '
# 找第一行断点
break1 = find_break_point(text, MAX_CHARS)
line1 = text[:break1].strip()
line2 = text[break1:].strip()
# 第二行也限制长度
if len(line2) > MAX_CHARS:
break2 = find_break_point(line2, MAX_CHARS)
line2 = line2[:break2].strip()
return line1 + r'\N' + line2
def split_long_text(text, start_sec, end_sec):
"""长文本拆成多条字幕,每条严格2行,时间均分"""
text = text.strip()
# 先模拟换行,计算实际需要几条字幕
blocks = []
remaining = text
while remaining:
# 第一行
if len(remaining) <= MAX_CHARS:
blocks.append(remaining)
break
break1 = find_break_point(remaining, MAX_CHARS)
line1 = remaining[:break1].strip()
rest = remaining[break1:].strip()
# 第二行
if len(rest) <= MAX_CHARS:
blocks.append(line1 + ' ' + rest)
break
break2 = find_break_point(rest, MAX_CHARS)
line2 = rest[:break2].strip()
blocks.append(line1 + ' ' + line2)
remaining = rest[break2:].strip()
# 时间均分
duration = end_sec - start_sec
time_per_block = duration / len(blocks)
result = []
for i, block in enumerate(blocks):
block_start = start_sec + i * time_per_block
block_end = start_sec + (i + 1) * time_per_block
result.append((block, block_start, block_end))
return result
ass_header = f"""[Script Info]
Title: Subtitles
ScriptType: v4.00+
PlayResX: {width}
PlayResY: {height}
WrapStyle: 0
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,PingFang SC,{font_size},&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,1,2,10,10,{margin_bottom},1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
"""
def sec_to_ass_time(sec):
"""秒数转ASS时间格式"""
h = int(sec // 3600)
m = int((sec % 3600) // 60)
s = int(sec % 60)
cs = int((sec % 1) * 100)
return f"{h}:{m:02d}:{s:02d}.{cs:02d}"
events = []
blocks = re.split(r'\n\n+', srt_content.strip())
for block in blocks:
lines = block.strip().split('\n')
if len(lines) >= 3:
time_line = lines[1]
text = ' '.join(lines[2:]).replace('\n', ' ')
# 标点符号替换为空格,便于换行分割
text = re.sub(r'[,。、:;?!,.:;?!""''「」『』【】()()《》]', ' ', text)
# 合并多个空格为一个
text = re.sub(r'\s+', ' ', text).strip()
match = re.match(r'(\d{2}):(\d{2}):(\d{2}),(\d{3}) --> (\d{2}):(\d{2}):(\d{2}),(\d{3})', time_line)
if match:
sh, sm, ss, sms = match.groups()[:4]
eh, em, es, ems = match.groups()[4:]
start_sec = int(sh) * 3600 + int(sm) * 60 + int(ss) + int(sms) / 1000
end_sec = int(eh) * 3600 + int(em) * 60 + int(es) + int(ems) / 1000
# 长文本拆成多条字幕
sub_blocks = split_long_text(text, start_sec, end_sec)
for sub_text, sub_start, sub_end in sub_blocks:
formatted_text = wrap_text_2lines(sub_text)
start = sec_to_ass_time(sub_start)
end = sec_to_ass_time(sub_end)
events.append(f"Dialogue: 0,{start},{end},Default,,0,0,0,,{formatted_text}")
with open(ass_path, 'w', encoding='utf-8') as f:
f.write(ass_header + '\n'.join(events))
def add_bgm(video_path, output_path, volume=0.08, bgm_path=None):
"""添加背景音乐"""
print(f"\n[4/4] 添加BGM")
if bgm_path is None:
bgm_path = BGM_DEFAULT
bgm_path = Path(bgm_path)
if not bgm_path.exists():
print(f" ⚠ BGM文件不存在: {bgm_path}")
return video_path
cmd = [
'ffmpeg', '-y', '-i', str(video_path),
'-stream_loop', '-1', '-i', str(bgm_path),
'-filter_complex',
f"[1:a]volume={volume}[bgm];[0:a][bgm]amix=inputs=2:duration=first[aout]",
'-map', '0:v', '-map', '[aout]',
'-c:v', 'copy', '-c:a', 'aac', '-b:a', '192k', str(output_path)
]
run_cmd(cmd, "添加BGM")
print(f" ✓ 最终视频: {get_duration(output_path):.1f}秒")
return output_path
def main():
parser = argparse.ArgumentParser(description='视频生成器')
parser.add_argument('config', help='配置文件路径 (YAML)')
parser.add_argument('--no-outro', action='store_true', help='不添加片尾')
parser.add_argument('--no-bgm', action='store_true', help='不添加BGM')
parser.add_argument('--fade', type=float, default=0.5, help='转场时长(秒)')
parser.add_argument('--bgm-volume', type=float, default=0.08, help='BGM音量')
parser.add_argument('--bgm', type=str, default=None, help='自定义BGM路径,可选: epic')
parser.add_argument('--ratio', type=str, default='16:9',
help=f'视频比例,支持: {", ".join(VALID_ASPECT_RATIOS)}')
parser.add_argument('--srt', type=str, default=None, help='字幕文件路径(SRT格式)')
args = parser.parse_args()
config_path = Path(args.config)
if not config_path.exists():
print(f"配置文件不存在: {config_path}")
sys.exit(1)
with open(config_path) as f:
config = yaml.safe_load(f)
work_dir = config_path.parent
output_path = work_dir / config.get('output', 'output.mp4')
if args.ratio == '16:9' and 'ratio' in config:
args.ratio = config['ratio']
if 'bgm_volume' in config and args.bgm_volume == 0.08:
args.bgm_volume = config['bgm_volume']
if args.ratio not in VALID_ASPECT_RATIOS:
print(f"错误: 不支持的比例 '{args.ratio}'")
print(f"支持的比例: {', '.join(VALID_ASPECT_RATIOS)}")
sys.exit(1)
scenes = config.get('scenes', [])
if not scenes:
print("配置文件中没有 scenes")
sys.exit(1)
images = []
durations = []
audio_files = []
for scene in scenes:
audio = work_dir / scene['audio']
if not audio.exists():
print(f"音频不存在: {audio}")
sys.exit(1)
audio_files.append(audio)
if 'images' in scene:
for img_cfg in scene['images']:
img = work_dir / img_cfg['file']
if not img.exists():
print(f"图片不存在: {img}")
sys.exit(1)
images.append(img)
d = img_cfg.get('duration')
if d is None:
print(f"\n⛔ {img_cfg['file']} 缺少 duration!每张图必须填写精确的秒数。")
sys.exit(1)
try:
d = float(d)
except (ValueError, TypeError):
print(f"\n⛔ {img_cfg['file']} 的 duration='{d}' 不是数字!必须填写精确的秒数。")
sys.exit(1)
if d <= 0:
print(f"\n⛔ {img_cfg['file']} 的 duration={d},必须大于0!")
sys.exit(1)
durations.append(d)
else:
img = work_dir / scene['image']
if not img.exists():
print(f"图片不存在: {img}")
sys.exit(1)
images.append(img)
durations.append(get_duration(audio))
total_audio_duration = sum(get_duration(af) for af in audio_files)
total_image_duration = sum(durations)
diff = abs(total_image_duration - total_audio_duration)
# 硬校验:图片总时长和音频总时长差超过2秒直接报错退出
if diff > 2.0:
print(f"\n❌ 音画时长严重不匹配!")
print(f" 图片总时长: {total_image_duration:.1f}s")
print(f" 音频总时长: {total_audio_duration:.1f}s")
print(f" 差值: {diff:.1f}s(超过2秒阈值)")
print(f"\n⛔ 请根据 narration.json 时间戳重新计算每张图的 duration!")
print(f" 禁止凭感觉手动填 duration,必须按语义分配!")
sys.exit(1)
if total_image_duration < total_audio_duration:
gap = total_audio_duration - total_image_duration + 0.5
durations[-1] += gap
print(f"\n⚠ 图片时长({total_image_duration:.1f}s) < 音频时长({total_audio_duration:.1f}s)")
print(f" 自动拉伸最后一张图片 +{gap:.1f}s")
print(f"\n{'='*50}")
print(f"视频生成器")
print(f"{'='*50}")
print(f"场景数: {len(scenes)}")
print(f"音频时长: {total_audio_duration:.1f}秒")
print(f"视频时长: {sum(durations):.1f}秒")
print(f"转场: {args.fade}秒 淡入淡出")
print(f"片尾: {'是' if not args.no_outro else '否'}")
print(f"BGM: {'是' if not args.no_bgm else '否'}")
temp_dir = work_dir / "temp"
temp_dir.mkdir(exist_ok=True)
video_only = temp_dir / "video_only.mp4"
generate_video_with_transitions(images, durations, video_only, args.fade, args.ratio)
audio_merged = temp_dir / "audio_merged.m4a"
merge_audio(audio_files, audio_merged)
video_with_audio = temp_dir / "video_with_audio.mp4"
combine_video_audio(video_only, audio_merged, video_with_audio)
current_video = video_with_audio
if args.srt:
srt_path = work_dir / args.srt if not Path(args.srt).is_absolute() else Path(args.srt)
video_with_subs = temp_dir / "video_with_subs.mp4"
current_video = burn_subtitles(current_video, srt_path, video_with_subs, args.ratio)
if not args.no_outro:
video_with_outro = temp_dir / "video_with_outro.mp4"
current_video = append_outro(current_video, video_with_outro, args.fade, args.ratio)
if not args.no_bgm:
bgm_path = None
if args.bgm:
if args.bgm == 'epic':
bgm_path = BGM_EPIC
else:
bgm_path = Path(args.bgm)
add_bgm(current_video, output_path, args.bgm_volume, bgm_path)
else:
subprocess.run(['cp', str(current_video), str(output_path)])
print(f"\n{'='*50}")
print(f"✅ 完成: {output_path}")
print(f"{'='*50}\n")
if __name__ == "__main__":
main()