@waterwx/dsh-novel-forge 2.1.2-alpha → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -368
- package/lib/client.js +19568 -21381
- package/lib/index.js +5158 -3958
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +80 -12
- package/lib/types/assets.d.ts +7 -9
- package/lib/types/assistant.d.ts +25 -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 +49 -29
- 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 +12 -14
- package/lib/types/client/locales.d.ts +12 -67
- package/lib/types/client/mount.d.ts +0 -2
- package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssetsTab.d.ts +1 -1
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
- package/lib/types/client/panel/AuthorHome.d.ts +12 -12
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +1 -1
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
- package/lib/types/client/panel/ImportModal.d.ts +1 -1
- package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
- package/lib/types/client/panel/ModelManager.d.ts +2 -2
- package/lib/types/client/panel/NovelPanel.d.ts +7 -2
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
- package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
- package/lib/types/client/panel/ReaderView.d.ts +1 -1
- package/lib/types/client/panel/RunPanel.d.ts +1 -1
- package/lib/types/client/panel/SettingsView.d.ts +13 -10
- package/lib/types/client/panel/ShelfView.d.ts +11 -2
- 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/WorldTab.d.ts +1 -1
- 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 +122 -103
- package/lib/types/home.d.ts +11 -0
- package/lib/types/index.d.ts +21 -13
- package/lib/types/llm-live.d.ts +62 -36
- package/lib/types/llm-retry.d.ts +38 -0
- package/lib/types/market-radar-scan.d.ts +0 -1
- package/lib/types/market-radar-sources.d.ts +1 -1
- 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 +2 -27
- package/lib/types/protocol.d.ts +479 -595
- 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 +31 -0
- 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/style-fingerprint.d.ts +78 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +41 -28
- 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/src/ai-scan.d.ts +0 -36
- package/lib/types/src/assets.d.ts +0 -37
- package/lib/types/src/assistant.d.ts +0 -45
- package/lib/types/src/author-assets.d.ts +0 -25
- package/lib/types/src/bookshelf.d.ts +0 -42
- package/lib/types/src/client/api.d.ts +0 -234
- package/lib/types/src/client/docx.d.ts +0 -15
- package/lib/types/src/client/index.d.ts +0 -14
- package/lib/types/src/client/locales.d.ts +0 -212
- package/lib/types/src/client/mount.d.ts +0 -9
- package/lib/types/src/client/panel/AdaptModeView.d.ts +0 -5
- package/lib/types/src/client/panel/AssetsTab.d.ts +0 -12
- package/lib/types/src/client/panel/AssistantTab.d.ts +0 -7
- package/lib/types/src/client/panel/AuthorAssetsView.d.ts +0 -4
- package/lib/types/src/client/panel/AuthorHome.d.ts +0 -32
- package/lib/types/src/client/panel/BookshelfBar.d.ts +0 -11
- package/lib/types/src/client/panel/CreateBookView.d.ts +0 -9
- package/lib/types/src/client/panel/FlowGuide.d.ts +0 -22
- package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +0 -4
- package/lib/types/src/client/panel/ImportModal.d.ts +0 -7
- package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +0 -20
- package/lib/types/src/client/panel/MangaWorkspace.d.ts +0 -12
- package/lib/types/src/client/panel/ModelManager.d.ts +0 -14
- package/lib/types/src/client/panel/NovelPanel.d.ts +0 -13
- package/lib/types/src/client/panel/ProgressConsole.d.ts +0 -17
- package/lib/types/src/client/panel/ProgressHomeView.d.ts +0 -5
- package/lib/types/src/client/panel/PropLibrary.d.ts +0 -8
- package/lib/types/src/client/panel/ReaderView.d.ts +0 -8
- package/lib/types/src/client/panel/ReasoningSection.d.ts +0 -10
- package/lib/types/src/client/panel/RunPanel.d.ts +0 -5
- package/lib/types/src/client/panel/SceneLibrary.d.ts +0 -16
- package/lib/types/src/client/panel/SettingsView.d.ts +0 -16
- package/lib/types/src/client/panel/ShelfView.d.ts +0 -15
- package/lib/types/src/client/panel/StoryboardTab.d.ts +0 -19
- package/lib/types/src/client/panel/StyleCard.d.ts +0 -8
- package/lib/types/src/client/panel/StyleGallery.d.ts +0 -1
- package/lib/types/src/client/panel/WorldTab.d.ts +0 -10
- package/lib/types/src/client/panel/controller.d.ts +0 -19
- package/lib/types/src/client/panel/helpers.d.ts +0 -18
- package/lib/types/src/client/panel/views.d.ts +0 -77
- package/lib/types/src/client/sidebar-entry.d.ts +0 -13
- package/lib/types/src/client/text.d.ts +0 -9
- package/lib/types/src/comic-presets.d.ts +0 -12
- package/lib/types/src/docx.d.ts +0 -19
- package/lib/types/src/engine.d.ts +0 -402
- package/lib/types/src/index.d.ts +0 -79
- package/lib/types/src/manga-genre-rules.d.ts +0 -22
- package/lib/types/src/novel-context.d.ts +0 -80
- package/lib/types/src/prompt-utils.d.ts +0 -35
- package/lib/types/src/protocol.d.ts +0 -2176
- package/lib/types/src/review-policy.d.ts +0 -97
- package/lib/types/src/routes.d.ts +0 -29
- package/lib/types/src/run.d.ts +0 -32
- package/lib/types/src/shot-language.d.ts +0 -46
- package/lib/types/src/story-beat-language.d.ts +0 -35
- package/lib/types/src/style-library.d.ts +0 -33
- package/lib/types/story-beat-language.d.ts +0 -35
- package/lib/types/style-library.d.ts +0 -33
- package/src/ai-scan.ts +0 -179
- package/src/assets.ts +0 -1774
- package/src/assistant.ts +0 -915
- package/src/author-assets.ts +0 -147
- package/src/bookshelf.ts +0 -167
- package/src/client/api.ts +0 -716
- 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/llmLive.ts +0 -101
- 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 -535
- package/src/client/panel/AssetsTab.tsx +0 -458
- package/src/client/panel/AssistantTab.tsx +0 -339
- package/src/client/panel/AuthorAssetsView.tsx +0 -208
- package/src/client/panel/AuthorHome.tsx +0 -234
- package/src/client/panel/BookAnalysisView.tsx +0 -60
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/client/panel/CreateBookView.tsx +0 -371
- package/src/client/panel/DirectorView.tsx +0 -200
- package/src/client/panel/FlowGuide.tsx +0 -84
- package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
- package/src/client/panel/IdeaInspirationView.tsx +0 -71
- package/src/client/panel/ImportModal.tsx +0 -303
- package/src/client/panel/KnowledgeBaseView.tsx +0 -73
- package/src/client/panel/LiveFeedLog.tsx +0 -76
- package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
- package/src/client/panel/MangaWorkspace.tsx +0 -715
- package/src/client/panel/MarketRadarView.tsx +0 -374
- package/src/client/panel/ModelManager.tsx +0 -205
- package/src/client/panel/NovelPanel.tsx +0 -4721
- package/src/client/panel/ProgressConsole.tsx +0 -63
- 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 -289
- 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 -4359
- 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 -5636
- package/src/global-assets.ts +0 -59
- package/src/index.ts +0 -346
- package/src/llm-live.ts +0 -68
- package/src/manga-genre-rules.ts +0 -87
- package/src/market-radar-scan.ts +0 -41
- package/src/market-radar-sources.ts +0 -173
- package/src/novel-context.ts +0 -287
- package/src/prompt-utils.ts +0 -73
- package/src/prompting.ts +0 -159
- package/src/protocol.ts +0 -2565
- package/src/review-policy.ts +0 -161
- package/src/routes.ts +0 -3939
- package/src/run.ts +0 -338
- package/src/shot-language.ts +0 -181
- package/src/story-beat-language.ts +0 -130
- package/src/style-library.ts +0 -84
package/src/run.ts
DELETED
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 生产单(Production Run):批量章节生产的标准执行器。
|
|
3
|
-
* 职责:计划补足 → 逐章生成(完整质量门)→ 被拒分级处理(豁免/修订+验证/待人工)→ 断点续跑。
|
|
4
|
-
* 与路由层解耦:只依赖 engine 导出函数与磁盘状态,自身不碰 HTTP。
|
|
5
|
-
* 串行纪律:单例执行器 + working 锁;每章重新从磁盘加载,写前合并易变字段。
|
|
6
|
-
*/
|
|
7
|
-
import { existsSync, mkdirSync, readFileSync, copyFileSync, writeFileSync } from 'node:fs'
|
|
8
|
-
import { join } from 'node:path'
|
|
9
|
-
import type { Context } from '@deepseek-ai/cordis'
|
|
10
|
-
import type { ChapterPlan, NovelConfig, ProjectState, ReviewReport, RunState } from './protocol.ts'
|
|
11
|
-
import {
|
|
12
|
-
loadProject,
|
|
13
|
-
saveProject,
|
|
14
|
-
mergeVolatileFromDisk,
|
|
15
|
-
planChapters,
|
|
16
|
-
generateChapterStream,
|
|
17
|
-
summarizeAndExtractFacts,
|
|
18
|
-
markForeshadowPlanted,
|
|
19
|
-
reviewChapter,
|
|
20
|
-
reviewChapterText,
|
|
21
|
-
authorReviewChapter,
|
|
22
|
-
autoLinkPlotlines,
|
|
23
|
-
readChapterFile,
|
|
24
|
-
rewriteChapterStream,
|
|
25
|
-
chapterFileName,
|
|
26
|
-
} from './engine.ts'
|
|
27
|
-
import { loadBookshelf } from './bookshelf.ts'
|
|
28
|
-
|
|
29
|
-
/** 生产单 checkpoint 文件名(放在书目录下)。 */
|
|
30
|
-
function runStateFile(outputDir: string): string {
|
|
31
|
-
return join(outputDir, 'run-state.json')
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export class ProductionRunner {
|
|
35
|
-
private state: RunState | null = null
|
|
36
|
-
private working = false
|
|
37
|
-
private pauseRequested = false
|
|
38
|
-
private stopRequested = false
|
|
39
|
-
/** 生产单绑定目录(start 时快照;所有读写固定用它,防止运行中切书导致写错目录)。 */
|
|
40
|
-
private bookDir: string | null = null
|
|
41
|
-
|
|
42
|
-
constructor(private deps: { ctx: Context; getConfig: () => NovelConfig }) {}
|
|
43
|
-
|
|
44
|
-
/** 当前生产单状态(内存优先;web 重启后从磁盘恢复)。 */
|
|
45
|
-
status(): RunState | null {
|
|
46
|
-
if (this.state !== null) return this.state
|
|
47
|
-
// 重启恢复:先找当前激活书目录,再扫书架所有书目录(防切书后找不到)。
|
|
48
|
-
const candidates: string[] = [this.deps.getConfig().outputDir]
|
|
49
|
-
const bookshelf = loadBookshelf()
|
|
50
|
-
for (const b of bookshelf.books) if (!candidates.includes(b.outputDir)) candidates.push(b.outputDir)
|
|
51
|
-
for (const outputDir of candidates) {
|
|
52
|
-
const file = runStateFile(outputDir)
|
|
53
|
-
if (!existsSync(file)) continue
|
|
54
|
-
try {
|
|
55
|
-
const raw = readFileSync(file, 'utf8')
|
|
56
|
-
const parsed = JSON.parse(raw) as RunState
|
|
57
|
-
if (parsed?.runId === undefined) continue
|
|
58
|
-
// 重启恢复:若之前 running,则视为可续跑(loop 未在跑)。
|
|
59
|
-
if (parsed.status === 'running') parsed.status = 'paused'
|
|
60
|
-
this.state = parsed
|
|
61
|
-
this.bookDir = outputDir
|
|
62
|
-
return this.state
|
|
63
|
-
} catch { /* 该目录 run-state 损坏则跳过 */ }
|
|
64
|
-
}
|
|
65
|
-
return null
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
private persist(): void {
|
|
69
|
-
if (this.state === null) return
|
|
70
|
-
const outputDir = this.bookDir ?? this.deps.getConfig().outputDir
|
|
71
|
-
mkdirSync(outputDir, { recursive: true })
|
|
72
|
-
writeFileSync(runStateFile(outputDir), JSON.stringify(this.state, null, 2), 'utf8')
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
private log(text: string): void {
|
|
76
|
-
if (this.state === null) return
|
|
77
|
-
this.state.log.push({ at: new Date().toISOString(), text })
|
|
78
|
-
if (this.state.log.length > 300) this.state.log = this.state.log.slice(-300)
|
|
79
|
-
this.state.updatedAt = new Date().toISOString()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** 启动/续跑生产单:startNo..endNo 区间,endNo 超出计划时先自动补计划。 */
|
|
83
|
-
async start(startNo: number, endNo: number, runDir?: string): Promise<RunState> {
|
|
84
|
-
const config = this.deps.getConfig()
|
|
85
|
-
if (this.working) throw new Error('生产单正在运行中,请先暂停或停止')
|
|
86
|
-
const outputDir = runDir ?? config.outputDir
|
|
87
|
-
let project = loadProject(outputDir)
|
|
88
|
-
if (project === undefined) throw new Error('输出目录中没有项目')
|
|
89
|
-
|
|
90
|
-
// 计划补足:目标区间超出已有计划 → 续写规划追加。
|
|
91
|
-
if (endNo > project.chapters.length) {
|
|
92
|
-
const need = endNo - project.chapters.length
|
|
93
|
-
this.log(`计划不足,追加 ${need} 章计划…`)
|
|
94
|
-
const chapters = await planChapters(this.deps.ctx, config, project, need, undefined, outputDir)
|
|
95
|
-
mergeVolatileFromDisk(outputDir, project)
|
|
96
|
-
project.chapters.push(...chapters)
|
|
97
|
-
project.updatedAt = new Date().toISOString()
|
|
98
|
-
saveProject(outputDir, project)
|
|
99
|
-
this.log(`计划已追加,全书 ${project.chapters.length} 章`)
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
this.state = {
|
|
103
|
-
runId: `run-${Date.now().toString(36)}`,
|
|
104
|
-
startNo,
|
|
105
|
-
endNo,
|
|
106
|
-
status: 'running',
|
|
107
|
-
currentNo: startNo,
|
|
108
|
-
stats: { generated: 0, revised: 0, exempted: 0, regenerated: 0, error: 0 },
|
|
109
|
-
pendingManual: [],
|
|
110
|
-
log: [],
|
|
111
|
-
startedAt: new Date().toISOString(),
|
|
112
|
-
updatedAt: new Date().toISOString(),
|
|
113
|
-
}
|
|
114
|
-
this.pauseRequested = false
|
|
115
|
-
this.stopRequested = false
|
|
116
|
-
// 绑定生产单目录(快照):运行期间切书不影响本单的读写目标。
|
|
117
|
-
this.bookDir = outputDir
|
|
118
|
-
this.working = true
|
|
119
|
-
this.persist()
|
|
120
|
-
void this.loop()
|
|
121
|
-
return this.state
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
pause(): void { this.pauseRequested = true }
|
|
125
|
-
resume(): void {
|
|
126
|
-
if (this.state === null || this.state.status !== 'paused') return
|
|
127
|
-
this.state.status = 'running'
|
|
128
|
-
this.working = true
|
|
129
|
-
this.pauseRequested = false
|
|
130
|
-
this.stopRequested = false
|
|
131
|
-
this.persist()
|
|
132
|
-
void this.loop()
|
|
133
|
-
}
|
|
134
|
-
stop(): void { this.stopRequested = true }
|
|
135
|
-
|
|
136
|
-
/** 主循环:从 currentNo 扫描到 endNo,逐章处理;支持暂停/停止。 */
|
|
137
|
-
private async loop(): Promise<void> {
|
|
138
|
-
// 全部读写固定使用生产单绑定目录(bookDir),绝不跟随当前激活书。
|
|
139
|
-
const config = this.deps.getConfig()
|
|
140
|
-
const outputDir = this.bookDir ?? config.outputDir
|
|
141
|
-
try {
|
|
142
|
-
while (this.state !== null) {
|
|
143
|
-
if (this.stopRequested) {
|
|
144
|
-
this.stopRequested = false
|
|
145
|
-
this.state.status = 'stopped'
|
|
146
|
-
this.log('生产单已停止')
|
|
147
|
-
this.persist()
|
|
148
|
-
break
|
|
149
|
-
}
|
|
150
|
-
if (this.pauseRequested) {
|
|
151
|
-
this.pauseRequested = false
|
|
152
|
-
this.state.status = 'paused'
|
|
153
|
-
this.log('生产单已暂停(随时可继续)')
|
|
154
|
-
this.persist()
|
|
155
|
-
break
|
|
156
|
-
}
|
|
157
|
-
const project = loadProject(outputDir)
|
|
158
|
-
if (project === undefined) {
|
|
159
|
-
this.state.status = 'error'
|
|
160
|
-
this.state.error = '项目丢失'
|
|
161
|
-
this.persist()
|
|
162
|
-
break
|
|
163
|
-
}
|
|
164
|
-
// 找下一个需要处理的章(approved 快进)。
|
|
165
|
-
let next: ChapterPlan | undefined
|
|
166
|
-
for (let no = this.state.currentNo; no <= this.state.endNo; no++) {
|
|
167
|
-
const ch = project.chapters.find(c => c.no === no)
|
|
168
|
-
if (ch === undefined) continue
|
|
169
|
-
if (ch.status === 'approved') { this.state.currentNo = no; continue }
|
|
170
|
-
next = ch
|
|
171
|
-
break
|
|
172
|
-
}
|
|
173
|
-
if (next === undefined) {
|
|
174
|
-
this.state.status = 'done'
|
|
175
|
-
this.log(`生产单完成:${this.state.startNo}-${this.state.endNo} 章处理完毕`)
|
|
176
|
-
this.persist()
|
|
177
|
-
break
|
|
178
|
-
}
|
|
179
|
-
this.state.currentNo = next.no
|
|
180
|
-
await this.processChapter(project, next)
|
|
181
|
-
this.persist()
|
|
182
|
-
}
|
|
183
|
-
} catch (error) {
|
|
184
|
-
if (this.state !== null) {
|
|
185
|
-
this.state.status = 'error'
|
|
186
|
-
this.state.error = (error as Error).message
|
|
187
|
-
this.log(`生产单异常:${(error as Error).message}`)
|
|
188
|
-
this.persist()
|
|
189
|
-
}
|
|
190
|
-
} finally {
|
|
191
|
-
this.working = false
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
private async processChapter(project: ProjectState, chapter: ChapterPlan): Promise<void> {
|
|
196
|
-
if (chapter.status === 'pending' || chapter.status === 'error' || chapter.status === 'written') {
|
|
197
|
-
await this.produce(project, chapter)
|
|
198
|
-
} else if (chapter.status === 'rejected') {
|
|
199
|
-
await this.handleRejected(project, chapter)
|
|
200
|
-
} else if (chapter.status === 'generating') {
|
|
201
|
-
// 中断残留:复位重生成。
|
|
202
|
-
chapter.status = 'pending'
|
|
203
|
-
await this.produce(project, chapter)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/** 完整质量门:生成 → 摘要+事实 → 伏笔标记 → 审稿 → 作者复盘。 */
|
|
208
|
-
private async produce(project: ProjectState, chapter: ChapterPlan): Promise<void> {
|
|
209
|
-
const { ctx, getConfig } = this.deps
|
|
210
|
-
const config = getConfig()
|
|
211
|
-
const outputDir = this.bookDir ?? config.outputDir
|
|
212
|
-
const no = chapter.no
|
|
213
|
-
const wasError = chapter.status === 'error'
|
|
214
|
-
chapter.status = 'generating'
|
|
215
|
-
chapter.error = undefined
|
|
216
|
-
chapter.generatingAt = new Date().toISOString()
|
|
217
|
-
mergeVolatileFromDisk(outputDir, project)
|
|
218
|
-
saveProject(outputDir, project)
|
|
219
|
-
this.log(`${wasError ? '重新生成' : '生成'} 第${no}章《${chapter.title}》…(模型 ${config.generateModel || config.model})`)
|
|
220
|
-
try {
|
|
221
|
-
for await (const step of generateChapterStream(ctx, config, project, outputDir, no)) { /* drain */ }
|
|
222
|
-
try { await summarizeAndExtractFacts(ctx, config, project, outputDir, no) } catch (e) { console.warn('[dsh-novel-forge] run summary/facts:', (e as Error).message) }
|
|
223
|
-
try { markForeshadowPlanted(project, outputDir, no) } catch (e) { console.warn('[dsh-novel-forge] run foreshadow:', (e as Error).message) }
|
|
224
|
-
if (config.autoReview ?? true) {
|
|
225
|
-
const report = await reviewChapter(ctx, config, project, outputDir, no)
|
|
226
|
-
if (this.state !== null) this.state.stats[wasError ? 'regenerated' : 'generated']++
|
|
227
|
-
this.log(`第${no}章 审稿 ${report.score}分 ${report.passed ? '✅ 通过' : '⚠️ 被拒'}(模型 ${config.reviewModel || config.model})`)
|
|
228
|
-
} else {
|
|
229
|
-
chapter.status = 'approved'
|
|
230
|
-
mergeVolatileFromDisk(outputDir, project)
|
|
231
|
-
saveProject(outputDir, project)
|
|
232
|
-
}
|
|
233
|
-
if (config.autoAuthorReview ?? true) {
|
|
234
|
-
try {
|
|
235
|
-
const body = readChapterFile(outputDir, chapter)
|
|
236
|
-
if (body !== undefined) {
|
|
237
|
-
let prevTail = ''
|
|
238
|
-
if (no > 1) {
|
|
239
|
-
const prev = project.chapters.find(c => c.no === no - 1)
|
|
240
|
-
if (prev !== undefined) prevTail = (readChapterFile(outputDir, prev) ?? '').replace(/^#.*$/m, '').trim().slice(-600)
|
|
241
|
-
}
|
|
242
|
-
const review = await authorReviewChapter(ctx, config, project, no, body, prevTail)
|
|
243
|
-
chapter.authorReview = review
|
|
244
|
-
if (review.advancedLines !== undefined) autoLinkPlotlines(project, no, review.advancedLines)
|
|
245
|
-
// 结果回灌:把本章状态变化/新线索写进编年录事实库,供后续计划与整本控制用。
|
|
246
|
-
const backfillFacts: string[] = [...(review.stateChanges ?? []), ...(review.clues ?? [])]
|
|
247
|
-
if (backfillFacts.length > 0) {
|
|
248
|
-
project.facts ??= []
|
|
249
|
-
for (const text of backfillFacts) project.facts.push({ chapterNo: no, text })
|
|
250
|
-
}
|
|
251
|
-
mergeVolatileFromDisk(outputDir, project)
|
|
252
|
-
saveProject(outputDir, project)
|
|
253
|
-
}
|
|
254
|
-
} catch (e) { console.warn('[dsh-novel-forge] run author review:', (e as Error).message) }
|
|
255
|
-
}
|
|
256
|
-
} catch (error) {
|
|
257
|
-
chapter.status = 'error'
|
|
258
|
-
chapter.error = (error as Error).message
|
|
259
|
-
mergeVolatileFromDisk(outputDir, project)
|
|
260
|
-
saveProject(outputDir, project)
|
|
261
|
-
if (this.state !== null) this.state.stats.error++
|
|
262
|
-
this.log(`第${no}章 失败:${(error as Error).message}`)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/** 被拒分级处理:无 high 豁免;有 high 按意见修订(最多 2 轮)+ 验证模式;仍不过 → 待人工。 */
|
|
267
|
-
private async handleRejected(project: ProjectState, chapter: ChapterPlan): Promise<void> {
|
|
268
|
-
const { ctx, getConfig } = this.deps
|
|
269
|
-
const config = getConfig()
|
|
270
|
-
const outputDir = this.bookDir ?? config.outputDir
|
|
271
|
-
const no = chapter.no
|
|
272
|
-
const report = chapter.review
|
|
273
|
-
const highs = (report?.issues ?? []).filter(i => i.severity === 'high')
|
|
274
|
-
if (highs.length === 0) {
|
|
275
|
-
chapter.status = 'approved'
|
|
276
|
-
project.updatedAt = new Date().toISOString()
|
|
277
|
-
saveProject(outputDir, project)
|
|
278
|
-
if (this.state !== null) this.state.stats.exempted++
|
|
279
|
-
this.log(`第${no}章 豁免通过(无 high)`)
|
|
280
|
-
return
|
|
281
|
-
}
|
|
282
|
-
this.log(`第${no}章 修订(${highs.length} 个 high)…`)
|
|
283
|
-
for (let round = 1; round <= 2; round++) {
|
|
284
|
-
const instr = '按审稿意见修订(优先处理):\n' + highs.map(h => `[${h.severity}] ${h.item}${h.suggestion !== '' ? `\n建议:${h.suggestion}` : ''}`).join('\n\n')
|
|
285
|
-
try {
|
|
286
|
-
for await (const step of rewriteChapterStream(ctx, config, project, outputDir, no, instr, undefined)) { /* drain */ }
|
|
287
|
-
} catch (error) {
|
|
288
|
-
this.log(`第${no}章 第${round}轮修订出错:${(error as Error).message}`)
|
|
289
|
-
continue
|
|
290
|
-
}
|
|
291
|
-
// rewrite 已写盘(pendingDraft + saveProject),重载拿最新草稿。
|
|
292
|
-
const fresh = loadProject(outputDir)
|
|
293
|
-
const freshCh = fresh?.chapters.find(c => c.no === no)
|
|
294
|
-
const draft = freshCh?.pendingDraft
|
|
295
|
-
if (draft === undefined || draft.length < 50) {
|
|
296
|
-
this.log(`第${no}章 第${round}轮草稿缺失,重试`)
|
|
297
|
-
continue
|
|
298
|
-
}
|
|
299
|
-
const verify = await reviewChapterText(ctx, config, fresh!, draft, chapter.review)
|
|
300
|
-
const highs2 = (verify.issues ?? []).filter(i => i.severity === 'high')
|
|
301
|
-
if (verify.passed || highs2.length === 0) {
|
|
302
|
-
// 应用草稿(与 draft/apply 路由同逻辑):备份原稿 → 落盘 → 定状态。
|
|
303
|
-
this.applyDraft(fresh!, freshCh!, draft, verify)
|
|
304
|
-
if (this.state !== null) this.state.stats.revised++
|
|
305
|
-
this.log(`第${no}章 第${round}轮修订通过(${verify.score}分)`)
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
this.log(`第${no}章 第${round}轮仍不过(${highs2.length} high)`)
|
|
309
|
-
}
|
|
310
|
-
if (this.state !== null) this.state.pendingManual.push(no)
|
|
311
|
-
this.log(`第${no}章 ⚠️ 两轮修订仍不过 → 保留草稿待人工`)
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
private applyDraft(project: ProjectState, chapter: ChapterPlan, draft: string, report: ReviewReport): void {
|
|
315
|
-
const config = this.deps.getConfig()
|
|
316
|
-
const outputDir = this.bookDir ?? config.outputDir
|
|
317
|
-
const fileName = chapterFileName(chapter)
|
|
318
|
-
mkdirSync(outputDir, { recursive: true })
|
|
319
|
-
const targetPath = join(outputDir, fileName)
|
|
320
|
-
if (existsSync(targetPath)) {
|
|
321
|
-
copyFileSync(targetPath, join(outputDir, `${fileName.replace(/\.md$/, '')}.bak.md`))
|
|
322
|
-
}
|
|
323
|
-
writeFileSync(targetPath, `# 第${chapter.no}章 ${chapter.title}\n\n${draft}\n`, 'utf8')
|
|
324
|
-
chapter.pendingDraft = undefined
|
|
325
|
-
chapter.chars = draft.length
|
|
326
|
-
chapter.file = fileName
|
|
327
|
-
if (typeof report.score === 'number') {
|
|
328
|
-
chapter.review = report
|
|
329
|
-
chapter.status = report.passed === true ? 'approved' : 'rejected'
|
|
330
|
-
} else {
|
|
331
|
-
chapter.status = 'written'
|
|
332
|
-
chapter.review = undefined
|
|
333
|
-
}
|
|
334
|
-
chapter.error = undefined
|
|
335
|
-
project.updatedAt = new Date().toISOString()
|
|
336
|
-
saveProject(outputDir, project)
|
|
337
|
-
}
|
|
338
|
-
}
|
package/src/shot-language.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 镜头语言词库(分镜规范化基准)。
|
|
3
|
-
* 分镜表的 shot/camera/composition/light 字段统一从这里取值(英文 id),
|
|
4
|
-
* 生成提示词时用 zh 中文词块;适配英文平台时用 en。
|
|
5
|
-
* 目的:让"景别/运镜/构图/光效"有标准词表,避免 LLM 自由发挥导致漂移。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** 景别。 */
|
|
9
|
-
export type ShotSizeId =
|
|
10
|
-
| 'extreme_wide' | 'wide' | 'full' | 'medium'
|
|
11
|
-
| 'medium_close' | 'close' | 'extreme_close' | 'big_extreme_close'
|
|
12
|
-
|
|
13
|
-
/** 运镜/机位。 */
|
|
14
|
-
export type CameraMoveId =
|
|
15
|
-
| 'static' | 'dolly_in' | 'dolly_out' | 'pan_left' | 'pan_right'
|
|
16
|
-
| 'track_left' | 'track_right' | 'follow' | 'pedestal_up' | 'pedestal_down'
|
|
17
|
-
| 'orbit' | 'handheld' | 'low_angle' | 'high_angle' | 'over_shoulder'
|
|
18
|
-
|
|
19
|
-
/** 构图。 */
|
|
20
|
-
export type CompositionId =
|
|
21
|
-
| 'rule_of_thirds' | 'center' | 'leading_line' | 'foreground'
|
|
22
|
-
| 'low' | 'overhead' | 'symmetry'
|
|
23
|
-
|
|
24
|
-
/** 光效。 */
|
|
25
|
-
export type LightingId =
|
|
26
|
-
| 'front' | 'side' | 'back' | 'top' | 'rembrandt'
|
|
27
|
-
| 'neon' | 'hard' | 'soft' | 'mood' | 'contrast'
|
|
28
|
-
|
|
29
|
-
export interface ShotLangEntry<T extends string> {
|
|
30
|
-
id: T
|
|
31
|
-
/** 中文(提示词/展示用)。 */
|
|
32
|
-
zh: string
|
|
33
|
-
/** 英文(英文平台适配用)。 */
|
|
34
|
-
en: string
|
|
35
|
-
/** 一句话说明(下拉/卡片)。 */
|
|
36
|
-
hint: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export const SHOT_SIZES: readonly ShotLangEntry<ShotSizeId>[] = [
|
|
40
|
-
{ id: 'extreme_wide', zh: '大远景', en: 'extreme wide shot', hint: '环境为主,人物很小' },
|
|
41
|
-
{ id: 'wide', zh: '远景', en: 'wide shot', hint: '人在景中,交代环境' },
|
|
42
|
-
{ id: 'full', zh: '全景', en: 'full shot', hint: '全身』人物整体' },
|
|
43
|
-
{ id: 'medium', zh: '中景', en: 'medium shot', hint: '膝部以上,叙事主力' },
|
|
44
|
-
{ id: 'medium_close', zh: '中近景', en: 'medium close-up', hint: '胸以上,表情+动作' },
|
|
45
|
-
{ id: 'close', zh: '近景', en: 'close-up', hint: '肩以上,突出表情' },
|
|
46
|
-
{ id: 'extreme_close', zh: '特写', en: 'extreme close-up', hint: '脸/物细节' },
|
|
47
|
-
{ id: 'big_extreme_close', zh: '大特写', en: 'big close-up', hint: '局部(眼/手/标志物)' },
|
|
48
|
-
]
|
|
49
|
-
|
|
50
|
-
export const CAMERA_MOVES: readonly ShotLangEntry<CameraMoveId>[] = [
|
|
51
|
-
{ id: 'static', zh: '固定机位', en: 'static camera', hint: '机位不动' },
|
|
52
|
-
{ id: 'dolly_in', zh: '推近', en: 'dolly in', hint: '镜头向主体逼近' },
|
|
53
|
-
{ id: 'dolly_out', zh: '拉远', en: 'dolly out', hint: '镜头远离主体' },
|
|
54
|
-
{ id: 'pan_left', zh: '左摇', en: 'pan left', hint: '机位不动,镜头左转' },
|
|
55
|
-
{ id: 'pan_right', zh: '右摇', en: 'pan right', hint: '机位不动,镜头右转' },
|
|
56
|
-
{ id: 'track_left', zh: '左横移', en: 'track left', hint: '机位随主体左移' },
|
|
57
|
-
{ id: 'track_right', zh: '右横移', en: 'track right', hint: '机位随主体右移' },
|
|
58
|
-
{ id: 'follow', zh: '跟随', en: 'follow shot', hint: '镜头跟着主体运动' },
|
|
59
|
-
{ id: 'pedestal_up', zh: '升镜', en: 'pedestal up', hint: '机位抬高' },
|
|
60
|
-
{ id: 'pedestal_down', zh: '降镜', en: 'pedestal down', hint: '机位降低' },
|
|
61
|
-
{ id: 'orbit', zh: '环绕', en: 'orbit shot', hint: '镜头绕主体转' },
|
|
62
|
-
{ id: 'handheld', zh: '手持晃动', en: 'handheld', hint: '真实感/紧张感' },
|
|
63
|
-
{ id: 'low_angle', zh: '低机位仰拍', en: 'low angle', hint: '突出威严/压迫' },
|
|
64
|
-
{ id: 'high_angle', zh: '高机位俯拍', en: 'high angle', hint: '突出渺小/全知' },
|
|
65
|
-
{ id: 'over_shoulder', zh: '过肩镜头', en: 'over-the-shoulder', hint: '对话常用' },
|
|
66
|
-
]
|
|
67
|
-
|
|
68
|
-
export const COMPOSITIONS: readonly ShotLangEntry<CompositionId>[] = [
|
|
69
|
-
{ id: 'rule_of_thirds', zh: '三分法', en: 'rule of thirds', hint: '主体偏三分之一处' },
|
|
70
|
-
{ id: 'center', zh: '中心对称', en: 'centered', hint: '主体居中' },
|
|
71
|
-
{ id: 'leading_line', zh: '引导线', en: 'leading lines', hint: '视线沿线条引向主体' },
|
|
72
|
-
{ id: 'foreground', zh: '前景遮挡', en: 'foreground framing', hint: '前景物框住主体' },
|
|
73
|
-
{ id: 'low', zh: '低机位', en: 'low camera', hint: '低于视线' },
|
|
74
|
-
{ id: 'overhead', zh: '俯拍', en: 'overhead', hint: '从上方俯视' },
|
|
75
|
-
{ id: 'symmetry', zh: '对称构图', en: 'symmetrical', hint: '左右镜像' },
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
export const LIGHTINGS: readonly ShotLangEntry<LightingId>[] = [
|
|
79
|
-
{ id: 'front', zh: '顺光', en: 'front light', hint: '正面均匀照明' },
|
|
80
|
-
{ id: 'side', zh: '侧光', en: 'side light', hint: '明暗对比强' },
|
|
81
|
-
{ id: 'back', zh: '逆光', en: 'back light', hint: '轮廓光/剪影' },
|
|
82
|
-
{ id: 'top', zh: '顶光', en: 'top light', hint: '俯照硬朗' },
|
|
83
|
-
{ id: 'rembrandt', zh: '伦勃朗光', en: 'rembrandt lighting', hint: '侧逆光,经典人像' },
|
|
84
|
-
{ id: 'neon', zh: '霓虹光', en: 'neon lighting', hint: '赛博/夜景' },
|
|
85
|
-
{ id: 'hard', zh: '硬光', en: 'hard light', hint: '犀利阴影' },
|
|
86
|
-
{ id: 'soft', zh: '柔光', en: 'soft light', hint: '柔和过渡' },
|
|
87
|
-
{ id: 'mood', zh: '氛围光', en: 'mood lighting', hint: '情绪化色温' },
|
|
88
|
-
{ id: 'contrast', zh: '高反差', en: 'high contrast', hint: '黑白/强明暗' },
|
|
89
|
-
]
|
|
90
|
-
|
|
91
|
-
export interface ShotLanguage {
|
|
92
|
-
shot?: ShotSizeId
|
|
93
|
-
camera: CameraMoveId[]
|
|
94
|
-
composition?: CompositionId
|
|
95
|
-
light: LightingId[]
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** 从中文文本归一化到景别(处理旧数据/LLM 口语)。未知回退 medium。 */
|
|
99
|
-
export function normalizeShotSize(text: string | undefined): ShotSizeId {
|
|
100
|
-
if (text === undefined || text === '') return 'medium'
|
|
101
|
-
const t = text.trim()
|
|
102
|
-
if (t.includes('大远景')) return 'extreme_wide'
|
|
103
|
-
if (t.includes('远景')) return 'wide'
|
|
104
|
-
if (t.includes('全景')) return 'full'
|
|
105
|
-
if (t.includes('中近景') || t.includes('中景')) return 'medium'
|
|
106
|
-
if (t.includes('近景') || t.includes('胸')) return 'close'
|
|
107
|
-
if (t.includes('大特写')) return 'big_extreme_close'
|
|
108
|
-
if (t.includes('特写')) return 'extreme_close'
|
|
109
|
-
return 'medium'
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** 从中文文本归一化到运镜列表。 */
|
|
113
|
-
export function normalizeCameras(text: string | undefined): CameraMoveId[] {
|
|
114
|
-
if (text === undefined || text === '') return ['static']
|
|
115
|
-
const t = text.trim()
|
|
116
|
-
const out: CameraMoveId[] = []
|
|
117
|
-
const push = (id: CameraMoveId) => { if (!out.includes(id)) out.push(id) }
|
|
118
|
-
if (t.includes('推近') || t.includes('推进')) push('dolly_in')
|
|
119
|
-
if (t.includes('拉远') || t.includes('拉出')) push('dolly_out')
|
|
120
|
-
if (t.includes('左摇')) push('pan_left')
|
|
121
|
-
if (t.includes('右摇')) push('pan_right')
|
|
122
|
-
if (t.includes('横移')) push(t.includes('左') ? 'track_left' : 'track_right')
|
|
123
|
-
if (t.includes('跟随') || t.includes('跟拍')) push('follow')
|
|
124
|
-
if (t.includes('升降') || t.includes('升') || t.includes('降')) push('pedestal_up')
|
|
125
|
-
if (t.includes('环绕')) push('orbit')
|
|
126
|
-
if (t.includes('手持') || t.includes('晃动')) push('handheld')
|
|
127
|
-
if (t.includes('低机位') || t.includes('仰拍') || t.includes('仰')) push('low_angle')
|
|
128
|
-
if (t.includes('高机位') || t.includes('俯拍') || t.includes('俯')) push('high_angle')
|
|
129
|
-
if (t.includes('过肩')) push('over_shoulder')
|
|
130
|
-
if (out.length === 0) push('static')
|
|
131
|
-
return out
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/** 从中文文本归一化到构图(可选)。 */
|
|
135
|
-
export function normalizeComposition(text: string | undefined): CompositionId | undefined {
|
|
136
|
-
if (text === undefined || text === '') return undefined
|
|
137
|
-
const t = text.trim()
|
|
138
|
-
if (t.includes('三分')) return 'rule_of_thirds'
|
|
139
|
-
if (t.includes('中心') || t.includes('居中')) return 'center'
|
|
140
|
-
if (t.includes('引导')) return 'leading_line'
|
|
141
|
-
if (t.includes('前景')) return 'foreground'
|
|
142
|
-
if (t.includes('低机位')) return 'low'
|
|
143
|
-
if (t.includes('俯拍') || t.includes('俯视')) return 'overhead'
|
|
144
|
-
if (t.includes('对称')) return 'symmetry'
|
|
145
|
-
return undefined
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/** 从中文文本归一化到光效列表。 */
|
|
149
|
-
export function normalizeLightings(text: string | undefined): LightingId[] {
|
|
150
|
-
if (text === undefined || text === '') return ['soft']
|
|
151
|
-
const t = text.trim()
|
|
152
|
-
const out: LightingId[] = []
|
|
153
|
-
const push = (id: LightingId) => { if (!out.includes(id)) out.push(id) }
|
|
154
|
-
if (t.includes('顺光')) push('front')
|
|
155
|
-
if (t.includes('侧光') || t.includes('伦勃朗')) push(t.includes('伦勃朗') ? 'rembrandt' : 'side')
|
|
156
|
-
if (t.includes('逆光')) push('back')
|
|
157
|
-
if (t.includes('顶光')) push('top')
|
|
158
|
-
if (t.includes('霓虹')) push('neon')
|
|
159
|
-
if (t.includes('硬光')) push('hard')
|
|
160
|
-
if (t.includes('柔光')) push('soft')
|
|
161
|
-
if (t.includes('氛围') || t.includes('情绪')) push('mood')
|
|
162
|
-
if (t.includes('高反差')) push('contrast')
|
|
163
|
-
if (out.length === 0) push('soft')
|
|
164
|
-
return out
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/** 取词条中文(ZHs 合并句子)。 */
|
|
168
|
-
export function sizeZh(id: ShotSizeId | undefined): string {
|
|
169
|
-
return SHOT_SIZES.find(e => e.id === id)?.zh ?? '中景'
|
|
170
|
-
}
|
|
171
|
-
export function cameraZh(ids: CameraMoveId[] | undefined): string {
|
|
172
|
-
if (ids === undefined || ids.length === 0) return '固定机位'
|
|
173
|
-
return CAMERA_MOVES.filter(e => ids.includes(e.id)).map(e => e.zh).join(' + ')
|
|
174
|
-
}
|
|
175
|
-
export function compoZh(id: CompositionId | undefined): string {
|
|
176
|
-
return COMPOSITIONS.find(e => e.id === id)?.zh ?? ''
|
|
177
|
-
}
|
|
178
|
-
export function lightZh(ids: LightingId[] | undefined): string {
|
|
179
|
-
if (ids === undefined || ids.length === 0) return '柔光'
|
|
180
|
-
return LIGHTINGS.filter(e => ids.includes(e.id)).map(e => e.zh).join(' + ')
|
|
181
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 编剧词库(剧情骨架规范化基准)。
|
|
3
|
-
* 骨架的 function(叙事功能)与 emotion(情绪走向)统一从这里取值(英文 id),
|
|
4
|
-
* 生成提示词/展示时用 zh 中文。
|
|
5
|
-
* 目的:让"叙事功能/情绪"有标准词表,避免 LLM 自由发挥导致情绪链漂移。
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
/** 叙事功能(对应节拍在剧情中的作用)。 */
|
|
9
|
-
export type StoryFunctionId =
|
|
10
|
-
| 'exposition' | 'conflict' | 'turn' | 'climax'
|
|
11
|
-
| 'resolve' | 'foreshadow' | 'character'
|
|
12
|
-
|
|
13
|
-
/** 情绪词(低→高能量的情绪阶,角色在节拍中的主要情绪点)。 */
|
|
14
|
-
export type EmotionId =
|
|
15
|
-
// 起:低能量
|
|
16
|
-
| 'calm' | 'indifferent' | 'expectant' | 'curious' | 'alert'
|
|
17
|
-
// 承:蓄势
|
|
18
|
-
| 'suppressed' | 'enduring' | 'worried' | 'irritable' | 'uneasy'
|
|
19
|
-
// 转:激化
|
|
20
|
-
| 'terrified' | 'angry' | 'collapsing' | 'resolute' | 'grieved'
|
|
21
|
-
// 合:落点
|
|
22
|
-
| 'relieved' | 'bittersweet' | 'triumphant' | 'reborn' | 'numb'
|
|
23
|
-
|
|
24
|
-
export interface BeatLangEntry<T extends string> {
|
|
25
|
-
id: T
|
|
26
|
-
/** 中文(展示用)。 */
|
|
27
|
-
zh: string
|
|
28
|
-
/** 英文(英文平台适配用)。 */
|
|
29
|
-
en: string
|
|
30
|
-
/** 一句话说明。 */
|
|
31
|
-
hint: string
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** 叙事功能词表。 */
|
|
35
|
-
export const STORY_FUNCTIONS: readonly BeatLangEntry<StoryFunctionId>[] = [
|
|
36
|
-
{ id: 'exposition', zh: '铺垫', en: 'exposition', hint: '介绍背景/人物/悬念' },
|
|
37
|
-
{ id: 'conflict', zh: '冲突', en: 'conflict', hint: '矛盾显现/加剧' },
|
|
38
|
-
{ id: 'turn', zh: '转折', en: 'turning point', hint: '剧情反转/方向改变' },
|
|
39
|
-
{ id: 'climax', zh: '高潮', en: 'climax', hint: '冲突顶峰/揭示' },
|
|
40
|
-
{ id: 'resolve', zh: '收束', en: 'resolution', hint: '矛盾化解/结果' },
|
|
41
|
-
{ id: 'foreshadow', zh: '伏笔', en: 'foreshadowing', hint: '埋下后续线索' },
|
|
42
|
-
{ id: 'character', zh: '人物塑造', en: 'characterization', hint: '展现人物特质/转变' },
|
|
43
|
-
]
|
|
44
|
-
|
|
45
|
-
/** 情绪词表(按能量/阶段排序)。 */
|
|
46
|
-
export const EMOTIONS: readonly BeatLangEntry<EmotionId>[] = [
|
|
47
|
-
// 起:低能量
|
|
48
|
-
{ id: 'calm', zh: '平静', en: 'calm', hint: '无波澜' },
|
|
49
|
-
{ id: 'indifferent', zh: '淡然', en: 'indifferent', hint: '不在乎' },
|
|
50
|
-
{ id: 'expectant', zh: '期待', en: 'expectant', hint: '有所期盼' },
|
|
51
|
-
{ id: 'curious', zh: '好奇', en: 'curious', hint: '想要探究' },
|
|
52
|
-
{ id: 'alert', zh: '警觉', en: 'alert', hint: '察觉到异样' },
|
|
53
|
-
// 承:蓄势
|
|
54
|
-
{ id: 'suppressed', zh: '压抑', en: 'suppressed', hint: '情绪被压制' },
|
|
55
|
-
{ id: 'enduring', zh: '隐忍', en: 'enduring', hint: '强忍着' },
|
|
56
|
-
{ id: 'worried', zh: '担忧', en: 'worried', hint: '担心后果' },
|
|
57
|
-
{ id: 'irritable', zh: '焦躁', en: 'irritable', hint: '烦躁不耐' },
|
|
58
|
-
{ id: 'uneasy', zh: '不安', en: 'uneasy', hint: '心里没底' },
|
|
59
|
-
// 转:激化
|
|
60
|
-
{ id: 'terrified', zh: '惊惧', en: 'terrified', hint: '极度恐惧' },
|
|
61
|
-
{ id: 'angry', zh: '愤怒', en: 'angry', hint: '强烈不满' },
|
|
62
|
-
{ id: 'collapsing', zh: '崩溃', en: 'collapsing', hint: '情绪失控' },
|
|
63
|
-
{ id: 'resolute', zh: '决绝', en: 'resolute', hint: '下定狠心' },
|
|
64
|
-
{ id: 'grieved', zh: '痛心', en: 'grieved', hint: '悲伤心痛' },
|
|
65
|
-
// 合:落点
|
|
66
|
-
{ id: 'relieved', zh: '释然', en: 'relieved', hint: '放下包袱' },
|
|
67
|
-
{ id: 'bittersweet', zh: '悲凉', en: 'bittersweet', hint: '苦涩无奈' },
|
|
68
|
-
{ id: 'triumphant', zh: '得意', en: 'triumphant', hint: '占据上风' },
|
|
69
|
-
{ id: 'reborn', zh: '重生', en: 'reborn', hint: '脱胎换骨' },
|
|
70
|
-
{ id: 'numb', zh: '麻木', en: 'numb', hint: '失去感知' },
|
|
71
|
-
]
|
|
72
|
-
|
|
73
|
-
export interface StoryBeatLanguage {
|
|
74
|
-
function: StoryFunctionId
|
|
75
|
-
emotion: EmotionId[]
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** 从中文归一化叙事功能(未知回退 exposition)。 */
|
|
79
|
-
export function normalizeStoryFunction(text: string | undefined): StoryFunctionId {
|
|
80
|
-
if (text === undefined || text === '') return 'exposition'
|
|
81
|
-
const t = text.trim()
|
|
82
|
-
if (t.includes('铺垫')) return 'exposition'
|
|
83
|
-
if (t.includes('冲突')) return 'conflict'
|
|
84
|
-
if (t.includes('转折')) return 'turn'
|
|
85
|
-
if (t.includes('高潮')) return 'climax'
|
|
86
|
-
if (t.includes('收束')) return 'resolve'
|
|
87
|
-
if (t.includes('伏笔')) return 'foreshadow'
|
|
88
|
-
if (t.includes('人物塑造')) return 'character'
|
|
89
|
-
return 'exposition'
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/** 从中文文本归一化情绪词列表(可含→箭头链)。 */
|
|
93
|
-
export function normalizeEmotions(text: string | undefined): EmotionId[] {
|
|
94
|
-
if (text === undefined || text === '') return ['calm']
|
|
95
|
-
const t = text.trim()
|
|
96
|
-
const out: EmotionId[] = []
|
|
97
|
-
const push = (id: EmotionId) => { if (!out.includes(id)) out.push(id) }
|
|
98
|
-
if (t.includes('平静')) push('calm')
|
|
99
|
-
if (t.includes('淡然')) push('indifferent')
|
|
100
|
-
if (t.includes('期待')) push('expectant')
|
|
101
|
-
if (t.includes('好奇')) push('curious')
|
|
102
|
-
if (t.includes('警觉')) push('alert')
|
|
103
|
-
if (t.includes('压抑')) push('suppressed')
|
|
104
|
-
if (t.includes('隐忍')) push('enduring')
|
|
105
|
-
if (t.includes('担忧')) push('worried')
|
|
106
|
-
if (t.includes('焦躁')) push('irritable')
|
|
107
|
-
if (t.includes('不安')) push('uneasy')
|
|
108
|
-
if (t.includes('惊惧')) push('terrified')
|
|
109
|
-
if (t.includes('愤怒')) push('angry')
|
|
110
|
-
if (t.includes('崩溃')) push('collapsing')
|
|
111
|
-
if (t.includes('决绝')) push('resolute')
|
|
112
|
-
if (t.includes('痛心')) push('grieved')
|
|
113
|
-
if (t.includes('释然')) push('relieved')
|
|
114
|
-
if (t.includes('悲凉')) push('bittersweet')
|
|
115
|
-
if (t.includes('得意')) push('triumphant')
|
|
116
|
-
if (t.includes('重生')) push('reborn')
|
|
117
|
-
if (t.includes('麻木')) push('numb')
|
|
118
|
-
if (out.length === 0) push('calm')
|
|
119
|
-
return out
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** 叙事功能中文。 */
|
|
123
|
-
export function functionZh(id: StoryFunctionId | undefined): string {
|
|
124
|
-
return STORY_FUNCTIONS.find(e => e.id === id)?.zh ?? '铺垫'
|
|
125
|
-
}
|
|
126
|
-
/** 情绪中文(箭头连接)。 */
|
|
127
|
-
export function emotionZh(ids: EmotionId[] | undefined): string {
|
|
128
|
-
if (ids === undefined || ids.length === 0) return '平静'
|
|
129
|
-
return EMOTIONS.filter(e => ids.includes(e.id)).map(e => e.zh).join('→')
|
|
130
|
-
}
|