dsh-cosplay-studio 2.0.4 → 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 +314 -27
- 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,6 +303,81 @@ 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) {
|
|
@@ -342,6 +425,9 @@ export function apply(ctx) {
|
|
|
342
425
|
updatedAt: project.updatedAt,
|
|
343
426
|
finalCount: (project.finals ?? []).length,
|
|
344
427
|
baseCount: (project.baseImages ?? []).length,
|
|
428
|
+
research: project.research
|
|
429
|
+
? { status: project.research.status, stage: project.research.stage, stageLabel: project.research.stageLabel }
|
|
430
|
+
: null,
|
|
345
431
|
}))
|
|
346
432
|
sendJson(response, 200, {
|
|
347
433
|
ok: true,
|
|
@@ -499,7 +585,10 @@ export function apply(ctx) {
|
|
|
499
585
|
sendJson(response, 404, { ok: false, error: '项目不存在' })
|
|
500
586
|
return
|
|
501
587
|
}
|
|
502
|
-
|
|
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 } })
|
|
503
592
|
})
|
|
504
593
|
|
|
505
594
|
route(API.recommend, async (request, response) => {
|
|
@@ -535,6 +624,7 @@ export function apply(ctx) {
|
|
|
535
624
|
return
|
|
536
625
|
}
|
|
537
626
|
const series = String(body?.series ?? '').trim()
|
|
627
|
+
const source = String(body?.source ?? 'name')
|
|
538
628
|
const config = readConfig()
|
|
539
629
|
let details = body?.details !== null && typeof body?.details === 'object' ? { ...body.details } : null
|
|
540
630
|
let note = ''
|
|
@@ -544,17 +634,30 @@ export function apply(ctx) {
|
|
|
544
634
|
details = await researchCharacter(config, { character, series, referencePath: body?.referenceFile ?? '' })
|
|
545
635
|
note = '角色卡由脚本模型生成'
|
|
546
636
|
} catch (error) {
|
|
547
|
-
details = defaultDetails(character, series)
|
|
637
|
+
details = defaultDetails(character, series, { reference: source === 'image' })
|
|
548
638
|
note = `脚本模型不可用(${error?.message ?? error}),已用内置模板`
|
|
549
639
|
}
|
|
550
640
|
} else {
|
|
551
|
-
details = defaultDetails(character, series)
|
|
552
|
-
note =
|
|
641
|
+
details = defaultDetails(character, series, { reference: source === 'image' })
|
|
642
|
+
note = source === 'image'
|
|
643
|
+
? '参考图项目:人物外观以参考图为准,角色卡里的发型/服装/道具只是占位描述,可留空'
|
|
644
|
+
: llmConfigured(config) ? '按内置模板生成角色卡' : '未配置脚本模型,角色卡用通用模板;想做得像请上传参考图'
|
|
553
645
|
}
|
|
554
646
|
}
|
|
555
647
|
const project = createProject({ character, series, details, source: body?.source ?? 'name' })
|
|
556
648
|
updateProject(project.id, { notes: [note] })
|
|
557
|
-
|
|
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 })
|
|
558
661
|
})
|
|
559
662
|
|
|
560
663
|
route(API.update, async (request, response) => {
|
|
@@ -579,7 +682,14 @@ export function apply(ctx) {
|
|
|
579
682
|
if (Array.isArray(body?.shots)) {
|
|
580
683
|
applyShots(id, body.shots)
|
|
581
684
|
}
|
|
582
|
-
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
|
+
}
|
|
583
693
|
if (Object.keys(patch).length > 0) updateProject(id, patch)
|
|
584
694
|
sendJson(response, 200, { ok: true, project: readProject(id) })
|
|
585
695
|
})
|
|
@@ -628,19 +738,22 @@ export function apply(ctx) {
|
|
|
628
738
|
sendJson(response, 409, { ok: false, error: '正在生成其它任务,请稍候' })
|
|
629
739
|
return
|
|
630
740
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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 })
|
|
634
745
|
sendJson(response, 200, { ok: true, queue: queueSnapshot() })
|
|
635
746
|
})
|
|
636
747
|
|
|
637
|
-
|
|
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) => {
|
|
638
751
|
if (request.method !== 'POST') {
|
|
639
752
|
response.writeHead(405, { allow: 'POST' })
|
|
640
753
|
response.end()
|
|
641
754
|
return
|
|
642
755
|
}
|
|
643
|
-
const body = await readBody(request,
|
|
756
|
+
const body = await readBody(request, 64 * 1024)
|
|
644
757
|
const id = String(body?.id ?? '')
|
|
645
758
|
const project = readProject(id)
|
|
646
759
|
if (project === null) {
|
|
@@ -648,26 +761,194 @@ export function apply(ctx) {
|
|
|
648
761
|
return
|
|
649
762
|
}
|
|
650
763
|
const config = readConfig()
|
|
651
|
-
const
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
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) {
|
|
655
781
|
try {
|
|
656
|
-
|
|
657
|
-
|
|
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(';')}`)
|
|
658
785
|
} catch (error) {
|
|
659
|
-
|
|
660
|
-
|
|
786
|
+
sendJson(response, 500, { ok: false, error: `智能分析失败:${error?.message ?? error}` })
|
|
787
|
+
return
|
|
661
788
|
}
|
|
662
|
-
} else {
|
|
663
|
-
shots = ruleScript(project.details, count)
|
|
664
|
-
note = '内置脚本模板'
|
|
665
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)
|
|
666
907
|
const updated = applyShots(id, shots)
|
|
667
908
|
updateProject(id, { status: 'awaiting-script-approval', notes: [...(updated.notes ?? []), note] })
|
|
668
909
|
sendJson(response, 200, { ok: true, project: readProject(id), note })
|
|
669
910
|
})
|
|
670
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
|
+
|
|
671
952
|
route(API.generate, async (request, response) => {
|
|
672
953
|
if (request.method !== 'POST') {
|
|
673
954
|
response.writeHead(405, { allow: 'POST' })
|
|
@@ -718,7 +999,13 @@ export function apply(ctx) {
|
|
|
718
999
|
sendJson(response, 409, { ok: false, error: '已有任务在运行' })
|
|
719
1000
|
return
|
|
720
1001
|
}
|
|
721
|
-
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
|
+
})
|
|
722
1009
|
sendJson(response, 200, { ok: true })
|
|
723
1010
|
})
|
|
724
1011
|
|