diff --git a/video-ai-system/engines/char-hero-design-packer/char-hero-design-packer.js b/video-ai-system/engines/char-hero-design-packer/char-hero-design-packer.js new file mode 100644 index 0000000..7b446f6 --- /dev/null +++ b/video-ai-system/engines/char-hero-design-packer/char-hero-design-packer.js @@ -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' ? '' : '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 }; diff --git a/video-ai-system/lib/cli.py b/video-ai-system/lib/cli.py index 0d92fe9..ea759d6 100644 --- a/video-ai-system/lib/cli.py +++ b/video-ai-system/lib/cli.py @@ -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 <提示词> — 多参考适配", "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> — 导演编码→编辑参数", diff --git a/video-ai-system/projects/deep-sea-voyage/EP01-BREAKDOWN-V3.hdlp b/video-ai-system/projects/deep-sea-voyage/EP01-BREAKDOWN-V3.hdlp new file mode 100644 index 0000000..3bdb52d --- /dev/null +++ b/video-ai-system/projects/deep-sea-voyage/EP01-BREAKDOWN-V3.hdlp @@ -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. | \ No newline at end of file diff --git a/video-ai-system/projects/deep-sea-voyage/EP01-PARSED.hdlp b/video-ai-system/projects/deep-sea-voyage/EP01-PARSED.hdlp new file mode 100644 index 0000000..ad52fb8 --- /dev/null +++ b/video-ai-system/projects/deep-sea-voyage/EP01-PARSED.hdlp @@ -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 + } +} \ No newline at end of file