D181: 角色一致性修复 + pack引擎v2升级
- 修复: 林昊正面/侧面不一致 → seedream_i2i 图生图重绘 - 新建: engines/char-hero-design-packer/char-hero-design-packer.js - 主视角T2I → 记录seed → 其余视角I2I参考同一张脸 + 锁定seed - 版本化输出 + manifest.json + --dry-run 预览 - 更新: cli.py cmd_pack 支持 .js 引擎 + --project --views --dry-run - 新增: EP01-BREAKDOWN-V3.hdlp, EP01-PARSED.hdlp
This commit is contained in:
parent
1a6066db76
commit
3eceff49c2
@ -0,0 +1,387 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* char-hero-design-packer.js — 角色资产打包引擎 v2.0
|
||||
*
|
||||
* 核心改进:
|
||||
* 1. 主视角先生成 → 记录 seed
|
||||
* 2. 其余视角用主视角作为图生图参考 → 锁定同一 seed
|
||||
* 3. 版本化输出目录
|
||||
* 4. manifest.json 记录所有种子和参考链
|
||||
*
|
||||
* 用法:
|
||||
* node char-hero-design-packer.js --character CHAR-001 --project deep-sea-voyage
|
||||
* node char-hero-design-packer.js --character CHAR-001 --project deep-sea-voyage --dry-run
|
||||
*/
|
||||
|
||||
const https = require('https');
|
||||
const http = require('http');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
// ===== 配置 =====
|
||||
|
||||
const MODEL = 'doubao-seedream-4-0-250828';
|
||||
const SIZE = '1440x2560';
|
||||
const COST_PER_IMAGE = 0.21; // ¥
|
||||
|
||||
// 视角定义
|
||||
const VIEW_CONFIGS = {
|
||||
front: {
|
||||
label: '正面',
|
||||
suffix: '_正面',
|
||||
promptAddon: 'full body shot, facing camera directly, looking at viewer',
|
||||
mode: 't2i' // 第一张用文生图
|
||||
},
|
||||
side: {
|
||||
label: '侧面',
|
||||
suffix: '_侧面',
|
||||
promptAddon: 'side profile view, turned 90 degrees to the right, profile silhouette visible',
|
||||
mode: 'i2i' // 用于主视角参考
|
||||
},
|
||||
hand: {
|
||||
label: '手部',
|
||||
suffix: '_手部',
|
||||
promptAddon: 'close-up shot focusing on hands, callused worker hands with engine oil stains, hands in foreground',
|
||||
mode: 'i2i'
|
||||
},
|
||||
threeQuarter: {
|
||||
label: '¾面',
|
||||
suffix: '¾面',
|
||||
promptAddon: 'three-quarter view, angled slightly to the right, facing partly away from camera, cinematic portrait',
|
||||
mode: 'i2i'
|
||||
}
|
||||
};
|
||||
|
||||
// ===== API 调用 =====
|
||||
|
||||
function getArkKey() {
|
||||
const envPaths = [
|
||||
path.join(__dirname, '..', '..', '.env'),
|
||||
path.join(__dirname, '..', '..', '..', '.env'),
|
||||
'D:/WorkBuddy/cang-ying/.env'
|
||||
];
|
||||
for (const p of envPaths) {
|
||||
if (fs.existsSync(p)) {
|
||||
const content = fs.readFileSync(p, 'utf8');
|
||||
const m = content.match(/ARK_API_KEY=(.+)/);
|
||||
if (m) return m[1].trim();
|
||||
}
|
||||
}
|
||||
throw new Error('ARK_API_KEY 未找到,请检查 .env 配置');
|
||||
}
|
||||
|
||||
function apiCall(payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const body = JSON.stringify(payload);
|
||||
const req = https.request({
|
||||
hostname: 'ark.cn-beijing.volces.com',
|
||||
path: '/api/v3/images/generations',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${getArkKey()}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Content-Length': Buffer.byteLength(body)
|
||||
}
|
||||
}, (res) => {
|
||||
let data = '';
|
||||
res.on('data', c => data += c);
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const result = JSON.parse(data);
|
||||
if (result.error) reject(new Error(JSON.stringify(result.error)));
|
||||
else resolve(result);
|
||||
} catch (e) { reject(new Error(data)); }
|
||||
});
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.write(body);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
function t2i(prompt, seed) {
|
||||
const payload = { model: MODEL, prompt, size: SIZE, n: 1 };
|
||||
if (seed) payload.seed = seed;
|
||||
return apiCall(payload);
|
||||
}
|
||||
|
||||
function i2i(prompt, imagePath, seed) {
|
||||
const imgBase64 = fs.readFileSync(imagePath).toString('base64');
|
||||
const ext = imagePath.endsWith('.png') ? 'png' : 'jpeg';
|
||||
const payload = {
|
||||
model: MODEL,
|
||||
prompt,
|
||||
image: `data:image/${ext};base64,${imgBase64}`,
|
||||
size: SIZE,
|
||||
n: 1
|
||||
};
|
||||
if (seed) payload.seed = seed;
|
||||
return apiCall(payload);
|
||||
}
|
||||
|
||||
function downloadImage(url, outputPath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const proto = url.startsWith('https') ? https : http;
|
||||
const file = fs.createWriteStream(outputPath);
|
||||
proto.get(url, (res) => {
|
||||
res.pipe(file);
|
||||
file.on('finish', () => { file.close(); resolve(outputPath); });
|
||||
}).on('error', (e) => { fs.unlink(outputPath, () => {}); reject(e); });
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 剧本解析 =====
|
||||
|
||||
function findCharacterInBreakdown(charId, project, breakdownPath) {
|
||||
// 优先用指定路径
|
||||
if (breakdownPath && fs.existsSync(breakdownPath)) {
|
||||
const content = fs.readFileSync(breakdownPath, 'utf8');
|
||||
return parseCharFromHdlp(content, charId);
|
||||
}
|
||||
|
||||
// 自动搜索项目目录
|
||||
const base = path.join(__dirname, '..', '..', 'projects', project);
|
||||
if (!fs.existsSync(base)) throw new Error(`项目目录不存在: ${base}`);
|
||||
|
||||
const files = fs.readdirSync(base).filter(f => f.includes('BREAKDOWN') && f.endsWith('.hdlp'));
|
||||
// 优先 V3 > V2 > V1
|
||||
files.sort((a, b) => {
|
||||
const getV = f => { const m = f.match(/V(\d+)/); return m ? parseInt(m[1]) : 0; };
|
||||
return getV(b) - getV(a);
|
||||
});
|
||||
|
||||
for (const f of files) {
|
||||
const content = fs.readFileSync(path.join(base, f), 'utf8');
|
||||
const result = parseCharFromHdlp(content, charId);
|
||||
if (result) return result;
|
||||
}
|
||||
|
||||
throw new Error(`角色 ${charId} 未在项目的 BREAKDOWN 中找到`);
|
||||
}
|
||||
|
||||
function parseCharFromHdlp(content, charId) {
|
||||
// 解析 CHAR 表格
|
||||
const lines = content.split('\n');
|
||||
let inCharTable = false;
|
||||
let headers = [];
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.includes('CHAR - 角色资产表') || line.includes('CHAR -')) {
|
||||
inCharTable = true;
|
||||
continue;
|
||||
}
|
||||
if (inCharTable && (line.startsWith('### ') || line.startsWith('## '))) {
|
||||
inCharTable = false;
|
||||
continue;
|
||||
}
|
||||
if (!inCharTable) continue;
|
||||
|
||||
// 表头行
|
||||
if (line.includes('| ID |') || line.includes('| ID | 名称 |')) {
|
||||
headers = line.split('|').map(h => h.trim()).filter(Boolean);
|
||||
continue;
|
||||
}
|
||||
if (line.includes('| :---')) continue; // 分隔行
|
||||
|
||||
// 数据行
|
||||
const cells = line.split('|').map(c => c.trim()).filter(Boolean);
|
||||
if (cells.length >= 4 && cells[0] === charId) {
|
||||
return {
|
||||
id: cells[0],
|
||||
name: cells[1] || '',
|
||||
description: cells[3] || cells[2] || '',
|
||||
englishPrompt: cells[4] || cells[3] || '',
|
||||
rawLine: line
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ===== 主流程 =====
|
||||
|
||||
async function packCharacter(charId, project, options = {}) {
|
||||
const {
|
||||
breakdownPath = null,
|
||||
views = ['front', 'side', 'hand'],
|
||||
dryRun = false
|
||||
} = options;
|
||||
|
||||
// 1. 查找角色描述
|
||||
console.error(`📖 查找角色 ${charId}...`);
|
||||
const charInfo = findCharacterInBreakdown(charId, project, breakdownPath);
|
||||
console.error(` 名称: ${charInfo.name}`);
|
||||
console.error(` 描述: ${charInfo.description.substring(0, 80)}...`);
|
||||
|
||||
// 2. 准备输出目录
|
||||
const outputDir = path.join(
|
||||
__dirname, '..', '..', 'outputs', 'pack', project, charId,
|
||||
`v${new Date().toISOString().slice(0,10).replace(/-/g,'')}`
|
||||
);
|
||||
if (!dryRun && !fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
console.error(`📁 输出目录: ${outputDir}`);
|
||||
console.error();
|
||||
|
||||
const basePrompt = charInfo.englishPrompt;
|
||||
|
||||
// 3. 干运行模式
|
||||
if (dryRun) {
|
||||
console.error('🔍 === 干运行预览 ===');
|
||||
const viewList = views.map(v => {
|
||||
const cfg = VIEW_CONFIGS[v];
|
||||
return {
|
||||
view: v,
|
||||
label: cfg.label,
|
||||
mode: cfg.mode,
|
||||
prompt: `${basePrompt} ${cfg.promptAddon}`,
|
||||
ref: cfg.mode === 'i2i' ? '<FRONT_MASTER>' : 'none',
|
||||
cost: COST_PER_IMAGE
|
||||
};
|
||||
});
|
||||
console.error(` 角色: ${charId} ${charInfo.name}`);
|
||||
console.error(` 视角度数: ${views.length} (${views.map(v => VIEW_CONFIGS[v].label).join(', ')})`);
|
||||
console.error(` 总成本: ¥${(views.length * COST_PER_IMAGE).toFixed(2)}`);
|
||||
console.error(` 成本明细:`);
|
||||
viewList.forEach(v => console.error(` ${v.label} (${v.mode}): ¥${COST_PER_IMAGE.toFixed(2)}`));
|
||||
console.error();
|
||||
return { dryRun: true, charInfo, views: viewList, cost: views.length * COST_PER_IMAGE };
|
||||
}
|
||||
|
||||
// 4. 首次视角用 T2I(主视角)
|
||||
let masterImage = null;
|
||||
let masterSeed = null;
|
||||
let masterViewConfig = null;
|
||||
const results = {};
|
||||
|
||||
// 找第一个 t2i 视角
|
||||
for (const v of views) {
|
||||
const cfg = VIEW_CONFIGS[v];
|
||||
if (cfg && cfg.mode === 't2i') {
|
||||
masterViewConfig = { view: v, cfg };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!masterViewConfig) {
|
||||
// 所有视角都是 i2i,先做一个 T2I 主视角
|
||||
masterViewConfig = { view: 'front', cfg: VIEW_CONFIGS.front };
|
||||
}
|
||||
|
||||
console.error(`🎨 步骤1: 生成主视角 (${masterViewConfig.cfg.label})...`);
|
||||
const masterPrompt = `${basePrompt} ${masterViewConfig.cfg.promptAddon}`;
|
||||
console.error(` 提示词: ${masterPrompt.substring(0, 100)}...`);
|
||||
|
||||
const masterResult = await t2i(masterPrompt);
|
||||
if (masterResult.error) throw new Error(JSON.stringify(masterResult.error));
|
||||
|
||||
masterSeed = masterResult.data?.[0]?.seed;
|
||||
const masterUrl = masterResult.data?.[0]?.url;
|
||||
console.error(` Seed: ${masterSeed || 'N/A'}`);
|
||||
|
||||
// 下载主视角
|
||||
const masterFile = path.join(outputDir, `${charId}${VIEW_CONFIGS.front.suffix}.png`);
|
||||
await downloadImage(masterUrl, masterFile);
|
||||
masterImage = masterFile;
|
||||
console.error(` ✅ ${masterFile}`);
|
||||
|
||||
results.front = { seed: masterSeed, path: masterFile, mode: 't2i', cost: COST_PER_IMAGE };
|
||||
|
||||
// 5. 其他视角用 I2I + 锁定 seed
|
||||
const remainingViews = views.filter(v => {
|
||||
const cfg = VIEW_CONFIGS[v];
|
||||
return cfg && cfg.mode !== 't2i';
|
||||
});
|
||||
|
||||
for (const view of remainingViews) {
|
||||
const cfg = VIEW_CONFIGS[view];
|
||||
console.error(`🎨 步骤: 生成${cfg.label} (I2I,参考=正面)...`);
|
||||
|
||||
const viewPrompt = `${basePrompt} ${cfg.promptAddon}`;
|
||||
console.error(` 提示词: ${viewPrompt.substring(0, 100)}...`);
|
||||
console.error(` 参考图: ${masterImage}`);
|
||||
if (masterSeed) console.error(` 锁定Seed: ${masterSeed}`);
|
||||
|
||||
const result = await i2i(viewPrompt, masterImage, masterSeed);
|
||||
if (result.error) throw new Error(JSON.stringify(result.error));
|
||||
|
||||
const viewSeed = result.data?.[0]?.seed;
|
||||
const viewUrl = result.data?.[0]?.url;
|
||||
console.error(` Seed: ${viewSeed || 'N/A'}`);
|
||||
|
||||
const viewFile = path.join(outputDir, `${charId}${cfg.suffix}.png`);
|
||||
await downloadImage(viewUrl, viewFile);
|
||||
console.error(` ✅ ${viewFile}`);
|
||||
|
||||
results[view] = { seed: viewSeed, path: viewFile, mode: 'i2i', refFrom: 'front', cost: COST_PER_IMAGE };
|
||||
}
|
||||
|
||||
// 6. 写入 manifest
|
||||
const totalCost = Object.values(results).reduce((s, r) => s + r.cost, 0);
|
||||
const manifest = {
|
||||
characterId: charId,
|
||||
characterName: charInfo.name,
|
||||
project,
|
||||
model: MODEL,
|
||||
generatedAt: new Date().toISOString(),
|
||||
masterSeed,
|
||||
totalCost,
|
||||
views: results
|
||||
};
|
||||
|
||||
const manifestPath = path.join(outputDir, 'manifest.json');
|
||||
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
||||
console.error(`📋 Manifest: ${manifestPath}`);
|
||||
console.error(`💰 总成本: ¥${totalCost.toFixed(2)}`);
|
||||
|
||||
return { dryRun: false, manifest, outputDir };
|
||||
}
|
||||
|
||||
// ===== CLI =====
|
||||
|
||||
async function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const getArg = (name) => {
|
||||
const idx = args.indexOf(name);
|
||||
return idx !== -1 ? args[idx + 1] : null;
|
||||
};
|
||||
|
||||
const character = getArg('--character');
|
||||
const project = getArg('--project');
|
||||
const breakdown = getArg('--breakdown');
|
||||
const viewsStr = getArg('--views');
|
||||
const dryRun = args.includes('--dry-run');
|
||||
const generateAll = args.includes('--generate-all');
|
||||
|
||||
if (!character) {
|
||||
console.log('用法: node char-hero-design-packer.js --character CHAR-001 --project <项目名> [--views front,side,hand] [--dry-run]');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 默认视图
|
||||
const views = viewsStr ? viewsStr.split(',').map(v => v.trim()) : ['front', 'side', 'hand'];
|
||||
const proj = project || 'default';
|
||||
|
||||
try {
|
||||
const result = await packCharacter(character, proj, {
|
||||
breakdownPath: breakdown,
|
||||
views,
|
||||
dryRun
|
||||
});
|
||||
if (!dryRun) {
|
||||
console.log(JSON.stringify(result.manifest, null, 2));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`❌ 错误: ${e.message}`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
}
|
||||
|
||||
module.exports = { packCharacter, findCharacterInBreakdown };
|
||||
@ -584,28 +584,100 @@ def cmd_director(script_path="", shots_dir="", output=""):
|
||||
print(f"❌ 导演失败: {r.stderr[:300]}")
|
||||
print(f"💡 请改用: python -m lib.cli render")
|
||||
|
||||
def cmd_pack(character="CHAR-003-SuBai"):
|
||||
"""角色资产生成: 多视角角色定妆照 → 自动批准入库"""
|
||||
def cmd_pack(character="CHAR-003-SuBai", *extra_args):
|
||||
"""角色资产生成: 多视角角色定妆照 → 自动批准入库
|
||||
args: <角色ID> [--project <项目>] [--views front,side,hand] [--dry-run]
|
||||
v2.0: 支持主视角→参考图链路 + seed锁定"""
|
||||
import subprocess as _sp
|
||||
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
engine = os.path.join(project_root, "engines", "char-hero-design-packer", "char-hero-design-packer.py")
|
||||
|
||||
if not os.path.exists(engine):
|
||||
print(f"⚠️ 资产打包引擎未找到")
|
||||
# 检测引擎类型 (.js vs .py)
|
||||
engine_js = os.path.join(project_root, "engines", "char-hero-design-packer", "char-hero-design-packer.js")
|
||||
engine_py = os.path.join(project_root, "engines", "char-hero-design-packer", "char-hero-design-packer.py")
|
||||
|
||||
engine = None
|
||||
runtime = None
|
||||
if os.path.exists(engine_js):
|
||||
engine = engine_js
|
||||
runtime = "node"
|
||||
elif os.path.exists(engine_py):
|
||||
engine = engine_py
|
||||
runtime = "python"
|
||||
|
||||
if not engine:
|
||||
print(f"⚠️ 资产打包引擎未找到 (char-hero-design-packer.js/.py)")
|
||||
print(f"💡 请用: python -m lib.cli image t2i \"角色定妆照描述\"")
|
||||
return
|
||||
|
||||
print(f"🎒 生成角色资产: {character}...")
|
||||
r = _sp.run([sys.executable, engine, "--character", character, "--generate-all"],
|
||||
capture_output=True, text=True, timeout=600)
|
||||
if r.returncode == 0:
|
||||
print(f"✅ {character} 资产已生成")
|
||||
db = get_db()
|
||||
db.execute("UPDATE assets SET status = 'approved', approved_at = datetime('now') WHERE name LIKE ?",
|
||||
(f"%{character}%",))
|
||||
db.commit()
|
||||
# 解析参数
|
||||
args = list(extra_args)
|
||||
project = None
|
||||
views = None
|
||||
breakdown = None
|
||||
dry_run = False
|
||||
|
||||
i = 0
|
||||
while i < len(args):
|
||||
if args[i] == "--project" and i+1 < len(args):
|
||||
project = args[i+1]; i += 2
|
||||
elif args[i] == "--views" and i+1 < len(args):
|
||||
views = args[i+1]; i += 2
|
||||
elif args[i] == "--breakdown" and i+1 < len(args):
|
||||
breakdown = args[i+1]; i += 2
|
||||
elif args[i] in ("--dry-run", "--dry_run", "true"):
|
||||
dry_run = True; i += 1
|
||||
else:
|
||||
i += 1
|
||||
|
||||
if not project:
|
||||
# 尝试从角色ID推断项目
|
||||
import glob
|
||||
proj_dirs = glob.glob(os.path.join(project_root, "projects", "*"))
|
||||
for pd in proj_dirs:
|
||||
breakdowns = glob.glob(os.path.join(pd, "*BREAKDOWN*V*.hdlp"))
|
||||
for bd in breakdowns:
|
||||
with open(bd, "r", encoding="utf-8") as f:
|
||||
if character in f.read():
|
||||
project = os.path.basename(pd)
|
||||
print(f"🔍 自动检测项目: {project}")
|
||||
break
|
||||
if project: break
|
||||
|
||||
engine_args = ["--character", character, "--generate-all"]
|
||||
engine_args.append("--project"); engine_args.append(project or "default")
|
||||
if views: engine_args.append("--views"); engine_args.append(views)
|
||||
if breakdown: engine_args.append("--breakdown"); engine_args.append(breakdown)
|
||||
if dry_run: engine_args.append("--dry-run")
|
||||
|
||||
print(f"🎒 打包角色资产: {character}")
|
||||
print(f" 项目: {project or '(未指定)'}")
|
||||
print(f" 引擎: {os.path.basename(engine)} ({runtime})")
|
||||
if views: print(f" 视角: {views}")
|
||||
if dry_run: print(f" 🔍 干运行模式")
|
||||
print()
|
||||
|
||||
if runtime == "node":
|
||||
r = _sp.run(["C:/Users/rtyyr/.workbuddy/binaries/node/versions/22.22.2/node.exe", engine] + engine_args,
|
||||
capture_output=True, text=True, timeout=600)
|
||||
else:
|
||||
print(f"❌ 失败: {r.stderr[:300] or r.stdout[:300]}")
|
||||
r = _sp.run([sys.executable, engine] + engine_args,
|
||||
capture_output=True, text=True, timeout=600)
|
||||
|
||||
if r.stderr:
|
||||
for line in r.stderr.strip().split("\n"):
|
||||
print(f" {line}")
|
||||
|
||||
if r.returncode == 0:
|
||||
if not dry_run:
|
||||
print(f"\n✅ {character} 角色资产打包完成")
|
||||
try:
|
||||
db = get_db()
|
||||
db.execute("UPDATE assets SET status = 'approved', approved_at = datetime('now') WHERE name LIKE ?",
|
||||
(f"%{character}%",))
|
||||
db.commit()
|
||||
except: pass
|
||||
else:
|
||||
print(f"\n❌ 打包失败: {r.stderr[:300] or r.stdout[:300]}")
|
||||
|
||||
def cmd_prompt(scene_text="", output=""):
|
||||
"""HLDP提示词: 场景描述 → Seedance提示词(HLDP引擎/AI兜底)"""
|
||||
@ -1853,7 +1925,7 @@ HELP_TEXT = {
|
||||
"multi-ref": "multi-ref check|gen <提示词> <ref1,ref2> — 多参考适配",
|
||||
"track": "track track|stabilize|pin|motion <视频> — 平面追踪",
|
||||
"director": "director <分镜.json> <镜头目录> — 自动导演编排",
|
||||
"pack": "pack [角色ID] — 角色资产生成(多视角定妆)",
|
||||
"pack": "pack <角色ID> [--project <项目>] [--views front,side,hand] [--dry-run] — 角色资产打包(主视角→参考+seed锁定)",
|
||||
"prompt": "prompt <场景描述> — HLDP提示词引擎",
|
||||
"parse": "parse <剧本> [输出] — 确定性剧本解析+AI兜底",
|
||||
"adapt": "adapt <导演编码.json> — 导演编码→编辑参数",
|
||||
|
||||
@ -0,0 +1,33 @@
|
||||
# EP01 BREAKDOWN V3 · D181 · 步骤③
|
||||
# tokens: 3019
|
||||
|
||||
好的,船长。剧本已锁定,资产拆解完毕,交付如下。
|
||||
|
||||
---
|
||||
|
||||
### CHAR - 角色资产表
|
||||
|
||||
| ID | 名称 | 描述 | Seedance 英文提示词 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| CHAR-001 | 林昊 | 33岁男性,秦山号深海矿工。面容沧桑但难掩英俊轮廓。身着破旧深蓝色工装,内搭深色T恤。双手布满老茧与机油污渍。眼神疲惫,充满警惕与迷茫。发型凌乱,胡子拉碴。 | A 33-year-old man, handsome but weathered and haggard face, unshaven. Wearing a worn dark blue mechanic jumpsuit. Hands covered in calluses and engine oil stains. Eyes filled with exhaustion and vigilance. Messy short black hair. Standing in a dim, rusty industrial corridor, cold blue industrial lighting, deep-sea horror atmosphere, 3D, photorealistic, cinematic. |
|
||||
| CHAR-002 | 半孢感染者 | 半人半孢子的恐怖造物。右半边是苍白流泪的中年男性面孔,嘴唇翕动无声求救。左半边皮肤透明隆起,下方有颗粒蠕动,左眼被黑色菌膜覆盖,嘴角撕裂至耳根,裂口呈蜂窝状,每个孔洞中都有黑色孢子脉动,呼吸时喷出孢子雾。右手正常,左手呈现白骨化。 | A terrifying half-human, half-fungus creature. Right side of face is a pale, weeping middle-aged man. Left side of face is grotesque, translucent skin with pulsating granules beneath, black fungal membrane covering the eye, torn mouth corner with honeycomb-like pores pulsing with black spores, exhaling a faint spore mist. One hand has fingernails torn off, revealing bone. Dim cold blue light, deep-sea sci-fi horror, 3D, hyper-realistic, cinematic. |
|
||||
|
||||
---
|
||||
|
||||
### ENV - 场景资产表
|
||||
|
||||
| ID | 名称 | 描述 | Seedance 英文提示词 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| ENV-001 | 秦山号船员宿舍 | 深海采矿船“秦山号”最底层的船员宿舍。空间狭小、破败。两侧是三层钢架床铺,被褥凌乱。金属舱壁布满铁锈和油污,地面有积水和油渍。唯一的舱门锈迹斑斑,上方有一个布满灰尘的四层钢化玻璃观察窗。天花板上的灯泡接触不良,忽明忽暗,发出惨淡的冷光。整个船体处于倾斜状态。 | Interior of a cramped, decaying crew quarters in a deep-sea mining vessel. Two sets of messy three-tier bunk beds. Rusty metal walls, oily puddles on the floor. A single heavy hatch door with a dust-covered observation window. A malfunctioning lightbulb flickers with dim, cold blue light, creating sharp, swaying shadows. The entire room is visibly tilted. Deep-sea industrial horror, 3D, photorealistic, cinematic lighting. |
|
||||
| ENV-002 | 舱门观察窗特写 | 从船员宿舍内侧看向舱门的视角。焦点是方形观察窗。窗内玻璃被林昊擦干净一部分,窗外则糊满厚重的灰尘和油脂。当感染者贴上来时,窗外是令人窒息的近距离恐怖。玻璃上溅有暗红色的血痕和黑色的孢子雾残留。 | Extreme close-up on a square, quadruple-layered steel glass observation window set in a rusty hatch. The inside glass is smeared clean, while the outside is caked with thick grime and hydraulic oil. Dim, flickering cold blue light from inside illuminates the dust. A sense of dread and claustrophobia, deep-sea horror atmosphere, 3D, photorealistic, shallow depth of field. |
|
||||
|
||||
---
|
||||
|
||||
### PROP - 道具资产表
|
||||
|
||||
| ID | 名称 | 描述 | Seedance 英文提示词 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| PROP-001 | 破旧工具包 | 一个硕大的黑色帆布工具包,表面磨损严重,有油污和划痕。拉链为金属材质。 | A large, worn-out black canvas tool bag. The surface is heavily stained with oil and grime, full of scratches. A sturdy metal zipper. Placed on a metal table in a dimly lit rusty ship cabin, cold blue industrial light, 3D, photorealistic, deep-sea horror prop. |
|
||||
| PROP-002 | 工牌 | 秦山号船员工牌。塑料材质,破旧。照片上的人脸被利器故意划烂,无法辨认。隐约可见印刷文字:编号 000768,姓名 林昊,岗位模糊不清。 | An old, scratched plastic ID badge for a deep-sea vessel named "Qin Shan". The photo ID area is violently scratched out. Text is barely legible, showing "No. 000768", "Name: Lin Hao". Lying in the open tool bag, under dim, flickering cold light, 3D, photorealistic, close-up, cinematic depth of field. |
|
||||
| PROP-003 | 重型管钳 | 一把半米长、沾满油污的金属管钳。手柄处缠着防滑的黑色胶带,整体沉重,带有铁锈和使用痕迹。 | A half-meter long, heavy-duty metal pipe wrench. Covered in grease and rust, with black anti-slip tape wrapped around the handle. Leaning against a rusty hatch, dim cold blue industrial lighting, deep-sea horror setting, 3D, photorealistic, aggressive texture. |
|
||||
| PROP-004 | 便携式手电筒 | 一个黄色的工业防爆手电筒,外壳坚固,有橡胶包裹。有使用划痕。 | A yellow, heavy-duty industrial explosion-proof flashlight with a rubberized grip. Scratched and worn from use. Lying inside a dark tool bag, under harsh cold blue light, 3D, photorealistic, deep-sea horror props. |
|
||||
181
video-ai-system/projects/deep-sea-voyage/EP01-PARSED.hdlp
Normal file
181
video-ai-system/projects/deep-sea-voyage/EP01-PARSED.hdlp
Normal file
@ -0,0 +1,181 @@
|
||||
{
|
||||
"episodes": [
|
||||
{
|
||||
"episode": 1,
|
||||
"scenes": [
|
||||
{
|
||||
"id": "1-1",
|
||||
"time": "日",
|
||||
"loc": "内",
|
||||
"name": "秦山号最底层船员宿舍",
|
||||
"characters": [
|
||||
{
|
||||
"name": "林昊",
|
||||
"desc": "33岁,身着工装,面容沧桑帅气"
|
||||
}
|
||||
],
|
||||
"dialogues": [
|
||||
{
|
||||
"character": "警报声",
|
||||
"emotion": "VO",
|
||||
"line": "滴滴滴滴……"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "虚弱,迷茫",
|
||||
"line": "这……这里是什么地方?"
|
||||
},
|
||||
{
|
||||
"character": "警报声",
|
||||
"emotion": "VO",
|
||||
"line": "滴滴……滴滴……"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "",
|
||||
"line": "秦山号……这……这是船舱?"
|
||||
},
|
||||
{
|
||||
"character": "剧烈脚步声",
|
||||
"emotion": "VO",
|
||||
"line": "咚咚咚咚(极快)……"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "警觉",
|
||||
"line": "谁!?"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "",
|
||||
"line": "整个船体都倾斜了?怎么回事?"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "",
|
||||
"line": "刚才跑过去的是谁?我……我又是谁?(甩甩脑袋)我怎么一点儿都记不起来了。"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "大喊",
|
||||
"line": "有人吗?!"
|
||||
},
|
||||
{
|
||||
"character": "警报声",
|
||||
"emotion": "VO",
|
||||
"line": "滴滴……滴滴……"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "",
|
||||
"line": "这是什么破地方。"
|
||||
},
|
||||
{
|
||||
"character": "警报声",
|
||||
"emotion": "VO",
|
||||
"line": "滴滴……滴滴……"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "",
|
||||
"line": "林昊?这是我吗?"
|
||||
},
|
||||
{
|
||||
"character": "剧烈脚步声",
|
||||
"emotion": "VO",
|
||||
"line": "咚咚咚咚(极快)……"
|
||||
},
|
||||
{
|
||||
"character": "警报声",
|
||||
"emotion": "VO",
|
||||
"line": "滴滴滴滴……"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "低声自语,恐惧",
|
||||
"line": "这到底是什么东西?"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "",
|
||||
"line": "不能坐以待毙!"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "皱眉",
|
||||
"line": "还是不行。"
|
||||
},
|
||||
{
|
||||
"character": "音效",
|
||||
"emotion": "",
|
||||
"line": "呲——!"
|
||||
},
|
||||
{
|
||||
"character": "林昊",
|
||||
"emotion": "惊喜",
|
||||
"line": "果然……"
|
||||
},
|
||||
{
|
||||
"character": "音效",
|
||||
"emotion": "",
|
||||
"line": "砰!!"
|
||||
},
|
||||
{
|
||||
"character": "警报声",
|
||||
"emotion": "VO",
|
||||
"line": "滴滴滴滴滴——"
|
||||
}
|
||||
],
|
||||
"descriptions": [
|
||||
"画面漆黑。",
|
||||
"切林昊主观视角:双眼缓缓睁开,天花板上的灯泡忽明忽暗。昏暗的灯光下,映入眼帘的是一间破旧的船员宿舍。两张三层船员床铺分别固定在两侧,床铺上十分凌乱,空无一人。宿舍舱门布满铁锈,满眼破败。",
|
||||
"林昊看向自己的双手,布满老茧,指甲缝里全是机油污渍。",
|
||||
"轰隆一声巨响,整个船舱猛烈震颤!林昊猛地从最下层床铺坐起,死死抓住床头铁杆固定身体。【特写:铁杆上的标记——秦山号。",
|
||||
"林昊猛转头看向宿舍舱门。透过布满灰尘的观察窗,一道黑影飞速闪过,速度不似人类。",
|
||||
"船舱震动逐渐停止,窗户上的灰尘被震落一部分。林昊从床铺跳下,刚站直身体便猛地前倾,险些摔倒。他赶忙扶住床沿,深吸一口气。",
|
||||
"林昊皱眉,稳住身形,一步步走向舱门。",
|
||||
"林昊走到舱门前,伸手擦拭玻璃上的灰尘。这是一块四层钢化玻璃,虽然内侧擦净,但外侧仍被厚重灰尘糊住,一片模糊。",
|
||||
"林昊试图扭动舱门把手,伴随一阵刺耳的吱呀声,把手纹丝未动。他皱眉看着手掌上的铁锈,以及舱门缝隙下渗出的液压油。",
|
||||
"林昊转身看向刚才躺过的下铺,发现旁边的悬桌上放着一个硕大的黑色破旧工具包。他快步走过去,一把拉开拉链。",
|
||||
"工具包打开。里面装着一个便携式手电筒、一块备用电源、两卷急救绷带、一把半米长的管钳、一个军绿色水壶,以及一个破旧的工牌。【特写:工牌上隐约写着——编号:000768,姓名:林昊,岗位:模糊不清。还有一张照片,照片上的人脸部位被人用利器故意划烂,只隐约可见年轻帅气的轮廓。",
|
||||
"林昊猛转头看向舱门观察窗,又是一道黑影一闪而逝。",
|
||||
"林昊迅速收起工具包背在身上,抄起管钳握在手中,来到舱门前。",
|
||||
"林昊用管钳卡住舱门把手,咬紧牙关猛地用力,门把手依然纹丝不动。",
|
||||
"林昊盯着舱门下的液压油渍,目光一凛。他举起管钳,冲着门把手下方的一个液压凸起狠狠砸了下去!",
|
||||
"伴随着泄压声,林昊嘴角扬起。他再次用管钳撬动门把手,伴随吱呀声,把手终于缓缓转动。",
|
||||
"一声巨响传来!一道不似人形的黑影重重地砸在舱门外的观察窗上。林昊透过玻璃,首先看到的是一只手——一只指甲全部脱落、指尖露出白骨的人类的手。那只手顺着玻璃缓缓滑下,留下五道暗红色的血痕。",
|
||||
"然后,一张脸贴上了玻璃。【特写:半人半孢子。右半边:四十岁男人的脸,皮肤苍白,眼泪滑落,嘴唇无声翕动——救我。杀了我。左半边:皮肤透明隆起,下方颗粒蠕动。左眼被黑色菌膜覆盖,菌丝扎入眼眶。嘴角撕裂至耳根,裂口呈蜂窝状,每个孔洞里孢子在脉动。每呼吸一次,黑色孢子雾从孔洞喷出,蒙上玻璃。",
|
||||
"林昊僵在原地,瞳孔剧烈收缩,手中的管钳在发抖。",
|
||||
"画面骤黑。"
|
||||
],
|
||||
"effects": [
|
||||
{
|
||||
"type": "closeup",
|
||||
"description": "轰隆一声巨响,整个船舱猛烈震颤!林昊猛地从最下层床铺坐起,死死抓住床头铁杆固定身体。【特写:铁杆上的标记——秦山号。"
|
||||
},
|
||||
{
|
||||
"type": "closeup",
|
||||
"description": "工具包打开。里面装着一个便携式手电筒、一块备用电源、两卷急救绷带、一把半米长的管钳、一个军绿色水壶,以及一个破旧的工牌。【特写:工牌上隐约写着——编号:000768,姓名:林昊,岗位:模糊不清。还有一张照片,照片上的人脸部位被人用利器故意划烂,只隐约可见年轻帅气的轮廓。"
|
||||
},
|
||||
{
|
||||
"type": "closeup",
|
||||
"description": "然后,一张脸贴上了玻璃。【特写:半人半孢子。右半边:四十岁男人的脸,皮肤苍白,眼泪滑落,嘴唇无声翕动——救我。杀了我。左半边:皮肤透明隆起,下方颗粒蠕动。左眼被黑色菌膜覆盖,菌丝扎入眼眶。嘴角撕裂至耳根,裂口呈蜂窝状,每个孔洞里孢子在脉动。每呼吸一次,黑色孢子雾从孔洞喷出,蒙上玻璃。"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"characters": [
|
||||
"剧烈脚步声",
|
||||
"林昊",
|
||||
"警报声",
|
||||
"音效"
|
||||
],
|
||||
"stats": {
|
||||
"episodes": 1,
|
||||
"scenes": 1,
|
||||
"dialogues": 22,
|
||||
"characters": 4
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user