@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/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: string
217
- /** 叙事功能:铺垫/冲突/转折/高潮/收束/伏笔/人物塑造。 */
218
- function: string
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: string
252
- /** 机位与运镜(如 低机位仰拍 + 缓慢推近)。 */
253
- camera: string
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: string
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 { exec } from 'node:child_process'
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
- exec(`explorer "${dir.replace(/"/g, '')}"`, (error) => {
2300
- if (error) {
2301
- writeJson(res, 500, { ok: false, error: error.message })
2302
- } else {
2303
- writeJson(res, 200, { ok: true })
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(config.outputDir, { recursive: true })
311
- const targetPath = join(config.outputDir, fileName)
311
+ mkdirSync(outputDir, { recursive: true })
312
+ const targetPath = join(outputDir, fileName)
312
313
  if (existsSync(targetPath)) {
313
- copyFileSync(targetPath, join(config.outputDir, `${fileName.replace(/\.md$/, '')}.bak.md`))
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(config.outputDir, project)
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;