@waterwx/dsh-novel-forge 1.7.2 → 1.7.3
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 +1 -1
- package/lib/client.js +918 -287
- package/lib/client.js.map +1 -1
- package/lib/index.js +372 -42
- package/lib/index.js.map +1 -1
- package/lib/types/protocol.d.ts +14 -10
- package/lib/types/shot-language.d.ts +46 -0
- package/lib/types/story-beat-language.d.ts +35 -0
- package/package.json +4 -2
- package/skills/chapter-batch/SKILL.md +81 -0
- package/src/client/panel/CreateBookView.tsx +4 -0
- package/src/client/panel/ImportModal.tsx +11 -2
- package/src/client/panel/NovelPanel.tsx +1 -0
- package/src/client/panel/RunPanel.tsx +1 -1
- package/src/client/panel/SceneLibrary.tsx +1 -1
- package/src/client/panel/ShelfView.tsx +2 -2
- package/src/client/panel/StoryboardTab.tsx +42 -10
- package/src/engine.ts +35 -14
- package/src/index.ts +1 -0
- package/src/protocol.ts +22 -10
- package/src/routes.ts +13 -7
- package/src/run.ts +5 -4
- package/src/shot-language.ts +181 -0
- package/src/story-beat-language.ts +130 -0
- package/lib/types/client/panel/RoleVisualPanel.d.ts +0 -15
package/src/protocol.ts
CHANGED
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
* state file format, and the NDJSON generation stream frames all live here so
|
|
5
5
|
* both halves spell exactly one vocabulary.
|
|
6
6
|
*/
|
|
7
|
+
import type {
|
|
8
|
+
ShotSizeId,
|
|
9
|
+
CameraMoveId,
|
|
10
|
+
CompositionId,
|
|
11
|
+
LightingId,
|
|
12
|
+
} from './shot-language.ts'
|
|
13
|
+
import type {
|
|
14
|
+
StoryFunctionId,
|
|
15
|
+
EmotionId,
|
|
16
|
+
} from './story-beat-language.ts'
|
|
7
17
|
|
|
8
18
|
/** The /api/dsh-novel-forge route family (same-origin, loopback-fenced). */
|
|
9
19
|
export const NOVEL_API = {
|
|
@@ -212,10 +222,10 @@ export interface StoryboardBeat {
|
|
|
212
222
|
id: string
|
|
213
223
|
/** 事件一句话(发生了什么)。 */
|
|
214
224
|
event: string
|
|
215
|
-
/**
|
|
216
|
-
emotion:
|
|
217
|
-
/**
|
|
218
|
-
function:
|
|
225
|
+
/** 情绪走向(情绪词 id 数组,来自 story-beat-language 词库,链式)。 */
|
|
226
|
+
emotion: EmotionId[]
|
|
227
|
+
/** 叙事功能(枚举 id,来自 story-beat-language 词库)。 */
|
|
228
|
+
function: StoryFunctionId
|
|
219
229
|
/** 因果:承接上一节拍的原因(可选)。 */
|
|
220
230
|
cause?: string
|
|
221
231
|
}
|
|
@@ -247,10 +257,12 @@ export interface StoryboardShot {
|
|
|
247
257
|
id: string
|
|
248
258
|
/** 挂载的节拍 id(骨架中的 b1…)。 */
|
|
249
259
|
beatId: string
|
|
250
|
-
/**
|
|
251
|
-
shot:
|
|
252
|
-
/**
|
|
253
|
-
camera:
|
|
260
|
+
/** 景别(枚举 id,来自 shot-language 词库)。 */
|
|
261
|
+
shot: ShotSizeId
|
|
262
|
+
/** 机位与运镜(枚举 id 数组,来自 shot-language 词库)。 */
|
|
263
|
+
camera: CameraMoveId[]
|
|
264
|
+
/** 构图(可选,枚举 id)。 */
|
|
265
|
+
composition?: CompositionId
|
|
254
266
|
/** 时长(秒,1-12)。 */
|
|
255
267
|
duration: number
|
|
256
268
|
/** 画面内容:角色动作 + 表情 + 服装/标志物。 */
|
|
@@ -259,8 +271,8 @@ export interface StoryboardShot {
|
|
|
259
271
|
line: string
|
|
260
272
|
/** 音效(无则空字符串)。 */
|
|
261
273
|
sound: string
|
|
262
|
-
/**
|
|
263
|
-
light:
|
|
274
|
+
/** 光效(枚举 id 数组,来自 shot-language 词库)。 */
|
|
275
|
+
light: LightingId[]
|
|
264
276
|
/** 承接上一镜头结尾状态(位置/动作/情绪/服装)。 */
|
|
265
277
|
prevState: string
|
|
266
278
|
/** 本镜头结束状态。 */
|
package/src/routes.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
12
|
-
import {
|
|
12
|
+
import { spawn } from 'node:child_process'
|
|
13
13
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import { join, basename, extname } from 'node:path'
|
|
15
15
|
import { homedir } from 'node:os'
|
|
@@ -2296,12 +2296,18 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2296
2296
|
if (!guard(req, res, 'POST')) return
|
|
2297
2297
|
const config = getConfig()
|
|
2298
2298
|
const dir = config.outputDir
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
}
|
|
2299
|
+
const child = spawn('explorer', [dir], { shell: false })
|
|
2300
|
+
let responded = false
|
|
2301
|
+
child.on('error', (error) => {
|
|
2302
|
+
if (responded) return
|
|
2303
|
+
responded = true
|
|
2304
|
+
writeJson(res, 500, { ok: false, error: error.message })
|
|
2305
|
+
})
|
|
2306
|
+
child.on('exit', (code) => {
|
|
2307
|
+
if (responded) return
|
|
2308
|
+
responded = true
|
|
2309
|
+
if (code === 0) writeJson(res, 200, { ok: true })
|
|
2310
|
+
else writeJson(res, 500, { ok: false, error: `explorer 退出码 ${code}` })
|
|
2305
2311
|
})
|
|
2306
2312
|
},
|
|
2307
2313
|
}
|
package/src/run.ts
CHANGED
|
@@ -306,11 +306,12 @@ export class ProductionRunner {
|
|
|
306
306
|
|
|
307
307
|
private applyDraft(project: ProjectState, chapter: ChapterPlan, draft: string, report: ReviewReport): void {
|
|
308
308
|
const config = this.deps.getConfig()
|
|
309
|
+
const outputDir = this.bookDir ?? config.outputDir
|
|
309
310
|
const fileName = chapterFileName(chapter)
|
|
310
|
-
mkdirSync(
|
|
311
|
-
const targetPath = join(
|
|
311
|
+
mkdirSync(outputDir, { recursive: true })
|
|
312
|
+
const targetPath = join(outputDir, fileName)
|
|
312
313
|
if (existsSync(targetPath)) {
|
|
313
|
-
copyFileSync(targetPath, join(
|
|
314
|
+
copyFileSync(targetPath, join(outputDir, `${fileName.replace(/\.md$/, '')}.bak.md`))
|
|
314
315
|
}
|
|
315
316
|
writeFileSync(targetPath, `# 第${chapter.no}章 ${chapter.title}\n\n${draft}\n`, 'utf8')
|
|
316
317
|
chapter.pendingDraft = undefined
|
|
@@ -325,6 +326,6 @@ export class ProductionRunner {
|
|
|
325
326
|
}
|
|
326
327
|
chapter.error = undefined
|
|
327
328
|
project.updatedAt = new Date().toISOString()
|
|
328
|
-
saveProject(
|
|
329
|
+
saveProject(outputDir, project)
|
|
329
330
|
}
|
|
330
331
|
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 镜头语言词库(分镜规范化基准)。
|
|
3
|
+
* 分镜表的 shot/camera/composition/light 字段统一从这里取值(英文 id),
|
|
4
|
+
* 生成提示词时用 zh 中文词块;适配英文平台时用 en。
|
|
5
|
+
* 目的:让"景别/运镜/构图/光效"有标准词表,避免 LLM 自由发挥导致漂移。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 景别。 */
|
|
9
|
+
export type ShotSizeId =
|
|
10
|
+
| 'extreme_wide' | 'wide' | 'full' | 'medium'
|
|
11
|
+
| 'medium_close' | 'close' | 'extreme_close' | 'big_extreme_close'
|
|
12
|
+
|
|
13
|
+
/** 运镜/机位。 */
|
|
14
|
+
export type CameraMoveId =
|
|
15
|
+
| 'static' | 'dolly_in' | 'dolly_out' | 'pan_left' | 'pan_right'
|
|
16
|
+
| 'track_left' | 'track_right' | 'follow' | 'pedestal_up' | 'pedestal_down'
|
|
17
|
+
| 'orbit' | 'handheld' | 'low_angle' | 'high_angle' | 'over_shoulder'
|
|
18
|
+
|
|
19
|
+
/** 构图。 */
|
|
20
|
+
export type CompositionId =
|
|
21
|
+
| 'rule_of_thirds' | 'center' | 'leading_line' | 'foreground'
|
|
22
|
+
| 'low' | 'overhead' | 'symmetry'
|
|
23
|
+
|
|
24
|
+
/** 光效。 */
|
|
25
|
+
export type LightingId =
|
|
26
|
+
| 'front' | 'side' | 'back' | 'top' | 'rembrandt'
|
|
27
|
+
| 'neon' | 'hard' | 'soft' | 'mood' | 'contrast'
|
|
28
|
+
|
|
29
|
+
export interface ShotLangEntry<T extends string> {
|
|
30
|
+
id: T
|
|
31
|
+
/** 中文(提示词/展示用)。 */
|
|
32
|
+
zh: string
|
|
33
|
+
/** 英文(英文平台适配用)。 */
|
|
34
|
+
en: string
|
|
35
|
+
/** 一句话说明(下拉/卡片)。 */
|
|
36
|
+
hint: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const SHOT_SIZES: readonly ShotLangEntry<ShotSizeId>[] = [
|
|
40
|
+
{ id: 'extreme_wide', zh: '大远景', en: 'extreme wide shot', hint: '环境为主,人物很小' },
|
|
41
|
+
{ id: 'wide', zh: '远景', en: 'wide shot', hint: '人在景中,交代环境' },
|
|
42
|
+
{ id: 'full', zh: '全景', en: 'full shot', hint: '全身』人物整体' },
|
|
43
|
+
{ id: 'medium', zh: '中景', en: 'medium shot', hint: '膝部以上,叙事主力' },
|
|
44
|
+
{ id: 'medium_close', zh: '中近景', en: 'medium close-up', hint: '胸以上,表情+动作' },
|
|
45
|
+
{ id: 'close', zh: '近景', en: 'close-up', hint: '肩以上,突出表情' },
|
|
46
|
+
{ id: 'extreme_close', zh: '特写', en: 'extreme close-up', hint: '脸/物细节' },
|
|
47
|
+
{ id: 'big_extreme_close', zh: '大特写', en: 'big close-up', hint: '局部(眼/手/标志物)' },
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
export const CAMERA_MOVES: readonly ShotLangEntry<CameraMoveId>[] = [
|
|
51
|
+
{ id: 'static', zh: '固定机位', en: 'static camera', hint: '机位不动' },
|
|
52
|
+
{ id: 'dolly_in', zh: '推近', en: 'dolly in', hint: '镜头向主体逼近' },
|
|
53
|
+
{ id: 'dolly_out', zh: '拉远', en: 'dolly out', hint: '镜头远离主体' },
|
|
54
|
+
{ id: 'pan_left', zh: '左摇', en: 'pan left', hint: '机位不动,镜头左转' },
|
|
55
|
+
{ id: 'pan_right', zh: '右摇', en: 'pan right', hint: '机位不动,镜头右转' },
|
|
56
|
+
{ id: 'track_left', zh: '左横移', en: 'track left', hint: '机位随主体左移' },
|
|
57
|
+
{ id: 'track_right', zh: '右横移', en: 'track right', hint: '机位随主体右移' },
|
|
58
|
+
{ id: 'follow', zh: '跟随', en: 'follow shot', hint: '镜头跟着主体运动' },
|
|
59
|
+
{ id: 'pedestal_up', zh: '升镜', en: 'pedestal up', hint: '机位抬高' },
|
|
60
|
+
{ id: 'pedestal_down', zh: '降镜', en: 'pedestal down', hint: '机位降低' },
|
|
61
|
+
{ id: 'orbit', zh: '环绕', en: 'orbit shot', hint: '镜头绕主体转' },
|
|
62
|
+
{ id: 'handheld', zh: '手持晃动', en: 'handheld', hint: '真实感/紧张感' },
|
|
63
|
+
{ id: 'low_angle', zh: '低机位仰拍', en: 'low angle', hint: '突出威严/压迫' },
|
|
64
|
+
{ id: 'high_angle', zh: '高机位俯拍', en: 'high angle', hint: '突出渺小/全知' },
|
|
65
|
+
{ id: 'over_shoulder', zh: '过肩镜头', en: 'over-the-shoulder', hint: '对话常用' },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
export const COMPOSITIONS: readonly ShotLangEntry<CompositionId>[] = [
|
|
69
|
+
{ id: 'rule_of_thirds', zh: '三分法', en: 'rule of thirds', hint: '主体偏三分之一处' },
|
|
70
|
+
{ id: 'center', zh: '中心对称', en: 'centered', hint: '主体居中' },
|
|
71
|
+
{ id: 'leading_line', zh: '引导线', en: 'leading lines', hint: '视线沿线条引向主体' },
|
|
72
|
+
{ id: 'foreground', zh: '前景遮挡', en: 'foreground framing', hint: '前景物框住主体' },
|
|
73
|
+
{ id: 'low', zh: '低机位', en: 'low camera', hint: '低于视线' },
|
|
74
|
+
{ id: 'overhead', zh: '俯拍', en: 'overhead', hint: '从上方俯视' },
|
|
75
|
+
{ id: 'symmetry', zh: '对称构图', en: 'symmetrical', hint: '左右镜像' },
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
export const LIGHTINGS: readonly ShotLangEntry<LightingId>[] = [
|
|
79
|
+
{ id: 'front', zh: '顺光', en: 'front light', hint: '正面均匀照明' },
|
|
80
|
+
{ id: 'side', zh: '侧光', en: 'side light', hint: '明暗对比强' },
|
|
81
|
+
{ id: 'back', zh: '逆光', en: 'back light', hint: '轮廓光/剪影' },
|
|
82
|
+
{ id: 'top', zh: '顶光', en: 'top light', hint: '俯照硬朗' },
|
|
83
|
+
{ id: 'rembrandt', zh: '伦勃朗光', en: 'rembrandt lighting', hint: '侧逆光,经典人像' },
|
|
84
|
+
{ id: 'neon', zh: '霓虹光', en: 'neon lighting', hint: '赛博/夜景' },
|
|
85
|
+
{ id: 'hard', zh: '硬光', en: 'hard light', hint: '犀利阴影' },
|
|
86
|
+
{ id: 'soft', zh: '柔光', en: 'soft light', hint: '柔和过渡' },
|
|
87
|
+
{ id: 'mood', zh: '氛围光', en: 'mood lighting', hint: '情绪化色温' },
|
|
88
|
+
{ id: 'contrast', zh: '高反差', en: 'high contrast', hint: '黑白/强明暗' },
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
export interface ShotLanguage {
|
|
92
|
+
shot?: ShotSizeId
|
|
93
|
+
camera: CameraMoveId[]
|
|
94
|
+
composition?: CompositionId
|
|
95
|
+
light: LightingId[]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 从中文文本归一化到景别(处理旧数据/LLM 口语)。未知回退 medium。 */
|
|
99
|
+
export function normalizeShotSize(text: string | undefined): ShotSizeId {
|
|
100
|
+
if (text === undefined || text === '') return 'medium'
|
|
101
|
+
const t = text.trim()
|
|
102
|
+
if (t.includes('大远景')) return 'extreme_wide'
|
|
103
|
+
if (t.includes('远景')) return 'wide'
|
|
104
|
+
if (t.includes('全景')) return 'full'
|
|
105
|
+
if (t.includes('中近景') || t.includes('中景')) return 'medium'
|
|
106
|
+
if (t.includes('近景') || t.includes('胸')) return 'close'
|
|
107
|
+
if (t.includes('大特写')) return 'big_extreme_close'
|
|
108
|
+
if (t.includes('特写')) return 'extreme_close'
|
|
109
|
+
return 'medium'
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** 从中文文本归一化到运镜列表。 */
|
|
113
|
+
export function normalizeCameras(text: string | undefined): CameraMoveId[] {
|
|
114
|
+
if (text === undefined || text === '') return ['static']
|
|
115
|
+
const t = text.trim()
|
|
116
|
+
const out: CameraMoveId[] = []
|
|
117
|
+
const push = (id: CameraMoveId) => { if (!out.includes(id)) out.push(id) }
|
|
118
|
+
if (t.includes('推近') || t.includes('推进')) push('dolly_in')
|
|
119
|
+
if (t.includes('拉远') || t.includes('拉出')) push('dolly_out')
|
|
120
|
+
if (t.includes('左摇')) push('pan_left')
|
|
121
|
+
if (t.includes('右摇')) push('pan_right')
|
|
122
|
+
if (t.includes('横移')) push(t.includes('左') ? 'track_left' : 'track_right')
|
|
123
|
+
if (t.includes('跟随') || t.includes('跟拍')) push('follow')
|
|
124
|
+
if (t.includes('升降') || t.includes('升') || t.includes('降')) push('pedestal_up')
|
|
125
|
+
if (t.includes('环绕')) push('orbit')
|
|
126
|
+
if (t.includes('手持') || t.includes('晃动')) push('handheld')
|
|
127
|
+
if (t.includes('低机位') || t.includes('仰拍') || t.includes('仰')) push('low_angle')
|
|
128
|
+
if (t.includes('高机位') || t.includes('俯拍') || t.includes('俯')) push('high_angle')
|
|
129
|
+
if (t.includes('过肩')) push('over_shoulder')
|
|
130
|
+
if (out.length === 0) push('static')
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** 从中文文本归一化到构图(可选)。 */
|
|
135
|
+
export function normalizeComposition(text: string | undefined): CompositionId | undefined {
|
|
136
|
+
if (text === undefined || text === '') return undefined
|
|
137
|
+
const t = text.trim()
|
|
138
|
+
if (t.includes('三分')) return 'rule_of_thirds'
|
|
139
|
+
if (t.includes('中心') || t.includes('居中')) return 'center'
|
|
140
|
+
if (t.includes('引导')) return 'leading_line'
|
|
141
|
+
if (t.includes('前景')) return 'foreground'
|
|
142
|
+
if (t.includes('低机位')) return 'low'
|
|
143
|
+
if (t.includes('俯拍') || t.includes('俯视')) return 'overhead'
|
|
144
|
+
if (t.includes('对称')) return 'symmetry'
|
|
145
|
+
return undefined
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** 从中文文本归一化到光效列表。 */
|
|
149
|
+
export function normalizeLightings(text: string | undefined): LightingId[] {
|
|
150
|
+
if (text === undefined || text === '') return ['soft']
|
|
151
|
+
const t = text.trim()
|
|
152
|
+
const out: LightingId[] = []
|
|
153
|
+
const push = (id: LightingId) => { if (!out.includes(id)) out.push(id) }
|
|
154
|
+
if (t.includes('顺光')) push('front')
|
|
155
|
+
if (t.includes('侧光') || t.includes('伦勃朗')) push(t.includes('伦勃朗') ? 'rembrandt' : 'side')
|
|
156
|
+
if (t.includes('逆光')) push('back')
|
|
157
|
+
if (t.includes('顶光')) push('top')
|
|
158
|
+
if (t.includes('霓虹')) push('neon')
|
|
159
|
+
if (t.includes('硬光')) push('hard')
|
|
160
|
+
if (t.includes('柔光')) push('soft')
|
|
161
|
+
if (t.includes('氛围') || t.includes('情绪')) push('mood')
|
|
162
|
+
if (t.includes('高反差')) push('contrast')
|
|
163
|
+
if (out.length === 0) push('soft')
|
|
164
|
+
return out
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** 取词条中文(ZHs 合并句子)。 */
|
|
168
|
+
export function sizeZh(id: ShotSizeId | undefined): string {
|
|
169
|
+
return SHOT_SIZES.find(e => e.id === id)?.zh ?? '中景'
|
|
170
|
+
}
|
|
171
|
+
export function cameraZh(ids: CameraMoveId[] | undefined): string {
|
|
172
|
+
if (ids === undefined || ids.length === 0) return '固定机位'
|
|
173
|
+
return CAMERA_MOVES.filter(e => ids.includes(e.id)).map(e => e.zh).join(' + ')
|
|
174
|
+
}
|
|
175
|
+
export function compoZh(id: CompositionId | undefined): string {
|
|
176
|
+
return COMPOSITIONS.find(e => e.id === id)?.zh ?? ''
|
|
177
|
+
}
|
|
178
|
+
export function lightZh(ids: LightingId[] | undefined): string {
|
|
179
|
+
if (ids === undefined || ids.length === 0) return '柔光'
|
|
180
|
+
return LIGHTINGS.filter(e => ids.includes(e.id)).map(e => e.zh).join(' + ')
|
|
181
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 编剧词库(剧情骨架规范化基准)。
|
|
3
|
+
* 骨架的 function(叙事功能)与 emotion(情绪走向)统一从这里取值(英文 id),
|
|
4
|
+
* 生成提示词/展示时用 zh 中文。
|
|
5
|
+
* 目的:让"叙事功能/情绪"有标准词表,避免 LLM 自由发挥导致情绪链漂移。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** 叙事功能(对应节拍在剧情中的作用)。 */
|
|
9
|
+
export type StoryFunctionId =
|
|
10
|
+
| 'exposition' | 'conflict' | 'turn' | 'climax'
|
|
11
|
+
| 'resolve' | 'foreshadow' | 'character'
|
|
12
|
+
|
|
13
|
+
/** 情绪词(低→高能量的情绪阶,角色在节拍中的主要情绪点)。 */
|
|
14
|
+
export type EmotionId =
|
|
15
|
+
// 起:低能量
|
|
16
|
+
| 'calm' | 'indifferent' | 'expectant' | 'curious' | 'alert'
|
|
17
|
+
// 承:蓄势
|
|
18
|
+
| 'suppressed' | 'enduring' | 'worried' | 'irritable' | 'uneasy'
|
|
19
|
+
// 转:激化
|
|
20
|
+
| 'terrified' | 'angry' | 'collapsing' | 'resolute' | 'grieved'
|
|
21
|
+
// 合:落点
|
|
22
|
+
| 'relieved' | 'bittersweet' | 'triumphant' | 'reborn' | 'numb'
|
|
23
|
+
|
|
24
|
+
export interface BeatLangEntry<T extends string> {
|
|
25
|
+
id: T
|
|
26
|
+
/** 中文(展示用)。 */
|
|
27
|
+
zh: string
|
|
28
|
+
/** 英文(英文平台适配用)。 */
|
|
29
|
+
en: string
|
|
30
|
+
/** 一句话说明。 */
|
|
31
|
+
hint: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** 叙事功能词表。 */
|
|
35
|
+
export const STORY_FUNCTIONS: readonly BeatLangEntry<StoryFunctionId>[] = [
|
|
36
|
+
{ id: 'exposition', zh: '铺垫', en: 'exposition', hint: '介绍背景/人物/悬念' },
|
|
37
|
+
{ id: 'conflict', zh: '冲突', en: 'conflict', hint: '矛盾显现/加剧' },
|
|
38
|
+
{ id: 'turn', zh: '转折', en: 'turning point', hint: '剧情反转/方向改变' },
|
|
39
|
+
{ id: 'climax', zh: '高潮', en: 'climax', hint: '冲突顶峰/揭示' },
|
|
40
|
+
{ id: 'resolve', zh: '收束', en: 'resolution', hint: '矛盾化解/结果' },
|
|
41
|
+
{ id: 'foreshadow', zh: '伏笔', en: 'foreshadowing', hint: '埋下后续线索' },
|
|
42
|
+
{ id: 'character', zh: '人物塑造', en: 'characterization', hint: '展现人物特质/转变' },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
/** 情绪词表(按能量/阶段排序)。 */
|
|
46
|
+
export const EMOTIONS: readonly BeatLangEntry<EmotionId>[] = [
|
|
47
|
+
// 起:低能量
|
|
48
|
+
{ id: 'calm', zh: '平静', en: 'calm', hint: '无波澜' },
|
|
49
|
+
{ id: 'indifferent', zh: '淡然', en: 'indifferent', hint: '不在乎' },
|
|
50
|
+
{ id: 'expectant', zh: '期待', en: 'expectant', hint: '有所期盼' },
|
|
51
|
+
{ id: 'curious', zh: '好奇', en: 'curious', hint: '想要探究' },
|
|
52
|
+
{ id: 'alert', zh: '警觉', en: 'alert', hint: '察觉到异样' },
|
|
53
|
+
// 承:蓄势
|
|
54
|
+
{ id: 'suppressed', zh: '压抑', en: 'suppressed', hint: '情绪被压制' },
|
|
55
|
+
{ id: 'enduring', zh: '隐忍', en: 'enduring', hint: '强忍着' },
|
|
56
|
+
{ id: 'worried', zh: '担忧', en: 'worried', hint: '担心后果' },
|
|
57
|
+
{ id: 'irritable', zh: '焦躁', en: 'irritable', hint: '烦躁不耐' },
|
|
58
|
+
{ id: 'uneasy', zh: '不安', en: 'uneasy', hint: '心里没底' },
|
|
59
|
+
// 转:激化
|
|
60
|
+
{ id: 'terrified', zh: '惊惧', en: 'terrified', hint: '极度恐惧' },
|
|
61
|
+
{ id: 'angry', zh: '愤怒', en: 'angry', hint: '强烈不满' },
|
|
62
|
+
{ id: 'collapsing', zh: '崩溃', en: 'collapsing', hint: '情绪失控' },
|
|
63
|
+
{ id: 'resolute', zh: '决绝', en: 'resolute', hint: '下定狠心' },
|
|
64
|
+
{ id: 'grieved', zh: '痛心', en: 'grieved', hint: '悲伤心痛' },
|
|
65
|
+
// 合:落点
|
|
66
|
+
{ id: 'relieved', zh: '释然', en: 'relieved', hint: '放下包袱' },
|
|
67
|
+
{ id: 'bittersweet', zh: '悲凉', en: 'bittersweet', hint: '苦涩无奈' },
|
|
68
|
+
{ id: 'triumphant', zh: '得意', en: 'triumphant', hint: '占据上风' },
|
|
69
|
+
{ id: 'reborn', zh: '重生', en: 'reborn', hint: '脱胎换骨' },
|
|
70
|
+
{ id: 'numb', zh: '麻木', en: 'numb', hint: '失去感知' },
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
export interface StoryBeatLanguage {
|
|
74
|
+
function: StoryFunctionId
|
|
75
|
+
emotion: EmotionId[]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 从中文归一化叙事功能(未知回退 exposition)。 */
|
|
79
|
+
export function normalizeStoryFunction(text: string | undefined): StoryFunctionId {
|
|
80
|
+
if (text === undefined || text === '') return 'exposition'
|
|
81
|
+
const t = text.trim()
|
|
82
|
+
if (t.includes('铺垫')) return 'exposition'
|
|
83
|
+
if (t.includes('冲突')) return 'conflict'
|
|
84
|
+
if (t.includes('转折')) return 'turn'
|
|
85
|
+
if (t.includes('高潮')) return 'climax'
|
|
86
|
+
if (t.includes('收束')) return 'resolve'
|
|
87
|
+
if (t.includes('伏笔')) return 'foreshadow'
|
|
88
|
+
if (t.includes('人物塑造')) return 'character'
|
|
89
|
+
return 'exposition'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** 从中文文本归一化情绪词列表(可含→箭头链)。 */
|
|
93
|
+
export function normalizeEmotions(text: string | undefined): EmotionId[] {
|
|
94
|
+
if (text === undefined || text === '') return ['calm']
|
|
95
|
+
const t = text.trim()
|
|
96
|
+
const out: EmotionId[] = []
|
|
97
|
+
const push = (id: EmotionId) => { if (!out.includes(id)) out.push(id) }
|
|
98
|
+
if (t.includes('平静')) push('calm')
|
|
99
|
+
if (t.includes('淡然')) push('indifferent')
|
|
100
|
+
if (t.includes('期待')) push('expectant')
|
|
101
|
+
if (t.includes('好奇')) push('curious')
|
|
102
|
+
if (t.includes('警觉')) push('alert')
|
|
103
|
+
if (t.includes('压抑')) push('suppressed')
|
|
104
|
+
if (t.includes('隐忍')) push('enduring')
|
|
105
|
+
if (t.includes('担忧')) push('worried')
|
|
106
|
+
if (t.includes('焦躁')) push('irritable')
|
|
107
|
+
if (t.includes('不安')) push('uneasy')
|
|
108
|
+
if (t.includes('惊惧')) push('terrified')
|
|
109
|
+
if (t.includes('愤怒')) push('angry')
|
|
110
|
+
if (t.includes('崩溃')) push('collapsing')
|
|
111
|
+
if (t.includes('决绝')) push('resolute')
|
|
112
|
+
if (t.includes('痛心')) push('grieved')
|
|
113
|
+
if (t.includes('释然')) push('relieved')
|
|
114
|
+
if (t.includes('悲凉')) push('bittersweet')
|
|
115
|
+
if (t.includes('得意')) push('triumphant')
|
|
116
|
+
if (t.includes('重生')) push('reborn')
|
|
117
|
+
if (t.includes('麻木')) push('numb')
|
|
118
|
+
if (out.length === 0) push('calm')
|
|
119
|
+
return out
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** 叙事功能中文。 */
|
|
123
|
+
export function functionZh(id: StoryFunctionId | undefined): string {
|
|
124
|
+
return STORY_FUNCTIONS.find(e => e.id === id)?.zh ?? '铺垫'
|
|
125
|
+
}
|
|
126
|
+
/** 情绪中文(箭头连接)。 */
|
|
127
|
+
export function emotionZh(ids: EmotionId[] | undefined): string {
|
|
128
|
+
if (ids === undefined || ids.length === 0) return '平静'
|
|
129
|
+
return EMOTIONS.filter(e => ids.includes(e.id)).map(e => e.zh).join('→')
|
|
130
|
+
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { NovelApi } from '../api.ts';
|
|
2
|
-
import type { ImageModelConfig, ProjectState } from '../../protocol.ts';
|
|
3
|
-
export declare function RoleVisualPanel({ api, project, refresh, styleId, filterId, imageApiEnabled, imageModels, onGotoRoles, onProgress, }: {
|
|
4
|
-
api: NovelApi;
|
|
5
|
-
project: ProjectState | null;
|
|
6
|
-
refresh: () => void | Promise<void>;
|
|
7
|
-
styleId?: string;
|
|
8
|
-
filterId?: string;
|
|
9
|
-
imageApiEnabled?: boolean;
|
|
10
|
-
/** 生图模型库(AI 生图时可选择用哪条)。 */
|
|
11
|
-
imageModels?: ImageModelConfig[];
|
|
12
|
-
onGotoRoles?: () => void;
|
|
13
|
-
/** 上报到「AI进度」控制台(角色形象页 / 漫剧工作台共用)。 */
|
|
14
|
-
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void;
|
|
15
|
-
}): import("react").JSX.Element;
|