@waterwx/dsh-novel-forge 1.2.0 → 1.3.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 +5 -0
- package/lib/client.js +2100 -651
- package/lib/client.js.map +1 -1
- package/lib/index.js +567 -173
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +4 -4
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/comic-presets.d.ts +12 -0
- package/lib/types/engine.d.ts +16 -13
- package/lib/types/index.d.ts +6 -0
- package/lib/types/protocol.d.ts +128 -89
- package/package.json +1 -1
- package/src/client/api.ts +10 -10
- package/src/client/panel/NovelPanel.tsx +650 -172
- package/src/comic-presets.ts +67 -0
- package/src/engine.ts +344 -188
- package/src/index.ts +15 -0
- package/src/protocol.ts +121 -96
- package/src/routes.ts +173 -52
package/src/index.ts
CHANGED
|
@@ -58,6 +58,12 @@ export interface Config {
|
|
|
58
58
|
autoAuthorReview?: boolean
|
|
59
59
|
/** 修订/润色产出草稿后自动附带一次 AI 审查(默认开,可在设置页关闭省 token)。 */
|
|
60
60
|
autoReviewAfterRevise?: boolean
|
|
61
|
+
/** 豆包/Seedream 生图 API Key。 */
|
|
62
|
+
imageApiKey?: string
|
|
63
|
+
/** 豆包/Seedream 生图模型 ID。 */
|
|
64
|
+
imageApiModel?: string
|
|
65
|
+
/** 是否启用豆包生图(默认关)。 */
|
|
66
|
+
imageApiEnabled?: boolean
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export const Config: z<Config> = z.object({
|
|
@@ -74,6 +80,9 @@ export const Config: z<Config> = z.object({
|
|
|
74
80
|
autoReview: z.boolean().default(true),
|
|
75
81
|
autoAuthorReview: z.boolean().default(true),
|
|
76
82
|
autoReviewAfterRevise: z.boolean().default(true),
|
|
83
|
+
imageApiKey: z.string().default(''),
|
|
84
|
+
imageApiModel: z.string().default(''),
|
|
85
|
+
imageApiEnabled: z.boolean().default(false),
|
|
77
86
|
})
|
|
78
87
|
|
|
79
88
|
/** Schema defaults, re-read for hand-built test contexts. */
|
|
@@ -110,6 +119,9 @@ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
|
|
|
110
119
|
autoReview: value?.autoReview ?? DEFAULT_AUTO_REVIEW,
|
|
111
120
|
autoAuthorReview: value?.autoAuthorReview ?? DEFAULT_AUTO_AUTHOR_REVIEW,
|
|
112
121
|
autoReviewAfterRevise: value?.autoReviewAfterRevise ?? DEFAULT_AUTO_REVIEW_AFTER_REVISE,
|
|
122
|
+
imageApiKey: value?.imageApiKey,
|
|
123
|
+
imageApiModel: value?.imageApiModel,
|
|
124
|
+
imageApiEnabled: value?.imageApiEnabled ?? false,
|
|
113
125
|
}
|
|
114
126
|
}
|
|
115
127
|
|
|
@@ -145,6 +157,9 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
145
157
|
if (patch.autoReview !== undefined) next.autoReview = patch.autoReview
|
|
146
158
|
if (patch.autoAuthorReview !== undefined) next.autoAuthorReview = patch.autoAuthorReview
|
|
147
159
|
if (patch.autoReviewAfterRevise !== undefined) next.autoReviewAfterRevise = patch.autoReviewAfterRevise
|
|
160
|
+
if (patch.imageApiKey !== undefined) next.imageApiKey = patch.imageApiKey
|
|
161
|
+
if (patch.imageApiModel !== undefined) next.imageApiModel = patch.imageApiModel
|
|
162
|
+
if (patch.imageApiEnabled !== undefined) next.imageApiEnabled = patch.imageApiEnabled
|
|
148
163
|
// Persist through the settings seam when available; otherwise keep in memory.
|
|
149
164
|
// (ctx.get is the non-strict service access — no inject requirement, same
|
|
150
165
|
// pattern installSettingsSection itself uses.)
|
package/src/protocol.ts
CHANGED
|
@@ -58,6 +58,8 @@ export const NOVEL_API = {
|
|
|
58
58
|
plotlines: '/api/dsh-novel-forge/plotlines',
|
|
59
59
|
/** 角色库:AI 提炼 / 采纳 / 更新 / 删除。 */
|
|
60
60
|
roles: '/api/dsh-novel-forge/roles',
|
|
61
|
+
scenes: '/api/dsh-novel-forge/scenes',
|
|
62
|
+
visualRules: '/api/dsh-novel-forge/visual-rules',
|
|
61
63
|
/** 作者复盘补跑:对已写章节补齐 authorReview(全书流式 / 单章 JSON)。 */
|
|
62
64
|
reviewBackfill: '/api/dsh-novel-forge/review/backfill',
|
|
63
65
|
/** 章节复位:generating 卡死 → pending(可重新生成)。 */
|
|
@@ -71,9 +73,8 @@ export const NOVEL_API = {
|
|
|
71
73
|
/** 拆书分析:对已写章节做结构/人物/文风/卖点四维体检(两阶段:源笔记→分节分析)。 */
|
|
72
74
|
breakdown: '/api/dsh-novel-forge/breakdown',
|
|
73
75
|
/** 漫剧分镜生成:章节 → 角色锚点 + 分镜表(可适配豆包/Seedance/SD)。 */
|
|
74
|
-
storyboard: '/api/dsh-novel-forge/storyboard',
|
|
75
76
|
/** 漫剧分集计划:读一卷 → 按故事弧线分集(高潮拆集/过渡并章)。 */
|
|
76
|
-
|
|
77
|
+
/** 漫画脚本:章节 → 分页分格漫画脚本(含角色视觉锚点)。 */
|
|
77
78
|
/** 生产单:启动批量生产(计划补足 + 逐章生成 + 被拒分级处理)。 */
|
|
78
79
|
runStart: '/api/dsh-novel-forge/run/start',
|
|
79
80
|
/** 生产单控制:pause / resume / stop。 */
|
|
@@ -494,98 +495,6 @@ export interface BreakdownResponse {
|
|
|
494
495
|
usedTokens: number
|
|
495
496
|
}
|
|
496
497
|
|
|
497
|
-
/** 漫剧分镜:一个角色锚点卡。 */
|
|
498
|
-
export interface StoryboardCharacterCard {
|
|
499
|
-
name: string
|
|
500
|
-
/** 视觉锚点:发型/五官/体型/标志物(中文描述)。 */
|
|
501
|
-
visualAnchor: string
|
|
502
|
-
/** AI 标签组(英文,全分镜强制复用)。 */
|
|
503
|
-
tags: string
|
|
504
|
-
/** 情绪表情库(英文,按需调用)。 */
|
|
505
|
-
expressions: string[]
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/** 漫剧分镜:一个分镜格。 */
|
|
509
|
-
export interface StoryboardPanel {
|
|
510
|
-
/** 序号。 */
|
|
511
|
-
index: number
|
|
512
|
-
/** 时间码(如 0:00-0:03)。 */
|
|
513
|
-
timecode: string
|
|
514
|
-
/** 景别(特写/中景/全景…)。 */
|
|
515
|
-
shot: string
|
|
516
|
-
/** 画面描述(具象动作,非抽象情感)。 */
|
|
517
|
-
visual: string
|
|
518
|
-
/** 台词(情绪标签 + 内容)。 */
|
|
519
|
-
dialogue: string
|
|
520
|
-
/** 转场/动效。 */
|
|
521
|
-
transition: string
|
|
522
|
-
/** AI 提示词(英文标签,豆包/SD 通用)。 */
|
|
523
|
-
prompt: string
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/** POST /storyboard 请求:章节 → 漫剧分镜。 */
|
|
527
|
-
export interface StoryboardRequest {
|
|
528
|
-
chapterNo: number
|
|
529
|
-
/** 赛道类型:爽文/甜宠/悬疑/搞笑(留空由 AI 推断)。 */
|
|
530
|
-
genre?: string
|
|
531
|
-
/** 目标平台(抖音/快手/B站…,默认抖音)。 */
|
|
532
|
-
platform?: string
|
|
533
|
-
/** AI 工具偏好:doubao / seedance / sd / mj(影响提示词格式,默认 doubao)。 */
|
|
534
|
-
tool?: string
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/** POST /storyboard 响应。 */
|
|
538
|
-
export interface StoryboardResponse {
|
|
539
|
-
/** 本集标题。 */
|
|
540
|
-
title: string
|
|
541
|
-
/** 赛道与节奏说明。 */
|
|
542
|
-
pacingNote: string
|
|
543
|
-
/** 本集钩子(结尾悬念)。 */
|
|
544
|
-
hook: string
|
|
545
|
-
/** 角色锚点卡。 */
|
|
546
|
-
characters: StoryboardCharacterCard[]
|
|
547
|
-
/** 分镜表。 */
|
|
548
|
-
panels: StoryboardPanel[]
|
|
549
|
-
/** 结尾钩子台词。 */
|
|
550
|
-
endingHook: string
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/** 漫剧分集计划:一集(含 1-N 章)。 */
|
|
554
|
-
export interface StoryboardEpisode {
|
|
555
|
-
/** 集号。 */
|
|
556
|
-
index: number
|
|
557
|
-
/** 集标题(冲突+悬念句式)。 */
|
|
558
|
-
title: string
|
|
559
|
-
/** 涵盖章节号。 */
|
|
560
|
-
chapters: number[]
|
|
561
|
-
/** 叙事任务:这集讲什么、为什么这么分。 */
|
|
562
|
-
narrativeJob: string
|
|
563
|
-
/** 开头钩子。 */
|
|
564
|
-
openingHook: string
|
|
565
|
-
/** 结尾钩子(逼看下集)。 */
|
|
566
|
-
endingHook: string
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/** POST /storyboard/plan 请求:读一卷 → 输出漫剧分集计划。 */
|
|
570
|
-
export interface StoryboardPlanRequest {
|
|
571
|
-
/** 卷号(1-5)。 */
|
|
572
|
-
volumeNo: number
|
|
573
|
-
/** 目标平台(默认抖音)。 */
|
|
574
|
-
platform?: string
|
|
575
|
-
/** 每集约 1-2 分钟,最多多少集(默认 25)。 */
|
|
576
|
-
maxEpisodes?: number
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
/** POST /storyboard/plan 响应。 */
|
|
580
|
-
export interface StoryboardPlanResponse {
|
|
581
|
-
/** 本卷漫剧化的叙事策略说明。 */
|
|
582
|
-
strategy: string
|
|
583
|
-
/** 分集计划。 */
|
|
584
|
-
episodes: StoryboardEpisode[]
|
|
585
|
-
/** 参与规划的章节数。 */
|
|
586
|
-
chaptersScanned: number
|
|
587
|
-
}
|
|
588
|
-
|
|
589
498
|
/** 角色卡:角色当前状态(从事实库聚合)。 */
|
|
590
499
|
export interface RoleStatusCard {
|
|
591
500
|
name: string
|
|
@@ -630,15 +539,111 @@ export interface RoleRecord {
|
|
|
630
539
|
/** 依据来源说明(哪几章哪些描写)。 */
|
|
631
540
|
source?: string
|
|
632
541
|
}
|
|
542
|
+
/** 角色参考图(dataURL 或 URL)。用于漫画/漫剧出图时锁定角色一致性。 */
|
|
543
|
+
imageUrl?: string
|
|
544
|
+
/** 角色图集(图像锚点):立绘/四视图/表情/场景/细节等,label 标注用途。 */
|
|
545
|
+
gallery?: RoleImage[]
|
|
546
|
+
/** 该角色所需的情绪表情清单(提炼时 LLM 补全 6-12 个,如"疲惫/麻木/压抑悲伤")。 */
|
|
547
|
+
expressions?: string[]
|
|
548
|
+
/** LLM 精修的四类生图提示词包(前端拼装版之外的高质量版,promptKit 接口生成)。 */
|
|
549
|
+
promptKit?: {
|
|
550
|
+
/** 立绘:全身/半身正视图。 */
|
|
551
|
+
portrait: { zh: string; en: string }
|
|
552
|
+
/** 四视图:正面/侧面/背面设定表。 */
|
|
553
|
+
sheet: { zh: string; en: string }
|
|
554
|
+
/** 表情:同一脸部锚换情绪,每表情一段。 */
|
|
555
|
+
expressions: { name: string; zh: string; en: string }[]
|
|
556
|
+
/** 细节:标志物局部特写。 */
|
|
557
|
+
details: { zh: string; en: string }
|
|
558
|
+
}
|
|
559
|
+
/** 漫画/漫剧重要性:main=必须建卡保证一致;support=有锚点即可;extra=路人随机生成不约束。 */
|
|
560
|
+
importance?: 'main' | 'support' | 'extra'
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/** 角色图集条目:一张已定型的角色图(图像锚点)。 */
|
|
564
|
+
export interface RoleImage {
|
|
565
|
+
/** 用途标签:立绘 / 四视图 / 表情-疲惫 / 场景 / 细节 等。 */
|
|
566
|
+
label: string
|
|
567
|
+
/** dataURL 或 URL。 */
|
|
568
|
+
dataUrl: string
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/** 场景库条目:从正文提炼的「镜头场景」视觉锚点(漫剧分镜/生图用)。 */
|
|
572
|
+
export interface SceneCard {
|
|
573
|
+
/** 场景名(唯一键),如「旧茶渡·地下石殿」。 */
|
|
574
|
+
name: string
|
|
575
|
+
/** 一句话定位:空间/氛围/功能。 */
|
|
576
|
+
summary: string
|
|
577
|
+
/** 幕归属:第一幕·后场 / 第二幕·卖场 / …(便于按剧情段落组织)。 */
|
|
578
|
+
act?: string
|
|
579
|
+
/** 时间光态:夜间闭店后 / 雨夜 / 凌晨 / 频闪灯… */
|
|
580
|
+
moment?: string
|
|
581
|
+
/** 关键情节镜头 1-3 条:该场景拍什么(人物动作+情绪+镜头推进)。 */
|
|
582
|
+
beats?: string[]
|
|
583
|
+
/** 人物在场状态:主角/关键角色在该场景的状态一句话。 */
|
|
584
|
+
characterState?: string
|
|
585
|
+
/** 环境构成:空间结构/陈设/标志物。 */
|
|
586
|
+
elements: string[]
|
|
587
|
+
/** 色调与光影(含 HEX 色板,可空)。 */
|
|
588
|
+
palette: string[]
|
|
589
|
+
/** 氛围关键词:压抑/神秘/空旷… */
|
|
590
|
+
moods: string[]
|
|
591
|
+
/** 中文生图提示词(连贯一段,写实电影感)。 */
|
|
592
|
+
zh: string
|
|
593
|
+
/** 英文生图提示词(逗号分隔标签)。 */
|
|
594
|
+
en: string
|
|
595
|
+
/** 关键标签。 */
|
|
596
|
+
tags: string[]
|
|
597
|
+
/** 依据来源(哪几章哪些描写)。 */
|
|
598
|
+
source?: string
|
|
599
|
+
/** 场景图集(图像锚点):全景/局部/氛围 等。 */
|
|
600
|
+
gallery?: RoleImage[]
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** POST /scenes 请求:场景库提炼 / 采纳 / 更新 / 删除 / 图集。 */
|
|
604
|
+
export interface ScenesRequest {
|
|
605
|
+
op: 'extract' | 'adopt' | 'update' | 'remove' | 'image' | 'removeImage'
|
|
606
|
+
/** adopt / update 时传入的场景卡(adopt 可修改后采纳)。 */
|
|
607
|
+
scene?: SceneCard
|
|
608
|
+
/** remove / image 时的场景名。 */
|
|
609
|
+
name?: string
|
|
610
|
+
/** op='image' 时的图片 dataURL 与用途标签(全景/局部/氛围)。 */
|
|
611
|
+
dataUrl?: string
|
|
612
|
+
label?: string
|
|
633
613
|
}
|
|
634
614
|
|
|
635
|
-
/** POST /
|
|
615
|
+
/** POST /visual-rules 请求:视觉世界观规则提炼 / 保存。 */
|
|
616
|
+
export interface VisualRulesRequest {
|
|
617
|
+
op: 'extract' | 'save'
|
|
618
|
+
/** op=save 时的规则数组。 */
|
|
619
|
+
rules?: string[]
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** POST /visual-rules 响应。 */
|
|
623
|
+
export interface VisualRulesResponse {
|
|
624
|
+
rules: string[]
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/** POST /scenes 响应。 */
|
|
628
|
+
export interface ScenesResponse {
|
|
629
|
+
scenes: SceneCard[]
|
|
630
|
+
/** op=extract 时的 AI 候选场景。 */
|
|
631
|
+
candidates?: SceneCard[]
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** POST /roles 请求:角色库增删改 + AI 提炼 + 参考图上传。 */
|
|
636
635
|
export interface RolesRequest {
|
|
637
|
-
op: 'extract' | 'adopt' | 'update' | 'remove' | 'visual'
|
|
636
|
+
op: 'extract' | 'adopt' | 'update' | 'remove' | 'visual' | 'image' | 'removeImage' | 'imageGenerate' | 'promptKit'
|
|
638
637
|
/** adopt / update 时传入的角色(adopt 可修改后采纳)。 */
|
|
639
638
|
role?: RoleRecord
|
|
640
639
|
/** remove 时的角色名。 */
|
|
641
640
|
name?: string
|
|
641
|
+
/** op='image' 时的参考图 dataURL。 */
|
|
642
|
+
dataUrl?: string
|
|
643
|
+
/** op='image' 时的图集用途标签(立绘/四视图/表情-x/场景/细节);缺省视为角色参考图(imageUrl)。 */
|
|
644
|
+
label?: string
|
|
645
|
+
/** op='imageGenerate' 时的漫画风格预设 id(用于统一角色图与漫画页风格)。 */
|
|
646
|
+
style?: string
|
|
642
647
|
}
|
|
643
648
|
|
|
644
649
|
/** POST /roles 响应。 */
|
|
@@ -648,6 +653,10 @@ export interface RolesResponse {
|
|
|
648
653
|
candidates?: RoleRecord[]
|
|
649
654
|
/** op=visual 时的动漫形象描述词(已写入角色卡)。 */
|
|
650
655
|
visual?: RoleRecord['imagePrompt']
|
|
656
|
+
/** op=promptKit 时的四类提示词精修包(已写入角色卡)。 */
|
|
657
|
+
promptKit?: RoleRecord['promptKit']
|
|
658
|
+
/** op=image 时的角色参考图 dataURL。 */
|
|
659
|
+
imageUrl?: string
|
|
651
660
|
}
|
|
652
661
|
|
|
653
662
|
/** POST /bible/patch 请求:局部修补设定圣经。 */
|
|
@@ -776,8 +785,15 @@ export interface ProjectState {
|
|
|
776
785
|
plotlines?: Plotline[]
|
|
777
786
|
/** 角色库(作者维护 + AI 提炼的主表)。 */
|
|
778
787
|
roles?: RoleRecord[]
|
|
788
|
+
/** 场景库(作者维护 + AI 提炼的主表)。 */
|
|
789
|
+
scenes?: SceneCard[]
|
|
790
|
+
/** 视觉世界观规则(生图/生视频必须遵守,从道藏提炼,注入所有提示词)。 */
|
|
791
|
+
visualRules?: string[]
|
|
779
792
|
/** 人物志:角色当前状态聚合结果(从编年录刷新后存档,打开页面直接显示)。 */
|
|
780
793
|
roleStatus?: RoleStatusCard[]
|
|
794
|
+
/** 漫剧分镜结果缓存:章号 → 分镜(持久化,刷新不丢)。 */
|
|
795
|
+
/** 漫剧分集计划缓存:卷号/0=全书 → 分集计划(持久化,刷新不丢)。 */
|
|
796
|
+
/** 漫画脚本缓存:章号 → 漫画脚本(持久化,刷新不丢)。 */
|
|
781
797
|
/** ISO timestamps. */
|
|
782
798
|
createdAt: string
|
|
783
799
|
updatedAt: string
|
|
@@ -807,6 +823,12 @@ export interface NovelConfig {
|
|
|
807
823
|
autoAuthorReview: boolean
|
|
808
824
|
/** 修订/润色产出草稿后是否自动附带一次 AI 审查(工作区显示新稿评分与剩余问题)。 */
|
|
809
825
|
autoReviewAfterRevise: boolean
|
|
826
|
+
/** 豆包/Seedream 生图 API Key(保存在服务端配置,不暴露给前端明文回显)。 */
|
|
827
|
+
imageApiKey?: string
|
|
828
|
+
/** 豆包/Seedream 生图模型 ID(如 doubao-seedream-5-0-pro-260628)。 */
|
|
829
|
+
imageApiModel?: string
|
|
830
|
+
/** 是否启用豆包生图(默认关,避免误调用付费 API)。 */
|
|
831
|
+
imageApiEnabled?: boolean
|
|
810
832
|
}
|
|
811
833
|
|
|
812
834
|
/** GET /status response. */
|
|
@@ -1017,6 +1039,9 @@ export interface ConfigPatch {
|
|
|
1017
1039
|
autoReview?: boolean
|
|
1018
1040
|
autoAuthorReview?: boolean
|
|
1019
1041
|
autoReviewAfterRevise?: boolean
|
|
1042
|
+
imageApiKey?: string
|
|
1043
|
+
imageApiModel?: string
|
|
1044
|
+
imageApiEnabled?: boolean
|
|
1020
1045
|
}
|
|
1021
1046
|
|
|
1022
1047
|
// ------------------------------------------------------------ assistant
|
package/src/routes.ts
CHANGED
|
@@ -65,14 +65,15 @@ import {
|
|
|
65
65
|
type RewriteRequest,
|
|
66
66
|
type RolesRequest,
|
|
67
67
|
type RolesResponse,
|
|
68
|
+
type SceneCard,
|
|
69
|
+
type ScenesRequest,
|
|
70
|
+
type ScenesResponse,
|
|
71
|
+
type VisualRulesRequest,
|
|
72
|
+
type VisualRulesResponse,
|
|
68
73
|
type OutlineSuggestRequest,
|
|
69
74
|
type OutlineSuggestResponse,
|
|
70
75
|
type BreakdownRequest,
|
|
71
76
|
type BreakdownResponse,
|
|
72
|
-
type StoryboardRequest,
|
|
73
|
-
type StoryboardResponse,
|
|
74
|
-
type StoryboardPlanRequest,
|
|
75
|
-
type StoryboardPlanResponse,
|
|
76
77
|
type SensitiveCheckRequest,
|
|
77
78
|
type SensitiveCheckResponse,
|
|
78
79
|
type SensitiveHit,
|
|
@@ -117,11 +118,13 @@ import {
|
|
|
117
118
|
analyzePlotlineHealth,
|
|
118
119
|
designPlotlinePlan,
|
|
119
120
|
extractRoles,
|
|
121
|
+
extractScenes,
|
|
122
|
+
generateRolePromptKit,
|
|
123
|
+
extractVisualRules,
|
|
120
124
|
extractRoleVisual,
|
|
121
125
|
suggestOutlines,
|
|
122
126
|
breakdownBook,
|
|
123
|
-
|
|
124
|
-
planStoryboardEpisodes,
|
|
127
|
+
generateRoleReferenceImage,
|
|
125
128
|
checkSensitiveText,
|
|
126
129
|
suggestForeshadows,
|
|
127
130
|
suggestPlotlines,
|
|
@@ -349,7 +352,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
349
352
|
return
|
|
350
353
|
}
|
|
351
354
|
try {
|
|
352
|
-
const bible = await extractBible(ctx, config, outline)
|
|
355
|
+
const bible = await extractBible(ctx, config, outline, project)
|
|
353
356
|
const now = new Date().toISOString()
|
|
354
357
|
const next = project ?? createProject(outline)
|
|
355
358
|
next.bible = bible
|
|
@@ -1683,6 +1686,69 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1683
1686
|
else project.roles[idx] = r
|
|
1684
1687
|
} else if (op === 'remove' && body?.name !== undefined) {
|
|
1685
1688
|
project.roles = project.roles.filter(x => x.name !== body.name)
|
|
1689
|
+
} else if (op === 'image') {
|
|
1690
|
+
const name = body?.name?.trim()
|
|
1691
|
+
const dataUrl = body?.dataUrl?.trim()
|
|
1692
|
+
if (name === undefined || name === '') {
|
|
1693
|
+
writeJson(res, 400, { error: 'name(角色名)必填' })
|
|
1694
|
+
return
|
|
1695
|
+
}
|
|
1696
|
+
if (dataUrl === undefined || dataUrl === '') {
|
|
1697
|
+
writeJson(res, 400, { error: 'dataUrl(参考图)必填' })
|
|
1698
|
+
return
|
|
1699
|
+
}
|
|
1700
|
+
const role = project.roles.find(r => r.name === name)
|
|
1701
|
+
if (role === undefined) {
|
|
1702
|
+
writeJson(res, 404, { error: `角色 ${name} 不存在` })
|
|
1703
|
+
return
|
|
1704
|
+
}
|
|
1705
|
+
const label = body?.label?.trim() ?? ''
|
|
1706
|
+
if (label !== '') {
|
|
1707
|
+
// 图集上传:带用途标签。标签「立绘」时同步为角色参考图(imageUrl)。
|
|
1708
|
+
role.gallery ??= []
|
|
1709
|
+
role.gallery = role.gallery.filter(g => g.label !== label)
|
|
1710
|
+
role.gallery.push({ label, dataUrl })
|
|
1711
|
+
if (label === '立绘' || label.includes('立绘')) role.imageUrl = dataUrl
|
|
1712
|
+
} else {
|
|
1713
|
+
// 兼容旧行为:不带标签 = 角色参考图。
|
|
1714
|
+
role.imageUrl = dataUrl
|
|
1715
|
+
}
|
|
1716
|
+
project.updatedAt = new Date().toISOString()
|
|
1717
|
+
saveProject(config.outputDir, project)
|
|
1718
|
+
writeJson(res, 200, { roles: project.roles, imageUrl: role.imageUrl } satisfies RolesResponse)
|
|
1719
|
+
return
|
|
1720
|
+
} else if (op === 'removeImage') {
|
|
1721
|
+
const name = body?.name?.trim()
|
|
1722
|
+
const label = body?.label?.trim() ?? ''
|
|
1723
|
+
const role = project.roles.find(r => r.name === name)
|
|
1724
|
+
if (role !== undefined) {
|
|
1725
|
+
if (label !== '') {
|
|
1726
|
+
role.gallery = (role.gallery ?? []).filter(g => g.label !== label)
|
|
1727
|
+
if (label === '立绘' || label.includes('立绘')) role.imageUrl = undefined
|
|
1728
|
+
} else {
|
|
1729
|
+
role.imageUrl = undefined
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
project.updatedAt = new Date().toISOString()
|
|
1733
|
+
saveProject(config.outputDir, project)
|
|
1734
|
+
writeJson(res, 200, { roles: project.roles } satisfies RolesResponse)
|
|
1735
|
+
return
|
|
1736
|
+
} else if (op === 'imageGenerate') {
|
|
1737
|
+
const name = body?.name?.trim()
|
|
1738
|
+
if (name === undefined || name === '') {
|
|
1739
|
+
writeJson(res, 400, { error: 'name(角色名)必填' })
|
|
1740
|
+
return
|
|
1741
|
+
}
|
|
1742
|
+
try {
|
|
1743
|
+
const imageUrl = await generateRoleReferenceImage(ctx, config, project, config.outputDir, name, body?.style ?? '')
|
|
1744
|
+
project.updatedAt = new Date().toISOString()
|
|
1745
|
+
saveProject(config.outputDir, project)
|
|
1746
|
+
writeJson(res, 200, { roles: project.roles, imageUrl } satisfies RolesResponse)
|
|
1747
|
+
return
|
|
1748
|
+
} catch (error) {
|
|
1749
|
+
writeJson(res, 500, { error: `角色参考图生成失败:${(error as Error).message}` })
|
|
1750
|
+
return
|
|
1751
|
+
}
|
|
1686
1752
|
} else if (op === 'visual') {
|
|
1687
1753
|
// 动漫形象描述词提炼:扫描正文外貌描写 → LLM 提炼 → 写入角色卡。
|
|
1688
1754
|
const name = body?.name?.trim()
|
|
@@ -1693,7 +1759,11 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1693
1759
|
try {
|
|
1694
1760
|
const visual = await extractRoleVisual(ctx, config, project, config.outputDir, name)
|
|
1695
1761
|
const role = project.roles.find(r => r.name === name)
|
|
1696
|
-
if (role !== undefined)
|
|
1762
|
+
if (role !== undefined) {
|
|
1763
|
+
role.imagePrompt = visual
|
|
1764
|
+
if ((visual.expressions ?? []).length > 0) role.expressions = visual.expressions
|
|
1765
|
+
if (visual.promptKit !== undefined) role.promptKit = visual.promptKit
|
|
1766
|
+
}
|
|
1697
1767
|
project.updatedAt = new Date().toISOString()
|
|
1698
1768
|
saveProject(config.outputDir, project)
|
|
1699
1769
|
writeJson(res, 200, { roles: project.roles, visual } satisfies RolesResponse)
|
|
@@ -1702,6 +1772,25 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1702
1772
|
writeJson(res, 500, { error: `形象提炼失败:${(error as Error).message}` })
|
|
1703
1773
|
return
|
|
1704
1774
|
}
|
|
1775
|
+
} else if (op === 'promptKit') {
|
|
1776
|
+
// 角色四类提示词精修包(立绘/四视图/表情/细节)
|
|
1777
|
+
const name = body?.name?.trim()
|
|
1778
|
+
if (name === undefined || name === '') {
|
|
1779
|
+
writeJson(res, 400, { error: 'name(角色名)必填' })
|
|
1780
|
+
return
|
|
1781
|
+
}
|
|
1782
|
+
try {
|
|
1783
|
+
const kit = await generateRolePromptKit(ctx, config, project, name)
|
|
1784
|
+
const role = project.roles.find(r => r.name === name)
|
|
1785
|
+
if (role !== undefined) role.promptKit = kit
|
|
1786
|
+
project.updatedAt = new Date().toISOString()
|
|
1787
|
+
saveProject(config.outputDir, project)
|
|
1788
|
+
writeJson(res, 200, { roles: project.roles, promptKit: kit } satisfies RolesResponse)
|
|
1789
|
+
return
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
writeJson(res, 500, { error: `提示词精修失败:${(error as Error).message}` })
|
|
1792
|
+
return
|
|
1793
|
+
}
|
|
1705
1794
|
} else {
|
|
1706
1795
|
writeJson(res, 400, { error: '未知的 roles op' })
|
|
1707
1796
|
return
|
|
@@ -1952,67 +2041,99 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1952
2041
|
},
|
|
1953
2042
|
}
|
|
1954
2043
|
|
|
1955
|
-
//
|
|
1956
|
-
/**
|
|
1957
|
-
const
|
|
2044
|
+
// --------------------------------------------------------------- scenes
|
|
2045
|
+
/** 场景库:AI 提炼 / 采纳 / 更新 / 删除 / 图集。 */
|
|
2046
|
+
const scenesRoute: WebRoute = {
|
|
1958
2047
|
kind: 'exact',
|
|
1959
|
-
path: NOVEL_API.
|
|
2048
|
+
path: NOVEL_API.scenes,
|
|
1960
2049
|
handler: async (req, res) => {
|
|
1961
2050
|
if (!guard(req, res, 'POST')) return
|
|
1962
2051
|
const config = getConfig()
|
|
1963
2052
|
const project = requireProject(res)
|
|
1964
2053
|
if (project === undefined) return
|
|
1965
|
-
const body = await readJsonBody<
|
|
1966
|
-
if (
|
|
1967
|
-
|
|
2054
|
+
const body = await readJsonBody<ScenesRequest>(req)
|
|
2055
|
+
if (project.scenes === undefined) project.scenes = []
|
|
2056
|
+
const op = body?.op
|
|
2057
|
+
if (op === 'extract') {
|
|
2058
|
+
try {
|
|
2059
|
+
const candidates = await extractScenes(ctx, config, project)
|
|
2060
|
+
writeJson(res, 200, { scenes: project.scenes, candidates } satisfies ScenesResponse)
|
|
2061
|
+
return
|
|
2062
|
+
} catch (error) {
|
|
2063
|
+
writeJson(res, 500, { error: `场景提炼失败:${(error as Error).message}` })
|
|
2064
|
+
return
|
|
2065
|
+
}
|
|
2066
|
+
} else if ((op === 'adopt' || op === 'update') && body?.scene !== undefined) {
|
|
2067
|
+
const s = body.scene
|
|
2068
|
+
const idx = project.scenes.findIndex(x => x.name === s.name)
|
|
2069
|
+
if (idx === -1) project.scenes.push(s)
|
|
2070
|
+
else project.scenes[idx] = s
|
|
2071
|
+
} else if (op === 'remove' && body?.name !== undefined) {
|
|
2072
|
+
project.scenes = project.scenes.filter(x => x.name !== body.name)
|
|
2073
|
+
} else if (op === 'image') {
|
|
2074
|
+
const name = body?.name?.trim()
|
|
2075
|
+
const dataUrl = body?.dataUrl?.trim()
|
|
2076
|
+
if (name === undefined || name === '' || dataUrl === undefined || dataUrl === '') {
|
|
2077
|
+
writeJson(res, 400, { error: 'name(场景名)与 dataUrl 必填' })
|
|
2078
|
+
return
|
|
2079
|
+
}
|
|
2080
|
+
const scene = project.scenes.find(x => x.name === name)
|
|
2081
|
+
if (scene === undefined) {
|
|
2082
|
+
writeJson(res, 404, { error: `场景 ${name} 不存在` })
|
|
2083
|
+
return
|
|
2084
|
+
}
|
|
2085
|
+
const label = body?.label?.trim() ?? '全景'
|
|
2086
|
+
scene.gallery ??= []
|
|
2087
|
+
scene.gallery = scene.gallery.filter(g => g.label !== label)
|
|
2088
|
+
scene.gallery.push({ label, dataUrl })
|
|
2089
|
+
} else if (op === 'removeImage') {
|
|
2090
|
+
const name = body?.name?.trim()
|
|
2091
|
+
const label = body?.label?.trim() ?? ''
|
|
2092
|
+
const scene = project.scenes.find(x => x.name === name)
|
|
2093
|
+
if (scene !== undefined && label !== '') {
|
|
2094
|
+
scene.gallery = (scene.gallery ?? []).filter(g => g.label !== label)
|
|
2095
|
+
}
|
|
2096
|
+
} else {
|
|
2097
|
+
writeJson(res, 400, { error: `未知操作 ${op}` })
|
|
1968
2098
|
return
|
|
1969
2099
|
}
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
config,
|
|
1974
|
-
project,
|
|
1975
|
-
config.outputDir,
|
|
1976
|
-
body!.chapterNo!,
|
|
1977
|
-
body?.genre ?? '',
|
|
1978
|
-
body?.platform ?? '抖音',
|
|
1979
|
-
body?.tool ?? 'doubao',
|
|
1980
|
-
)
|
|
1981
|
-
writeJson(res, 200, result satisfies StoryboardResponse)
|
|
1982
|
-
} catch (error) {
|
|
1983
|
-
writeJson(res, 500, { error: `分镜生成失败:${(error as Error).message}` })
|
|
1984
|
-
}
|
|
2100
|
+
project.updatedAt = new Date().toISOString()
|
|
2101
|
+
saveProject(config.outputDir, project)
|
|
2102
|
+
writeJson(res, 200, { scenes: project.scenes } satisfies ScenesResponse)
|
|
1985
2103
|
},
|
|
1986
2104
|
}
|
|
1987
2105
|
|
|
1988
|
-
//
|
|
1989
|
-
/**
|
|
1990
|
-
const
|
|
2106
|
+
// ----------------------------------------------------------- visual rules
|
|
2107
|
+
/** 视觉世界观规则:从道藏提炼(生图/生视频纠偏),或手动保存。 */
|
|
2108
|
+
const visualRulesRoute: WebRoute = {
|
|
1991
2109
|
kind: 'exact',
|
|
1992
|
-
path: NOVEL_API.
|
|
2110
|
+
path: NOVEL_API.visualRules,
|
|
1993
2111
|
handler: async (req, res) => {
|
|
1994
2112
|
if (!guard(req, res, 'POST')) return
|
|
1995
2113
|
const config = getConfig()
|
|
1996
2114
|
const project = requireProject(res)
|
|
1997
2115
|
if (project === undefined) return
|
|
1998
|
-
const body = await readJsonBody<
|
|
1999
|
-
if (
|
|
2000
|
-
|
|
2116
|
+
const body = await readJsonBody<VisualRulesRequest>(req)
|
|
2117
|
+
if (body?.op === 'extract') {
|
|
2118
|
+
try {
|
|
2119
|
+
const rules = await extractVisualRules(ctx, config, project)
|
|
2120
|
+
project.visualRules = rules
|
|
2121
|
+
project.updatedAt = new Date().toISOString()
|
|
2122
|
+
saveProject(config.outputDir, project)
|
|
2123
|
+
writeJson(res, 200, { rules } satisfies VisualRulesResponse)
|
|
2124
|
+
return
|
|
2125
|
+
} catch (error) {
|
|
2126
|
+
writeJson(res, 500, { error: `视觉规则提炼失败:${(error as Error).message}` })
|
|
2127
|
+
return
|
|
2128
|
+
}
|
|
2129
|
+
} else if (body?.op === 'save' && Array.isArray(body.rules)) {
|
|
2130
|
+
project.visualRules = body.rules.filter(r => typeof r === 'string' && r.trim() !== '').map(r => r.trim().slice(0, 80)).slice(0, 12)
|
|
2131
|
+
project.updatedAt = new Date().toISOString()
|
|
2132
|
+
saveProject(config.outputDir, project)
|
|
2133
|
+
writeJson(res, 200, { rules: project.visualRules } satisfies VisualRulesResponse)
|
|
2001
2134
|
return
|
|
2002
2135
|
}
|
|
2003
|
-
|
|
2004
|
-
const result = await planStoryboardEpisodes(
|
|
2005
|
-
ctx,
|
|
2006
|
-
config,
|
|
2007
|
-
project,
|
|
2008
|
-
body!.volumeNo!,
|
|
2009
|
-
body?.platform ?? '抖音',
|
|
2010
|
-
body?.maxEpisodes ?? 25,
|
|
2011
|
-
)
|
|
2012
|
-
writeJson(res, 200, result satisfies StoryboardPlanResponse)
|
|
2013
|
-
} catch (error) {
|
|
2014
|
-
writeJson(res, 500, { error: `分集计划生成失败:${(error as Error).message}` })
|
|
2015
|
-
}
|
|
2136
|
+
writeJson(res, 400, { error: 'op 须为 extract 或 save' })
|
|
2016
2137
|
},
|
|
2017
2138
|
}
|
|
2018
2139
|
|
|
@@ -2121,6 +2242,8 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2121
2242
|
renameRoute,
|
|
2122
2243
|
plotlinesRoute,
|
|
2123
2244
|
rolesRoute,
|
|
2245
|
+
scenesRoute,
|
|
2246
|
+
visualRulesRoute,
|
|
2124
2247
|
sensitiveRoute,
|
|
2125
2248
|
reviewBackfillRoute,
|
|
2126
2249
|
chapterResetRoute,
|
|
@@ -2129,8 +2252,6 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2129
2252
|
openFolderRoute,
|
|
2130
2253
|
outlineSuggestRoute,
|
|
2131
2254
|
breakdownRoute,
|
|
2132
|
-
storyboardRoute,
|
|
2133
|
-
storyboardPlanRoute,
|
|
2134
2255
|
runStartRoute,
|
|
2135
2256
|
runControlRoute,
|
|
2136
2257
|
runStatusRoute,
|