dsh-cosplay-studio 2.0.4 → 2.1.0
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 +33 -16
- package/lib/card.js +156 -52
- package/lib/client.js +376 -24
- package/lib/comfy.js +10 -2
- package/lib/index.js +314 -27
- package/lib/project.js +147 -32
- package/lib/research.js +719 -0
- package/lib/tools/finalize.py +100 -17
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -13,6 +13,17 @@
|
|
|
13
13
|
|  |  |  |
|
|
14
14
|
|  |  |  |
|
|
15
15
|
|
|
16
|
+
## 工作流程
|
|
17
|
+
|
|
18
|
+
四步都在同一个工作台里完成,每一步都能先看、再改,确认后才往下走:
|
|
19
|
+
|
|
20
|
+
| 步骤 | 界面 |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| ① 角色研究:联网检索 + 图片分类 + 视觉模型提取特征 + 粗略草图确认 |  |
|
|
23
|
+
| ② 基准图微调:输入要求手动改,或让视觉模型比对参考图后智能改 |  |
|
|
24
|
+
| ③ 镜头脚本:按角色特征生成 6 镜,可逐镜改、智能补镜、手动加镜 |  |
|
|
25
|
+
| ④ 成片与导出:先出 1440×2560 原图 PNG,再按需导出 JPEG / 预览 / 文案 |  |
|
|
26
|
+
|
|
16
27
|
---
|
|
17
28
|
|
|
18
29
|
## 一、安装
|
|
@@ -30,7 +41,7 @@ D:\DSH\profiles\desktop\node_modules\dsh-cosplay-studio
|
|
|
30
41
|
```json
|
|
31
42
|
{
|
|
32
43
|
"dependencies": {
|
|
33
|
-
"dsh-cosplay-studio": "2.0
|
|
44
|
+
"dsh-cosplay-studio": "2.1.0"
|
|
34
45
|
},
|
|
35
46
|
"dsh": {
|
|
36
47
|
"profile": {
|
|
@@ -45,7 +56,7 @@ D:\DSH\profiles\desktop\node_modules\dsh-cosplay-studio
|
|
|
45
56
|
### 方式 B:打包分发
|
|
46
57
|
|
|
47
58
|
```bash
|
|
48
|
-
pnpm pack # 产出 dsh-cosplay-studio-2.0.
|
|
59
|
+
pnpm pack # 产出 dsh-cosplay-studio-2.1.0.tgz
|
|
49
60
|
```
|
|
50
61
|
|
|
51
62
|
把 `.tgz` 或整个目录发给别人;对方按方式 A 解压/放入 `node_modules` 并在 `package.json` 登记即可。发布到 npm 后可走插件市场安装。
|
|
@@ -90,25 +101,30 @@ curl -L -o dsh-cosplay-studio.tgz \
|
|
|
90
101
|
|
|
91
102
|
| 入口 | 做法 | 说明 |
|
|
92
103
|
| --- | --- | --- |
|
|
93
|
-
| A 角色名 | 输入角色名(可加作品名) |
|
|
94
|
-
| B 参考图 | 上传一张二次元角色图 |
|
|
95
|
-
| C 随机推荐 | 点「随机推荐 3 个热门角色」/「换一批」 | 内置 28
|
|
104
|
+
| A 角色名 | 输入角色名(可加作品名) | 自动联网检索角色资料与立绘/cosplay 图,视觉模型提取发型、服装、道具、性格等特征 |
|
|
105
|
+
| B 参考图 | 上传一张二次元角色图 | 图是唯一设计依据;视觉模型读图提取特征,必要时可「补充网络参考图」 |
|
|
106
|
+
| C 随机推荐 | 点「随机推荐 3 个热门角色」/「换一批」 | 内置 28 个近年高热角色池,选中后自动走入口 A 的研究流程 |
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
选定角色后点「创建项目」,插件会用 DSH 宿主的检索 / 视觉能力做一轮**角色形象研究**:检索资料 → 下载并分类角色参考图(自动跳过风景、汉字、截图)→ 提取外观特征 → 生成 640×1024 粗略草图供确认。整个过程有分阶段进度(正在检索信息 / 正在根据参考图分析特征 / 正在生成粗略草图)。研究结果不满意可以「重新研究」,参考图项目可以「补充网络参考图」。
|
|
98
109
|
|
|
99
|
-
###
|
|
110
|
+
### 三道闸门
|
|
100
111
|
|
|
101
|
-
1.
|
|
102
|
-
2.
|
|
112
|
+
1. **角色形象**:看研究出的特征与粗略草图,必要时直接改特征字段后保存。
|
|
113
|
+
2. **基准图**:生成 1152×2048 基准图。确认人物、服装、背景都对了再往下;不满意可以「微调基准图」——输入要求手动改,或让视觉模型比对参考图/角色卡后智能改,结果作为新版本追加,旧图保留。
|
|
114
|
+
3. **镜头脚本**:按角色特征生成 6 镜(每镜含情境/表情/动作/手/机位/道具,且带角色专属细节),可逐镜改,也可「智能补 2 镜(不重复)」或手动加一镜。确认后才跑 1440×2560 成片。
|
|
103
115
|
|
|
104
116
|
### 出片与导出
|
|
105
117
|
|
|
106
|
-
|
|
118
|
+
成片直接按 1440×2560 出原图(`raw/*.png`,不额外压缩),先看原图:
|
|
119
|
+
|
|
120
|
+
- 每张成片可以单独「微调」:输入要求手动改,或「智能修改」让视觉模型比对参考图/已确认基准图后只修最需要改的瑕疵(手、脚、五官、衣边);改动幅度可选,改坏了可以「撤销上次微调」。
|
|
121
|
+
- 满意后再「导出成片」:可选 **去油光** / **身体化妆**,导出 JPEG(默认 q95、上限 6 MB)并生成:
|
|
122
|
+
- `00_基准角色_<角色>.jpg`
|
|
123
|
+
- `00_全套预览.jpg`(2×2 拼图)
|
|
124
|
+
- `01_动作名.jpg`、`02_动作名.jpg` …
|
|
125
|
+
- `文案.txt`(标题 3 条 / 正文 / 标签)
|
|
107
126
|
|
|
108
|
-
|
|
109
|
-
- `00_全套预览.jpg`(2×2 拼图)
|
|
110
|
-
- `01_动作名.jpg`、`02_动作名.jpg` …
|
|
111
|
-
- `文案.txt`(标题 3 条 / 正文 / 标签)
|
|
127
|
+
导出时会把参考图带来的白色海报留白自动补成角色配色的满幅渐变(不需要可以关闭)。去油光 / 身体化妆 / 预览 / 文案都改为可选的后续步骤,不再强制和出片绑在一起。
|
|
112
128
|
|
|
113
129
|
---
|
|
114
130
|
|
|
@@ -141,6 +157,7 @@ curl -L -o dsh-cosplay-studio.tgz \
|
|
|
141
157
|
## 六、已知限制
|
|
142
158
|
|
|
143
159
|
- 只支持 NVIDIA GPU(8GB 显存起步);4K 会 OOM。
|
|
144
|
-
-
|
|
145
|
-
-
|
|
160
|
+
- 本地 Z-Image-Turbo 的 img2img 结构惯性较强:基准图/成片微调能可靠地改道具、服饰细节、手部细节,但抬手臂、换姿势这类大幅动作会被原图压住;姿势请交给镜头脚本,或直接用更大改动幅度重绘(可能改变人物与构图)。
|
|
161
|
+
- 智能修改会先给出问题清单再重绘,对已有成片重绘仍可能把鞋子/边缘改坏;每次覆盖前都备份在 `raw/.history/`,可随时「撤销上次微调」。
|
|
162
|
+
- 联网检索使用 Bing 网页兜底(DSH 账号的 web.search 在插件 HTTP 请求里拿不到发起会话),个别角色可能仍混入不相关图片;插件会用视觉模型过滤风景、汉字、截图,并在研究日志里写明跳过原因。
|
|
146
163
|
- 背景渐变颜色刻意避开米/沙/驼等接近肤色的色系,否则 P7b 的肤色遮罩会把背景一起提亮。
|
package/lib/card.js
CHANGED
|
@@ -17,10 +17,32 @@ export const DEFAULT_POSE = 'standing in a relaxed three-quarter turn toward the
|
|
|
17
17
|
// HANDS is a fixed paragraph in v2.1 — it never changes across shots.
|
|
18
18
|
export const DEFAULT_HANDS = 'exactly five fingers on each hand, fingers clearly separated, natural knuckles, no extra or fused fingers'
|
|
19
19
|
|
|
20
|
+
// Hands and feet are the weakest area of the local model. This paragraph is
|
|
21
|
+
// appended to every base/shot prompt; the matching terms also go into the
|
|
22
|
+
// negative prompt (see comfy.js EXTRA_NEGATIVE).
|
|
23
|
+
export const LIMB_DETAIL = 'Extremities are rendered carefully: each hand has exactly five fingers with natural knuckles, clean nails and correct proportion for the pose; each foot has exactly five toes where the shoes expose them, and the shoes, straps and laces sit correctly on the feet with believable weight; no melted, duplicated or missing parts.'
|
|
24
|
+
|
|
20
25
|
// Poses with a turned body tend to crop the legs. This sentence rides along in
|
|
21
26
|
// the POSE line so every shot still frames the whole figure head to shoes.
|
|
22
27
|
const POSE_FRAMING = 'The entire figure stays inside the frame from the top of the head down to the shoes, with empty backdrop above the head and empty floor around the feet.'
|
|
23
28
|
|
|
29
|
+
// Reference projects start from an uploaded picture. The text encoder does not
|
|
30
|
+
// know that character, so every design detail must be delegated to the picture
|
|
31
|
+
// itself and img2img (denoise ~0.75) carries it into the photo.
|
|
32
|
+
const REFERENCE_IDENTITY = 'Photorealistic live-action cosplay photograph of a real 18-year-old East Asian woman cosplaying the exact character shown in the reference image. Keep the reference character\'s identity exactly: the same hairstyle and hair colour, eye colour, face shape, skin tone, costume pieces with their exact colours and patterns, accessories, held prop and overall silhouette, translating the illustration into real fabric, leather, metal, lace and feathers with believable material weight and clean seams.'
|
|
33
|
+
|
|
34
|
+
// Full-bleed gradient: the user's brief is "character-adapted pure colour
|
|
35
|
+
// backdrop, no blank or white areas". The colours come from the character card
|
|
36
|
+
// palette (top / bottom), so each project gets its own backdrop instead of a
|
|
37
|
+
// generic grey studio wall.
|
|
38
|
+
const referenceBackdrop = (palette) => {
|
|
39
|
+
const top = String(palette?.top ?? '').trim() || 'ivory white'
|
|
40
|
+
const bottom = String(palette?.bottom ?? '').trim() || 'pale grey'
|
|
41
|
+
return `BACKDROP: one perfectly smooth vertical gradient studio backdrop that fills the whole frame edge to edge, from ${top} at the top to ${bottom} at the bottom, taken from the character's own signature palette; no white areas, no blank paper, no visible wall or floor line, no light spots or bright patches, evenly lit and completely empty, the shot contains only the person against this full-bleed gradient, no scenery, nothing else in the frame.`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const LIGHTING = 'LIGHTING AND CAMERA: soft key light from camera left slightly above eye level, soft fill from the right, gentle falloff, even exposure with natural skin tone, highlights are soft and controlled. Sony A7R IV, 85mm f/1.8, ISO 800, shallow depth of field, fine film grain. The face and eyes are tack-sharp and in critical focus, no motion blur on the subject, the backdrop stays a smooth even gradient top to bottom. Ultra photorealistic, looks like a real person photographed at a cosplay shoot, not anime, not illustration, not 3D render, no plastic waxy skin. Vertical 9:16 composition.'
|
|
45
|
+
|
|
24
46
|
// Backdrops stay off the beige / sand / terracotta corner of the colour wheel:
|
|
25
47
|
// the P7b body-makeup mask treats low-saturation tan as skin, so a skin-toned
|
|
26
48
|
// paper backdrop gets brightened and left with visible streaks.
|
|
@@ -47,26 +69,38 @@ function hashText(text) {
|
|
|
47
69
|
return hash
|
|
48
70
|
}
|
|
49
71
|
|
|
50
|
-
export function defaultDetails(character, series = '') {
|
|
72
|
+
export function defaultDetails(character, series = '', options = {}) {
|
|
73
|
+
const name = String(character ?? '').trim() || 'a mysterious heroine'
|
|
74
|
+
const from = String(series ?? '').trim()
|
|
75
|
+
// Reference projects: the picture is the only design source, so the card
|
|
76
|
+
// must not invent hair / costume details that would fight the image.
|
|
77
|
+
const reference = options.reference === true
|
|
78
|
+
const canonical = from === '' ? `${name}'s official design` : `${name}'s official design in ${from}`
|
|
51
79
|
const palette = PALETTES[hashText(character + series) % PALETTES.length]
|
|
52
80
|
return {
|
|
53
|
-
character:
|
|
54
|
-
series:
|
|
81
|
+
character: name,
|
|
82
|
+
series: from,
|
|
55
83
|
genderWord: 'woman',
|
|
56
|
-
hair:
|
|
57
|
-
|
|
58
|
-
|
|
84
|
+
hair: reference
|
|
85
|
+
? 'the hairstyle and hair colour exactly as shown in the reference image'
|
|
86
|
+
: `${canonical}: the canonical hairstyle, hair colour and length`,
|
|
87
|
+
face: reference
|
|
88
|
+
? 'the face shape, features and eyes exactly as shown in the reference image'
|
|
89
|
+
: 'the canonical face shape and features of the character',
|
|
90
|
+
eyeColor: reference ? 'the eye colour shown in the reference image' : 'the canonical eye colour',
|
|
59
91
|
skinTone: SKIN_TONES[hashText(character) % SKIN_TONES.length],
|
|
60
92
|
bangsRule: DEFAULT_BANGS,
|
|
61
93
|
eyeCover: '',
|
|
62
94
|
braid: '',
|
|
63
|
-
costume:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
costume: reference
|
|
96
|
+
? 'the exact costume, accessories and held prop shown in the reference image, every piece and colour faithful to the picture, made in fabric, leather, metal and lace with visible stitching, believable material weight and clean seams, worn over a natural body with realistic proportions, together with the matching boots worn on both feet'
|
|
97
|
+
: `a faithful, well-made cosplay rendition of ${canonical}, covering the exact top, skirt or trousers, sleeves, belt and accessories, made in fabric and leather with visible stitching, believable material weight and clean seams, worn over a natural body with realistic proportions, together with the character's matching boots worn on both feet and, where the design calls for them, her gloves`,
|
|
98
|
+
paletteTop: reference ? 'ivory white' : palette[0],
|
|
99
|
+
paletteBottom: reference ? 'pale grey' : palette[1],
|
|
100
|
+
props: reference ? 'the signature prop shown in the reference image' : 'empty hands',
|
|
67
101
|
personality: 'composed, quietly confident, a little guarded',
|
|
68
|
-
identity: '
|
|
69
|
-
element: 'wind',
|
|
102
|
+
identity: reference ? 'the character shown in the reference image' : name,
|
|
103
|
+
element: reference ? '' : 'wind',
|
|
70
104
|
}
|
|
71
105
|
}
|
|
72
106
|
|
|
@@ -96,7 +130,7 @@ export function buildCard(details) {
|
|
|
96
130
|
'',
|
|
97
131
|
`HANDS: ${DEFAULT_HANDS}.`,
|
|
98
132
|
'',
|
|
99
|
-
|
|
133
|
+
LIGHTING,
|
|
100
134
|
].join('\n')
|
|
101
135
|
}
|
|
102
136
|
|
|
@@ -112,11 +146,55 @@ export function shotPrompt(card, shot) {
|
|
|
112
146
|
? 'her hands are empty and relaxed'
|
|
113
147
|
: `she holds ${props} lightly in one hand`,
|
|
114
148
|
)
|
|
149
|
+
parts.push(LIMB_DETAIL)
|
|
115
150
|
if (!/inside the frame/i.test(pose)) parts.push(POSE_FRAMING)
|
|
116
151
|
const line = parts.join('. ').replace(/\.\.+/g, '.').replace(/\.$/, '')
|
|
117
152
|
return card.replace(/^POSE: .*$/m, `POSE: ${line}.`)
|
|
118
153
|
}
|
|
119
154
|
|
|
155
|
+
/* -------------------------------------------- reference (uploaded picture) */
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Base image for a reference project: the picture carries every design detail.
|
|
159
|
+
* The optional detail line is the researched text description — img2img uses it
|
|
160
|
+
* to reconstruct pieces the illustration hides (back of the costume, materials).
|
|
161
|
+
*/
|
|
162
|
+
export function referenceBasePrompt(detailLine = '', palette = null) {
|
|
163
|
+
const details = String(detailLine ?? '').trim()
|
|
164
|
+
const paragraphs = [REFERENCE_IDENTITY]
|
|
165
|
+
if (details !== '') paragraphs.push(`CHARACTER DETAILS FROM RESEARCH: ${details}.`)
|
|
166
|
+
paragraphs.push(FRAMING, LIMB_DETAIL, referenceBackdrop(palette), LIGHTING)
|
|
167
|
+
return paragraphs.join('\n\n')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Shot prompt for a reference project. Like shotPrompt() it swaps only the
|
|
172
|
+
* action, but the character paragraphs come from the reference image instead
|
|
173
|
+
* of a name the text encoder may not know.
|
|
174
|
+
*/
|
|
175
|
+
export function referenceShotPrompt(shot, detailLine = '', palette = null) {
|
|
176
|
+
const pose = String(shot.pose ?? '').trim() || DEFAULT_POSE
|
|
177
|
+
const hands = String(shot.hands ?? '').trim()
|
|
178
|
+
const props = String(shot.props ?? '').trim()
|
|
179
|
+
const details = String(detailLine ?? '').trim()
|
|
180
|
+
const parts = [REFERENCE_IDENTITY]
|
|
181
|
+
if (details !== '') parts.push(`CHARACTER DETAILS FROM RESEARCH: ${details}.`)
|
|
182
|
+
parts.push(FRAMING, '')
|
|
183
|
+
if (String(shot.situation ?? '').trim() !== '') parts.push(`SITUATION: ${String(shot.situation).trim()}`)
|
|
184
|
+
if (String(shot.expression ?? '').trim() !== '') parts.push(`EXPRESSION: ${String(shot.expression).trim()}`)
|
|
185
|
+
parts.push(`POSE: ${pose}${/inside the frame/i.test(pose) ? '' : ` ${POSE_FRAMING}`}`)
|
|
186
|
+
parts.push(`HANDS: ${hands === '' ? DEFAULT_HANDS : hands}`)
|
|
187
|
+
parts.push(
|
|
188
|
+
props === '' || /^(no props|empty hands|none)$/i.test(props)
|
|
189
|
+
? 'PROP: her hands are empty and relaxed'
|
|
190
|
+
: `PROP: she holds ${props} lightly in one hand`,
|
|
191
|
+
)
|
|
192
|
+
if (String(shot.camera ?? '').trim() !== '') parts.push(`CAMERA: ${String(shot.camera).trim()}`)
|
|
193
|
+
parts.push(LIMB_DETAIL)
|
|
194
|
+
parts.push('', referenceBackdrop(palette), '', LIGHTING)
|
|
195
|
+
return parts.join('\n')
|
|
196
|
+
}
|
|
197
|
+
|
|
120
198
|
/* --------------------------------------------------------------- script */
|
|
121
199
|
|
|
122
200
|
const SHOT_ARCHETYPES = [
|
|
@@ -182,10 +260,15 @@ const SHOT_ARCHETYPES = [
|
|
|
182
260
|
},
|
|
183
261
|
]
|
|
184
262
|
|
|
185
|
-
/** Rule based script: the
|
|
263
|
+
/** Rule based script: the archetypes in a character-specific order, no model required. */
|
|
186
264
|
export function ruleScript(details, count = 6) {
|
|
187
265
|
const wanted = Math.max(1, Math.min(SHOT_ARCHETYPES.length, Number(count) || 6))
|
|
188
|
-
|
|
266
|
+
// Rotate the archetype order by a hash of the character name so two projects
|
|
267
|
+
// do not start with the same shot, and the fallback still varies.
|
|
268
|
+
const offset = Number.parseInt(hashText(String(details?.character ?? '')), 36) % SHOT_ARCHETYPES.length || 0
|
|
269
|
+
const ordered = [...SHOT_ARCHETYPES.slice(offset), ...SHOT_ARCHETYPES.slice(0, offset)]
|
|
270
|
+
const personality = String(details?.personality ?? '').trim()
|
|
271
|
+
return ordered.slice(0, wanted).map((shot, index) => ({
|
|
189
272
|
id: index + 1,
|
|
190
273
|
type: shot.type,
|
|
191
274
|
title: shot.title,
|
|
@@ -195,7 +278,7 @@ export function ruleScript(details, count = 6) {
|
|
|
195
278
|
hands: shot.hands,
|
|
196
279
|
camera: shot.camera,
|
|
197
280
|
props: shot.props === 'the character signature prop' ? (details?.props || 'empty hands') : shot.props,
|
|
198
|
-
prompt: `${shot.type}|${shot.title}`,
|
|
281
|
+
prompt: `${shot.type}|${shot.title}${personality === '' ? '' : `(${personality})`}`,
|
|
199
282
|
}))
|
|
200
283
|
}
|
|
201
284
|
|
|
@@ -283,20 +366,67 @@ JSON 字段(全部字符串):
|
|
|
283
366
|
"note": "一句中文备注:你判断最难还原的一件配饰"
|
|
284
367
|
}`
|
|
285
368
|
|
|
286
|
-
const SCRIPT_SYSTEM = `你是真人 cosplay 棚拍的摄影指导。给定角色设定,设计一组镜头脚本,只输出 JSON 数组,不要解释。
|
|
369
|
+
export const SCRIPT_SYSTEM = `你是真人 cosplay 棚拍的摄影指导。给定角色设定,设计一组镜头脚本,只输出 JSON 数组,不要解释。
|
|
287
370
|
|
|
288
371
|
硬性要求:
|
|
289
|
-
1. 每一镜必须同时给出六项:情境(situation)、表情(expression)、动作(pose)、手(hands)、机位(camera)、道具(props
|
|
290
|
-
2.
|
|
291
|
-
3.
|
|
292
|
-
4.
|
|
293
|
-
5.
|
|
294
|
-
6.
|
|
295
|
-
7.
|
|
372
|
+
1. 每一镜必须同时给出六项:情境(situation)、表情(expression)、动作(pose)、手(hands)、机位(camera)、道具(props),每项都要具体到可以直接写进图片提示词。
|
|
373
|
+
2. 情境写「她在做什么具体的事」:例如刚从舞台侧幕走出来、整理被风吹乱的披风、把道具横在身前打量、蹲下系鞋带;不要写「站着摆拍」这类空话。
|
|
374
|
+
3. 动作写清重心在左腿还是右腿、身体朝向、下巴高度、肩线松紧;表情写清眼睛看哪里、眼睑开合、嘴角状态;手写清两只手各自在哪、手指是张开/并拢/轻握。
|
|
375
|
+
4. 机位写清高度(平视/低机位/高机位)、角度(正面/三分之四侧面/背面回眸)、距离与构图占比;每镜至少两种机位,至少一镜不看镜头。
|
|
376
|
+
5. 差异化:表情不重样;手的位置每镜不同;姿态覆盖站、坐/蹲、动态抓拍、道具镜头中的至少三种;至少一镜带这个角色标志性的小动作或道具互动。
|
|
377
|
+
6. 每一镜至少两处只属于这个角色的具体细节(发饰、纹样、配色、道具、元素),不要写成通用模特。
|
|
378
|
+
7. 姿势必须完整落在画面内:全身从头到鞋,头顶留空、脚下留地;不要写只拍半身或裁掉腿的机位。
|
|
379
|
+
8. 只写正向画面,禁止 not / no / without 这类负向句;禁止摄影棚器材实景词(softbox、light stand、strobe、reflector、spotlight)。
|
|
380
|
+
9. 手部宁少勿多:能一手扶腰、一手垂放就够;道具只写单手轻握的一件;没有道具写 empty hands。
|
|
381
|
+
10. 角色一律 18 岁以上成年人;不写性化、暴露或暗示性动作。
|
|
382
|
+
11. 每组 4–6 镜,全部英文(type 与 title 用中文,title 不超过 12 个字)。
|
|
296
383
|
|
|
297
384
|
每镜字段:
|
|
298
385
|
{"type":"A 情绪特写|B 不看她|C 动态抓拍|D 标志动作 + 道具|E 极端机位|F 坐蹲卧","title":"中文小标题","situation":"...","expression":"...","pose":"...","hands":"...","camera":"...","props":"..."}`
|
|
299
386
|
|
|
387
|
+
/** User turn for the script model: character facts + the shots we already have
|
|
388
|
+
* (smart add must not repeat an existing expression/pose/camera). */
|
|
389
|
+
export function scriptUserText(details, count = 6, existing = []) {
|
|
390
|
+
const lines = [
|
|
391
|
+
`角色:${details?.character ?? ''}(${details?.series || '作品未知'})`,
|
|
392
|
+
`性格:${details?.personality ?? ''}`,
|
|
393
|
+
`身份:${details?.identity ?? ''}`,
|
|
394
|
+
`元素/能力:${details?.element ?? ''}`,
|
|
395
|
+
`标志道具:${details?.props ?? 'empty hands'}`,
|
|
396
|
+
`发型发色:${details?.hair ?? ''}`,
|
|
397
|
+
`服装:${String(details?.costume ?? '').slice(0, 400)}`,
|
|
398
|
+
]
|
|
399
|
+
if (Array.isArray(existing) && existing.length > 0) {
|
|
400
|
+
lines.push('', '已有分镜(新分镜必须与它们不重复;可延续同一组角色的性格与服装细节):')
|
|
401
|
+
existing.slice(0, 12).forEach((shot, index) => {
|
|
402
|
+
lines.push(`${index + 1}. ${shot?.title ?? ''}|情境:${shot?.situation ?? ''}|表情:${shot?.expression ?? ''}|动作:${shot?.pose ?? ''}|手:${shot?.hands ?? ''}|机位:${shot?.camera ?? ''}`)
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
lines.push('', `需要${existing?.length > 0 ? '新增' : '输出'}的镜头数量:${count}`, '按系统提示输出 JSON 数组。')
|
|
406
|
+
return lines.join('\n')
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/** Normalise a model's shot array into the project shot shape. */
|
|
410
|
+
export function normalizeShotItems(list, startIndex = 1) {
|
|
411
|
+
if (!Array.isArray(list) || list.length === 0) throw new Error('脚本不是数组')
|
|
412
|
+
return list.slice(0, 8).map((item, index) => {
|
|
413
|
+
const type = String(item?.type ?? '').trim() || `${String.fromCharCode(65 + ((startIndex - 1 + index) % 6))} 镜头`
|
|
414
|
+
const title = String(item?.title ?? '').trim() || `镜头${startIndex + index}`
|
|
415
|
+
return {
|
|
416
|
+
id: startIndex + index,
|
|
417
|
+
type,
|
|
418
|
+
title,
|
|
419
|
+
situation: String(item?.situation ?? '').trim(),
|
|
420
|
+
expression: String(item?.expression ?? '').trim(),
|
|
421
|
+
pose: String(item?.pose ?? '').trim(),
|
|
422
|
+
hands: String(item?.hands ?? '').trim(),
|
|
423
|
+
camera: String(item?.camera ?? '').trim(),
|
|
424
|
+
props: String(item?.props ?? 'empty hands').trim() || 'empty hands',
|
|
425
|
+
prompt: `${type}|${title}`,
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
}
|
|
429
|
+
|
|
300
430
|
/** Ask the chat model for the character details (throws when unavailable). */
|
|
301
431
|
export async function researchCharacter(config, { character, series = '' } = {}) {
|
|
302
432
|
if (!llmConfigured(config)) throw new Error('未配置对话模型')
|
|
@@ -319,32 +449,6 @@ export async function researchCharacter(config, { character, series = '' } = {})
|
|
|
319
449
|
/** Ask the chat model for the shot script (throws when unavailable). */
|
|
320
450
|
export async function writeScript(config, details, count = 6) {
|
|
321
451
|
if (!llmConfigured(config)) throw new Error('未配置对话模型')
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
`性格:${details?.personality ?? ''}`,
|
|
325
|
-
`身份:${details?.identity ?? ''}`,
|
|
326
|
-
`元素/能力:${details?.element ?? ''}`,
|
|
327
|
-
`标志道具:${details?.props ?? 'empty hands'}`,
|
|
328
|
-
`镜头数量:${count}`,
|
|
329
|
-
'按系统提示输出 JSON 数组。',
|
|
330
|
-
].join('\n')
|
|
331
|
-
const text = await chatComplete(config, SCRIPT_SYSTEM, user)
|
|
332
|
-
const list = extractJson(text)
|
|
333
|
-
if (!Array.isArray(list) || list.length === 0) throw new Error('脚本不是数组')
|
|
334
|
-
return list.slice(0, 8).map((item, index) => {
|
|
335
|
-
const type = String(item.type ?? '').trim() || `${String.fromCharCode(65 + index)} 镜头`
|
|
336
|
-
const title = String(item.title ?? '').trim() || `镜头${index + 1}`
|
|
337
|
-
return {
|
|
338
|
-
id: index + 1,
|
|
339
|
-
type,
|
|
340
|
-
title,
|
|
341
|
-
situation: String(item.situation ?? '').trim(),
|
|
342
|
-
expression: String(item.expression ?? '').trim(),
|
|
343
|
-
pose: String(item.pose ?? '').trim(),
|
|
344
|
-
hands: String(item.hands ?? '').trim(),
|
|
345
|
-
camera: String(item.camera ?? '').trim(),
|
|
346
|
-
props: String(item.props ?? 'empty hands').trim() || 'empty hands',
|
|
347
|
-
prompt: `${type}|${title}`,
|
|
348
|
-
}
|
|
349
|
-
})
|
|
452
|
+
const text = await chatComplete(config, SCRIPT_SYSTEM, scriptUserText(details, count))
|
|
453
|
+
return normalizeShotItems(extractJson(text), 1)
|
|
350
454
|
}
|