diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/build-create-prompts.cjs b/project/短视频脚本创作/V1.0/mcn-work-shop/build-create-prompts.cjs new file mode 100644 index 0000000..dc40fcb --- /dev/null +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/build-create-prompts.cjs @@ -0,0 +1,111 @@ +/* build-create-prompts.mjs — 构造 6 个视频的 AI 创作任务 prompt(与前端 AI创作 按钮完全一致:人设卡+视频选题块+创作指令),并提交 /api/run */ +const { DatabaseSync } = require('node:sqlite'); +const path = require('path'); + +const DB = path.join(__dirname, 'mcn-plugin.db'); +const API = 'http://localhost:8900/api/run'; +const ACCOUNT_NAME = '俊希'; +const ACCOUNT_ID = 3; +const VIDEO_IDS = [309, 313, 315, 316, 318, 320]; + +const db = new DatabaseSync(DB, { readOnly: true }); + +// —— 与前端一致的函数(data-pages.js)—— +const stripTitleQuotes = (s) => String(s || '').trim().replace(/^["“”'‘’「『]+|["“”'‘’」』]+$/g, ''); +const cleanTitle = (t) => { + const s = String(t || '').trim(); + const idx = s.indexOf('#'); + if (idx <= 0) return stripTitleQuotes(s); + return stripTitleQuotes(s.slice(0, idx)) + s.slice(idx); +}; +const personaForCreate = (text) => { + const s = String(text || '').trim(); + const idx = s.indexOf('## 十一、'); + return idx >= 0 ? s.slice(0, idx).trim() : s; +}; +const extractVideoTopic = (content) => { + const s = String(content || ''); + const i = s.indexOf('# 选题'); + if (i < 0) return ''; + const rest = s.slice(i + '# 选题'.length); + const j = rest.search(/\n# /); + return (j >= 0 ? rest.slice(0, j) : rest).trim(); +}; + +// —— 读俊希 persona —— +const p = db.prepare('SELECT content_json FROM account_persona WHERE account_id=? ORDER BY id DESC LIMIT 1').get(ACCOUNT_ID); +const personaText = personaForCreate(p ? p.content_json : ''); +console.log('persona len:', personaText.length); + +const tasks = []; +for (const vid of VIDEO_IDS) { + const v = db.prepare('SELECT id, aweme_id, video_title, video_url FROM account_videos WHERE id=?').get(vid); + if (!v) { console.error('video not found', vid); continue; } + const s = db.prepare('SELECT content_json FROM account_video_source WHERE aweme_id=?').get(v.aweme_id); + const content = String(s ? s.content_json : ''); + const title = cleanTitle(v.video_title); + + // 前端 buildTopicBlock 逻辑 + const tc = extractVideoTopic(content); + let block, mode, createTail; + if (tc) { + block = `\n【视频选题】\n标题:《${title}》\n# 选题\n${tc}\n`; + mode = 'topic'; + createTail = '请结合账号设定与视频选题,创作一个全新的故事脚本(新故事、新情节,不要复刻原视频),输出完整故事脚本。'; + } else { + const body = content.trim(); + if (!body) { + block = `\n【视频选题】\n标题:《${title}》\n`; + mode = 'none'; + createTail = '请结合账号设定与视频选题,创作一个全新的故事脚本(新故事、新情节,不要复刻原视频),输出完整故事脚本。'; + } else { + block = `\n【视频解析内容(供提炼选题)】\n标题:《${title}》\n${body}\n`; + mode = 'content'; + createTail = '请先通读以上【视频解析内容】,提炼出该视频的选题(赛道/受众/视频主题/开场钩子/中间留人/结尾落点等),再结合账号设定与提炼出的选题,创作一个全新的故事脚本(新故事、新情节,不要复刻原视频),输出完整故事脚本。'; + } + } + const personaBlock = personaText ? `\n【账号设定(人设卡)】\n${personaText}\n` : ''; + const noPersonaNote = personaText ? '' : `\n该账号暂无账号设定,请先为达人「${ACCOUNT_NAME}」提取账号设定(生成人设卡),再基于账号设定与选题创作。`; + + const prompt = `请调用 Skill 工具加载「短视频脚本创作」技能(技能根目录 = 用户技能目录 ~/.workbuddy/skills/短视频脚本创作/,先读取其 SKILL.md 及 references 规范;若 Skill 工具无法加载,请直接读取该目录下的 SKILL.md 文件)。遵循 S1-S11 完整创作流程,最终输出 S9 标准脚本格式(按场次+对白组织,画面标注【景别|环境|光线|场景|场景元素】),禁止输出分镜头表格。 +请为达人「${ACCOUNT_NAME}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${block}${createTail} + +【任务完成后必须执行】 +将完整脚本保存到工作台数据库 mcn-plugin.db 的 rewrite_log 表(数据库路径:D:\\AgentSkill\\mcn-video-script\\project\\短视频脚本创作\\V1.0\\mcn-work-shop\\mcn-plugin.db),字段: +- video_id=${v.id}(整数) +- aweme_id=${v.aweme_id} +- account_id=${ACCOUNT_ID} +- title=脚本标题 +- script_text=完整脚本全文 +- status='done' +(用 node:sqlite 或 sqlite3 写入;若该表不存在则创建后再写) +完成后回复:脚本标题 + 是否已保存到数据库。`; + + tasks.push({ videoId: v.id, mode, title, prompt }); + console.log(`TASK v${v.id} | mode=${mode} | promptLen=${prompt.length} | ${title.slice(0, 30)}`); +} +db.close(); + +// —— 提交任务(逐个串行,间隔 3s)—— +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); +(async () => { + const results = []; + for (const t of tasks) { + try { + const resp = await fetch(API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ prompt: t.prompt, name: '创作指令' }), + }); + const d = await resp.json(); + results.push({ videoId: t.videoId, id: d.id, ok: d.ok, error: d.error }); + console.log(`SUBMIT v${t.videoId} →`, d.ok ? ('id=' + d.id) : ('ERR ' + d.error)); + } catch (e) { + results.push({ videoId: t.videoId, error: e.message }); + console.log(`SUBMIT v${t.videoId} → EXCEPTION ${e.message}`); + } + await sleep(3000); + } + console.log('\n=== RESULTS ==='); + console.log(JSON.stringify(results, null, 1)); +})(); diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/check-run-status.cjs b/project/短视频脚本创作/V1.0/mcn-work-shop/check-run-status.cjs new file mode 100644 index 0000000..908dfde --- /dev/null +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/check-run-status.cjs @@ -0,0 +1,18 @@ +const { DatabaseSync } = require('node:sqlite'); +const db = new DatabaseSync('C:/Users/maidou/.workbuddy/workbuddy.db', { readOnly: true }); +const fmt = (ms) => ms ? new Date(ms).toLocaleString('zh-CN') : '-'; + +console.log('=== automation_runs 最近 15 条 ==='); +const rows = db.prepare('SELECT thread_id, automation_id, status, thread_title, source_cwd, result_success, created_at, updated_at FROM automation_runs ORDER BY updated_at DESC LIMIT 15').all(); +rows.forEach(r => { + console.log(String(r.thread_id || '').slice(0, 20), '|', r.automation_id, '|', r.status, '|', String(r.thread_title || '').slice(0, 22), '| succ:', r.result_success, '|', fmt(r.created_at), '->', fmt(r.updated_at)); +}); + +console.log('\n=== automations 6 任务 ==='); +const ids = ['automation-1788249381425','automation-1788249384444','automation-1788249387466','automation-1788249390480','automation-1788249393493','automation-1788249396520']; +const q = db.prepare('SELECT id, name, status, schedule_type, scheduled_at, next_run_at, last_run_at, created_at, updated_at FROM automations WHERE id=?'); +for (const id of ids) { + const r = q.get(id); + if (!r) { console.log('NOT FOUND', id); continue; } + console.log(r.id, '|', r.name, '|', r.status, '|', r.schedule_type, '| sched=', r.scheduled_at, '| next=', r.next_run_at, '| last=', r.last_run_at, '| created=', fmt(r.created_at), '| updated=', fmt(r.updated_at)); +} diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/poll-create-tasks.cjs b/project/短视频脚本创作/V1.0/mcn-work-shop/poll-create-tasks.cjs new file mode 100644 index 0000000..72b35e2 --- /dev/null +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/poll-create-tasks.cjs @@ -0,0 +1,51 @@ +/* poll-create-tasks.cjs — 轮询 6 个创作任务状态,全部完成/失败后退出 */ +const TASKS = [ + { videoId: 309, id: 'automation-1788250646656' }, + { videoId: 313, id: 'automation-1788250649673' }, + { videoId: 315, id: 'automation-1788250652690' }, + { videoId: 316, id: 'automation-1788250655707' }, + { videoId: 318, id: 'automation-1788250658730' }, + { videoId: 320, id: 'automation-1788250661754' }, +]; +const API = 'http://localhost:8900/api/run/status?id='; +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +const stateOf = new Map(TASKS.map((t) => [t.videoId, 'queued'])); +let lastPrint = ''; + +async function check() { + const snap = []; + for (const t of TASKS) { + try { + const r = await fetch(API + encodeURIComponent(t.id)); + const d = await r.json(); + stateOf.set(t.videoId, d.state || 'unknown'); + snap.push(`v${t.videoId}:${d.state || '?'}`); + } catch (e) { + snap.push(`v${t.videoId}:ERR`); + } + } + const line = snap.join(' '); + if (line !== lastPrint) { + console.log(new Date().toLocaleTimeString('zh-CN', { hour12: false }) + ' | ' + line); + lastPrint = line; + } + const vals = [...stateOf.values()]; + const done = vals.filter((s) => ['done', 'error', 'notfound'].includes(s)).length; + return done; +} + +(async () => { + console.log('START polling 6 create tasks: ' + new Date().toLocaleTimeString('zh-CN', { hour12: false })); + const t0 = Date.now(); + while (Date.now() - t0 < 3 * 60 * 60 * 1000) { // 最长 3 小时 + const done = await check(); + if (done >= TASKS.length) { + console.log('\nALL DONE: ' + [...stateOf.entries()].map(([v, s]) => `v${v}=${s}`).join(' ')); + process.exit(0); + } + await sleep(30000); + } + console.log('\nTIMEOUT 3h, current: ' + [...stateOf.entries()].map(([v, s]) => `v${v}=${s}`).join(' ')); + process.exit(1); +})(); diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js index ce1c14c..e918e42 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/public/data-pages.js @@ -2,6 +2,10 @@ 依赖 app.js 暴露的工具:esc/api/renderMarkdown/copyText/showToast/fmtSize */ 'use strict'; +/* AI创作技能指令(09-01 修复:任务必须按「短视频脚本创作」技能执行,禁止模型自由发挥导致分镜表格/TXT/MD 格式混杂。 + 定位方式跨环境:优先用 Skill 工具按技能名加载;技能位于用户技能目录 ~/.workbuddy/skills/短视频脚本创作/(部署到用户环境时同此标准位),不写死机器绝对路径 */ +const SKILL_HINT_CREATE = '请调用 Skill 工具加载「短视频脚本创作」技能(技能根目录 = 用户技能目录 ~/.workbuddy/skills/短视频脚本创作/,先读取其 SKILL.md 及 references 规范;若 Skill 工具无法加载,请直接读取该目录下的 SKILL.md 文件)。遵循 S1-S11 完整创作流程(选题→框架→大纲→情绪→结构→脚本),最终输出 S9 标准脚本格式(按场次+对白组织,画面标注【景别|环境|光线|场景|场景元素】),禁止输出分镜头表格、禁止偏离脚本格式。'; + const fmt = (v) => (v == null || v === '') ? '-' : String(v); /* 过万数字以"万"为单位,保留一位小数(如 57141 → 5.7万);已是"x.x万"文本(like_display 抖音原生格式)直接透传(09-01 修复:旧实现 Number('7.8万')=NaN→'-') */ const fmtWan = (v) => { @@ -500,8 +504,8 @@ async function renderAccountDetail(view, id) { : `该视频尚未解析,将先解析视频内容,再基于达人账号设定为达人「${r.name}」创作一条全新故事脚本(新故事、新情节,不复刻原视频)。`, target: `达人「${r.name}」|选题《${r.title}》`, prompt: parsed - ? `请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${tb.block}${createTail}` - : `请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析这个抖音视频并做拆解分析(${r.url ? '视频地址:' + r.url + '\n' : ''}视频标题:《${r.title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。`, + ? `${SKILL_HINT_CREATE}\n请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${tb.block}${createTail}` + : `${SKILL_HINT_CREATE}\n请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析这个抖音视频并做拆解分析(${r.url ? '视频地址:' + r.url + '\n' : ''}视频标题:《${r.title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。`, label: '创作指令', btn: b, }); @@ -640,8 +644,8 @@ async function renderRewrites(view) { : `该视频尚未解析,将先解析视频内容,再为达人「${r.name}」创作一条全新故事脚本(新故事、新情节,不复刻原视频)。`, target: `达人「${r.name}」|选题《${r.title}》`, prompt: parsed - ? `请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${tb.block}${createTail}` - : `请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析这个抖音视频并做拆解分析(${r.url ? '视频地址:' + r.url + '\n' : ''}视频标题:《${r.title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。`, + ? `${SKILL_HINT_CREATE}\n请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}${tb.block}${createTail}` + : `${SKILL_HINT_CREATE}\n请为达人「${r.name}」创作一条全新的短视频故事脚本。${personaBlock}${noPersonaNote}\n该视频尚未解析,请先解析这个抖音视频并做拆解分析(${r.url ? '视频地址:' + r.url + '\n' : ''}视频标题:《${r.title}》),再基于账号设定与提炼出的选题,创作完整故事脚本。`, label: '创作指令', btn: b, }); diff --git a/project/短视频脚本创作/V1.0/mcn-work-shop/server.js b/project/短视频脚本创作/V1.0/mcn-work-shop/server.js index 10ff458..da84adc 100644 --- a/project/短视频脚本创作/V1.0/mcn-work-shop/server.js +++ b/project/短视频脚本创作/V1.0/mcn-work-shop/server.js @@ -564,7 +564,7 @@ const server = http.createServer((req, res) => { const s = String(runs.status || '').toUpperCase(); if (s === 'IN_PROGRESS') state = 'running'; else if (s === 'PENDING_REVIEW') state = 'review'; // 任务停在「待用户确认/已中断」,前端应提示而非假装执行中 - else if (['DONE', 'COMPLETED', 'SUCCESS', 'FINISHED'].includes(s)) state = 'done'; + else if (['DONE', 'COMPLETED', 'SUCCESS', 'FINISHED', 'ACCEPTED'].includes(s)) state = 'done'; // ACCEPTED=客户端调度器终态(成功) else if (['ERROR', 'CANCELLED', 'FAILED', 'INTERRUPTED'].includes(s)) state = 'error'; else state = 'pending'; // ACCEPTED / QUEUED / PENDING 等 = 排队等待执行 }