优化
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
|
const { execFile } = require('child_process')
|
||||||
const { syncDraft, registerDraft, triggerDirectoryScan } = require('./sync-to-jianying')
|
const { syncDraft, registerDraft, triggerDirectoryScan } = require('./sync-to-jianying')
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -128,17 +129,25 @@ async function batchUploadToOSS(inputDir, files) {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
function buildTimeline(items, defaultDurationUs) {
|
function buildTimeline(items, defaultDurationUs) {
|
||||||
// 音频为主轴,视频适配音频(短视频行业标准)
|
// 音频为主轴,视频调速适配(≤2x 加速,>2x 截断)
|
||||||
// 有视频时长时取 max(不截断音频),无视频时用音频时长
|
|
||||||
let offset = 0
|
let offset = 0
|
||||||
return items.map(item => {
|
return items.map(item => {
|
||||||
const audioDur = (item.duration != null) ? item.duration * US : 0
|
const audioDur = (item.duration != null) ? item.duration * US : 0
|
||||||
const videoDur = (item.videoDuration != null) ? item.videoDuration * US : 0
|
const videoDur = (item.videoDuration != null) ? item.videoDuration * US : 0
|
||||||
// 有视频:保证音频不被截断;无视频(图片模式):用音频时长
|
// 无 TTS:用视频时长或固定时长
|
||||||
const dur = videoDur > 0
|
if (audioDur <= 0) {
|
||||||
? Math.max(audioDur, videoDur)
|
const dur = videoDur || defaultDurationUs
|
||||||
: (audioDur || defaultDurationUs)
|
const entry = { start: offset, end: offset + dur, duration: dur, speed: 1 }
|
||||||
const entry = { start: offset, end: offset + dur, duration: dur }
|
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
|
offset += dur
|
||||||
return entry
|
return entry
|
||||||
})
|
})
|
||||||
@@ -236,7 +245,28 @@ async function assemble(args) {
|
|||||||
if (mode === 'images') {
|
if (mode === 'images') {
|
||||||
await addImages(draftUrl, items, imgUrls, timeline, width, height, animation)
|
await addImages(draftUrl, items, imgUrls, timeline, width, height, animation)
|
||||||
} else {
|
} 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)
|
const missingUrl = items.filter(it => it.video && !it.videoUrl)
|
||||||
if (missingUrl.length > 0) {
|
if (missingUrl.length > 0) {
|
||||||
const { uploadFile } = require('./oss-upload')
|
const { uploadFile } = require('./oss-upload')
|
||||||
@@ -295,7 +325,11 @@ async function assemble(args) {
|
|||||||
// -- 添加特效 --
|
// -- 添加特效 --
|
||||||
step++; console.log(`[${step}/${totalSteps}] 添加特效...`)
|
step++; console.log(`[${step}/${totalSteps}] 添加特效...`)
|
||||||
if (effectsStr) {
|
if (effectsStr) {
|
||||||
await addEffects(draftUrl, effectsStr, totalDurationUs)
|
try {
|
||||||
|
await addEffects(draftUrl, effectsStr, totalDurationUs)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` 特效跳过: ${e.message}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(' 跳过(未指定 --effects)')
|
console.log(' 跳过(未指定 --effects)')
|
||||||
}
|
}
|
||||||
@@ -303,7 +337,11 @@ async function assemble(args) {
|
|||||||
// -- 添加滤镜 --
|
// -- 添加滤镜 --
|
||||||
step++; console.log(`[${step}/${totalSteps}] 添加滤镜...`)
|
step++; console.log(`[${step}/${totalSteps}] 添加滤镜...`)
|
||||||
if (filterStr) {
|
if (filterStr) {
|
||||||
await addFilter(draftUrl, filterStr, totalDurationUs)
|
try {
|
||||||
|
await addFilter(draftUrl, filterStr, totalDurationUs)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(` 滤镜跳过: ${e.message}`)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(' 跳过(未指定 --filter)')
|
console.log(' 跳过(未指定 --filter)')
|
||||||
}
|
}
|
||||||
@@ -376,6 +414,60 @@ async function addImages(draftUrl, items, imgUrls, timeline, width, height, anim
|
|||||||
// 添加视频(从 manifest 读取时长)
|
// 添加视频(从 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) {
|
async function addVideos(draftUrl, inputDir, items, timeline, width, height) {
|
||||||
const videoInfos = items.map((item, i) => {
|
const videoInfos = items.map((item, i) => {
|
||||||
const tl = timeline[i]
|
const tl = timeline[i]
|
||||||
@@ -722,9 +814,11 @@ async function main() {
|
|||||||
await assemble(args)
|
await assemble(args)
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch(err => {
|
if (require.main === module) {
|
||||||
console.error(`\n错误: ${err.message}`)
|
main().catch(err => {
|
||||||
process.exit(1)
|
console.error(`\n错误: ${err.message}`)
|
||||||
})
|
process.exit(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = { assemble }
|
module.exports = { assemble }
|
||||||
|
|||||||
Reference in New Issue
Block a user