@waterwx/dsh-novel-forge 2.0.1-alpha → 2.1.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 +368 -362
- package/lib/client.js +20340 -19622
- package/lib/index.js +11313 -7285
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +0 -6
- package/lib/types/assets.d.ts +20 -9
- package/lib/types/assistant.d.ts +29 -0
- package/lib/types/author-assets.d.ts +1 -3
- package/lib/types/bookshelf.d.ts +13 -3
- package/lib/types/client/api.d.ts +136 -27
- package/lib/types/client/chip-prefs.d.ts +36 -0
- package/lib/types/client/docx.d.ts +2 -2
- package/lib/types/client/llmLive.d.ts +31 -0
- package/lib/types/client/locales.d.ts +15 -70
- package/lib/types/client/mount.d.ts +0 -2
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorHome.d.ts +13 -11
- package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +5 -1
- package/lib/types/client/panel/DirectorView.d.ts +7 -0
- package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
- package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
- package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
- package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
- package/lib/types/client/panel/ModelManager.d.ts +2 -2
- package/lib/types/client/panel/NovelPanel.d.ts +6 -1
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
- package/lib/types/client/panel/SettingsView.d.ts +12 -9
- package/lib/types/client/panel/ShelfView.d.ts +10 -1
- package/lib/types/client/panel/SubPage.d.ts +27 -0
- package/lib/types/client/panel/TensionView.d.ts +18 -0
- package/lib/types/client/panel/TimelineView.d.ts +18 -0
- package/lib/types/client/panel/helpers.d.ts +5 -3
- package/lib/types/client/panel/market-radar-state.d.ts +56 -0
- package/lib/types/client/panel/outline-markdown.d.ts +112 -0
- package/lib/types/client/panel/status-badge.d.ts +13 -0
- package/lib/types/client/panel/storage-parse.d.ts +46 -0
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/usePersistentState.d.ts +17 -0
- package/lib/types/client/panel/views.d.ts +21 -35
- package/lib/types/client/run-status.d.ts +21 -0
- package/lib/types/client/sidebar-entry.d.ts +0 -2
- package/lib/types/digest-parse.d.ts +29 -0
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +150 -103
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/home.d.ts +11 -0
- package/lib/types/index.d.ts +26 -12
- package/lib/types/llm-live.d.ts +70 -0
- package/lib/types/llm-retry.d.ts +38 -0
- package/lib/types/market-radar-scan.d.ts +18 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/migrate-output.d.ts +25 -0
- package/lib/types/model-capability.d.ts +119 -0
- package/lib/types/model-request.d.ts +36 -0
- package/lib/types/output-budget.d.ts +86 -0
- package/lib/types/plotline-align.d.ts +35 -0
- package/lib/types/prompt-slots.d.ts +57 -0
- package/lib/types/prompting.d.ts +10 -0
- package/lib/types/protocol.d.ts +841 -601
- package/lib/types/revision.d.ts +76 -0
- package/lib/types/routes.d.ts +9 -3
- package/lib/types/run-selection.d.ts +51 -0
- package/lib/types/run.d.ts +32 -1
- package/lib/types/settings-persist.d.ts +59 -0
- package/lib/types/snapshots.d.ts +46 -0
- package/lib/types/stage-contract.d.ts +58 -0
- package/lib/types/stream-fallback.d.ts +60 -0
- package/lib/types/strip-headings.d.ts +33 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +50 -27
- package/assets/styles/arcane-thick-3d.webp +0 -0
- package/assets/styles/chinese-ink.webp +0 -0
- package/assets/styles/clay-stopmotion-3d.webp +0 -0
- package/assets/styles/dark-ghibli.webp +0 -0
- package/assets/styles/dark-gothic-3d.webp +0 -0
- package/assets/styles/film-photo.webp +0 -0
- package/assets/styles/guofeng-heavy.webp +0 -0
- package/assets/styles/guoman-2d-gufeng.webp +0 -0
- package/assets/styles/guoman-3d-tianyuan.webp +0 -0
- package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
- package/assets/styles/horror-game-3d.webp +0 -0
- package/assets/styles/hyperreal-3d.webp +0 -0
- package/assets/styles/japan-cel-dark.webp +0 -0
- package/assets/styles/korean-webtoon.webp +0 -0
- package/assets/styles/low-cyberpunk-3d.webp +0 -0
- package/assets/styles/nextgen-cg.webp +0 -0
- package/assets/styles/noir-film.webp +0 -0
- package/assets/styles/openworld-survival.webp +0 -0
- package/assets/styles/paper-cut-3d.webp +0 -0
- package/assets/styles/pixar-adult-3d.webp +0 -0
- package/assets/styles/pop-marvel-2d.webp +0 -0
- package/assets/styles/sandiao-qban.webp +0 -0
- package/assets/styles/shinkai-anime.webp +0 -0
- package/assets/styles/tactical-stealth.webp +0 -0
- package/assets/styles/urban-lightreal-3d.webp +0 -0
- package/assets/styles/vaporwave-3d.webp +0 -0
- package/assets/styles/vhs90.webp +0 -0
- package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
- package/lib/types/client/panel/FlowGuide.d.ts +0 -22
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
- package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
- package/lib/types/client/panel/ModelPicker.d.ts +0 -11
- package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
- package/lib/types/client/panel/PropLibrary.d.ts +0 -8
- package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
- package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
- package/lib/types/client/panel/StyleCard.d.ts +0 -8
- package/lib/types/client/panel/StyleGallery.d.ts +0 -1
- package/lib/types/comic-presets.d.ts +0 -12
- package/lib/types/manga-genre-rules.d.ts +0 -22
- package/lib/types/prompt-utils.d.ts +0 -35
- package/lib/types/review-policy.d.ts +0 -97
- package/lib/types/shot-language.d.ts +0 -46
- package/lib/types/story-beat-language.d.ts +0 -35
- package/lib/types/style-library.d.ts +0 -33
- package/skills/chapter-batch/SKILL.md +0 -81
- package/src/ai-scan.ts +0 -179
- package/src/assets.ts +0 -879
- package/src/assistant.ts +0 -770
- package/src/author-assets.ts +0 -147
- package/src/bookshelf.ts +0 -167
- package/src/client/api.ts +0 -617
- package/src/client/css-modules.d.ts +0 -8
- package/src/client/docx.ts +0 -69
- package/src/client/index.ts +0 -34
- package/src/client/locales.ts +0 -418
- package/src/client/mount.tsx +0 -97
- package/src/client/mount.tsx._new +0 -97
- package/src/client/mount.tsx._nf +0 -97
- package/src/client/mount.tsx._tmp +0 -97
- package/src/client/panel/AdaptModeView.tsx +0 -520
- package/src/client/panel/AssetsTab.tsx +0 -458
- package/src/client/panel/AssistantTab.tsx +0 -301
- package/src/client/panel/AuthorAssetsView.tsx +0 -208
- package/src/client/panel/AuthorHome.tsx +0 -204
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/client/panel/CreateBookView.tsx +0 -352
- package/src/client/panel/FlowGuide.tsx +0 -84
- package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
- package/src/client/panel/ImportModal.tsx +0 -303
- package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
- package/src/client/panel/MangaWorkspace.tsx +0 -715
- package/src/client/panel/ModelManager.tsx +0 -205
- package/src/client/panel/NovelPanel.tsx +0 -4620
- package/src/client/panel/ProgressConsole.tsx +0 -64
- package/src/client/panel/ProgressHomeView.tsx +0 -87
- package/src/client/panel/PropLibrary.tsx +0 -175
- package/src/client/panel/ReaderView.tsx +0 -306
- package/src/client/panel/ReasoningSection.tsx +0 -31
- package/src/client/panel/RunPanel.tsx +0 -204
- package/src/client/panel/SceneLibrary.tsx +0 -453
- package/src/client/panel/SettingsView.tsx +0 -286
- package/src/client/panel/ShelfView.tsx +0 -242
- package/src/client/panel/StoryboardTab.tsx +0 -472
- package/src/client/panel/StyleCard.tsx +0 -90
- package/src/client/panel/StyleGallery.tsx +0 -58
- package/src/client/panel/WorldTab.tsx +0 -202
- package/src/client/panel/controller.ts +0 -45
- package/src/client/panel/helpers.ts +0 -67
- package/src/client/panel/panel.module.css +0 -4360
- package/src/client/panel/views.tsx +0 -314
- package/src/client/sidebar-entry.ts +0 -122
- package/src/client/text.ts +0 -28
- package/src/comic-presets.ts +0 -67
- package/src/docx.ts +0 -83
- package/src/engine.ts +0 -5200
- package/src/index.ts +0 -329
- package/src/manga-genre-rules.ts +0 -87
- package/src/novel-context.ts +0 -287
- package/src/prompt-utils.ts +0 -73
- package/src/protocol.ts +0 -2233
- package/src/review-policy.ts +0 -161
- package/src/routes.ts +0 -3419
- package/src/run.ts +0 -331
- package/src/shot-language.ts +0 -181
- package/src/story-beat-language.ts +0 -130
- package/src/style-library.ts +0 -84
|
@@ -1,715 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 漫剧工作台(5步流程):
|
|
3
|
-
* 顶部常驻:方案切换器 + 全流程步骤条(唯一导航);
|
|
4
|
-
* 主体按步骤渲染独立页面:①创建方案 ②一键生成 ③角色定妆 ④场景底图 ⑤导出即梦脚本。
|
|
5
|
-
* 每步页面只显示自己的内容;前置不足显示明确提示,不静默降级、不夹带别区内容。
|
|
6
|
-
*/
|
|
7
|
-
import { useMemo, useState } from 'react'
|
|
8
|
-
import type { NovelApi } from '../api.ts'
|
|
9
|
-
import type { ChapterPlan, ImageModelConfig, MangaPlan, ProjectState } from '../../protocol.ts'
|
|
10
|
-
import { STYLE_CATEGORIES, STYLE_LIBRARY, findStyle, type StyleCategory } from '../../style-library.ts'
|
|
11
|
-
import { StyleCard } from './StyleCard.tsx'
|
|
12
|
-
import { GENRES } from '../../manga-genre-rules.ts'
|
|
13
|
-
import { StoryboardTab } from './StoryboardTab.tsx'
|
|
14
|
-
import { MangaRoleLibrary } from './MangaRoleLibrary.tsx'
|
|
15
|
-
import { SceneLibrary } from './SceneLibrary.tsx'
|
|
16
|
-
import { PropLibrary } from './PropLibrary.tsx'
|
|
17
|
-
import { FlowGuide, type FlowTarget } from './FlowGuide.tsx'
|
|
18
|
-
import css from './panel.module.css'
|
|
19
|
-
|
|
20
|
-
/** 步骤页编号:1建方案 2一键生成 3角色定妆 4场景底图 5分镜·提示词 6道具库。 */
|
|
21
|
-
type StepView = 1 | 2 | 3 | 4 | 5 | 6
|
|
22
|
-
|
|
23
|
-
const FLOW_TARGET_TO_VIEW: Record<FlowTarget, StepView> = {
|
|
24
|
-
plan: 1,
|
|
25
|
-
rules: 2,
|
|
26
|
-
skeleton: 5,
|
|
27
|
-
table: 5,
|
|
28
|
-
import: 2,
|
|
29
|
-
makeup: 3,
|
|
30
|
-
scenes: 4,
|
|
31
|
-
props: 6,
|
|
32
|
-
prompts: 5,
|
|
33
|
-
export: 5,
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function MangaWorkspace({
|
|
37
|
-
api,
|
|
38
|
-
project,
|
|
39
|
-
chapters,
|
|
40
|
-
onProjectChanged,
|
|
41
|
-
onProgress,
|
|
42
|
-
}: {
|
|
43
|
-
api: NovelApi
|
|
44
|
-
project: ProjectState | null
|
|
45
|
-
chapters: ChapterPlan[]
|
|
46
|
-
/** 方案/资产变更已持久化后触发(刷新项目)。 */
|
|
47
|
-
onProjectChanged?: () => void | Promise<void>
|
|
48
|
-
/** 是否启用生图(漫剧卡出定妆图)。 */
|
|
49
|
-
/** 上报到「AI进度」控制台(漫剧工作台内所有 LLM/方案操作)。 */
|
|
50
|
-
onProgress?: (text: string, kind?: 'info' | 'done' | 'error') => void
|
|
51
|
-
}) {
|
|
52
|
-
const plans = useMemo(() => project?.mangaPlans ?? [], [project?.mangaPlans])
|
|
53
|
-
const activePlan = plans.find(p => p.active) ?? null
|
|
54
|
-
|
|
55
|
-
const [view, setView] = useState<StepView>(2)
|
|
56
|
-
const [busy, setBusy] = useState(false)
|
|
57
|
-
const [error, setError] = useState('')
|
|
58
|
-
const [exported, setExported] = useState(false)
|
|
59
|
-
// 全局当前章节(角色/场景/分镜/导出都基于此章节)
|
|
60
|
-
const [currentChapter, setCurrentChapter] = useState<number | null>(null)
|
|
61
|
-
// 章节选择面板
|
|
62
|
-
const [showChapterPicker, setShowChapterPicker] = useState(false)
|
|
63
|
-
const [chapterQuery, setChapterQuery] = useState('')
|
|
64
|
-
// 一键生成状态
|
|
65
|
-
const [autoGenChapter, setAutoGenChapter] = useState<number | null>(null)
|
|
66
|
-
const [autoGenBusy, setAutoGenBusy] = useState(false)
|
|
67
|
-
const [autoGenResult, setAutoGenResult] = useState<{ chapterNo: number; skeletonBeats: number; shotCount: number; promptCount: number; importedRoles: number; needMakeupRoles: number; extraRoles: number; pendingCandidates: number; pendingRoleNames: string[] } | null>(null)
|
|
68
|
-
const [showRulesModal, setShowRulesModal] = useState(false)
|
|
69
|
-
const [roleFocus, setRoleFocus] = useState<'import' | 'cards'>('cards')
|
|
70
|
-
const [rulesDraft, setRulesDraft] = useState<string[]>([])
|
|
71
|
-
|
|
72
|
-
// 风格选择表单(第①步页)
|
|
73
|
-
const [cat, setCat] = useState<StyleCategory>('3d')
|
|
74
|
-
const [selStyle, setSelStyle] = useState('')
|
|
75
|
-
const [selFilter, setSelFilter] = useState('')
|
|
76
|
-
const [selGenre, setSelGenre] = useState('')
|
|
77
|
-
const [planName, setPlanName] = useState('')
|
|
78
|
-
|
|
79
|
-
// 无方案时强制停在创建方案页。
|
|
80
|
-
const effectiveView: StepView = plans.length === 0 ? 1 : view
|
|
81
|
-
|
|
82
|
-
const baseStyles = useMemo(() => STYLE_LIBRARY.filter(s => s.stackable !== true).sort((a, b) => (b.weight ?? 0) - (a.weight ?? 0)), [])
|
|
83
|
-
const filterStyles = useMemo(() => STYLE_LIBRARY.filter(s => s.stackable === true), [])
|
|
84
|
-
const activeStyle = activePlan !== null ? findStyle(activePlan.styleId) : undefined
|
|
85
|
-
const activeFilter = activePlan?.filterId !== undefined ? findStyle(activePlan.filterId) : undefined
|
|
86
|
-
|
|
87
|
-
const refresh = async (): Promise<void> => { await onProjectChanged?.() }
|
|
88
|
-
|
|
89
|
-
// 章节状态:已完成(有分镜+有提示词+已导出)/ 进行中(有分镜)/ 未开始
|
|
90
|
-
const chapterStatus = useMemo(() => {
|
|
91
|
-
const map = new Map<number, 'done' | 'doing' | 'todo'>()
|
|
92
|
-
const written = chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.status !== 'error')
|
|
93
|
-
for (const c of written) {
|
|
94
|
-
const sb = (project?.storyboards ?? []).find(e => e.chapterNo === c.no)
|
|
95
|
-
const hasPrompts = (sb?.prompts ?? []).length > 0
|
|
96
|
-
const hasTable = (sb?.table?.shots ?? []).length > 0
|
|
97
|
-
if (hasPrompts && hasTable) map.set(c.no, 'done')
|
|
98
|
-
else if (hasTable) map.set(c.no, 'doing')
|
|
99
|
-
else map.set(c.no, 'todo')
|
|
100
|
-
}
|
|
101
|
-
return map
|
|
102
|
-
}, [chapters, project?.storyboards])
|
|
103
|
-
|
|
104
|
-
// 初始化 currentChapter 为第一个已写章节
|
|
105
|
-
const writtenChapters = chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.status !== 'error')
|
|
106
|
-
const effectiveChapter = currentChapter ?? writtenChapters[0]?.no ?? null
|
|
107
|
-
|
|
108
|
-
/** 有视频提示词的章节列表(导出用)。 */
|
|
109
|
-
|
|
110
|
-
/** 导出整章「即梦素材包」到剪贴板:素材编号(@图N) + 角色定妆图提示词 + 场景底图提示词 + 逐镜卡 + 整集画布脚本 + 步骤卡。 */
|
|
111
|
-
const exportMangaPackage = (chapterNo: number): void => {
|
|
112
|
-
const entry = (project?.storyboards ?? []).find(e => e.chapterNo === chapterNo)
|
|
113
|
-
if (entry === undefined || entry.table === undefined || (entry.prompts ?? []).length === 0) return
|
|
114
|
-
const chapter = chapters.find(c => c.no === chapterNo)
|
|
115
|
-
const prompts = entry.prompts!
|
|
116
|
-
const table = entry.table
|
|
117
|
-
const mangaById = new Map((project?.mangaRoles ?? []).map(c => [c.id, c]))
|
|
118
|
-
const sceneByName = new Map((project?.scenes ?? []).map(s => [s.name, s] as const))
|
|
119
|
-
|
|
120
|
-
// 参考清单(按名字,供上传后手动@),保留收集角色/场景引用
|
|
121
|
-
const roleIds: string[] = []
|
|
122
|
-
for (const p of prompts) for (const id of p.mangaRoleIds ?? []) if (!roleIds.includes(id)) roleIds.push(id)
|
|
123
|
-
const sceneNames: string[] = []
|
|
124
|
-
for (const p of prompts) {
|
|
125
|
-
const sn = p.sceneName
|
|
126
|
-
if (sn !== undefined && sn !== '' && sn !== '(未标注)' && !sceneNames.includes(sn)) sceneNames.push(sn)
|
|
127
|
-
}
|
|
128
|
-
const rules = project?.visualRules ?? []
|
|
129
|
-
|
|
130
|
-
const lines: string[] = []
|
|
131
|
-
lines.push('# ' + (project?.bookName ?? '本书') + ' 第' + chapterNo + '章《' + (chapter?.title ?? '') + '》 · 即梦素材包')
|
|
132
|
-
lines.push('生成时间:' + new Date().toLocaleString('zh-CN'))
|
|
133
|
-
lines.push('镜头数:' + prompts.length)
|
|
134
|
-
lines.push('')
|
|
135
|
-
|
|
136
|
-
// 0. 参考素材清单(按名字,上传后手动@对应)
|
|
137
|
-
lines.push('## 📌 参考素材清单(按名字,上传后手动@)')
|
|
138
|
-
for (const id of roleIds) {
|
|
139
|
-
const c = mangaById.get(id)
|
|
140
|
-
if (c !== undefined) lines.push(`- ${c.name}(角色定妆图)${c.imageUrl !== undefined ? '·已有图' : '·待生成'}`)
|
|
141
|
-
}
|
|
142
|
-
for (const n of sceneNames) lines.push(`- ${n}(场景底图)`)
|
|
143
|
-
lines.push('')
|
|
144
|
-
|
|
145
|
-
// 1. 角色定妆图提示词
|
|
146
|
-
lines.push('## 📌 角色定妆图提示词(复制去即梦生角色图)')
|
|
147
|
-
for (const id of roleIds) {
|
|
148
|
-
const c = mangaById.get(id)
|
|
149
|
-
if (c === undefined) continue
|
|
150
|
-
lines.push(`### ${c.name}(${c.identity ?? ''})`)
|
|
151
|
-
const zh = c.imagePrompt?.zh ?? c.appearance ?? ''
|
|
152
|
-
if (zh !== '') {
|
|
153
|
-
lines.push(zh)
|
|
154
|
-
if (c.imagePrompt?.negativePrompt !== undefined && c.imagePrompt.negativePrompt !== '') lines.push('负面词:' + c.imagePrompt.negativePrompt)
|
|
155
|
-
} else {
|
|
156
|
-
lines.push('(尚未生成形象锚点,先到「③ 角色定妆」提炼)')
|
|
157
|
-
}
|
|
158
|
-
lines.push('')
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 2. 场景底图提示词
|
|
162
|
-
if (sceneNames.length > 0) {
|
|
163
|
-
lines.push('## 📌 场景底图提示词(复制去即梦生成场景图)')
|
|
164
|
-
for (const n of sceneNames) {
|
|
165
|
-
const s = sceneByName.get(n)
|
|
166
|
-
const zh = s?.zh ?? ''
|
|
167
|
-
lines.push(`### ${n}`)
|
|
168
|
-
if (zh !== '') {
|
|
169
|
-
lines.push(zh)
|
|
170
|
-
if (s?.negativePrompt !== undefined && s.negativePrompt !== '') lines.push('负面词:' + s.negativePrompt)
|
|
171
|
-
} else {
|
|
172
|
-
lines.push('(尚未提炼场景,先到「④ 场景库」提炼)')
|
|
173
|
-
}
|
|
174
|
-
lines.push('')
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// 3. 逐镜提示词(时间轴 + 台词 + 名字参考)
|
|
179
|
-
lines.push('## 📌 逐镜提示词(逐镜生成:粘贴画布对话框,手动@对应的角色/场景图)')
|
|
180
|
-
let acc = 0
|
|
181
|
-
for (const p of prompts) {
|
|
182
|
-
const shot = table.shots.find(s => s.id === p.shotId)
|
|
183
|
-
const dur = shot?.duration ?? 6
|
|
184
|
-
const start = acc
|
|
185
|
-
const end = acc + dur
|
|
186
|
-
acc = end
|
|
187
|
-
const roleNames = (p.mangaRoleIds ?? []).map(id => mangaById.get(id)?.name ?? id).join('、')
|
|
188
|
-
const scn = p.sceneName !== undefined && p.sceneName !== '' && p.sceneName !== '(未标注)' ? p.sceneName : ''
|
|
189
|
-
const lineText = (shot?.line ?? '').trim()
|
|
190
|
-
lines.push(`### 镜头 ${p.shotId}`)
|
|
191
|
-
lines.push('- **时间轴**:' + start + '-' + end + 's')
|
|
192
|
-
if (p.camera !== undefined && p.camera !== '') lines.push('- **运镜**:' + p.camera)
|
|
193
|
-
if (p.motion !== undefined) lines.push('- **运动幅度**:' + p.motion)
|
|
194
|
-
lines.push('- **参考角色**:' + (roleNames !== '' ? roleNames : '(智能角色)'))
|
|
195
|
-
if (scn !== '') lines.push('- **参考场景**:' + scn)
|
|
196
|
-
if (lineText !== '') lines.push('- **台词**:' + lineText)
|
|
197
|
-
lines.push('- **提示词**:' + p.text)
|
|
198
|
-
if (p.negativePrompt !== undefined && p.negativePrompt !== '') lines.push('- **负面词**:' + p.negativePrompt)
|
|
199
|
-
lines.push('')
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// 4. 整集画布脚本(一次多镜)
|
|
203
|
-
lines.push('## 📌 整集画布脚本(一次多镜:整段粘贴,手动@)')
|
|
204
|
-
lines.push('【全局设定】')
|
|
205
|
-
lines.push('- 风格:' + (activeStyle?.keywords ?? activeFilter?.keywords ?? '写实电影感'))
|
|
206
|
-
if (activeFilter !== undefined) lines.push('- 滤镜:' + activeFilter.keywords)
|
|
207
|
-
for (const id of roleIds) {
|
|
208
|
-
const c = mangaById.get(id)
|
|
209
|
-
if (c !== undefined) lines.push(`- 人物:${c.name}(${c.identity ?? ''}),参考定妆图,严格保持面孔一致`)
|
|
210
|
-
}
|
|
211
|
-
for (const n of sceneNames) lines.push(`- 场景:${n}`)
|
|
212
|
-
if (rules.length > 0) lines.push('- 视觉规则:' + rules.join(';'))
|
|
213
|
-
lines.push('- 禁止字幕、禁止水印、禁止变形。')
|
|
214
|
-
lines.push('')
|
|
215
|
-
for (const p of prompts) {
|
|
216
|
-
const shot = table.shots.find(s => s.id === p.shotId)
|
|
217
|
-
lines.push(`**镜头${p.shotId}(${shot?.duration ?? 6}s)**`)
|
|
218
|
-
lines.push(p.text)
|
|
219
|
-
lines.push('')
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// 5. 步骤卡
|
|
223
|
-
lines.push('## 📌 即梦出片 4 步')
|
|
224
|
-
lines.push('1. 出图:复制上面【角色定妆图提示词】【场景底图提示词】去即梦生成人物图、场景图。')
|
|
225
|
-
lines.push('2. 摆画布:把生成的人物图、场景图都拖进即梦「画布」。')
|
|
226
|
-
lines.push('3. 出片:逐镜 → 复制每镜【提示词】粘贴画布对话框,手动@对应的角色/场景图;整集 → 复制【整集画布脚本】一次生成。')
|
|
227
|
-
lines.push('4. 剪辑:剪映把镜头按顺序拼成一集(60-120s),加字幕、配音、BGM,导出 9:16 无黑边。')
|
|
228
|
-
|
|
229
|
-
const markdown = lines.join('\n')
|
|
230
|
-
void navigator.clipboard?.writeText(markdown)
|
|
231
|
-
// 落盘到资产库 manga-assets/素材包/
|
|
232
|
-
void api.exportPackage(chapterNo, chapter?.title ?? '', markdown).then(res => {
|
|
233
|
-
onProgress?.('已保存素材包到资产库:' + res.file, 'done')
|
|
234
|
-
}).catch(err => { onProgress?.('素材包落盘失败:' + (err as Error).message, 'error') })
|
|
235
|
-
setExported(true)
|
|
236
|
-
setTimeout(() => setExported(false), 2000)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** 一键生成:骨架→分镜→角色提名→自动导入→自动分级→视频提示词。 */
|
|
240
|
-
const autoGenerate = async (): Promise<void> => {
|
|
241
|
-
if (autoGenChapter === null || activePlan === null) return
|
|
242
|
-
setAutoGenBusy(true)
|
|
243
|
-
setError('')
|
|
244
|
-
setAutoGenResult(null)
|
|
245
|
-
onProgress?.('一键生成:拆剧情→生镜头→导角色→写提示词…')
|
|
246
|
-
try {
|
|
247
|
-
const res = await api.mangaRoles({ op: 'autoGenerate', chapterNo: autoGenChapter, styleId: activePlan.styleId, filterId: activePlan.filterId })
|
|
248
|
-
setAutoGenResult(res.autoGenerate ?? null)
|
|
249
|
-
await refresh()
|
|
250
|
-
onProgress?.('一键生成完成:' + (res.autoGenerate?.shotCount ?? 0) + '个镜头,' + (res.autoGenerate?.importedRoles ?? 0) + '个角色', 'done')
|
|
251
|
-
} catch (err) {
|
|
252
|
-
const m = (err as Error).message
|
|
253
|
-
setError(m)
|
|
254
|
-
onProgress?.('一键生成失败:' + m, 'error')
|
|
255
|
-
} finally {
|
|
256
|
-
setAutoGenBusy(false)
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/** 打开漫剧资产库文件夹。 */
|
|
261
|
-
const openAssets = async (): Promise<void> => {
|
|
262
|
-
try {
|
|
263
|
-
await api.mangaRoles({ op: 'openAssets' })
|
|
264
|
-
} catch (err) {
|
|
265
|
-
setError('打开资产库失败:' + (err as Error).message)
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const mutate = async (req: import('../../protocol.ts').MangaPlansRequest): Promise<boolean> => {
|
|
270
|
-
const opLabel = req.op === 'create' ? '创建漫剧方案「' + (req.name ?? '') + '」' : req.op === 'activate' ? '切换漫剧方案' : '删除漫剧方案'
|
|
271
|
-
setBusy(true)
|
|
272
|
-
setError('')
|
|
273
|
-
onProgress?.(opLabel + '…')
|
|
274
|
-
try {
|
|
275
|
-
await api.manhuaPlans(req)
|
|
276
|
-
onProgress?.(opLabel + ' 完成', 'done')
|
|
277
|
-
return true
|
|
278
|
-
} catch (err) {
|
|
279
|
-
const m = (err as Error).message
|
|
280
|
-
setError(m)
|
|
281
|
-
onProgress?.(opLabel + ' 失败:' + m, 'error')
|
|
282
|
-
return false
|
|
283
|
-
} finally {
|
|
284
|
-
setBusy(false)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const createPlan = async (): Promise<void> => {
|
|
289
|
-
const n = planName.trim()
|
|
290
|
-
if (n === '' || selStyle === '') {
|
|
291
|
-
setError('请选择基底风格并填写方案名')
|
|
292
|
-
return
|
|
293
|
-
}
|
|
294
|
-
const ok = await mutate({ op: 'create', name: n, styleId: selStyle, filterId: selFilter !== '' ? selFilter : undefined, genre: selGenre !== '' ? selGenre : undefined })
|
|
295
|
-
if (ok) {
|
|
296
|
-
setPlanName('')
|
|
297
|
-
setSelStyle('')
|
|
298
|
-
setSelFilter('')
|
|
299
|
-
setSelGenre('')
|
|
300
|
-
await refresh()
|
|
301
|
-
setView(2)
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const pickStyle = (id: string): void => {
|
|
306
|
-
setSelStyle(id)
|
|
307
|
-
if (planName.trim() === '') {
|
|
308
|
-
const s = findStyle(id)
|
|
309
|
-
setPlanName((project?.bookName ?? '本书') + ' · ' + (s?.name ?? '') + '版')
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/** 流程第②步:从道藏提炼视觉规则(注入所有提示词)。 */
|
|
314
|
-
const extractRules = async (): Promise<void> => {
|
|
315
|
-
setBusy(true)
|
|
316
|
-
setError('')
|
|
317
|
-
onProgress?.('从道藏提炼视觉规则…')
|
|
318
|
-
try {
|
|
319
|
-
const r = await api.visualRules({ op: 'extract' })
|
|
320
|
-
await refresh()
|
|
321
|
-
onProgress?.('已提炼 ' + r.rules.length + ' 条视觉规则(已注入所有提示词)', 'done')
|
|
322
|
-
} catch (err) {
|
|
323
|
-
const m = (err as Error).message
|
|
324
|
-
setError(m)
|
|
325
|
-
onProgress?.('提炼视觉规则失败:' + m, 'error')
|
|
326
|
-
} finally {
|
|
327
|
-
setBusy(false)
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** 全流程步骤条导航:步骤 → 对应页面。 */
|
|
332
|
-
const navigateFlow = (target: FlowTarget): void => {
|
|
333
|
-
setError('')
|
|
334
|
-
setView(FLOW_TARGET_TO_VIEW[target])
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
if (project === null) {
|
|
338
|
-
return <div className={css.card}><span className={css.meta}>请先开书或选择一本书,再进入漫剧工作台。</span></div>
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const rules = project.visualRules ?? []
|
|
342
|
-
|
|
343
|
-
return (
|
|
344
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-10)' }}>
|
|
345
|
-
{/* 顶部常驻:方案切换器 + 全流程步骤条(唯一导航) */}
|
|
346
|
-
<div className={css.card}>
|
|
347
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
348
|
-
<span className={css.cardTitle}>🎬 {activePlan?.name ?? '漫剧工作台'}</span>
|
|
349
|
-
<div className={css.row}>
|
|
350
|
-
<span className={css.badge}>🎨 {activeStyle?.name ?? activePlan?.styleId}</span>
|
|
351
|
-
{activeFilter !== undefined && <span className={css.badge}>+{activeFilter.name}</span>}
|
|
352
|
-
<select
|
|
353
|
-
className={css.input}
|
|
354
|
-
style={{ width: 'auto', padding: 'var(--nf-space-4) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)', borderRadius: 'var(--nf-radius-8)' }}
|
|
355
|
-
value={activePlan?.id ?? ''}
|
|
356
|
-
onChange={async e => { if (await mutate({ op: 'activate', id: e.target.value })) await refresh() }}
|
|
357
|
-
>
|
|
358
|
-
{plans.map(p => <option key={p.id} value={p.id}>{p.name}</option>)}
|
|
359
|
-
</select>
|
|
360
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} style={{ minWidth: 84 }} onClick={() => { setSelStyle(''); setPlanName(''); setView(1) }}>+ 新建</button>
|
|
361
|
-
<button
|
|
362
|
-
type="button"
|
|
363
|
-
className={css.button + ' ' + css.buttonSmall}
|
|
364
|
-
onClick={() => { void onProjectChanged?.() }}
|
|
365
|
-
title="刷新项目数据(切换书后或章节未显示时点此)"
|
|
366
|
-
>
|
|
367
|
-
🔄 刷新
|
|
368
|
-
</button>
|
|
369
|
-
<button
|
|
370
|
-
type="button"
|
|
371
|
-
className={css.button + ' ' + css.buttonSmall}
|
|
372
|
-
onClick={() => { void openAssets() }}
|
|
373
|
-
title="打开漫剧资产库(角色图/场景图/提示词自动保存到这里)"
|
|
374
|
-
>
|
|
375
|
-
📁 资产库
|
|
376
|
-
</button>
|
|
377
|
-
<button
|
|
378
|
-
type="button"
|
|
379
|
-
className={css.button + ' ' + css.buttonSmall}
|
|
380
|
-
onClick={() => { setRulesDraft([...(project?.visualRules ?? [])]); setShowRulesModal(true) }}
|
|
381
|
-
title="视觉规则(从道藏提炼,生图/生视频时强制注入)"
|
|
382
|
-
>
|
|
383
|
-
📋 视觉规则{rules.length > 0 ? `(${rules.length})` : ''}
|
|
384
|
-
</button>
|
|
385
|
-
<button
|
|
386
|
-
type="button"
|
|
387
|
-
className={css.button + ' ' + css.buttonSmall}
|
|
388
|
-
disabled={busy}
|
|
389
|
-
onClick={async () => {
|
|
390
|
-
if (activePlan === null) return
|
|
391
|
-
if (!window.confirm('删除方案「' + activePlan.name + '」?将同时清除该方案的分镜、提示词和角色卡(场景库保留)。')) return
|
|
392
|
-
if (await mutate({ op: 'remove', id: activePlan.id })) {
|
|
393
|
-
setAutoGenResult(null)
|
|
394
|
-
setAutoGenChapter(null)
|
|
395
|
-
setExported(false)
|
|
396
|
-
setError('')
|
|
397
|
-
await refresh()
|
|
398
|
-
setView(1)
|
|
399
|
-
}
|
|
400
|
-
}}
|
|
401
|
-
>
|
|
402
|
-
🗑 删除当前方案
|
|
403
|
-
</button>
|
|
404
|
-
</div>
|
|
405
|
-
</div>
|
|
406
|
-
{/* 当前章节 + 选章节(长篇友好:不横排,用面板搜索/分页/按状态) */}
|
|
407
|
-
{effectiveView !== 1 && (
|
|
408
|
-
<div style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-8)', padding: 'var(--nf-space-6) 0', borderTop: '1px solid var(--nf-border)', marginTop: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
409
|
-
<span style={{ fontSize: 'var(--nf-fs-12)', color: 'var(--nf-text-dim, #888)' }}>当前章节:</span>
|
|
410
|
-
<span className={css.badge} style={{ fontSize: 'var(--nf-fs-13)', padding: 'var(--nf-space-2) var(--nf-space-8)' }}>
|
|
411
|
-
{effectiveChapter !== null
|
|
412
|
-
? '第' + effectiveChapter + '章 ' + (chapters.find(c => c.no === effectiveChapter)?.title ?? '')
|
|
413
|
-
: '(未选章节)'}
|
|
414
|
-
</span>
|
|
415
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => { setShowChapterPicker(true) }}>📚 选章节</button>
|
|
416
|
-
<span className={css.meta}>共 {writtenChapters.length} 章可出片</span>
|
|
417
|
-
</div>
|
|
418
|
-
)}
|
|
419
|
-
<FlowGuide project={project} onNavigate={navigateFlow} exported={exported} />
|
|
420
|
-
</div>
|
|
421
|
-
|
|
422
|
-
{error !== '' && <div className={css.importError}>{error}</div>}
|
|
423
|
-
|
|
424
|
-
{/* ① 创建方案页 */}
|
|
425
|
-
{effectiveView === 1 && (
|
|
426
|
-
<div className={css.card}>
|
|
427
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
428
|
-
<span className={css.cardTitle}>① 创建方案 · 选择视觉风格</span>
|
|
429
|
-
{plans.length > 0 && <button type="button" className={css.button} disabled={busy} onClick={() => { setView(2) }}>返回工作台</button>}
|
|
430
|
-
</div>
|
|
431
|
-
<span className={css.meta}>选一个基底风格,下方可叠加影视滤镜(可选)。此后所有提示词按此风格生成。</span>
|
|
432
|
-
|
|
433
|
-
<div className={css.row} style={{ gap: 'var(--nf-space-8)', flexWrap: 'wrap', margin: '10px 0' }}>
|
|
434
|
-
{STYLE_CATEGORIES.map(c => (
|
|
435
|
-
<button key={c.id} type="button" className={css.button + (cat === c.id ? ' ' + css.buttonPrimary : '')} style={{ flex: 1, minWidth: 120 }} onClick={() => { setCat(c.id) }}>
|
|
436
|
-
{c.icon} {c.label}({baseStyles.filter(s => s.category === c.id).length})
|
|
437
|
-
</button>
|
|
438
|
-
))}
|
|
439
|
-
</div>
|
|
440
|
-
<div className={css.meta}>{STYLE_CATEGORIES.find(c => c.id === cat)?.desc}</div>
|
|
441
|
-
|
|
442
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 1fr))', gap: 'var(--nf-space-12)', margin: '10px 0' }}>
|
|
443
|
-
{baseStyles.filter(s => s.category === cat).map(s => (
|
|
444
|
-
<StyleCard key={s.id} style={s} selected={selStyle === s.id} onClick={() => { pickStyle(s.id) }} />
|
|
445
|
-
))}
|
|
446
|
-
</div>
|
|
447
|
-
|
|
448
|
-
{filterStyles.length > 0 && (
|
|
449
|
-
<div className={css.field}>
|
|
450
|
-
<label className={css.fieldLabel}>叠加滤镜(可选)</label>
|
|
451
|
-
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 'var(--nf-space-6)' }}>
|
|
452
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall + (selFilter === '' ? ' ' + css.buttonPrimary : '')} onClick={() => { setSelFilter('') }}>无</button>
|
|
453
|
-
{filterStyles.map(s => (
|
|
454
|
-
<button key={s.id} type="button" className={css.button + ' ' + css.buttonSmall + (selFilter === s.id ? ' ' + css.buttonPrimary : '')} onClick={() => { setSelFilter(s.id) }}>{s.name}</button>
|
|
455
|
-
))}
|
|
456
|
-
</div>
|
|
457
|
-
</div>
|
|
458
|
-
)}
|
|
459
|
-
|
|
460
|
-
<div className={css.field}>
|
|
461
|
-
<label className={css.fieldLabel}>题材(可选,注入对应题材规则)</label>
|
|
462
|
-
<select className={css.input} value={selGenre} onChange={e => { setSelGenre(e.target.value) }}>
|
|
463
|
-
<option value="">通用(不限定)</option>
|
|
464
|
-
{GENRES.map(g => <option key={g.id} value={g.id}>{g.label}</option>)}
|
|
465
|
-
</select>
|
|
466
|
-
</div>
|
|
467
|
-
<div className={css.field}>
|
|
468
|
-
<label className={css.fieldLabel}>方案名</label>
|
|
469
|
-
<input className={css.input} value={planName} placeholder="例如:《保质期》3D 皮克斯版" onChange={e => { setPlanName(e.target.value) }} />
|
|
470
|
-
</div>
|
|
471
|
-
<div className={css.row}>
|
|
472
|
-
<button type="button" className={css.button + ' ' + css.buttonPrimary} disabled={busy || selStyle === '' || planName.trim() === ''} onClick={() => { void createPlan() }}>
|
|
473
|
-
{busy ? '创建中…' : '🎬 创建方案'}
|
|
474
|
-
</button>
|
|
475
|
-
<span className={css.meta}>{selStyle !== '' ? '已选:' + (findStyle(selStyle)?.name ?? selStyle) : '点击卡片选择基底风格'}</span>
|
|
476
|
-
</div>
|
|
477
|
-
</div>
|
|
478
|
-
)}
|
|
479
|
-
|
|
480
|
-
{/* ② 一键生成页 */}
|
|
481
|
-
{effectiveView === 2 && (
|
|
482
|
-
<div className={css.card}>
|
|
483
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
484
|
-
<span className={css.cardTitle}>② 一键生成 · 剧情+分镜+角色+提示词</span>
|
|
485
|
-
</div>
|
|
486
|
-
<span className={css.meta} style={{ display: 'block', marginTop: 'var(--nf-space-4)' }}>
|
|
487
|
-
选一章,点一键生成——系统自动拆剧情、生镜头、提名角色、自动导入分级、写视频提示词。完成后直接去定妆。
|
|
488
|
-
</span>
|
|
489
|
-
|
|
490
|
-
<div className={css.row} style={{ alignItems: 'flex-end', flexWrap: 'wrap', marginTop: 'var(--nf-space-8)' }}>
|
|
491
|
-
<div className={css.field} style={{ flex: 1, minWidth: 200 }}>
|
|
492
|
-
<label className={css.fieldLabel}>选择章节</label>
|
|
493
|
-
<select
|
|
494
|
-
className={css.input}
|
|
495
|
-
value={autoGenChapter ?? ''}
|
|
496
|
-
onChange={e => { setAutoGenChapter(e.target.value !== '' ? Number(e.target.value) : null) }}
|
|
497
|
-
>
|
|
498
|
-
<option value="">(选择章节)</option>
|
|
499
|
-
{chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.status !== 'error').map(c => (
|
|
500
|
-
<option key={c.no} value={c.no}>第{c.no}章 {c.title}</option>
|
|
501
|
-
))}
|
|
502
|
-
</select>
|
|
503
|
-
</div>
|
|
504
|
-
<button
|
|
505
|
-
type="button"
|
|
506
|
-
className={css.button + ' ' + css.buttonPrimary}
|
|
507
|
-
disabled={autoGenChapter === null || autoGenBusy}
|
|
508
|
-
onClick={() => { void autoGenerate() }}
|
|
509
|
-
>
|
|
510
|
-
{autoGenBusy ? '生成中…(约1-2分钟)' : '⚡ 一键生成'}
|
|
511
|
-
</button>
|
|
512
|
-
</div>
|
|
513
|
-
|
|
514
|
-
{chapters.filter(c => c.status !== 'pending' && c.status !== 'generating' && c.status !== 'error').length === 0 && (
|
|
515
|
-
<div style={{ marginTop: 'var(--nf-space-8)', padding: 'var(--nf-space-10)', background: 'rgba(250,173,20,0.08)', border: '1px solid rgba(250,173,20,0.3)', borderRadius: 'var(--nf-radius-8)' }}>
|
|
516
|
-
<span style={{ fontSize: 'var(--nf-fs-13)', color: '#b45309' }}>⚠️ 暂无已完成章节。请先在「小说工坊」写完章节,或点顶部「🔄 刷新」重新加载。</span>
|
|
517
|
-
</div>
|
|
518
|
-
)}
|
|
519
|
-
|
|
520
|
-
{autoGenBusy && (
|
|
521
|
-
<div style={{ marginTop: 'var(--nf-space-8)', padding: 'var(--nf-space-8)', background: 'rgba(139,200,234,0.1)', borderRadius: 'var(--nf-radius-8)' }}>
|
|
522
|
-
<span className={css.meta}>正在生成:拆剧情 → 生镜头 → 提名角色 → 自动导入 → 写提示词…</span>
|
|
523
|
-
</div>
|
|
524
|
-
)}
|
|
525
|
-
|
|
526
|
-
{autoGenResult !== null && !autoGenBusy && (
|
|
527
|
-
<div style={{ marginTop: 'var(--nf-space-10)', border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-10)' }}>
|
|
528
|
-
<span className={css.cardTitle} style={{ fontSize: 'var(--nf-fs-14)' }}>✅ 生成完成</span>
|
|
529
|
-
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: 'var(--nf-space-8)', marginTop: 'var(--nf-space-6)' }}>
|
|
530
|
-
<div><div style={{ fontSize: 'var(--nf-fs-20)', fontWeight: 600 }}>{autoGenResult.shotCount}</div><span className={css.meta}>镜头数</span></div>
|
|
531
|
-
<div><div style={{ fontSize: 'var(--nf-fs-20)', fontWeight: 600 }}>{autoGenResult.importedRoles}</div><span className={css.meta}>导入角色</span></div>
|
|
532
|
-
<div><div style={{ fontSize: 'var(--nf-fs-20)', fontWeight: 600, color: '#52C41A' }}>{autoGenResult.needMakeupRoles}</div><span className={css.meta}>需定妆</span></div>
|
|
533
|
-
<div><div style={{ fontSize: 'var(--nf-fs-20)', fontWeight: 600, color: '#888' }}>{autoGenResult.extraRoles}</div><span className={css.meta}>路人(不做图)</span></div>
|
|
534
|
-
<div><div style={{ fontSize: 'var(--nf-fs-20)', fontWeight: 600, color: '#FAAD14' }}>{autoGenResult.pendingCandidates}</div><span className={css.meta}>待确认</span></div>
|
|
535
|
-
</div>
|
|
536
|
-
{autoGenResult.pendingCandidates > 0 && (
|
|
537
|
-
<div style={{ marginTop: 'var(--nf-space-6)', padding: 'var(--nf-space-8)', background: 'var(--nf-warning-soft, #fffbeb)', borderRadius: 'var(--nf-radius-8)', border: '1px solid var(--nf-warning-border, #fde68a)' }}>
|
|
538
|
-
<span style={{ color: '#b45309', fontSize: 'var(--nf-fs-12)', fontWeight: 600 }}>⚠️ {autoGenResult.pendingCandidates} 个角色待确认:</span>
|
|
539
|
-
<span style={{ color: '#b45309', fontSize: 'var(--nf-fs-12)' }}>{autoGenResult.pendingRoleNames.join('、')}</span>
|
|
540
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} style={{ marginLeft: 'var(--nf-space-6)' }} onClick={() => { setRoleFocus('import'); setView(3) }}>去处理 →</button>
|
|
541
|
-
</div>
|
|
542
|
-
)}
|
|
543
|
-
<div className={css.row} style={{ marginTop: 'var(--nf-space-10)' }}>
|
|
544
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall + ' ' + css.buttonPrimary} onClick={() => { setRoleFocus('cards'); setView(3) }}>下一步:③ 角色定妆 →</button>
|
|
545
|
-
|
|
546
|
-
</div>
|
|
547
|
-
</div>
|
|
548
|
-
)}
|
|
549
|
-
|
|
550
|
-
</div>
|
|
551
|
-
)}
|
|
552
|
-
{/* ⑤⑥ 角色页(同一实例,focus 切换) */}
|
|
553
|
-
{(effectiveView === 3) && (
|
|
554
|
-
<MangaRoleLibrary
|
|
555
|
-
api={api}
|
|
556
|
-
project={project}
|
|
557
|
-
refresh={() => refresh()}
|
|
558
|
-
styleId={activePlan?.styleId}
|
|
559
|
-
filterId={activePlan?.filterId}
|
|
560
|
-
focus={roleFocus}
|
|
561
|
-
showCards={true}
|
|
562
|
-
chapterNo={effectiveChapter}
|
|
563
|
-
onProgress={onProgress}
|
|
564
|
-
/>
|
|
565
|
-
)}
|
|
566
|
-
|
|
567
|
-
{/* ⑧ 场景库页 */}
|
|
568
|
-
{effectiveView === 4 && (
|
|
569
|
-
<SceneLibrary
|
|
570
|
-
api={api}
|
|
571
|
-
project={project}
|
|
572
|
-
refresh={() => refresh()}
|
|
573
|
-
styleId={activePlan?.styleId}
|
|
574
|
-
filterId={activePlan?.filterId}
|
|
575
|
-
chapterNo={effectiveChapter}
|
|
576
|
-
onProgress={onProgress}
|
|
577
|
-
/>
|
|
578
|
-
)}
|
|
579
|
-
|
|
580
|
-
{/* 道具库页 */}
|
|
581
|
-
{effectiveView === 6 && (
|
|
582
|
-
<PropLibrary
|
|
583
|
-
api={api}
|
|
584
|
-
project={project}
|
|
585
|
-
refresh={() => refresh()}
|
|
586
|
-
onProgress={onProgress}
|
|
587
|
-
/>
|
|
588
|
-
)}
|
|
589
|
-
|
|
590
|
-
{/* ⑥ 分镜·提示词(专注提示词 + 导出存档) */}
|
|
591
|
-
{effectiveView === 5 && (
|
|
592
|
-
<>
|
|
593
|
-
<StoryboardTab
|
|
594
|
-
api={api}
|
|
595
|
-
project={project}
|
|
596
|
-
chapters={chapters}
|
|
597
|
-
onProjectChanged={onProjectChanged}
|
|
598
|
-
styleId={activePlan?.styleId}
|
|
599
|
-
filterId={activePlan?.filterId}
|
|
600
|
-
mode="prompts"
|
|
601
|
-
onProgress={onProgress}
|
|
602
|
-
/>
|
|
603
|
-
<div className={css.card}>
|
|
604
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
605
|
-
<span className={css.cardTitle}>📦 导出整章(存档到资产库 + 复制剪贴板)</span>
|
|
606
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall + ' ' + css.buttonPrimary} disabled={effectiveChapter === null} onClick={() => { if (effectiveChapter !== null) exportMangaPackage(effectiveChapter) }}>
|
|
607
|
-
{exported ? '✅ 已保存+复制' : '📋 导出即梦素材包'}
|
|
608
|
-
</button>
|
|
609
|
-
</div>
|
|
610
|
-
<span className={css.meta}>导出的素材包:角色/场景提示词 + 逐镜提示词(时间轴/台词)+ 整集脚本 + 步骤卡,整章落盘资产库并复制。无提示词的章节先在上面「生成视频提示词」。</span>
|
|
611
|
-
</div>
|
|
612
|
-
</>
|
|
613
|
-
)}
|
|
614
|
-
|
|
615
|
-
{/* 章节选择面板(长篇友好:搜索 + 状态) */}
|
|
616
|
-
{showChapterPicker && (
|
|
617
|
-
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--nf-space-16)' }} onClick={() => { setShowChapterPicker(false); setChapterQuery('') }}>
|
|
618
|
-
<div style={{ background: 'var(--nf-bg, #fff)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-16)', maxWidth: 640, width: '100%', maxHeight: '80vh', overflow: 'auto', border: '1px solid var(--nf-border, #e4e3dd)' }} onClick={e => e.stopPropagation()}>
|
|
619
|
-
<div className={css.row} style={{ justifyContent: 'space-between', marginBottom: 'var(--nf-space-12)' }}>
|
|
620
|
-
<span className={css.cardTitle}>📚 选择章节</span>
|
|
621
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => { setShowChapterPicker(false); setChapterQuery('') }}>X</button>
|
|
622
|
-
</div>
|
|
623
|
-
<input className={css.input} style={{ marginBottom: 'var(--nf-space-8)' }} placeholder="输入章节号或标题搜索…" value={chapterQuery} onChange={e => { setChapterQuery(e.target.value) }} />
|
|
624
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)' }}>
|
|
625
|
-
{writtenChapters.filter(c => {
|
|
626
|
-
const q = chapterQuery.trim().toLowerCase()
|
|
627
|
-
if (q === '') return true
|
|
628
|
-
return String(c.no).includes(q) || (c.title ?? '').toLowerCase().includes(q)
|
|
629
|
-
}).map(c => {
|
|
630
|
-
const status = chapterStatus.get(c.no) ?? 'todo'
|
|
631
|
-
const isActive = effectiveChapter === c.no
|
|
632
|
-
const icon = status === 'done' ? '✅' : status === 'doing' ? '🔵' : '⚪'
|
|
633
|
-
const label = status === 'done' ? '已完成' : status === 'doing' ? '进行中(待提示词)' : '未开始'
|
|
634
|
-
return (
|
|
635
|
-
<button key={c.no} type="button" style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 'var(--nf-space-8)', padding: 'var(--nf-space-6) var(--nf-space-10)', borderRadius: 'var(--nf-radius-8)', border: isActive ? '2px solid var(--nf-accent)' : '1px solid var(--nf-border)', background: isActive ? 'var(--nf-accent-soft, rgba(139,200,234,0.15))' : 'var(--nf-bg)', cursor: 'pointer', textAlign: 'left' }} onClick={() => { setCurrentChapter(c.no); setAutoGenChapter(c.no); setShowChapterPicker(false); setChapterQuery('') }}>
|
|
636
|
-
<span style={{ fontSize: 'var(--nf-fs-13)' }}>第{c.no}章 {c.title ?? ''}</span>
|
|
637
|
-
<span style={{ fontSize: 'var(--nf-fs-12)', color: 'var(--nf-text-dim, #888)' }}>{icon} {label}</span>
|
|
638
|
-
</button>
|
|
639
|
-
)
|
|
640
|
-
})}
|
|
641
|
-
</div>
|
|
642
|
-
{writtenChapters.length === 0 && <span className={css.meta}>暂无已写章节,请先在「小说工坊」写章节。</span>}
|
|
643
|
-
</div>
|
|
644
|
-
</div>
|
|
645
|
-
)}
|
|
646
|
-
|
|
647
|
-
{/* 视觉规则悬浮窗 */}
|
|
648
|
-
{showRulesModal && (
|
|
649
|
-
<div style={{ position: 'fixed', inset: 0, background: 'rgba(0,0,0,0.5)', zIndex: 1000, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 'var(--nf-space-16)' }} onClick={() => setShowRulesModal(false)}>
|
|
650
|
-
<div style={{ background: 'var(--nf-bg, #fff)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-16)', maxWidth: 560, width: '100%', maxHeight: '80vh', overflow: 'auto', border: '1px solid var(--nf-border, #e4e3dd)' }} onClick={e => e.stopPropagation()}>
|
|
651
|
-
<div className={css.row} style={{ justifyContent: 'space-between', marginBottom: 'var(--nf-space-12)' }}>
|
|
652
|
-
<span className={css.cardTitle}>视觉规则</span>
|
|
653
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => setShowRulesModal(false)}>X</button>
|
|
654
|
-
</div>
|
|
655
|
-
<span className={css.meta} style={{ display: 'block', marginBottom: 'var(--nf-space-12)' }}>从道藏提炼,生图/生视频时强制注入,防止模型画错反常识设定。最多12条,每条80字内。</span>
|
|
656
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-8)' }}>
|
|
657
|
-
{rulesDraft.map((r, i) => (
|
|
658
|
-
<div key={i} className={css.row} style={{ gap: 'var(--nf-space-8)' }}>
|
|
659
|
-
<span style={{ minWidth: 24, color: 'var(--nf-text-secondary, #6b7280)', fontSize: 'var(--nf-fs-12)' }}>{i + 1}.</span>
|
|
660
|
-
<input
|
|
661
|
-
type="text"
|
|
662
|
-
className={css.input}
|
|
663
|
-
style={{ flex: 1 }}
|
|
664
|
-
value={r}
|
|
665
|
-
maxLength={80}
|
|
666
|
-
onChange={e => { const next = [...rulesDraft]; next[i] = e.target.value; setRulesDraft(next) }}
|
|
667
|
-
/>
|
|
668
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => setRulesDraft(rulesDraft.filter((_, idx) => idx !== i))}>删</button>
|
|
669
|
-
</div>
|
|
670
|
-
))}
|
|
671
|
-
{rulesDraft.length === 0 && <span className={css.meta}>暂无规则,点重新提炼从道藏生成,或添加手动添加。</span>}
|
|
672
|
-
</div>
|
|
673
|
-
<div className={css.row} style={{ marginTop: 'var(--nf-space-16)', gap: 'var(--nf-space-8)', flexWrap: 'wrap' }}>
|
|
674
|
-
<button type="button" className={css.button + ' ' + css.buttonSmall} onClick={() => setRulesDraft([...rulesDraft, ''])}>+ 添加</button>
|
|
675
|
-
<button
|
|
676
|
-
type="button"
|
|
677
|
-
className={css.button + ' ' + css.buttonSmall}
|
|
678
|
-
disabled={busy}
|
|
679
|
-
onClick={async () => {
|
|
680
|
-
setBusy(true)
|
|
681
|
-
try {
|
|
682
|
-
const res = await api.visualRules({ op: 'extract' })
|
|
683
|
-
setRulesDraft(res.rules ?? [])
|
|
684
|
-
await refresh()
|
|
685
|
-
} catch (err) { setError((err as Error).message) }
|
|
686
|
-
finally { setBusy(false) }
|
|
687
|
-
}}
|
|
688
|
-
>
|
|
689
|
-
重新提炼
|
|
690
|
-
</button>
|
|
691
|
-
<button
|
|
692
|
-
type="button"
|
|
693
|
-
className={css.button + ' ' + css.buttonSmall + ' ' + css.buttonPrimary}
|
|
694
|
-
disabled={busy}
|
|
695
|
-
onClick={async () => {
|
|
696
|
-
setBusy(true)
|
|
697
|
-
try {
|
|
698
|
-
const cleaned = rulesDraft.map(r => r.trim()).filter(r => r !== '').slice(0, 12)
|
|
699
|
-
await api.visualRules({ op: 'save', rules: cleaned })
|
|
700
|
-
setRulesDraft(cleaned)
|
|
701
|
-
await refresh()
|
|
702
|
-
setShowRulesModal(false)
|
|
703
|
-
} catch (err) { setError((err as Error).message) }
|
|
704
|
-
finally { setBusy(false) }
|
|
705
|
-
}}
|
|
706
|
-
>
|
|
707
|
-
保存
|
|
708
|
-
</button>
|
|
709
|
-
</div>
|
|
710
|
-
</div>
|
|
711
|
-
</div>
|
|
712
|
-
)}
|
|
713
|
-
</div>
|
|
714
|
-
)
|
|
715
|
-
}
|