更新
This commit is contained in:
@@ -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}%`);
|
||||
|
||||
@@ -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,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface VideoProps {
|
||||
enableSubtitles?: boolean;
|
||||
subtitleStyle?: SubtitleStyle;
|
||||
titleStyle?: TitleStyle;
|
||||
width?: number;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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`,
|
||||
}}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user