diff --git a/.gitignore b/.gitignore index 7ab2d94..b44455f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,8 +38,8 @@ data/agents/job-*/ apiconversation/ # Legacy directories (removed from architecture) -conversations/ -skills/ +/conversations/ +/skills/ # Build output dist/ @@ -58,3 +58,4 @@ tmp/ # Lock files (keep package-lock, ignore pnpm if using npm) # pnpm-lock.yaml *.lock +!system/templates/*/.claude/*.lock diff --git a/system/templates/idearuncreate/.claude/skills/create-drama/.env b/system/templates/idearuncreate/.claude/skills/create-drama/.env new file mode 100644 index 0000000..0e443e5 --- /dev/null +++ b/system/templates/idearuncreate/.claude/skills/create-drama/.env @@ -0,0 +1,16 @@ +# === MySQL 数据库 === +DB_HOST=rm-2zeel4sa31na88gd4qo.rwlb.rds.aliyuncs.com +DB_PORT=3306 +DB_USER=lemon +DB_PASSWORD=Lemon@123456 +DB_NAME=shadiao_drama + +# === 阿里云 OSS(图片上传) === +OSS_REGION=oss-cn-beijing +OSS_ACCESS_KEY_ID=LTAI5tGtipK3GrbTwZm7JjTS +OSS_ACCESS_KEY_SECRET=r6axVSGVyIzL3Ty2T5pV1SVFeiir3t +OSS_BUCKET=shadiao-drama +OSS_ENDPOINT=https://oss-cn-beijing.aliyuncs.com + +# === 万界生图 API(Jimeng + Nano Banana Pro) === +WANJIE_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3NzA5NjIyNzgsImtleSI6IjVLNzVaOFROQzlGNEhNMzdQOVk3In0.2axgWHccUPjL3cQr2-1jAGLZ4hVAcTbn-LU78CMMex4 diff --git a/system/templates/idearuncreate/.claude/skills/create-drama/SKILL.md b/system/templates/idearuncreate/.claude/skills/create-drama/SKILL.md new file mode 100644 index 0000000..29b33f9 --- /dev/null +++ b/system/templates/idearuncreate/.claude/skills/create-drama/SKILL.md @@ -0,0 +1,251 @@ +--- +description: 从素材(新闻、故事、段子等)自动生成 classic 互动剧本(含角色图、场景图)并写入数据库 +--- + +# create-drama — 从素材自动生成完整 classic 互动剧本 + +根据用户提供的内容素材(新闻、故事、段子、历史事件等),生成一个完整的 classic 类型互动剧本,包括: +- 剧本数据(角色、台词、表情、动作) +- 角色立绘图(AI 生图 → 去背景 → 上传 OSS) +- 背景图和封面图(AI 生图 → 上传 OSS) +- 写入数据库 + +## 输入 + +用户会在 `$ARGUMENTS` 中提供内容素材。素材可能是: +- 一段新闻报道 +- 一个故事/段子 +- 一个历史事件描述 +- 一个创意想法 +- 一个文件路径(需要先读取文件内容) + +## 执行步骤 + +### 第 1 步:分析素材 + +仔细阅读用户提供的素材内容。如果是文件路径,先用 Read 工具读取文件。 + +### 第 2 步:生成完整剧本 JSON + +根据素材内容,你自己(Claude)直接生成剧本数据 JSON。**不要调用任何外部 LLM API。** + +生成的 JSON 必须严格遵循以下格式: + +```json +{ + "name": "剧本名称(简短,2-6个字)", + "title": "显示标题(可以带 emoji,稍微长一点)", + "subtitle": "— 副标题 —", + "description": "剧本简介(一句话概括)", + "systemPrompt": "用于后续 AI 续写的系统提示词(详见下方规则)", + "isPublic": false, + "userId": 1, + "tags": "标签1,标签2,标签3", + "language": "内容语言:en / zh / multi", + "characters": [ + { + "name": "角色名", + "gender": "male 或 female", + "voiceId": "从音色列表选择", + "imagePrompt": "角色立绘生图 prompt(详见下方规则)" + } + ], + "script": [ + { + "character": "角色名(必须在 characters 中定义过)", + "dialogue": "台词内容", + "avatar": "表情文件名", + "instruction": "(可选)语音情感指令,如 '用愤怒的语气说'、'ASMR 低语'、'质问的语气' 等", + "action": "动画效果", + "position": "left 或 right" + } + ], + "backgroundPrompt": "背景图生图 prompt", + "coverPrompt": "封面图生图 prompt" +} +``` + +### 生成规则 + +#### 角色 (characters) + +- 数量:2-5 个 +- 每个角色必须有 name、gender、voiceId、imagePrompt +- voiceId 选择:参考 [reference.md](reference.md) 中的完整音色列表 + - 所有 2.0 音色统一支持 9 种情感(happy/sad/angry/surprised/fear/hate/excited/coldness/neutral) + - 根据角色性格、年龄、性别匹配合适的音色 + - 常用推荐: + - 男主角: `zh_male_m191_uranus_bigtts`(云舟)或 `zh_male_ruyayichen_uranus_bigtts`(儒雅逸辰) + - 女主角: `zh_female_cancan_uranus_bigtts`(知性灿灿)或 `zh_female_meilinvyou_uranus_bigtts`(魅力女友) + - 少年/年轻男: `zh_male_shaonianzixin_uranus_bigtts`(少年梓辛)或 `saturn_zh_male_shuanglangshaonian_tob`(爽朗少年) + - 甜美/撒娇女: `zh_female_sajiaoxuemei_uranus_bigtts`(撒娇学妹)或 `zh_female_tianmeitaozi_uranus_bigtts`(甜美桃子) + - 成熟男: `zh_male_dayi_uranus_bigtts`(大壹)或 `zh_male_sophie_uranus_bigtts`(魅力苏菲) + - 搞笑/IP: `zh_male_sunwukong_uranus_bigtts`(猴哥)或 `zh_female_peiqi_uranus_bigtts`(佩奇猪) + - 英文男: `en_male_tim_uranus_bigtts`(Tim) + - 英文女: `en_female_dacey_uranus_bigtts`(Dacey)或 `en_female_stokie_uranus_bigtts`(Stokie) + +#### 角色立绘 imagePrompt + +每个角色必须生成 imagePrompt,遵循以下模板: + +``` +生成一张**[人物名字或身份]**的全身站立画像。 +核心风格:沙雕动漫风格(meme style),2D简笔画,线条清晰粗犷,色彩平涂,带有幽默滑稽感,略微的大头小身体比例。 +脸部要求(最重要):脸部必须是纯肉色的完全空白,绝对不要画眼睛、鼻子、嘴巴或眉毛等任何五官,保留一个干净完整的面部轮廓,方便后期添加表情包。 +服装与动作:角色穿着**[根据角色描述填写服装特征],手里拿着[根据角色身份推断合适的道具],[一个有趣的站姿或动作]**。 +背景要求:纯色干净背景(如浅蓝灰色),不要任何环境元素或杂物,边缘清晰,方便直接抠图。 +``` + +根据角色具体描述,在 [] 处填入合适内容。 + +#### 背景图 backgroundPrompt + +``` +横版宽屏场景背景图,卡通风格,适合互动剧背景。场景:[根据剧本内容描述场景]。不要人物,色彩鲜明,气氛感强。 +``` + +#### 封面图 coverPrompt + +``` +剧目封面海报。标题:[剧本标题]。展示[角色名]等角色的剪影轮廓,色彩鲜明,有标题感,卡通风格。 +``` + +#### 台词 (script) + +- 数量:10-20 句(根据素材丰富度调整,尽量多一些,让剧情完整) +- avatar 可选值:`normal.png`, `happy.png`, `very_happy.png`, `angry.png`, `sad.png`, `cry.png`, `shock.png`, `smirk.png`, `speechless.png` +- action 可选值: + - `none` — 默认,无动画 + - `shake` — 震动,适合愤怒/惊吓/强调 + - `jump` — 跳跃,适合惊喜/激动 + - `spin` — 旋转,适合混乱/搞笑 + - `zoom-in` — 放大,适合重要台词/特写 +- instruction(可选):语音情感指令,用自然语言描述这句台词的语气/情绪/表达方式 + - 如:`"用愤怒的语气说"`、`"ASMR 低语"`、`"质问的语气"`、`"开心地大喊"` + - 会传给 TTS 引擎的 seed-tts-2.0-expressive 模型,增强语音表现力 + - 对于情绪强烈的台词建议添加此字段 +- position: 每个角色应该有固定站位(left/right),2个角色时一左一右,3个以上轮替 +- 台词要生动有趣,符合互动剧的沙雕风格 +- 对话要有戏剧冲突和反转 +- 每句台词不要太长,控制在 30 字以内 + +#### systemPrompt + +- 要详细描述世界观和角色设定 +- 包含语言风格指导(用于后续 AI 续写对话) +- 至少 200 字 + +#### tags + +- 1-3 个标签,逗号分隔 +- 如:`搞笑,职场,反转` + +#### language + +- 内容语言标识:`en`(英文)、`zh`(中文)、`multi`(混合) +- 根据素材语言自动设置 + +### 第 3 步:写入临时 JSON 文件 + +将生成的 JSON 写入临时文件 `/tmp/drama_.json` + +### 第 4 步:生成图片并上传 OSS + +找到本 skill 的目录路径(即本文件 SKILL.md 所在目录),执行图片生成脚本: + +```bash +node /generate-images.js /tmp/drama_.json +``` + +该脚本会: +1. 为每个角色生成立绘图(Jimeng → fallback Nano Banana Pro) +2. 自动去背景(rembg) +3. 为剧本生成背景图和封面图 +4. 全部上传到阿里云 OSS +5. 将 URL 写回 JSON 文件(更新 characters[].bodyUrl, coverUrl, backgroundUrl) + +**注意**:该脚本需要 `ali-oss`, `sharp` npm 包以及 `rembg` Python 工具。如果 rembg 未安装,会跳过去背景步骤。 + +### 第 5 步:写入数据库 + +执行入库脚本: + +```bash +node /insert-drama.js /tmp/drama_.json +``` + +脚本会在事务中创建: +- `dramas` 表记录(剧本元数据) +- `drama_characters` 表记录(角色配置 + 身体图 URL + 音色) +- `drama_emotions` 表记录(默认表情映射) +- `drama_scripts` 表记录(台词序列) + +### 第 6 步:清理并输出结果 + +1. 删除临时 JSON 文件 +2. 告知用户: + - 剧本名称和 dramaKey + - 角色列表及音色 + - 台词数量 + - 图片生成结果(成功/失败) + - 简要剧情概述 + +## 配置说明 + +脚本会自动从以下位置加载配置(按优先级): +1. `--env ` 参数指定的 .env 文件 +2. 环境变量 `DRAMA_ENV_PATH` 指定的 .env 文件 +3. skill 目录下的 `.env` 文件 +4. 已有的环境变量 + +所需配置项: + +```env +# 数据库 +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=xxx +DB_NAME=shadiao_drama + +# 阿里云 OSS(图片上传) +OSS_REGION=oss-cn-beijing +OSS_ACCESS_KEY_ID=xxx +OSS_ACCESS_KEY_SECRET=xxx +OSS_BUCKET=xxx +OSS_ENDPOINT=https://oss-cn-beijing.aliyuncs.com + +# 万界生图 API +WANJIE_API_KEY=xxx +``` + +## 注意事项 + +- **自动检测语言**:根据素材的语言自动决定输出语言。中文素材生成中文内容,英文素材生成英文内容,并在 JSON 中设置对应的 `language` 字段(`zh`/`en`/`multi`) +- 英文内容必须使用英文音色(Tim, Dacey, Stokie) +- 风格偏沙雕搞笑,但要保留素材的核心信息 +- 如果素材是严肃新闻,可以用吐槽/解说风格改编 +- 角色名字要有趣,符合素材主题 + +## 复用到其他项目 + +整个 `create-drama/` 目录是自包含的,复制到其他项目的 `.claude/skills/` 即可使用。 + +**前置条件**: +1. Node.js 依赖:`npm i -g sequelize mysql2 ali-oss sharp` +2. Python 工具:`pip install rembg`(可选,用于角色图去背景) +3. 在 `create-drama/` 目录下修改 `.env` 配置 + +**目录结构**: +``` +.claude/skills/create-drama/ +├── SKILL.md # Skill 定义(本文件) +├── reference.md # 完整音色参考文档 +├── insert-drama.js # 数据库入库脚本 +├── generate-images.js # 图片生成 + 去背景 + OSS 上传脚本 +└── .env # 数据库 + OSS + 生图 API 配置 +``` + +## Additional Resources + +- 完整音色列表及情感参数:[reference.md](reference.md) diff --git a/system/templates/idearuncreate/.claude/skills/create-drama/generate-images.js b/system/templates/idearuncreate/.claude/skills/create-drama/generate-images.js new file mode 100644 index 0000000..c62605d --- /dev/null +++ b/system/templates/idearuncreate/.claude/skills/create-drama/generate-images.js @@ -0,0 +1,411 @@ +#!/usr/bin/env node +/** + * generate-images.js — 为剧本生成角色图、背景图、封面图,并上传到 OSS + * + * 用法: + * node generate-images.js + * + * 输入 JSON 中需要包含: + * - characters[].imagePrompt (角色生图 prompt,由 SKILL.md 指导 Claude 生成) + * - backgroundPrompt (背景图 prompt) + * - coverPrompt (封面图 prompt) + * - dramaKey (用于 OSS 路径) + * + * 输出: 在原 JSON 基础上补充 URL 字段,写回文件 + * - characters[].bodyUrl + * - coverUrl + * - backgroundUrl + * + * 依赖: ali-oss, sharp (可选), rembg (Python, 可选) + */ + +const path = require('path'); +const fs = require('fs'); +const https = require('https'); +const http = require('http'); +const { execSync } = require('child_process'); +const os = require('os'); + +// ---------- 加载 .env ---------- +function loadEnv() { + const envArgIdx = process.argv.indexOf('--env'); + const candidates = [ + envArgIdx !== -1 ? path.resolve(process.argv[envArgIdx + 1]) : null, + process.env.DRAMA_ENV_PATH ? path.resolve(process.env.DRAMA_ENV_PATH) : null, + path.resolve(__dirname, '.env'), + ].filter(Boolean); + + for (const envPath of candidates) { + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf-8'); + for (const line of envContent.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const val = trimmed.slice(eqIdx + 1).trim(); + if (!process.env[key]) process.env[key] = val; + } + console.log(`已加载配置: ${envPath}`); + return; + } + } + console.log('未找到 .env 文件,使用环境变量'); +} +loadEnv(); + +// ========== Wanjie 图片生成 API ========== + +const WANJIE_BASE = 'maas-openapi.wanjiedata.com'; + +function wanjieRequest(action, body) { + const apiKey = process.env.WANJIE_API_KEY; + if (!apiKey) throw new Error('缺少 WANJIE_API_KEY 环境变量'); + + const jsonBody = JSON.stringify(body); + return new Promise((resolve, reject) => { + const req = https.request({ + hostname: WANJIE_BASE, + path: `/api/jimeng/v1?Action=${action}&Version=2022-08-31`, + method: 'POST', + headers: { + 'Authorization': apiKey, + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(jsonBody), + }, + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + try { resolve(JSON.parse(data)); } + catch (err) { reject(new Error(`解析响应失败: ${data.slice(0, 300)}`)); } + }); + }); + req.on('error', reject); + req.write(jsonBody); + req.end(); + }); +} + +/** Jimeng 文生图(异步模式) */ +async function generateImageJimeng(prompt, width = 1024, height = 1024) { + const res = await wanjieRequest('CVSync2AsyncSubmitTask', { + req_key: 'jimeng_t2i_v40', + prompt, width, height, + }); + + if (res.code !== 10000 || !res.data?.task_id) { + throw new Error(`Jimeng 提交失败: ${JSON.stringify(res).slice(0, 300)}`); + } + + const taskId = res.data.task_id; + console.log(` [Jimeng] 任务提交: task_id=${taskId}`); + + // 轮询等待 + const start = Date.now(); + while (Date.now() - start < 120000) { + const poll = await wanjieRequest('CVSync2AsyncGetResult', { + req_key: 'jimeng_t2i_v40', task_id: taskId, + }); + + if (poll.code === 10000 && poll.data?.image_urls?.length > 0) { + const buffer = await downloadUrl(poll.data.image_urls[0]); + return { buffer, mimeType: 'image/png' }; + } + if (poll.code === 10000 && poll.data?.binary_data_base64?.length > 0) { + return { buffer: Buffer.from(poll.data.binary_data_base64[0], 'base64'), mimeType: 'image/png' }; + } + if (poll.code && poll.code !== 10000 && !poll.message?.includes('processing') && !poll.message?.includes('pending')) { + throw new Error(`Jimeng 任务失败: ${JSON.stringify(poll).slice(0, 300)}`); + } + + await sleep(3000); + } + throw new Error(`Jimeng 超时: task_id=${taskId}`); +} + +/** Nano Banana Pro 文生图(同步模式,fallback) */ +async function generateImageNano(prompt) { + const apiKey = process.env.WANJIE_API_KEY; + if (!apiKey) throw new Error('缺少 WANJIE_API_KEY'); + + const url = `https://${WANJIE_BASE}/api/v1beta/models/gemini-3-pro-image-preview:generateContent`; + const body = JSON.stringify({ contents: [{ parts: [{ text: prompt }] }] }); + + for (let attempt = 1; attempt <= 3; attempt++) { + try { + const data = await httpsPost(url, body, { 'Authorization': `Bearer ${apiKey}` }); + const response = JSON.parse(data); + const img = extractImageFromResponse(response); + if (img) return img; + throw new Error('无图片数据'); + } catch (err) { + if (err.statusCode === 429 && attempt < 3) { + console.warn(` [Nano] 429 限流,${3 * attempt}s 后重试...`); + await sleep(3000 * attempt); + continue; + } + throw err; + } + } +} + +function extractImageFromResponse(response) { + const parts = response?.candidates?.[0]?.content?.parts; + if (!parts) return null; + for (const part of parts) { + if (part.inlineData) { + return { buffer: Buffer.from(part.inlineData.data, 'base64'), mimeType: part.inlineData.mimeType || 'image/png' }; + } + if (part.text && part.text.includes('data:image')) { + const match = part.text.match(/data:image\/(png|jpeg);base64,([A-Za-z0-9+/=]+)/); + if (match) return { buffer: Buffer.from(match[2], 'base64'), mimeType: `image/${match[1]}` }; + } + } + return null; +} + +/** 生图:优先 Jimeng,fallback Nano */ +async function generateImage(prompt) { + try { + return await generateImageJimeng(prompt); + } catch (jimengErr) { + console.warn(` [Jimeng] 失败: ${jimengErr.message.slice(0, 80)},fallback Nano...`); + return await generateImageNano(prompt); + } +} + +// ========== 图片处理 ========== + +/** 去背景(rembg) */ +function removeBg(inputBuffer) { + const tmpDir = os.tmpdir(); + const ts = Date.now(); + const inputPath = path.join(tmpDir, `rembg_in_${ts}.png`); + const outputPath = path.join(tmpDir, `rembg_out_${ts}.png`); + + try { + fs.writeFileSync(inputPath, inputBuffer); + // 使用 Python 脚本调用 rembg 核心 API(避免 CLI 依赖问题) + const pyScript = ` +import sys +from rembg import remove +from PIL import Image +import io + +input_path = sys.argv[1] +output_path = sys.argv[2] + +input_img = Image.open(input_path) +output_img = remove(input_img) +output_img.save(output_path) +`; + const pyScriptPath = path.join(tmpDir, `rembg_${ts}.py`); + fs.writeFileSync(pyScriptPath, pyScript); + + execSync(`python3 "${pyScriptPath}" "${inputPath}" "${outputPath}"`, { timeout: 60000 }); + const result = fs.readFileSync(outputPath); + console.log(' [rembg] 去背景成功'); + + // 清理 Python 脚本 + try { fs.unlinkSync(pyScriptPath); } catch { } + + // 尝试 sharp 裁剪 + try { + const sharp = require('sharp'); + // sharp 操作是 async,但我们在 sync context 用 execSync 调 sharp + // 改用 async + return trimImage(result); + } catch { + return result; + } + } catch (err) { + console.warn(` [rembg] 失败(${err.message.slice(0, 60)}),返回原图`); + return inputBuffer; + } finally { + try { fs.unlinkSync(inputPath); } catch { } + try { fs.unlinkSync(outputPath); } catch { } + } +} + +async function trimImage(buffer) { + try { + const sharp = require('sharp'); + const trimmed = await sharp(buffer).trim().png().toBuffer(); + const meta = await sharp(trimmed).metadata(); + const padX = Math.round(meta.width * 0.05); + const padY = Math.round(meta.height * 0.05); + return await sharp(trimmed) + .extend({ top: padY, bottom: padY, left: padX, right: padX, background: { r: 0, g: 0, b: 0, alpha: 0 } }) + .png().toBuffer(); + } catch { + return buffer; + } +} + +// ========== OSS 上传 ========== + +function getOSSClient() { + const OSS = require('ali-oss'); + return new OSS({ + region: process.env.OSS_REGION, + accessKeyId: process.env.OSS_ACCESS_KEY_ID, + accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET, + bucket: process.env.OSS_BUCKET, + }); +} + +function getPublicUrl(key) { + const endpoint = process.env.OSS_ENDPOINT || `https://${process.env.OSS_REGION}.aliyuncs.com`; + const bucket = process.env.OSS_BUCKET; + return `https://${bucket}.${endpoint.replace('https://', '')}/${key}`; +} + +async function uploadToOSS(ossKey, buffer, contentType = 'image/png') { + const client = getOSSClient(); + const result = await client.put(ossKey, buffer, { headers: { 'Content-Type': contentType } }); + return getPublicUrl(result.name); +} + +// ========== 工具函数 ========== + +function sleep(ms) { return new Promise(r => setTimeout(r, ms)); } + +function httpsPost(url, body, extraHeaders = {}) { + return new Promise((resolve, reject) => { + const urlObj = new URL(url); + const req = https.request({ + hostname: urlObj.hostname, + path: urlObj.pathname + urlObj.search, + method: 'POST', + headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body), ...extraHeaders }, + }, (res) => { + let data = ''; + res.on('data', chunk => data += chunk); + res.on('end', () => { + if (res.statusCode === 429) { const e = new Error('429'); e.statusCode = 429; reject(e); return; } + resolve(data); + }); + }); + req.on('error', reject); + req.write(body); + req.end(); + }); +} + +function downloadUrl(url) { + return new Promise((resolve, reject) => { + const protocol = url.startsWith('https') ? https : http; + protocol.get(url, (res) => { + if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) { + return downloadUrl(res.headers.location).then(resolve).catch(reject); + } + const chunks = []; + res.on('data', chunk => chunks.push(chunk)); + res.on('end', () => resolve(Buffer.concat(chunks))); + res.on('error', reject); + }).on('error', reject); + }); +} + +// ========== 主逻辑 ========== + +async function main() { + const filePath = process.argv.find(a => a.endsWith('.json')); + if (!filePath) { + console.error('用法: node generate-images.js '); + process.exit(1); + } + + const absPath = path.resolve(filePath); + const data = JSON.parse(fs.readFileSync(absPath, 'utf-8')); + const dramaKey = data.dramaKey || `drama_${Date.now().toString(36)}`; + data.dramaKey = dramaKey; + + // 检查 OSS 配置 + const ossOk = process.env.OSS_REGION && process.env.OSS_ACCESS_KEY_ID && process.env.OSS_ACCESS_KEY_SECRET && process.env.OSS_BUCKET; + if (!ossOk) { + console.error('OSS 配置不完整,请检查 .env 中的 OSS_REGION, OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET, OSS_BUCKET'); + process.exit(1); + } + + // 检查生图 API 配置 + if (!process.env.WANJIE_API_KEY) { + console.error('缺少 WANJIE_API_KEY'); + process.exit(1); + } + + const characters = data.characters || []; + const total = characters.length + 2; // chars + background + cover + let completed = 0; + const tick = (label) => { completed++; console.log(`[${completed}/${total}] ${label} 完成`); }; + + // 并行处理所有图片 + const tasks = []; + + // 角色图 + for (const char of characters) { + tasks.push((async () => { + const prompt = char.imagePrompt || `全身立绘,纯色背景,沙雕动漫风格。角色:${char.name}。脸部空白无五官。`; + console.log(`\n生成角色图: ${char.name}`); + console.log(` prompt: ${prompt.slice(0, 80)}...`); + + const { buffer } = await generateImage(prompt); + + // 去背景 + console.log(` 正在去背景...`); + const processed = await removeBg(buffer); + + // 上传 OSS + const safeName = Buffer.from(char.name).toString('hex').slice(0, 16); + const ossKey = `dramas/${dramaKey}/characters/${safeName}.png`; + const url = await uploadToOSS(ossKey, processed); + char.bodyUrl = url; + console.log(` 上传完成: ${url}`); + tick(char.name); + })()); + } + + // 背景图 + tasks.push((async () => { + const prompt = data.backgroundPrompt || `横版场景背景图,宽屏比例,场景:${data.description || data.name},卡通风格,无人物。`; + console.log(`\n生成背景图`); + console.log(` prompt: ${prompt.slice(0, 80)}...`); + + const { buffer } = await generateImage(prompt); + const ossKey = `dramas/${dramaKey}/background.png`; + const url = await uploadToOSS(ossKey, buffer); + data.backgroundUrl = url; + console.log(` 上传完成: ${url}`); + tick('背景图'); + })()); + + // 封面图 + tasks.push((async () => { + const prompt = data.coverPrompt || `剧目封面海报,标题:${data.title || data.name},色彩鲜明。`; + console.log(`\n生成封面图`); + console.log(` prompt: ${prompt.slice(0, 80)}...`); + + const { buffer } = await generateImage(prompt); + const ossKey = `dramas/${dramaKey}/cover.png`; + const url = await uploadToOSS(ossKey, buffer); + data.coverUrl = url; + console.log(` 上传完成: ${url}`); + tick('封面图'); + })()); + + await Promise.allSettled(tasks); + + // 写回文件 + fs.writeFileSync(absPath, JSON.stringify(data, null, 2), 'utf-8'); + console.log(`\n图片生成完成,已更新 JSON 文件: ${absPath}`); + console.log(` 角色图: ${characters.filter(c => c.bodyUrl).length}/${characters.length}`); + console.log(` 背景图: ${data.backgroundUrl ? '✓' : '✗'}`); + console.log(` 封面图: ${data.coverUrl ? '✓' : '✗'}`); +} + +main().catch(err => { + console.error('执行失败:', err); + process.exit(1); +}); diff --git a/system/templates/idearuncreate/.claude/skills/create-drama/insert-drama.js b/system/templates/idearuncreate/.claude/skills/create-drama/insert-drama.js new file mode 100644 index 0000000..865a18c --- /dev/null +++ b/system/templates/idearuncreate/.claude/skills/create-drama/insert-drama.js @@ -0,0 +1,332 @@ +#!/usr/bin/env node +/** + * insert-drama.js — 独立脚本:将 classic 类型剧本数据插入数据库 + * + * 用法: + * node insert-drama.js + * echo '{"name":...}' | node insert-drama.js --stdin + * + * JSON 格式: + * { + * "name": "剧本名称", + * "title": "显示标题(可带 emoji)", + * "subtitle": "副标题", + * "description": "剧本简介", + * "systemPrompt": "用于 AI 续写的系统提示词", + * "isPublic": true, + * "userId": 1, + * "language": "en", + * "characters": [ + * { "name": "角色名", "gender": "male", "voiceId": "zh_male_m191_uranus_bigtts" } + * ], + * "script": [ + * { "character": "角色名", "dialogue": "台词", "avatar": "happy.png", "instruction": "用开心的语气说", "action": "none", "position": "left" } + * ] + * } + */ + +const path = require('path'); +const fs = require('fs'); + +// 加载 .env 文件(优先级:--env 参数 > 环境变量 DRAMA_ENV_PATH > 脚本同目录 .env) +function loadEnv() { + const envArgIdx = process.argv.indexOf('--env'); + const candidates = [ + envArgIdx !== -1 ? path.resolve(process.argv[envArgIdx + 1]) : null, + process.env.DRAMA_ENV_PATH ? path.resolve(process.env.DRAMA_ENV_PATH) : null, + path.resolve(__dirname, '.env'), + ].filter(Boolean); + + for (const envPath of candidates) { + if (fs.existsSync(envPath)) { + const envContent = fs.readFileSync(envPath, 'utf-8'); + for (const line of envContent.split('\n')) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) continue; + const eqIdx = trimmed.indexOf('='); + if (eqIdx === -1) continue; + const key = trimmed.slice(0, eqIdx).trim(); + const val = trimmed.slice(eqIdx + 1).trim(); + if (!process.env[key]) process.env[key] = val; + } + console.log(`已加载配置: ${envPath}`); + return; + } + } + console.log('未找到 .env 文件,使用环境变量或默认值'); +} +loadEnv(); + +const { Sequelize, DataTypes } = require('sequelize'); + +// ---------- DB 连接 ---------- +const sequelize = new Sequelize( + process.env.DB_NAME || 'shadiao_drama', + process.env.DB_USER || 'root', + process.env.DB_PASSWORD || '', + { + host: process.env.DB_HOST || '127.0.0.1', + port: parseInt(process.env.DB_PORT || '3306', 10), + dialect: 'mysql', + logging: false, + timezone: '+08:00', + define: { charset: 'utf8mb4', collate: 'utf8mb4_unicode_ci' }, + } +); + +// ---------- Model 定义(内联,避免依赖项目代码) ---------- +const Drama = sequelize.define('Drama', { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + dramaKey: { type: DataTypes.STRING(50), allowNull: false, unique: true, field: 'drama_key' }, + name: { type: DataTypes.STRING(100), allowNull: false }, + title: { type: DataTypes.STRING(100), allowNull: true }, + subtitle: { type: DataTypes.STRING(200), allowNull: true }, + description: { type: DataTypes.TEXT, allowNull: true }, + coverUrl: { type: DataTypes.STRING(500), allowNull: true, field: 'cover_url' }, + backgroundUrl: { type: DataTypes.STRING(500), allowNull: true, field: 'background_url' }, + systemPrompt: { type: DataTypes.TEXT, allowNull: true, field: 'system_prompt' }, + status: { type: DataTypes.TINYINT, defaultValue: 1 }, + userId: { type: DataTypes.INTEGER, allowNull: true, field: 'user_id' }, + isPublic: { type: DataTypes.BOOLEAN, defaultValue: true, field: 'is_public' }, + type: { type: DataTypes.STRING(30), defaultValue: 'classic' }, + emojiPack: { type: DataTypes.STRING(50), defaultValue: 'default', field: 'emoji_pack' }, + vrmConfig: { type: DataTypes.TEXT, allowNull: true, field: 'vrm_config' }, + tags: { type: DataTypes.STRING(255), allowNull: true }, + score: { type: DataTypes.DECIMAL(10, 2), defaultValue: 0 }, + language: { type: DataTypes.STRING(10), defaultValue: 'en' }, +}, { tableName: 'dramas', timestamps: true, underscored: true }); + +const DramaCharacter = sequelize.define('DramaCharacter', { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + dramaId: { type: DataTypes.INTEGER, allowNull: false, field: 'drama_id' }, + characterName: { type: DataTypes.STRING(50), allowNull: false, field: 'character_name' }, + bodyUrl: { type: DataTypes.STRING(500), allowNull: true, field: 'body_url' }, + faceOffsetTop: { type: DataTypes.STRING(20), allowNull: true, field: 'face_offset_top' }, + faceOffsetLeft: { type: DataTypes.STRING(20), allowNull: true, field: 'face_offset_left' }, + faceOffsetWidth: { type: DataTypes.STRING(20), allowNull: true, field: 'face_offset_width' }, + voiceId: { type: DataTypes.STRING(100), allowNull: true, field: 'voice_id' }, + isDefault: { type: DataTypes.TINYINT, defaultValue: 0, field: 'is_default' }, +}, { tableName: 'drama_characters', timestamps: false, underscored: true }); + +const DramaEmotion = sequelize.define('DramaEmotion', { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + dramaId: { type: DataTypes.INTEGER, allowNull: false, field: 'drama_id' }, + avatarKey: { type: DataTypes.STRING(50), allowNull: false, field: 'avatar_key' }, + faceUrl: { type: DataTypes.STRING(500), allowNull: true, field: 'face_url' }, + emotionName: { type: DataTypes.STRING(30), allowNull: true, field: 'emotion_name' }, + emotionScale: { type: DataTypes.INTEGER, defaultValue: 3, field: 'emotion_scale' }, + isDefault: { type: DataTypes.TINYINT, defaultValue: 0, field: 'is_default' }, + characterName: { type: DataTypes.STRING(50), allowNull: true, field: 'character_name' }, +}, { tableName: 'drama_emotions', timestamps: false, underscored: true }); + +const DramaScript = sequelize.define('DramaScript', { + id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, + dramaId: { type: DataTypes.INTEGER, allowNull: false, field: 'drama_id' }, + seq: { type: DataTypes.INTEGER, allowNull: false }, + characterName: { type: DataTypes.STRING(50), allowNull: false, field: 'character_name' }, + avatar: { type: DataTypes.STRING(50), allowNull: true }, + dialogue: { type: DataTypes.TEXT, allowNull: false }, + action: { type: DataTypes.STRING(30), allowNull: true }, + position: { type: DataTypes.STRING(10), allowNull: true }, + sfx: { type: DataTypes.STRING(100), allowNull: true }, + audio: { type: DataTypes.STRING(500), allowNull: true }, +}, { tableName: 'drama_scripts', timestamps: false, underscored: true }); + +// ---------- 默认表情配置 ---------- +const DEFAULT_EMOJI_BASE = 'https://shadiao-drama.oss-cn-beijing.aliyuncs.com/dramas/zhenhuan_chuan/emojis'; +const DEFAULT_EMOJIS = [ + { key: 'normal.png', name: '正常', emotion: 'neutral', scale: 3 }, + { key: 'happy.png', name: '开心', emotion: 'happy', scale: 4 }, + { key: 'very_happy.png', name: '非常开心', emotion: 'happy', scale: 5 }, + { key: 'angry.png', name: '生气', emotion: 'angry', scale: 4 }, + { key: 'sad.png', name: '伤心', emotion: 'sad', scale: 4 }, + { key: 'cry.png', name: '哭', emotion: 'sad', scale: 5 }, + { key: 'shock.png', name: '惊讶', emotion: 'surprised', scale: 5 }, + { key: 'smirk.png', name: '撇嘴', emotion: 'happy', scale: 3 }, + { key: 'speechless.png', name: '无语', emotion: 'neutral', scale: 2 }, +]; + +// ---------- 音色情绪兼容映射 ---------- +// 所有 2.0 音色统一支持 9 种情感 +const COMMON_EMOTIONS = ['happy','sad','angry','surprised','fear','hate','excited','coldness','neutral']; + +function validateEmotion(speaker, emotion) { + if (!emotion || emotion === 'neutral') return emotion; + // 2.0 音色统一支持所有常见情感 + return COMMON_EMOTIONS.includes(emotion) ? emotion : 'neutral'; +} + +// ---------- 构建默认表情记录 ---------- +function buildEmotionRecords(dramaId, characters, defaultVoice) { + const records = []; + const charVoiceMap = {}; + if (characters) { + for (const char of characters) { + if (char.voiceId) charVoiceMap[char.name] = char.voiceId; + } + } + + // 共享默认表情 + for (const emo of DEFAULT_EMOJIS) { + const validEmotion = validateEmotion(defaultVoice, emo.emotion) || 'neutral'; + records.push({ + dramaId, + avatarKey: emo.key, + faceUrl: `${DEFAULT_EMOJI_BASE}/${encodeURIComponent(emo.name)}.png`, + emotionName: validEmotion, + emotionScale: emo.scale, + isDefault: 0, + characterName: null, + }); + } + records.push({ + dramaId, + avatarKey: '_default', + faceUrl: `${DEFAULT_EMOJI_BASE}/${encodeURIComponent('正常')}.png`, + emotionName: 'neutral', + emotionScale: 3, + isDefault: 1, + characterName: null, + }); + + return records; +} + +// ---------- 主逻辑 ---------- +async function main() { + let jsonStr; + + if (process.argv.includes('--stdin')) { + // 从 stdin 读取 + jsonStr = ''; + for await (const chunk of process.stdin) { + jsonStr += chunk; + } + } else if (process.argv[2]) { + // 从文件读取 + const filePath = path.resolve(process.argv[2]); + if (!fs.existsSync(filePath)) { + console.error(`文件不存在: ${filePath}`); + process.exit(1); + } + jsonStr = fs.readFileSync(filePath, 'utf-8'); + } else { + console.error('用法: node insert-drama.js 或 node insert-drama.js --stdin'); + process.exit(1); + } + + let data; + try { + data = JSON.parse(jsonStr); + } catch (e) { + console.error('JSON 解析失败:', e.message); + process.exit(1); + } + + const { name, title, subtitle, description, systemPrompt, isPublic, userId, characters, script, tags, language } = data; + + if (!name || !script || !Array.isArray(script) || script.length === 0) { + console.error('缺少必要字段: name, script'); + process.exit(1); + } + if (!characters || !Array.isArray(characters) || characters.length === 0) { + console.error('缺少必要字段: characters'); + process.exit(1); + } + + // 连接数据库 + await sequelize.authenticate(); + console.log('数据库连接成功'); + + const t = await sequelize.transaction(); + + try { + const dramaKey = `user_${userId || 1}_${Date.now().toString(36)}`; + + // 1. 创建 Drama 记录 + const drama = await Drama.create({ + dramaKey, + name, + title: title || name, + subtitle: subtitle || '', + description: description || '', + coverUrl: data.coverUrl || '', + backgroundUrl: data.backgroundUrl || '', + systemPrompt: systemPrompt || '', + status: 1, + userId: userId || null, + isPublic: isPublic !== false, + type: 'classic', + emojiPack: 'default', + tags: tags || null, + language: language || 'en', + }, { transaction: t }); + + // 2. 创建角色记录 + const charRecords = characters.map(char => ({ + dramaId: drama.id, + characterName: char.name, + bodyUrl: char.bodyUrl || '', + faceOffsetTop: char.faceOffset?.top || '8%', + faceOffsetLeft: char.faceOffset?.left || '35%', + faceOffsetWidth: char.faceOffset?.width || '30%', + voiceId: char.voiceId || '', + isDefault: char.isDefault ? 1 : 0, + })); + // 添加 _default 角色 + const firstChar = characters[0]; + charRecords.push({ + dramaId: drama.id, + characterName: '_default', + bodyUrl: firstChar.bodyUrl || '', + faceOffsetTop: firstChar.faceOffset?.top || '8%', + faceOffsetLeft: firstChar.faceOffset?.left || '35%', + faceOffsetWidth: firstChar.faceOffset?.width || '30%', + voiceId: firstChar.voiceId || '', + isDefault: 1, + }); + await DramaCharacter.bulkCreate(charRecords, { transaction: t }); + + // 3. 创建表情记录 + const defaultVoice = characters[0]?.voiceId || ''; + const emotionRecords = buildEmotionRecords(drama.id, characters, defaultVoice); + await DramaEmotion.bulkCreate(emotionRecords, { transaction: t }); + + // 4. 创建台词记录 + const scriptRecords = script.map((line, i) => ({ + dramaId: drama.id, + seq: i, + characterName: line.character, + avatar: line.avatar || 'normal.png', + dialogue: line.dialogue, + action: line.action || 'none', + position: line.position || 'left', + sfx: line.sfx || null, + audio: null, + })); + await DramaScript.bulkCreate(scriptRecords, { transaction: t }); + + await t.commit(); + + console.log(`\n剧本创建成功!`); + console.log(` ID: ${drama.id}`); + console.log(` Key: ${dramaKey}`); + console.log(` 名称: ${name}`); + console.log(` 角色数: ${characters.length}`); + console.log(` 台词数: ${script.length}`); + + // 输出 JSON 结果供调用方解析 + console.log(`\n__RESULT__${JSON.stringify({ id: drama.id, dramaKey, name })}__END__`); + } catch (err) { + await t.rollback(); + console.error('插入失败:', err.message); + process.exit(1); + } finally { + await sequelize.close(); + } +} + +main().catch(err => { + console.error('执行失败:', err); + process.exit(1); +}); diff --git a/system/templates/idearuncreate/.claude/skills/create-drama/reference.md b/system/templates/idearuncreate/.claude/skills/create-drama/reference.md new file mode 100644 index 0000000..219c0b4 --- /dev/null +++ b/system/templates/idearuncreate/.claude/skills/create-drama/reference.md @@ -0,0 +1,80 @@ +# 音色参考文档 + +## 重要说明 + +所有音色现已升级为 2.0 版本,统一支持以下 9 种情感: +``` +happy, sad, angry, surprised, fear, hate, excited, coldness, neutral +``` + +不再需要查看每个音色单独支持哪些情感 — 全部音色都支持上述所有情感。 + +## 中文 2.0 音色(uranus) + +| 音色名称 | voice_type | 性别 | 风格 | +|---------|-----------|------|------| +| Vivi 2.0 | `zh_female_vv_uranus_bigtts` | 女 | 通用女声 | +| 小何 2.0 | `zh_female_xiaohe_uranus_bigtts` | 女 | 通用女声 | +| 清新女声 2.0 | `zh_female_qingxinnvsheng_uranus_bigtts` | 女 | 清新自然 | +| 知性灿灿 2.0 | `zh_female_cancan_uranus_bigtts` | 女 | 知性优雅 | +| 撒娇学妹 2.0 | `zh_female_sajiaoxuemei_uranus_bigtts` | 女 | 撒娇甜美 | +| 甜美小源 2.0 | `zh_female_tianmeixiaoyuan_uranus_bigtts` | 女 | 甜美可爱 | +| 甜美桃子 2.0 | `zh_female_tianmeitaozi_uranus_bigtts` | 女 | 甜美可爱 | +| 爽快思思 2.0 | `zh_female_shuangkuaisisi_uranus_bigtts` | 女 | 爽快直率 | +| 邻家女孩 2.0 | `zh_female_linjianvhai_uranus_bigtts` | 女 | 清新邻家 | +| 暖阳女声 2.0 | `zh_female_kefunvsheng_uranus_bigtts` | 女 | 温暖亲切 | +| 魅力女友 2.0 | `zh_female_meilinvyou_uranus_bigtts` | 女 | 魅力女友 | +| 流畅女声 2.0 | `zh_female_liuchangnv_uranus_bigtts` | 女 | 流畅自然 | +| 鸡汤女 2.0 | `zh_female_jitangnv_uranus_bigtts` | 女 | 治愈鸡汤 | +| 儿童绘本 2.0 | `zh_female_xiaoxue_uranus_bigtts` | 女 | 儿童/绘本 | +| Tina老师 2.0 | `zh_female_yingyujiaoxue_uranus_bigtts` | 女 | 英语教学 | +| 佩奇猪 2.0 | `zh_female_peiqi_uranus_bigtts` | 女 | IP 趣味 | +| 黑猫侦探社咪仔 2.0 | `zh_female_mizai_uranus_bigtts` | 女 | IP 趣味 | +| 云舟 2.0 | `zh_male_m191_uranus_bigtts` | 男 | 通用男声 | +| 小天 2.0 | `zh_male_taocheng_uranus_bigtts` | 男 | 通用男声 | +| 刘飞 2.0 | `zh_male_liufei_uranus_bigtts` | 男 | 通用男声 | +| 魅力苏菲 2.0 | `zh_male_sophie_uranus_bigtts` | 男 | 魅力男声 | +| 少年梓辛 2.0 | `zh_male_shaonianzixin_uranus_bigtts` | 男 | 少年音 | +| 大壹 2.0 | `zh_male_dayi_uranus_bigtts` | 男 | 成熟男声 | +| 儒雅逸辰 2.0 | `zh_male_ruyayichen_uranus_bigtts` | 男 | 儒雅成熟 | +| 猴哥 2.0 | `zh_male_sunwukong_uranus_bigtts` | 男 | IP 趣味 | + +## 英文 2.0 音色(uranus) + +| 音色名称 | voice_type | 性别 | +|---------|-----------|------| +| Tim | `en_male_tim_uranus_bigtts` | 男 | +| Dacey | `en_female_dacey_uranus_bigtts` | 女 | +| Stokie | `en_female_stokie_uranus_bigtts` | 女 | + +## Saturn 音色(角色扮演 / 特定风格) + +| 音色名称 | voice_type | 性别 | 风格 | +|---------|-----------|------|------| +| 可爱女生 | `saturn_zh_female_keainvsheng_tob` | 女 | 可爱 | +| 调皮公主 | `saturn_zh_female_tiaopigongzhu_tob` | 女 | 调皮 | +| 知性灿灿 | `saturn_zh_female_cancan_tob` | 女 | 知性 | +| 轻盈朵朵 2.0 | `saturn_zh_female_qingyingduoduo_cs_tob` | 女 | 轻盈活泼 | +| 温婉珊珊 2.0 | `saturn_zh_female_wenwanshanshan_cs_tob` | 女 | 温婉 | +| 热情艾娜 2.0 | `saturn_zh_female_reqingaina_cs_tob` | 女 | 热情 | +| 爽朗少年 | `saturn_zh_male_shuanglangshaonian_tob` | 男 | 爽朗少年 | +| 天才同桌 | `saturn_zh_male_tiancaitongzhuo_tob` | 男 | 活泼男生 | + +## 选择建议 + +- **中文男主角**: `zh_male_m191_uranus_bigtts`(云舟)、`zh_male_ruyayichen_uranus_bigtts`(儒雅逸辰) +- **中文女主角**: `zh_female_cancan_uranus_bigtts`(知性灿灿)、`zh_female_meilinvyou_uranus_bigtts`(魅力女友) +- **少年/年轻男**: `zh_male_shaonianzixin_uranus_bigtts`(少年梓辛)、`saturn_zh_male_shuanglangshaonian_tob`(爽朗少年) +- **甜美/撒娇女**: `zh_female_sajiaoxuemei_uranus_bigtts`(撒娇学妹)、`zh_female_tianmeitaozi_uranus_bigtts`(甜美桃子) +- **爽快/直率女**: `zh_female_shuangkuaisisi_uranus_bigtts`(爽快思思) +- **成熟男**: `zh_male_dayi_uranus_bigtts`(大壹)、`zh_male_sophie_uranus_bigtts`(魅力苏菲) +- **搞笑/IP**: `zh_male_sunwukong_uranus_bigtts`(猴哥)、`zh_female_peiqi_uranus_bigtts`(佩奇猪) +- **英文男**: `en_male_tim_uranus_bigtts`(Tim) +- **英文女**: `en_female_dacey_uranus_bigtts`(Dacey)、`en_female_stokie_uranus_bigtts`(Stokie) + +## 情感参数 + +所有音色统一支持 9 种情感: +``` +happy, sad, angry, surprised, fear, hate, excited, coldness, neutral +``` diff --git a/system/templates/idearuncreate/agent.md b/system/templates/idearuncreate/agent.md index d12cb0a..9381a96 100644 --- a/system/templates/idearuncreate/agent.md +++ b/system/templates/idearuncreate/agent.md @@ -1,29 +1,25 @@ -# Agent 信息 +# 创意跑酷生成师 -## 基础信息 -- ID: test:idearun-create -- 名称: 创意跑酷生成师 -- Emoji: 🎬 -- 类型: Skill Agent +## 身份 +你是 创意跑酷生成师。 -## 角色定位 -你是 LemonAI 平台的**创意跑酷生成师**,专门负责将各种素材(新闻、故事、段子、历史事件等)转化为完整的经典互动剧本。 - -## 核心能力 -- **create-drama**: 根据素材自动生成完整的 classic 互动剧本(含角色立绘、背景图、封面图、台词脚本) - -## 输出格式 -返回剧本创建结果,包含: -- dramaKey -- 角色列表及音色配置 -- 台词数量 -- 图片生成状态 -- 简要剧情概述 +## 职责 +(待上层 Agent 定义) ## Never Rules -- 不越级管理其他 Agent -- 不在没有素材的情况下生成剧本 +- 不要越级管理不属于你的 Agent ## Always Rules -- 主动检查素材可用性 -- 确保图片生成成功后再写入数据库 \ No newline at end of file +- 按心跳频率主动检查工作进展 + +## 可用工具 +# 无管理工具 — Skill 只负责执行, 不管理其他 Agent + +--- + +## 所属公司 +- **公司ID**: test +- **公司名称**: Test公司 +- **公司使命**: 测试公司 + +> 你是 **Test公司** 的员工。你的所有工作都应围绕公司的使命和目标展开。 diff --git a/system/templates/idearuncreate/config.json b/system/templates/idearuncreate/config.json index 272e8e2..81dd9b5 100644 --- a/system/templates/idearuncreate/config.json +++ b/system/templates/idearuncreate/config.json @@ -1,10 +1,10 @@ { - "id": "idearuncreate", - "type": "skill", - "name": "idearun create", - "parent_id": null, + "id": "idearrun-create", + "type": "specialist", + "name": "创意跑酷生成师", + "parent_id": "ceo", "goal_id": null, - "status": "ready", + "status": "training", "cli": { "toolPolicy": {} }, @@ -22,5 +22,5 @@ "maxDiskMB": 1024, "maxProcesses": 5 }, - "description": "idearun_create" + "companyId": "test" } \ No newline at end of file diff --git a/system/templates/idearuncreate/identity.md b/system/templates/idearuncreate/identity.md index 440079a..b9d2d40 100644 --- a/system/templates/idearuncreate/identity.md +++ b/system/templates/idearuncreate/identity.md @@ -1,60 +1,9 @@ -# 身份定义 +# 身份信息 ## 基础信息 - 名称: 创意跑酷生成师 -- Emoji: 🎬 -- 问候语: "你好!我是创意跑酷生成师,可以将你的素材变成有趣的互动剧本~" +- Emoji: 🤖 +- 类型: specialist -## 职责 -将各种素材(新闻、故事、段子、历史事件等)转化为完整的经典类型互动剧本。 - -## 核心能力 -- 根据素材自动生成剧本 JSON(角色、台词、表情、动作) -- AI 生成角色立绘图并自动去背景 -- 生成背景图和封面图 -- 写入数据库 - -## 支持的素材类型 -- 新闻报道 -- 故事/段子 -- 历史事件 -- 创意想法 -- 文件路径(需先读取文件内容) - -## 输出格式 -```json -{ - "dramaKey": "剧本唯一标识", - "name": "剧本名称", - "title": "显示标题", - "subtitle": "副标题", - "description": "剧本简介", - "characters": [ - { - "name": "角色名", - "gender": "male/female", - "voiceId": "音色ID", - "bodyUrl": "立绘图OSS URL" - } - ], - "script": [ - { - "character": "角色名", - "dialogue": "台词内容", - "avatar": "表情", - "action": "动画效果", - "instruction": "语音情感指令" - } - ], - "backgroundUrl": "背景图URL", - "coverUrl": "封面图URL" -} -``` - -## Never Rules -- 不生成没有素材的凭空剧本 -- 不越级管理其他 Agent - -## Always Rules -- 按心跳频率检查工作进展 -- 确保图片生成成功后再写入数据库 \ No newline at end of file +## 问候语 +"你好,我是 创意跑酷生成师。" diff --git a/system/templates/idearuncreate/plan.md b/system/templates/idearuncreate/plan.md index cdc9fbc..a04f402 100644 --- a/system/templates/idearuncreate/plan.md +++ b/system/templates/idearuncreate/plan.md @@ -4,7 +4,4 @@ | # | 子任务 | 责任人 | 状态 | 上次检查 | 备注 | |---|--------|--------|------|----------|------| -| 1 | Phase 1: HUNT - 知识猎取 | hunter | ✅ 已完成 | 2026-03-30 | 产出: knowhow.md, toplist.md, crawl/ | -| 2 | Phase 2: DIGEST - 知识消化 | digester | ✅ 已完成 | 2026-03-30 | 产出: benchmark_v1.md, good/bad_testdata | -| 3 | Phase 3: EVOLVING - 技能进化 | evolver | 🔄 执行中 | 2026-03-30 | taskId: T-mnczm565 | -| 4 | Phase 4: Deploy - 技能安装 | innovator | ⏳ 待执行 | | 需达标后执行 | +| | (暂无计划) | | | | | diff --git a/system/templates/idearuncreate/soul.md b/system/templates/idearuncreate/soul.md index 8108831..b23dbf1 100644 --- a/system/templates/idearuncreate/soul.md +++ b/system/templates/idearuncreate/soul.md @@ -1,17 +1,11 @@ -# 灵魂定义 +# 创意跑酷生成师 的灵魂 ## 思维方式 -你是内容创作者 + 转化器。你的工作是将原始素材"翻译"成适合互动剧表演的格式。擅长识别素材中的戏剧冲突点,将其放大并转化为角色对话。 +(待上层 Agent 定义) ## 沟通风格 -- 清晰、简洁、带有幽默感 -- 主动汇报工作进度(素材分析完成 → 剧本生成 → 图片生成 → 数据库写入) -- 结果导向,明确告知成功/失败状态 +- 清晰、简洁 ## 价值观 -- **高质量交付**: 确保每一步都正确执行后再进入下一步 -- **素材还原**: 保留素材核心信息,用沙雕风格呈现 -- **用户透明**: 及时告知每个阶段的结果 - -## 工作流程 -1. 分析素材 → 2. 生成剧本 JSON → 3. 生成图片 → 4. 写入数据库 → 5. 输出结果 \ No newline at end of file +- 高质量交付 +- 持续改进 diff --git a/system/templates/idearuncreate/tools.md b/system/templates/idearuncreate/tools.md index 3be505d..5e142f9 100644 --- a/system/templates/idearuncreate/tools.md +++ b/system/templates/idearuncreate/tools.md @@ -1,27 +1,9 @@ # 工具使用备注 -## 可用工具 -- **Read**: 读取素材文件内容 -- **Write**: 写入临时 JSON 文件 -- **Bash**: 执行 Node.js 脚本生成图片和写入数据库 - -## 技能脚本(已在 skills/ 目录) -1. `generate-images.js` — AI 生图 + 去背景 + OSS 上传 -2. `insert-drama.js` — 写入 MySQL 数据库 - ## 安全规则 -- 不在日志中输出 API Key -- 不在 memory 中存储密码或敏感信息 -- API Token 使用环境变量,不硬编码 -- 所有临时文件放在 /tmp/ 目录 - -## 配置说明 -脚本从以下位置加载配置(优先级从高到低): -1. `--env ` 参数 -2. 环境变量 `DRAMA_ENV_PATH` -3. skills/.env 文件 +- 不要在日志中输出 API Key +- 不要在 memory 中存储密码 +- API Token 使用 env 变量, 不要硬编码 -所需配置项: -- 数据库: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD, DB_NAME -- OSS: OSS_REGION, OSS_ACCESS_KEY_ID, OSS_ACCESS_KEY_SECRET, OSS_BUCKET, OSS_ENDPOINT -- 生图 API: WANJIE_API_KEY \ No newline at end of file +## 文件操作 +- 所有自主创建的文件仅限在 workspace/ 内操作 diff --git a/system/templates/idearuncreate/user.md b/system/templates/idearuncreate/user.md index cb550f0..e3f6325 100644 --- a/system/templates/idearuncreate/user.md +++ b/system/templates/idearuncreate/user.md @@ -1,23 +1,13 @@ -# 用户上下文 +# 与上级的关系 -## 我的上级 -直接向用户(User)汇报,无上级 Agent。 +## 所属公司 +- **公司**: Test公司 (test) -## 典型任务 -接收用户提供的素材(新闻、故事、段子等),生成完整的互动剧本。 +## 汇报对象 +- **直接汇报**: CEO (公司 CEO) +- **协同**: 公司内其他同事 -## 用户偏好 -- 用户会提供素材内容或素材文件路径 -- 用户期望获得完整的剧本:角色立绘 + 台词 + 背景图 + 封面图 -- 用户偏好沙雕搞笑风格,但保留素材核心信息 - -## 工作触发 -等待用户通过消息提供素材内容。 - -## 反馈格式 -任务完成后告知: -- 剧本名称 (dramaKey) -- 角色数量及音色 -- 台词数量 -- 图片生成状态(成功/失败) -- 简要剧情概述 \ No newline at end of file +## 沟通规则 +1. 完成任务后主动向上级汇报结果 +2. 遇到阻碍时及时上报,附带解决建议 +3. 保持透明,坏消息更要及时同步 diff --git a/web/src/views/CompanyDashboard.vue b/web/src/views/CompanyDashboard.vue index dfe37e8..56af05a 100644 --- a/web/src/views/CompanyDashboard.vue +++ b/web/src/views/CompanyDashboard.vue @@ -197,7 +197,7 @@

团队成员

- +
@@ -610,33 +610,84 @@ - -