Compare commits
4
Commits
4831b32d20
...
1f10082087
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f10082087 | ||
|
|
4e61ee17b6 | ||
|
|
b95e8b2a87 | ||
|
|
277b49403c |
@@ -143,8 +143,10 @@
|
||||
<div class="mb-3 flex flex-wrap items-start justify-between gap-3">
|
||||
<h2 class="text-xs uppercase tracking-wider text-zinc-400">Source</h2>
|
||||
<div v-if="isStudio" class="flex flex-wrap gap-4 text-sm text-zinc-300" aria-label="Identity locks">
|
||||
<template v-if="form.engine!=='qwen21'">
|
||||
<label class="flex items-start gap-2"><input v-model="form.lockFace" type="checkbox" class="mt-1 accent-amber-400"><span>Lock face<span class="block text-xs text-zinc-500">Hero encode + lock on Klein / Krea stills</span></span></label>
|
||||
<label class="flex items-start gap-2"><input v-model="form.lockOutfit" type="checkbox" class="mt-1 accent-amber-400"><span>Lock outfit<span class="block text-xs text-zinc-500">Hero encode + lock on Klein / Krea stills</span></span></label>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-4">
|
||||
@@ -206,6 +208,7 @@
|
||||
</details>
|
||||
<button type="button" class="text-sm text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2" @click="settingsOpen=true">Generation settings</button>
|
||||
<p v-if="!video && form.engine==='krea'" class="text-sm text-zinc-400">Krea does not attach the hero identity reference.</p>
|
||||
<p v-if="!video && form.engine==='qwen21'" class="text-sm text-zinc-400">Qwen 2.1 is T2I — no hero, locks, or LoRA stack in this build.</p>
|
||||
<p v-if="form.mode==='extend'" class="text-sm text-amber-200">{{ form.startClipId ? 'Extend · '+(clips.find((c:any)=>c.id===form.startClipId)?.name || form.startClipId)+' · last frame' : 'Select the clip in the family strip or Previous clip.' }}</p>
|
||||
</template>
|
||||
|
||||
@@ -249,8 +252,10 @@
|
||||
</label>
|
||||
<button type="button" class="mb-4 text-sm text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2" @click="form.settings.seedMode='random';form.settings.seed=randomSeed()">Random</button>
|
||||
<label v-if="video"><input v-model="form.settings.turbo" type="checkbox" class="accent-amber-400"> Turbo</label>
|
||||
<template v-if="form.engine!=='qwen21'">
|
||||
<LoraSelect v-model="form.settings.loraStack" class="mt-5" :names="pickerLoras" :use="currentLoraUse" :clip-strength="!video" filename-subtitle :aliases="{...loraNames.aliases,...library.loraAliases}" :triggers="{...loraNames.triggers,...library.loraTriggers}" missing-label="No cached LoRAs yet — Refresh to check Comfy" @insert-trigger="form.promptSections.action += ' '+$event" />
|
||||
<button type="button" class="mt-2 text-sm text-amber-200 outline-none ring-amber-300/40 focus-visible:ring-2" @click="loadLoras(true)">Refresh LoRAs</button>
|
||||
</template>
|
||||
<details class="mt-6"><summary>Compiled prompt</summary>
|
||||
<pre class="whitespace-pre-wrap text-xs text-zinc-400">{{ compiled }}</pre>
|
||||
<p v-if="!video" class="mt-3 text-xs text-zinc-400">Negative: {{ stylePrompt(form.imageStyles,true) || 'None' }}</p>
|
||||
@@ -316,6 +321,7 @@ const tasks=['generate','edit','compose','refine','video','extend','music']
|
||||
const settingsOpen=ref(false),detailsOpen=ref(false),submitting=ref(false),error=ref(''),batchText=ref('')
|
||||
const video=computed(()=>['video','extend'].includes(form.mode))
|
||||
const kreaSticky=computed(()=>['edit','compose','iterate'].includes(form.mode))
|
||||
const qwenOnlyGenerate=computed(()=>form.mode!=='generate')
|
||||
const engineOptions=computed(()=>{
|
||||
if(video.value){
|
||||
return [
|
||||
@@ -325,13 +331,23 @@ const engineOptions=computed(()=>{
|
||||
}
|
||||
return [
|
||||
{value:'flux',label:'Klein',disabled:false,hint:''},
|
||||
{value:'krea',label:'Krea 2',disabled:kreaSticky.value,hint:kreaSticky.value?'sticky hero = Klein':''}
|
||||
{value:'krea',label:'Krea 2',disabled:kreaSticky.value,hint:kreaSticky.value?'sticky hero = Klein':''},
|
||||
{value:'qwen21',label:'Qwen 2.1',disabled:qwenOnlyGenerate.value,hint:qwenOnlyGenerate.value?'Qwen 2.1 is T2I in this build':''}
|
||||
]
|
||||
})
|
||||
watch(engineOptions,opts=>{
|
||||
const current=opts.find(o=>o.value===form.engine)
|
||||
if(!current || current.disabled)form.engine=opts.find(o=>!o.disabled)?.value || form.engine
|
||||
})
|
||||
watch(()=>form.engine,(engine,prev)=>{
|
||||
if(engine==='qwen21' && prev!=='qwen21'){
|
||||
form.settings.steps=25
|
||||
form.settings.cfg=1
|
||||
form.lockFace=false
|
||||
form.lockOutfit=false
|
||||
form.settings.loraStack=[]
|
||||
}
|
||||
})
|
||||
const stills=computed(()=>library.value.stills.filter((s:any)=>s.folderId===form.folderId))
|
||||
const clips=computed(()=>library.value.clips.filter((s:any)=>s.folderId===form.folderId))
|
||||
const compiled=computed(()=>compilePrompt(form.promptSections,video.value,form))
|
||||
@@ -348,7 +364,7 @@ const inputStill=computed(()=>stills.value.find((s:any)=>s.id===form.imageAId))
|
||||
const family=computed(()=>folderAssets(library.value,jobs.value,form.folderId))
|
||||
const sourceAsset=computed(()=>form.startClipId?clips.value.find((c:any)=>c.id===form.startClipId):stills.value.find((s:any)=>s.id===(form.imageAId||form.identityStillId)))
|
||||
const sourceSize=computed(()=>measured[sourceAsset.value?.id] || sourceAsset.value)
|
||||
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value))
|
||||
const resolvedSize=computed(()=>resolveSize(form.settings.aspect,sourceSize.value,undefined,form.engine))
|
||||
|
||||
function isHidden(asset:any){
|
||||
if(!xaigen || !asset?.id)return false
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Qwen Image 2.1 (engine `qwen21`) — host weights + Comfy graph
|
||||
|
||||
## Host paths (this RTX 5080 box)
|
||||
|
||||
| Role | Resolved path |
|
||||
|---|---|
|
||||
| ComfyUI root (Klein / host agent `ComfyUI (1)`) | `C:\Users\ianjm\AppData\Local\Comfy-Desktop\ComfyUI-Installs\ComfyUI (1)\ComfyUI` |
|
||||
| Models root (Desktop Shared via `shared_model_paths.yaml`) | `C:\Users\ianjm\AppData\Local\Comfy-Desktop\ComfyUI-Shared\models` |
|
||||
|
||||
Weights must land under Shared so every Desktop instance sees them:
|
||||
|
||||
- `diffusion_models\qwen-image-2.1-Q8_0.gguf` (~7.59 GiB) — from `abenzerps/Qwen-Image-2.1-Uncensored-GGUF`
|
||||
- `text_encoders\qwen3vl_8b_int8_convrot.safetensors` — from `Comfy-Org/Qwen-Image-2.1` (INT8; BF16 8B VL OOMs on 16 GB)
|
||||
- `vae\qwen_image_2.1_vae_bf16.safetensors` — from `Comfy-Org/Qwen-Image-2.1` (**not** the old Qwen-Image 1.0 VAE)
|
||||
|
||||
Custom node: `custom_nodes\ComfyUI-GGUF` (`UnetLoaderGGUF`). Do not install a second GGUF pack.
|
||||
|
||||
## Setup
|
||||
|
||||
```powershell
|
||||
powershell -ExecutionPolicy Bypass -File scripts\setup-qwen21.ps1
|
||||
```
|
||||
|
||||
DiT download uses the exact host invocation:
|
||||
|
||||
```powershell
|
||||
hf download hf://abenzerps/Qwen-Image-2.1-Uncensored-GGUF/qwen-image-2.1-Q8_0.gguf
|
||||
```
|
||||
|
||||
That lands in the Hugging Face hub cache; the setup script copies it into Shared `diffusion_models`. The abenzerps GGUF ships with `kv_count=0` (no `general.architecture`) **and** Q8_0-quantized 1D RMSNorm weights (logical 128 → packed 136), which breaks Comfy’s fused `rms_rope`. `scripts/tag-qwen21-gguf.py` rewrites the file with `general.architecture=qwen_image` and promotes small/1D tensors to F32 so city96 `UnetLoaderGGUF` can load it.
|
||||
|
||||
Restart Comfy **only when idle** (`COMFY_CONTROL_URL/status` → `gpu.busy=false`). Confirm `object_info` lists `UnetLoaderGGUF` and the three filenames.
|
||||
|
||||
## App
|
||||
|
||||
- Engine key: `qwen21` · UI label: **Qwen 2.1**
|
||||
- Generate (T2I) only in this build. Edit / Compose / Iterate / Video disable with: “Qwen 2.1 is T2I in this build”.
|
||||
- Sampler defaults: euler / simple / cfg **1** / steps **25** · `ModelSamplingAuraFlow` shift **3.1**
|
||||
- Default canvas **1024×1024** (aspect 16:9 / 9:16 / 1:1 → long-edge square via `TextEncodeQwenImage21` resolution; multiples of 32). Drop to 768 if VRAM errors.
|
||||
- No hero / locks / Klein LoRA stack on this engine.
|
||||
|
||||
Graph: `server/assets/studio2_qwen21_t2i.json`.
|
||||
@@ -0,0 +1,225 @@
|
||||
# Download Qwen Image 2.1 GGUF + companions into the live Comfy Desktop Shared models tree.
|
||||
# Also ensures city96/ComfyUI-GGUF is present on the ComfyUI (1) install.
|
||||
#
|
||||
# Example:
|
||||
# powershell -ExecutionPolicy Bypass -File scripts\setup-qwen21.ps1
|
||||
# powershell -File scripts\setup-qwen21.ps1 -SkipDownload
|
||||
# powershell -File scripts\setup-qwen21.ps1 -ComfyRoot "C:\...\ComfyUI (1)\ComfyUI"
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ComfyRoot = '',
|
||||
[string]$ModelsRoot = '',
|
||||
[switch]$SkipDownload,
|
||||
[switch]$SkipNode,
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'Continue'
|
||||
|
||||
function Write-Step([string]$Message) {
|
||||
Write-Host ""
|
||||
Write-Host "==> $Message" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
function Write-Ok([string]$Message) {
|
||||
Write-Host " $Message" -ForegroundColor Green
|
||||
}
|
||||
|
||||
function Write-Warn([string]$Message) {
|
||||
Write-Host " warn $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Resolve-ComfyRoot([string]$Hint) {
|
||||
$candidates = @(
|
||||
$Hint,
|
||||
$env:COMFY_ROOT,
|
||||
$env:COMFYUI_ROOT,
|
||||
(Join-Path $env:LOCALAPPDATA 'Comfy-Desktop\ComfyUI-Installs\ComfyUI (1)\ComfyUI'),
|
||||
(Join-Path $env:LOCALAPPDATA 'Comfy-Desktop\ComfyUI-Installs\ComfyUI\ComfyUI'),
|
||||
'C:\ComfyUI',
|
||||
(Join-Path $env:USERPROFILE 'ComfyUI')
|
||||
) | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\', '/') }
|
||||
|
||||
foreach ($path in $candidates) {
|
||||
if (Test-Path (Join-Path $path 'main.py')) { return (Resolve-Path $path).Path }
|
||||
if (Test-Path (Join-Path $path 'ComfyUI\main.py')) { return (Resolve-Path (Join-Path $path 'ComfyUI')).Path }
|
||||
}
|
||||
throw "Could not find ComfyUI root (main.py). Pass -ComfyRoot."
|
||||
}
|
||||
|
||||
function Resolve-ModelsRoot([string]$Hint, [string]$ComfyRootPath) {
|
||||
$shared = Join-Path $env:LOCALAPPDATA 'Comfy-Desktop\ComfyUI-Shared\models'
|
||||
$candidates = @(
|
||||
$Hint,
|
||||
$env:COMFY_MODELS_ROOT,
|
||||
$shared,
|
||||
(Join-Path $ComfyRootPath 'models')
|
||||
) | Where-Object { $_ } | ForEach-Object { $_.TrimEnd('\', '/') }
|
||||
|
||||
foreach ($path in $candidates) {
|
||||
if (Test-Path $path) { return (Resolve-Path $path).Path }
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path $shared | Out-Null
|
||||
return (Resolve-Path $shared).Path
|
||||
}
|
||||
|
||||
function Find-HfCli {
|
||||
foreach ($name in @('hf', 'huggingface-cli')) {
|
||||
$cmd = Get-Command $name -ErrorAction SilentlyContinue
|
||||
if ($cmd) { return $cmd.Source }
|
||||
}
|
||||
throw "Hugging Face CLI not found (hf / huggingface-cli). Install with: pip install -U huggingface_hub"
|
||||
}
|
||||
|
||||
function Ensure-Dir([string]$Path) {
|
||||
if (-not (Test-Path $Path)) {
|
||||
New-Item -ItemType Directory -Force -Path $Path | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-ModelFile([string]$Path, [long]$MinBytes) {
|
||||
if (-not (Test-Path $Path)) { throw "Missing model file: $Path" }
|
||||
$item = Get-Item $Path
|
||||
if ($item.Length -lt $MinBytes) {
|
||||
throw "Suspicious size for $Path ($($item.Length) bytes). Expected >= $MinBytes. Delete and re-run."
|
||||
}
|
||||
Write-Ok ("{0} {1:N0} bytes" -f $item.FullName, $item.Length)
|
||||
}
|
||||
|
||||
function Flatten-IfNested([string]$DestDir, [string]$FileName) {
|
||||
$nested = Get-ChildItem $DestDir -Recurse -File -Filter $FileName -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.DirectoryName -ne $DestDir } |
|
||||
Select-Object -First 1
|
||||
if ($nested) {
|
||||
$target = Join-Path $DestDir $FileName
|
||||
if ((Test-Path $target) -and -not $Force) {
|
||||
Write-Warn "Nested copy found but destination already exists: $target"
|
||||
return
|
||||
}
|
||||
Move-Item -Force $nested.FullName $target
|
||||
Write-Ok "Flattened $($nested.FullName) -> $target"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-HfFile {
|
||||
param(
|
||||
[string]$Cli,
|
||||
[string]$Repo,
|
||||
[string]$File,
|
||||
[string]$LocalDir
|
||||
)
|
||||
Ensure-Dir $LocalDir
|
||||
$dest = Join-Path $LocalDir (Split-Path $File -Leaf)
|
||||
if ((Test-Path $dest) -and -not $Force) {
|
||||
$len = (Get-Item $dest).Length
|
||||
if ($len -gt 1MB) {
|
||||
Write-Ok "Already present: $dest ($len bytes)"
|
||||
return
|
||||
}
|
||||
}
|
||||
Write-Step "hf download $Repo $File"
|
||||
& $Cli download $Repo $File --local-dir $LocalDir
|
||||
if ($LASTEXITCODE -ne 0) { throw "hf download failed for $Repo / $File (exit $LASTEXITCODE)" }
|
||||
Flatten-IfNested -DestDir $LocalDir -FileName (Split-Path $File -Leaf)
|
||||
}
|
||||
|
||||
# --- resolve paths ---
|
||||
Write-Step "Resolve ComfyUI root"
|
||||
$root = Resolve-ComfyRoot $ComfyRoot
|
||||
Write-Ok $root
|
||||
|
||||
Write-Step "Resolve models root (Desktop Shared preferred)"
|
||||
$models = Resolve-ModelsRoot $ModelsRoot $root
|
||||
Write-Ok $models
|
||||
|
||||
$Diff = Join-Path $models 'diffusion_models'
|
||||
$TE = Join-Path $models 'text_encoders'
|
||||
$VAE = Join-Path $models 'vae'
|
||||
Ensure-Dir $Diff
|
||||
Ensure-Dir $TE
|
||||
Ensure-Dir $VAE
|
||||
|
||||
if (-not $SkipDownload) {
|
||||
$cli = Find-HfCli
|
||||
Write-Ok "HF CLI: $cli"
|
||||
|
||||
# DiT GGUF only (no Q6/Q5/Q4, no full bf16 DiT)
|
||||
# Exact invocation required on this host:
|
||||
# hf download hf://abenzerps/Qwen-Image-2.1-Uncensored-GGUF/qwen-image-2.1-Q8_0.gguf
|
||||
# That form writes into the Hugging Face hub cache (not --local-dir).
|
||||
Write-Step "hf download hf://abenzerps/Qwen-Image-2.1-Uncensored-GGUF/qwen-image-2.1-Q8_0.gguf"
|
||||
$ditName = 'qwen-image-2.1-Q8_0.gguf'
|
||||
$ditDest = Join-Path $Diff $ditName
|
||||
if ((Test-Path $ditDest) -and -not $Force -and (Get-Item $ditDest).Length -gt 1MB) {
|
||||
Write-Ok "Already present: $ditDest ($((Get-Item $ditDest).Length) bytes)"
|
||||
} else {
|
||||
& $cli download "hf://abenzerps/Qwen-Image-2.1-Uncensored-GGUF/$ditName"
|
||||
if ($LASTEXITCODE -ne 0) { throw "hf download failed for DiT GGUF (exit $LASTEXITCODE)" }
|
||||
$cacheRoot = Join-Path $env:USERPROFILE '.cache\huggingface\hub\models--abenzerps--Qwen-Image-2.1-Uncensored-GGUF'
|
||||
$found = Get-ChildItem $cacheRoot -Recurse -File -Filter $ditName -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending |
|
||||
Select-Object -First 1
|
||||
if (-not $found) { throw "hf download finished but $ditName was not found under $cacheRoot" }
|
||||
Ensure-Dir $Diff
|
||||
Copy-Item -Force $found.FullName $ditDest
|
||||
Write-Ok "Copied $($found.FullName) -> $ditDest ($((Get-Item $ditDest).Length) bytes)"
|
||||
}
|
||||
|
||||
# abenzerps ships kv_count=0 + Q8_0 1D RMSNorms (packed 136 vs logical 128).
|
||||
# Tag architecture=qwen_image and promote small/1D tensors to F32 for ComfyUI-GGUF.
|
||||
Write-Step "Tag DiT GGUF (architecture + F32 norms)"
|
||||
$tagger = Join-Path $PSScriptRoot 'tag-qwen21-gguf.py'
|
||||
$pyCandidates = @(
|
||||
(Join-Path $root '.venv\Scripts\python.exe'),
|
||||
(Join-Path (Split-Path $root) 'standalone-env\python.exe'),
|
||||
(Get-Command python -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source)
|
||||
) | Where-Object { $_ -and (Test-Path $_) }
|
||||
$py = $pyCandidates | Select-Object -First 1
|
||||
if (-not $py) { throw "No Python found to run $tagger" }
|
||||
$fixed = Join-Path $Diff 'qwen-image-2.1-Q8_0.fixed.gguf'
|
||||
& $py $tagger $ditDest $fixed
|
||||
if ($LASTEXITCODE -ne 0) { throw "tag-qwen21-gguf.py failed (exit $LASTEXITCODE)" }
|
||||
Copy-Item -Force $fixed $ditDest
|
||||
Write-Ok "Installed tagged GGUF at $ditDest"
|
||||
|
||||
# TE + VAE companions from Comfy-Org (INT8 TE for 16 GB VRAM)
|
||||
Invoke-HfFile -Cli $cli -Repo 'Comfy-Org/Qwen-Image-2.1' -File 'text_encoders/qwen3vl_8b_int8_convrot.safetensors' -LocalDir $TE
|
||||
Invoke-HfFile -Cli $cli -Repo 'Comfy-Org/Qwen-Image-2.1' -File 'vae/qwen_image_2.1_vae_bf16.safetensors' -LocalDir $VAE
|
||||
}
|
||||
|
||||
Write-Step "Verify weights"
|
||||
Assert-ModelFile (Join-Path $Diff 'qwen-image-2.1-Q8_0.gguf') 7000000000
|
||||
Assert-ModelFile (Join-Path $TE 'qwen3vl_8b_int8_convrot.safetensors') 1000000000
|
||||
Assert-ModelFile (Join-Path $VAE 'qwen_image_2.1_vae_bf16.safetensors') 100000000
|
||||
|
||||
if (-not $SkipNode) {
|
||||
Write-Step "city96 ComfyUI-GGUF"
|
||||
$nodes = Join-Path $root 'custom_nodes\ComfyUI-GGUF'
|
||||
if (-not (Test-Path $nodes)) {
|
||||
git clone https://github.com/city96/ComfyUI-GGUF $nodes
|
||||
if ($LASTEXITCODE -ne 0) { throw "git clone ComfyUI-GGUF failed" }
|
||||
Write-Ok "Cloned $nodes"
|
||||
} else {
|
||||
Write-Ok "Already present: $nodes"
|
||||
}
|
||||
|
||||
$py = Join-Path $root '.venv\Scripts\python.exe'
|
||||
if (-not (Test-Path $py)) {
|
||||
$py = Join-Path (Split-Path $root) 'standalone-env\python.exe'
|
||||
}
|
||||
if (Test-Path $py) {
|
||||
Write-Step "Ensure gguf package in Comfy python ($py)"
|
||||
& $py -m pip install -q gguf
|
||||
if ($LASTEXITCODE -ne 0) { Write-Warn "pip install gguf returned $LASTEXITCODE" }
|
||||
else { Write-Ok "gguf importable" }
|
||||
} else {
|
||||
Write-Warn "Comfy python not found; install gguf into the interpreter that runs main.py"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done. Restart Comfy only when /status shows gpu.busy=false." -ForegroundColor Cyan
|
||||
Write-Host "ComfyRoot=$root"
|
||||
Write-Host "ModelsRoot=$models"
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Make abenzerps Qwen Image 2.1 DiT GGUF loadable in city96 ComfyUI-GGUF.
|
||||
|
||||
abenzerps/Qwen-Image-2.1-Uncensored-GGUF ships Q8_0 with:
|
||||
- kv_count=0 (no general.architecture)
|
||||
- 1D RMSNorm weights quantized to Q8_0 (logical 128 -> packed 136),
|
||||
which breaks Comfy's fused rms_rope path
|
||||
|
||||
This rewrite:
|
||||
1. Adds general.architecture=qwen_image
|
||||
2. Dequantizes small / 1D tensors to F32 (city96 convert keeps them hiprec)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Match city96 ComfyUI-GGUF/tools/convert.py QUANTIZATION_THRESHOLD
|
||||
QUANTIZATION_THRESHOLD = 1024
|
||||
|
||||
|
||||
def main() -> int:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("src", type=Path)
|
||||
ap.add_argument("dst", type=Path, nargs="?", default=None)
|
||||
ap.add_argument("--arch", default="qwen_image")
|
||||
ap.add_argument("--name", default="qwen-image-2.1")
|
||||
ap.add_argument("--inplace", action="store_true", help="Replace src after a successful tag")
|
||||
args = ap.parse_args()
|
||||
|
||||
import gguf
|
||||
import numpy as np
|
||||
|
||||
src = args.src.resolve()
|
||||
if not src.is_file():
|
||||
print(f"missing source: {src}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
dst = (args.dst.resolve() if args.dst else src.with_name(src.stem + ".tagged.gguf"))
|
||||
|
||||
reader = gguf.GGUFReader(str(src))
|
||||
|
||||
def get_field(name: str):
|
||||
field = reader.fields.get(name)
|
||||
if field is None:
|
||||
return None
|
||||
try:
|
||||
return field.contents()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
if dst == src:
|
||||
dst = src.with_name(src.stem + ".retag.gguf")
|
||||
|
||||
f32 = gguf.GGMLQuantizationType.F32
|
||||
compat = {gguf.GGMLQuantizationType.F32, gguf.GGMLQuantizationType.F16, gguf.GGMLQuantizationType.BF16}
|
||||
|
||||
print(f"rewriting {src} -> {dst} (arch={args.arch}, tensors={len(reader.tensors)})")
|
||||
writer = gguf.GGUFWriter(str(dst), arch=args.arch, use_temp_file=True)
|
||||
writer.add_name(args.name)
|
||||
try:
|
||||
writer.add_type("model")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
promoted = 0
|
||||
for tensor in reader.tensors:
|
||||
logical = tuple(int(x) for x in tensor.shape)
|
||||
# GGUF stores dims reversed vs torch; logical numel is what matters
|
||||
numel = 1
|
||||
for d in logical:
|
||||
numel *= d
|
||||
qtype = tensor.tensor_type
|
||||
data = tensor.data
|
||||
if hasattr(data, "copy"):
|
||||
data = data.copy()
|
||||
|
||||
needs_f32 = qtype not in compat and (len(logical) <= 1 or numel <= QUANTIZATION_THRESHOLD)
|
||||
if needs_f32:
|
||||
# Dequantize packed blocks -> float32 with logical shape (GGUF order)
|
||||
dequant = gguf.quants.dequantize(np.asarray(data), qtype).astype(np.float32, copy=False)
|
||||
expected = numel
|
||||
if dequant.size != expected:
|
||||
print(
|
||||
f"dequant size mismatch {tensor.name}: got {dequant.size} want {expected} "
|
||||
f"shape={logical} qtype={qtype}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 3
|
||||
data = dequant.reshape(logical)
|
||||
qtype = f32
|
||||
promoted += 1
|
||||
|
||||
writer.add_tensor(tensor.name, data, raw_dtype=qtype)
|
||||
|
||||
writer.write_header_to_file()
|
||||
writer.write_kv_data_to_file()
|
||||
writer.write_tensors_to_file(progress=True)
|
||||
writer.close()
|
||||
|
||||
check = gguf.GGUFReader(str(dst))
|
||||
field = check.fields.get("general.architecture")
|
||||
got = field.contents() if field is not None else None
|
||||
print(
|
||||
f"verified architecture={got!r} tensors={len(check.tensors)} "
|
||||
f"promoted_f32={promoted} size={dst.stat().st_size}"
|
||||
)
|
||||
if got != args.arch or len(check.tensors) != len(reader.tensors):
|
||||
print("tag failed", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
# Spot-check a known norm weight is F32 @ 128
|
||||
sample = next((t for t in check.tensors if t.name.endswith("attn.norm_q.weight")), None)
|
||||
if sample is not None:
|
||||
print(
|
||||
f"sample {sample.name}: shape={tuple(int(x) for x in sample.shape)} "
|
||||
f"type={sample.tensor_type.name} data_shape={tuple(sample.data.shape)}"
|
||||
)
|
||||
|
||||
if args.inplace:
|
||||
bak = src.with_suffix(src.suffix + ".untagged.bak")
|
||||
if bak.exists():
|
||||
bak.unlink()
|
||||
src.replace(bak)
|
||||
dst.replace(src)
|
||||
print(f"inplace: {src} (backup {bak})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"4": {
|
||||
"inputs": {
|
||||
"unet_name": "qwen-image-2.1-Q8_0.gguf"
|
||||
},
|
||||
"class_type": "UnetLoaderGGUF",
|
||||
"_meta": { "title": "Load Qwen 2.1 GGUF" }
|
||||
},
|
||||
"3": {
|
||||
"inputs": {
|
||||
"shift": 3.1,
|
||||
"model": ["4", 0]
|
||||
},
|
||||
"class_type": "ModelSamplingAuraFlow",
|
||||
"_meta": { "title": "Qwen 2.1 shift" }
|
||||
},
|
||||
"5": {
|
||||
"inputs": {
|
||||
"clip_name": "qwen3vl_8b_int8_convrot.safetensors",
|
||||
"type": "qwen_image",
|
||||
"device": "default"
|
||||
},
|
||||
"class_type": "CLIPLoader",
|
||||
"_meta": { "title": "Load Qwen 2.1 CLIP" }
|
||||
},
|
||||
"6": {
|
||||
"inputs": {
|
||||
"vae_name": "qwen_image_2.1_vae_bf16.safetensors"
|
||||
},
|
||||
"class_type": "VAELoader",
|
||||
"_meta": { "title": "Load Qwen 2.1 VAE" }
|
||||
},
|
||||
"9": {
|
||||
"inputs": {
|
||||
"clip": ["5", 0],
|
||||
"prompt": "",
|
||||
"negative_prompt": "",
|
||||
"resolution": 1024
|
||||
},
|
||||
"class_type": "TextEncodeQwenImage21",
|
||||
"_meta": { "title": "Text Encode Qwen Image 2.1" }
|
||||
},
|
||||
"15": {
|
||||
"inputs": {
|
||||
"seed": 1,
|
||||
"steps": 25,
|
||||
"cfg": 1,
|
||||
"sampler_name": "euler",
|
||||
"scheduler": "simple",
|
||||
"denoise": 1,
|
||||
"model": ["3", 0],
|
||||
"positive": ["9", 0],
|
||||
"negative": ["9", 1],
|
||||
"latent_image": ["9", 2]
|
||||
},
|
||||
"class_type": "KSampler",
|
||||
"_meta": { "title": "KSampler" }
|
||||
},
|
||||
"20": {
|
||||
"inputs": {
|
||||
"samples": ["15", 0],
|
||||
"vae": ["6", 0]
|
||||
},
|
||||
"class_type": "VAEDecode",
|
||||
"_meta": { "title": "VAE Decode" }
|
||||
},
|
||||
"21": {
|
||||
"inputs": {
|
||||
"filename_prefix": "studio2-qwen21",
|
||||
"images": ["20", 0]
|
||||
},
|
||||
"class_type": "SaveImage",
|
||||
"_meta": { "title": "Save Image" }
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,10 @@ import { queueSeeds } from '~/shared/studio2/seed.mjs';
|
||||
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import template from '../../assets/studio2_minimax_native.json';
|
||||
import qwen21Template from '../../assets/studio2_qwen21_t2i.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';
|
||||
import { createJob, restoreJob, getJob, emitJob, type Job } from '../jobs';
|
||||
import { markStudioLive, onLiveVideoSettled, type StudioJob } from '../studioQueue';
|
||||
import { readRecord, saveRecord, studio2Root, records } from './store';
|
||||
@@ -126,6 +128,28 @@ async function prepareGraph(r: any) {
|
||||
r.graphId = 'studio2_minimax_native.json';
|
||||
}
|
||||
}
|
||||
else if (q.engine === 'qwen21') {
|
||||
if (q.mode !== 'generate')
|
||||
throw new Error('Qwen 2.1 is T2I in this build.');
|
||||
const size = resolveQwen21Size(s.aspect);
|
||||
s.width = size.width;
|
||||
s.height = size.height;
|
||||
graph = structuredClone(qwen21Template);
|
||||
graph['9'].inputs.prompt = q.compiledPrompt;
|
||||
graph['9'].inputs.negative_prompt = stylePrompt(q.imageStyles, true);
|
||||
// TextEncodeQwenImage21 builds the 64-ch empty latent from resolution (square T2I).
|
||||
graph['9'].inputs.resolution = Math.max(size.width, size.height);
|
||||
graph['15'].inputs.seed = s.seed;
|
||||
graph['15'].inputs.steps = s.steps || 25;
|
||||
graph['15'].inputs.cfg = s.cfg ?? 1;
|
||||
graph['15'].inputs.sampler_name = 'euler';
|
||||
graph['15'].inputs.scheduler = 'simple';
|
||||
graph['21'].inputs.filename_prefix = prefix + '/image';
|
||||
r.sampleLatent = 'qwen21 textencode latent';
|
||||
r.sampleDenoise = null;
|
||||
r.heroReferenceAttached = false;
|
||||
r.graphId = 'studio2_qwen21_t2i.json';
|
||||
}
|
||||
else {
|
||||
const mode = identityPlan?.mode ?? (q.mode === 'iterate' ? (a ? 'edit' : 'generate') : q.mode);
|
||||
const found = q.engine === 'krea' ? await resolveKreaGenerateAssets() : null;
|
||||
|
||||
@@ -21,5 +21,5 @@ export async function resolveRequestSize(owner: string, request: any) {
|
||||
}
|
||||
}
|
||||
if (video && request.startClipId && !dimensions?.width) throw new Error('Cannot measure the previous clip.')
|
||||
Object.assign(request.settings,resolveSize(request.settings.aspect,dimensions))
|
||||
Object.assign(request.settings,resolveSize(request.settings.aspect,dimensions,undefined,request.engine))
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function appearanceLock({lockFace = true, lockOutfit = true} = {}) {
|
||||
export function compilePrompt(value, video = false, locks = {}) {
|
||||
if (video) return [compileSections(value),appearanceLock(locks)].filter(Boolean).join('\n\n')
|
||||
const hasImage=!!(locks.imageAId || locks.identityStillId)
|
||||
const attached=hasImage && locks.engine !== 'krea'
|
||||
const attached=hasImage && locks.engine !== 'krea' && locks.engine !== 'qwen21'
|
||||
const reference=locks.mode!=='generate' && locks.identityStillId && locks.imageAId && locks.identityStillId!==locks.imageAId ? 'second (hero) reference' : 'image reference'
|
||||
return [sectionsFrom(value).action, stylePrompt(locks.imageStyles),
|
||||
attached && locks.lockFace !== false ? `Preserve the person’s face, glasses, hair, and body identity from the ${reference}.` : '',
|
||||
@@ -31,9 +31,10 @@ export function validateRequest(raw, xaigen = false) {
|
||||
const mode = raw.mode || 'generate', engine = raw.engine || 'flux'
|
||||
if (!['generate','edit','compose','refine','iterate','video','extend'].includes(mode)) fail('Unknown task.')
|
||||
const video = ['video','extend'].includes(mode)
|
||||
if (!(video ? ['minimax','ltx'] : ['flux','krea']).includes(engine)) fail('Engine does not support this task.')
|
||||
if (!(video ? ['minimax','ltx'] : ['flux','krea','qwen21']).includes(engine)) fail('Engine does not support this task.')
|
||||
if (engine === 'ltx' && !xaigen) fail('LTX is available only in xAIGen.')
|
||||
if (engine === 'krea' && ['edit','compose','iterate'].includes(mode)) fail('Sticky hero edits require Klein. Krea remains available for Generate and masked Refine; no engine fallback is performed.')
|
||||
if (engine === 'qwen21' && mode !== 'generate') fail('Qwen 2.1 is T2I in this build.')
|
||||
if (Array.isArray(raw.batch) && raw.batch.length>30) fail('A batch supports up to 31 shots including the first prompt.')
|
||||
if (Array.isArray(raw.guides) && raw.guides.length>8) fail('Use up to eight guide stills.')
|
||||
if (!raw.folderId) fail('Choose a folder.')
|
||||
@@ -46,8 +47,13 @@ export function validateRequest(raw, xaigen = false) {
|
||||
const promptSections = video ? allSections : {action:allSections.action}
|
||||
if (!promptSections.action.trim()) fail('Enter an action prompt.')
|
||||
const number=(key, fallback, min, max) => { const n=Number(raw.settings?.[key] ?? fallback); if (!Number.isFinite(n)||n<min||n>max) fail(`Invalid ${key}.`); return n }
|
||||
for (const key of ['steps']) if (!Number.isInteger(number(key,key==='steps'?24:1,0,2147483647))) fail(`Invalid ${key}.`)
|
||||
for (const key of ['width','height']) if (number(key,key==='width'?1344:768,256,4096)%32) fail('Dimensions must be multiples of 32.')
|
||||
const qwen = engine === 'qwen21'
|
||||
const defaultSteps = video ? 8 : qwen ? 25 : 24
|
||||
const defaultCfg = video ? 1.5 : qwen ? 1 : 4
|
||||
const defaultWidth = qwen ? 1024 : 1344
|
||||
const defaultHeight = qwen ? 1024 : 768
|
||||
for (const key of ['steps']) if (!Number.isInteger(number(key,key==='steps'?defaultSteps:1,0,2147483647))) fail(`Invalid ${key}.`)
|
||||
for (const key of ['width','height']) if (number(key,key==='width'?defaultWidth:defaultHeight,256,4096)%32) fail('Dimensions must be multiples of 32.')
|
||||
const duration=number('duration',5,1,15)
|
||||
if (video) {
|
||||
if (promptSections.cameraBeats.slice(0,-1).some(b=>b.durationSec == null)) fail('Only the last camera beat can run to the end.')
|
||||
@@ -55,10 +61,10 @@ 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:raw.lockFace !== false, lockOutfit:raw.lockOutfit !== false, hideInput:!!raw.hideInput, folderId:String(raw.folderId), identityStillId:raw.identityStillId || '', imageAId:raw.imageAId || '', imageBId:raw.imageBId || '', maskId:raw.maskId || '', startClipId:raw.startClipId || '', endStillId:raw.endStillId || '',
|
||||
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 || '',
|
||||
startFrameSource: raw.startClipId ? {kind:'previous-last-frame',clipId:raw.startClipId} : {kind:'still',stillId:raw.imageAId || ''},
|
||||
promptSections, compiledPrompt:compilePrompt(promptSections,video,raw),
|
||||
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',1344,256,4096),height:number('height',768,256,4096),steps:number('steps',video?8:24,1,100),cfg:number('cfg',video?1.5:4,0,20),seedMode:raw.settings?.seedMode==='locked'?'locked':'random',seed:shotSeeds[0],duration:number('duration',5,1,15),turbo:raw.settings?.turbo !== false, loraStack:Array.isArray(raw.settings?.loraStack)?raw.settings.loraStack:[] },
|
||||
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:[]) },
|
||||
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}}) }
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import {restoreLoras,loraUse} from './lora-memory.mjs'
|
||||
import { restoreInput } from './contracts.mjs'
|
||||
export const aspectOptions = [{id:'auto',label:'Automatic',hint:'Match start or hero still'},{id:'16:9',label:'16:9',hint:'1344 × 768 · padded'},{id:'9:16',label:'9:16',hint:'768 × 1344 · padded'},{id:'1:1',label:'1:1',hint:'960 × 960 · padded'}]
|
||||
export function resolveSize(aspect, source, fallback = {width:1344,height:768}) {
|
||||
export function resolveQwen21Size(aspect) {
|
||||
const presets = {'16:9':[1024,576],'9:16':[576,1024],'1:1':[1024,1024]}
|
||||
if (presets[aspect]) return {width:presets[aspect][0],height:presets[aspect][1]}
|
||||
return {width:1024,height:1024}
|
||||
}
|
||||
export function resolveSize(aspect, source, fallback = {width:1344,height:768}, engine = '') {
|
||||
if (engine === 'qwen21') return resolveQwen21Size(aspect)
|
||||
const presets = {'16:9':[1344,768],'9:16':[768,1344],'1:1':[960,960]}
|
||||
if (presets[aspect]) return {width:presets[aspect][0],height:presets[aspect][1]}
|
||||
if (!source?.width || !source?.height) return {width:fallback.width || 1344,height:fallback.height || 768}
|
||||
@@ -35,7 +41,7 @@ export function restoreAsset(form, item, xaigen = false, memory) {
|
||||
const restored = restoreInput(item)
|
||||
const next = {...form,...restored,settings:{...form.settings,...restored.settings}}
|
||||
next.settings.seedMode='random'
|
||||
next.engine=restored.engine==='krea'?'krea':'flux'
|
||||
next.engine=restored.engine==='qwen21'?'qwen21':restored.engine==='krea'?'krea':'flux'
|
||||
next.mode=['generate','edit','compose','refine','iterate'].includes(restored.mode)?restored.mode:'edit'
|
||||
next.imageAId=item.id
|
||||
next.startClipId=''
|
||||
@@ -62,7 +68,7 @@ export function restoreAll(form,item,xaigen=false,memory) {
|
||||
const restored=restoreInput(item),next={...form,...restored,settings:{...form.settings,...restored.settings,seedMode:'random'}}
|
||||
if(item.kind==='clip') return {...bindClip(next,item,xaigen,memory),mode:'extend'}
|
||||
next.mode=restored.mode==='iterate'?'edit':restored.mode || 'edit'
|
||||
next.engine=restored.engine==='krea'?'krea':'flux'
|
||||
next.engine=restored.engine==='qwen21'?'qwen21':restored.engine==='krea'?'krea':'flux'
|
||||
next.imageAId=item.id
|
||||
next.startClipId=''
|
||||
return restoreLoras(next,item.studio2?.settings?.loraStack ?? item.settings?.loraStack,xaigen,memory)
|
||||
|
||||
@@ -2,7 +2,9 @@ import {filterLoraStackForUse} from '../../utils/loras.ts'
|
||||
|
||||
export function loraUse(form) {
|
||||
const video=['video','extend'].includes(form.mode)
|
||||
return video ? (form.engine==='ltx'?'ltx':'minimax') : (form.engine==='krea'?'krea':'flux')
|
||||
if (video) return form.engine==='ltx'?'ltx':'minimax'
|
||||
if (form.engine==='qwen21') return 'qwen21'
|
||||
return form.engine==='krea'?'krea':'flux'
|
||||
}
|
||||
|
||||
// One controller per bench. Storage is optional (SSR/private browsing still works).
|
||||
|
||||
@@ -74,3 +74,10 @@ test('image chips compile positive and negative independently; video chips do no
|
||||
assert.doesNotMatch(compilePrompt(q.promptSections,false,q),/Picture 1/)
|
||||
assert.deepEqual(parseBatch('A wrapped\nsentence\n\nSecond shot\r\n\r\nThird'),['A wrapped\nsentence','Second shot','Third'])
|
||||
})
|
||||
test('qwen21 is generate-only T2I with 25/1 defaults and no locks',()=>{
|
||||
const q=validateRequest({mode:'generate',engine:'qwen21',folderId:'f',promptSections:{action:'a red bicycle'},settings:{aspect:'1:1'}})
|
||||
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.throws(()=>validateRequest({mode:'edit',engine:'qwen21',folderId:'f',imageAId:'s',promptSections:{action:'x'}}),/T2I in this build/)
|
||||
})
|
||||
|
||||
+5
-1
@@ -283,7 +283,7 @@ export function kleinLoraOnKreaMessage(name: string) {
|
||||
return imageLoraEngineMismatchMessage(name, 'krea')
|
||||
}
|
||||
|
||||
export type LoraUse = 'minimax' | 'ltx' | 'flux' | 'krea'
|
||||
export type LoraUse = 'minimax' | 'ltx' | 'flux' | 'krea' | 'qwen21'
|
||||
|
||||
export function isSystemGraphLora(name: string) {
|
||||
const id = loraIdentityKey(name)
|
||||
@@ -301,16 +301,19 @@ export function inferLoraUse(name: string): LoraUse[] | null {
|
||||
if (/ltx/.test(value)) uses.push('ltx')
|
||||
if (/klein|flux/.test(value)) uses.push('flux')
|
||||
if (/krea/.test(value)) uses.push('krea')
|
||||
if (/qwen/.test(value)) uses.push('qwen21')
|
||||
return uses.length ? uses : null
|
||||
}
|
||||
|
||||
export function loraMatchesUse(name: string, use: LoraUse) {
|
||||
if (use === 'qwen21') return false
|
||||
return !isSystemGraphLora(name) && !!inferLoraUse(name)?.includes(use)
|
||||
}
|
||||
|
||||
/** Saved stacks may contain unlabelled adapters; never carry a known wrong engine. */
|
||||
export function filterLoraStackForUse(stack: unknown, use: LoraUse, xaigen: boolean) {
|
||||
if (use === 'ltx' && !xaigen) return []
|
||||
if (use === 'qwen21') return []
|
||||
return filterLoraStackForStudio(stack, xaigen).filter(item => {
|
||||
const uses = inferLoraUse(item.name)
|
||||
return !isSystemGraphLora(item.name) && (!uses || uses.includes(use))
|
||||
@@ -318,6 +321,7 @@ export function filterLoraStackForUse(stack: unknown, use: LoraUse, xaigen: bool
|
||||
}
|
||||
|
||||
export function filterLorasForUse(names: string[], use: LoraUse) {
|
||||
if (use === 'qwen21') return []
|
||||
return names.filter(name => loraMatchesUse(name, use))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user