Compare commits
2
Commits
bb27af506e
...
2842855c7b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2842855c7b | ||
|
|
e0292c8d2a |
@@ -116,6 +116,9 @@
|
||||
<span class="field-label">Prompt</span>
|
||||
<textarea v-model="form.promptSections.action" class="field-area" placeholder="Describe the image or the shot." />
|
||||
</label>
|
||||
<label v-if="form.engine==='qwen21' && (form.mode==='generate' || form.mode==='edit')" class="inline-flex items-center gap-2 text-[var(--type-sm)] text-[var(--muted)]">
|
||||
<input v-model="form.enhancePrompt" type="checkbox" class="accent-[var(--amber)]">Enhance prompt
|
||||
</label>
|
||||
|
||||
<div>
|
||||
<span class="field-label">Negative prompt</span>
|
||||
@@ -375,6 +378,7 @@ import {isXaigenStudio} from '~/utils/videoModels'
|
||||
import {LOAD_GENERATION_PAYLOAD_KEY} from '~/utils/queuedJob'
|
||||
|
||||
const NEG_STORAGE_KEY='studio2-negative-chips'
|
||||
const ENHANCE_STORAGE_KEY='studio2-qwen21-enhance-prompt'
|
||||
function readStoredNegatives(){
|
||||
if(!import.meta.client)return [...defaultNegatives]
|
||||
try{
|
||||
@@ -388,6 +392,14 @@ function writeStoredNegatives(list:string[]){
|
||||
if(!import.meta.client)return
|
||||
try{localStorage.setItem(NEG_STORAGE_KEY,JSON.stringify(list))}catch{/* ignore */}
|
||||
}
|
||||
function readStoredEnhancePrompt(){
|
||||
if(!import.meta.client)return false
|
||||
try{return localStorage.getItem(ENHANCE_STORAGE_KEY)==='1'}catch{return false}
|
||||
}
|
||||
function writeStoredEnhancePrompt(on:boolean){
|
||||
if(!import.meta.client)return
|
||||
try{localStorage.setItem(ENHANCE_STORAGE_KEY,on?'1':'0')}catch{/* ignore */}
|
||||
}
|
||||
|
||||
const xaigen=isXaigenStudio(useRuntimeConfig().public)
|
||||
const instanceName=String(useRuntimeConfig().public.instanceName || 'AIGen')
|
||||
@@ -407,7 +419,7 @@ const liveMusic=ref<Record<string,any>>({})
|
||||
const negativeDraft=ref('')
|
||||
const filmstripEl=ref<HTMLElement|null>(null)
|
||||
const tileRefs=new Map<string,HTMLElement>()
|
||||
const form=reactive<any>({mode:'generate',engine:'flux',imageStyles:{positive:[],negative:readStoredNegatives()},lockFace:true,lockOutfit:true,hideInput:xaigen,folderId:'',identityStillId:'',imageAId:'',imageBId:'',maskId:'',startClipId:'',endStillId:'',guides:[],promptSections:sectionsFrom({}),settings:{aspect:'auto',fps:24,steps:24,cfg:4,seed:null,seedMode:'random',duration:5,turbo:true,loraStack:[]}})
|
||||
const form=reactive<any>({mode:'generate',engine:'flux',enhancePrompt:readStoredEnhancePrompt(),imageStyles:{positive:[],negative:readStoredNegatives()},lockFace:true,lockOutfit:true,hideInput:xaigen,folderId:'',identityStillId:'',imageAId:'',imageBId:'',maskId:'',startClipId:'',endStillId:'',guides:[],promptSections:sectionsFrom({}),settings:{aspect:'auto',fps:24,steps:24,cfg:4,seed:null,seedMode:'random',duration:5,turbo:true,loraStack:[]}})
|
||||
const music=reactive({tags:DEFAULT_MUSIC_TAGS,lyrics:DEFAULT_MUSIC_LYRICS,duration:60,seed:'',name:''})
|
||||
const tagsPlaceholder=DEFAULT_MUSIC_TAGS,lyricsPlaceholder=DEFAULT_MUSIC_LYRICS
|
||||
let loraMemory=createLoraMemory(xaigen)
|
||||
@@ -451,6 +463,7 @@ watch(()=>form.engine,(engine,prev)=>{
|
||||
form.settings.loraStack=[]
|
||||
}
|
||||
})
|
||||
watch(()=>form.enhancePrompt,on=>writeStoredEnhancePrompt(!!on))
|
||||
watch(()=>[form.mode,form.engine],([mode,engine])=>{
|
||||
if(engine==='qwen21' && mode==='edit'){
|
||||
form.settings.steps=25
|
||||
|
||||
@@ -19,7 +19,14 @@ const rows=computed(()=>{const m=props.metadata||{},s=m.settings||{};const time=
|
||||
{label:'Start frame',value:m.startFrameSource?JSON.stringify(m.startFrameSource):'None'},
|
||||
{label:'Handoff source',value:m.handoffSource || 'None'},{label:'Handoff fitted (resampled)',value:m.fitted==null?'Not recorded':m.fitted?'Yes':'No'},
|
||||
{label:'End still',value:m.endStillId||'None'},{label:'Guide stills',value:m.guides?.length?JSON.stringify(m.guides):'None'},
|
||||
{label:'Compiled prompt',value:m.compiledPrompt},{label:'Queued',value:time(m.queuedAt)},{label:'Started',value:time(m.startedAt)},{label:'Saved',value:time(m.savedAt)},
|
||||
{label:'Compiled prompt',value:m.compiledPrompt},
|
||||
{label:'Prompt (raw)',value:m.promptRaw || null},
|
||||
{label:'Enhance prompt',value:m.enhancePrompt==null?null:m.enhancePrompt?'On':'Off'},
|
||||
{label:'Enhance wh_ratio',value:m.enhance?.wh_ratio || null},
|
||||
{label:'Enhance ratio_follow',value:m.enhance?.ratio_follow || null},
|
||||
{label:'Enhance parse_ok',value:m.enhance?.parse_ok==null?null:m.enhance.parse_ok?'Yes':'No'},
|
||||
{label:'Enhance thinking',value:m.enhance?.thinking || null},
|
||||
{label:'Queued',value:time(m.queuedAt)},{label:'Started',value:time(m.startedAt)},{label:'Saved',value:time(m.savedAt)},
|
||||
{label:'GPU seconds',value:m.gpuSeconds},{label:'Wall seconds',value:m.wallTime},{label:'Comfy prompt ID',value:m.promptId},{label:'Host cleanup',value:m.purgeResult}
|
||||
]})
|
||||
</script>
|
||||
|
||||
@@ -158,6 +158,7 @@
|
||||
</template>
|
||||
<template v-else>
|
||||
<label class="m-label">Prompt<textarea v-model="ctx.form.promptSections.action" rows="5" class="m-area" placeholder="Describe the image or the shot." /></label>
|
||||
<label v-if="ctx.form.engine==='qwen21' && (ctx.form.mode==='generate' || ctx.form.mode==='edit')" class="m-check"><input v-model="ctx.form.enhancePrompt" type="checkbox"> Enhance prompt</label>
|
||||
<div>
|
||||
<span class="m-label">Negative prompt</span>
|
||||
<div class="m-neg">
|
||||
|
||||
+4
-2
@@ -39,6 +39,8 @@ Restart Comfy **only when idle** (`COMFY_CONTROL_URL/status` → `gpu.busy=false
|
||||
- Face / outfit locks stay Klein semantics — they do not drive Qwen slots.
|
||||
- Sampler defaults: euler / simple / cfg **1** / steps **25**. Edit uses `QwenImage21Cache` (device auto, dtype int8). T2I uses `ModelSamplingAuraFlow` shift **3.1**.
|
||||
- Default canvas **1024×1024** (aspect 16:9 / 9:16 / 1:1 → long-edge ~1024 via `TextEncodeQwenImage21` resolution; multiples of 32). Edit with aspect `auto` follows `image_1` aspect at ~1024.
|
||||
- No hero / locks / Klein LoRA stack on this engine.
|
||||
- Optional **Enhance prompt** (off by default): runs a separate PE-only Comfy graph (`CLIPLoader` + rewrite node), then frees VRAM and queues the existing T2I/Edit graph with the rewritten prompt. Never loads PE CLIP + DiT together on 16 GB. Fail closed if `parse_ok` is false or the rewrite is empty.
|
||||
- PE weights (int8 only): `text_encoders\qwen3.5_9b_qwen_image_2.1_pe_{t2i,i2i}.int8_convrot.safetensors` — do not replace the image TE `qwen3vl_8b_int8_convrot.safetensors`.
|
||||
- Custom node: `ComfyUI-Qwen-Image-2.1-Prompt-Enhancer` (`QwenImage21_T2IPromptRewrite` / `QwenImage21_EditPromptRewrite`).
|
||||
|
||||
Graphs: `server/assets/studio2_qwen21_t2i.json`, `server/assets/studio2_qwen21_edit.json`.
|
||||
Graphs: `server/assets/studio2_qwen21_t2i.json`, `server/assets/studio2_qwen21_edit.json`, `server/assets/studio2_qwen21_pe_t2i.json`, `server/assets/studio2_qwen21_pe_edit.json`.
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"5": {
|
||||
"inputs": {
|
||||
"clip_name": "qwen3.5_9b_qwen_image_2.1_pe_i2i.int8_convrot.safetensors",
|
||||
"type": "qwen_image",
|
||||
"device": "default"
|
||||
},
|
||||
"class_type": "CLIPLoader",
|
||||
"_meta": { "title": "Load Qwen 2.1 PE Edit CLIP" }
|
||||
},
|
||||
"10": {
|
||||
"inputs": {
|
||||
"image": "qwen21_pe_edit_source.png"
|
||||
},
|
||||
"class_type": "LoadImage",
|
||||
"_meta": { "title": "image_1 source" }
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"clip": ["5", 0],
|
||||
"prompt": "",
|
||||
"image_1": ["10", 0],
|
||||
"temperature": 1.0,
|
||||
"top_p": 0.95,
|
||||
"presence_penalty": 0,
|
||||
"max_length": 24000,
|
||||
"seed": 42
|
||||
},
|
||||
"class_type": "QwenImage21_EditPromptRewrite",
|
||||
"_meta": { "title": "Qwen 2.1 Edit Prompt Rewrite" }
|
||||
},
|
||||
"30": {
|
||||
"inputs": { "source": ["9", 0] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE positive_prompt" }
|
||||
},
|
||||
"31": {
|
||||
"inputs": { "source": ["9", 2] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE wh_ratio" }
|
||||
},
|
||||
"32": {
|
||||
"inputs": { "source": ["9", 3] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE ratio_follow" }
|
||||
},
|
||||
"33": {
|
||||
"inputs": { "source": ["9", 4] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE thinking" }
|
||||
},
|
||||
"34": {
|
||||
"inputs": { "source": ["9", 5] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE parse_ok" }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"5": {
|
||||
"inputs": {
|
||||
"clip_name": "qwen3.5_9b_qwen_image_2.1_pe_t2i.int8_convrot.safetensors",
|
||||
"type": "qwen_image",
|
||||
"device": "default"
|
||||
},
|
||||
"class_type": "CLIPLoader",
|
||||
"_meta": { "title": "Load Qwen 2.1 PE T2I CLIP" }
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"clip": ["5", 0],
|
||||
"prompt": "",
|
||||
"temperature": 1.0,
|
||||
"top_p": 0.95,
|
||||
"top_k": 20,
|
||||
"presence_penalty": 1.5,
|
||||
"max_new_tokens": 16256,
|
||||
"seed": 42
|
||||
},
|
||||
"class_type": "QwenImage21_T2IPromptRewrite",
|
||||
"_meta": { "title": "Qwen 2.1 T2I Prompt Rewrite" }
|
||||
},
|
||||
"30": {
|
||||
"inputs": { "source": ["9", 0] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE positive_prompt" }
|
||||
},
|
||||
"31": {
|
||||
"inputs": { "source": ["9", 2] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE wh_ratio" }
|
||||
},
|
||||
"32": {
|
||||
"inputs": { "source": ["9", 3] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE thinking" }
|
||||
},
|
||||
"33": {
|
||||
"inputs": { "source": ["9", 4] },
|
||||
"class_type": "PreviewAny",
|
||||
"_meta": { "title": "PE parse_ok" }
|
||||
}
|
||||
}
|
||||
+110
-40
@@ -9,7 +9,9 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from '
|
||||
import { join } from 'node:path';
|
||||
import template from '../../assets/studio2_minimax_native.json';
|
||||
import qwen21Template from '../../assets/studio2_qwen21_t2i.json'
|
||||
import qwen21EditTemplate from '../../assets/studio2_qwen21_edit.json';
|
||||
import qwen21EditTemplate from '../../assets/studio2_qwen21_edit.json'
|
||||
import qwen21PeT2iTemplate from '../../assets/studio2_qwen21_pe_t2i.json'
|
||||
import qwen21PeEditTemplate from '../../assets/studio2_qwen21_pe_edit.json';
|
||||
import { nativeVideoGraph, attachHeroReference, applyResolvedImageSize } from '~/shared/studio2/graphs.mjs';
|
||||
import { compilePrompt, scopedFile } from '~/shared/studio2/contracts.mjs';
|
||||
import { resolveQwen21Size } from '~/shared/studio2/input.mjs';
|
||||
@@ -17,7 +19,7 @@ import { createJob, restoreJob, getJob, emitJob, type Job } from '../jobs';
|
||||
import { markStudioLive, onLiveVideoSettled, type StudioJob } from '../studioQueue';
|
||||
import { readRecord, saveRecord, studio2Root, records } from './store';
|
||||
import { ensureComfyReady } from '../comfyLifecycle';
|
||||
import { comfyFetch, queuePrompt, fetchHistory, extractVideo } from '../comfy';
|
||||
import { comfyFetch, queuePrompt, fetchHistory, extractVideo, freeComfyVram } from '../comfy';
|
||||
import { extractEditedImage } from '../imageComfy';
|
||||
import { buildImageV2Workflow, resolveKreaGenerateAssets } from '../imageWorkflowV2';
|
||||
import { resolveGraphLoraNames, applyUserLoraToGraph, ensureComfyLoraNames } from '../loras';
|
||||
@@ -37,6 +39,107 @@ function update(r: any, state: string) { r.state = state; r.updatedAt = Date.now
|
||||
// extend/video start frame (which is either the exact anchor PNG or an already-canvas-sized
|
||||
// extracted last frame). Fitting these every hop is what destroys identity across chained extends.
|
||||
const NEVER_FIT = new Set(['hero', 'start']);
|
||||
|
||||
function previewAnyText(outputs: Record<string, any> | undefined, nodeId: string): string {
|
||||
const node = outputs?.[nodeId]
|
||||
if (!node || typeof node !== 'object') return ''
|
||||
const raw = node.text ?? node.STRING ?? node.source
|
||||
if (Array.isArray(raw)) return String(raw[0] ?? '')
|
||||
if (raw == null) return ''
|
||||
return String(raw)
|
||||
}
|
||||
|
||||
function harvestQwen21Pe(history: Record<string, any> | null | undefined, promptId: string, edit: boolean) {
|
||||
const outputs = history?.[promptId]?.outputs as Record<string, any> | undefined
|
||||
const positive_prompt = previewAnyText(outputs, '30').trim()
|
||||
const wh_ratio = previewAnyText(outputs, '31').trim()
|
||||
const ratio_follow = edit ? previewAnyText(outputs, '32').trim() : ''
|
||||
const thinking = previewAnyText(outputs, edit ? '33' : '32').trim()
|
||||
const parseRaw = previewAnyText(outputs, edit ? '34' : '33').trim()
|
||||
const parse_ok = /^(true|1|yes)$/i.test(parseRaw)
|
||||
return { positive_prompt, wh_ratio, ratio_follow, thinking, parse_ok }
|
||||
}
|
||||
|
||||
async function waitPromptHistory(r: any, job: Job, promptId: string) {
|
||||
let history: any = null, missingSince = 0
|
||||
for (;;) {
|
||||
try { history = await fetchHistory(promptId) }
|
||||
catch { history = null }
|
||||
const e = history?.[promptId]
|
||||
if (job.status === 'cancelled') throw new Error('Cancelled')
|
||||
if (['error', 'interrupted'].includes(e?.status?.status_str))
|
||||
throw new Error('Comfy generation failed or was interrupted; inspect the host error log.')
|
||||
if (!e) {
|
||||
try {
|
||||
const response = await comfyFetch('/queue', { signal: AbortSignal.timeout(5000) })
|
||||
if (response.ok) {
|
||||
const q = await response.json() as any
|
||||
const present = [...(q.queue_running || []), ...(q.queue_pending || [])].some((entry: any) => entry[1] === promptId)
|
||||
if (present) missingSince = 0
|
||||
else if (!missingSince) missingSince = Date.now()
|
||||
else if (Date.now() - missingSince > 60000) throw new Error('PROMPT_MISSING')
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.message === 'PROMPT_MISSING')
|
||||
throw new Error('The saved Comfy prompt is no longer in history or the queue. It was not resubmitted.')
|
||||
}
|
||||
}
|
||||
if (e?.status?.completed || e?.status?.status_str === 'success') break
|
||||
if (job.status === 'cancelled') throw new Error('Cancelled')
|
||||
await new Promise(resolve => setTimeout(resolve, 1500))
|
||||
}
|
||||
return history
|
||||
}
|
||||
|
||||
async function runQwen21PromptEnhance(r: any, job: Job) {
|
||||
const q = r.request, s = q.settings
|
||||
if (!q.enhancePrompt || q.engine !== 'qwen21' || !['generate', 'edit'].includes(q.mode)) return
|
||||
const edit = q.mode === 'edit'
|
||||
if (edit && !q.imageAId) throw new Error('Choose the source still.')
|
||||
update(r, 'enhancing')
|
||||
emitJob(job, { type: 'status', message: 'Enhancing prompt…' })
|
||||
let prompt = String(q.compiledPrompt || '')
|
||||
if (edit && !/<image1>/i.test(prompt))
|
||||
prompt = `Keep the subject in <image1>. ${prompt}`.trim()
|
||||
const graph = structuredClone(edit ? qwen21PeEditTemplate : qwen21PeT2iTemplate)
|
||||
graph['9'].inputs.prompt = prompt
|
||||
graph['9'].inputs.seed = s.seed
|
||||
if (edit) {
|
||||
const a = await upload(r, 'pe-source', readFileSync(stillPath(r.owner, getStill(r.owner, q.imageAId).id)))
|
||||
graph['10'].inputs.image = a
|
||||
graph['9'].inputs.image_1 = ['10', 0]
|
||||
if (q.imageBId) {
|
||||
const b = await upload(r, 'pe-compose', readFileSync(stillPath(r.owner, getStill(r.owner, q.imageBId).id)))
|
||||
graph['11'] = {
|
||||
inputs: { image: b },
|
||||
class_type: 'LoadImage',
|
||||
_meta: { title: 'image_2 optional' }
|
||||
}
|
||||
graph['9'].inputs.image_2 = ['11', 0]
|
||||
}
|
||||
}
|
||||
r.peGraphId = edit ? 'studio2_qwen21_pe_edit.json' : 'studio2_qwen21_pe_t2i.json'
|
||||
saveRecord(r)
|
||||
const queued = await queuePrompt(graph, job.clientId)
|
||||
r.pePromptId = queued.prompt_id
|
||||
saveRecord(r)
|
||||
const history = await waitPromptHistory(r, job, queued.prompt_id)
|
||||
const result = harvestQwen21Pe(history, queued.prompt_id, edit)
|
||||
if (!result.parse_ok || !result.positive_prompt)
|
||||
throw new Error('Prompt enhance failed to parse a rewrite. Try again or turn Enhance prompt off.')
|
||||
r.request.promptRaw = q.compiledPrompt
|
||||
r.request.compiledPrompt = result.positive_prompt
|
||||
r.request.enhance = {
|
||||
wh_ratio: result.wh_ratio || '',
|
||||
...(edit ? { ratio_follow: result.ratio_follow || '' } : {}),
|
||||
parse_ok: true,
|
||||
thinking: result.thinking || ''
|
||||
}
|
||||
r.pePromptId = ''
|
||||
saveRecord(r)
|
||||
await freeComfyVram()
|
||||
}
|
||||
|
||||
async function upload(r: any, name: string, data: Buffer): Promise<string> {
|
||||
const prefix = String(useRuntimeConfig().comfyFilenamePrefix).replace(/\/$/, '') + `/studio2/${r.id}/${r.index}`;
|
||||
let fitted = false;
|
||||
@@ -234,56 +337,23 @@ async function run(r: any, job: Job) {
|
||||
r.request.compiledPrompt = compilePrompt(prompts[r.index], ['video', 'extend'].includes(r.request.mode), r.request);
|
||||
update(r, 'waking');
|
||||
await ensureComfyReady(message => emitJob(job, { type: 'status', message }));
|
||||
if (job.status === 'cancelled')
|
||||
throw new Error('Cancelled');
|
||||
await runQwen21PromptEnhance(r, job);
|
||||
if (job.status === 'cancelled')
|
||||
throw new Error('Cancelled');
|
||||
const graph = await prepareGraph(r);
|
||||
if (job.status === 'cancelled')
|
||||
throw new Error('Cancelled');
|
||||
update(r, 'submitting');
|
||||
emitJob(job, { type: 'status', message: 'Generating…' });
|
||||
const queued = await queuePrompt(graph, job.clientId);
|
||||
job.promptId = queued.prompt_id;
|
||||
r.promptId = queued.prompt_id;
|
||||
r.renderStartedAt = Date.now();
|
||||
update(r, 'rendering');
|
||||
}
|
||||
let history: any, missingSince = 0;
|
||||
for (;;) {
|
||||
try {
|
||||
history = await fetchHistory(r.promptId);
|
||||
}
|
||||
catch {
|
||||
history = null;
|
||||
}
|
||||
const e = history?.[r.promptId];
|
||||
if (job.status === 'cancelled')
|
||||
throw new Error('Cancelled');
|
||||
if (['error', 'interrupted'].includes(e?.status?.status_str))
|
||||
throw new Error('Comfy generation failed or was interrupted; inspect the host error log.');
|
||||
if (!e) {
|
||||
try {
|
||||
const response = await comfyFetch('/queue', { signal: AbortSignal.timeout(5000) });
|
||||
if (response.ok) {
|
||||
const q = await response.json() as any;
|
||||
const present = [...(q.queue_running || []), ...(q.queue_pending || [])].some((entry: any) => entry[1] === r.promptId);
|
||||
if (present)
|
||||
missingSince = 0;
|
||||
else if (!missingSince)
|
||||
missingSince = Date.now();
|
||||
else if (Date.now() - missingSince > 60000)
|
||||
throw new Error('PROMPT_MISSING');
|
||||
}
|
||||
}
|
||||
catch (error: any) {
|
||||
if (error.message === 'PROMPT_MISSING')
|
||||
throw new Error('The saved Comfy prompt is no longer in history or the queue. It was not resubmitted.');
|
||||
}
|
||||
}
|
||||
if (e?.status?.completed || e?.status?.status_str === 'success')
|
||||
break;
|
||||
if (job.status === 'cancelled')
|
||||
throw new Error('Cancelled');
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
}
|
||||
const history = await waitPromptHistory(r, job, r.promptId);
|
||||
const messages = history[r.promptId]?.status?.messages || [];
|
||||
const executionStart = messages.find((m: any) => m[0] === 'execution_start')?.[1]?.timestamp;
|
||||
const executionEnd = messages.find((m: any) => m[0] === 'execution_success')?.[1]?.timestamp;
|
||||
|
||||
@@ -61,7 +61,8 @@ export function validateRequest(raw, xaigen = false) {
|
||||
}
|
||||
const frames=Math.ceil((duration*number('fps',24,1,60)-5)/17)*17+5
|
||||
const shotSeeds=queueSeeds(raw.settings,1+(Array.isArray(raw.batch)?raw.batch.length:0))
|
||||
return { mode, engine, shotSeeds, imageStyles:video?{positive:[],negative:[]}:normalizeStyles(raw.imageStyles), hideOutput:!!raw.hideOutput, lockFace:qwen ? false : raw.lockFace !== false, lockOutfit:qwen ? false : raw.lockOutfit !== false, hideInput:!!raw.hideInput, folderId:String(raw.folderId), identityStillId:qwen ? '' : (raw.identityStillId || ''), imageAId:raw.imageAId || '', imageBId:raw.imageBId || '', maskId:raw.maskId || '', startClipId:raw.startClipId || '', endStillId:raw.endStillId || '',
|
||||
const enhancePrompt = qwen && !video && !!raw.enhancePrompt
|
||||
return { mode, engine, shotSeeds, imageStyles:video?{positive:[],negative:[]}:normalizeStyles(raw.imageStyles), hideOutput:!!raw.hideOutput, enhancePrompt, lockFace:qwen ? false : raw.lockFace !== false, lockOutfit:qwen ? false : raw.lockOutfit !== false, hideInput:!!raw.hideInput, folderId:String(raw.folderId), identityStillId:qwen ? '' : (raw.identityStillId || ''), imageAId:raw.imageAId || '', imageBId:raw.imageBId || '', maskId:raw.maskId || '', startClipId:raw.startClipId || '', endStillId:raw.endStillId || '',
|
||||
startFrameSource: raw.startClipId ? {kind:'previous-last-frame',clipId:raw.startClipId} : {kind:'still',stillId:raw.imageAId || ''},
|
||||
promptSections, compiledPrompt:compilePrompt(promptSections,video,{...raw,engine,lockFace:qwen?false:raw.lockFace,lockOutfit:qwen?false:raw.lockOutfit}),
|
||||
settings: { aspect:['auto','16:9','9:16','1:1'].includes(raw.settings?.aspect)?raw.settings.aspect:'auto', fps:number('fps',24,1,60), width:number('width',defaultWidth,256,4096),height:number('height',defaultHeight,256,4096),steps:number('steps',defaultSteps,1,100),cfg:number('cfg',defaultCfg,0,20),seedMode:raw.settings?.seedMode==='locked'?'locked':'random',seed:shotSeeds[0],duration:number('duration',5,1,15),turbo:raw.settings?.turbo !== false, loraStack:qwen?[]:(Array.isArray(raw.settings?.loraStack)?raw.settings.loraStack:[]) },
|
||||
|
||||
@@ -110,6 +110,7 @@ export function hydrateStudio2Job(form, saved, { stillIds = new Set(), clipIds =
|
||||
...form,
|
||||
mode,
|
||||
engine,
|
||||
enhancePrompt: engine === 'qwen21' && !!src.enhancePrompt,
|
||||
lockFace: src.lockFace !== false,
|
||||
lockOutfit: src.lockOutfit !== false,
|
||||
hideInput: !!src.hideInput,
|
||||
|
||||
@@ -8,8 +8,8 @@ export function gpuStatus(jobs, health, upscales=[], musicJobs=[]) {
|
||||
return {label:music.status==='waiting'?'Queued':'Generating',tone:'busy',detail:String(stage)}
|
||||
}
|
||||
if(health.yue2?.busy)return {label:'Generating',tone:'busy',detail:'YuE2 is using the GPU'}
|
||||
const job=jobs.find(j=>['waking','submitting','rendering','saving'].includes(j.state))
|
||||
if(job)return {label:{waking:'Waking up',submitting:'Submitting',rendering:'Generating',saving:'Saving'}[job.state],tone:'busy',detail:job.request?.promptSections?.action || 'Generation in progress'}
|
||||
const job=jobs.find(j=>['waking','enhancing','submitting','rendering','saving'].includes(j.state))
|
||||
if(job)return {label:{waking:'Waking up',enhancing:'Enhancing prompt…',submitting:'Submitting',rendering:'Generating…',saving:'Saving'}[job.state],tone:'busy',detail:job.request?.promptSections?.action || 'Generation in progress'}
|
||||
if(health.comfy?.busy || health.imageComfy?.busy)return {label:'Busy',tone:'busy',detail:'GPU is working on another job'}
|
||||
if(health.comfy?.ok || health.imageComfy?.ok)return {label:'Ready',tone:'ready',detail:'No job running'}
|
||||
if(health.comfy?.asleep)return {label:'Sleeping',tone:'sleeping',detail:'Wakes when a job starts'}
|
||||
|
||||
@@ -81,6 +81,10 @@ test('qwen21 supports Generate and Edit with 25/1 defaults and no locks',()=>{
|
||||
assert.equal(q.engine,'qwen21');assert.equal(q.settings.steps,25);assert.equal(q.settings.cfg,1)
|
||||
assert.equal(q.settings.width,1024);assert.equal(q.settings.height,1024)
|
||||
assert.equal(q.lockFace,false);assert.equal(q.lockOutfit,false);assert.deepEqual(q.settings.loraStack,[])
|
||||
assert.equal(q.enhancePrompt,false)
|
||||
const enhanced=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',enhancePrompt:true,promptSections:{action:'corgi'},settings:{aspect:'1:1'}})
|
||||
assert.equal(enhanced.enhancePrompt,true)
|
||||
assert.equal(validateRequest({mode:'generate',engine:'flux',folderId:'f',enhancePrompt:true,promptSections:{action:'x'},settings:{aspect:'1:1'}}).enhancePrompt,false)
|
||||
const edit=validateRequest({mode:'edit',engine:'qwen21',folderId:'f',imageAId:'s',promptSections:{action:'change the sky'},settings:{aspect:'auto'}})
|
||||
assert.equal(edit.mode,'edit');assert.equal(edit.settings.steps,25);assert.equal(edit.settings.cfg,1)
|
||||
assert.equal(edit.lockFace,false);assert.deepEqual(edit.settings.loraStack,[])
|
||||
@@ -105,4 +109,11 @@ test('custom negative chips survive normalizeStyles and bench exposes Load actio
|
||||
const src=readFileSync(new URL('../components/studio2/Bench.vue',import.meta.url),'utf8')
|
||||
assert.match(src,/loadSelectedJob/);assert.match(src,/hydrateStudio2Job/);assert.match(src,/Edit source/)
|
||||
assert.match(src,/aspect:'auto'/);assert.match(src,/strip-actions/);assert.match(src,/addNegative/)
|
||||
assert.match(src,/Enhance prompt/);assert.match(src,/enhancePrompt/)
|
||||
const peT2i=readFileSync(new URL('../server/assets/studio2_qwen21_pe_t2i.json',import.meta.url),'utf8')
|
||||
const peEdit=readFileSync(new URL('../server/assets/studio2_qwen21_pe_edit.json',import.meta.url),'utf8')
|
||||
assert.match(peT2i,/QwenImage21_T2IPromptRewrite/);assert.match(peT2i,/pe_t2i\.int8_convrot/)
|
||||
assert.match(peEdit,/QwenImage21_EditPromptRewrite/);assert.match(peEdit,/pe_i2i\.int8_convrot/)
|
||||
assert.doesNotMatch(peT2i,/UnetLoader|KSampler|VAELoader/)
|
||||
assert.doesNotMatch(peEdit,/UnetLoader|KSampler|VAELoader/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user