Wire Qwen Turbo toggle through Studio 2 and the runner.
Force 6/1 when Turbo is on, skip negatives, keep PE in front, and fail closed if the r128 LoRA is missing. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -119,6 +119,12 @@
|
|||||||
<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)]">
|
<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
|
<input v-model="form.enhancePrompt" type="checkbox" class="accent-[var(--amber)]">Enhance prompt
|
||||||
</label>
|
</label>
|
||||||
|
<div v-if="form.engine==='qwen21' && (form.mode==='generate' || form.mode==='edit')" class="flex flex-col gap-1">
|
||||||
|
<label class="inline-flex items-center gap-2 text-[var(--type-sm)] text-[var(--muted)]">
|
||||||
|
<input v-model="form.turbo" type="checkbox" class="accent-[var(--amber)]">Turbo
|
||||||
|
</label>
|
||||||
|
<p v-if="form.turbo" class="text-[11px] text-[var(--muted)]">6-step Viggle distill. CFG stays 1. Enhance prompt recommended.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span class="field-label">Negative prompt</span>
|
<span class="field-label">Negative prompt</span>
|
||||||
@@ -379,6 +385,7 @@ import {LOAD_GENERATION_PAYLOAD_KEY} from '~/utils/queuedJob'
|
|||||||
|
|
||||||
const NEG_STORAGE_KEY='studio2-negative-chips'
|
const NEG_STORAGE_KEY='studio2-negative-chips'
|
||||||
const ENHANCE_STORAGE_KEY='studio2-qwen21-enhance-prompt'
|
const ENHANCE_STORAGE_KEY='studio2-qwen21-enhance-prompt'
|
||||||
|
const TURBO_STORAGE_KEY='studio2-qwen21-turbo'
|
||||||
function readStoredNegatives(){
|
function readStoredNegatives(){
|
||||||
if(!import.meta.client)return [...defaultNegatives]
|
if(!import.meta.client)return [...defaultNegatives]
|
||||||
try{
|
try{
|
||||||
@@ -400,6 +407,14 @@ function writeStoredEnhancePrompt(on:boolean){
|
|||||||
if(!import.meta.client)return
|
if(!import.meta.client)return
|
||||||
try{localStorage.setItem(ENHANCE_STORAGE_KEY,on?'1':'0')}catch{/* ignore */}
|
try{localStorage.setItem(ENHANCE_STORAGE_KEY,on?'1':'0')}catch{/* ignore */}
|
||||||
}
|
}
|
||||||
|
function readStoredTurbo(){
|
||||||
|
if(!import.meta.client)return false
|
||||||
|
try{return localStorage.getItem(TURBO_STORAGE_KEY)==='1'}catch{return false}
|
||||||
|
}
|
||||||
|
function writeStoredTurbo(on:boolean){
|
||||||
|
if(!import.meta.client)return
|
||||||
|
try{localStorage.setItem(TURBO_STORAGE_KEY,on?'1':'0')}catch{/* ignore */}
|
||||||
|
}
|
||||||
|
|
||||||
const xaigen=isXaigenStudio(useRuntimeConfig().public)
|
const xaigen=isXaigenStudio(useRuntimeConfig().public)
|
||||||
const instanceName=String(useRuntimeConfig().public.instanceName || 'AIGen')
|
const instanceName=String(useRuntimeConfig().public.instanceName || 'AIGen')
|
||||||
@@ -419,7 +434,7 @@ const liveMusic=ref<Record<string,any>>({})
|
|||||||
const negativeDraft=ref('')
|
const negativeDraft=ref('')
|
||||||
const filmstripEl=ref<HTMLElement|null>(null)
|
const filmstripEl=ref<HTMLElement|null>(null)
|
||||||
const tileRefs=new Map<string,HTMLElement>()
|
const tileRefs=new Map<string,HTMLElement>()
|
||||||
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 form=reactive<any>({mode:'generate',engine:'flux',enhancePrompt:readStoredEnhancePrompt(),turbo:readStoredTurbo(),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 music=reactive({tags:DEFAULT_MUSIC_TAGS,lyrics:DEFAULT_MUSIC_LYRICS,duration:60,seed:'',name:''})
|
||||||
const tagsPlaceholder=DEFAULT_MUSIC_TAGS,lyricsPlaceholder=DEFAULT_MUSIC_LYRICS
|
const tagsPlaceholder=DEFAULT_MUSIC_TAGS,lyricsPlaceholder=DEFAULT_MUSIC_LYRICS
|
||||||
let loraMemory=createLoraMemory(xaigen)
|
let loraMemory=createLoraMemory(xaigen)
|
||||||
@@ -456,7 +471,7 @@ watch(engineOptions,opts=>{
|
|||||||
})
|
})
|
||||||
watch(()=>form.engine,(engine,prev)=>{
|
watch(()=>form.engine,(engine,prev)=>{
|
||||||
if(engine==='qwen21' && prev!=='qwen21'){
|
if(engine==='qwen21' && prev!=='qwen21'){
|
||||||
form.settings.steps=25
|
form.settings.steps=form.turbo?6:25
|
||||||
form.settings.cfg=1
|
form.settings.cfg=1
|
||||||
form.lockFace=false
|
form.lockFace=false
|
||||||
form.lockOutfit=false
|
form.lockOutfit=false
|
||||||
@@ -464,9 +479,15 @@ watch(()=>form.engine,(engine,prev)=>{
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
watch(()=>form.enhancePrompt,on=>writeStoredEnhancePrompt(!!on))
|
watch(()=>form.enhancePrompt,on=>writeStoredEnhancePrompt(!!on))
|
||||||
|
watch(()=>form.turbo,on=>{
|
||||||
|
writeStoredTurbo(!!on)
|
||||||
|
if(form.engine!=='qwen21')return
|
||||||
|
form.settings.steps=on?6:25
|
||||||
|
form.settings.cfg=1
|
||||||
|
})
|
||||||
watch(()=>[form.mode,form.engine],([mode,engine])=>{
|
watch(()=>[form.mode,form.engine],([mode,engine])=>{
|
||||||
if(engine==='qwen21' && mode==='edit'){
|
if(engine==='qwen21' && mode==='edit'){
|
||||||
form.settings.steps=25
|
form.settings.steps=form.turbo?6:25
|
||||||
form.settings.cfg=1
|
form.settings.cfg=1
|
||||||
form.lockFace=false
|
form.lockFace=false
|
||||||
form.lockOutfit=false
|
form.lockOutfit=false
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const rows=computed(()=>{const m=props.metadata||{},s=m.settings||{};const time=
|
|||||||
{label:'Compiled prompt',value:m.compiledPrompt},
|
{label:'Compiled prompt',value:m.compiledPrompt},
|
||||||
{label:'Prompt (raw)',value:m.promptRaw || null},
|
{label:'Prompt (raw)',value:m.promptRaw || null},
|
||||||
{label:'Enhance prompt',value:m.enhancePrompt==null?null:m.enhancePrompt?'On':'Off'},
|
{label:'Enhance prompt',value:m.enhancePrompt==null?null:m.enhancePrompt?'On':'Off'},
|
||||||
|
{label:'Turbo',value:m.turbo==null?null:m.turbo?'On':'Off'},
|
||||||
|
{label:'Turbo LoRA',value:m.lora || null},
|
||||||
|
{label:'Turbo sigmas',value:m.sigmas || null},
|
||||||
{label:'Enhance wh_ratio',value:m.enhance?.wh_ratio || null},
|
{label:'Enhance wh_ratio',value:m.enhance?.wh_ratio || null},
|
||||||
{label:'Enhance ratio_follow',value:m.enhance?.ratio_follow || 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 parse_ok',value:m.enhance?.parse_ok==null?null:m.enhance.parse_ok?'Yes':'No'},
|
||||||
|
|||||||
@@ -159,6 +159,8 @@
|
|||||||
<template v-else>
|
<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 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>
|
<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>
|
||||||
|
<label v-if="ctx.form.engine==='qwen21' && (ctx.form.mode==='generate' || ctx.form.mode==='edit')" class="m-check"><input v-model="ctx.form.turbo" type="checkbox"> Turbo</label>
|
||||||
|
<p v-if="ctx.form.engine==='qwen21' && ctx.form.turbo" class="m-muted">6-step Viggle distill. CFG stays 1. Enhance prompt recommended.</p>
|
||||||
<div>
|
<div>
|
||||||
<span class="m-label">Negative prompt</span>
|
<span class="m-label">Negative prompt</span>
|
||||||
<div class="m-neg">
|
<div class="m-neg">
|
||||||
|
|||||||
+3
-2
@@ -42,6 +42,7 @@ Restart Comfy **only when idle** (`COMFY_CONTROL_URL/status` → `gpu.busy=false
|
|||||||
- Edit follows `image_1` via the encode node’s latent (resolution long-edge ~1024). Do not inject a picker EmptyLatentImage onto the edit sampler.
|
- Edit follows `image_1` via the encode node’s latent (resolution long-edge ~1024). Do not inject a picker EmptyLatentImage onto the edit sampler.
|
||||||
- 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.
|
- 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`.
|
- 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`).
|
- Optional **Turbo** (off by default): Viggle DMD LoRA `Qwen-Image-2.1-viggle-turbo-v0.2.1-6step-lora-r128.safetensors` on the same Q8 GGUF via `ViggleTurboLora` + `ViggleTurboSigmas` (`1.0, 0.9375, 0.875, 0.75, 0.5, 0.25`), 6 steps, CFG 1, empty negative. Not a new engine. Enhance prompt stays compatible and is recommended with Turbo.
|
||||||
|
- Custom node: `ComfyUI-Viggle-Turbo` (`viggle_turbo.py`). Do not merge the LoRA with stock `LoraLoaderModelOnly` (lossy on int8/bf16).
|
||||||
|
|
||||||
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`.
|
Graphs: `server/assets/studio2_qwen21_t2i.json`, `server/assets/studio2_qwen21_edit.json`, `server/assets/studio2_qwen21_t2i_turbo.json`, `server/assets/studio2_qwen21_edit_turbo.json`, `server/assets/studio2_qwen21_pe_t2i.json`, `server/assets/studio2_qwen21_pe_edit.json`.
|
||||||
|
|||||||
@@ -10,11 +10,14 @@ import { join } from 'node:path';
|
|||||||
import template from '../../assets/studio2_minimax_native.json';
|
import template from '../../assets/studio2_minimax_native.json';
|
||||||
import qwen21Template from '../../assets/studio2_qwen21_t2i.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 qwen21T2iTurboTemplate from '../../assets/studio2_qwen21_t2i_turbo.json'
|
||||||
|
import qwen21EditTurboTemplate from '../../assets/studio2_qwen21_edit_turbo.json'
|
||||||
import qwen21PeT2iTemplate from '../../assets/studio2_qwen21_pe_t2i.json'
|
import qwen21PeT2iTemplate from '../../assets/studio2_qwen21_pe_t2i.json'
|
||||||
import qwen21PeEditTemplate from '../../assets/studio2_qwen21_pe_edit.json';
|
import qwen21PeEditTemplate from '../../assets/studio2_qwen21_pe_edit.json';
|
||||||
import { nativeVideoGraph, attachHeroReference, applyResolvedImageSize } from '~/shared/studio2/graphs.mjs';
|
import { nativeVideoGraph, attachHeroReference, applyResolvedImageSize } from '~/shared/studio2/graphs.mjs';
|
||||||
import { compilePrompt, scopedFile } from '~/shared/studio2/contracts.mjs';
|
import { compilePrompt, scopedFile } from '~/shared/studio2/contracts.mjs';
|
||||||
import { resolveQwen21Size } from '~/shared/studio2/qwen21-size.mjs';
|
import { resolveQwen21Size } from '~/shared/studio2/qwen21-size.mjs';
|
||||||
|
import { QWEN21_TURBO_LORA, QWEN21_TURBO_SIGMAS, QWEN21_TURBO_STEPS } from '~/shared/studio2/qwen21-turbo.mjs';
|
||||||
import { createJob, restoreJob, getJob, emitJob, type Job } from '../jobs';
|
import { createJob, restoreJob, getJob, emitJob, type Job } from '../jobs';
|
||||||
import { markStudioLive, onLiveVideoSettled, type StudioJob } from '../studioQueue';
|
import { markStudioLive, onLiveVideoSettled, type StudioJob } from '../studioQueue';
|
||||||
import { readRecord, saveRecord, studio2Root, records } from './store';
|
import { readRecord, saveRecord, studio2Root, records } from './store';
|
||||||
@@ -235,12 +238,20 @@ async function prepareGraph(r: any) {
|
|||||||
else if (q.engine === 'qwen21') {
|
else if (q.engine === 'qwen21') {
|
||||||
if (!['generate', 'edit'].includes(q.mode))
|
if (!['generate', 'edit'].includes(q.mode))
|
||||||
throw new Error('Qwen 2.1 supports Generate and Edit.')
|
throw new Error('Qwen 2.1 supports Generate and Edit.')
|
||||||
|
const turbo = !!q.turbo
|
||||||
|
if (turbo) {
|
||||||
|
q.lora = QWEN21_TURBO_LORA
|
||||||
|
q.sigmas = QWEN21_TURBO_SIGMAS
|
||||||
|
s.steps = QWEN21_TURBO_STEPS
|
||||||
|
s.cfg = 1
|
||||||
|
}
|
||||||
|
const negative = turbo ? '' : stylePrompt(q.imageStyles, true)
|
||||||
if (q.mode === 'edit') {
|
if (q.mode === 'edit') {
|
||||||
if (!a) throw new Error('Choose the source still.')
|
if (!a) throw new Error('Choose the source still.')
|
||||||
let prompt = String(q.compiledPrompt || '')
|
let prompt = String(q.compiledPrompt || '')
|
||||||
if (!/<image1>/i.test(prompt))
|
if (!/<image1>/i.test(prompt))
|
||||||
prompt = `Keep the subject in <image1>. ${prompt}`.trim()
|
prompt = `Keep the subject in <image1>. ${prompt}`.trim()
|
||||||
graph = structuredClone(qwen21EditTemplate)
|
graph = structuredClone(turbo ? qwen21EditTurboTemplate : qwen21EditTemplate)
|
||||||
graph['10'].inputs.image = a
|
graph['10'].inputs.image = a
|
||||||
if (b) {
|
if (b) {
|
||||||
graph['11'] = {
|
graph['11'] = {
|
||||||
@@ -251,10 +262,18 @@ async function prepareGraph(r: any) {
|
|||||||
graph['9'].inputs['images.image_2'] = ['11', 0]
|
graph['9'].inputs['images.image_2'] = ['11', 0]
|
||||||
}
|
}
|
||||||
graph['9'].inputs.prompt = prompt
|
graph['9'].inputs.prompt = prompt
|
||||||
graph['9'].inputs.negative_prompt = stylePrompt(q.imageStyles, true)
|
graph['9'].inputs.negative_prompt = negative
|
||||||
// Official edit: sampler latent comes from TextEncode (sized from image_1).
|
// Official edit: sampler latent comes from TextEncode (sized from image_1).
|
||||||
// Do not inject EmptyLatentImage — a mismatched empty canvas shifts the edit.
|
|
||||||
graph['9'].inputs.resolution = 1024
|
graph['9'].inputs.resolution = 1024
|
||||||
|
if (turbo) {
|
||||||
|
graph['7'].inputs.lora_name = QWEN21_TURBO_LORA
|
||||||
|
graph['7'].inputs.strength = 1.0
|
||||||
|
graph['31'].inputs.noise_seed = s.seed
|
||||||
|
graph['33'].inputs.nodes = QWEN21_TURBO_SIGMAS
|
||||||
|
graph['36'].inputs.filename_prefix = prefix + '/image'
|
||||||
|
r.sampleLatent = 'qwen21 edit latent'
|
||||||
|
r.graphId = 'studio2_qwen21_edit_turbo.json'
|
||||||
|
} else {
|
||||||
graph['15'].inputs.seed = s.seed
|
graph['15'].inputs.seed = s.seed
|
||||||
graph['15'].inputs.steps = s.steps || 25
|
graph['15'].inputs.steps = s.steps || 25
|
||||||
graph['15'].inputs.cfg = s.cfg ?? 1
|
graph['15'].inputs.cfg = s.cfg ?? 1
|
||||||
@@ -262,21 +281,30 @@ async function prepareGraph(r: any) {
|
|||||||
graph['15'].inputs.scheduler = 'simple'
|
graph['15'].inputs.scheduler = 'simple'
|
||||||
graph['21'].inputs.filename_prefix = prefix + '/image'
|
graph['21'].inputs.filename_prefix = prefix + '/image'
|
||||||
r.sampleLatent = 'qwen21 edit latent'
|
r.sampleLatent = 'qwen21 edit latent'
|
||||||
|
r.graphId = 'studio2_qwen21_edit.json'
|
||||||
|
}
|
||||||
r.sampleDenoise = null
|
r.sampleDenoise = null
|
||||||
r.heroReferenceAttached = false
|
r.heroReferenceAttached = false
|
||||||
r.graphId = 'studio2_qwen21_edit.json'
|
|
||||||
} else {
|
} else {
|
||||||
// Bench aspect is law for Generate. PE wh_ratio is advisory only (never sizes the canvas).
|
// Bench aspect is law for Generate. PE wh_ratio is advisory only (never sizes the canvas).
|
||||||
const size = resolveQwen21Size(s.aspect)
|
const size = resolveQwen21Size(s.aspect)
|
||||||
s.width = size.width
|
s.width = size.width
|
||||||
s.height = size.height
|
s.height = size.height
|
||||||
graph = structuredClone(qwen21Template)
|
graph = structuredClone(turbo ? qwen21T2iTurboTemplate : qwen21Template)
|
||||||
graph['9'].inputs.prompt = q.compiledPrompt
|
graph['9'].inputs.prompt = q.compiledPrompt
|
||||||
graph['9'].inputs.negative_prompt = stylePrompt(q.imageStyles, true)
|
graph['9'].inputs.negative_prompt = negative
|
||||||
// TextEncode conditioning only — EmptyLatentImage (node 16) owns canvas size, matching the official T2I template.
|
|
||||||
graph['9'].inputs.resolution = 1024
|
graph['9'].inputs.resolution = 1024
|
||||||
graph['16'].inputs.width = size.width
|
graph['16'].inputs.width = size.width
|
||||||
graph['16'].inputs.height = size.height
|
graph['16'].inputs.height = size.height
|
||||||
|
if (turbo) {
|
||||||
|
graph['7'].inputs.lora_name = QWEN21_TURBO_LORA
|
||||||
|
graph['7'].inputs.strength = 1.0
|
||||||
|
graph['31'].inputs.noise_seed = s.seed
|
||||||
|
graph['33'].inputs.nodes = QWEN21_TURBO_SIGMAS
|
||||||
|
graph['36'].inputs.filename_prefix = prefix + '/image'
|
||||||
|
r.sampleLatent = 'qwen21 empty latent'
|
||||||
|
r.graphId = 'studio2_qwen21_t2i_turbo.json'
|
||||||
|
} else {
|
||||||
graph['15'].inputs.seed = s.seed
|
graph['15'].inputs.seed = s.seed
|
||||||
graph['15'].inputs.steps = s.steps || 25
|
graph['15'].inputs.steps = s.steps || 25
|
||||||
graph['15'].inputs.cfg = s.cfg ?? 1
|
graph['15'].inputs.cfg = s.cfg ?? 1
|
||||||
@@ -284,9 +312,10 @@ async function prepareGraph(r: any) {
|
|||||||
graph['15'].inputs.scheduler = 'simple'
|
graph['15'].inputs.scheduler = 'simple'
|
||||||
graph['21'].inputs.filename_prefix = prefix + '/image'
|
graph['21'].inputs.filename_prefix = prefix + '/image'
|
||||||
r.sampleLatent = 'qwen21 empty latent'
|
r.sampleLatent = 'qwen21 empty latent'
|
||||||
|
r.graphId = 'studio2_qwen21_t2i.json'
|
||||||
|
}
|
||||||
r.sampleDenoise = null
|
r.sampleDenoise = null
|
||||||
r.heroReferenceAttached = false
|
r.heroReferenceAttached = false
|
||||||
r.graphId = 'studio2_qwen21_t2i.json'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -344,7 +373,15 @@ async function run(r: any, job: Job) {
|
|||||||
throw new Error('Cancelled');
|
throw new Error('Cancelled');
|
||||||
update(r, 'submitting');
|
update(r, 'submitting');
|
||||||
emitJob(job, { type: 'status', message: 'Generating…' });
|
emitJob(job, { type: 'status', message: 'Generating…' });
|
||||||
const queued = await queuePrompt(graph, job.clientId);
|
let queued: { prompt_id: string; number?: number }
|
||||||
|
try {
|
||||||
|
queued = await queuePrompt(graph, job.clientId);
|
||||||
|
} catch (error: any) {
|
||||||
|
const msg = String(error?.statusMessage || error?.message || error)
|
||||||
|
if (r.request?.turbo && /lora|ViggleTurbo|not in list|value_not_in_list/i.test(msg))
|
||||||
|
throw new Error(`Qwen Turbo LoRA missing on host (${QWEN21_TURBO_LORA}). Install the r128 file under models/loras and restart Comfy.`)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
job.promptId = queued.prompt_id;
|
job.promptId = queued.prompt_id;
|
||||||
r.promptId = queued.prompt_id;
|
r.promptId = queued.prompt_id;
|
||||||
r.renderStartedAt = Date.now();
|
r.renderStartedAt = Date.now();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {normalizeStyles,stylePrompt} from './styles.mjs'
|
|||||||
import { queueSeeds } from './seed.mjs'
|
import { queueSeeds } from './seed.mjs'
|
||||||
import { sectionsFrom, compileSections } from './prompt.mjs'
|
import { sectionsFrom, compileSections } from './prompt.mjs'
|
||||||
import { QWEN21_ASPECT_IDS, resolveQwen21Size } from './qwen21-size.mjs'
|
import { QWEN21_ASPECT_IDS, resolveQwen21Size } from './qwen21-size.mjs'
|
||||||
|
import { QWEN21_TURBO_CFG, QWEN21_TURBO_LORA, QWEN21_TURBO_SIGMAS, QWEN21_TURBO_STEPS } from './qwen21-turbo.mjs'
|
||||||
export { sectionsFrom } from './prompt.mjs'
|
export { sectionsFrom } from './prompt.mjs'
|
||||||
export const cameraFamilies = ['static','dolly in','dolly out','pan left','pan right','tilt up','tilt down','zoom in','zoom out','orbit left','orbit right']
|
export const cameraFamilies = ['static','dolly in','dolly out','pan left','pan right','tilt up','tilt down','zoom in','zoom out','orbit left','orbit right']
|
||||||
export const identityLock = '<Picture 1> / hero is identity for face, hair, glasses, body, and wardrobe. Keep the person and clothes fully preserved. Start frame is composition, pose, place, and camera only. Do not copy facial drift from the start frame.'
|
export const identityLock = '<Picture 1> / hero is identity for face, hair, glasses, body, and wardrobe. Keep the person and clothes fully preserved. Start frame is composition, pose, place, and camera only. Do not copy facial drift from the start frame.'
|
||||||
@@ -65,12 +66,15 @@ export function validateRequest(raw, xaigen = false) {
|
|||||||
const frames=Math.ceil((duration*number('fps',24,1,60)-5)/17)*17+5
|
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))
|
const shotSeeds=queueSeeds(raw.settings,1+(Array.isArray(raw.batch)?raw.batch.length:0))
|
||||||
const enhancePrompt = qwen && !video && !!raw.enhancePrompt
|
const enhancePrompt = qwen && !video && !!raw.enhancePrompt
|
||||||
|
const turbo = qwen && !video && !!raw.turbo
|
||||||
const width = qwenSize ? qwenSize.width : number('width',defaultWidth,256,4096)
|
const width = qwenSize ? qwenSize.width : number('width',defaultWidth,256,4096)
|
||||||
const height = qwenSize ? qwenSize.height : number('height',defaultHeight,256,4096)
|
const height = qwenSize ? qwenSize.height : number('height',defaultHeight,256,4096)
|
||||||
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 || '',
|
const steps = turbo ? QWEN21_TURBO_STEPS : number('steps',defaultSteps,1,100)
|
||||||
|
const cfg = turbo ? QWEN21_TURBO_CFG : number('cfg',defaultCfg,0,20)
|
||||||
|
return { mode, engine, shotSeeds, imageStyles:video?{positive:[],negative:[]}:normalizeStyles(raw.imageStyles), hideOutput:!!raw.hideOutput, enhancePrompt, turbo, ...(turbo ? { lora: QWEN21_TURBO_LORA, sigmas: QWEN21_TURBO_SIGMAS } : {}), 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 || ''},
|
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}),
|
promptSections, compiledPrompt:compilePrompt(promptSections,video,{...raw,engine,lockFace:qwen?false:raw.lockFace,lockOutfit:qwen?false:raw.lockOutfit}),
|
||||||
settings: { aspect, fps:number('fps',24,1,60), width, height, 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:[]) },
|
settings: { aspect, fps:number('fps',24,1,60), width, height, steps, cfg, 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:[]) },
|
||||||
guides: (Array.isArray(raw.guides)?raw.guides:[]).slice(0,8).map(g=>{ if (!g.stillId || !Number.isInteger(g.frame) || g.frame<1 || g.frame>=frames) fail('Each guide requires a still and positive frame index.'); return {stillId:String(g.stillId),frame:g.frame} }),
|
guides: (Array.isArray(raw.guides)?raw.guides:[]).slice(0,8).map(g=>{ if (!g.stillId || !Number.isInteger(g.frame) || g.frame<1 || g.frame>=frames) fail('Each guide requires a still and positive frame index.'); return {stillId:String(g.stillId),frame:g.frame} }),
|
||||||
batch: (Array.isArray(raw.batch)?raw.batch:[]).slice(0,30).map(s=>{const next=sectionsFrom({...promptSections,...(typeof s==='string'?{action:s}:s)});if(!next.action.trim())fail('Every shot needs an action prompt.');return video?next:{action:next.action}}) }
|
batch: (Array.isArray(raw.batch)?raw.batch:[]).slice(0,30).map(s=>{const next=sectionsFrom({...promptSections,...(typeof s==='string'?{action:s}:s)});if(!next.action.trim())fail('Every shot needs an action prompt.');return video?next:{action:next.action}}) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ export function hydrateStudio2Job(form, saved, { stillIds = new Set(), clipIds =
|
|||||||
mode,
|
mode,
|
||||||
engine,
|
engine,
|
||||||
enhancePrompt: engine === 'qwen21' && !!src.enhancePrompt,
|
enhancePrompt: engine === 'qwen21' && !!src.enhancePrompt,
|
||||||
|
turbo: engine === 'qwen21' && !!src.turbo,
|
||||||
lockFace: src.lockFace !== false,
|
lockFace: src.lockFace !== false,
|
||||||
lockOutfit: src.lockOutfit !== false,
|
lockOutfit: src.lockOutfit !== false,
|
||||||
hideInput: !!src.hideInput,
|
hideInput: !!src.hideInput,
|
||||||
|
|||||||
@@ -83,13 +83,18 @@ test('qwen21 supports Generate and Edit with 25/1 defaults and no locks',()=>{
|
|||||||
assert.equal(q.settings.width,1024);assert.equal(q.settings.height,1024)
|
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.lockFace,false);assert.equal(q.lockOutfit,false);assert.deepEqual(q.settings.loraStack,[])
|
||||||
assert.equal(q.enhancePrompt,false)
|
assert.equal(q.enhancePrompt,false)
|
||||||
|
assert.equal(q.turbo,false)
|
||||||
const wide=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',promptSections:{action:'wide'},settings:{aspect:'16:9'}})
|
const wide=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',promptSections:{action:'wide'},settings:{aspect:'16:9'}})
|
||||||
assert.equal(wide.settings.width,1536);assert.equal(wide.settings.height,864)
|
assert.equal(wide.settings.width,1536);assert.equal(wide.settings.height,864)
|
||||||
const tall=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',promptSections:{action:'tall'},settings:{aspect:'9:16'}})
|
const tall=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',promptSections:{action:'tall'},settings:{aspect:'9:16'}})
|
||||||
assert.equal(tall.settings.width,864);assert.equal(tall.settings.height,1536)
|
assert.equal(tall.settings.width,864);assert.equal(tall.settings.height,1536)
|
||||||
|
const turbo=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',turbo:true,promptSections:{action:'fast'},settings:{aspect:'1:1',steps:25,cfg:4}})
|
||||||
|
assert.equal(turbo.turbo,true);assert.equal(turbo.settings.steps,6);assert.equal(turbo.settings.cfg,1)
|
||||||
|
assert.match(String(turbo.lora),/viggle-turbo-v0\.2\.1-6step-lora-r128/)
|
||||||
|
assert.match(String(turbo.sigmas),/0\.9375/)
|
||||||
const enhanced=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',enhancePrompt:true,promptSections:{action:'corgi'},settings:{aspect:'1:1'}})
|
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(enhanced.enhancePrompt,true)
|
||||||
assert.equal(validateRequest({mode:'generate',engine:'flux',folderId:'f',enhancePrompt:true,promptSections:{action:'x'},settings:{aspect:'1:1'}}).enhancePrompt,false)
|
assert.equal(validateRequest({mode:'generate',engine:'flux',folderId:'f',enhancePrompt:true,turbo:true,promptSections:{action:'x'},settings:{aspect:'1:1'}}).turbo,false)
|
||||||
const edit=validateRequest({mode:'edit',engine:'qwen21',folderId:'f',imageAId:'s',promptSections:{action:'change the sky'},settings:{aspect:'auto'}})
|
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.mode,'edit');assert.equal(edit.settings.steps,25);assert.equal(edit.settings.cfg,1)
|
||||||
assert.equal(edit.lockFace,false);assert.deepEqual(edit.settings.loraStack,[])
|
assert.equal(edit.lockFace,false);assert.deepEqual(edit.settings.loraStack,[])
|
||||||
@@ -115,6 +120,7 @@ test('custom negative chips survive normalizeStyles and bench exposes Load actio
|
|||||||
assert.match(src,/loadSelectedJob/);assert.match(src,/hydrateStudio2Job/);assert.match(src,/Edit source/)
|
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,/aspect:'auto'/);assert.match(src,/strip-actions/);assert.match(src,/addNegative/)
|
||||||
assert.match(src,/Enhance prompt/);assert.match(src,/enhancePrompt/)
|
assert.match(src,/Enhance prompt/);assert.match(src,/enhancePrompt/)
|
||||||
|
assert.match(src,/\bTurbo\b/);assert.match(src,/form\.turbo/)
|
||||||
const peT2i=readFileSync(new URL('../server/assets/studio2_qwen21_pe_t2i.json',import.meta.url),'utf8')
|
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')
|
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(peT2i,/QwenImage21_T2IPromptRewrite/);assert.match(peT2i,/pe_t2i\.int8_convrot/)
|
||||||
@@ -123,6 +129,10 @@ test('custom negative chips survive normalizeStyles and bench exposes Load actio
|
|||||||
assert.doesNotMatch(peEdit,/UnetLoader|KSampler|VAELoader/)
|
assert.doesNotMatch(peEdit,/UnetLoader|KSampler|VAELoader/)
|
||||||
const t2i=readFileSync(new URL('../server/assets/studio2_qwen21_t2i.json',import.meta.url),'utf8')
|
const t2i=readFileSync(new URL('../server/assets/studio2_qwen21_t2i.json',import.meta.url),'utf8')
|
||||||
assert.match(t2i,/EmptyLatentImage/);assert.match(t2i,/"latent_image":\s*\["16",\s*0\]/)
|
assert.match(t2i,/EmptyLatentImage/);assert.match(t2i,/"latent_image":\s*\["16",\s*0\]/)
|
||||||
|
const turboT2i=readFileSync(new URL('../server/assets/studio2_qwen21_t2i_turbo.json',import.meta.url),'utf8')
|
||||||
|
const turboEdit=readFileSync(new URL('../server/assets/studio2_qwen21_edit_turbo.json',import.meta.url),'utf8')
|
||||||
|
assert.match(turboT2i,/ViggleTurboLora/);assert.match(turboT2i,/ViggleTurboSigmas/);assert.match(turboT2i,/SamplerCustomAdvanced/)
|
||||||
|
assert.match(turboT2i,/0\.9375/);assert.match(turboEdit,/ViggleTurboLora/);assert.match(turboEdit,/"latent":\s*\["9",\s*2\]/)
|
||||||
assert.deepEqual(resolveQwen21Size('16:9'),{width:1536,height:864})
|
assert.deepEqual(resolveQwen21Size('16:9'),{width:1536,height:864})
|
||||||
assert.deepEqual(resolveQwen21Size('1:1'),{width:1024,height:1024})
|
assert.deepEqual(resolveQwen21Size('1:1'),{width:1024,height:1024})
|
||||||
assert.deepEqual(resolveQwen21Size('9:16'),{width:864,height:1536})
|
assert.deepEqual(resolveQwen21Size('9:16'),{width:864,height:1536})
|
||||||
|
|||||||
Reference in New Issue
Block a user