更新代码

This commit is contained in:
Kevin Wong
2026-02-02 10:51:27 +08:00
parent cf679b34bf
commit 6801d3e8aa
38 changed files with 2234 additions and 293 deletions

View File

@@ -19,6 +19,8 @@ interface RenderOptions {
outputPath: string;
fps?: number;
enableSubtitles?: boolean;
width?: number;
height?: number;
}
async function parseArgs(): Promise<RenderOptions> {
@@ -77,8 +79,10 @@ async function main() {
console.log(`Loaded captions with ${captions.segments?.length || 0} segments`);
}
// 获取视频时长
// 获取视频时长和尺寸
let durationInFrames = 300; // 默认 12 秒
let videoWidth = 1280;
let videoHeight = 720;
try {
// 使用 ffprobe 获取视频时长
const { execSync } = require('child_process');
@@ -89,6 +93,18 @@ async function main() {
const durationInSeconds = parseFloat(ffprobeOutput.trim());
durationInFrames = Math.ceil(durationInSeconds * fps);
console.log(`Video duration: ${durationInSeconds}s (${durationInFrames} frames at ${fps}fps)`);
// 使用 ffprobe 获取视频尺寸
const dimensionsOutput = execSync(
`ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 "${options.videoPath}"`,
{ encoding: 'utf-8' }
);
const [width, height] = dimensionsOutput.trim().split('x').map(Number);
if (width && height) {
videoWidth = width;
videoHeight = height;
console.log(`Video dimensions: ${videoWidth}x${videoHeight}`);
}
} catch (e) {
console.warn('Could not get video duration, using default:', e);
}
@@ -119,9 +135,11 @@ async function main() {
},
});
// Override duration
// Override duration and dimensions
composition.durationInFrames = durationInFrames;
composition.fps = fps;
composition.width = videoWidth;
composition.height = videoHeight;
// Render the video
console.log('Rendering video...');

View File

@@ -15,13 +15,13 @@ interface SubtitlesProps {
/**
* 逐字高亮字幕组件
* 根据时间戳逐字高亮显示字幕
* 根据时间戳逐字高亮显示字幕(无背景,纯文字描边)
*/
export const Subtitles: React.FC<SubtitlesProps> = ({
captions,
highlightColor = '#FFFFFF',
normalColor = 'rgba(255, 255, 255, 0.5)',
fontSize = 36,
highlightColor = '#FFFF00',
normalColor = '#FFFFFF',
fontSize = 52,
}) => {
const frame = useCurrentFrame();
const { fps } = useVideoConfig();
@@ -43,43 +43,45 @@ export const Subtitles: React.FC<SubtitlesProps> = ({
style={{
justifyContent: 'flex-end',
alignItems: 'center',
paddingBottom: '60px',
paddingBottom: '6%',
}}
>
<div
<p
style={{
background: 'rgba(0, 0, 0, 0.6)',
padding: '12px 24px',
borderRadius: '12px',
maxWidth: '80%',
margin: 0,
fontSize: `${fontSize}px`,
fontFamily: '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif',
fontWeight: 800,
lineHeight: 1.4,
textAlign: 'center',
maxWidth: '90%',
wordBreak: 'keep-all',
letterSpacing: '2px',
}}
>
<p
style={{
margin: 0,
fontSize: `${fontSize}px`,
fontFamily: '"Noto Sans SC", "Microsoft YaHei", sans-serif',
fontWeight: 500,
lineHeight: 1.5,
}}
>
{currentSegment.words.map((word, index) => (
{currentSegment.words.map((word, index) => {
const isHighlighted = index <= currentWordIndex;
return (
<span
key={`${word.word}-${index}`}
style={{
color: index <= currentWordIndex ? highlightColor : normalColor,
transition: 'color 0.1s ease',
textShadow: index <= currentWordIndex
? '0 2px 10px rgba(255,255,255,0.3)'
: 'none',
color: isHighlighted ? highlightColor : normalColor,
textShadow: `
-3px -3px 0 #000,
3px -3px 0 #000,
-3px 3px 0 #000,
3px 3px 0 #000,
0 0 12px rgba(0,0,0,0.9),
0 4px 8px rgba(0,0,0,0.6)
`,
transition: 'color 0.05s ease',
}}
>
{word.word}
</span>
))}
</p>
</div>
);
})}
</p>
</AbsoluteFill>
);
};

View File

@@ -14,7 +14,7 @@ interface TitleProps {
/**
* 片头标题组件
* 在视频开头显示标题,带淡入淡出效果
* 在视频顶部显示标题,带淡入淡出效果
*/
export const Title: React.FC<TitleProps> = ({
title,
@@ -49,46 +49,45 @@ export const Title: React.FC<TitleProps> = ({
const opacity = Math.min(fadeInOpacity, fadeOutOpacity);
// 轻微的缩放动画
const scale = interpolate(
// 轻微的下滑动画
const translateY = interpolate(
currentTimeInSeconds,
[0, 0.5],
[0.95, 1],
[-20, 0],
{ extrapolateRight: 'clamp' }
);
return (
<AbsoluteFill
style={{
justifyContent: 'center',
justifyContent: 'flex-start',
alignItems: 'center',
paddingTop: '6%',
opacity,
}}
>
<div
<h1
style={{
transform: `scale(${scale})`,
transform: `translateY(${translateY}px)`,
textAlign: 'center',
padding: '40px 60px',
background: 'linear-gradient(135deg, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0.5) 100%)',
borderRadius: '20px',
backdropFilter: 'blur(10px)',
color: '#FFFFFF',
fontSize: '72px',
fontWeight: 900,
fontFamily: '"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", sans-serif',
textShadow: `
0 0 10px rgba(0,0,0,0.9),
0 0 20px rgba(0,0,0,0.7),
0 4px 8px rgba(0,0,0,0.8),
0 8px 16px rgba(0,0,0,0.5)
`,
margin: 0,
padding: '0 5%',
lineHeight: 1.3,
letterSpacing: '4px',
}}
>
<h1
style={{
color: 'white',
fontSize: '48px',
fontWeight: 'bold',
fontFamily: '"Noto Sans SC", "Microsoft YaHei", sans-serif',
textShadow: '0 4px 20px rgba(0,0,0,0.5)',
margin: 0,
lineHeight: 1.4,
}}
>
{title}
</h1>
</div>
{title}
</h1>
</AbsoluteFill>
);
};

View File

@@ -8,7 +8,7 @@ interface VideoLayerProps {
/**
* 视频图层组件
* 渲染底层视频和音频
* 渲染底层视频和音频,视频自动循环以匹配音频长度
*/
export const VideoLayer: React.FC<VideoLayerProps> = ({
videoSrc,
@@ -21,10 +21,11 @@ export const VideoLayer: React.FC<VideoLayerProps> = ({
<AbsoluteFill>
<OffthreadVideo
src={videoUrl}
loop
style={{
width: '100%',
height: '100%',
objectFit: 'contain',
objectFit: 'cover',
}}
/>
{audioSrc && <Audio src={staticFile(audioSrc)} />}