@waterwx/dsh-novel-forge 1.4.1 → 1.5.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/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/horror-game-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/shinkai-anime.webp +0 -0
- package/assets/styles/tactical-stealth.webp +0 -0
- package/assets/styles/vaporwave-3d.webp +0 -0
- package/assets/styles/vhs90.webp +0 -0
- package/lib/client.js +1455 -301
- package/lib/client.js.map +1 -1
- package/lib/index.js +589 -6
- package/lib/index.js.map +1 -1
- package/lib/types/client/api.d.ts +8 -0
- package/lib/types/client/panel/MangaWorkspace.d.ts +9 -0
- package/lib/types/client/panel/NovelPanel.d.ts +1 -1
- package/lib/types/client/panel/StoryboardTab.d.ts +13 -0
- package/lib/types/client/panel/StyleGallery.d.ts +1 -0
- package/lib/types/engine.d.ts +19 -3
- package/lib/types/protocol.d.ts +149 -0
- package/lib/types/style-library.d.ts +33 -0
- package/package.json +2 -1
- package/src/client/api.ts +20 -0
- package/src/client/panel/MangaWorkspace.tsx +185 -0
- package/src/client/panel/NovelPanel.tsx +18 -3
- package/src/client/panel/StoryboardTab.tsx +287 -0
- package/src/client/panel/StyleGallery.tsx +109 -0
- package/src/engine.ts +247 -0
- package/src/protocol.ts +164 -0
- package/src/routes.ts +192 -2
- package/src/style-library.ts +78 -0
package/src/routes.ts
CHANGED
|
@@ -12,6 +12,8 @@ import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
|
12
12
|
import { exec } from 'node:child_process'
|
|
13
13
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
14
14
|
import { join, basename, extname } from 'node:path'
|
|
15
|
+
import { homedir } from 'node:os'
|
|
16
|
+
import { fileURLToPath } from 'node:url'
|
|
15
17
|
import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
|
|
16
18
|
import type { Context } from '@deepseek-ai/cordis'
|
|
17
19
|
import { ProductionRunner } from './run.ts'
|
|
@@ -55,6 +57,9 @@ import {
|
|
|
55
57
|
type ForeshadowResponse,
|
|
56
58
|
type JobFrame,
|
|
57
59
|
type LoadOutlineRequest,
|
|
60
|
+
type MangaPlan,
|
|
61
|
+
type MangaPlansRequest,
|
|
62
|
+
type MangaPlansResponse,
|
|
58
63
|
type LoadOutlineResponse,
|
|
59
64
|
type NovelConfig,
|
|
60
65
|
type PlotlinesRequest,
|
|
@@ -85,6 +90,12 @@ import {
|
|
|
85
90
|
type SensitiveHit,
|
|
86
91
|
type StatusResponse,
|
|
87
92
|
type StoryBible,
|
|
93
|
+
type StoryboardSkeletonRequest,
|
|
94
|
+
type StoryboardSkeletonResponse,
|
|
95
|
+
type StoryboardTableRequest,
|
|
96
|
+
type StoryboardTableResponse,
|
|
97
|
+
type StoryboardPromptsRequest,
|
|
98
|
+
type StoryboardPromptsResponse,
|
|
88
99
|
type StyleEngineRequest,
|
|
89
100
|
type SummaryRequest,
|
|
90
101
|
type VolumesRequest,
|
|
@@ -135,6 +146,9 @@ import {
|
|
|
135
146
|
reverseOutlineFromChapters,
|
|
136
147
|
breakdownBook,
|
|
137
148
|
generateRoleReferenceImage,
|
|
149
|
+
generateStoryboardSkeleton,
|
|
150
|
+
generateStoryboardTable,
|
|
151
|
+
generateStoryboardPrompts,
|
|
138
152
|
checkSensitiveText,
|
|
139
153
|
suggestForeshadows,
|
|
140
154
|
suggestPlotlines,
|
|
@@ -146,6 +160,9 @@ import {
|
|
|
146
160
|
/** Cap on JSON request bodies (generous: cover images travel as base64). */
|
|
147
161
|
const MAX_JSON_BODY_BYTES = 64 * 1024 * 1024
|
|
148
162
|
|
|
163
|
+
/** 包内置风格效果图目录(assets/styles,随 npm 包分发)。 */
|
|
164
|
+
const builtinStyleDir = fileURLToPath(new URL('../assets/styles/', import.meta.url))
|
|
165
|
+
|
|
149
166
|
/** Loopback-only fence (mirrors the family plugins' pairing routes). */
|
|
150
167
|
function isLoopbackRequest(request: IncomingMessage): boolean {
|
|
151
168
|
const address = request.socket.remoteAddress
|
|
@@ -1854,7 +1871,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1854
1871
|
return
|
|
1855
1872
|
}
|
|
1856
1873
|
try {
|
|
1857
|
-
const visual = await extractRoleVisual(ctx, config, project, config.outputDir, name)
|
|
1874
|
+
const visual = await extractRoleVisual(ctx, config, project, config.outputDir, name, body?.styleId, body?.filterId)
|
|
1858
1875
|
const role = project.roles.find(r => r.name === name)
|
|
1859
1876
|
if (role !== undefined) {
|
|
1860
1877
|
role.imagePrompt = visual
|
|
@@ -1877,7 +1894,7 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
1877
1894
|
return
|
|
1878
1895
|
}
|
|
1879
1896
|
try {
|
|
1880
|
-
const kit = await generateRolePromptKit(ctx, config, project, name)
|
|
1897
|
+
const kit = await generateRolePromptKit(ctx, config, project, name, body?.styleId, body?.filterId)
|
|
1881
1898
|
const role = project.roles.find(r => r.name === name)
|
|
1882
1899
|
if (role !== undefined) role.promptKit = kit
|
|
1883
1900
|
project.updatedAt = new Date().toISOString()
|
|
@@ -2146,6 +2163,174 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2146
2163
|
},
|
|
2147
2164
|
}
|
|
2148
2165
|
|
|
2166
|
+
// ------------------------------------------------- storyboard prompts
|
|
2167
|
+
/** 分镜·提示词级:分镜表 → 即梦可粘贴视频提示词。 */
|
|
2168
|
+
const storyboardPromptsRoute: WebRoute = {
|
|
2169
|
+
kind: 'exact',
|
|
2170
|
+
path: NOVEL_API.storyboardPrompts,
|
|
2171
|
+
handler: async (req, res) => {
|
|
2172
|
+
if (!guard(req, res, 'POST')) return
|
|
2173
|
+
const config = getConfig()
|
|
2174
|
+
const project = requireProject(res)
|
|
2175
|
+
if (project === undefined) return
|
|
2176
|
+
const body = await readJsonBody<StoryboardPromptsRequest>(req)
|
|
2177
|
+
const no = body?.chapterNo
|
|
2178
|
+
if (!Number.isInteger(no) || no === undefined || no < 1) {
|
|
2179
|
+
writeJson(res, 400, { error: 'chapterNo 须为正整数' })
|
|
2180
|
+
return
|
|
2181
|
+
}
|
|
2182
|
+
if (body?.table === undefined || (body.table.shots ?? []).length === 0) {
|
|
2183
|
+
writeJson(res, 400, { error: 'table 不能为空,请先生成分镜表' })
|
|
2184
|
+
return
|
|
2185
|
+
}
|
|
2186
|
+
try {
|
|
2187
|
+
const prompts = await generateStoryboardPrompts(ctx, config, project, config.outputDir, no, body.table, body?.styleId, body?.filterId)
|
|
2188
|
+
writeJson(res, 200, { prompts } satisfies StoryboardPromptsResponse)
|
|
2189
|
+
} catch (error) {
|
|
2190
|
+
writeJson(res, 500, { error: `视频提示词生成失败:${(error as Error).message}` })
|
|
2191
|
+
}
|
|
2192
|
+
},
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
// ---------------------------------------------------- storyboard table
|
|
2196
|
+
/** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
|
|
2197
|
+
const storyboardTableRoute: WebRoute = {
|
|
2198
|
+
kind: 'exact',
|
|
2199
|
+
path: NOVEL_API.storyboardTable,
|
|
2200
|
+
handler: async (req, res) => {
|
|
2201
|
+
if (!guard(req, res, 'POST')) return
|
|
2202
|
+
const config = getConfig()
|
|
2203
|
+
const project = requireProject(res)
|
|
2204
|
+
if (project === undefined) return
|
|
2205
|
+
const body = await readJsonBody<StoryboardTableRequest>(req)
|
|
2206
|
+
const no = body?.chapterNo
|
|
2207
|
+
if (!Number.isInteger(no) || no === undefined || no < 1) {
|
|
2208
|
+
writeJson(res, 400, { error: 'chapterNo 须为正整数' })
|
|
2209
|
+
return
|
|
2210
|
+
}
|
|
2211
|
+
if (body?.skeleton === undefined || (body.skeleton.beats ?? []).length === 0) {
|
|
2212
|
+
writeJson(res, 400, { error: 'skeleton 不能为空,请先生成剧情骨架' })
|
|
2213
|
+
return
|
|
2214
|
+
}
|
|
2215
|
+
try {
|
|
2216
|
+
const table = await generateStoryboardTable(ctx, config, project, config.outputDir, no, body.skeleton, body?.styleId, body?.filterId)
|
|
2217
|
+
writeJson(res, 200, { table } satisfies StoryboardTableResponse)
|
|
2218
|
+
} catch (error) {
|
|
2219
|
+
writeJson(res, 500, { error: `分镜表生成失败:${(error as Error).message}` })
|
|
2220
|
+
}
|
|
2221
|
+
},
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// ------------------------------------------------------ manhua plans
|
|
2225
|
+
/** 漫剧方案管理:create / remove / activate。 */
|
|
2226
|
+
const manhuaPlansRoute: WebRoute = {
|
|
2227
|
+
kind: 'exact',
|
|
2228
|
+
path: NOVEL_API.manhuaPlans,
|
|
2229
|
+
handler: async (req, res) => {
|
|
2230
|
+
if (!guard(req, res, 'POST')) return
|
|
2231
|
+
const config = getConfig()
|
|
2232
|
+
const project = requireProject(res)
|
|
2233
|
+
if (project === undefined) return
|
|
2234
|
+
const body = await readJsonBody<MangaPlansRequest>(req)
|
|
2235
|
+
if (project.mangaPlans === undefined) project.mangaPlans = []
|
|
2236
|
+
if (body?.op === 'create') {
|
|
2237
|
+
const name = body.name?.trim()
|
|
2238
|
+
const styleId = body.styleId?.trim()
|
|
2239
|
+
if (name === undefined || name === '' || styleId === undefined || styleId === '') {
|
|
2240
|
+
writeJson(res, 400, { error: 'name 与 styleId 不能为空' })
|
|
2241
|
+
return
|
|
2242
|
+
}
|
|
2243
|
+
if (project.mangaPlans.some(p => p.name === name)) {
|
|
2244
|
+
writeJson(res, 400, { error: `方案名「${name}」已存在` })
|
|
2245
|
+
return
|
|
2246
|
+
}
|
|
2247
|
+
const id = `manga-${Date.now().toString(36)}`
|
|
2248
|
+
project.mangaPlans.push({
|
|
2249
|
+
id,
|
|
2250
|
+
name: name.slice(0, 40),
|
|
2251
|
+
styleId,
|
|
2252
|
+
filterId: body.filterId?.trim() !== '' ? body.filterId?.trim() : undefined,
|
|
2253
|
+
active: project.mangaPlans.length === 0,
|
|
2254
|
+
createdAt: new Date().toISOString(),
|
|
2255
|
+
updatedAt: new Date().toISOString(),
|
|
2256
|
+
})
|
|
2257
|
+
} else if (body?.op === 'remove' && body.id !== undefined) {
|
|
2258
|
+
const removed = project.mangaPlans.find(p => p.id === body.id)
|
|
2259
|
+
project.mangaPlans = project.mangaPlans.filter(p => p.id !== body.id)
|
|
2260
|
+
if (removed?.active === true && project.mangaPlans.length > 0) {
|
|
2261
|
+
project.mangaPlans[0].active = true
|
|
2262
|
+
}
|
|
2263
|
+
} else if (body?.op === 'activate' && body.id !== undefined) {
|
|
2264
|
+
project.mangaPlans.forEach(p => { p.active = p.id === body.id })
|
|
2265
|
+
} else {
|
|
2266
|
+
writeJson(res, 400, { error: 'op 须为 create/remove/activate' })
|
|
2267
|
+
return
|
|
2268
|
+
}
|
|
2269
|
+
project.updatedAt = new Date().toISOString()
|
|
2270
|
+
saveProject(config.outputDir, project)
|
|
2271
|
+
writeJson(res, 200, { plans: project.mangaPlans } satisfies MangaPlansResponse)
|
|
2272
|
+
},
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// ------------------------------------------------------ style image
|
|
2276
|
+
/** 风格库效果图:GET /styles/image?id=<styleId>,从 ~/.dsh/dsh-novel-forge-styles/<id>.png 读取。 */
|
|
2277
|
+
const styleImageRoute: WebRoute = {
|
|
2278
|
+
kind: 'exact',
|
|
2279
|
+
path: '/api/dsh-novel-forge/styles/image',
|
|
2280
|
+
handler: async (req, res) => {
|
|
2281
|
+
if (!isLoopbackRequest(req)) {
|
|
2282
|
+
writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
2283
|
+
return
|
|
2284
|
+
}
|
|
2285
|
+
const url = new URL(req.url ?? '', 'http://localhost')
|
|
2286
|
+
const id = (url.searchParams.get('id') ?? '').replace(/[^a-z0-9-]/gi, '')
|
|
2287
|
+
if (id === '') {
|
|
2288
|
+
writeJson(res, 404, { error: 'style image not found' })
|
|
2289
|
+
return
|
|
2290
|
+
}
|
|
2291
|
+
// 图片来源:用户数据目录缩略图(自定义覆盖)→ 原图 → 包内置缩略图(随插件分发)。
|
|
2292
|
+
const base = join(homedir(), '.dsh', 'dsh-novel-forge-styles')
|
|
2293
|
+
const candidates: Array<{ file: string; type: string }> = [
|
|
2294
|
+
{ file: join(base, 'thumbs', id + '.webp'), type: 'image/webp' },
|
|
2295
|
+
{ file: join(base, id + '.png'), type: 'image/png' },
|
|
2296
|
+
{ file: join(builtinStyleDir, id + '.webp'), type: 'image/webp' },
|
|
2297
|
+
]
|
|
2298
|
+
for (const c of candidates) {
|
|
2299
|
+
if (existsSync(c.file)) {
|
|
2300
|
+
res.writeHead(200, { 'content-type': c.type, 'cache-control': 'public, max-age=86400' })
|
|
2301
|
+
res.end(readFileSync(c.file))
|
|
2302
|
+
return
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
2305
|
+
writeJson(res, 404, { error: 'style image not found' })
|
|
2306
|
+
},
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
// ------------------------------------------------------- storyboard
|
|
2310
|
+
/** 分镜·编剧级:单章 → 剧情骨架(节拍链)。 */
|
|
2311
|
+
const storyboardSkeletonRoute: WebRoute = {
|
|
2312
|
+
kind: 'exact',
|
|
2313
|
+
path: NOVEL_API.storyboardSkeleton,
|
|
2314
|
+
handler: async (req, res) => {
|
|
2315
|
+
if (!guard(req, res, 'POST')) return
|
|
2316
|
+
const config = getConfig()
|
|
2317
|
+
const project = requireProject(res)
|
|
2318
|
+
if (project === undefined) return
|
|
2319
|
+
const body = await readJsonBody<StoryboardSkeletonRequest>(req)
|
|
2320
|
+
const no = body?.chapterNo
|
|
2321
|
+
if (!Number.isInteger(no) || no === undefined || no < 1) {
|
|
2322
|
+
writeJson(res, 400, { error: 'chapterNo 须为正整数' })
|
|
2323
|
+
return
|
|
2324
|
+
}
|
|
2325
|
+
try {
|
|
2326
|
+
const skeleton = await generateStoryboardSkeleton(ctx, config, project, config.outputDir, no)
|
|
2327
|
+
writeJson(res, 200, { skeleton } satisfies StoryboardSkeletonResponse)
|
|
2328
|
+
} catch (error) {
|
|
2329
|
+
writeJson(res, 500, { error: `剧情骨架生成失败:${(error as Error).message}` })
|
|
2330
|
+
}
|
|
2331
|
+
},
|
|
2332
|
+
}
|
|
2333
|
+
|
|
2149
2334
|
// ----------------------------------------------------------- breakdown
|
|
2150
2335
|
/** 拆书分析:对已写章节做结构/人物/文风/卖点体检(两阶段:源笔记→分节分析)。 */
|
|
2151
2336
|
const breakdownRoute: WebRoute = {
|
|
@@ -2388,6 +2573,11 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
|
|
|
2388
2573
|
openFolderRoute,
|
|
2389
2574
|
outlineSuggestRoute,
|
|
2390
2575
|
outlineReverseRoute,
|
|
2576
|
+
manhuaPlansRoute,
|
|
2577
|
+
styleImageRoute,
|
|
2578
|
+
storyboardSkeletonRoute,
|
|
2579
|
+
storyboardTableRoute,
|
|
2580
|
+
storyboardPromptsRoute,
|
|
2391
2581
|
breakdownRoute,
|
|
2392
2582
|
runStartRoute,
|
|
2393
2583
|
runControlRoute,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 风格库(内置模板):漫剧方案的视觉基底 + 可叠加滤镜。
|
|
3
|
+
* 字段来自通用风格模板(豆包整理),keywords 即风格前缀词块,生成时直接套用。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type StyleCategory = '3d' | 'game' | '2d' | 'film' | 'craft'
|
|
7
|
+
|
|
8
|
+
export interface ArtStyle {
|
|
9
|
+
id: string
|
|
10
|
+
/** 风格名(用户可见)。 */
|
|
11
|
+
name: string
|
|
12
|
+
category: StyleCategory
|
|
13
|
+
/** 特点一句话(卡片展示)。 */
|
|
14
|
+
traits: string
|
|
15
|
+
/** 风格关键词/前缀(生成角色图/视频提示词时套用)。 */
|
|
16
|
+
keywords: string
|
|
17
|
+
/** 是否可作为滤镜叠加在基底风格之上(影视实验类;缺省 false)。 */
|
|
18
|
+
stackable?: boolean
|
|
19
|
+
/** 默认推荐排序权重(0-1,推荐池排序参考)。 */
|
|
20
|
+
weight?: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const STYLE_CATEGORIES: ReadonlyArray<{ id: StyleCategory; label: string; icon: string; desc: string }> = [
|
|
24
|
+
{ id: '3d', label: '3D 类', icon: '🧊', desc: '人物稳定性最好,视频不易崩脸' },
|
|
25
|
+
{ id: 'game', label: '游戏 3D', icon: '🎮', desc: '游戏渲染质感与镜头语言(生存/恐怖/潜行)' },
|
|
26
|
+
{ id: '2d', label: '2D 二次元绘画', icon: '✏️', desc: '手绘感,人物一致性略弱' },
|
|
27
|
+
{ id: 'film', label: '影视实验质感', icon: '🎞️', desc: '滤镜/影像体系,可叠加在 3D/2D 之上' },
|
|
28
|
+
{ id: 'craft', label: '国风/特殊工艺', icon: '🏮', desc: '国风与传统工艺质感' },
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/** 内置风格模板(18 个)。 */
|
|
32
|
+
export const STYLE_LIBRARY: ArtStyle[] = [
|
|
33
|
+
// ---------------- 3D 类 ----------------
|
|
34
|
+
{ id: 'pixar-adult-3d', name: '成人向皮克斯 3D', category: '3d', traits: 'CG 三维渲染,材质细腻,微表情表现力强;需压制 Q 版可爱感', keywords: '3D皮克斯动画,成人写实CGI,电影光影,拒绝Q版', weight: 1.0 },
|
|
35
|
+
{ id: 'arcane-thick-3d', name: '双城之战美漫厚涂 3D', category: '3d', traits: '粗重轮廓线,高对比硬阴影,材质粗粝,悲剧、压抑、科幻题材适配高', keywords: '双城之战美术,3D美漫厚涂,强轮廓光影,暗调粗粝渲染', weight: 0.95 },
|
|
36
|
+
{ id: 'nextgen-cg', name: '次世代游戏 CG', category: '3d', traits: '接近真人的三维渲染,照片级布料、皮肤材质,冷峻写实', keywords: '次世代游戏CG渲染,超写实3D,真实物理材质,电影布光', weight: 0.9 },
|
|
37
|
+
{ id: 'low-cyberpunk-3d', name: '底层赛博朋克 3D', category: '3d', traits: '锈蚀工业、破碎霓虹、屏幕噪点;避开华丽高楼,偏向破败底层', keywords: '底层赛博朋克3D,废土工业,破碎霓虹,画面噪点色散', weight: 0.85 },
|
|
38
|
+
{ id: 'vaporwave-3d', name: '复古蒸汽波 3D', category: '3d', traits: '粉-蓝-紫霓虹轮廓、90 年代复古网格、胶片色散颗粒,颓废疏离', keywords: '3D复古蒸汽波,90年代三维渲染,霓虹轮廓光,胶片颗粒', weight: 0.8 },
|
|
39
|
+
{ id: 'dark-gothic-3d', name: '暗黑哥特 3D', category: '3d', traits: '大面积阴影、冷青色调、衰败破败质感,适合幽暗密闭空间', keywords: '3D暗黑哥特,冷青暗光,衰败质感,大面积阴影', weight: 0.8 },
|
|
40
|
+
{ id: 'clay-stopmotion-3d', name: '黏土 / 定格 3D', category: '3d', traits: '捏塑颗粒肌理,手工定格质感;可做压抑向,不局限可爱风', keywords: '3D黏土定格渲染,泥塑肌理,电影打光', weight: 0.7 },
|
|
41
|
+
{ id: 'openworld-survival', name: '开放世界生存游戏风', category: 'game', traits: '场景破败感强,货架、仓库、管线、灰尘、锈蚀明显;人物带一点“生存者”气质;适合探索感、逃亡感、世界观展示', keywords: '开放世界生存游戏风格,废土场景,破败仓库,锈蚀金属货架,灰尘粒子,冷色写实光影', weight: 0.8 },
|
|
42
|
+
{ id: 'horror-game-3d', name: '恐怖游戏 3D', category: 'game', traits: '大面积阴影,荧光灯频闪,空间压抑;适合悬疑、压迫、精神异常段落', keywords: '恐怖游戏 3D 风格,昏暗仓库,老旧荧光灯,硬阴影,压迫空间,冷灰蓝调,低饱和', weight: 0.75 },
|
|
43
|
+
{ id: 'tactical-stealth', name: '战术潜行游戏风', category: 'game', traits: '低视角、第三人称镜头感;人物在货架之间移动,光影克制;适合紧张剧情', keywords: '战术潜行游戏视角,第三人称镜头,货架通道,冷色暗光,真实3D渲染,紧张压迫氛围', weight: 0.7 },
|
|
44
|
+
// ---------------- 2D 类 ----------------
|
|
45
|
+
{ id: 'korean-webtoon', name: '韩漫条漫风', category: '2d', traits: '修长人物,低饱和高级灰,氛围感强,都市向漫剧爆款画风', keywords: '韩漫2D绘画,精致人物,低饱和灰调,氛围感插画', weight: 1.0 },
|
|
46
|
+
{ id: 'japan-cel-dark', name: '日系赛璐璐动画', category: '2d', traits: '干净勾线,硬阴影;分明亮版、暗调悲剧版(边缘行者质感)', keywords: '日系赛璐璐动画,清晰轮廓线,硬阴影,暗调都市动画', weight: 0.9 },
|
|
47
|
+
{ id: 'shinkai-anime', name: '新海诚动画风', category: '2d', traits: '极致环境光影、丁达尔、空气中尘埃粒子,擅长雨夜、夜景外景', keywords: '新海诚动画渲染,细腻环境光,漂浮尘埃粒子', weight: 0.85 },
|
|
48
|
+
{ id: 'pop-marvel-2d', name: '波普美漫', category: '2d', traits: '网点半调、强撞色,线条张扬,节奏冲击力强', keywords: '平行宇宙波普美漫,网点半调,漫画粗线条', weight: 0.8 },
|
|
49
|
+
{ id: 'dark-ghibli', name: '暗黑吉卜力手绘', category: '2d', traits: '手绘水彩肌理;强制关闭温暖治愈,做残酷冷调版本', keywords: '吉卜力手绘动画,暗黑写实,水彩肌理,拒绝暖光治愈', weight: 0.75 },
|
|
50
|
+
// ---------------- 影视实验质感(可叠加滤镜) ----------------
|
|
51
|
+
{ id: 'noir-film', name: '黑色电影 Noir', category: 'film', traits: '高反差黑白,只保留少量色彩作为视觉锚点,张力极强', keywords: '黑色电影,大部分黑白,局部保留色彩,硬侧光,胶片颗粒', stackable: true, weight: 1.0 },
|
|
52
|
+
{ id: 'vhs90', name: 'VHS 90 录像带复古', category: 'film', traits: '扫描线、色彩偏移、磁带噪点,模拟老旧档案录像', keywords: 'VHS录像带质感,扫描线,色彩偏移,磁带噪点', stackable: true, weight: 0.9 },
|
|
53
|
+
{ id: 'film-photo', name: '胶片电影写真', category: 'film', traits: '电影机拍摄质感,轻微颗粒,景深虚化,无夸张特效', keywords: '电影写真质感,35mm胶片,胶片颗粒,浅景深', stackable: true, weight: 0.85 },
|
|
54
|
+
// ---------------- 国风 / 特殊工艺 ----------------
|
|
55
|
+
{ id: 'chinese-ink', name: '新中式水墨动画', category: 'craft', traits: '水墨晕染,留白写意', keywords: '水墨动画渲染,写意晕染,国风2D', weight: 1.0 },
|
|
56
|
+
{ id: 'paper-cut-3d', name: '纸雕定格', category: 'craft', traits: '多层纸张镂空叠层,光影穿透纸面', keywords: '立体纸雕定格,纸张肌理,层叠镂空光影', weight: 0.9 },
|
|
57
|
+
{ id: 'guofeng-heavy', name: '工笔重彩国风', category: 'craft', traits: '精细线条,矿物颜料质感,古典华美', keywords: '工笔重彩,国风2D绘画,矿物颜料质感,精细勾线', weight: 0.8 },
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
/** 按分类取风格。 */
|
|
61
|
+
export function stylesByCategory(category: StyleCategory): ArtStyle[] {
|
|
62
|
+
return STYLE_LIBRARY.filter(s => s.category === category)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** 取单个风格(未找到返回 undefined)。 */
|
|
66
|
+
export function findStyle(id: string): ArtStyle | undefined {
|
|
67
|
+
return STYLE_LIBRARY.find(s => s.id === id)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 按基底 + 滤镜拼接风格关键词(角色图/分镜/视频提示词共用;缺省回退 3D 动漫)。 */
|
|
71
|
+
export function styleKeywords(styleId?: string, filterId?: string): string {
|
|
72
|
+
const parts: string[] = []
|
|
73
|
+
const base = styleId !== undefined ? findStyle(styleId) : undefined
|
|
74
|
+
if (base !== undefined) parts.push(base.keywords)
|
|
75
|
+
const filter = filterId !== undefined ? findStyle(filterId) : undefined
|
|
76
|
+
if (filter !== undefined) parts.push(filter.keywords)
|
|
77
|
+
return parts.join(',') || '3D动漫,超精细建模,电影光影'
|
|
78
|
+
}
|