소스 정보
- 저장소
- MatrixReligio/ProductVideoCreator
- 최근 소스 활동
- 2026년 1월 26일 04:35
- 감지된 SKILL.md 언어
- 중국어
- 스타
- 41
- 포크
- 8
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
SOC 직업 분류 기준
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/MatrixReligio/ProductVideoCreator --skill subtitles명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
| 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 值 |
| 字幕遮挡内容 | 使用动态位置调整 |