@ninkch/dsh-infinite-canvas 0.19.2 → 0.19.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/README.zh-CN.md +50 -0
- package/assets/canvas-director/SKILL.md +235 -0
- package/assets/canvas-director/forms/cinematic.md +56 -0
- package/assets/canvas-director/forms/commercial.md +63 -0
- package/assets/canvas-director/forms/documentary.md +57 -0
- package/assets/canvas-director/forms/explainer.md +57 -0
- package/assets/canvas-director/forms/interview.md +56 -0
- package/assets/canvas-director/forms/music-video.md +62 -0
- package/assets/canvas-director/forms/short-drama.md +60 -0
- package/assets/canvas-director/forms/trailer.md +58 -0
- package/assets/canvas-director/genres/action-mission.md +58 -0
- package/assets/canvas-director/genres/adventure.md +78 -0
- package/assets/canvas-director/genres/broken-mirror-reunion.md +112 -0
- package/assets/canvas-director/genres/counterattack.md +58 -0
- package/assets/canvas-director/genres/crime-police.md +78 -0
- package/assets/canvas-director/genres/family-relation.md +58 -0
- package/assets/canvas-director/genres/fantasy.md +82 -0
- package/assets/canvas-director/genres/hk-retro.md +80 -0
- package/assets/canvas-director/genres/identity-swap.md +58 -0
- package/assets/canvas-director/genres/modern-urban.md +78 -0
- package/assets/canvas-director/genres/parent-child-secret.md +113 -0
- package/assets/canvas-director/genres/period-power-struggle.md +58 -0
- package/assets/canvas-director/genres/rebirth-transmigration.md +58 -0
- package/assets/canvas-director/genres/revenge-face-slap.md +58 -0
- package/assets/canvas-director/genres/slice-of-life.md +58 -0
- package/assets/canvas-director/genres/suspense-rule.md +58 -0
- package/assets/canvas-director/genres/system-flow.md +58 -0
- package/assets/canvas-director/genres/wealthy-romance.md +58 -0
- package/assets/canvas-director/genres/workplace-comedy.md +58 -0
- package/assets/canvas-director/genres/wuxia.md +78 -0
- package/assets/canvas-director/genres/xianxia-cultivation.md +58 -0
- package/assets/canvas-director/industries/auto.md +44 -0
- package/assets/canvas-director/industries/education.md +44 -0
- package/assets/canvas-director/industries/fashion-beauty.md +45 -0
- package/assets/canvas-director/industries/finance.md +44 -0
- package/assets/canvas-director/industries/fmcg.md +44 -0
- package/assets/canvas-director/industries/food.md +45 -0
- package/assets/canvas-director/industries/medical.md +45 -0
- package/assets/canvas-director/industries/realestate.md +44 -0
- package/assets/canvas-director/industries/tech.md +44 -0
- package/assets/canvas-director/industries/travel.md +45 -0
- package/assets/canvas-director/lenses/bergman.md +207 -0
- package/assets/canvas-director/lenses/bi_gan.md +198 -0
- package/assets/canvas-director/lenses/coen_brothers.md +204 -0
- package/assets/canvas-director/lenses/fellini.md +207 -0
- package/assets/canvas-director/lenses/fincher.md +195 -0
- package/assets/canvas-director/lenses/hitchcock.md +203 -0
- package/assets/canvas-director/lenses/hou_hsiao_hsien.md +202 -0
- package/assets/canvas-director/lenses/kubrick.md +206 -0
- package/assets/canvas-director/lenses/kurosawa.md +206 -0
- package/assets/canvas-director/lenses/lu_yang.md +200 -0
- package/assets/canvas-director/lenses/malick.md +201 -0
- package/assets/canvas-director/lenses/michael_mann.md +202 -0
- package/assets/canvas-director/lenses/nolan.md +195 -0
- package/assets/canvas-director/lenses/park_chan_wook.md +202 -0
- package/assets/canvas-director/lenses/refn.md +196 -0
- package/assets/canvas-director/lenses/scorsese.md +205 -0
- package/assets/canvas-director/lenses/spielberg.md +202 -0
- package/assets/canvas-director/lenses/tarkovsky.md +194 -0
- package/assets/canvas-director/lenses/tsui_hark.md +201 -0
- package/assets/canvas-director/lenses/villeneuve.md +198 -0
- package/assets/canvas-director/lenses/wong_kar_wai.md +199 -0
- package/assets/canvas-director/lenses/wu_ershan.md +206 -0
- package/assets/canvas-director/lenses/yuen_woo_ping.md +200 -0
- package/assets/canvas-director/lenses/zhang_yimou.md +202 -0
- package/assets/canvas-director/looks/anime-thick-paint.md +81 -0
- package/assets/canvas-director/looks/dark-myth-cg.md +83 -0
- package/assets/canvas-director/looks/east-epic-3d.md +83 -0
- package/assets/canvas-director/looks/ghibli-watercolor.md +82 -0
- package/assets/canvas-director/looks/guoman-3d-ink.md +82 -0
- package/assets/canvas-director/looks/guoman-3d-xianxia.md +82 -0
- package/assets/canvas-director/looks/hk-retro-film.md +81 -0
- package/assets/canvas-director/looks/realistic-cinema.md +83 -0
- package/assets/canvas-director/looks/shinkai-light.md +82 -0
- package/assets/canvas-director/manifest.json +603 -0
- package/assets/canvas-director/moves/camera-moves.md +79 -0
- package/assets/canvas-director/moves/fight-rhythm.md +77 -0
- package/assets/canvas-director/pack.js +342 -0
- package/assets/canvas-studio/SKILL.md +34 -3
- package/assets/director/engine.js +17 -4
- package/client.js +19577 -18938
- package/docs/guide.en.md +66 -1
- package/docs/guide.zh-CN.md +64 -1
- package/index-core-skill-profile.js +39 -0
- package/index.js +127 -3
- package/package.json +65 -60
- package/skill-pack-loader.js +147 -0
- package/skill-pack.js +179 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* canvas-director 技能包的 Host 侧加载器。
|
|
3
|
+
*
|
|
4
|
+
* 职责:
|
|
5
|
+
* - 读 manifest.json 并缓存(文件未变不重复解析);
|
|
6
|
+
* - 按层读卡文件(带缓存);
|
|
7
|
+
* - 用 pack.js 的纯函数按生成能力切片并拼装;
|
|
8
|
+
* - 提供层清单供 canvas_list_skills 展示。
|
|
9
|
+
*
|
|
10
|
+
* 一切路径限制在 assets/canvas-director/ 之内;缺文件不抛错(该层跳过,由调用方决定是否提示)。
|
|
11
|
+
*/
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
|
|
16
|
+
export const PACK_DIR_REL = 'assets/canvas-director';
|
|
17
|
+
export const LAYER_DIRS = {
|
|
18
|
+
lenses: 'lenses',
|
|
19
|
+
genres: 'genres',
|
|
20
|
+
industries: 'industries',
|
|
21
|
+
forms: 'forms',
|
|
22
|
+
looks: 'looks',
|
|
23
|
+
};
|
|
24
|
+
/** 单数层名 → manifest 里的复数数组名(cards 用 lens/genre/... ,索引用 lenses/genres/...)。 */
|
|
25
|
+
export const LAYER_OF = {
|
|
26
|
+
lens: 'lenses',
|
|
27
|
+
genre: 'genres',
|
|
28
|
+
industry: 'industries',
|
|
29
|
+
form: 'forms',
|
|
30
|
+
look: 'looks',
|
|
31
|
+
};
|
|
32
|
+
/** 复数数组名 → 读文件用的子目录名。 */
|
|
33
|
+
export const DIR_OF = LAYER_DIRS;
|
|
34
|
+
|
|
35
|
+
/** manifest 与各卡文件的缓存:key = 绝对路径,value = { mtimeMs, size, data }。 */
|
|
36
|
+
const fileCache = new Map();
|
|
37
|
+
/** pack.js 模块缓存。 */
|
|
38
|
+
let PACK_MOD = null;
|
|
39
|
+
|
|
40
|
+
/** 读文件并缓存(按 mtime+size 失效)。文件不存在返回 null。 */
|
|
41
|
+
function readCached(abs) {
|
|
42
|
+
let st;
|
|
43
|
+
try { st = statSync(abs); } catch { return null; }
|
|
44
|
+
const hit = fileCache.get(abs);
|
|
45
|
+
if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size) return hit.data;
|
|
46
|
+
const data = readFileSync(abs, 'utf8');
|
|
47
|
+
fileCache.set(abs, { mtimeMs: st.mtimeMs, size: st.size, data });
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 清空缓存(测试用)。 */
|
|
52
|
+
export function clearPackCache() { fileCache.clear(); PACK_MOD = null; }
|
|
53
|
+
|
|
54
|
+
/** 懒加载 pack.js(纯函数模块,与 Client 同源)。 */
|
|
55
|
+
export async function getPackModule() {
|
|
56
|
+
if (PACK_MOD) return PACK_MOD;
|
|
57
|
+
const abs = path.join(HERE_DIR, PACK_DIR_REL, 'pack.js');
|
|
58
|
+
if (!existsSync(abs)) throw new Error('缺少 pack.js:' + abs);
|
|
59
|
+
PACK_MOD = await import(pathToFileURL(abs).href);
|
|
60
|
+
return PACK_MOD;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 由 index.js 在模块初始化时注入(避免本文件反向依赖 index)。 */
|
|
64
|
+
let HERE_DIR = '';
|
|
65
|
+
export function initPackBase(hereDir) { HERE_DIR = String(hereDir || ''); }
|
|
66
|
+
|
|
67
|
+
/** 读 manifest(未解析的原始对象)。缺失返回 null。 */
|
|
68
|
+
export function readManifestRaw(hereDir) {
|
|
69
|
+
const base = hereDir || HERE_DIR;
|
|
70
|
+
const abs = path.join(base, PACK_DIR_REL, 'manifest.json');
|
|
71
|
+
const raw = readCached(abs);
|
|
72
|
+
if (!raw) return null;
|
|
73
|
+
try { return JSON.parse(raw); } catch { return null; }
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 读一层里的一张卡(按 manifest 的 file 字段)。layers 用 manifest 的复数名(lenses/genres/...)。缺失返回 null。 */
|
|
77
|
+
export function readCard(hereDir, layer, file) {
|
|
78
|
+
const base = hereDir || HERE_DIR;
|
|
79
|
+
const plural = LAYER_OF[layer] || layer; // 允许传单数
|
|
80
|
+
const dir = DIR_OF[plural];
|
|
81
|
+
if (!dir) return null;
|
|
82
|
+
const rel = String(file || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
83
|
+
if (!rel || rel.includes('..')) return null;
|
|
84
|
+
const abs = path.join(base, PACK_DIR_REL, dir, rel);
|
|
85
|
+
const baseDir = path.join(base, PACK_DIR_REL);
|
|
86
|
+
const resolved = path.resolve(abs);
|
|
87
|
+
if (!resolved.startsWith(path.resolve(baseDir) + path.sep)) return null;
|
|
88
|
+
return readCached(resolved);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 读 moves 下的文件(不属五层索引,按文件名直取)。 */
|
|
92
|
+
export function readMove(hereDir, file) {
|
|
93
|
+
const base = hereDir || HERE_DIR;
|
|
94
|
+
const rel = String(file || '').replace(/\\/g, '/').replace(/^\/+/, '');
|
|
95
|
+
if (!rel || rel.includes('..')) return null;
|
|
96
|
+
const abs = path.resolve(path.join(base, PACK_DIR_REL, 'moves', rel));
|
|
97
|
+
const baseDir = path.resolve(path.join(base, PACK_DIR_REL, 'moves'));
|
|
98
|
+
if (!abs.startsWith(baseDir + path.sep)) return null;
|
|
99
|
+
return readCached(abs);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 层清单(给 canvas_list_skills 用):每层数组,元素含 id/name/group/oneliner/tier/available。 */
|
|
103
|
+
export function packInventory(hereDir) {
|
|
104
|
+
const base = hereDir || HERE_DIR;
|
|
105
|
+
const raw = readManifestRaw(base);
|
|
106
|
+
const out = { lenses: [], genres: [], industries: [], forms: [], looks: [], moves: [] };
|
|
107
|
+
if (!raw) return out;
|
|
108
|
+
for (const layer of Object.keys(LAYER_DIRS)) {
|
|
109
|
+
const list = Array.isArray(raw[layer]) ? raw[layer] : [];
|
|
110
|
+
out[layer] = list.map((it) => ({
|
|
111
|
+
id: String(it.id || ''),
|
|
112
|
+
name: String(it.name || ''),
|
|
113
|
+
group: String(it.group || ''),
|
|
114
|
+
oneliner: String(it.oneliner || ''),
|
|
115
|
+
tier: String(it.tier || ''),
|
|
116
|
+
glyph: String(it.glyph || it.id || ''),
|
|
117
|
+
file: String(it.file || ''),
|
|
118
|
+
available: !!readCard(base, layer, it.file),
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
const moves = Array.isArray(raw.moves) ? raw.moves : [];
|
|
122
|
+
out.moves = moves.map((it) => ({
|
|
123
|
+
id: String(it.id || ''),
|
|
124
|
+
name: String(it.name || ''),
|
|
125
|
+
file: String(it.file || ''),
|
|
126
|
+
oneliner: String(it.oneliner || ''),
|
|
127
|
+
available: !!readMove(base, it.file),
|
|
128
|
+
}));
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 从 inventory 取某层某 id 的条目(layer 可传单数或复数)。 */
|
|
133
|
+
export function findEntry(inv, layer, id) {
|
|
134
|
+
const plural = LAYER_OF[layer] || layer;
|
|
135
|
+
const list = (inv && inv[plural]) || [];
|
|
136
|
+
return list.find((x) => x.id === id) || null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** 全部合法 id(给 normalizeSkillProfile 的枚举用)。 */
|
|
140
|
+
export function allowedIds(inv) {
|
|
141
|
+
return {
|
|
142
|
+
lenses: ((inv && inv.lenses) || []).filter((x) => x.available).map((x) => x.id),
|
|
143
|
+
genres: [...((inv && inv.genres) || []), ...((inv && inv.industries) || [])].filter((x) => x.available).map((x) => x.id),
|
|
144
|
+
forms: ((inv && inv.forms) || []).filter((x) => x.available).map((x) => x.id),
|
|
145
|
+
looks: ((inv && inv.looks) || []).filter((x) => x.available).map((x) => x.id),
|
|
146
|
+
};
|
|
147
|
+
}
|
package/skill-pack.js
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host 侧「技能包切片」构建:把四层配置 + manifest 变成可注入的文本。
|
|
3
|
+
*
|
|
4
|
+
* 与 Client 侧同源(都用 assets/canvas-director/pack.js 的纯函数),保证
|
|
5
|
+
* 画布自生成与对话侧写出的提示词口径一致。这是「技能要对所有提示词生成起作用」的落点。
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
getPackModule, packInventory, readCard, readMove, findEntry, allowedIds, readManifestRaw,
|
|
9
|
+
} from './skill-pack-loader.js';
|
|
10
|
+
import { normalizeSkillProfile } from './index-core-skill-profile.js';
|
|
11
|
+
|
|
12
|
+
/** moves 永远随 storyboard/prompt 走(运镜是这两类生成的必需品)。 */
|
|
13
|
+
const MOVES_FOR_KIND = {
|
|
14
|
+
script: [],
|
|
15
|
+
storyboard: ['camera-moves'],
|
|
16
|
+
prompt: ['camera-moves'],
|
|
17
|
+
assets: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 构建某生成能力下的注入文本。
|
|
22
|
+
* @param {object} opts
|
|
23
|
+
* @param {string} opts.kind script | storyboard | prompt | assets
|
|
24
|
+
* @param {object} opts.profile 技能配置(已归一或原始,函数内部会再归一)
|
|
25
|
+
* @param {string} opts.hereDir 插件根目录
|
|
26
|
+
* @param {boolean} opts.zh 中文
|
|
27
|
+
* @returns {Promise<{text:string,bytes:number,used:Array,profile:object,inventory:object}>}
|
|
28
|
+
*/
|
|
29
|
+
export async function buildSkillPack(opts) {
|
|
30
|
+
const o = opts || {};
|
|
31
|
+
const kind = String(o.kind || 'script');
|
|
32
|
+
const zh = o.zh !== false;
|
|
33
|
+
const hereDir = String(o.hereDir || '');
|
|
34
|
+
|
|
35
|
+
const inv = packInventory(hereDir);
|
|
36
|
+
const allow = allowedIds(inv);
|
|
37
|
+
const profile = normalizeSkillProfile(o.profile, allow);
|
|
38
|
+
|
|
39
|
+
// 内层:genres 里既可能是叙事题材 id,也可能是行业题材 id —— 两者都从各自层读
|
|
40
|
+
const items = [];
|
|
41
|
+
const lensEntry = profile.lens ? findEntry(inv, 'lenses', profile.lens) : null;
|
|
42
|
+
if (lensEntry) {
|
|
43
|
+
const md = readCard(hereDir, 'lenses', lensEntry.file);
|
|
44
|
+
if (md) items.push({ layer: 'lens', md, meta: lensEntry });
|
|
45
|
+
}
|
|
46
|
+
for (const gid of profile.genres) {
|
|
47
|
+
const ge = findEntry(inv, 'genres', gid);
|
|
48
|
+
if (ge) {
|
|
49
|
+
const md = readCard(hereDir, 'genres', ge.file);
|
|
50
|
+
if (md) { items.push({ layer: 'genre', md, meta: ge }); continue; }
|
|
51
|
+
}
|
|
52
|
+
const ie = findEntry(inv, 'industries', gid);
|
|
53
|
+
if (ie) {
|
|
54
|
+
const md = readCard(hereDir, 'industries', ie.file);
|
|
55
|
+
if (md) items.push({ layer: 'industry', md, meta: ie });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const formEntry = profile.form ? findEntry(inv, 'forms', profile.form) : null;
|
|
59
|
+
if (formEntry) {
|
|
60
|
+
const md = readCard(hereDir, 'forms', formEntry.file);
|
|
61
|
+
if (md) items.push({ layer: 'form', md, meta: formEntry });
|
|
62
|
+
}
|
|
63
|
+
const lookEntry = profile.look ? findEntry(inv, 'looks', profile.look) : null;
|
|
64
|
+
if (lookEntry) {
|
|
65
|
+
const md = readCard(hereDir, 'looks', lookEntry.file);
|
|
66
|
+
if (md) items.push({ layer: 'look', md, meta: lookEntry });
|
|
67
|
+
}
|
|
68
|
+
for (const mid of (MOVES_FOR_KIND[kind] || [])) {
|
|
69
|
+
const mv = (inv.moves || []).find((x) => x.id === mid);
|
|
70
|
+
if (!mv) continue;
|
|
71
|
+
const md = readMove(hereDir, mv.file);
|
|
72
|
+
if (md) items.push({ layer: 'moves', md, meta: mv });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const P = await getPackModule();
|
|
76
|
+
// 一条卡都没取到 → 不注入(连 head 都不给:否则「未配置」也会往 system 里塞一段空壳)
|
|
77
|
+
if (!items.length) {
|
|
78
|
+
return { text: '', bytes: 0, used: [], profile, inventory: inv };
|
|
79
|
+
}
|
|
80
|
+
const head = headLine(kind, profile, inv, zh);
|
|
81
|
+
const built = P.buildPackText(kind, items, { zh, head });
|
|
82
|
+
return { text: built.text, bytes: built.bytes, used: built.used, profile, inventory: inv };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 注入块的头部一行:把生效配置写清,并声明优先级(模型据此裁决冲突)。 */
|
|
86
|
+
function headLine(kind, profile, inv, zh) {
|
|
87
|
+
const nameOf = (layer, id) => {
|
|
88
|
+
if (!id) return '—';
|
|
89
|
+
const e = findEntry(inv, layer, id);
|
|
90
|
+
return e ? e.name : id;
|
|
91
|
+
};
|
|
92
|
+
const genreNames = (profile.genres || []).map((g) => {
|
|
93
|
+
const e = findEntry(inv, 'genres', g) || findEntry(inv, 'industries', g);
|
|
94
|
+
return e ? e.name : g;
|
|
95
|
+
});
|
|
96
|
+
if (zh) {
|
|
97
|
+
return [
|
|
98
|
+
'【画布技能包 · ' + kindLabel(kind) + '】',
|
|
99
|
+
'导演透镜:' + nameOf('lenses', profile.lens) + ' 题材:' + (genreNames.join('、') || '—')
|
|
100
|
+
+ ' 类型规格:' + nameOf('forms', profile.form) + ' 影调:' + (profile.look ? nameOf('looks', profile.look) : '跟随导演'),
|
|
101
|
+
'优先级:用户显式指令 > 导演透镜 > 类型规格(仅画幅与单镜时长上压过透镜)> 题材 > 影调 > 技能默认。以下为对应切片,写提示词时必须遵守;与本块冲突的通则让位于本块。',
|
|
102
|
+
].join('\n');
|
|
103
|
+
}
|
|
104
|
+
return [
|
|
105
|
+
'[Canvas skill pack · ' + kindLabel(kind, false) + ']',
|
|
106
|
+
'Director lens: ' + nameOf('lenses', profile.lens) + ' Genre: ' + (genreNames.join(', ') || '-')
|
|
107
|
+
+ ' Delivery form: ' + nameOf('forms', profile.form) + ' Look: ' + (profile.look ? nameOf('looks', profile.look) : 'follow the lens'),
|
|
108
|
+
'Precedence: explicit user instruction > director lens > delivery form (only aspect and clip length) > genre > look > skill defaults. The slices below are binding; where a generic rule disagrees, this block wins.',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function kindLabel(kind, zh) {
|
|
113
|
+
const ZH = { script: '剧本', storyboard: '分镜', prompt: '提示词', assets: '资产设定图' };
|
|
114
|
+
const EN = { script: 'script', storyboard: 'storyboard', prompt: 'prompt', assets: 'asset sheet' };
|
|
115
|
+
const m = zh === false ? EN : ZH;
|
|
116
|
+
return m[kind] || kind;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 给 canvas_list_skills 用的技能清单(4 份内置技能 + 何时用)。 */
|
|
120
|
+
export const SKILL_CATALOG = [
|
|
121
|
+
{ name: 'canvas-studio', when: '任何时候先读:建项目 / 写节点 / 触发生成 / 查状态' },
|
|
122
|
+
{ name: 'canvas-craft', when: '写剧本或拆分镜前:剧本四要素、分镜六要素、提示词规范' },
|
|
123
|
+
{ name: 'canvas-layout', when: '批量建节点前:一类型一列、绝不重叠' },
|
|
124
|
+
{ name: 'canvas-director', when: '写任何提示词前:导演透镜 / 题材 / 类型规格 / 影调 / 运镜,先用 canvas_skill_pack 取切片' },
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
/** 层清单的可读渲染(canvas_list_skills 的正文)。 */
|
|
128
|
+
export function renderInventory(inv, profile, zh) {
|
|
129
|
+
const lines = [];
|
|
130
|
+
const ZH_LABEL = { lenses: '导演透镜', genres: '叙事题材', industries: '行业题材', forms: '类型规格', looks: '影调质感' };
|
|
131
|
+
const EN_LABEL = { lenses: 'Director lenses', genres: 'Genres', industries: 'Industries', forms: 'Delivery forms', looks: 'Looks' };
|
|
132
|
+
const L = zh === false ? EN_LABEL : ZH_LABEL;
|
|
133
|
+
for (const layer of ['lenses', 'genres', 'industries', 'forms', 'looks']) {
|
|
134
|
+
const list = (inv && inv[layer]) || [];
|
|
135
|
+
if (!list.length) continue;
|
|
136
|
+
lines.push('');
|
|
137
|
+
lines.push('### ' + L[layer] + '(' + list.length + ')');
|
|
138
|
+
// 按 group 分组展示
|
|
139
|
+
const byGroup = new Map();
|
|
140
|
+
for (const it of list) {
|
|
141
|
+
const g = it.group || '';
|
|
142
|
+
if (!byGroup.has(g)) byGroup.set(g, []);
|
|
143
|
+
byGroup.get(g).push(it);
|
|
144
|
+
}
|
|
145
|
+
for (const [g, arr] of byGroup) {
|
|
146
|
+
lines.push((g ? g + ':' : '') + arr.map((x) => x.id + '=' + x.name + (x.available ? '' : '(缺文件)')).join(' '));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
const mv = (inv && inv.moves) || [];
|
|
150
|
+
if (mv.length) {
|
|
151
|
+
lines.push('');
|
|
152
|
+
lines.push('### ' + (zh === false ? 'Camera & rhythm' : '运镜与节奏') + '(' + mv.length + ')');
|
|
153
|
+
lines.push(mv.map((x) => x.id + '=' + x.name).join(' '));
|
|
154
|
+
}
|
|
155
|
+
return lines.join('\n');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** 当前项目配置的可读渲染。 */
|
|
159
|
+
export function renderProfile(profile, inv, zh) {
|
|
160
|
+
const nameOf = (layer, id) => {
|
|
161
|
+
if (!id) return '';
|
|
162
|
+
const e = findEntry(inv, layer, id);
|
|
163
|
+
return e ? e.name : id;
|
|
164
|
+
};
|
|
165
|
+
const g = (profile.genres || []).map((x) => {
|
|
166
|
+
const e = findEntry(inv, 'genres', x) || findEntry(inv, 'industries', x);
|
|
167
|
+
return e ? e.name : x;
|
|
168
|
+
});
|
|
169
|
+
if (zh === false) {
|
|
170
|
+
return 'lens=' + (profile.lens || '(none)') + ' genres=' + (g.join(',') || '(none)')
|
|
171
|
+
+ ' form=' + (profile.form || '(none)') + ' look=' + (profile.look || '(follow the lens)');
|
|
172
|
+
}
|
|
173
|
+
return '导演=' + (nameOf('lenses', profile.lens) || '(未选)')
|
|
174
|
+
+ ' 题材=' + (g.join('、') || '(未选)')
|
|
175
|
+
+ ' 类型规格=' + (nameOf('forms', profile.form) || '(未选)')
|
|
176
|
+
+ ' 影调=' + (profile.look ? nameOf('looks', profile.look) : '跟随导演');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export { readManifestRaw, normalizeSkillProfile };
|