OpenMontage 中 HeyGen 数字人视频与 Remotion 合成集成实战:从 /v2/video/generate 到帧精确渲染
2026/9/6 19:42:15 网站建设 项目流程

OpenMontage 中 HeyGen 数字人视频与 Remotion 合成集成实战:从 /v2/video/generate 到帧精确渲染

【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage

本篇基于 OpenMontage 仓库中 avatar-video 技能包下的参考文档 remotion-integration.md,系统讲解如何调用 HeyGen 生成数字人(avatar)视频,并将其以帧精确的方式嵌入 Remotion 合成工程中。读完后你将掌握:MP4 背景版与 WebM 透明版两种输出格式的选择依据、尺寸/帧率对齐方法、OffthreadVideo的帧精确渲染原理、calculateMetadata动态时长方案,以及完整的“生成—轮询—bundle—渲染”生产工作流。

1. 集成定位与整体工作流

该参考文档是 avatar-video 技能(SKILL.md)的“Integration”部分,专门回答一个问题:HeyGen 生成的数字人视频如何进入 Remotion 程序化合成流水线。在 OpenMontage 中,Remotion 工程位于 remotion-composer 目录,其Root.tsx注册了TalkingHeadExplainerCinematicRenderer等多个Composition,其中TalkingHead合成正是“数字人视频 + 叠加层 + 字幕”的三层结构,与本文档描述的合成模式一致。

典型工作流四步:

  1. 调用 HeyGen 生成数字人视频;
  2. 轮询状态直至完成,获取视频 URL;
  3. 下载视频,或直接在 Remotion 中引用 URL;
  4. 在 Remotion 中与其他元素(背景、叠加层、动效)合成。

快速上手

// 1. Get avatar with default voice const avatar = await getAvatarDetails(avatarId); // 2. Generate video (MP4 with background - most common) const videoId = await generateVideo({ video_inputs: [{ character: { type: "avatar", avatar_id: avatar.id, avatar_style: "normal" }, voice: { type: "text", input_text: script, voice_id: avatar.default_voice_id }, background: { type: "color", value: "#1a1a2e" }, }], dimension: { width: 1920, height: 1080 }, }); // 3. Poll for completion (10-15+ min) // 4. Use in Remotion with motion graphics overlaid on top

生成接口为POST https://api.heygen.com/v2/video/generate,请求需携带X-Api-Key头(技能包的 frontmatter 声明了环境变量要求HEYGEN_API_KEY)。完整的请求字段表、多场景视频与video_inputs结构见同目录参考文档 video-generation.md;轮询实现、状态类型与下载重试见 video-status.md。

注意区分:仓库中的 heygen_video.py 是一个以 HeyGen 为通道调度 VEO/Sora/Kling 等模型的云端视频生成工具,与本文讨论的 HeyGen 原生 avatar 接口是两回事;本地的照片驱动说话头工具则见 talking_head.py(基于 SadTalker)。avatar 集成工作流完全走 avatar-video 技能包的 HTTP API 路径。

2. 选择正确的输出格式:MP4 背景版 vs WebM 透明版

这是本集成中最关键的一个决策。参考文档给出的选择矩阵:

你的合成形态推荐格式原因
数字人作为主讲人,动效叠加在其上MP4 + 背景色更简单,叠加层直接放上层
Loom 风格(数字人悬浮于录屏之上)WebM +closeUp,Remotion 中加遮罩需要透明背景,用 CSS 做圆形遮罩
数字人叠加在其他视频/内容之上WebM(透明)需要“看穿”到数字人背后的内容
全屏数字人MP4 + 背景色标准做法

多数场景直接用带背景的 MP4;只有当需要透过数字人看到其背后内容时才用 WebM。两个端点的差异:

  • POST /v2/video/generate→ 输出 MP4,支持normal/closeUp/circle三种avatar_style
  • POST /v1/video.webm→ 输出透明 WebM,仅支持normalcloseUp(圆形取景需在 Remotion 中用 CSSborder-radius: 50%实现)。

/v1/video.webm/v2/video/generate的请求结构不同:前者是扁平字段(avatar_pose_idavatar_styleinput_textvoice_id等),字段约束与“input_text+voice_id二选一搭配input_audio”的规则见 video-generation.md 的 WebM 章节。

3. 并行开发工作流:不要干等 10–15 分钟

HeyGen 视频生成通常需要10–15 分钟以上(video-status.md 给出的超时建议是 15–20 分钟)。文档推荐的工作方式是提交即退出、并行开发

  1. 启动 HeyGen 生成— 把video_id存到文件,进程立即退出(video-status.md中的“Resumable Status Checking”章节给出了pending-video.json存状态 + 稍后查询的完整实现);
  2. 搭建 Remotion 合成— 使用占位视频,或数字人的preview_video_url(一段短循环片段);
  3. 定期查询 HeyGen 状态— 构建完成后或周期性轮询;
  4. 就绪后替换占位— 换成真实视频 URL。

两个实用的工程技巧:

  • 按文稿估算时长:按约 150 词/分钟的语速,wordCount / 150 * 60 * fps即可得到近似的帧数,用于在视频就绪前就搭好合成骨架;
  • 组件解耦设计:让组件在“有/没有数字人视频”两种状态下都能工作,这样动效部分可以独立测试与预览。

4. 尺寸对齐:HeyGen 与 Remotion 共用一份常量

关键原则:HeyGen 的输出尺寸必须与 Remotion 合成完全一致。文档给出共享的维度常量:

// Shared dimension constants for both HeyGen and Remotion const DIMENSIONS = { landscape_1080p: { width: 1920, height: 1080 }, landscape_720p: { width: 1280, height: 720 }, portrait_1080p: { width: 1080, height: 1920 }, portrait_720p: { width: 720, height: 1280 }, square_1080p: { width: 1080, height: 1080 }, square_720p: { width: 720, height: 720 }, } as const; type DimensionPreset = keyof typeof DIMENSIONS;

这套预设与技能包 dimensions.md 的官方分辨率表一致:横屏 16:9(1280×720 / 1920×1080)、竖屏 9:16(720×1280 / 1080×1920)、方形 1:1(720×720 / 1080×1080)。

对应到 Remotion 侧的Root.tsx注册:

// remotion/src/Root.tsx import { Composition } from "remotion"; import { AvatarComposition } from "./AvatarComposition"; export const RemotionRoot: React.FC = () => { return ( <> <Composition id="AvatarVideo" component={AvatarComposition} durationInFrames={300} // Will be set dynamically fps={30} width={DIMENSIONS.landscape_1080p.width} height={DIMENSIONS.landscape_1080p.height} defaultProps={{ avatarVideoUrl: "" }} /> </> ); };

仓库内的真实工程印证了这个模式:Root.tsx 注册TalkingHead合成时采用竖屏1080×1920fps={30},而 TalkingHead.tsx 内部的POSITION_STYLESlower_thirdleft_panel等位置预设)正是按 9:16 的 1080×1920 画布坐标设计的——尺寸常量一旦错位,所有像素级定位都会跟着错位。

5. 生成带背景的 MP4:标准生成函数

async function generateHeyGenVideo( script: string, avatarId: string, voiceId: string, preset: DimensionPreset ): Promise<string> { const dimension = DIMENSIONS[preset]; const response = await fetch("https://api.heygen.com/v2/video/generate", { method: "POST", headers: { "X-Api-Key": process.env.HEYGEN_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ video_inputs: [ { character: { type: "avatar", avatar_id: avatarId, avatar_style: "normal", }, voice: { type: "text", input_text: script, voice_id: voiceId, }, background: { type: "color", value: "#00FF00", // Green screen for compositing }, }, ], dimension, }), }); const { data } = await response.json(); return data.video_id; }

封装成面向 Remotion 的生成函数时,把风格与背景色参数化:

async function generateAvatarForRemotion( script: string, avatarId: string, voiceId: string, options: { style?: "normal" | "closeUp" | "circle"; backgroundColor?: string; } = {} ): Promise<string> { const { style = "normal", backgroundColor = "#1a1a2e" } = options; const response = await fetch("https://api.heygen.com/v2/video/generate", { method: "POST", headers: { "X-Api-Key": process.env.HEYGEN_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ video_inputs: [{ character: { type: "avatar", avatar_id: avatarId, avatar_style: style, }, voice: { type: "text", input_text: script, voice_id: voiceId, }, background: { type: "color", value: backgroundColor }, }], dimension: { width: 1920, height: 1080 }, }), }); const { data } = await response.json(); return data.video_id; }

请求字段补充说明(源自 video-generation.md 的字段表):

  • character.avatar_style可取"normal"/"closeUp"/"circle"
  • voice.type"text"外还支持"audio"(需audio_url)与"silence"(需duration);speed取值 0.5–2.0,pitch取值 -20 到 20;
  • background.type"color"外还支持"image"/"video"(用url+fit字段);
  • 顶层还支持test: true测试模式(不扣额度、带水印)、caption自动字幕、callback_urlWebhook 通知——生产工作流建议开发阶段一律开测试模式。

6. 透明背景 WebM:/v1/video.webm端点

只有在需要“看到数字人背后的内容”时才用 WebM(例如数字人悬浮在录屏之上):

// Use /v1/video.webm endpoint for transparent background // Note: Different structure than /v2/video/generate const response = await fetch("https://api.heygen.com/v1/video.webm", { method: "POST", headers: { "X-Api-Key": process.env.HEYGEN_API_KEY!, "Content-Type": "application/json", }, body: JSON.stringify({ avatar_pose_id: avatarPoseId, // Required: avatar pose ID avatar_style: "normal", // Required: "normal" or "closeUp" only input_text: script, // Required (with voice_id) voice_id: voiceId, // Required (with input_text) dimension: { width: 1920, height: 1080 }, }), });

注意avatar_pose_id来自数字人详情接口(GET /v2/avatar/{avatar_id}/details),avatar_style只有normal/closeUp两个合法值。WebM 与 MP4 共用同一状态查询端点GET /v2/videos/{video_id},完成后video_url指向.webm文件。

7. 在 Remotion 中使用 HeyGen 视频:务必使用 OffthreadVideo

核心结论:数字人视频必须用OffthreadVideo而非Video基础Video组件依赖浏览器解码器,渲染时并非帧精确,会产生抖动(jitter);OffthreadVideo通过 FFmpeg 逐帧抽帧,得到平滑、帧精确的输出。它包含在remotion核心包中,无需额外安装。

仓库源码可以直接印证这一实践:TalkingHead.tsx 的第一层就是OffthreadVideo,并且素材先经过resolveAsset(videoSrc)归一化为本地路径(对应后文“先下载再用”策略):

// Layer 1: Video background <OffthreadVideo src={resolveAsset(videoSrc)} style={{ width: "100%", height: "100%", objectFit: "cover" }} />

基本用法

// remotion/src/AvatarComposition.tsx import { OffthreadVideo, useVideoConfig } from "remotion"; interface AvatarCompositionProps { avatarVideoUrl: string; } export const AvatarComposition: React.FC<AvatarCompositionProps> = ({ avatarVideoUrl, }) => { return ( <div style={{ flex: 1, backgroundColor: "#1a1a2e" }}> <OffthreadVideo src={avatarVideoUrl} style={{ width: "100%", height: "100%", objectFit: "contain", }} /> </div> ); };

WebM 透明背景(三层结构)

使用/v1/video.webm产物时不需要任何色度键(chroma key)处理,加transparent属性即可:

import { OffthreadVideo, AbsoluteFill, Sequence } from "remotion"; export const AvatarWithMotionGraphics: React.FC<{ avatarWebmUrl: string }> = ({ avatarWebmUrl }) => { return ( <AbsoluteFill> {/* Layer 1: Your background/content */} <AbsoluteFill style={{ backgroundColor: "#1a1a2e" }}> <YourMotionGraphics /> </AbsoluteFill> {/* Layer 2: Avatar with transparent background */} <OffthreadVideo src={avatarWebmUrl} transparent style={{ position: "absolute", bottom: 0, right: 0, width: "50%", height: "auto", }} /> {/* Layer 3: Overlays on top of avatar */} <Sequence from={30}> <AnimatedTitle text="Welcome!" /> </Sequence> </AbsoluteFill> ); };

Loom 风格:圆形数字人悬浮于录屏之上

closeUp风格 + WebM,圆形取景在 Remotion 侧用 CSS 实现:

import { OffthreadVideo, AbsoluteFill } from "remotion"; export const LoomStyleComposition: React.FC<{ screenRecordingUrl: string; avatarWebmUrl: string; // Generated with avatar_style: "closeUp" via /v1/video.webm }> = ({ screenRecordingUrl, avatarWebmUrl }) => { return ( <AbsoluteFill> {/* Screen recording fills the frame */} <OffthreadVideo src={screenRecordingUrl} style={{ width: "100%", height: "100%" }} /> {/* Avatar with circular mask - transparent bg shows screen behind */} <OffthreadVideo src={avatarWebmUrl} transparent style={{ position: "absolute", bottom: 40, left: 40, width: 180, height: 180, borderRadius: "50%", // Circular mask applied in CSS overflow: "hidden", objectFit: "cover", }} /> </AbsoluteFill> ); };

再次强调:WebM 不支持circle风格,圆形必须靠borderRadius: "50%"遮罩。

分层合成(背景图 + 数字人 + 标题 + Logo)

import { OffthreadVideo, Sequence, useVideoConfig, Img } from "remotion"; interface LayeredAvatarProps { avatarVideoUrl: string; backgroundUrl: string; logoUrl: string; title: string; } export const LayeredAvatarComposition: React.FC<LayeredAvatarProps> = ({ avatarVideoUrl, backgroundUrl, logoUrl, title, }) => { const { fps } = useVideoConfig(); return ( <div style={{ position: "relative", width: "100%", height: "100%" }}> {/* Layer 1: Background */} <Img src={backgroundUrl} style={{ position: "absolute", width: "100%", height: "100%", objectFit: "cover", }} /> {/* Layer 2: Avatar video - use OffthreadVideo to prevent jitter */} <OffthreadVideo src={avatarVideoUrl} style={{ position: "absolute", bottom: 0, right: 0, width: "40%", height: "auto", }} /> {/* Layer 3: Title (appears after 1 second) */} <Sequence from={fps}> <div style={{ position: "absolute", top: 50, left: 50, color: "white", fontSize: 48, fontWeight: "bold", }}>{title}</div> </Sequence> {/* Layer 4: Logo */} <Img src={logoUrl} style={{ position: "absolute", top: 20, right: 20, width: 100, height: "auto", }} /> </div> ); };

这种“底层视频 + 中层人物 + 上层动效”的三层结构,与 TalkingHead.tsx 的实现完全同构:Layer 1 是OffthreadVideo视频底,Layer 2 是带Sequence时间轴的叠加卡片(通过in_seconds/out_seconds换算成帧),Layer 3 是最顶层的字幕CaptionOverlay。仓库还实现了lower_third/upper_third/left_panel/right_panel/full_overlay五种位置预设,每个叠加层带 8 帧淡入淡出——这些细节正是“叠加层放在数字人视频之上”这一思路的完整落地。

旧方案:绿幕 + 色度键(不推荐)

若你手里只有绿底 MP4,文档给出的“旧方案”仅是基础混合模式:

// Note: True chroma key requires WebGL or post-processing // WebM transparent background is much simpler <OffthreadVideo src={avatarVideoUrl} style={{ mixBlendMode: "multiply" }} // Basic compositing only />

文档明确建议:真正的色度键需要 WebGL 或后处理,优先改用 WebM 透明背景。

8. 完整工作流:生成、轮询、Bundle、渲染

把生成与渲染串起来的端到端函数:

import { bundle } from "@remotion/bundler"; import { renderMedia, selectComposition } from "@remotion/renderer"; async function generateAvatarVideoForRemotion( script: string, outputPath: string ) { // 1. Generate HeyGen video console.log("Generating HeyGen avatar video..."); const videoId = await generateHeyGenVideo( script, "josh_lite3_20230714", "1bd001e7e50f421d891986aad5158bc8", "landscape_1080p" ); // 2. Wait for completion console.log("Waiting for HeyGen video..."); const avatarVideoUrl = await waitForVideo(videoId); console.log(`HeyGen video ready: ${avatarVideoUrl}`); // 3. Get video duration for Remotion const avatarDuration = await getVideoDuration(avatarVideoUrl); const durationInFrames = Math.ceil(avatarDuration * 30); // 30 fps // 4. Bundle Remotion project console.log("Bundling Remotion project..."); const bundleLocation = await bundle({ entryPoint: "./remotion/src/index.ts", }); // 5. Select composition const composition = await selectComposition({ serveUrl: bundleLocation, id: "AvatarVideo", inputProps: { avatarVideoUrl }, }); // 6. Render final video console.log("Rendering final composition..."); await renderMedia({ composition: { ...composition, durationInFrames }, serveUrl: bundleLocation, codec: "h264", outputLocation: outputPath, inputProps: { avatarVideoUrl }, }); console.log(`Final video rendered: ${outputPath}`); return outputPath; }

其中waitForVideo即 video-status.md 中的轮询实现:每 5 秒查询一次GET /v2/videos/{video_id},状态为pending/processing时继续等待,completed时返回video_urlfailed时抛出failure_message。状态机四态(pendingprocessingcompleted/failed)以及“状态显示 completed 后 URL 可能还不可立即访问、下载要带指数退避重试”的细节都在该文档中。

动态时长:calculateMetadata

数字人视频的实际时长取决于文稿长度,硬编码durationInFrames并不合理。Remotion 的calculateMetadata可以在渲染前根据inputProps动态计算合成元数据:

// remotion/src/AvatarComposition.tsx import { CalculateMetadataFunction } from "remotion"; export const calculateAvatarMetadata: CalculateMetadataFunction< AvatarCompositionProps > = async ({ props }) => { // Fetch video duration from HeyGen video const duration = await getVideoDurationInSeconds(props.avatarVideoUrl); return { durationInFrames: Math.ceil(duration * 30), fps: 30, width: 1920, height: 1080, }; }; // In Root.tsx <Composition id="AvatarVideo" component={AvatarComposition} calculateMetadata={calculateAvatarMetadata} defaultProps={{ avatarVideoUrl: "" }} />

这个模式在仓库真实工程中同样是标准做法:Root.tsx 中Explainer合成的calculateMetadata依据cuts的最后一个out_seconds计算durationInFrames(外加 1 秒收尾淡出),CinematicRendererTitledVideo也各自挂载了calculateCinematicMetadatacalculateTitledVideoMetadata——可见“按素材动态算时长”是 OpenMontage Remotion 工程的一贯范式。

9. 最佳实践

9.1 用绿幕换取合成灵活性

希望后期合成时,生成阶段直接给绿底:

background: { type: "color", value: "#00FF00", // Pure green for chroma key }

(配合 7 节末尾的说明:真色度键成本较高,能走 WebM 透明背景就走 WebM。)

9.2 帧率对齐:HeyGen 默认 25 fps

HeyGen 输出默认 25 fps,Remotion 合成帧率与之不一致时需要显式处理:

// Option 1: Match HeyGen's 25 fps fps: 25 // Option 2: Use 30 fps with playback rate adjustment <OffthreadVideo src={avatarVideoUrl} playbackRate={25/30} // Slow down slightly to match />

9.3 URL 直连 vs 先下载

直接用 URL的适用条件:Remotion Studio 预览(npm run dev)、URL 在渲染完成前不会过期、追求开发期快速迭代:

// Direct URL usage - simpler, faster for dev <OffthreadVideo src={avatarVideoUrl} />

先下载的适用条件:HeyGen 的 URL 约24 小时后过期;渲染会延后或重复进行;网络可靠性存疑;需要离线渲染。带指数退避的下载实现:

// Download with retry for reliability async function downloadVideoWithRetry( url: string, outputPath: string, maxRetries = 5 ): Promise<string> { for (let attempt = 0; attempt < maxRetries; attempt++) { try { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); const buffer = await response.arrayBuffer(); await fs.promises.writeFile(outputPath, Buffer.from(buffer)); return outputPath; } catch (error) { const delay = 2000 * Math.pow(2, attempt); console.log(`Retry ${attempt + 1}/${maxRetries} in ${delay}ms...`); await new Promise((r) => setTimeout(r, delay)); } } throw new Error("Download failed after retries"); } // Use local file in Remotion const localPath = await downloadVideoWithRetry(avatarVideoUrl, "./public/avatar.mp4");

混合方案(生产推荐):元数据中同时保存 URL 与本地路径,本地优先:

// Save both URL and local path in metadata const metadata = { videoUrl: result.video_url, // For quick preview localPath: "./public/avatar.mp4", // For reliable rendering expiresAt: Date.now() + 24 * 60 * 60 * 1000, // URL expiration }; // In Remotion component, prefer local if available const videoSrc = fs.existsSync(localPath) ? staticFile("avatar.mp4") : avatarVideoUrl;

resolveAsset.ts 在仓库中扮演的正是“URL/本地路径归一化”的角色:TalkingHead合成入口的resolveAsset(videoSrc)保证渲染时拿到的始终是可访问的资源引用。

9.4 数字人位置预设

const AVATAR_POSITIONS = { fullscreen: { width: "100%", height: "100%", position: "center" }, bottomRight: { width: "40%", bottom: 0, right: 0 }, bottomLeft: { width: "40%", bottom: 0, left: 0 }, pictureInPicture: { width: "25%", bottom: 20, right: 20 }, leftThird: { width: "33%", left: 0, height: "100%" }, };

10. 输出格式与质量参数

  • HeyGen 输出:MP4(H.264),音频 AAC,分辨率即请求时指定的dimension
  • Remotion 输出:可选 H.264(默认)、VP8、VP9、ProRes,质量设置应不低于 HeyGen 源:
await renderMedia({ codec: "h264", crf: 18, // High quality // ... });

11. 故障排查

视频在 Remotion 中不播放

  1. 检查 URL 可访问性(CORS 问题);
  2. 确认视频格式兼容;
  3. 先下载到本地再试。

尺寸不匹配HeyGen 与 Remotion 必须使用完全相同的尺寸,建议共用一份配置:

// Shared config const VIDEO_CONFIG = { width: 1920, height: 1080, fps: 30 }; // HeyGen dimension: { width: VIDEO_CONFIG.width, height: VIDEO_CONFIG.height } // Remotion <Composition width={VIDEO_CONFIG.width} height={VIDEO_CONFIG.height} />

渲染时视频抖动

  1. OffthreadVideo替换Video——基础Video使用浏览器解码器,非帧精确;
  2. 只需改导入(核心包已包含,无需额外安装):
// Before (causes jitter) import { Video } from "remotion"; // After (frame-accurate) import { OffthreadVideo } from "remotion";
  1. 透明 WebM 记得加transparent属性:<OffthreadVideo src={avatarWebmUrl} transparent />

音画不同步(音频漂移)

  • 核对源视频帧率;
  • 检查是否存在编码问题;
  • 考虑用一致参数重新编码。

12. 小结:把该文档放回仓库语境

这篇参考文档在 avatar-video 技能中承担“Integration”角色,与技能包其他参考文档形成完整闭环:avatars.md 选数字人与默认音色、video-generation.md 管请求构造、video-status.md 管轮询与下载、dimensions.md 管分辨率,而本文聚焦的 remotion-integration 则负责“最后一公里”——把云生成的 MP4/WebM 以帧精确、透明安全、尺寸一致的方式嵌进 Remotion 程序化合成。OpenMontage 的 remotion-composer 工程(OffthreadVideo底座、位置预设叠加层、calculateMetadata动态时长)与 talking_head.py 本地工具共同构成了数字人视频从生成到合成的完整技术栈,照本文档的步骤即可复制出一套可运行的 avatar 成片流水线。

【免费下载链接】OpenMontageWorld's first open-source, agentic video production system. 12 production pipelines, 100+ tools, 700+ agent skill and production-knowledge files. Turn your AI coding assistant into a full video production studio.项目地址: https://gitcode.com/GitHub_Trending/op/OpenMontage

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询