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:
鉴影 ICE-GL-CA001 2026-07-10 16:33:56 +08:00
parent 1a6066db76
commit 3eceff49c2
4 changed files with 689 additions and 16 deletions

View File

@ -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 };

View File

@ -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"],
# 解析参数
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:
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:
print(f"{character} 资产已生成")
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"❌ 失败: {r.stderr[:300] or r.stdout[:300]}")
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> — 导演编码→编辑参数",

View File

@ -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. |

View 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
}
}