@waterwx/dsh-novel-forge 1.7.0 → 1.7.2
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/lib/client.js +2537 -1858
- package/lib/client.js.map +1 -1
- package/lib/index.js +657 -45
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +2 -0
- package/lib/types/client/locales.d.ts +0 -1
- package/lib/types/client/panel/FlowGuide.d.ts +21 -0
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +18 -0
- package/lib/types/client/panel/MangaWorkspace.d.ts +2 -2
- package/lib/types/client/panel/StoryboardTab.d.ts +5 -1
- package/lib/types/engine.d.ts +15 -8
- package/lib/types/protocol.d.ts +124 -0
- package/package.json +2 -1
- package/src/client/api.ts +5 -0
- package/src/client/locales.ts +0 -1
- package/src/client/panel/AssetsTab.tsx +22 -22
- package/src/client/panel/AssistantTab.tsx +3 -3
- package/src/client/panel/BookshelfBar.tsx +1 -1
- package/src/client/panel/CreateBookView.tsx +10 -10
- package/src/client/panel/FlowGuide.tsx +83 -0
- package/src/client/panel/MangaRoleLibrary.tsx +810 -0
- package/src/client/panel/MangaWorkspace.tsx +359 -297
- package/src/client/panel/NovelPanel.tsx +107 -107
- package/src/client/panel/ReaderView.tsx +4 -4
- package/src/client/panel/RunPanel.tsx +8 -8
- package/src/client/panel/SceneLibrary.tsx +68 -67
- package/src/client/panel/StoryboardTab.tsx +105 -41
- package/src/client/panel/StyleCard.tsx +13 -13
- package/src/client/panel/StyleGallery.tsx +2 -2
- package/src/client/panel/WorldTab.tsx +5 -5
- package/src/client/panel/panel.module.css +861 -510
- package/src/client/panel/views.tsx +24 -24
- package/src/engine.ts +482 -51
- package/src/protocol.ts +125 -0
- package/src/routes.ts +203 -0
- package/src/client/panel/RoleVisualPanel.tsx +0 -485
package/src/protocol.ts
CHANGED
|
@@ -44,6 +44,8 @@ export const NOVEL_API = {
|
|
|
44
44
|
bookshelfImportTextPreview: '/api/dsh-novel-forge/bookshelf/import-text/preview',
|
|
45
45
|
/** 漫剧方案管理:create/remove/activate。 */
|
|
46
46
|
manhuaPlans: '/api/dsh-novel-forge/manhua/plans',
|
|
47
|
+
/** 漫剧角色库:从分镜提名(规则+LLM两段式)/ 建卡 / 更新 / 删除 / 形象锚点 / 精修提示词。 */
|
|
48
|
+
mangaRoles: '/api/dsh-novel-forge/manga/roles',
|
|
47
49
|
/** 分镜·编剧级:单章 → 剧情骨架(节拍链)。 */
|
|
48
50
|
storyboardSkeleton: '/api/dsh-novel-forge/storyboard/skeleton',
|
|
49
51
|
/** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
|
|
@@ -225,6 +227,8 @@ export interface StoryboardSkeleton {
|
|
|
225
227
|
arc: string
|
|
226
228
|
/** 剧情节拍链(时间顺序,因果连贯)。 */
|
|
227
229
|
beats: StoryboardBeat[]
|
|
230
|
+
/** 本章全部出镜角色(正文中的确切称谓,去重;漫剧角色库提名地基)。 */
|
|
231
|
+
characters?: string[]
|
|
228
232
|
}
|
|
229
233
|
|
|
230
234
|
/** POST /storyboard/skeleton 请求:生成单章剧情骨架。 */
|
|
@@ -261,6 +265,10 @@ export interface StoryboardShot {
|
|
|
261
265
|
prevState: string
|
|
262
266
|
/** 本镜头结束状态。 */
|
|
263
267
|
nextState: string
|
|
268
|
+
/** 本镜头出镜角色(引用小说库规范名/漫剧卡名,1-4 个;漫剧角色库提名地基)。 */
|
|
269
|
+
characters?: string[]
|
|
270
|
+
/** 本镜头已绑定漫剧卡的 id(定妆图引用,供出图/生视频时按卡取参考图)。 */
|
|
271
|
+
mangaRoleIds?: string[]
|
|
264
272
|
}
|
|
265
273
|
|
|
266
274
|
/** 分镜·导演级:单章分镜表。 */
|
|
@@ -269,6 +277,10 @@ export interface StoryboardTable {
|
|
|
269
277
|
shots: StoryboardShot[]
|
|
270
278
|
/** 生成时注入的场景卡名称(前端标注消费关系)。 */
|
|
271
279
|
usedScenes?: string[]
|
|
280
|
+
/** 本章全部出镜角色(各镜头 characters 去重汇总;骨架有则兜底)。 */
|
|
281
|
+
characters?: string[]
|
|
282
|
+
/** 本章已绑定漫剧卡的 id(去重汇总,定妆图引用)。 */
|
|
283
|
+
mangaRoleIds?: string[]
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
/** POST /storyboard/table 请求:骨架 → 分镜表。 */
|
|
@@ -293,6 +305,8 @@ export interface StoryboardPrompt {
|
|
|
293
305
|
shotId: string
|
|
294
306
|
/** 即梦/视频模型可粘贴的中文提示词(画面+运镜+光效+风格词块)。 */
|
|
295
307
|
text: string
|
|
308
|
+
/** 本镜头绑定的漫剧卡 id(定妆图引用:出图/生视频时按卡取 imageUrl/立绘)。 */
|
|
309
|
+
mangaRoleIds?: string[]
|
|
296
310
|
}
|
|
297
311
|
|
|
298
312
|
/** 分镜持久化:单章的分镜产出(骨架 + 分镜表 + 视频提示词,可分别存在)。 */
|
|
@@ -354,6 +368,113 @@ export interface MangaPlansResponse {
|
|
|
354
368
|
plans: MangaPlan[]
|
|
355
369
|
}
|
|
356
370
|
|
|
371
|
+
/** 漫剧角色卡:制作角色(要建模/出图/锁一致性的「上镜角色」),与小说角色库弱关联。 */
|
|
372
|
+
export interface MangaRoleCard {
|
|
373
|
+
/** 稳定 id。 */
|
|
374
|
+
id: string
|
|
375
|
+
/** 来源小说角色名(只读追溯,非强外键;禁止反向写回小说库)。 */
|
|
376
|
+
sourceRoleName?: string
|
|
377
|
+
/** 漫剧用名(可短剧化改名)。 */
|
|
378
|
+
name: string
|
|
379
|
+
/** 身份一句话。 */
|
|
380
|
+
identity: string
|
|
381
|
+
/** 核心功能:主角/导师/感情线/反派/搭档/线人/功能性。 */
|
|
382
|
+
coreFunction: 'protagonist' | 'mentor' | 'love_interest' | 'antagonist' | 'sidekick' | 'informant' | 'functional'
|
|
383
|
+
/** 与主角的关系。 */
|
|
384
|
+
protagonistRelation: 'enemy' | 'friend' | 'mentor' | 'lover' | 'exploit' | 'neutral'
|
|
385
|
+
/** 口头禅/说话方式(配音一致性)。 */
|
|
386
|
+
speechStyle: string
|
|
387
|
+
/** ≤3 个极致性格标签。 */
|
|
388
|
+
traits: string[]
|
|
389
|
+
/** 1-2 个辨识度外貌点。 */
|
|
390
|
+
appearance: string
|
|
391
|
+
/** 3-5 个关键剧情节点。 */
|
|
392
|
+
keyScenes: string[]
|
|
393
|
+
/** 上场集数(分镜提名导入时记录)。 */
|
|
394
|
+
appearsInEpisodes: number[]
|
|
395
|
+
/** 待匹配 → 待确认 → 已导入 → 已定妆 → 已归档。 */
|
|
396
|
+
status: 'pending_match' | 'pending_confirm' | 'imported' | 'anchored' | 'archived'
|
|
397
|
+
/** 形象资产(沿用小说角色库同款结构)。 */
|
|
398
|
+
imagePrompt?: RoleRecord['imagePrompt']
|
|
399
|
+
/** 情绪表情清单(锚点提炼时产出)。 */
|
|
400
|
+
expressions?: string[]
|
|
401
|
+
/** 四类精修生图提示词(立绘/四视图/表情/细节)。 */
|
|
402
|
+
promptKit?: RoleRecord['promptKit']
|
|
403
|
+
/** 角色参考图。 */
|
|
404
|
+
imageUrl?: string
|
|
405
|
+
/** 图集(图像锚点)。 */
|
|
406
|
+
gallery?: RoleImage[]
|
|
407
|
+
/** 生成锚点/精修时的方案风格 id(旧风格检测用)。 */
|
|
408
|
+
promptStyleId?: string
|
|
409
|
+
createdAt: string
|
|
410
|
+
updatedAt: string
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** 分镜提名 → 小说角色库匹配的一条候选。 */
|
|
414
|
+
export interface MangaRoleCandidate {
|
|
415
|
+
/** 分镜里的原始称谓(正文确切用法)。 */
|
|
416
|
+
rawName: string
|
|
417
|
+
/** matched=命中小说角色;ambiguous=多个候选待定;not_in_library=小说库没有;already_imported=已建漫剧卡(跳过)。 */
|
|
418
|
+
verdict: 'matched' | 'ambiguous' | 'not_in_library' | 'already_imported'
|
|
419
|
+
/** 规则阶段候选短名单(≤5,精确名优先 + 身份/简称匹配)。 */
|
|
420
|
+
matches: Array<{ roleName: string; reason: string }>
|
|
421
|
+
/** LLM/规则确认的命中角色名(ambiguous 时可能为空)。 */
|
|
422
|
+
matchedRoleName?: string
|
|
423
|
+
/** not_in_library 时给出建议:backfill=正文有该称谓但小说库漏提炼(回小说库补);manga_new=漫剧新增角色(直接建卡)。 */
|
|
424
|
+
novelHint?: 'backfill' | 'manga_new'
|
|
425
|
+
/** 采纳时预填的漫剧卡建议。 */
|
|
426
|
+
suggested: {
|
|
427
|
+
name: string
|
|
428
|
+
identity: string
|
|
429
|
+
coreFunction: MangaRoleCard['coreFunction']
|
|
430
|
+
protagonistRelation: MangaRoleCard['protagonistRelation']
|
|
431
|
+
speechStyle: string
|
|
432
|
+
traits: string[]
|
|
433
|
+
appearance: string
|
|
434
|
+
keyScenes: string[]
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** POST /manga/roles 请求:漫剧角色库操作。 */
|
|
439
|
+
export interface MangaRolesRequest {
|
|
440
|
+
op: 'nominate' | 'adopt' | 'update' | 'remove' | 'visual' | 'promptKit' | 'image' | 'removeImage' | 'imageGenerate' | 'mode'
|
|
441
|
+
/** op='mode':短剧精简模式开关。 */
|
|
442
|
+
shortDramaMode?: boolean
|
|
443
|
+
/** op='nominate':从哪章分镜提名角色。 */
|
|
444
|
+
chapterNo?: number
|
|
445
|
+
/** op='adopt' / op='update':漫剧角色卡。 */
|
|
446
|
+
card?: MangaRoleCard
|
|
447
|
+
/** op='remove' / 'visual' / 'promptKit' / 'image' / 'removeImage' / 'imageGenerate':漫剧卡 id。 */
|
|
448
|
+
id?: string
|
|
449
|
+
/** op='visual' / 'promptKit':漫剧方案基底风格 id。 */
|
|
450
|
+
styleId?: string
|
|
451
|
+
/** 可选滤镜风格 id。 */
|
|
452
|
+
filterId?: string
|
|
453
|
+
/** op='image':定妆图 dataURL。 */
|
|
454
|
+
dataUrl?: string
|
|
455
|
+
/** op='image':图集用途标签(立绘/四视图/表情-x/场景/细节);缺省视为角色参考图(imageUrl)。 */
|
|
456
|
+
label?: string
|
|
457
|
+
/** op='imageGenerate':漫画风格预设 id(可选)。 */
|
|
458
|
+
style?: string
|
|
459
|
+
/** op='imageGenerate':生图模型库条目 id(缺省用启用条目)。 */
|
|
460
|
+
modelId?: string
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** POST /manga/roles 响应。 */
|
|
464
|
+
export interface MangaRolesResponse {
|
|
465
|
+
cards: MangaRoleCard[]
|
|
466
|
+
/** op='nominate' 时的 AI 提名候选。 */
|
|
467
|
+
candidates?: MangaRoleCandidate[]
|
|
468
|
+
/** op='visual' 时的形象锚点(已写入漫剧卡)。 */
|
|
469
|
+
visual?: RoleRecord['imagePrompt']
|
|
470
|
+
/** op='promptKit' 时的四类精修提示词包(已写入漫剧卡)。 */
|
|
471
|
+
promptKit?: RoleRecord['promptKit']
|
|
472
|
+
/** op='image' / 'imageGenerate' 时的定妆图 dataURL(已写入漫剧卡)。 */
|
|
473
|
+
imageUrl?: string
|
|
474
|
+
/** op='mode' 时的短剧精简模式状态。 */
|
|
475
|
+
shortDramaMode?: boolean
|
|
476
|
+
}
|
|
477
|
+
|
|
357
478
|
|
|
358
479
|
/** Chapter lifecycle states (the writing pipeline's state machine). */
|
|
359
480
|
export type ChapterStatus =
|
|
@@ -1045,6 +1166,10 @@ export interface ProjectState {
|
|
|
1045
1166
|
plotlines?: Plotline[]
|
|
1046
1167
|
/** 角色库(作者维护 + AI 提炼的主表)。 */
|
|
1047
1168
|
roles?: RoleRecord[]
|
|
1169
|
+
/** 漫剧角色库(制作角色投影:从分镜提名导入,独立于小说角色库)。 */
|
|
1170
|
+
mangaRoles?: MangaRoleCard[]
|
|
1171
|
+
/** 短剧精简模式:漫剧角色库按 5-8 上镜角色 / 功能标签 / 关系闭环 / 人设极致化约束。 */
|
|
1172
|
+
shortDramaMode?: boolean
|
|
1048
1173
|
/** 场景库(作者维护 + AI 提炼的主表)。 */
|
|
1049
1174
|
scenes?: SceneCard[]
|
|
1050
1175
|
/** 视觉世界观规则(生图/生视频必须遵守,从道藏提炼,注入所有提示词)。 */
|
package/src/routes.ts
CHANGED
|
@@ -60,6 +60,9 @@ import {
|
|
|
60
60
|
type MangaPlan,
|
|
61
61
|
type MangaPlansRequest,
|
|
62
62
|
type MangaPlansResponse,
|
|
63
|
+
type MangaRoleCard,
|
|
64
|
+
type MangaRolesRequest,
|
|
65
|
+
type MangaRolesResponse,
|
|
63
66
|
type LoadOutlineResponse,
|
|
64
67
|
type NovelConfig,
|
|
65
68
|
type PlotlinesRequest,
|
|
@@ -142,12 +145,16 @@ import {
|
|
|
142
145
|
extractRoles,
|
|
143
146
|
extractScenes,
|
|
144
147
|
generateRolePromptKit,
|
|
148
|
+
generateMangaRolePromptKit,
|
|
145
149
|
extractVisualRules,
|
|
146
150
|
extractRoleVisual,
|
|
151
|
+
extractMangaRoleVisual,
|
|
152
|
+
nominateMangaRoles,
|
|
147
153
|
suggestOutlines,
|
|
148
154
|
reverseOutlineFromChapters,
|
|
149
155
|
breakdownBook,
|
|
150
156
|
generateRoleReferenceImage,
|
|
157
|
+
generateMangaRoleReferenceImage,
|
|
151
158
|
testImageEndpoint,
|
|
152
159
|
generateStoryboardSkeleton,
|
|
153
160
|
generateStoryboardTable,
|
|
@@ -1919,6 +1926,201 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1919
1926
|
},
|
|
1920
1927
|
}
|
|
1921
1928
|
|
|
1929
|
+
// ------------------------------------------------------- manga roles
|
|
1930
|
+
/** 漫剧角色库:从分镜提名(规则+LLM 两段式)/ 建卡 / 更新 / 删除 / 形象锚点 / 精修提示词。 */
|
|
1931
|
+
const mangaRolesRoute: WebRoute = {
|
|
1932
|
+
kind: 'exact',
|
|
1933
|
+
path: NOVEL_API.mangaRoles,
|
|
1934
|
+
handler: async (req, res) => {
|
|
1935
|
+
if (!guard(req, res, 'POST')) return
|
|
1936
|
+
const config = getConfig()
|
|
1937
|
+
const project = requireProject(res)
|
|
1938
|
+
if (project === undefined) return
|
|
1939
|
+
const body = await readJsonBody<MangaRolesRequest>(req)
|
|
1940
|
+
if (project.mangaRoles === undefined) project.mangaRoles = []
|
|
1941
|
+
const op = body?.op
|
|
1942
|
+
if (op === 'nominate') {
|
|
1943
|
+
const chapterNo = body?.chapterNo
|
|
1944
|
+
if (typeof chapterNo !== 'number' || chapterNo <= 0) {
|
|
1945
|
+
writeJson(res, 400, { error: 'chapterNo(章号)必填' })
|
|
1946
|
+
return
|
|
1947
|
+
}
|
|
1948
|
+
try {
|
|
1949
|
+
const candidates = await nominateMangaRoles(ctx, config, project, config.outputDir, chapterNo)
|
|
1950
|
+
writeJson(res, 200, { cards: project.mangaRoles, candidates } satisfies MangaRolesResponse)
|
|
1951
|
+
return
|
|
1952
|
+
} catch (error) {
|
|
1953
|
+
writeJson(res, 500, { error: `提名失败:${(error as Error).message}` })
|
|
1954
|
+
return
|
|
1955
|
+
}
|
|
1956
|
+
} else if (op === 'adopt' || op === 'update') {
|
|
1957
|
+
const card = body?.card
|
|
1958
|
+
if (card === undefined || typeof card !== 'object') {
|
|
1959
|
+
writeJson(res, 400, { error: 'card(漫剧角色卡)必填' })
|
|
1960
|
+
return
|
|
1961
|
+
}
|
|
1962
|
+
const name = (card.name ?? '').trim()
|
|
1963
|
+
if (name === '') {
|
|
1964
|
+
writeJson(res, 400, { error: '漫剧角色名不能为空' })
|
|
1965
|
+
return
|
|
1966
|
+
}
|
|
1967
|
+
const now = new Date().toISOString()
|
|
1968
|
+
const existing = card.id !== '' ? project.mangaRoles.find(c => c.id === card.id) : undefined
|
|
1969
|
+
const next: MangaRoleCard = {
|
|
1970
|
+
id: card.id !== '' ? card.id : 'mr-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6),
|
|
1971
|
+
sourceRoleName: card.sourceRoleName !== undefined && card.sourceRoleName !== '' ? card.sourceRoleName : undefined,
|
|
1972
|
+
name,
|
|
1973
|
+
identity: card.identity ?? '',
|
|
1974
|
+
coreFunction: card.coreFunction ?? 'functional',
|
|
1975
|
+
protagonistRelation: card.protagonistRelation ?? 'neutral',
|
|
1976
|
+
speechStyle: card.speechStyle ?? '',
|
|
1977
|
+
traits: Array.isArray(card.traits) ? card.traits.map(t => String(t).slice(0, 20)).filter(t => t !== '').slice(0, 3) : [],
|
|
1978
|
+
appearance: card.appearance ?? '',
|
|
1979
|
+
keyScenes: Array.isArray(card.keyScenes) ? card.keyScenes.map(k => String(k).slice(0, 120)).filter(k => k !== '').slice(0, 6) : [],
|
|
1980
|
+
appearsInEpisodes: Array.isArray(card.appearsInEpisodes)
|
|
1981
|
+
? [...new Set(card.appearsInEpisodes.map(n => Number(n)).filter(n => Number.isFinite(n) && n > 0))].sort((a, b) => a - b).slice(0, 200)
|
|
1982
|
+
: [],
|
|
1983
|
+
status: card.status ?? 'imported',
|
|
1984
|
+
imagePrompt: card.imagePrompt,
|
|
1985
|
+
expressions: card.expressions,
|
|
1986
|
+
promptKit: card.promptKit,
|
|
1987
|
+
imageUrl: card.imageUrl,
|
|
1988
|
+
gallery: card.gallery,
|
|
1989
|
+
promptStyleId: card.promptStyleId,
|
|
1990
|
+
createdAt: existing?.createdAt ?? now,
|
|
1991
|
+
updatedAt: now,
|
|
1992
|
+
}
|
|
1993
|
+
if (existing !== undefined) {
|
|
1994
|
+
const idx = project.mangaRoles.findIndex(c => c.id === card.id)
|
|
1995
|
+
project.mangaRoles[idx] = next
|
|
1996
|
+
} else {
|
|
1997
|
+
project.mangaRoles.push(next)
|
|
1998
|
+
}
|
|
1999
|
+
project.updatedAt = now
|
|
2000
|
+
saveProject(config.outputDir, project)
|
|
2001
|
+
writeJson(res, 200, { cards: project.mangaRoles } satisfies MangaRolesResponse)
|
|
2002
|
+
return
|
|
2003
|
+
} else if (op === 'remove') {
|
|
2004
|
+
const id = body?.id
|
|
2005
|
+
if (typeof id !== 'string' || id === '') {
|
|
2006
|
+
writeJson(res, 400, { error: 'id(漫剧卡 id)必填' })
|
|
2007
|
+
return
|
|
2008
|
+
}
|
|
2009
|
+
project.mangaRoles = project.mangaRoles.filter(c => c.id !== id)
|
|
2010
|
+
project.updatedAt = new Date().toISOString()
|
|
2011
|
+
saveProject(config.outputDir, project)
|
|
2012
|
+
writeJson(res, 200, { cards: project.mangaRoles } satisfies MangaRolesResponse)
|
|
2013
|
+
return
|
|
2014
|
+
} else if (op === 'visual') {
|
|
2015
|
+
const id = body?.id?.trim()
|
|
2016
|
+
if (id === undefined || id === '') {
|
|
2017
|
+
writeJson(res, 400, { error: 'id(漫剧卡 id)必填' })
|
|
2018
|
+
return
|
|
2019
|
+
}
|
|
2020
|
+
try {
|
|
2021
|
+
const visual = await extractMangaRoleVisual(ctx, config, project, config.outputDir, id, body?.styleId, body?.filterId)
|
|
2022
|
+
project.updatedAt = new Date().toISOString()
|
|
2023
|
+
saveProject(config.outputDir, project)
|
|
2024
|
+
writeJson(res, 200, { cards: project.mangaRoles, visual } satisfies MangaRolesResponse)
|
|
2025
|
+
return
|
|
2026
|
+
} catch (error) {
|
|
2027
|
+
writeJson(res, 500, { error: `形象锚点生成失败:${(error as Error).message}` })
|
|
2028
|
+
return
|
|
2029
|
+
}
|
|
2030
|
+
} else if (op === 'promptKit') {
|
|
2031
|
+
const id = body?.id?.trim()
|
|
2032
|
+
if (id === undefined || id === '') {
|
|
2033
|
+
writeJson(res, 400, { error: 'id(漫剧卡 id)必填' })
|
|
2034
|
+
return
|
|
2035
|
+
}
|
|
2036
|
+
try {
|
|
2037
|
+
const kit = await generateMangaRolePromptKit(ctx, config, project, id, body?.styleId, body?.filterId)
|
|
2038
|
+
project.updatedAt = new Date().toISOString()
|
|
2039
|
+
saveProject(config.outputDir, project)
|
|
2040
|
+
writeJson(res, 200, { cards: project.mangaRoles, promptKit: kit } satisfies MangaRolesResponse)
|
|
2041
|
+
return
|
|
2042
|
+
} catch (error) {
|
|
2043
|
+
writeJson(res, 500, { error: `提示词精修失败:${(error as Error).message}` })
|
|
2044
|
+
return
|
|
2045
|
+
}
|
|
2046
|
+
} else if (op === 'image') {
|
|
2047
|
+
// 定妆图上传:带标签进图集;标签含「立绘」时同步为参考图(imageUrl)。
|
|
2048
|
+
const id = body?.id?.trim()
|
|
2049
|
+
const dataUrl = body?.dataUrl?.trim()
|
|
2050
|
+
if (id === undefined || id === '') {
|
|
2051
|
+
writeJson(res, 400, { error: 'id(漫剧卡 id)必填' })
|
|
2052
|
+
return
|
|
2053
|
+
}
|
|
2054
|
+
if (dataUrl === undefined || dataUrl === '') {
|
|
2055
|
+
writeJson(res, 400, { error: 'dataUrl(定妆图)必填' })
|
|
2056
|
+
return
|
|
2057
|
+
}
|
|
2058
|
+
const card = project.mangaRoles.find(c => c.id === id)
|
|
2059
|
+
if (card === undefined) {
|
|
2060
|
+
writeJson(res, 404, { error: '漫剧角色卡不存在' })
|
|
2061
|
+
return
|
|
2062
|
+
}
|
|
2063
|
+
const label = body?.label?.trim() ?? ''
|
|
2064
|
+
if (label !== '') {
|
|
2065
|
+
card.gallery ??= []
|
|
2066
|
+
card.gallery = card.gallery.filter(g => g.label !== label)
|
|
2067
|
+
card.gallery.push({ label, dataUrl })
|
|
2068
|
+
if (label.includes('立绘')) card.imageUrl = dataUrl
|
|
2069
|
+
} else {
|
|
2070
|
+
card.imageUrl = dataUrl
|
|
2071
|
+
}
|
|
2072
|
+
card.updatedAt = new Date().toISOString()
|
|
2073
|
+
project.updatedAt = new Date().toISOString()
|
|
2074
|
+
saveProject(config.outputDir, project)
|
|
2075
|
+
writeJson(res, 200, { cards: project.mangaRoles, imageUrl: card.imageUrl } satisfies MangaRolesResponse)
|
|
2076
|
+
return
|
|
2077
|
+
} else if (op === 'removeImage') {
|
|
2078
|
+
const id = body?.id?.trim()
|
|
2079
|
+
const label = body?.label?.trim() ?? ''
|
|
2080
|
+
const card = project.mangaRoles.find(c => c.id === id)
|
|
2081
|
+
if (card !== undefined) {
|
|
2082
|
+
if (label !== '') {
|
|
2083
|
+
card.gallery = (card.gallery ?? []).filter(g => g.label !== label)
|
|
2084
|
+
if (label.includes('立绘')) card.imageUrl = undefined
|
|
2085
|
+
} else {
|
|
2086
|
+
card.imageUrl = undefined
|
|
2087
|
+
}
|
|
2088
|
+
card.updatedAt = new Date().toISOString()
|
|
2089
|
+
}
|
|
2090
|
+
project.updatedAt = new Date().toISOString()
|
|
2091
|
+
saveProject(config.outputDir, project)
|
|
2092
|
+
writeJson(res, 200, { cards: project.mangaRoles } satisfies MangaRolesResponse)
|
|
2093
|
+
return
|
|
2094
|
+
} else if (op === 'imageGenerate') {
|
|
2095
|
+
const id = body?.id?.trim()
|
|
2096
|
+
if (id === undefined || id === '') {
|
|
2097
|
+
writeJson(res, 400, { error: 'id(漫剧卡 id)必填' })
|
|
2098
|
+
return
|
|
2099
|
+
}
|
|
2100
|
+
try {
|
|
2101
|
+
const imageUrl = await generateMangaRoleReferenceImage(ctx, config, project, config.outputDir, id, body?.style ?? '', body?.modelId)
|
|
2102
|
+
project.updatedAt = new Date().toISOString()
|
|
2103
|
+
saveProject(config.outputDir, project)
|
|
2104
|
+
writeJson(res, 200, { cards: project.mangaRoles, imageUrl } satisfies MangaRolesResponse)
|
|
2105
|
+
return
|
|
2106
|
+
} catch (error) {
|
|
2107
|
+
writeJson(res, 500, { error: `定妆图生成失败:${(error as Error).message}` })
|
|
2108
|
+
return
|
|
2109
|
+
}
|
|
2110
|
+
} else if (op === 'mode') {
|
|
2111
|
+
// 短剧精简模式开关(漫剧角色库 5-8 人 / 功能标签 / 关系闭环 / 人设极致化)。
|
|
2112
|
+
project.shortDramaMode = body?.shortDramaMode === true
|
|
2113
|
+
project.updatedAt = new Date().toISOString()
|
|
2114
|
+
saveProject(config.outputDir, project)
|
|
2115
|
+
writeJson(res, 200, { cards: project.mangaRoles, shortDramaMode: project.shortDramaMode } satisfies MangaRolesResponse)
|
|
2116
|
+
return
|
|
2117
|
+
} else {
|
|
2118
|
+
writeJson(res, 400, { error: '未知的 manga/roles op' })
|
|
2119
|
+
return
|
|
2120
|
+
}
|
|
2121
|
+
},
|
|
2122
|
+
}
|
|
2123
|
+
|
|
1922
2124
|
// ------------------------------------------------------- chapter reset
|
|
1923
2125
|
/** 章节复位:generating 卡死 → pending(可重新生成)。 */
|
|
1924
2126
|
const chapterResetRoute: WebRoute = {
|
|
@@ -2587,6 +2789,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2587
2789
|
renameRoute,
|
|
2588
2790
|
plotlinesRoute,
|
|
2589
2791
|
rolesRoute,
|
|
2792
|
+
mangaRolesRoute,
|
|
2590
2793
|
scenesRoute,
|
|
2591
2794
|
visualRulesRoute,
|
|
2592
2795
|
sensitiveRoute,
|