diff --git a/.claude/skills/video-from-script/SKILL.md b/.claude/skills/video-from-script/SKILL.md index 9aab205..a076dfe 100644 --- a/.claude/skills/video-from-script/SKILL.md +++ b/.claude/skills/video-from-script/SKILL.md @@ -128,7 +128,7 @@ node scripts/pipeline.js run --manifest --phase images ### Step 2-C: 人工确认(可跳过) -展示分镜图给用户 → 用户可确认全部 / 替换候选图 / 删除不合格项。 +告知图片路径给用户自行查看 → 用户可确认全部 / 替换候选图 / 删除不合格项。 用户确认后 `pipeline.js confirm --manifest --all`,跳过则批量设置 `confirmed=true`。 ### Step 3-A: 视频提示词(B 模式专属,子 Agent 执行) diff --git a/.claude/skills/video-from-script/scripts/lib/capcut-timeline.js b/.claude/skills/video-from-script/scripts/lib/capcut-timeline.js index 793b747..0e9f566 100644 --- a/.claude/skills/video-from-script/scripts/lib/capcut-timeline.js +++ b/.claude/skills/video-from-script/scripts/lib/capcut-timeline.js @@ -23,8 +23,8 @@ const { US } = require('./capcut-api') function buildTimeline(items) { let offset = 0 return items.map(item => { - const audioDur = (item.audioDuration != null) ? item.audioDuration * US : 0 - const videoDur = (item.videoDuration != null) ? item.videoDuration * US : 0 + const audioDur = Math.round((item.audioDuration != null) ? item.audioDuration * US : 0) + const videoDur = Math.round((item.videoDuration != null) ? item.videoDuration * US : 0) const hasVideo = !!(item.video || item.videoUrl || item.url) // 无 TTS 音频 diff --git a/.claude/skills/video-from-script/scripts/lib/phase-upload.js b/.claude/skills/video-from-script/scripts/lib/phase-upload.js index 7b2b50b..87a2800 100644 --- a/.claude/skills/video-from-script/scripts/lib/phase-upload.js +++ b/.claude/skills/video-from-script/scripts/lib/phase-upload.js @@ -5,71 +5,112 @@ */ 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') + const dir = getManifestDir(manifestPath) + const { uploadFile } = require('../oss-upload') - // 图片(含首尾帧 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 - ) + // 自动修复:用户手动换图后 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 路径`) + } - if (imageItems.length === 0 && videoItems.length === 0) { - log('upload', '无待上传文件,跳过') - return - } + // 图片(含首尾帧 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) { - 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 (imageItems.length === 0 && videoItems.length === 0) { + log('upload', '无待上传文件,跳过') + return + } - // 上传视频 - 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) - } - } + // 上传图片 + 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 } diff --git a/accounts/瞬息实验室/account.json b/accounts/瞬息实验室/account.json index 499496a..44d9a6a 100644 --- a/accounts/瞬息实验室/account.json +++ b/accounts/瞬息实验室/account.json @@ -12,7 +12,7 @@ "imageStylePrompt": "prompts/图片提示词.md", "videoStylePrompt": "prompts/视频提示词.md", "references": [ - { "file": "0_3.png", "url": "https://muye-ai-chat.oss-cn-hangzhou.aliyuncs.com/tmp/0_3.png" } + { "file": "mj-image.jpg", "url": "https://muye-ai-chat.oss-cn-hangzhou.aliyuncs.com/tmp/mj-image.jpg?OSSAccessKeyId=LTAI5tPV9Ag3csf41GZjaLTA&Expires=1809282229&Signature=knoxb7C0u133hwslYW4MhrO2KOs%3D" } ], "capcut": { "effects": [], diff --git a/accounts/瞬息实验室/references/mj-image.jpg b/accounts/瞬息实验室/references/mj-image.jpg new file mode 100644 index 0000000..3bbf59d Binary files /dev/null and b/accounts/瞬息实验室/references/mj-image.jpg differ