自驾路线动态地图短视频生成器
基于 Remotion 的模板化流水线。林芝桃花季 v5 已验收,本技能将同一套管线固化为「输入路书 → 输出成片」,任意自驾路线复用。
前置环境
- 项目目录:任意 Remotion 项目(参考
remotion-videos/),装好@remotion/renderer、ffmpeg-static(抽帧用) - Python 脚本用带 PIL+numpy 的解释器:
<本机 venv python 路径>(例:C:/Users/<用户名>/.workbuddy/binaries/python/envs/default/Scripts/python.exe;缺依赖时pip install pillow numpy;分享给他人时必须替换为对方机器上的解释器路径) - 合规图源:高德卫星瓦片(geo-map-compliance-guard 白名单内;禁止 Google/OSM)
- 站点代表图:每站一张竖版真实摄影图(严禁 AI 生成),放入
public/assets/real-*.jpg
极简输入模式(只给站点名)
用户只给一串站点名(如「林芝→波密→墨脱→米林→拉萨」)时,其余字段由 AI 自动补全,但必须走下面的数据获取与验证规则:
| 字段 | 自动获取方式 | 强制要求 |
|---|---|---|
| 真实经纬度 | 联网检索(BIGEMAP/腾讯地图地名检索/百科),GCJ-02 坐标 | ≥2 个来源交叉验证;县城/村镇必须落在河谷或居民点位置,山口/湖畔坐标要与地形吻合;查不到就明确告知用户,绝不编造。腾讯地图 API 坑(2026-09-02 阿里 v2.4 实测):poi_sug 关键词补全不返回坐标(lat/lng=None),拿坐标必须用 poi_search(keyword, region, raw=True);同名歧义要警惕(如「冻嘎拉山口」返回的是喜马拉雅南坡同名点而非阿里龙嘎拉),POI 拿不到可靠坐标时用相邻两站走廊中点近似并标注 |
| altitude | 联网检索地名海拔 | 同上,交叉验证 |
| mileageToNext | 优先联网查真实公路里程;查不到按球面距离 × 1.35 绕路系数估算 | 估算值必须向用户标注「估算」 |
| via 途经点 | 按真实公路常识判断(翻哪座山口、经过哪个镇) | 联网核实道路走向,不凭空造点 |
| subtitle / tagline / day | 按语境创作 | 正常创作即可 |
| image | 联网找每站竖版真实摄影图 | 严禁 AI 生成;下载后目视确认是当地真实景观 |
第五道校验门(落图门)——极简模式强制:constants 生成后、渲染前,把所有站点坐标投影到底图上抽一帧,人眼核对每个 POI 是否落在合理地形位置(县城在河谷、垭口在山脊、湖边在湖岸)。坐标错一个,路线就会在真实地图上"飞"。
极简模式产出后,先向用户展示「站点-坐标-海拔-里程表」请其确认,再进入七步流水线。
输入规范(必需路书数据表)
对每个站点收集以下字段,缺一不可:
| 字段 | 说明 | 示例 |
|---|---|---|
| id / 名称 | 英文 id + 中文名 | motuo / 墨脱县城 |
| subtitle | 英文副标题 | Motuo · Lotus Land |
| 真实经纬度 | GCJ-02 火星坐标(与高德瓦片对齐;来源:BIGEMAP/腾讯地图/高德坐标拾取器) | 95.3320, 29.3250 |
| day / dayLabel | 日期与 DAY 序号 | 4.6 / DAY 5 |
| mileageToNext | 到下一站当日里程(0=当日不挪窝) | 350 |
| altitude | 海拔 | 1100m |
| tagline | 一句话亮点 | 扎墨公路双进 · 莲花秘境 1100 米 |
| attractions | 该站代表性景点名列表(每站 ≥1,photo 卡片渲染为 amber chip) | ['扎墨公路', '果果塘大拐弯'] |
| image | 真图文件名 | real-motuo.jpg |
| via | 到达本站途中的真实道路途经点(每段 1-4 个,让路线按真实公路走向曲折) | [v(L.galongla, '嘎隆拉隧道')] |
| cam | 到达本站的镜头 keyframe {scale, cx, cy}(用下方推导规则算) |
{scale:0.95, cx:L.motuo.x, cy:L.motuo.y} |
via 两个铁律(v2 踩坑后写入模板):
1. 必须带 name:用 v(坐标, '地名') 构造(v 已在 constants 模板定义),地图行进段才会显示途经点名,否则只画点无标名
2. 归属语义:Node[i].via 属于「Node[i-1] → Node[i]」这一段;起点 Node[0](出发站)的 via 永远不生效(expandRoute 从 i=1 开始取)。例:拉萨→日喀则 段的途经点必须写在日喀则节点上,写在拉萨节点上会被静默忽略——阿里 v1 就把羊湖线挂在拉萨上,导致 D3 段直线穿越没走羊湖
七步流水线
- 校验数据:逐站核对经纬度真实性与 GCJ-02 对齐;行程表 ↔ 站点表双向对账(每个站点都能在行程里找到对应日,里程加总=总里程)
- 下载底图:算所有站点经纬度包围盒 → 运行
scripts/download_tiles.py(见命令区),把输出打印的X0/Y0填入 constants 的ll()偏移 - 生成 constants.ts:复制
templates/constants.template.ts,替换VIDEO_CONFIG/COPY/MAP/L/ROUTE/ENDING - 复制渲染器:
templates/Video.tsx→ 项目 composition 目录(纯渲染器,数据全来自 constants,无需改) - 合成 BGM:
scripts/bgm_lotus.py --duration <视频秒数>,输出到public/assets/ - 渲染:
npx remotion render <id> out/<name>.mp4 --concurrency=1 - 抽帧验证:按时间轴抽关键帧逐项核对 → 通过后交付。交付规范(用户明确要求):成片完成后必须显式报告文件位置——给出绝对路径(含文件名)+ 大小 + 时长 + 分辨率,例如
D:\...\remotion-videos\out\ali-great-loop-v2.mp4(350MB · 77.5s · 1080×1920@30fps),并附简要修改/验收摘要
镜头 scale 推导规则(核心)
cam = {scale, cx, cy}:scale=底图像素→屏幕像素比;cx/cy=镜头中心世界坐标。
| 当日行程 | scale | 语义 |
|---|---|---|
| ≤50km(短程) | ≥1.5 | 特写放大,目的地占画幅 ≥40% |
| 50–150km(中程) | 0.9–1.35 | 中观,看得到路线走向 |
| ≥300km(远程) | ≤0.62 | 大视角全景,整段路线入画 |
- 镜头中心
cx/cy:短程=本站点坐标;远程=段两端点中点(让整段入画) - 水平 clamp:
cx ≤ screenCenterX / scale(防镜头中心超出底图右边界露黑边,波密/墨脱实测踩坑) - 镜头在段间平滑插值(easeInOut),行进动画 ≥3s(96 帧)
校验门(交付前强制;极简模式共五道)
- 坐标门:所有站点在底图范围内;GCJ-02 与瓦片投影一致(ll() 偏移 = 脚本打印的 X0/Y0)
- 索引门:Video.tsx 中
segStartIdx = segmentIndexInExp[i-1] + (ROUTE[i-1].via.length || 0)—— 必须补 via 偏移,否则段起点回撤到上一段 via(v5 实测 bug) - 时长门:BGM 时长 ≥ 视频时长 + 0.3s
- 画幅门:目的地占画幅 ≥40%;短程特写 / 远程全景;每段行进 ≥3s
- 落图门(v2 后强制):渲染完成后抽关键帧人眼核对——① POI 是否落在合理地形(县城在河谷、垭口在山脊、湖在湖岸);② 路线是否贴合底图真实道路(重点看山口/环湖/跨江段,折线穿山即为 via 缺失或顺序错误);③ 地名清晰可读(站点标签 ≥46px、途经点名 ≥22px);④ 卡通小车可见醒目。任一不满足即回改重渲染。
常用命令
PY="<本机 venv python 路径>"
# 1. 下载底图(参数=路线包围盒;X0/Y0 打印值填入 constants)
"$PY" scripts/download_tiles.py --lon-min 91 --lon-max 96 --lat-min 28.9 --lat-max 30.1 \
--zoom 10 --out public/assets/base-map-real.jpg
# 2. BGM(时长=视频秒数,自动对齐并循环旋律模式)
"$PY" scripts/bgm_lotus.py --duration 54.6 --out public/assets/bgm-lotus.wav
# 3. 类型检查 + 渲染
npx tsc --noEmit && npx remotion render <compId> out/<name>.mp4 --concurrency=1
# 4. 抽帧(用 ffmpeg-static 路径;按时间轴取关键帧)
node extract-frames.cjs
经验教训(防踩坑清单)
- 底图:瓦片 URL 用
webst0X.is.autonavi.com/appmaptile?style=6(style=7/8 是占位图,webrd01 超时);校验 JPEG magic 且 >3000B,8 host 轮换防限流(荒漠瓦片可小至 1854B,阈值别设太高;部分瓦片是 PNG 魔数\x89P,识别时两种都要认) - 行进子路径:车只跑当前段
expPts.slice(s, e+1),不是完整累积路径(v4 bug) - 屏幕恒定:POI 标签/线宽/小车要
1/cam.scale反缩放,任意镜头下保持清晰 - 底图提亮:压暗底图 + CSS
brightness(1.45) saturate(1.2)组合,避免画面过暗(v5 实测) - 地图区画幅:竖版地图显示区建议 240–1200(约 50% 画幅),上下留黑避免与全屏景图割裂
- 片头精简:≤2.5s,标题+副标题即可,不要拼音行
- 到达全屏景图:2.5s 全幅真图 + 底部信息条(DAY/日期/海拔/tagline/下一站里程),替代小图卡
- 收尾:统计 3.5s 后,可加 5s 全屏目的地真图 + 中央一行字(如「行程结束,拉萨开始自由安排」)
- via 归属语义(阿里 v2 教训):
Node[i].via属于「i-1 → i」段,起点 Node[0].via 永远不生效;每段途经点必须按真实公路拐点排(翻哪座山口、过哪个镇),宁多勿少,路线才贴路 - 路线贴路(阿里 v2 修复):路径用直线 + 拐角圆角(
cornerR = sw(26),quadratic 过渡),不要用 cubic-bezier 控制点——控制点会飘离道路,折线在卫星底图上显「飞线」。途经点足够密(每段 ≥3 个)时圆角折线视觉上即贴合道路 - 地名字号规范(阿里 v2 修复):地图站点标签当前站 46px / 已过站 34px;途经点当前段显示名 22px 白字黑描边;photo 全屏景图站名 100px、tagline 40px、景点 chip 30px、副标题 26px。竖版 1080 宽下这些是「刷手机视频流能看清」的下限
- 卡通小车(阿里 v2 修复):车身 1.5x 放大 + 白色描边光晕,卫星底图上才醒目;加车尾尘土循环 + 行进颠簸增强存在感
- BGM 文件名:从
constants的BGM常量引用(模板已内置assets/bgm-lotus.wav),渲染器不写死文件名,换路线只改 constants - TDZ 陷阱(阿里 v2 全量渲染崩溃教训):
const sw = useMemo(() => cam.scale * swBase, [cam])这种「被 useMemo 回调里又调用」的派生函数,必须先声明 sw,再声明引用 sw 的 useMemo。useMemo回调本身不一定首帧执行,但跨 phase 重渲染时(如 travel→photo→outro 触发 curSegPath 重算)会进入回调——此时若 sw 还在 TDZ 抛Cannot access 'sw' before initialization。TS 编译无法发现,必须定点冒烟 frames 170-320 等 phase 切换点。模板 Video.tsx 与项目端必须同步修复 - 渲染输出文件不要
rm -f(阿里 v2.1 启动失败教训):WorkBuddy 后台 Bash 对rm -f out/<old>.mp4做了 safe-delete 包装(genie-trash + PowerShell COM fallback),文件实际已不存在时 fallback 失败 →SAFE_DELETE_FAIL_CLOSED→ 整个渲染 abort。正确做法:用mv out/<old>.mp4 out/<old>-backup-<reason>.mp4改名旧文件,再启动npx remotion render。或者直接换新输出文件名(-v2.1.mp4) - 抽帧工具路径(验收必备):Git Bash 默认无
ffmpeg命令,bash命令里调ffmpeg会直接 exit 127。Remotion 项目自带node_modules/ffmpeg-static/ffmpeg.exe,验收脚本必须用绝对路径./node_modules/ffmpeg-static/ffmpeg.exe。这与抽帧select='eq(n,N)'配合抽关键帧 - 抽帧验收的时间轴计算(避免误判):每个 phase 有明确帧数(75/96/75/171×11/105/150),抽帧前先算出
outroStart = intro + travel0 + photo + 11×171,否则会把其他 phase 的瞬间帧误读为该 phase 的 bug。outro 阶段数字滚动(dayVal/stopVal/kmVal)是 process value——抽帧定格可能看到「13 天 11 站 2391 km」之类的过程值,不是终态错误。终态要看 phase 末帧(如 outro 末帧 = outroStart + outroFrames - 1) - 项目端与模板同步修复:用户指令修改(如「3 晚瑞吉」「15 日 · 9.5 — 9.18」「角标动态化」)必须项目 + 模板两端都改。模板的硬编码在用户没要求改具体文案时容易漏;下次复用模板又会重复错
- 真图选择要语义对齐(圣象天门 → 纳木错教训):「找一张纳木错真图」+ 班戈站 attractions 改成「纳木错」,意味着班戈站的
image字段必须从「圣象天门拱门图」(哪怕地理上就在纳木错北岸)换成「通用纳木错湖景图」。视觉特征与文字标签必须语义对齐,否则观众会觉得"图与字不符" - 交付必须显示文件位置(用户明确要求):渲染完成后回复用户时,第一条信息就是成片的绝对路径(含文件名),紧跟大小/时长/分辨率;备份文件、抽帧图等辅助产物也一并列出路径。用户要能直接复制路径去打开文件,不许只丢一句「做好了」