with one click
subtitles
自动生成视频字幕。解析配音文案,生成同步字幕,使用 Remotion 组件渲染。支持中英文双语字幕。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
自动生成视频字幕。解析配音文案,生成同步字幕,使用 Remotion 组件渲染。支持中英文双语字幕。
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
为视频添加背景音乐。支持免版权音乐来源、音量混合、淡入淡出效果。当需要为视频添加背景音乐、调整音乐与配音音量平衡时使用。
使用 Remotion 合成最终视频。当需要将片头、录屏、配音、片尾组合成完整视频时使用。包含动画效果、时间线管理、多尺寸模板和故障处理。
使用 edge-tts 生成多语言配音(中文/英文)。当需要为视频生成语音旁白、基于时间线同步配音时使用。支持语速调整、多种声音选择和配音验证。
从公开网站收集视频素材(图片、Logo、图标)。当需要为图文视频准备素材时使用。包含免版权图片来源、下载规范和素材组织方式。
产品介绍视频自动化生成。当用户需要创建产品演示视频、公司介绍短片、宣传片时使用。支持录屏演示型、图文展示型、混合型三种视频类型。
使用 Playwright 进行浏览器自动化录屏。当需要录制 Web 应用操作演示、生成带时间线的屏幕录像时使用。
| name | subtitles |
| description | 自动生成视频字幕。解析配音文案,生成同步字幕,使用 Remotion 组件渲染。支持中英文双语字幕。 |
| argument-hint | ["配音元数据文件路径"] |
项目提供了可复用的字幕组件模板:
// 从模板导入
import { SubtitleDisplay, createSubtitlesFromMetadata } from "./components/SubtitleDisplay";
import type { Subtitle } from "./config/types";
模板位置:templates/components/SubtitleDisplay.tsx
字幕时间必须基于 voiceover_metadata.json 的 actual_duration(实际时长),而非 target_duration(目标时长)。
// ✅ 正确:使用 actual_duration 计算 end 时间
const SUBTITLES: Subtitle[] = [
{
start: segment.start_time,
end: segment.start_time + segment.actual_duration, // 使用实际时长
text: segment.text
},
];
// ❌ 错误:使用 target_duration(可能导致字幕与音频不同步)
const SUBTITLES: Subtitle[] = [
{
start: segment.start_time,
end: segment.start_time + segment.target_duration, // 不要这样做
text: segment.text
},
];
import { validateSubtitleTiming } from "./components/SubtitleDisplay";
const { valid, warnings } = validateSubtitleTiming(SUBTITLES, VIDEO_DURATION);
if (!valid) {
console.warn("字幕时间警告:", warnings);
}
配音元数据 (voiceover_metadata.json)
↓
解析文案和时间线
↓
生成字幕数据 (subtitles.json)
↓
Remotion 字幕组件渲染
{
"voice": "zh-CN-YunjianNeural",
"total_duration": 85,
"segments": [
{
"index": 0,
"start_time": 0.5,
"target_duration": 7.0,
"actual_duration": 6.3,
"text": "1993年,黄仁勋创立NVIDIA...",
"language": "zh"
}
]
}
{
"subtitles": [
{
"id": 0,
"startFrame": 15,
"endFrame": 204,
"text": "1993年,黄仁勋创立NVIDIA",
"language": "zh"
}
],
"fps": 30,
"style": {
"fontSize": 48,
"fontFamily": "Noto Sans SC",
"color": "#FFFFFF",
"backgroundColor": "rgba(0, 0, 0, 0.7)",
"position": "bottom"
}
}
#!/usr/bin/env python3
"""
字幕生成脚本 - 从配音元数据生成字幕
"""
import json
from pathlib import Path
FPS = 30
MAX_CHARS_PER_LINE = 20 # 中文每行最大字符数
MAX_WORDS_PER_LINE = 8 # 英文每行最大单词数
def split_text(text: str, language: str = "zh") -> list:
"""
将长文本拆分成多行
"""
if language == "zh":
# 中文按字符拆分
lines = []
current_line = ""
for char in text:
if len(current_line) >= MAX_CHARS_PER_LINE:
lines.append(current_line)
current_line = char
else:
current_line += char
if current_line:
lines.append(current_line)
return lines
else:
# 英文按单词拆分
words = text.split()
lines = []
current_line = []
for word in words:
if len(current_line) >= MAX_WORDS_PER_LINE:
lines.append(" ".join(current_line))
current_line = [word]
else:
current_line.append(word)
if current_line:
lines.append(" ".join(current_line))
return lines
def generate_subtitles(metadata_path: str, output_path: str):
"""
从配音元数据生成字幕文件
"""
with open(metadata_path, "r", encoding="utf-8") as f:
metadata = json.load(f)
subtitles = []
subtitle_id = 0
for segment in metadata["segments"]:
text = segment.get("full_text", segment.get("text", ""))
language = segment.get("language", "zh")
start_time = segment["start_time"]
duration = segment["actual_duration"]
# 拆分长文本
lines = split_text(text, language)
# 计算每行显示时长
line_duration = duration / len(lines) if lines else duration
for i, line in enumerate(lines):
line_start = start_time + i * line_duration
line_end = line_start + line_duration
subtitles.append({
"id": subtitle_id,
"startFrame": int(line_start * FPS),
"endFrame": int(line_end * FPS),
"text": line.strip(),
"language": language
})
subtitle_id += 1
output_data = {
"subtitles": subtitles,
"fps": FPS,
"style": {
"fontSize": 48,
"fontFamily": "Noto Sans SC" if metadata["segments"][0].get("language") == "zh" else "Inter",
"color": "#FFFFFF",
"backgroundColor": "rgba(0, 0, 0, 0.7)",
"position": "bottom",
"padding": "12px 24px",
"borderRadius": "8px"
}
}
with open(output_path, "w", encoding="utf-8") as f:
json.dump(output_data, f, ensure_ascii=False, indent=2)
print(f"✓ 生成 {len(subtitles)} 条字幕")
print(f"✓ 输出: {output_path}")
if __name__ == "__main__":
generate_subtitles(
metadata_path="public/audio/voiceover_metadata.json",
output_path="public/subtitles.json"
)
import React from "react";
import { useCurrentFrame, interpolate, Easing } from "remotion";
interface Subtitle {
id: number;
startFrame: number;
endFrame: number;
text: string;
language: string;
}
interface SubtitleStyle {
fontSize: number;
fontFamily: string;
color: string;
backgroundColor: string;
position: "top" | "bottom" | "center";
padding: string;
borderRadius: string;
}
interface SubtitleDisplayProps {
subtitles: Subtitle[];
style: SubtitleStyle;
}
export const SubtitleDisplay: React.FC<SubtitleDisplayProps> = ({
subtitles,
style,
}) => {
const frame = useCurrentFrame();
// 找到当前帧应显示的字幕
const currentSubtitle = subtitles.find(
(sub) => frame >= sub.startFrame && frame < sub.endFrame
);
if (!currentSubtitle) return null;
// 淡入淡出动画
const fadeInDuration = 6; // 0.2秒
const fadeOutDuration = 6;
const opacity = interpolate(
frame,
[
currentSubtitle.startFrame,
currentSubtitle.startFrame + fadeInDuration,
currentSubtitle.endFrame - fadeOutDuration,
currentSubtitle.endFrame,
],
[0, 1, 1, 0],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
easing: Easing.ease,
}
);
// 位置样式
const positionStyle: React.CSSProperties =
style.position === "top"
? { top: 60 }
: style.position === "center"
? { top: "50%", transform: "translateY(-50%)" }
: { bottom: 80 };
return (
<div
style={{
position: "absolute",
left: 0,
right: 0,
display: "flex",
justifyContent: "center",
...positionStyle,
}}
>
<div
style={{
opacity,
fontSize: style.fontSize,
fontFamily: style.fontFamily,
color: style.color,
backgroundColor: style.backgroundColor,
padding: style.padding,
borderRadius: style.borderRadius,
textAlign: "center",
maxWidth: "80%",
lineHeight: 1.4,
textShadow: "2px 2px 4px rgba(0,0,0,0.8)",
}}
>
{currentSubtitle.text}
</div>
</div>
);
};
import React from "react";
import { Composition, staticFile } from "remotion";
import { SubtitleDisplay } from "./components/SubtitleDisplay";
import subtitleData from "../public/subtitles.json";
export const FinalVideo: React.FC = () => {
return (
<>
{/* 其他视频内容 */}
{/* 字幕层(最上层) */}
<SubtitleDisplay
subtitles={subtitleData.subtitles}
style={subtitleData.style}
/>
</>
);
};
| 风格 | fontSize | fontFamily | 背景 | 适用场景 |
|---|---|---|---|---|
| 默认 | 48 | Noto Sans SC | rgba(0,0,0,0.7) | 通用 |
| 科技 | 42 | Inter | rgba(0,10,30,0.8) | 科技视频 |
| 简约 | 44 | sans-serif | transparent | 简洁风格 |
| 强调 | 56 | Noto Sans SC | #76B900 | 重点内容 |
// 双语字幕显示
interface BilingualSubtitle {
primary: string; // 主语言
secondary: string; // 次语言
}
export const BilingualSubtitleDisplay: React.FC<{
subtitles: BilingualSubtitle[];
}> = ({ subtitles }) => {
// ...
return (
<div style={{ textAlign: "center" }}>
<div style={{ fontSize: 48, marginBottom: 8 }}>
{currentSubtitle.primary}
</div>
<div style={{ fontSize: 32, opacity: 0.8 }}>
{currentSubtitle.secondary}
</div>
</div>
);
};
// 避免字幕遮挡重要内容
const getSubtitlePosition = (
frame: number,
scenes: Scene[]
): "top" | "bottom" => {
const currentScene = scenes.find(
(s) => frame >= s.startFrame && frame < s.endFrame
);
// 如果当前场景底部有重要内容,字幕显示在顶部
if (currentScene?.hasBottomContent) {
return "top";
}
return "bottom";
};
def export_srt(subtitles: list, output_path: str, fps: int = 30):
"""导出 SRT 格式字幕文件"""
def frame_to_timecode(frame: int) -> str:
total_seconds = frame / fps
hours = int(total_seconds // 3600)
minutes = int((total_seconds % 3600) // 60)
seconds = int(total_seconds % 60)
milliseconds = int((total_seconds % 1) * 1000)
return f"{hours:02d}:{minutes:02d}:{seconds:02d},{milliseconds:03d}"
with open(output_path, "w", encoding="utf-8") as f:
for i, sub in enumerate(subtitles, 1):
f.write(f"{i}\n")
f.write(f"{frame_to_timecode(sub['startFrame'])} --> {frame_to_timecode(sub['endFrame'])}\n")
f.write(f"{sub['text']}\n\n")
print(f"✓ SRT 导出: {output_path}")
| 问题 | 解决方案 |
|---|---|
| 字幕闪烁 | 增加 fadeInDuration/fadeOutDuration |
| 字幕太长 | 调整 MAX_CHARS_PER_LINE |
| 字体不显示 | 确保使用 @remotion/google-fonts 加载字体 |
| 字幕位置不对 | 调整 position 和 bottom/top 值 |
| 字幕遮挡内容 | 使用动态位置调整 |