优化
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
const axios = require('axios')
|
||||
const path = require('path')
|
||||
const fs = require('fs')
|
||||
const { execFile } = require('child_process')
|
||||
const { syncDraft, registerDraft, triggerDirectoryScan } = require('./sync-to-jianying')
|
||||
|
||||
// ============================================================================
|
||||
@@ -128,17 +129,25 @@ async function batchUploadToOSS(inputDir, files) {
|
||||
// ============================================================================
|
||||
|
||||
function buildTimeline(items, defaultDurationUs) {
|
||||
// 音频为主轴,视频适配音频(短视频行业标准)
|
||||
// 有视频时长时取 max(不截断音频),无视频时用音频时长
|
||||
// 音频为主轴,视频调速适配(≤2x 加速,>2x 截断)
|
||||
let offset = 0
|
||||
return items.map(item => {
|
||||
const audioDur = (item.duration != null) ? item.duration * US : 0
|
||||
const videoDur = (item.videoDuration != null) ? item.videoDuration * US : 0
|
||||
// 有视频:保证音频不被截断;无视频(图片模式):用音频时长
|
||||
const dur = videoDur > 0
|
||||
? Math.max(audioDur, videoDur)
|
||||
: (audioDur || defaultDurationUs)
|
||||
const entry = { start: offset, end: offset + dur, duration: dur }
|
||||
// 无 TTS:用视频时长或固定时长
|
||||
if (audioDur <= 0) {
|
||||
const dur = videoDur || defaultDurationUs
|
||||
const entry = { start: offset, end: offset + dur, duration: dur, speed: 1 }
|
||||
offset += dur
|
||||
return entry
|
||||
}
|
||||
// 有 TTS:音频时长为主轴
|
||||
const dur = audioDur
|
||||
const ratio = videoDur > 0 ? videoDur / audioDur : 1
|
||||
// ≤2x: 加速到音频时长;>2x: 截断(视频只取前 audioDur 部分)
|
||||
const speed = ratio <= 2 ? ratio : 1
|
||||
const needAdjust = videoDur > audioDur + 100000 // 视频比音频长 0.1s 以上才需要调整
|
||||
const entry = { start: offset, end: offset + dur, duration: dur, speed, needAdjust }
|
||||
offset += dur
|
||||
return entry
|
||||
})
|
||||
@@ -236,7 +245,28 @@ async function assemble(args) {
|
||||
if (mode === 'images') {
|
||||
await addImages(draftUrl, items, imgUrls, timeline, width, height, animation)
|
||||
} else {
|
||||
// 视频模式:确保所有 item 都有 videoUrl(CapCut API 需要公网 URL)
|
||||
// 视频模式:调速 → 上传 OSS → 添加到草稿
|
||||
// Step 1: ffmpeg 调速(在上传前,避免传两份)
|
||||
let adjustedCount = 0
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i]
|
||||
const tl = timeline[i]
|
||||
if (tl.needAdjust && item.video) {
|
||||
const videoPath = path.resolve(inputDir, item.video)
|
||||
const audioDur = tl.duration / US
|
||||
const adjustedPath = await adjustVideoSpeed(videoPath, audioDur)
|
||||
if (adjustedPath !== videoPath) {
|
||||
item.video = path.relative(inputDir, adjustedPath)
|
||||
item.videoDuration = audioDur
|
||||
adjustedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
if (adjustedCount > 0) {
|
||||
console.log(` 视频调速: ${adjustedCount}/${items.length} 个`)
|
||||
}
|
||||
|
||||
// Step 2: 上传(已调速的)视频到 OSS
|
||||
const missingUrl = items.filter(it => it.video && !it.videoUrl)
|
||||
if (missingUrl.length > 0) {
|
||||
const { uploadFile } = require('./oss-upload')
|
||||
@@ -295,7 +325,11 @@ async function assemble(args) {
|
||||
// -- 添加特效 --
|
||||
step++; console.log(`[${step}/${totalSteps}] 添加特效...`)
|
||||
if (effectsStr) {
|
||||
try {
|
||||
await addEffects(draftUrl, effectsStr, totalDurationUs)
|
||||
} catch (e) {
|
||||
console.log(` 特效跳过: ${e.message}`)
|
||||
}
|
||||
} else {
|
||||
console.log(' 跳过(未指定 --effects)')
|
||||
}
|
||||
@@ -303,7 +337,11 @@ async function assemble(args) {
|
||||
// -- 添加滤镜 --
|
||||
step++; console.log(`[${step}/${totalSteps}] 添加滤镜...`)
|
||||
if (filterStr) {
|
||||
try {
|
||||
await addFilter(draftUrl, filterStr, totalDurationUs)
|
||||
} catch (e) {
|
||||
console.log(` 滤镜跳过: ${e.message}`)
|
||||
}
|
||||
} else {
|
||||
console.log(' 跳过(未指定 --filter)')
|
||||
}
|
||||
@@ -376,6 +414,60 @@ async function addImages(draftUrl, items, imgUrls, timeline, width, height, anim
|
||||
// 添加视频(从 manifest 读取时长)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* ffmpeg 调速:将视频调整为指定时长
|
||||
* ratio <= 2x: 加速;ratio > 2x: 截断
|
||||
* 返回调整后的文件路径(调整失败则返回原路径)
|
||||
*/
|
||||
async function adjustVideoSpeed(videoPath, targetDurationSec) {
|
||||
if (!fs.existsSync(videoPath)) return videoPath
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 先获取视频时长
|
||||
execFile('ffprobe', [
|
||||
'-v', 'quiet', '-show_entries', 'format=duration',
|
||||
'-of', 'csv=p=0', videoPath
|
||||
], (err, stdout) => {
|
||||
if (err) { resolve(videoPath); return }
|
||||
const videoDur = parseFloat(stdout.trim())
|
||||
if (!videoDur || videoDur <= 0 || videoDur <= targetDurationSec + 0.1) {
|
||||
resolve(videoPath); return
|
||||
}
|
||||
|
||||
const ratio = videoDur / targetDurationSec
|
||||
const outPath = videoPath.replace(/(\.\w+)$/, '_adj$1')
|
||||
|
||||
if (ratio <= 2) {
|
||||
// 加速:setpts=PTS/speed, atempo=speed (音频变速)
|
||||
const speed = ratio.toFixed(3)
|
||||
const atempo = Math.min(speed, 2.0) // atempo 单次上限 2.0
|
||||
execFile('ffmpeg', [
|
||||
'-y', '-i', videoPath,
|
||||
'-filter_complex', `setpts=PTS/${speed}${videoPath.endsWith('.mp4') ? `,atempo=${atempo}` : ''}`,
|
||||
'-an', // 去掉音频(视频模式不需要原始音轨)
|
||||
outPath
|
||||
], { timeout: 30000 }, (err) => {
|
||||
if (err) { console.log(` 调速失败,使用原始视频: ${err.message}`); resolve(videoPath); return }
|
||||
console.log(` 调速: ${videoDur.toFixed(1)}s → ${targetDurationSec.toFixed(1)}s (${speed}x)`)
|
||||
resolve(outPath)
|
||||
})
|
||||
} else {
|
||||
// 截断:取前 targetDuration 秒
|
||||
execFile('ffmpeg', [
|
||||
'-y', '-i', videoPath,
|
||||
'-t', String(targetDurationSec),
|
||||
'-c', 'copy',
|
||||
outPath
|
||||
], { timeout: 30000 }, (err) => {
|
||||
if (err) { console.log(` 截断失败,使用原始视频: ${err.message}`); resolve(videoPath); return }
|
||||
console.log(` 截断: ${videoDur.toFixed(1)}s → ${targetDurationSec.toFixed(1)}s`)
|
||||
resolve(outPath)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function addVideos(draftUrl, inputDir, items, timeline, width, height) {
|
||||
const videoInfos = items.map((item, i) => {
|
||||
const tl = timeline[i]
|
||||
@@ -722,9 +814,11 @@ async function main() {
|
||||
await assemble(args)
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(err => {
|
||||
console.error(`\n错误: ${err.message}`)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = { assemble }
|
||||
|
||||
Reference in New Issue
Block a user