This commit is contained in:
Kevin Wong
2026-02-11 13:48:45 +08:00
parent e33dfc3031
commit 96a298e51c
282 changed files with 93514 additions and 461 deletions

View File

@@ -146,22 +146,27 @@ async function main() {
publicDir,
});
// 统一 inputProps包含视频尺寸供 calculateMetadata 使用
const inputProps = {
videoSrc: videoFileName,
captions,
title: options.title,
titleDuration: options.titleDuration || 3,
subtitleStyle: options.subtitleStyle,
titleStyle: options.titleStyle,
enableSubtitles: options.enableSubtitles !== false,
width: videoWidth,
height: videoHeight,
};
// Select the composition
const composition = await selectComposition({
serveUrl: bundleLocation,
id: 'ViGentVideo',
inputProps: {
videoSrc: videoFileName,
captions,
title: options.title,
titleDuration: options.titleDuration || 3,
subtitleStyle: options.subtitleStyle,
titleStyle: options.titleStyle,
enableSubtitles: options.enableSubtitles !== false,
},
inputProps,
});
// Override duration and dimensions
// Override duration and dimensions (保险:确保与 ffprobe 检测值一致)
composition.durationInFrames = durationInFrames;
composition.fps = fps;
composition.width = videoWidth;
@@ -174,15 +179,7 @@ async function main() {
serveUrl: bundleLocation,
codec: 'h264',
outputLocation: options.outputPath,
inputProps: {
videoSrc: videoFileName,
captions,
title: options.title,
titleDuration: options.titleDuration || 3,
subtitleStyle: options.subtitleStyle,
titleStyle: options.titleStyle,
enableSubtitles: options.enableSubtitles !== false,
},
inputProps,
onProgress: ({ progress }) => {
const percent = Math.round(progress * 100);
process.stdout.write(`\rRendering: ${percent}%`);

View File

@@ -14,8 +14,12 @@ export const RemotionRoot: React.FC = () => {
component={Video}
durationInFrames={300} // 默认值,会被 render.ts 覆盖
fps={25}
width={1280}
height={720}
width={1080}
height={1920}
calculateMetadata={async ({ props }) => ({
width: props.width || 1080,
height: props.height || 1920,
})}
defaultProps={{
videoSrc: '',
audioSrc: undefined,
@@ -23,6 +27,8 @@ export const RemotionRoot: React.FC = () => {
title: undefined,
titleDuration: 3,
enableSubtitles: true,
width: 1080,
height: 1920,
}}
/>
</>

View File

@@ -14,6 +14,8 @@ export interface VideoProps {
enableSubtitles?: boolean;
subtitleStyle?: SubtitleStyle;
titleStyle?: TitleStyle;
width?: number;
height?: number;
}
/**

View File

@@ -6,23 +6,19 @@ import {
getCurrentWordIndex,
} from '../utils/captions';
/**
* 字幕样式接口
* 使用 snake_case 与 Python 后端保持一致
*/
export interface SubtitleStyle {
font_file?: string;
fontFamily?: string;
font_family?: string;
fontSize?: number;
font_size?: number;
highlightColor?: string;
highlight_color?: string;
normalColor?: string;
normal_color?: string;
strokeColor?: string;
stroke_color?: string;
strokeSize?: number;
stroke_size?: number;
letterSpacing?: number;
letter_spacing?: number;
bottomMargin?: number;
bottom_margin?: number;
}
@@ -55,7 +51,7 @@ const buildTextShadow = (color: string, size: number) => {
export const Subtitles: React.FC<SubtitlesProps> = ({ captions, style }) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { fps, width } = useVideoConfig();
const currentTimeInSeconds = frame / fps;
@@ -69,15 +65,27 @@ export const Subtitles: React.FC<SubtitlesProps> = ({ captions, style }) => {
// 获取当前高亮字的索引
const currentWordIndex = getCurrentWordIndex(currentSegment, currentTimeInSeconds);
// 使用统一的 snake_case 属性名称
const fontFile = style?.font_file;
const fontFamily = style?.fontFamily || style?.font_family;
const fontSize = style?.fontSize || style?.font_size || 52;
const highlightColor = style?.highlightColor || style?.highlight_color || '#FFFF00';
const normalColor = style?.normalColor || style?.normal_color || '#FFFFFF';
const strokeColor = style?.strokeColor || style?.stroke_color || '#000000';
const strokeSize = style?.strokeSize || style?.stroke_size || 3;
const letterSpacing = style?.letterSpacing || style?.letter_spacing || 2;
const bottomMargin = style?.bottomMargin || style?.bottom_margin;
const fontFamily = style?.font_family;
const widthScale = Math.min(1, width / 1080);
const responsiveScale = Math.max(0.55, widthScale);
const baseFontSize = style?.font_size ?? 52;
const baseStrokeSize = style?.stroke_size ?? 3;
const baseLetterSpacing = style?.letter_spacing ?? 2;
const baseBottomMargin = style?.bottom_margin;
const fontSize = Math.max(28, Math.round(baseFontSize * responsiveScale));
const highlightColor = style?.highlight_color ?? '#FFFF00';
const normalColor = style?.normal_color ?? '#FFFFFF';
const strokeColor = style?.stroke_color ?? '#000000';
const strokeSize = Math.max(1, Math.round(baseStrokeSize * responsiveScale));
const letterSpacing = Math.max(0, baseLetterSpacing * responsiveScale);
const bottomMargin = typeof baseBottomMargin === 'number'
? Math.max(0, Math.round(baseBottomMargin * responsiveScale))
: undefined;
const fontFamilyName = fontFamily || 'SubtitleFont';
const fontFamilyCss = fontFile
? `'${fontFamilyName}'`
@@ -109,8 +117,12 @@ export const Subtitles: React.FC<SubtitlesProps> = ({ captions, style }) => {
fontWeight: 800,
lineHeight: 1.4,
textAlign: 'center',
maxWidth: '90%',
wordBreak: 'keep-all',
width: '100%',
boxSizing: 'border-box',
padding: '0 6%',
whiteSpace: 'normal',
wordBreak: 'break-word',
overflowWrap: 'anywhere',
letterSpacing: `${letterSpacing}px`,
}}
>
@@ -118,13 +130,13 @@ export const Subtitles: React.FC<SubtitlesProps> = ({ captions, style }) => {
const isHighlighted = index <= currentWordIndex;
return (
<span
key={`${word.word}-${index}`}
style={{
color: isHighlighted ? highlightColor : normalColor,
textShadow: buildTextShadow(strokeColor, strokeSize),
transition: 'color 0.05s ease',
}}
>
key={`${word.word}-${index}`}
style={{
color: isHighlighted ? highlightColor : normalColor,
textShadow: buildTextShadow(strokeColor, strokeSize),
transition: 'color 0.05s ease',
}}
>
{word.word}
</span>
);

View File

@@ -7,22 +7,19 @@ import {
staticFile,
} from 'remotion';
/**
* 标题样式接口
* 使用 snake_case 与 Python 后端保持一致
*/
export interface TitleStyle {
font_file?: string;
fontFamily?: string;
font_family?: string;
fontSize?: number;
font_size?: number;
color?: string;
strokeColor?: string;
stroke_color?: string;
strokeSize?: number;
stroke_size?: number;
letterSpacing?: number;
letter_spacing?: number;
topMargin?: number;
top_margin?: number;
fontWeight?: number;
font_weight?: number;
}
@@ -50,7 +47,7 @@ const buildTextShadow = (color: string, size: number) => {
`${size}px -${size}px 0 ${color}`,
`-${size}px ${size}px 0 ${color}`,
`${size}px ${size}px 0 ${color}`,
`0 0 ${size * 2}px rgba(0,0,0,0.7)`,
`0 0 ${size * 4}px rgba(0,0,0,0.9)`,
`0 4px 8px rgba(0,0,0,0.6)`
].join(',');
};
@@ -62,7 +59,7 @@ export const Title: React.FC<TitleProps> = ({
style,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
const { fps, width } = useVideoConfig();
const currentTimeInSeconds = frame / fps;
@@ -97,15 +94,27 @@ export const Title: React.FC<TitleProps> = ({
{ extrapolateRight: 'clamp' }
);
// 使用统一的 snake_case 属性名称
const fontFile = style?.font_file;
const fontFamily = style?.fontFamily || style?.font_family;
const fontSize = style?.fontSize || style?.font_size || 72;
const color = style?.color || '#FFFFFF';
const strokeColor = style?.strokeColor || style?.stroke_color || '#000000';
const strokeSize = style?.strokeSize || style?.stroke_size || 8;
const letterSpacing = style?.letterSpacing || style?.letter_spacing || 4;
const topMargin = style?.topMargin || style?.top_margin;
const fontWeight = style?.fontWeight || style?.font_weight || 900;
const fontFamily = style?.font_family;
const widthScale = Math.min(1, width / 1080);
const responsiveScale = Math.max(0.55, widthScale);
const baseFontSize = style?.font_size ?? 72;
const baseStrokeSize = style?.stroke_size ?? 8;
const baseLetterSpacing = style?.letter_spacing ?? 4;
const baseTopMargin = style?.top_margin;
const fontSize = Math.max(36, Math.round(baseFontSize * responsiveScale));
const color = style?.color ?? '#FFFFFF';
const strokeColor = style?.stroke_color ?? '#000000';
const strokeSize = Math.max(1, Math.round(baseStrokeSize * responsiveScale));
const letterSpacing = Math.max(0, baseLetterSpacing * responsiveScale);
const topMargin = typeof baseTopMargin === 'number'
? Math.max(0, Math.round(baseTopMargin * responsiveScale))
: undefined;
const fontWeight = style?.font_weight ?? 900;
const fontFamilyName = fontFamily || 'TitleFont';
const fontFamilyCss = fontFile
? `'${fontFamilyName}'`
@@ -140,8 +149,13 @@ export const Title: React.FC<TitleProps> = ({
fontFamily: fontFamilyCss,
textShadow: buildTextShadow(strokeColor, strokeSize),
margin: 0,
width: '100%',
boxSizing: 'border-box',
padding: '0 5%',
lineHeight: 1.3,
whiteSpace: 'normal',
wordBreak: 'break-word',
overflowWrap: 'anywhere',
letterSpacing: `${letterSpacing}px`,
}}
>