dsh-cosplay-studio 2.0.3 → 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 +33 -16
- package/lib/card.js +156 -52
- package/lib/client.js +376 -24
- package/lib/comfy.js +10 -2
- package/lib/index.js +333 -34
- package/lib/project.js +147 -32
- package/lib/research.js +719 -0
- package/lib/tools/finalize.py +100 -17
- package/package.json +3 -2
package/lib/index.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// 4. post-processing (de-oil / JPEG cap / 2×2 preview / delivery copy);
|
|
9
9
|
// 5. the JSON API the browser half talks to, plus project media serving.
|
|
10
10
|
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'
|
|
12
12
|
import { join, relative, resolve, sep } from 'node:path'
|
|
13
13
|
import {
|
|
14
14
|
API_PREFIX, MEDIA_PREFIX, PROJECTS_DIR, dshHome, logLine, patchConfig, readConfig, setupLog, clearLog,
|
|
@@ -18,13 +18,15 @@ import {
|
|
|
18
18
|
modelStatus, resolveComfyLayout, selfTest, startComfy, stopComfy, waitForComfy,
|
|
19
19
|
} from './comfy.js'
|
|
20
20
|
import {
|
|
21
|
-
|
|
21
|
+
SCRIPT_SYSTEM, buildCard, chatComplete, defaultDetails, llmConfigured, normalizeShotItems, researchCharacter,
|
|
22
|
+
ruleScript, scriptUserText, shotPrompt, writeScript,
|
|
22
23
|
} from './card.js'
|
|
23
24
|
import {
|
|
24
|
-
cancelQueue, createProject, enqueueBase,
|
|
25
|
+
cancelQueue, createProject, enqueueBase, enqueueEdit, enqueueFinalize, enqueueShots,
|
|
25
26
|
listProjects, projectDirs, queueSnapshot, readProject, saveProject, updateProject,
|
|
26
27
|
} from './project.js'
|
|
27
28
|
import { recommend } from './hot.js'
|
|
29
|
+
import { createRandomProject, llmText, parseJson, researchState, startResearch } from './research.js'
|
|
28
30
|
|
|
29
31
|
const inject = ['webServer']
|
|
30
32
|
|
|
@@ -44,11 +46,17 @@ const API = {
|
|
|
44
46
|
update: `${API_PREFIX}/project/update`,
|
|
45
47
|
reference: `${API_PREFIX}/project/reference`,
|
|
46
48
|
base: `${API_PREFIX}/project/base`,
|
|
49
|
+
baseEdit: `${API_PREFIX}/project/base/edit`,
|
|
50
|
+
shotEdit: `${API_PREFIX}/project/shot/edit`,
|
|
51
|
+
restoreShot: `${API_PREFIX}/project/shot/restore`,
|
|
47
52
|
script: `${API_PREFIX}/project/script`,
|
|
53
|
+
scriptAdd: `${API_PREFIX}/project/script/add`,
|
|
48
54
|
generate: `${API_PREFIX}/project/generate`,
|
|
49
55
|
finalize: `${API_PREFIX}/project/finalize`,
|
|
50
56
|
queue: `${API_PREFIX}/queue`,
|
|
51
57
|
openFolder: `${API_PREFIX}/project/open-folder`,
|
|
58
|
+
research: `${API_PREFIX}/project/research`,
|
|
59
|
+
random: `${API_PREFIX}/project/random`,
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
const TUTORIAL = [
|
|
@@ -280,7 +288,7 @@ function referenceSave(id, body) {
|
|
|
280
288
|
function applyShots(id, shots) {
|
|
281
289
|
const project = readProject(id)
|
|
282
290
|
if (project === null) throw new Error('项目不存在')
|
|
283
|
-
const normalized = (Array.isArray(shots) ? shots : []).slice(0,
|
|
291
|
+
const normalized = (Array.isArray(shots) ? shots : []).slice(0, 10).map((shot, index) => ({
|
|
284
292
|
id: index + 1,
|
|
285
293
|
type: String(shot.type ?? 'A'),
|
|
286
294
|
title: String(shot.title ?? `镜头 ${index + 1}`),
|
|
@@ -295,10 +303,103 @@ function applyShots(id, shots) {
|
|
|
295
303
|
return updateProject(id, { shots: normalized })
|
|
296
304
|
}
|
|
297
305
|
|
|
306
|
+
function mediaTypeOfName(name) {
|
|
307
|
+
const lower = String(name).toLowerCase()
|
|
308
|
+
if (lower.endsWith('.png')) return 'image/png'
|
|
309
|
+
if (lower.endsWith('.webp')) return 'image/webp'
|
|
310
|
+
return 'image/jpeg'
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Script model path: configured chat endpoint first, then the DSH host model,
|
|
314
|
+
* then the built-in templates. Keeps existing shots for smart additions. */
|
|
315
|
+
async function generateShotList(ctx, project, count, existing = []) {
|
|
316
|
+
const config = readConfig()
|
|
317
|
+
const user = scriptUserText(project.details, count, existing)
|
|
318
|
+
const startIndex = existing.length + 1
|
|
319
|
+
if (llmConfigured(config)) {
|
|
320
|
+
try {
|
|
321
|
+
const text = await chatComplete(config, SCRIPT_SYSTEM, user)
|
|
322
|
+
const parsed = parseJson(text)
|
|
323
|
+
const list = Array.isArray(parsed) ? parsed : parsed?.shots
|
|
324
|
+
return { shots: normalizeShotItems(list, startIndex), note: '镜头脚本由脚本模型生成' }
|
|
325
|
+
} catch {
|
|
326
|
+
// fall through to the host model
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
let lastError = null
|
|
331
|
+
for (const maxTokens of [9000, 12000]) {
|
|
332
|
+
try {
|
|
333
|
+
const text = await llmText(ctx, `${SCRIPT_SYSTEM}\n\n${user}\n\n只输出 JSON 数组,不要输出任何思考过程或解释。`, { maxTokens })
|
|
334
|
+
const parsed = parseJson(text)
|
|
335
|
+
const list = Array.isArray(parsed) ? parsed : parsed?.shots
|
|
336
|
+
return { shots: normalizeShotItems(list, startIndex), note: '镜头脚本由 DSH 模型生成' }
|
|
337
|
+
} catch (error) {
|
|
338
|
+
lastError = error
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
throw lastError ?? new Error('脚本模型没有返回内容')
|
|
342
|
+
} catch (error) {
|
|
343
|
+
return { shots: ruleScript(project.details, Math.max(1, Math.min(6, count))), note: `内置脚本模板(${error?.message ?? error})` }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Smart edit: compare the picture with the reference / approved base, let the
|
|
348
|
+
* vision model name the flaws and produce one English img2img instruction. */
|
|
349
|
+
async function smartEditInstruction(ctx, project, target, file, note = '') {
|
|
350
|
+
const dirs = projectDirs(project.id)
|
|
351
|
+
const images = []
|
|
352
|
+
const add = (folder, name) => {
|
|
353
|
+
if (name === '') return
|
|
354
|
+
const path = join(folder, name)
|
|
355
|
+
if (!existsSync(path) || images.some((image) => image.name === name)) return
|
|
356
|
+
images.push({ data: readFileSync(path), mediaType: mediaTypeOfName(name), name })
|
|
357
|
+
}
|
|
358
|
+
add(target === 'base' ? dirs.base : dirs.raw, file)
|
|
359
|
+
add(dirs.refs, String(project.referenceFile ?? ''))
|
|
360
|
+
add(dirs.base, String(project.approvedBase ?? ''))
|
|
361
|
+
const ask = note === ''
|
|
362
|
+
? '没有额外要求,请自行找出最需要修的瑕疵(手、脚、五官、服装边缘、穿模)'
|
|
363
|
+
: `用户的修改要求:${note}`
|
|
364
|
+
const prompt = `你在为一套真人 cosplay 照片做精修指导,只输出 JSON。\n\n角色设定(英文):\n${String(project.card ?? '').slice(0, 1800)}\n\n${ask}\n\n请比对图片:第一张是待修改的图片${images.length > 1 ? ',后面的图片是该角色的参考图 / 已确认基准图' : ''}。找出需要修的 1-4 处具体问题,并给出一句可直接用于 img2img 重绘的英文编辑指令:只描述要改的局部,明确保持同一人物、同一姿势、同一机位、同一服装与背景不变;优先修手、脚、五官和衣物边缘这类细节。\n\n只输出:{"issues":["中文问题1","中文问题2"],"fix":"one English editing instruction"}`
|
|
365
|
+
let lastError = null
|
|
366
|
+
for (const maxTokens of [4000, 6000]) {
|
|
367
|
+
try {
|
|
368
|
+
const text = await llmText(ctx, `${prompt}\n\n不要输出任何思考过程。`, { images, maxTokens })
|
|
369
|
+
const parsed = parseJson(text)
|
|
370
|
+
const issues = Array.isArray(parsed?.issues) ? parsed.issues.map((value) => String(value)).filter(Boolean).slice(0, 4) : []
|
|
371
|
+
const fix = String(parsed?.fix ?? '').trim()
|
|
372
|
+
if (fix === '') throw new Error('智能分析没有给出修改指令')
|
|
373
|
+
return { issues, fix }
|
|
374
|
+
} catch (error) {
|
|
375
|
+
lastError = error
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
throw lastError ?? new Error('智能分析失败')
|
|
379
|
+
}
|
|
380
|
+
|
|
298
381
|
/* ----------------------------------------------------------------- apply */
|
|
299
382
|
|
|
300
383
|
export function apply(ctx) {
|
|
301
384
|
const disposers = []
|
|
385
|
+
const disposeAll = () => {
|
|
386
|
+
while (disposers.length > 0) {
|
|
387
|
+
const dispose = disposers.pop()
|
|
388
|
+
try {
|
|
389
|
+
dispose()
|
|
390
|
+
} catch {
|
|
391
|
+
// ignore
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// The plugin loader ignores a plugin's return value, so binding cleanup to
|
|
396
|
+
// the returned function alone leaks routes: a hot reload then fails with
|
|
397
|
+
// "webserver: duplicate exact route ...". Bind it to the fiber instead.
|
|
398
|
+
if (typeof ctx.effect === 'function') {
|
|
399
|
+
ctx.effect(() => disposeAll)
|
|
400
|
+
} else if (typeof ctx.on === 'function') {
|
|
401
|
+
ctx.on('dispose', disposeAll)
|
|
402
|
+
}
|
|
302
403
|
ctx.logger?.info?.('[cosplay-studio] host half loaded')
|
|
303
404
|
|
|
304
405
|
const route = (path, handler) => {
|
|
@@ -324,6 +425,9 @@ export function apply(ctx) {
|
|
|
324
425
|
updatedAt: project.updatedAt,
|
|
325
426
|
finalCount: (project.finals ?? []).length,
|
|
326
427
|
baseCount: (project.baseImages ?? []).length,
|
|
428
|
+
research: project.research
|
|
429
|
+
? { status: project.research.status, stage: project.research.stage, stageLabel: project.research.stageLabel }
|
|
430
|
+
: null,
|
|
327
431
|
}))
|
|
328
432
|
sendJson(response, 200, {
|
|
329
433
|
ok: true,
|
|
@@ -481,7 +585,10 @@ export function apply(ctx) {
|
|
|
481
585
|
sendJson(response, 404, { ok: false, error: '项目不存在' })
|
|
482
586
|
return
|
|
483
587
|
}
|
|
484
|
-
|
|
588
|
+
// A host restart leaves research jobs marked running forever; normalise so
|
|
589
|
+
// the panel shows "重新研究" instead of a spinner that never ends.
|
|
590
|
+
const research = researchState(project)
|
|
591
|
+
sendJson(response, 200, { ok: true, project: research === null ? project : { ...project, research } })
|
|
485
592
|
})
|
|
486
593
|
|
|
487
594
|
route(API.recommend, async (request, response) => {
|
|
@@ -517,6 +624,7 @@ export function apply(ctx) {
|
|
|
517
624
|
return
|
|
518
625
|
}
|
|
519
626
|
const series = String(body?.series ?? '').trim()
|
|
627
|
+
const source = String(body?.source ?? 'name')
|
|
520
628
|
const config = readConfig()
|
|
521
629
|
let details = body?.details !== null && typeof body?.details === 'object' ? { ...body.details } : null
|
|
522
630
|
let note = ''
|
|
@@ -526,17 +634,30 @@ export function apply(ctx) {
|
|
|
526
634
|
details = await researchCharacter(config, { character, series, referencePath: body?.referenceFile ?? '' })
|
|
527
635
|
note = '角色卡由脚本模型生成'
|
|
528
636
|
} catch (error) {
|
|
529
|
-
details = defaultDetails(character, series)
|
|
637
|
+
details = defaultDetails(character, series, { reference: source === 'image' })
|
|
530
638
|
note = `脚本模型不可用(${error?.message ?? error}),已用内置模板`
|
|
531
639
|
}
|
|
532
640
|
} else {
|
|
533
|
-
details = defaultDetails(character, series)
|
|
534
|
-
note =
|
|
641
|
+
details = defaultDetails(character, series, { reference: source === 'image' })
|
|
642
|
+
note = source === 'image'
|
|
643
|
+
? '参考图项目:人物外观以参考图为准,角色卡里的发型/服装/道具只是占位描述,可留空'
|
|
644
|
+
: llmConfigured(config) ? '按内置模板生成角色卡' : '未配置脚本模型,角色卡用通用模板;想做得像请上传参考图'
|
|
535
645
|
}
|
|
536
646
|
}
|
|
537
647
|
const project = createProject({ character, series, details, source: body?.source ?? 'name' })
|
|
538
648
|
updateProject(project.id, { notes: [note] })
|
|
539
|
-
|
|
649
|
+
// Name projects start the slow research right away (web search → pictures →
|
|
650
|
+
// vision analysis → draft). Image projects start after the upload arrives.
|
|
651
|
+
let researching = false
|
|
652
|
+
if (body?.research !== false && source !== 'image') {
|
|
653
|
+
try {
|
|
654
|
+
startResearch(ctx, config, project.id)
|
|
655
|
+
researching = true
|
|
656
|
+
} catch (error) {
|
|
657
|
+
updateProject(project.id, { notes: [note, `角色研究未能启动:${error?.message ?? error}`] })
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
sendJson(response, 200, { ok: true, project: readProject(project.id), note, researching })
|
|
540
661
|
})
|
|
541
662
|
|
|
542
663
|
route(API.update, async (request, response) => {
|
|
@@ -561,7 +682,14 @@ export function apply(ctx) {
|
|
|
561
682
|
if (Array.isArray(body?.shots)) {
|
|
562
683
|
applyShots(id, body.shots)
|
|
563
684
|
}
|
|
564
|
-
if (typeof body?.status === 'string')
|
|
685
|
+
if (typeof body?.status === 'string') {
|
|
686
|
+
patch.status = body.status
|
|
687
|
+
// Remember which base the user approved: later shots are img2img from it.
|
|
688
|
+
if (body.status === 'base-approved') {
|
|
689
|
+
const bases = Array.isArray(project.baseImages) ? project.baseImages : []
|
|
690
|
+
if (bases.length > 0) patch.approvedBase = bases[bases.length - 1]
|
|
691
|
+
}
|
|
692
|
+
}
|
|
565
693
|
if (Object.keys(patch).length > 0) updateProject(id, patch)
|
|
566
694
|
sendJson(response, 200, { ok: true, project: readProject(id) })
|
|
567
695
|
})
|
|
@@ -610,19 +738,22 @@ export function apply(ctx) {
|
|
|
610
738
|
sendJson(response, 409, { ok: false, error: '正在生成其它任务,请稍候' })
|
|
611
739
|
return
|
|
612
740
|
}
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
741
|
+
// Reference projects default to img2img from the uploaded picture; the
|
|
742
|
+
// text-to-image path stays available with an explicit useReference:false.
|
|
743
|
+
const useReference = String(project.referenceFile ?? '') !== '' && body?.useReference !== false
|
|
744
|
+
enqueueBase(config, layout, id, body?.count === 2 ? 2 : 1, { useReference })
|
|
616
745
|
sendJson(response, 200, { ok: true, queue: queueSnapshot() })
|
|
617
746
|
})
|
|
618
747
|
|
|
619
|
-
|
|
748
|
+
// Manual / smart retouch: 'base' appends a new base version, 'shot' replaces
|
|
749
|
+
// one raw final. Smart mode asks the vision model what to fix first.
|
|
750
|
+
const editRoute = (target) => async (request, response) => {
|
|
620
751
|
if (request.method !== 'POST') {
|
|
621
752
|
response.writeHead(405, { allow: 'POST' })
|
|
622
753
|
response.end()
|
|
623
754
|
return
|
|
624
755
|
}
|
|
625
|
-
const body = await readBody(request,
|
|
756
|
+
const body = await readBody(request, 64 * 1024)
|
|
626
757
|
const id = String(body?.id ?? '')
|
|
627
758
|
const project = readProject(id)
|
|
628
759
|
if (project === null) {
|
|
@@ -630,26 +761,194 @@ export function apply(ctx) {
|
|
|
630
761
|
return
|
|
631
762
|
}
|
|
632
763
|
const config = readConfig()
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
764
|
+
const { layout } = detectComfyDir(config)
|
|
765
|
+
if (layout === null || (await comfyStats(config.comfyPort)) === null) {
|
|
766
|
+
sendJson(response, 409, { ok: false, error: 'ComfyUI 没有运行,请先在「安装」页启动' })
|
|
767
|
+
return
|
|
768
|
+
}
|
|
769
|
+
if (queueSnapshot().running) {
|
|
770
|
+
sendJson(response, 409, { ok: false, error: '正在生成其它任务,请稍候' })
|
|
771
|
+
return
|
|
772
|
+
}
|
|
773
|
+
const file = String(body?.file ?? '').trim()
|
|
774
|
+
if (target === 'shot' && file === '') {
|
|
775
|
+
sendJson(response, 400, { ok: false, error: '请选择要修改的成片' })
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
let instruction = String(body?.instruction ?? '').trim()
|
|
779
|
+
const notes = []
|
|
780
|
+
if (body?.smart === true) {
|
|
637
781
|
try {
|
|
638
|
-
|
|
639
|
-
|
|
782
|
+
const analysis = await smartEditInstruction(ctx, project, target, file, instruction)
|
|
783
|
+
instruction = analysis.fix
|
|
784
|
+
if (analysis.issues.length > 0) notes.push(`智能微调分析:${analysis.issues.join(';')}`)
|
|
640
785
|
} catch (error) {
|
|
641
|
-
|
|
642
|
-
|
|
786
|
+
sendJson(response, 500, { ok: false, error: `智能分析失败:${error?.message ?? error}` })
|
|
787
|
+
return
|
|
643
788
|
}
|
|
644
|
-
} else {
|
|
645
|
-
shots = ruleScript(project.details, count)
|
|
646
|
-
note = '内置脚本模板'
|
|
647
789
|
}
|
|
790
|
+
if (instruction === '') {
|
|
791
|
+
sendJson(response, 400, { ok: false, error: '请输入修改要求,或改用智能修改' })
|
|
792
|
+
return
|
|
793
|
+
}
|
|
794
|
+
try {
|
|
795
|
+
enqueueEdit(config, layout, id, { target, file, instruction, denoise: Number(body?.denoise), notes })
|
|
796
|
+
sendJson(response, 200, { ok: true, project: readProject(id), issues: notes })
|
|
797
|
+
} catch (error) {
|
|
798
|
+
sendJson(response, 500, { ok: false, error: error?.message ?? String(error) })
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
route(API.baseEdit, editRoute('base'))
|
|
802
|
+
route(API.shotEdit, editRoute('shot'))
|
|
803
|
+
|
|
804
|
+
// Undo the most recent retouch of one raw final (backups live in raw/.history).
|
|
805
|
+
route(API.restoreShot, async (request, response) => {
|
|
806
|
+
if (request.method !== 'POST') {
|
|
807
|
+
response.writeHead(405, { allow: 'POST' })
|
|
808
|
+
response.end()
|
|
809
|
+
return
|
|
810
|
+
}
|
|
811
|
+
const body = await readBody(request, 16 * 1024)
|
|
812
|
+
const id = String(body?.id ?? '')
|
|
813
|
+
const project = readProject(id)
|
|
814
|
+
if (project === null) {
|
|
815
|
+
sendJson(response, 404, { ok: false, error: '项目不存在' })
|
|
816
|
+
return
|
|
817
|
+
}
|
|
818
|
+
const file = String(body?.file ?? '').trim()
|
|
819
|
+
if (file === '' || file.includes('/') || file.includes('\\') || file.includes('..')) {
|
|
820
|
+
sendJson(response, 400, { ok: false, error: '文件名不合法' })
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
const dirs = projectDirs(id)
|
|
824
|
+
const history = join(dirs.raw, '.history')
|
|
825
|
+
const current = join(dirs.raw, file)
|
|
826
|
+
if (!existsSync(history) || !existsSync(current)) {
|
|
827
|
+
sendJson(response, 404, { ok: false, error: '没有可恢复的历史版本' })
|
|
828
|
+
return
|
|
829
|
+
}
|
|
830
|
+
const versions = readdirSync(history)
|
|
831
|
+
.filter((name) => name.startsWith(`${file}.`) && name.endsWith('.png'))
|
|
832
|
+
.sort()
|
|
833
|
+
if (versions.length === 0) {
|
|
834
|
+
sendJson(response, 404, { ok: false, error: '没有可恢复的历史版本' })
|
|
835
|
+
return
|
|
836
|
+
}
|
|
837
|
+
const latest = versions[versions.length - 1]
|
|
838
|
+
writeFileSync(join(history, `${file}.${Date.now()}.png`), readFileSync(current))
|
|
839
|
+
writeFileSync(current, readFileSync(join(history, latest)))
|
|
840
|
+
const updated = updateProject(id, {
|
|
841
|
+
pendingFinals: [...new Set([...(project.pendingFinals ?? []), file])],
|
|
842
|
+
notes: [...(project.notes ?? []), `已恢复上一版成片:${file}`],
|
|
843
|
+
})
|
|
844
|
+
sendJson(response, 200, { ok: true, project: updated })
|
|
845
|
+
})
|
|
846
|
+
|
|
847
|
+
route(API.research, async (request, response) => {
|
|
848
|
+
if (request.method !== 'POST') {
|
|
849
|
+
response.writeHead(405, { allow: 'POST' })
|
|
850
|
+
response.end()
|
|
851
|
+
return
|
|
852
|
+
}
|
|
853
|
+
const body = await readBody(request, 64 * 1024)
|
|
854
|
+
const id = String(body?.id ?? '')
|
|
855
|
+
if (readProject(id) === null) {
|
|
856
|
+
sendJson(response, 404, { ok: false, error: '项目不存在' })
|
|
857
|
+
return
|
|
858
|
+
}
|
|
859
|
+
const config = readConfig()
|
|
860
|
+
try {
|
|
861
|
+
const project = startResearch(ctx, config, id, { supplement: body?.supplement === true })
|
|
862
|
+
sendJson(response, 200, { ok: true, project })
|
|
863
|
+
} catch (error) {
|
|
864
|
+
sendJson(response, 500, { ok: false, error: error?.message ?? String(error) })
|
|
865
|
+
}
|
|
866
|
+
})
|
|
867
|
+
|
|
868
|
+
route(API.random, async (request, response) => {
|
|
869
|
+
if (request.method !== 'POST') {
|
|
870
|
+
response.writeHead(405, { allow: 'POST' })
|
|
871
|
+
response.end()
|
|
872
|
+
return
|
|
873
|
+
}
|
|
874
|
+
const config = readConfig()
|
|
875
|
+
try {
|
|
876
|
+
const pick = await createRandomProject(ctx, config)
|
|
877
|
+
const project = createProject({
|
|
878
|
+
character: pick.character,
|
|
879
|
+
series: pick.series,
|
|
880
|
+
details: defaultDetails(pick.character, pick.series),
|
|
881
|
+
source: 'random',
|
|
882
|
+
})
|
|
883
|
+
updateProject(project.id, { notes: [pick.reason === '' ? '随机推荐角色' : `随机推荐:${pick.reason}`] })
|
|
884
|
+
startResearch(ctx, config, project.id)
|
|
885
|
+
sendJson(response, 200, { ok: true, project: readProject(project.id), pick })
|
|
886
|
+
} catch (error) {
|
|
887
|
+
sendJson(response, 500, { ok: false, error: error?.message ?? String(error) })
|
|
888
|
+
}
|
|
889
|
+
})
|
|
890
|
+
|
|
891
|
+
route(API.script, async (request, response) => {
|
|
892
|
+
if (request.method !== 'POST') {
|
|
893
|
+
response.writeHead(405, { allow: 'POST' })
|
|
894
|
+
response.end()
|
|
895
|
+
return
|
|
896
|
+
}
|
|
897
|
+
const body = await readBody(request, 1024 * 1024)
|
|
898
|
+
const id = String(body?.id ?? '')
|
|
899
|
+
const project = readProject(id)
|
|
900
|
+
if (project === null) {
|
|
901
|
+
sendJson(response, 404, { ok: false, error: '项目不存在' })
|
|
902
|
+
return
|
|
903
|
+
}
|
|
904
|
+
const config = readConfig()
|
|
905
|
+
const count = Math.max(4, Math.min(6, Number(body?.count ?? config.shotCount ?? 6)))
|
|
906
|
+
const { shots, note } = await generateShotList(ctx, project, count)
|
|
648
907
|
const updated = applyShots(id, shots)
|
|
649
908
|
updateProject(id, { status: 'awaiting-script-approval', notes: [...(updated.notes ?? []), note] })
|
|
650
909
|
sendJson(response, 200, { ok: true, project: readProject(id), note })
|
|
651
910
|
})
|
|
652
911
|
|
|
912
|
+
// Smart add: the script model designs extra shots that must not repeat the
|
|
913
|
+
// ones already in the list.
|
|
914
|
+
route(API.scriptAdd, async (request, response) => {
|
|
915
|
+
if (request.method !== 'POST') {
|
|
916
|
+
response.writeHead(405, { allow: 'POST' })
|
|
917
|
+
response.end()
|
|
918
|
+
return
|
|
919
|
+
}
|
|
920
|
+
const body = await readBody(request, 64 * 1024)
|
|
921
|
+
const id = String(body?.id ?? '')
|
|
922
|
+
const project = readProject(id)
|
|
923
|
+
if (project === null) {
|
|
924
|
+
sendJson(response, 404, { ok: false, error: '项目不存在' })
|
|
925
|
+
return
|
|
926
|
+
}
|
|
927
|
+
const existing = Array.isArray(project.shots) ? project.shots : []
|
|
928
|
+
if (existing.length === 0) {
|
|
929
|
+
sendJson(response, 409, { ok: false, error: '还没有镜头脚本,请先生成' })
|
|
930
|
+
return
|
|
931
|
+
}
|
|
932
|
+
const add = Math.max(1, Math.min(3, Number(body?.count ?? 2)))
|
|
933
|
+
const limit = Math.max(0, 10 - existing.length)
|
|
934
|
+
if (limit === 0) {
|
|
935
|
+
sendJson(response, 409, { ok: false, error: '已经有 10 个分镜,先删除一些再补' })
|
|
936
|
+
return
|
|
937
|
+
}
|
|
938
|
+
const { shots: fresh, note } = await generateShotList(ctx, project, Math.min(add, limit), existing)
|
|
939
|
+
const merged = [...existing, ...fresh].slice(0, 10).map((shot, index) => ({
|
|
940
|
+
...shot,
|
|
941
|
+
id: index + 1,
|
|
942
|
+
prompt: `${shot.type ?? 'A'}|${shot.title ?? `镜头 ${index + 1}`}`,
|
|
943
|
+
}))
|
|
944
|
+
const updated = updateProject(id, {
|
|
945
|
+
shots: merged,
|
|
946
|
+
status: 'awaiting-script-approval',
|
|
947
|
+
notes: [...(project.notes ?? []), note, `智能补充分镜 ${fresh.length} 个`],
|
|
948
|
+
})
|
|
949
|
+
sendJson(response, 200, { ok: true, project: updated, note })
|
|
950
|
+
})
|
|
951
|
+
|
|
653
952
|
route(API.generate, async (request, response) => {
|
|
654
953
|
if (request.method !== 'POST') {
|
|
655
954
|
response.writeHead(405, { allow: 'POST' })
|
|
@@ -700,7 +999,13 @@ export function apply(ctx) {
|
|
|
700
999
|
sendJson(response, 409, { ok: false, error: '已有任务在运行' })
|
|
701
1000
|
return
|
|
702
1001
|
}
|
|
703
|
-
enqueueFinalize(config, layout, id
|
|
1002
|
+
enqueueFinalize(config, layout, id, {
|
|
1003
|
+
deoil: body?.deoil === true,
|
|
1004
|
+
makeup: body?.makeup === true,
|
|
1005
|
+
flatten: body?.flatten !== false,
|
|
1006
|
+
preview: body?.preview !== false,
|
|
1007
|
+
copy: body?.copy !== false,
|
|
1008
|
+
})
|
|
704
1009
|
sendJson(response, 200, { ok: true })
|
|
705
1010
|
})
|
|
706
1011
|
|
|
@@ -798,13 +1103,7 @@ export function apply(ctx) {
|
|
|
798
1103
|
disposers.push(disposeMedia)
|
|
799
1104
|
|
|
800
1105
|
return async () => {
|
|
801
|
-
|
|
802
|
-
try {
|
|
803
|
-
dispose()
|
|
804
|
-
} catch {
|
|
805
|
-
// ignore
|
|
806
|
-
}
|
|
807
|
-
}
|
|
1106
|
+
disposeAll()
|
|
808
1107
|
}
|
|
809
1108
|
}
|
|
810
1109
|
|