Files
video-create/.claude/skills/video-from-script/scripts/lib/phase-upload.js
sion123 7abe5f7c96 fix(video-pipeline): 自动修复上传前引用的候选图片路径并修复音频时长精度
在 `phase-upload` 中添加 `autoFixFile` 逻辑,当 `item.file` 指向不存在的文件时,自动从 `candidates` 中匹配实际存在的文件并更新 `item.file`,避免上传阶段因用户手动换图删除候选文件而导致失败。同时修复 `capcut-timeline` 音频/视频时长计算,使用 `Math.round` 避免微秒级浮点精度问题。
2026-05-03 16:11:08 +08:00

117 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Phase: upload — OSS 上传
*
* 将图片(含首尾帧)和视频上传到 OSS回写 url / videoUrl
*/
const path = require('path')
const fs = require('fs')
const { saveManifest, log, getManifestDir } = require('./pipeline-utils')
/**
* 自动修复 item.file如果当前 file 不存在,从 candidates 中匹配实际存在的文件。
* 用户手动换图时可能删除了非选中候选,导致 file 指向不存在的文件。
*/
function autoFixFile(item, dir) {
const currentPath = path.resolve(dir, item.file)
if (fs.existsSync(currentPath)) return false
const cands = item.candidates || []
const existing = cands.filter(c => fs.existsSync(path.resolve(dir, c)))
if (existing.length === 0) {
log('upload', `${item.file} 不存在candidates 中也没有文件`)
return false
}
const oldFile = item.file
const candMatch = oldFile.match(/_cand(\d+)\./)
const targetCand = candMatch ? `_cand${candMatch[1]}.` : null
const matched = targetCand
? (existing.find(c => c.includes(targetCand)) || existing[0])
: existing[0]
item.file = matched
log('upload', ` 🔧 ${path.basename(oldFile)}${path.basename(matched)} (${existing.length}/${cands.length} 候选存在)`)
return true
}
async function phaseUpload(manifest, manifestPath) {
const dir = getManifestDir(manifestPath)
const { uploadFile } = require('../oss-upload')
// 自动修复:用户手动换图后 file 可能指向已删除的候选
let fixedCount = 0
for (const item of manifest.items) {
if (item.status === 'done' && item.file && !item.url) {
if (autoFixFile(item, dir)) fixedCount++
}
}
if (fixedCount > 0) {
saveManifest(manifestPath, manifest)
log('upload', `已自动修复 ${fixedCount} 个 file 路径`)
}
// 图片(含首尾帧 first frame
const imageItems = manifest.items.filter(it =>
it.status === 'done' && it.file && !it.url
)
// 视频
const videoItems = manifest.items.filter(it =>
it.status === 'done' && it.video && !it.videoUrl
)
if (imageItems.length === 0 && videoItems.length === 0) {
log('upload', '无待上传文件,跳过')
return
}
// 上传图片
if (imageItems.length > 0) {
log('upload', `图片: ${imageItems.length}`)
for (let i = 0; i < imageItems.length; i++) {
const item = imageItems[i]
const filePath = path.resolve(dir, item.file)
try {
const { url } = await uploadFile(filePath)
item.url = url
log('upload', ` [${i + 1}/${imageItems.length}] ${item.file} → OK`)
} catch (err) {
item.error = `上传失败: ${err.message}`
log('upload', ` [${i + 1}/${imageItems.length}] 失败: ${err.message}`)
}
// 首尾帧模式:上传 lastFrame
if (item.url && item.lastFrame && !item.lastFrameUrl) {
const lastPath = path.resolve(dir, item.lastFrame)
try {
const { url } = await uploadFile(lastPath)
item.lastFrameUrl = url
log('upload', ` [${i + 1}/${imageItems.length}] lastFrame → OK`)
} catch (err) {
log('upload', ` [${i + 1}/${imageItems.length}] lastFrame 上传失败: ${err.message}`)
}
}
saveManifest(manifestPath, manifest)
}
}
// 上传视频
if (videoItems.length > 0) {
log('upload', `视频: ${videoItems.length}`)
for (let i = 0; i < videoItems.length; i++) {
const item = videoItems[i]
const videoPath = path.resolve(dir, item.video)
try {
const { url } = await uploadFile(videoPath)
item.videoUrl = url
log('upload', ` [${i + 1}/${videoItems.length}] ${item.video} → OK`)
} catch (err) {
log('upload', ` [${i + 1}/${videoItems.length}] 失败: ${err.message}`)
}
saveManifest(manifestPath, manifest)
}
}
}
module.exports = { phaseUpload }