cerevox 2.30.6 → 2.32.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/sandbox.d.ts +1 -0
- package/dist/core/sandbox.d.ts.map +1 -1
- package/dist/core/sandbox.js +5 -5
- package/dist/core/sandbox.js.map +1 -1
- package/dist/mcp/servers/prompts/rules/creative-ad.md +2 -2
- package/dist/mcp/servers/prompts/rules/freeform.md +40 -36
- package/dist/mcp/servers/prompts/rules/general-video.md +8 -8
- package/dist/mcp/servers/prompts/rules/material-creation.md +10 -0
- package/dist/mcp/servers/prompts/rules/music-video.md +2 -2
- package/dist/mcp/servers/prompts/rules/stage-play.md +2 -2
- package/dist/mcp/servers/prompts/rules/story-telling.md +2 -2
- package/dist/mcp/servers/prompts/skills/workflows/general-video.md +8 -8
- package/dist/mcp/servers/prompts/skills/workflows/music-video.md +2 -2
- package/dist/mcp/servers/prompts/zerocut-core.md +36 -37
- package/dist/mcp/servers/zerocut.d.ts.map +1 -1
- package/dist/mcp/servers/zerocut.js +74 -30
- package/dist/mcp/servers/zerocut.js.map +1 -1
- package/dist/utils/coze.d.ts.map +1 -1
- package/dist/utils/coze.js +12 -50
- package/dist/utils/coze.js.map +1 -1
- package/dist/utils/videokit.d.ts.map +1 -1
- package/dist/utils/videokit.js +71 -210
- package/dist/utils/videokit.js.map +1 -1
- package/package.json +1 -1
- package/dist/mcp/servers/prompts/rules/anime-series.md +0 -182
package/dist/utils/videokit.js
CHANGED
|
@@ -482,23 +482,19 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
482
482
|
if (!asset)
|
|
483
483
|
throw new Error(`Missing asset: ${clip.assetId}`);
|
|
484
484
|
const idx = inputIndexByAsset[asset.id];
|
|
485
|
-
// For video tracks, we need to handle both video and audio from video files
|
|
486
485
|
let sel;
|
|
487
486
|
if (track.type === 'audio') {
|
|
488
487
|
sel = `[${idx}:a]`;
|
|
489
488
|
}
|
|
490
489
|
else if (track.type === 'video') {
|
|
491
|
-
// For video tracks, we process the video stream
|
|
492
490
|
sel = `[${idx}:v]`;
|
|
493
|
-
//
|
|
491
|
+
// 若视频素材自带音频,把音频也按同样时间处理
|
|
494
492
|
if (asset.type === 'video' && (await checkVideoHasAudio(asset.uri))) {
|
|
495
493
|
const audioSel = `[${idx}:a]`;
|
|
496
|
-
// Process audio with same timing and effects as video
|
|
497
494
|
const audioStart = clip.inMs / 1000;
|
|
498
495
|
const audioDur = (clip.durationMs +
|
|
499
496
|
(clip.transitionIn ? clip.transitionIn.durationMs : 0)) /
|
|
500
497
|
1000;
|
|
501
|
-
// Get real duration for audio speed adjustment
|
|
502
498
|
let originalDurationMs = asset.durationMs;
|
|
503
499
|
const realDuration = await getMediaDuration(asset.uri);
|
|
504
500
|
if (realDuration !== null) {
|
|
@@ -512,79 +508,66 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
512
508
|
const speedRatio = originalDurationSec / targetDurationSec;
|
|
513
509
|
let audioTrim;
|
|
514
510
|
if (Math.abs(originalDurationSec - targetDurationSec) > 0.01) {
|
|
515
|
-
// Duration mismatch: adjust playback speed using atempo
|
|
516
511
|
audioTrim = `atrim=start=${audioStart}:duration=${originalDurationSec},asetpts=PTS-STARTPTS,atempo=${speedRatio}`;
|
|
517
512
|
}
|
|
518
513
|
else {
|
|
519
|
-
// Duration matches: use normal trim
|
|
520
514
|
audioTrim = `atrim=start=${audioStart}:duration=${audioDur},asetpts=PTS-STARTPTS`;
|
|
521
515
|
}
|
|
522
|
-
|
|
516
|
+
// (①)更稳的 adelay,并在其后归零
|
|
517
|
+
const audioShift = clip.startMs > 0
|
|
518
|
+
? `adelay=${Math.round(clip.startMs)}:all=1,asetpts=PTS-STARTPTS`
|
|
519
|
+
: '';
|
|
523
520
|
const audioLabel = newLabel();
|
|
524
|
-
// Apply same effects to audio as video (where applicable)
|
|
525
521
|
const audioEffects = [];
|
|
526
522
|
if (clip.effects) {
|
|
527
523
|
for (const ef of clip.effects) {
|
|
528
524
|
switch (ef.name) {
|
|
529
525
|
case 'fadeIn': {
|
|
530
|
-
const
|
|
531
|
-
const d = Number(params?.durationMs ?? 500) / 1000;
|
|
526
|
+
const d = Number(ef.params?.durationMs ?? 500) / 1000;
|
|
532
527
|
audioEffects.push(`afade=t=in:st=0:d=${d}`);
|
|
533
528
|
break;
|
|
534
529
|
}
|
|
535
530
|
case 'fadeOut': {
|
|
536
|
-
const
|
|
537
|
-
const d = Number(params?.durationMs ?? 500) / 1000;
|
|
531
|
+
const d = Number(ef.params?.durationMs ?? 500) / 1000;
|
|
538
532
|
audioEffects.push(`afade=t=out:st=${Math.max(0, audioDur - d)}:d=${d}`);
|
|
539
533
|
break;
|
|
540
534
|
}
|
|
541
535
|
case 'gain': {
|
|
542
|
-
const
|
|
543
|
-
const db = Number(
|
|
536
|
+
const p = ef.params;
|
|
537
|
+
const db = Number(p?.db ?? 0) || Number(p?.gain ?? 0);
|
|
544
538
|
let gain = Math.pow(10, db / 20);
|
|
545
|
-
if (
|
|
546
|
-
gain *=
|
|
547
|
-
}
|
|
539
|
+
if (p?.volume)
|
|
540
|
+
gain *= p.volume;
|
|
548
541
|
audioEffects.push(`volume=${gain.toFixed(2)}`);
|
|
549
542
|
break;
|
|
550
543
|
}
|
|
551
544
|
case 'speed': {
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
if (rate !== 1) {
|
|
545
|
+
const rate = Number(ef.params?.rate ?? 1);
|
|
546
|
+
if (rate !== 1)
|
|
555
547
|
audioEffects.push(`atempo=${rate.toFixed(3)}`);
|
|
556
|
-
}
|
|
557
548
|
break;
|
|
558
549
|
}
|
|
559
550
|
}
|
|
560
551
|
}
|
|
561
552
|
}
|
|
562
|
-
|
|
553
|
+
// (②)仅音频分支进入 amix 前统一采样率/声道
|
|
554
|
+
fg.push(`${audioSel}${audioTrim}${audioEffects.length ? ',' + audioEffects.join(',') : ''}${audioShift ? ',' + audioShift : ''},aformat=sample_rates=48000:channel_layouts=stereo[${audioLabel}]`);
|
|
563
555
|
videoAudioOuts.push(`[${audioLabel}]`);
|
|
564
556
|
}
|
|
565
557
|
}
|
|
566
558
|
else {
|
|
567
|
-
sel = `[${idx}:v]`;
|
|
559
|
+
sel = `[${idx}:v]`;
|
|
568
560
|
}
|
|
569
561
|
const start = clip.inMs / 1000;
|
|
570
|
-
// Extend duration for transitionIn to compensate for overlap
|
|
571
562
|
const dur = (clip.durationMs +
|
|
572
563
|
(clip.transitionIn ? clip.transitionIn.durationMs : 0)) /
|
|
573
564
|
1000;
|
|
574
|
-
// For clips with transitionIn: add static frames at beginning and start earlier
|
|
575
|
-
// For clips without transitionIn: use original startMs
|
|
576
565
|
const hasTransition = clip.transitionIn && track.type === 'video';
|
|
577
566
|
const transitionDuration = hasTransition
|
|
578
567
|
? clip.transitionIn.durationMs / 1000
|
|
579
568
|
: 0;
|
|
580
|
-
const actualStartSec = hasTransition
|
|
581
|
-
? (clip.startMs - clip.transitionIn.durationMs) / 1000
|
|
582
|
-
: clip.startMs / 1000;
|
|
583
|
-
// Get real duration from media file, fallback to asset.durationMs, then clip.durationMs
|
|
584
569
|
let originalDurationMs = asset.durationMs;
|
|
585
|
-
// Try to get real duration from file if asset.durationMs is missing or potentially inaccurate
|
|
586
570
|
const realDuration = await getMediaDuration(asset.uri);
|
|
587
|
-
// console.log(realDuration, asset.uri);
|
|
588
571
|
if (realDuration !== null) {
|
|
589
572
|
originalDurationMs = Math.round(realDuration * 1000);
|
|
590
573
|
console.debug(`Dynamic duration detection: ${asset.uri} = ${originalDurationMs}ms (was ${asset.durationMs}ms)`);
|
|
@@ -597,105 +580,84 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
597
580
|
const speedRatio = originalDurationSec / targetDurationSec;
|
|
598
581
|
let trim;
|
|
599
582
|
if (track.type === 'audio') {
|
|
600
|
-
// For audio tracks, distinguish between pure audio files and audio from video files
|
|
601
583
|
if (asset.type === 'audio') {
|
|
602
|
-
// Pure audio files: always use original speed, just trim to target duration
|
|
603
584
|
trim = `atrim=start=${start}:duration=${targetDurationSec},asetpts=PTS-STARTPTS`;
|
|
604
585
|
}
|
|
605
586
|
else {
|
|
606
|
-
// Audio from video files: check if we need to adjust playback speed
|
|
607
587
|
if (Math.abs(originalDurationSec - targetDurationSec) > 0.01) {
|
|
608
|
-
// Duration mismatch: adjust playback speed using atempo
|
|
609
588
|
trim = `atrim=start=${start}:duration=${originalDurationSec},asetpts=PTS-STARTPTS,atempo=${speedRatio}`;
|
|
610
589
|
}
|
|
611
590
|
else {
|
|
612
|
-
// Duration matches: use normal trim
|
|
613
591
|
trim = `atrim=start=${start}:duration=${dur},asetpts=PTS-STARTPTS`;
|
|
614
592
|
}
|
|
615
593
|
}
|
|
616
594
|
}
|
|
617
595
|
else {
|
|
618
|
-
// For video, check if we need to adjust playback speed
|
|
619
596
|
if (Math.abs(originalDurationSec - targetDurationSec) > 0.01) {
|
|
620
|
-
// Duration mismatch: adjust playback speed using setpts
|
|
621
597
|
trim = `trim=start=${start}:duration=${originalDurationSec},setpts=PTS/(${speedRatio}),fps=${project.settings.fps}`;
|
|
622
598
|
}
|
|
623
599
|
else {
|
|
624
|
-
// Duration matches: use normal trim
|
|
625
600
|
trim = `trim=start=${start}:duration=${dur},setpts=PTS-STARTPTS,fps=${project.settings.fps}`;
|
|
626
601
|
}
|
|
627
602
|
}
|
|
628
603
|
const shift = track.type === 'audio'
|
|
629
|
-
?
|
|
604
|
+
? clip.startMs > 0
|
|
605
|
+
? `adelay=${Math.round(clip.startMs)}:all=1,asetpts=PTS-STARTPTS`
|
|
606
|
+
: ''
|
|
630
607
|
: hasTransition
|
|
631
608
|
? `tpad=start_duration=${transitionDuration}:start_mode=clone`
|
|
632
|
-
: '';
|
|
609
|
+
: '';
|
|
633
610
|
// 背景音乐默认 gain
|
|
634
611
|
if (clip.assetId.includes('bgm')) {
|
|
635
612
|
const gainEffects = clip.effects?.find(e => e.name === 'gain');
|
|
636
613
|
if (!gainEffects) {
|
|
637
614
|
clip.effects = clip.effects || [];
|
|
638
|
-
clip.effects.push({
|
|
639
|
-
name: 'gain',
|
|
640
|
-
params: {
|
|
641
|
-
db: -15,
|
|
642
|
-
},
|
|
643
|
-
});
|
|
615
|
+
clip.effects.push({ name: 'gain', params: { db: -25 } });
|
|
644
616
|
}
|
|
645
617
|
}
|
|
646
|
-
// Effects mapping (partial demo)
|
|
647
618
|
const effects = [];
|
|
648
619
|
if (clip.effects)
|
|
649
620
|
for (const ef of clip.effects) {
|
|
650
621
|
switch (ef.name) {
|
|
651
622
|
case 'fadeIn': {
|
|
652
|
-
const
|
|
653
|
-
const d = Number(params?.durationMs ?? 500) / 1000;
|
|
623
|
+
const d = Number(ef.params?.durationMs ?? 500) / 1000;
|
|
654
624
|
effects.push(track.type === 'audio'
|
|
655
625
|
? `afade=t=in:st=0:d=${d}`
|
|
656
626
|
: `fade=t=in:st=0:d=${d}`);
|
|
657
627
|
break;
|
|
658
628
|
}
|
|
659
629
|
case 'fadeOut': {
|
|
660
|
-
const
|
|
661
|
-
const d = Number(params?.durationMs ?? 500) / 1000;
|
|
630
|
+
const d = Number(ef.params?.durationMs ?? 500) / 1000;
|
|
662
631
|
effects.push(track.type === 'audio'
|
|
663
632
|
? `afade=t=out:st=${Math.max(0, dur - d)}:d=${d}`
|
|
664
633
|
: `fade=t=out:st=${Math.max(0, dur - d)}:d=${d}`);
|
|
665
634
|
break;
|
|
666
635
|
}
|
|
667
636
|
case 'gain': {
|
|
668
|
-
const
|
|
669
|
-
const db = Number(
|
|
637
|
+
const p = ef.params;
|
|
638
|
+
const db = Number(p?.db ?? 0) || Number(p?.gain ?? 0);
|
|
670
639
|
let gain = Math.pow(10, db / 20);
|
|
671
|
-
if (
|
|
672
|
-
gain *=
|
|
673
|
-
}
|
|
674
|
-
// if (track.id.includes('bgm')) {
|
|
675
|
-
// gain *= 0.5;
|
|
676
|
-
// }
|
|
640
|
+
if (p?.volume)
|
|
641
|
+
gain *= p.volume;
|
|
677
642
|
effects.push(`volume=${gain.toFixed(2)}`);
|
|
678
643
|
break;
|
|
679
644
|
}
|
|
680
645
|
case 'color': {
|
|
681
646
|
if (track.type === 'video') {
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
effects.push(`eq=saturation=${p.saturation ?? 1}:contrast=${p.contrast ?? 1}:brightness=${p.brightness ?? 0}`);
|
|
647
|
+
const p = ef.params;
|
|
648
|
+
effects.push(`eq=saturation=${p?.saturation ?? 1}:contrast=${p?.contrast ?? 1}:brightness=${p?.brightness ?? 0}`);
|
|
685
649
|
}
|
|
686
650
|
break;
|
|
687
651
|
}
|
|
688
652
|
case 'blur': {
|
|
689
653
|
if (track.type === 'video') {
|
|
690
|
-
const
|
|
691
|
-
const s = Number(params?.strength ?? 10);
|
|
654
|
+
const s = Number(ef.params?.strength ?? 10);
|
|
692
655
|
effects.push(`gblur=sigma=${s}`);
|
|
693
656
|
}
|
|
694
657
|
break;
|
|
695
658
|
}
|
|
696
659
|
case 'speed': {
|
|
697
|
-
const
|
|
698
|
-
const rate = Number(params?.rate ?? 1);
|
|
660
|
+
const rate = Number(ef.params?.rate ?? 1);
|
|
699
661
|
if (rate !== 1) {
|
|
700
662
|
if (track.type === 'video')
|
|
701
663
|
effects.push(`setpts=${(1 / rate).toFixed(6)}*PTS`);
|
|
@@ -708,24 +670,17 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
708
670
|
break;
|
|
709
671
|
}
|
|
710
672
|
}
|
|
711
|
-
// Filters mapping - apply custom FFmpeg filters
|
|
712
|
-
if (clip.filters) {
|
|
713
|
-
for (const filter of clip.filters) {
|
|
714
|
-
let filterStr = filter.name;
|
|
715
|
-
if (filter.params && Object.keys(filter.params).length > 0) {
|
|
716
|
-
const paramStr = Object.entries(filter.params)
|
|
717
|
-
.map(([key, value]) => `${key}=${value}`)
|
|
718
|
-
.join(':');
|
|
719
|
-
filterStr += `=${paramStr}`;
|
|
720
|
-
}
|
|
721
|
-
effects.push(filterStr);
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
673
|
const lIn = newLabel();
|
|
725
|
-
const lScale = newLabel();
|
|
726
674
|
const lOut = newLabel();
|
|
727
|
-
|
|
728
|
-
//
|
|
675
|
+
const effStr = effects.length ? ',' + effects.join(',') : '';
|
|
676
|
+
// (②)仅音频分支统一 aformat;视频分支不加 aformat
|
|
677
|
+
if (track.type === 'audio') {
|
|
678
|
+
fg.push(`${sel}${trim}${effStr},aformat=sample_rates=48000:channel_layouts=stereo[${lIn}]`);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
fg.push(`${sel}${trim}${effStr}[${lIn}]`);
|
|
682
|
+
}
|
|
683
|
+
// Scale / shift
|
|
729
684
|
if (track.type === 'video') {
|
|
730
685
|
const scaleAndPad = `scale=${project.settings.resolution.width}:${project.settings.resolution.height}:force_original_aspect_ratio=decrease,pad=${project.settings.resolution.width}:${project.settings.resolution.height}:(ow-iw)/2:(oh-ih)/2,setsar=1`;
|
|
731
686
|
if (shift) {
|
|
@@ -740,8 +695,8 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
740
695
|
fg.push(`[${lIn}]${shift}[${lOut}]`);
|
|
741
696
|
}
|
|
742
697
|
else {
|
|
743
|
-
//
|
|
744
|
-
fg.push(`[${lIn}]
|
|
698
|
+
// (④)acopy -> anull
|
|
699
|
+
fg.push(`[${lIn}]anull[${lOut}]`);
|
|
745
700
|
}
|
|
746
701
|
}
|
|
747
702
|
outs.push(`[${lOut}]`);
|
|
@@ -761,7 +716,8 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
761
716
|
else if (outs.length) {
|
|
762
717
|
if (track.type === 'audio') {
|
|
763
718
|
const l = newLabel();
|
|
764
|
-
|
|
719
|
+
// (③)amix 后追加对齐
|
|
720
|
+
fg.push(`${outs.join('')}amix=inputs=${outs.length}:normalize=0,aresample=async=1:first_pts=0,asetpts=N/SR/TB[${l}]`);
|
|
765
721
|
audioTrackOuts.push(`[${l}]`);
|
|
766
722
|
}
|
|
767
723
|
else if (track.type === 'video') {
|
|
@@ -782,33 +738,33 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
782
738
|
}
|
|
783
739
|
// Simple audio mixing: BGM + Dialog
|
|
784
740
|
let finalAudioOuts = [...audioTrackOuts];
|
|
785
|
-
// If we have multiple audio tracks, mix them together
|
|
786
741
|
if (audioTrackOuts.length >= 1) {
|
|
787
|
-
// Find the first audio track with only 1 clip (likely BGM) and reduce its volume
|
|
788
742
|
const audioTracks = project.timeline.tracks.filter(t => t.type === 'audio');
|
|
789
743
|
const bgmTrackIndex = audioTracks.findIndex(t => t.clips[0].assetId.includes('bgm'));
|
|
790
744
|
const bgmClipTrackIndex = bgmTrackIndex != null
|
|
791
745
|
? bgmTrackIndex
|
|
792
746
|
: audioTracks.findIndex(t => t.clips.length === 1);
|
|
793
|
-
//
|
|
794
|
-
const
|
|
795
|
-
|
|
747
|
+
// 用 label 精确定位 BGM 对应的输出分支,避免被 audioTrackOuts 的顺序变化影响
|
|
748
|
+
const bgmOutLabel = bgmClipTrackIndex >= 0 && bgmClipTrackIndex < audioTrackOuts.length
|
|
749
|
+
? audioTrackOuts[bgmClipTrackIndex]
|
|
750
|
+
: undefined;
|
|
751
|
+
const processedTracks = audioTrackOuts.map(track => {
|
|
752
|
+
if (track === bgmOutLabel) {
|
|
796
753
|
// BGM track (single clip audio track)
|
|
797
|
-
const volumeLabel = newLabel();
|
|
798
754
|
const fadeoutLabel = newLabel();
|
|
799
|
-
// Get BGM track duration to calculate fadeout start time
|
|
800
755
|
const bgmTrack = audioTracks[bgmClipTrackIndex];
|
|
801
|
-
const bgmClip = bgmTrack.clips[bgmTrack.clips.length - 1]; // 多BGM
|
|
756
|
+
const bgmClip = bgmTrack.clips[bgmTrack.clips.length - 1]; // 多BGM取最后一个
|
|
802
757
|
const bgmDurationSec = (bgmClip.startMs + bgmClip.durationMs) / 1000;
|
|
803
|
-
const fadeoutStartSec = Math.max(0, bgmDurationSec - 3); //
|
|
804
|
-
|
|
805
|
-
fg.push(
|
|
758
|
+
const fadeoutStartSec = Math.max(0, bgmDurationSec - 3); // 末尾 3s 淡出
|
|
759
|
+
// 去掉 volume=1.0,避免干扰你前面对 BGM 的任何增益调整
|
|
760
|
+
fg.push(`${track}afade=t=out:st=${fadeoutStartSec}:d=3[${fadeoutLabel}]`);
|
|
806
761
|
return `[${fadeoutLabel}]`;
|
|
807
762
|
}
|
|
808
763
|
return track;
|
|
809
764
|
});
|
|
810
765
|
const l = newLabel();
|
|
811
|
-
|
|
766
|
+
// (③)amix 后追加对齐
|
|
767
|
+
fg.push(`${processedTracks.join('')}amix=inputs=${processedTracks.length}:normalize=0,aresample=async=1:first_pts=0,asetpts=N/SR/TB[${l}]`);
|
|
812
768
|
finalAudioOuts = [`[${l}]`];
|
|
813
769
|
}
|
|
814
770
|
// Calculate total video duration
|
|
@@ -847,7 +803,8 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
847
803
|
const lIn = newLabel();
|
|
848
804
|
const lOut = newLabel();
|
|
849
805
|
fg.push(`${sel}atrim=start=0:duration=${dur},asetpts=PTS-STARTPTS[${lIn}]`);
|
|
850
|
-
|
|
806
|
+
// (①)字幕延迟也用 :all=1,并归零
|
|
807
|
+
fg.push(`[${lIn}]adelay=${Math.round(tpadSec * 1000)}:all=1,asetpts=PTS-STARTPTS[${lOut}]`);
|
|
851
808
|
dialogAudioOuts.push(`[${lOut}]`);
|
|
852
809
|
}
|
|
853
810
|
}
|
|
@@ -857,24 +814,23 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
857
814
|
// Process final audio output
|
|
858
815
|
let outA = '';
|
|
859
816
|
if (finalAudioOuts.length > 0) {
|
|
860
|
-
// Use the ducked/mixed audio from timeline processing
|
|
861
817
|
const audioOut = finalAudioOuts.length > 1
|
|
862
818
|
? (() => {
|
|
863
819
|
const l = newLabel();
|
|
864
|
-
|
|
820
|
+
// (③)amix 后追加对齐
|
|
821
|
+
fg.push(`${finalAudioOuts.join('')}amix=inputs=${finalAudioOuts.length}:normalize=0,aresample=async=1:first_pts=0,asetpts=N/SR/TB[${l}]`);
|
|
865
822
|
return `[${l}]`;
|
|
866
823
|
})()
|
|
867
824
|
: finalAudioOuts[0];
|
|
868
|
-
// Audio processing completed (no additional padding)
|
|
869
825
|
outA = audioOut;
|
|
870
826
|
}
|
|
871
827
|
else if (dialogAudioOuts.length > 0) {
|
|
872
|
-
// Fallback: use subtitle audio if no timeline audio
|
|
873
828
|
outA =
|
|
874
829
|
dialogAudioOuts.length > 1
|
|
875
830
|
? (() => {
|
|
876
831
|
const l = newLabel();
|
|
877
|
-
|
|
832
|
+
// (③)amix 后追加对齐
|
|
833
|
+
fg.push(`${dialogAudioOuts.join('')}amix=inputs=${dialogAudioOuts.length}:normalize=0,aresample=async=1:first_pts=0,asetpts=N/SR/TB[${l}]`);
|
|
878
834
|
return `[${l}]`;
|
|
879
835
|
})()
|
|
880
836
|
: dialogAudioOuts[0];
|
|
@@ -884,12 +840,8 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
884
840
|
let finalV = outV;
|
|
885
841
|
if (project.subtitles?.length && outV) {
|
|
886
842
|
const libassOk = strat === 'ass' || (strat === 'auto' && hasLibass(ffmpegBin));
|
|
887
|
-
// Default force_style for better CJK wrapping; adjust margins as needed.
|
|
888
|
-
// NOTE: kept inside compile scope so it can be customized later if needed.
|
|
889
|
-
// 保留默认的 MarginL 和 MarginR,不管是否有 position
|
|
890
843
|
const defaultForceStyle = `WrapStyle=3,MarginL=60,MarginR=60`;
|
|
891
844
|
if (libassOk) {
|
|
892
|
-
// ASS mode
|
|
893
845
|
const def = project.subtitles[0]?.style || {};
|
|
894
846
|
const ass = composeAssFromItems(project.subtitles, def);
|
|
895
847
|
const assName = opts.subtitlesFileName ?? 'subtitles.ass';
|
|
@@ -900,13 +852,10 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
900
852
|
.replace(/\\/g, '/')
|
|
901
853
|
.replace(/:/g, '\\:')
|
|
902
854
|
.replace(/'/g, "\\'");
|
|
903
|
-
// Enable Unicode line breaking and provide a global WrapStyle fallback.
|
|
904
|
-
// Keep quotes: they are parsed by FFmpeg filter args, not the shell.
|
|
905
855
|
fg.push(`${finalV}subtitles=filename='${arg}':wrap_unicode=1:force_style='${defaultForceStyle}'[${l}]`);
|
|
906
856
|
finalV = `[${l}]`;
|
|
907
857
|
}
|
|
908
858
|
else if (strat === 'srt') {
|
|
909
|
-
// SRT mode
|
|
910
859
|
const srt = composeSrtFromItems(project.subtitles);
|
|
911
860
|
const srtName = opts.subtitlesFileName ?? 'subtitles.srt';
|
|
912
861
|
const srtPath = path_1.default.join(tmpDir, srtName);
|
|
@@ -916,12 +865,10 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
916
865
|
.replace(/\\/g, '/')
|
|
917
866
|
.replace(/:/g, '\\:')
|
|
918
867
|
.replace(/'/g, "\\'");
|
|
919
|
-
// SRT is also rendered by libass; enable Unicode wrapping here too.
|
|
920
868
|
fg.push(`${outV}subtitles=filename='${arg}':wrap_unicode=1:force_style='${defaultForceStyle}'[${l}]`);
|
|
921
869
|
finalV = `[${l}]`;
|
|
922
870
|
}
|
|
923
871
|
else {
|
|
924
|
-
// drawtext fallback
|
|
925
872
|
for (const sub of project.subtitles) {
|
|
926
873
|
const l = newLabel();
|
|
927
874
|
const ff = sub.style?.fontFamily
|
|
@@ -954,7 +901,7 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
954
901
|
? '(0+40)'
|
|
955
902
|
: vertAlign === 'middle'
|
|
956
903
|
? '(h/2-th/2)'
|
|
957
|
-
: '(h-th-40)';
|
|
904
|
+
: '(h-th-40)';
|
|
958
905
|
const st = (sub.startMs / 1000).toFixed(3);
|
|
959
906
|
const et = (sub.endMs / 1000).toFixed(3);
|
|
960
907
|
fg.push(`${finalV}drawtext=text='${txt}'${ff}:fontcolor=${fc}:fontsize=${fs}:fontcolor_outline=${oc}:outline=${ow}:bold=${bo}:x=${exprX}:y=${exprY}:enable='between(t,${st},${et})'[${l}]`);
|
|
@@ -973,6 +920,11 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
973
920
|
PropagateID: (0, uuid_1.v4)(),
|
|
974
921
|
ReservedCode2: '',
|
|
975
922
|
};
|
|
923
|
+
// ✅ 计算输出文件,并在缺少扩展名时指定格式 (-f)
|
|
924
|
+
const container = project.export.container || 'mp4';
|
|
925
|
+
const outFile = project.export.outFile || opts.outFile || `output.${container}`;
|
|
926
|
+
// 是否有扩展名(简单判断 basename 里是否包含 “.ext”)
|
|
927
|
+
const hasExt = /\.[a-zA-Z0-9]{2,5}$/.test(require('path').basename(outFile));
|
|
976
928
|
const args = [
|
|
977
929
|
...inputArgs,
|
|
978
930
|
'-filter_complex',
|
|
@@ -996,102 +948,11 @@ async function compileToFfmpeg(project, opts = {}) {
|
|
|
996
948
|
'use_metadata_tags',
|
|
997
949
|
'-metadata',
|
|
998
950
|
`AIGC=${JSON.stringify(aigcMetadata)}`,
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
951
|
+
// ⬇️ 如果没有扩展名,则明确指定容器格式
|
|
952
|
+
...(!hasExt ? ['-f', container] : []),
|
|
953
|
+
outFile,
|
|
1002
954
|
];
|
|
1003
|
-
// Generate formatted command for better readability
|
|
1004
|
-
const formatCmd = () => {
|
|
1005
|
-
const inputArgs = [];
|
|
1006
|
-
let filterComplex = '';
|
|
1007
|
-
const outputArgs = [];
|
|
1008
|
-
let i = 0;
|
|
1009
|
-
while (i < args.length) {
|
|
1010
|
-
if (args[i] === '-i') {
|
|
1011
|
-
inputArgs.push(`-i ${args[i + 1]}`);
|
|
1012
|
-
i += 2;
|
|
1013
|
-
}
|
|
1014
|
-
else if (args[i] === '-filter_complex') {
|
|
1015
|
-
// Format filter_complex content with logical grouping like test-corrected-ffmpeg.sh
|
|
1016
|
-
const filterContent = args[i + 1];
|
|
1017
|
-
const filters = filterContent
|
|
1018
|
-
.split(';')
|
|
1019
|
-
.map(filter => filter.trim())
|
|
1020
|
-
.filter(filter => filter.length > 0);
|
|
1021
|
-
const videoProcessing = [];
|
|
1022
|
-
const transitions = [];
|
|
1023
|
-
const audioProcessing = [];
|
|
1024
|
-
const mixing = [];
|
|
1025
|
-
for (const filter of filters) {
|
|
1026
|
-
if (/^\[\d+:v\]/.test(filter)) {
|
|
1027
|
-
// Video input processing
|
|
1028
|
-
videoProcessing.push(` ${filter};`);
|
|
1029
|
-
}
|
|
1030
|
-
else if (/^\[v\d+\]\[v\d+\]xfade/.test(filter) ||
|
|
1031
|
-
/^\[x\d+\]\[v\d+\]xfade/.test(filter)) {
|
|
1032
|
-
// Video transitions
|
|
1033
|
-
transitions.push(` ${filter};`);
|
|
1034
|
-
}
|
|
1035
|
-
else if (/^\[\d+:a\]/.test(filter)) {
|
|
1036
|
-
// Audio input processing
|
|
1037
|
-
audioProcessing.push(` ${filter};`);
|
|
1038
|
-
}
|
|
1039
|
-
else if (/amix|volume|afade/.test(filter)) {
|
|
1040
|
-
// Audio mixing
|
|
1041
|
-
mixing.push(` ${filter};`);
|
|
1042
|
-
}
|
|
1043
|
-
else if (/subtitles/.test(filter)) {
|
|
1044
|
-
// Subtitle processing
|
|
1045
|
-
mixing.push(` ${filter};`);
|
|
1046
|
-
}
|
|
1047
|
-
else {
|
|
1048
|
-
// Other filters, group with previous category
|
|
1049
|
-
if (mixing.length > 0) {
|
|
1050
|
-
mixing.push(` ${filter};`);
|
|
1051
|
-
}
|
|
1052
|
-
else if (audioProcessing.length > 0) {
|
|
1053
|
-
audioProcessing.push(` ${filter};`);
|
|
1054
|
-
}
|
|
1055
|
-
else if (transitions.length > 0) {
|
|
1056
|
-
transitions.push(` ${filter};`);
|
|
1057
|
-
}
|
|
1058
|
-
else {
|
|
1059
|
-
videoProcessing.push(` ${filter};`);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
}
|
|
1063
|
-
const formattedSections = [];
|
|
1064
|
-
if (videoProcessing.length > 0)
|
|
1065
|
-
formattedSections.push(videoProcessing.join('\n'));
|
|
1066
|
-
if (transitions.length > 0)
|
|
1067
|
-
formattedSections.push(transitions.join('\n'));
|
|
1068
|
-
if (audioProcessing.length > 0)
|
|
1069
|
-
formattedSections.push(audioProcessing.join('\n'));
|
|
1070
|
-
if (mixing.length > 0)
|
|
1071
|
-
formattedSections.push(mixing.join('\n'));
|
|
1072
|
-
const formattedFilter = formattedSections.join('\n').replace(/;$/, ''); // Remove last semicolon
|
|
1073
|
-
filterComplex = `-filter_complex "\n${formattedFilter}\n"`;
|
|
1074
|
-
i += 2;
|
|
1075
|
-
}
|
|
1076
|
-
else if (args[i] === '-map') {
|
|
1077
|
-
outputArgs.push(`-map ${args[i + 1]}`);
|
|
1078
|
-
i += 2;
|
|
1079
|
-
}
|
|
1080
|
-
else {
|
|
1081
|
-
outputArgs.push(args[i]);
|
|
1082
|
-
i++;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
const parts = [
|
|
1086
|
-
`${ffmpegBin} \\`,
|
|
1087
|
-
`${inputArgs.join(' ')} \\`, // Join input args in one line
|
|
1088
|
-
...(filterComplex ? [`${filterComplex} \\`] : []),
|
|
1089
|
-
outputArgs.join(' '), // Join output args in one line
|
|
1090
|
-
];
|
|
1091
|
-
return parts.join('\n');
|
|
1092
|
-
};
|
|
1093
955
|
const cmd = (0, shell_quote_1.quote)([ffmpegBin, ...args]);
|
|
1094
|
-
// const formattedCmd = formatCmd();
|
|
1095
956
|
return { cmd, args, filterGraph, extraFiles, ffmpegBin };
|
|
1096
957
|
}
|
|
1097
958
|
// ===========================
|