@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.
Files changed (36) hide show
  1. package/lib/client.js +2537 -1858
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +657 -45
  4. package/lib/index.js.map +1 -1
  5. package/lib/types/client/api.d.ts +2 -0
  6. package/lib/types/client/locales.d.ts +0 -1
  7. package/lib/types/client/panel/FlowGuide.d.ts +21 -0
  8. package/lib/types/client/panel/MangaRoleLibrary.d.ts +18 -0
  9. package/lib/types/client/panel/MangaWorkspace.d.ts +2 -2
  10. package/lib/types/client/panel/StoryboardTab.d.ts +5 -1
  11. package/lib/types/engine.d.ts +15 -8
  12. package/lib/types/protocol.d.ts +124 -0
  13. package/package.json +2 -1
  14. package/src/client/api.ts +5 -0
  15. package/src/client/locales.ts +0 -1
  16. package/src/client/panel/AssetsTab.tsx +22 -22
  17. package/src/client/panel/AssistantTab.tsx +3 -3
  18. package/src/client/panel/BookshelfBar.tsx +1 -1
  19. package/src/client/panel/CreateBookView.tsx +10 -10
  20. package/src/client/panel/FlowGuide.tsx +83 -0
  21. package/src/client/panel/MangaRoleLibrary.tsx +810 -0
  22. package/src/client/panel/MangaWorkspace.tsx +359 -297
  23. package/src/client/panel/NovelPanel.tsx +107 -107
  24. package/src/client/panel/ReaderView.tsx +4 -4
  25. package/src/client/panel/RunPanel.tsx +8 -8
  26. package/src/client/panel/SceneLibrary.tsx +68 -67
  27. package/src/client/panel/StoryboardTab.tsx +105 -41
  28. package/src/client/panel/StyleCard.tsx +13 -13
  29. package/src/client/panel/StyleGallery.tsx +2 -2
  30. package/src/client/panel/WorldTab.tsx +5 -5
  31. package/src/client/panel/panel.module.css +861 -510
  32. package/src/client/panel/views.tsx +24 -24
  33. package/src/engine.ts +482 -51
  34. package/src/protocol.ts +125 -0
  35. package/src/routes.ts +203 -0
  36. 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,