From f5e648aff25bbedba2537d9e5cae22401f89079a Mon Sep 17 00:00:00 2001 From: cammo22 <78992276+cammo22@users.noreply.github.com> Date: Fri, 23 Oct 2026 04:09:02 +0200 Subject: [PATCH] 1.2.1: modelli dalla barra in alto (Qwen e Klein 3), Editor box senza blocchi, LoRA a due scaffali MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Il modello si sceglie solo dalla barra in alto, da qualsiasi pagina: Qwen, Klein 3 e Klein 3 Base. FLUX.2 Klein 4B è tolto (selettore, catalogo, grafi, riconoscimento dei LoRA); le impostazioni vecchie passano a Qwen e i suoi file non vengono più scambiati per quelli di Klein 3. - Editor box: fino a 16 riquadri e riquadri dentro i riquadri (strumento Disegna di serie, il numero sposta, le maniglie ridimensionano; Sposta afferra dal corpo; un clic sceglie il più piccolo sotto il punto). - LoRA: due scaffali, Qwen e Flux (Klein 3), con file in loras/qwen e loras/flux, pile separate e carte per scaffale. Corretto il motivo per cui con Klein non funzionavano: l'elenco non portava `famiglie` (ogni LoRA di Klein risultava «non compatibile»), la richiesta con Klein non portava LoRA e le azioni rapide accendevano LoRA di Qwen. Rete di sicurezza nel motore per i LoRA dell'altro scaffale. - Prove nuove: test/lora-scaffali.mjs, test/box-lora-ui.mjs; aggiornati quelle di modelli, catalogo, spazio, LoRA, grafi e GUI. Versione 1.2.1 (CHANGELOG, README, CLAUDE.md). --- CHANGELOG.md | 24 ++ CLAUDE.md | 5 +- README.md | 13 +- package-lock.json | 4 +- package.json | 4 +- src/main/idle.ts | 4 +- src/main/index.ts | 24 +- src/main/installa/catalogo.ts | 20 -- src/main/lora.ts | 62 +++++- src/main/motore/grafi.ts | 20 +- src/main/motore/klein.ts | 21 +- src/main/motore/lavori.ts | 25 ++- src/main/motore/regioni.ts | 4 +- src/main/motore/spazio.ts | 6 +- src/renderer/src/App.tsx | 3 + src/renderer/src/anteprime.ts | 3 +- src/renderer/src/api.ts | 11 +- src/renderer/src/azioni.ts | 47 ++-- src/renderer/src/box.ts | 12 +- src/renderer/src/componenti/AvvisoLora.tsx | 4 +- src/renderer/src/componenti/ScegliModello.tsx | 47 +--- src/renderer/src/componenti/SceltaLora.tsx | 14 +- src/renderer/src/orbita.css | 10 +- src/renderer/src/pagine/Box.tsx | 35 +-- src/renderer/src/pagine/Crea.tsx | 18 +- src/renderer/src/pagine/Impostazioni.tsx | 2 +- src/renderer/src/pagine/Lora.tsx | 207 ++++++++++++------ src/renderer/src/pagine/Studio.tsx | 14 +- src/renderer/src/pagine/modifica/Schede.tsx | 14 +- src/renderer/src/pilaLora.ts | 10 +- src/renderer/src/stato.ts | 55 +++-- src/shared/loraBase.ts | 42 ++-- src/shared/modelli.ts | 14 +- src/shared/tipi.ts | 12 +- test/asset.mjs | 5 +- test/box-lora-ui.mjs | 122 +++++++++++ ...__vae-text-encorder-for-flux-klein-4b.json | 56 ----- ...lack-forest-labs__FLUX.2-klein-4b-fp8.json | 53 ----- test/catalogo.mjs | 7 +- test/finto.js | 2 +- test/grafi_v2.mjs | 18 +- test/gui-v2-flussi.mjs | 7 +- test/lora-scaffali.mjs | 87 ++++++++ test/lora-ssr.mjs | 4 +- test/lora.mjs | 22 +- test/modelli.mjs | 7 +- test/pila-lora.mjs | 3 + test/regioni.mjs | 4 +- test/spazio.mjs | 4 +- 49 files changed, 752 insertions(+), 459 deletions(-) create mode 100644 test/box-lora-ui.mjs delete mode 100644 test/campioni-hf/Comfy-Org__vae-text-encorder-for-flux-klein-4b.json delete mode 100644 test/campioni-hf/black-forest-labs__FLUX.2-klein-4b-fp8.json create mode 100644 test/lora-scaffali.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index f642d97..c9bb861 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 1.2.1 — 2026-10-23 + +Tre correzioni dell'interfaccia Orbita e una semplificazione: i modelli si scelgono dalla barra in alto e sono due (Qwen e Klein 3), l'Editor box non si blocca più dopo qualche riquadro, e i LoRA hanno due scaffali, Qwen e Flux, che ora funzionano anche con Klein. + +### Modelli + +- **La scelta del modello sta nella barra in alto** dell'app (quella bianca, con RAM e VRAM), visibile da qualsiasi pagina: **Qwen**, **Klein 3** (8 passi) e **Klein 3 Base** (50 passi). Spariscono le tessere «Motore» dai pannelli di Crea, Modifica e Box. Se i file di Klein non ci sono, la pillola ha il puntino e il pannellino per scaricarli. +- **FLUX.2 Klein 4B è tolto**: via dal selettore, dal catalogo (modello e text encoder), dai grafi, dal riconoscimento dei LoRA. Le impostazioni salvate con Klein 4B passano da sole a Qwen; una foto fatta con lui si apre come Qwen. I suoi file già scaricati compaiono in Opzioni → File inutilizzati («tolto dal catalogo») e non vengono più scambiati per quelli di Klein 3. + +### Editor box + +- **Più di tre riquadri, e riquadri dentro i riquadri.** Un riquadro grande si prendeva tutto il mouse, quindi non si poteva più disegnare niente sopra; col terzo o quarto riquadro la tela era già piena. Ora lo strumento **Disegna** (di serie) fa nascere un riquadro nuovo ovunque, anche dentro un altro; il **numero** del riquadro lo sposta, le maniglie lo ridimensionano. Lo strumento **Sposta** afferra il riquadro dal corpo come prima. Un clic su un punto sceglie il riquadro più piccolo che lo contiene. Il tetto passa da 8 a **16**, come nel motore. + +### LoRA + +- **Due scaffali: Qwen e Flux (Klein 3).** La pagina LoRA ha una scheda per scaffale, ognuna con la sua zona per trascinare i file o incollare un link, la sua pila (ordine e forza) e le sue carte. I file vanno in `loras/qwen` e `loras/flux`; i nomi dei tensori decidono lo scaffale anche se carichi dalla scheda sbagliata (l'app lo dice). I LoRA che non si riconoscono restano nello scaffale della scheda da cui li hai caricati; quelli fatti per altri modelli (SD, SDXL, FLUX.1, FLUX.2) stanno in «Altri», dove si può scegliere a mano per quale modello usarli. +- **Perché i LoRA non funzionavano con Klein:** l'elenco dei LoRA non mandava all'interfaccia per quali modelli erano fatti, e ogni LoRA di Klein risultava «non compatibile» con Klein; inoltre la richiesta con Klein non portava nessun LoRA e le azioni rapide accendevano LoRA di Qwen. Ora la richiesta porta la pila del modello scelto; Klein veloce e Klein Base usano la stessa. Cambiando modello la pila dell'altro scaffale resta lì, ma non parte. +- Le azioni rapide, Camera e gli avvisi «LoRA consigliato» accendono solo LoRA che vanno col modello scelto (sono LoRA di Qwen: con Klein si va col modello da solo). Una rete di sicurezza nel motore toglie dalla richiesta i LoRA dell'altro scaffale. +- Non ancora provato con la GPU: il risultato di un LoRA vero su Klein (il motore li carica come per Qwen, ComfyUI ha la mappatura dei nomi di Klein). + +### Prove + +- Nuovi: `test/lora-scaffali.mjs` (importazione nei due scaffali con file safetensors finti, famiglie nell'elenco, note vecchie), `test/box-lora-ui.mjs` (barra dei modelli, sei riquadri e uno dentro l'altro, schede dei LoRA, pannellino di download). Aggiornati i test dei modelli, del catalogo, dello spazio, dei LoRA e dei grafi. + ## 1.2.0 — 2026-10-23 L'app cambia faccia e il motore impara cose nuove: l'**interfaccia Orbita** con il pannello dei controlli a sinistra, un quarto modello veloce (FLUX.2 Klein 4B), più LoRA insieme, l'editor a box, «senza sfondo» ed Estrai con SAM 2. Il motore passa alla versione 9: al primo avvio l'app lo aggiorna da sola (i modelli già scaricati restano dove sono). diff --git a/CLAUDE.md b/CLAUDE.md index 21129bd..4927c63 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,10 +57,12 @@ interfaccia**, installato e guidato dall'app. `motore/klein.ts` (`fileKlein`) trova i file (impostazioni → `risolti.json` → scansione cartelle, int8 prima di bf16) o dà un errore in italiano. - Grafo (`grafi.ts`, `condiziona`/`latenteVuota`/`campiona`): `UNETLoader` + `CLIPLoader` tipo `flux3` + `VAELoader`, `CLIPTextEncode`, foto di riferimento `VAEEncode` → `ReferenceLatent` (positivo e negativo, in catena), `EmptyFlux2LatentImage`, KSampler euler/simple (distillato cfg 1, base cfg 3,5). - Niente LoRA, Turbo, cache KV; i tag `` diventano ordinali. **I grafi Qwen non devono cambiare**: si controlla con un confronto sul grafo vecchio prima di ogni modifica. + Niente Turbo né cache KV (i LoRA invece sì: `applicaLora` vale per tutte le famiglie); i tag `` diventano ordinali. **I grafi Qwen non devono cambiare**: si controlla con un confronto sul grafo vecchio prima di ogni modifica. - **Testo con Klein**: ogni lavoro `descrivi` (migliora prompt, Foto→prompt, Idee, Estrai, Fusion/Esempio, Sorpresa, controllo omino, traduzione LLM) con famiglia Klein scrive col Qwen3-VL 4B (`conEncoderDiTesto` in `lavori.ts`, `encoderKleinFile` in `klein.ts`; `Richiesta.encoderKlein: false` per escluderlo): caricato come `krea2` e con `thinking: true` (con `false` il nodo mette un `` vuoto e il 4B risponde «user»/vuoto). Con Qwen resta l'encoder di Qwen. - `motore/pup.ts` = i prompt upsampler ufficiali; `lavori.ts` `potenzia()` li manda al Qwen3-VL prima del disegno (`impostazioni.potenziaKlein`, cache per prompt). - Non provato con la GPU qui: qualità, INT8 su 6-8 GB, `ReferenceLatent` in catena (alternativa `ReferenceMultiLatent`). +- **1.2.1: i modelli sono tre pillole** (`qwen-base`, `klein`, `klein-base`: «Qwen», «Klein 3», «Klein 3 Base») e si scelgono **solo dalla barra in alto** (`App.tsx` → `SelettoreModello` in `componenti/ScegliModello.tsx`, da qualsiasi pagina; nei pannelli di Crea/Modifica/Box non c'è un secondo selettore). **FLUX.2 Klein 4B è stato tolto** (niente famiglia `klein4b`, niente voci `klein4b-*` nel catalogo): le impostazioni vecchie passano a Qwen in `modelloDaImpostazioni`, i suoi file in `models/` si vedono come «tolto dal catalogo» (`idle.ts`) e `modelli:file` non li scambia per file di Klein 3 (`eFlux2`). +- **LoRA a due scaffali** (1.2.1): `GruppoLora = 'qwen' | 'flux'` (`shared/loraBase.ts`: `gruppoDiFamiglia`, `gruppoDa`, `vaBeneCon`; Klein veloce e Klein Base hanno gli stessi). File in `loras/qwen` e `loras/flux` (`lora.ts` `scaffale()`: i nomi dei tensori decidono, se non dicono niente vale la scheda da cui si carica, SDXL & co. restano fuori in «Altri» e si assegnano a mano con `InfoLora.gruppo`). **`elencoLora()` deve portare `famiglie` e `gruppo`**: nella 1.2.0 non lo faceva e il renderer dava «non compatibile» a ogni LoRA di Klein. Lo store ha **due pile** (`loraPile`, `loraAttive` = quella del modello scelto; `setLoraPila`/`toggleLoraPer` per scaffale); `richiestaBase()` manda la pila del modello, `loraPer`/`fileLoraCamera` (azioni rapide, Camera) solo LoRA che vanno col modello, e `soloLoraDelModello` in `lavori.ts` toglie quelli dell'altro scaffale. Prove: `test/lora-scaffali.mjs`, `test/box-lora-ui.mjs`. ## I grafi (src/main/motore/grafi.ts) - Pipeline ufficiale: `UnetLoaderGGUF` → LoRA (`LoraLoaderModelOnly`) → `QwenImage21Cache`; `CLIPLoader` tipo `qwen_image` @@ -147,6 +149,7 @@ interfaccia**, installato e guidato dall'app. il vecchio `testo` del titolo si trasforma in elemento in `vecchioInNuovo()`. `costruisci.ts` raggruppa (densità aerea/normale/fitta) e sceglie un modello fra i primi senza ripetere; `disegna.ts` disegna su canvas (schermo e stampa: cornice, ombra, z, elementi); `esporta.ts` JPEG/PDF (`pdf.ts`, senza librerie). Le foto grandi non restano in cache. +- **Editor box** (modo Box dello Studio, `pagine/Box.tsx` + `box.ts`): fino a 16 riquadri (`MAX_BOX`, come `regioniPronte`). Strumento **Disegna** (di serie): il corpo dei riquadri lascia passare il mouse (`.box-cornice.disegna .box-regione { pointer-events: none }`) così si disegna anche dentro uno grande; si sposta dal numero, si ridimensiona dalle maniglie, un clic sceglie il riquadro più piccolo sotto il punto (`riquadroSotto`). Strumento **Sposta**: il corpo si afferra. Non mettere controlli sopra la tela (coprono i punti di partenza del disegno): stanno nel pannello. - **Estrai** (modo di Modifica, `componenti/Estrai.tsx` + `estrai.ts` + `riquadri.ts`): a foto caricata `trovaElementi` chiede al Qwen3-VL (`descrivi` con la foto intera) un JSON `{bbox_2d 0–1000, label, kind: person|object}` (`leggiElementi`, tollera fence e JSON troncato, toglie i riquadri doppi). La ricerca parte **solo col pulsante** e la fa il Qwen3-VL (`descrivi`, `SISTEMA_ELEMENTI`, `maxToken 800`; niente rilevatore a parte: Florence è stato provato e tolto). Si sceglie con un clic (più clic = il riquadro più grande che contiene il punto) o riquadrando a mano; il tipo (persona/oggetto) si corregge. `richiestaEstrai` ritaglia la cosa (+12%) in un temp e fa una **Crea** con quella come ``: diff --git a/README.md b/README.md index 7b1db4a..b50d80b 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,12 @@ Gira su una scheda NVIDIA **da 6-8 GB**, senza cloud e senza abbonamenti. Crea: il preset Foto a sinistra, i tre modelli in alto, la Vespa a Positano +## Novità della 1.2.1 + +- **I modelli si scelgono dalla barra in alto** (sempre visibile, da qualsiasi pagina): **Qwen**, **Klein 3** e **Klein 3 Base**. **FLUX.2 Klein 4B** non c'è più. +- **Editor box**: si possono fare fino a **16 riquadri** e un riquadro anche **dentro un altro** (prima uno grande si prendeva il mouse). Con **Disegna** si trascina ovunque, il numero del riquadro lo sposta e le maniglie lo ridimensionano; con **Sposta** si afferra dal corpo. Un clic su un punto sceglie il riquadro più piccolo che c'è lì sotto. +- **LoRA divisi in due scaffali**, **Qwen** e **Flux (Klein 3)**, ognuno con la sua pila: carichi i file (o un link) nella scheda giusta, e l'app li mette nello scaffale giusto anche se sbagli scheda. I LoRA di Flux ora funzionano con Klein veloce e Klein Base. + ## Novità della 1.2.0: l'interfaccia Orbita - **Un solo Studio** con quattro modi: **Crea**, **Modifica**, **Box** e **Ricette**. I controlli stanno nel **pannello a sinistra**, il pulsante CREA è fisso in fondo e la risoluzione finale si legge sempre. @@ -43,7 +49,7 @@ Gira su una scheda NVIDIA **da 6-8 GB**, senza cloud e senza abbonamenti. - **Editor a box**: un riquadro per ogni cosa, con il suo prompt (regional prompting). - **Senza sfondo** (BiRefNet) ed **Estrai con SAM 2**: ritagli dai pixel della tua foto, senza ridisegnarla. - **LoRA multipli** in fila, con la forza di ciascuno, su Qwen e su Klein. -- **FLUX.2 Klein 4B** (4 passi) come quarto modello: a caldo è circa il 15 % più veloce di Klein veloce a 8 passi (Crea 5,4 s contro 6,4 s sulla RTX 4060). +- ~~FLUX.2 Klein 4B~~ (tolto nella 1.2.1). - **Modelli uncensored** scaricabili solo se accendi l'interruttore in Opzioni e accetti l'avviso; ogni voce è marcata. ## Installare @@ -117,7 +123,7 @@ Nella barra in alto scegli **chi disegna**: **Qwen base** (Qwen-Image 2.1, 40 pa Il Turbo di Qwen non c'è più: Klein veloce è il modo rapido. Anche la scrittura dei testi (Migliora prompt, Foto → prompt, Idee, Estrai, Unisci foto, Come l'esempio) usa il **Qwen3-VL 4B** di Klein, con Klein e con Qwen: scrive a 46 token al secondo sulla RTX 4060, -contro 3,3 dell'encoder 8B int8 e 0,55 dell'8B GGUF. «Migliora prompt + Crea» con Qwen passa da 186 a 134 secondi. In Opzioni puoi tornare all'encoder di Qwen. Klein non usa i LoRA: l'app li nasconde. +contro 3,3 dell'encoder 8B int8 e 0,55 dell'8B GGUF. «Migliora prompt + Crea» con Qwen passa da 186 a 134 secondi. In Opzioni puoi tornare all'encoder di Qwen. I LoRA hanno due scaffali, uno per Qwen e uno per Klein: ognuno si vede solo col suo modello. ## Estrai @@ -142,8 +148,7 @@ Klein ha i suoi file (versioni int8 o bf16, più l'encoder Qwen3-VL 4B e il VAE **Riusa**, **Varia**, **Rifinisci 2K**, **Ingrandisci 2×/4×**. Le impostazioni stanno dentro ogni PNG: trascini un'immagine fatta con DaProd Image e torna con prompt, seed e LoRA. - **Ogni foto entra come PNG**: JPG, WEBP, AVIF, HEIC, TIFF, i RAW delle fotocamere, girata per il verso giusto. -- **LoRA con un clic** (in Opzioni): trascini i `.safetensors` o incolli un link Hugging Face / Civitai, l'app controlla che - siano davvero per Qwen-Image 2.1, aggiunge le parole chiave al prompt. +- **LoRA con un clic** (pagina LoRA): due schede, **Qwen** e **Flux (Klein 3)**. Trascini i `.safetensors` o incolli un link Hugging Face / Civitai, l'app guarda per quale modello sono fatti, li mette nello scaffale giusto e aggiunge le parole chiave al prompt. - **La barra in alto** dice sempre quanta RAM e VRAM stai usando e quanto lavora la scheda video. ## Requisiti diff --git a/package-lock.json b/package-lock.json index fdb297c..6fca28d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "daprod-image", - "version": "1.2.0", + "version": "1.2.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "daprod-image", - "version": "1.2.0", + "version": "1.2.1", "license": "MIT", "dependencies": { "electron-updater": "^6.8.9" diff --git a/package.json b/package.json index 2ed4094..03f2a24 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "daprod-image", "productName": "DaProd Image", - "version": "1.2.0", + "version": "1.2.1", "description": "DaProd Image: crea e modifica foto con Qwen-Image-2.1 sul tuo PC (CUDA, da 6 GB di VRAM). Turbo a 8 passi, preset per poster e infografiche, zone con anteprima dal vivo, LoRA con un clic, galleria.", "author": "DaProd", "license": "MIT", @@ -14,7 +14,7 @@ "build": "electron-vite build", "start": "electron-vite preview", "typecheck": "tsc --noEmit -p tsconfig.node.json && tsc --noEmit -p tsconfig.web.json", - "test": "node test/modelli.mjs && node test/catalogo.mjs && node test/spazio.mjs && node test/lora.mjs && node test/regioni.mjs && node test/adatta.mjs && node test/grafi_v2.mjs && node test/confronta_grafi.mjs && node test/asset.mjs", + "test": "node test/modelli.mjs && node test/catalogo.mjs && node test/spazio.mjs && node test/lora.mjs && node test/lora-scaffali.mjs && node test/regioni.mjs && node test/adatta.mjs && node test/grafi_v2.mjs && node test/confronta_grafi.mjs && node test/asset.mjs", "prendi-uv": "node scripts/prendi-uv.mjs", "dist": "npm run prendi-uv && npm run build && electron-builder --win --x64", "prova": "node test/prove.mjs", diff --git a/src/main/idle.ts b/src/main/idle.ts index 98ebfa4..3ebcc05 100644 --- a/src/main/idle.ts +++ b/src/main/idle.ts @@ -64,13 +64,13 @@ export interface FileInutile { } // i file che c'erano nelle versioni vecchie e ora non sono più nel catalogo -const TOLTI = ['qwen-image-2.1-UC-Q5_K_M.gguf', 'qwen3vl_8b_bf16.safetensors'] +const TOLTI = ['qwen-image-2.1-UC-Q5_K_M.gguf', 'qwen3vl_8b_bf16.safetensors', 'flux-2-klein-4b-fp8.safetensors', 'qwen_3_4b.safetensors'] /** i file dei modelli che non sono in uso: non nelle impostazioni, non nel catalogo (o tolti dal catalogo): si possono buttare */ export function fileInutilizzati(): FileInutile[] { const imp = impostazioni() // i file di Klein (scelti a mano o scaricati dall'app) sono in uso quanto quelli di Qwen - const klein = ['klein-int8', 'klein-bf16', 'klein-base-int8', 'klein-base-bf16', 'klein-te', 'klein-vae', 'klein4b-fp8', 'klein4b-te', 'llm-huihui-4b'].map((id) => risoltoSalvato(id)?.file || '') + const klein = ['klein-int8', 'klein-bf16', 'klein-base-int8', 'klein-base-bf16', 'klein-te', 'klein-vae', 'llm-huihui-4b'].map((id) => risoltoSalvato(id)?.file || '') const inUso = new Set([imp.modello, imp.encoder, imp.vae, imp.upscaler, imp.loraTurbo, imp.kleinModello, imp.kleinBase, imp.kleinEncoder, imp.kleinVae, ...klein].filter(Boolean).map((x) => x.toLowerCase())) const noti = new Set(statoModelli().filter((v) => !v.link).map((v) => (risoltoSalvato(v.id)?.file || v.file).toLowerCase()).filter(Boolean)) const mmproj = (n: string): boolean => /mmproj|\.mmproj\./i.test(n) diff --git a/src/main/index.ts b/src/main/index.ts index 2ff6601..eec7792 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,7 +5,7 @@ import { appendFileSync, copyFileSync, existsSync, readFileSync, readdirSync, rm import { basename, extname, join, normalize } from 'node:path' import { pathToFileURL } from 'node:url' import { randomUUID } from 'node:crypto' -import type { Download, Famiglia, FiltroGalleria, Impostazioni, InfoLora, Richiesta } from '@shared/tipi' +import type { Download, Famiglia, FiltroGalleria, GruppoLora, Impostazioni, InfoLora, Richiesta } from '@shared/tipi' import { vaBeneCon } from '@shared/loraBase' import { DATI, ENTRATA, MINIATURE, RADICE, TEMP, assicura } from './percorsi' import { impostazioni, salvaImpostazioni } from './impostazioni' @@ -210,20 +210,20 @@ function registraIpc(): void { const tutti = [...leggi('diffusion_models', /\.(gguf|safetensors)$/i), ...leggi('unet', /\.(gguf|safetensors)$/i)] const enc = leggi('text_encoders', /\.(safetensors|gguf)$/i) const vae = leggi('vae', /\.safetensors$/i) - // i file di Klein sono altri: nelle scelte di Qwen non devono comparire - const isKlein = (n: string): boolean => /klein/i.test(n) - const isKleinTe = (n: string): boolean => /(qwen3|qwen_3).*4b/i.test(n) || isKlein(n) + // i file di Klein sono altri: nelle scelte di Qwen non devono comparire. I file di FLUX.2 Klein 4B/9B (tolto nella 1.2.1) e il + // Qwen3 4B di solo testo che serviva a lui non sono di FLUX 3 Klein: restano fuori da tutte le liste. + const eFlux2 = (n: string): boolean => /klein.*[-_](4b|9b)|[-_](4b|9b).*klein|^qwen_3_4b\./i.test(n) + const isKlein = (n: string): boolean => /klein/i.test(n) && !eFlux2(n) + const isKleinTe = (n: string): boolean => (/(qwen3|qwen_3).*4b/i.test(n) && !eFlux2(n)) || isKlein(n) const isKleinVae = (n: string): boolean => isKlein(n) || /flux.?[23]/i.test(n) return { - diffusione: tutti.filter((n) => !isKlein(n)), - encoder: enc.filter((n) => !isKleinTe(n)), + diffusione: tutti.filter((n) => !isKlein(n) && !eFlux2(n)), + encoder: enc.filter((n) => !isKleinTe(n) && !eFlux2(n)), vae: vae.filter((n) => !isKleinVae(n)), upscaler: leggi('upscale_models', /\.(pth|safetensors)$/i), klein: tutti.filter(isKlein), kleinTe: enc.filter(isKleinTe), - kleinVae: vae.filter(isKleinVae), - klein4b: tutti.filter((n) => /klein.*4b|4b.*klein/i.test(n)), - klein4bTe: enc.filter((n) => /^qwen_?3_4b/i.test(n)) + kleinVae: vae.filter(isKleinVae) } }) gestisci('modelli:inutilizzati', () => fileInutilizzati()) @@ -325,14 +325,14 @@ function registraIpc(): void { // i LoRA che si possono usare con questo modello: compatibili ('si') e da provare ('?'), non quelli di un altro modello gestisci('lora:compatibili', (famiglia: Famiglia) => elencoLora().filter((l) => vaBeneCon(l, famiglia) !== 'no')) gestisci('lora:aggiorna', (file: string, m: Partial) => aggiornaLora(file, m)) - gestisci('lora:importa', (percorsi: string[]) => importaLora(percorsi)) + gestisci('lora:importa', (percorsi: string[], gruppo?: GruppoLora) => importaLora(percorsi, gruppo)) gestisci('lora:elimina', (file: string) => eliminaLora(file)) gestisci('lora:apriCartella', () => { const c = join(impostazioni().cartellaModelli, 'loras') assicura(c) return shell.openPath(c) }) - gestisci('lora:scarica', async (link: string) => { + gestisci('lora:scarica', async (link: string, gruppo?: GruppoLora) => { const d = nuovoDownload('LoRA da ' + (() => { try { return new URL(link).hostname @@ -341,7 +341,7 @@ function registraIpc(): void { } })()) try { - const file = await scaricaLora(link, d.avanz, d.ctrl.signal) + const file = await scaricaLora(link, d.avanz, d.ctrl.signal, gruppo) d.info.stato = 'fatto' d.info.nome = file } catch (e) { diff --git a/src/main/installa/catalogo.ts b/src/main/installa/catalogo.ts index 5f021d1..88dc312 100644 --- a/src/main/installa/catalogo.ts +++ b/src/main/installa/catalogo.ts @@ -8,10 +8,6 @@ const ENC_MR = 'mradermacher/Qwen3-VL-8B-Instruct-Uncensored-V2-GGUF' const KLEIN = 'black-forest-labs/flux-3-image-klein-comfy' // revisioni fissate (controllate il 22/10/2026): il download è riproducibile anche se il repo cambia dopo const KLEIN_REV = 'feef60b0aaf63a4773ace96636ebbcf43b881312' -const KLEIN4B_FP8 = 'black-forest-labs/FLUX.2-klein-4b-fp8' -const KLEIN4B_FP8_REV = '5b4408e59397a4a37ccb46afe426d8ed86379441' -const KLEIN4B_COMFY = 'Comfy-Org/vae-text-encorder-for-flux-klein-4b' -const KLEIN4B_COMFY_REV = '87cfba3d4b98662fb80f23921bb37bf887c97c58' const BIREFNET = 'ZhengPeng7/BiRefNet' const BIREFNET_REV = 'e2bf8e4460fc8fa32bba5ea4d94b3233d367b0e4' const SAM2 = 'facebook/sam2.1-hiera-small' @@ -117,20 +113,6 @@ const GREZZO: VoceCatalogo[] = [ file: '', cartella: 'vae', url: '', byte: 336_211_292, circa: true, repo: KLEIN, revisione: KLEIN_REV, cerca: ['^flux2-vae\\.safetensors$', '^(?=.*flux)(?=.*vae)', '^(?=.*vae)(?!.*qwen)'] }, - // --- FLUX.2 Klein 4B (Black Forest Labs, gennaio 2026): licenza Apache-2.0, 4 passi senza CFG. Il denoiser è l'FP8 ufficiale (4 GB); - // l'encoder è un Qwen3 4B di solo testo (quello dei workflow di ComfyUI); il VAE è lo stesso di Klein 3 (flux2-vae, voce `klein-vae`). --- - { - id: 'klein4b-fp8', tipo: 'klein', nome: 'FLUX.2 Klein 4B · FP8', - descrizione: 'Klein da 4 miliardi di parametri: 4 passi senza CFG, licenza Apache-2.0 (uso commerciale libero). Non fa le modifiche con tante foto bene come Klein 3.', - file: '', cartella: 'diffusion_models', url: '', byte: 4_070_624_520, circa: true, - repo: KLEIN4B_FP8, revisione: KLEIN4B_FP8_REV, cerca: ['^flux-2-klein-4b-fp8\\.safetensors$', '^(?=.*klein)(?=.*4b)(?!.*base)(?=.*fp8)'] - }, - { - id: 'klein4b-te', tipo: 'klein-te', nome: 'Text encoder di FLUX.2 Klein 4B (Qwen3 4B)', - descrizione: 'Il Qwen3 4B di solo testo che legge il prompt per FLUX.2 Klein 4B. Pesa 8 GB e lavora dalla RAM.', - file: '', cartella: 'text_encoders', url: '', byte: 8_044_982_048, circa: true, - repo: KLEIN4B_COMFY, revisione: KLEIN4B_COMFY_REV, cerca: ['^qwen_3_4b\\.safetensors$'] - }, // --- togli sfondo e estrai fedele: si scaricano al primo uso, girano in una fase a parte (pochi centinaia di MB di VRAM) e poi escono dalla scheda --- { id: 'sfondo-birefnet', tipo: 'sfondo', nome: 'BiRefNet (togli sfondo)', @@ -243,8 +225,6 @@ const META: Record> = { ...Object.fromEntries(['klein-int8', 'klein-bf16', 'klein-base-int8', 'klein-base-bf16'].map((id) => [id, { licenza: 'FLUX Kommunity', licenzaUrl: LIC_KOMMUNITY, ruolo: 'denoiser', avviso: AVVISO_KOMMUNITY } as Partial])), 'klein-te': { licenza: 'Apache-2.0', licenzaUrl: LIC_APACHE, ruolo: 'encoder' }, 'klein-vae': { licenza: 'Apache-2.0', licenzaUrl: LIC_APACHE, ruolo: 'vae' }, - 'klein4b-fp8': { licenza: 'Apache-2.0', licenzaUrl: 'https://huggingface.co/black-forest-labs/FLUX.2-klein-4B/raw/main/LICENSE.md', ruolo: 'denoiser' }, - 'klein4b-te': { licenza: 'Apache-2.0', licenzaUrl: LIC_APACHE, ruolo: 'encoder' }, 'sfondo-birefnet': { licenza: 'MIT', licenzaUrl: 'https://huggingface.co/ZhengPeng7/BiRefNet', ruolo: 'sfondo' }, 'seg-sam2': { licenza: 'Apache-2.0', licenzaUrl: LIC_APACHE, ruolo: 'segmentatore' }, 'llm-huihui-4b': { licenza: 'Apache-2.0', licenzaUrl: LIC_APACHE, ruolo: 'llm', avviso: AVVISO_UNC }, diff --git a/src/main/lora.ts b/src/main/lora.ts index 77a54c3..1deeeb6 100644 --- a/src/main/lora.ts +++ b/src/main/lora.ts @@ -1,11 +1,12 @@ // I LoRA: i file stanno in models/loras, le note (nome, parole chiave, forza, anteprima) in lora.json. // Si importano trascinandoli, si scaricano da un link (Hugging Face, Civitai o diretto), -// e dall'intestazione safetensors si capisce per quale modello sono (Qwen-Image 2.1, FLUX 3 Klein, FLUX.2 Klein 4B: `shared/loraBase.ts`). +// e dall'intestazione safetensors si capisce per quale modello sono (Qwen-Image 2.1 o FLUX 3 Klein: `shared/loraBase.ts`). +// Due scaffali: i LoRA di Qwen stanno in loras/qwen, quelli di Flux (Klein 3) in loras/flux; quelli che non si riconoscono restano in loras. import { shell } from 'electron' -import { copyFileSync, existsSync, openSync, readFileSync, readSync, closeSync, readdirSync, statSync, writeFileSync } from 'node:fs' +import { copyFileSync, existsSync, openSync, readFileSync, readSync, closeSync, readdirSync, renameSync, statSync, writeFileSync } from 'node:fs' import { basename, extname, join, relative, resolve, sep } from 'node:path' -import type { Famiglia, InfoLora } from '@shared/tipi' -import { analizzaChiavi } from '@shared/loraBase' +import type { Famiglia, GruppoLora, InfoLora } from '@shared/tipi' +import { analizzaChiavi, gruppoDa } from '@shared/loraBase' import { DATI, assicura, scriviAtomico } from './percorsi' import { impostazioni } from './impostazioni' import { scarica } from './installa/scarica' @@ -15,6 +16,12 @@ const NOTE = join(DATI, 'lora.json') type Nota = Partial> & { compatibile?: InfoLora['compatibile']; base?: string } const cartellaLora = (): string => join(impostazioni().cartellaModelli, 'loras') +const FAMIGLIE_VALIDE: Famiglia[] = ['qwen', 'klein', 'klein-base'] +/** un LoRA messo a mano in loras/qwen o loras/flux sta in quello scaffale (se i nomi dei tensori non dicono altro) */ +const gruppoDaCartella = (file: string): GruppoLora | undefined => { + const primo = file.split(/[\\/]/)[0].toLowerCase() + return file.includes('\\') || file.includes('/') ? (primo === 'qwen' || primo === 'flux' ? primo : undefined) : undefined +} function note(): Record { try { @@ -101,7 +108,8 @@ export function elencoLora(): InfoLora[] { // ComfyUI vuole il nome relativo alla cartella loras, con le barre di Windows const file = relative(c, p) let nota = n[file] - if (!nota || nota.compatibile === undefined || nota.famiglie === undefined) { + // (le note di prima della 1.2.1 possono dire 'klein4b', un modello che l'app non ha più: si rianalizzano) + if (!nota || nota.compatibile === undefined || nota.famiglie === undefined || nota.famiglie.some((f) => !FAMIGLIE_VALIDE.includes(f))) { const a = analizza(p) const delCatalogo = CATALOGO.find((x) => x.tipo === 'lora' && x.file === v) nota = { nome: delCatalogo?.nome || a.nome || basename(v, extname(v)), parole: a.parole || '', forza: 1, aggiunta: st.mtimeMs, ...nota, compatibile: a.compatibile, base: a.base, famiglie: a.famiglie } @@ -119,6 +127,8 @@ export function elencoLora(): InfoLora[] { note: nota.note, compatibile: nota.compatibile || '?', base: nota.base, + famiglie: nota.famiglie, + gruppo: nota.gruppo ?? gruppoDaCartella(file), aggiunta: nota.aggiunta || st.mtimeMs }) } @@ -134,16 +144,30 @@ export function aggiornaLora(file: string, modifiche: Partial): void { scriviNote(n) } -export function importaLora(percorsi: string[]): string[] { +/** dove va un file da mettere fra i LoRA: i nomi dei tensori decidono lo scaffale (qwen o flux); se non dicono niente vale la scelta di chi lo carica; + * se non c'è nemmeno quella resta nella cartella grande. Ritorna la cartella e lo scaffale (undefined = nessuno). */ +function scaffale(percorso: string, scelto?: GruppoLora): { cartella: string; gruppo?: GruppoLora; riconosciuto: boolean } { + const a = analizza(percorso) + const riconosciuto = gruppoDa({ compatibile: a.compatibile, famiglie: a.famiglie }) + // un LoRA di un altro modello (SD, SDXL…) non va in nessuno scaffale, nemmeno se l'utente ha aperto quello: si vede in «Altri» + const gruppo = riconosciuto ?? (a.compatibile === '?' ? scelto : undefined) + return { cartella: gruppo ? join(cartellaLora(), gruppo) : cartellaLora(), gruppo, riconosciuto: !!riconosciuto } +} + +/** `gruppo`: lo scaffale della scheda da cui si carica (Qwen o Flux). Se i nomi dei tensori dicono un altro modello, vince quello. */ +export function importaLora(percorsi: string[], gruppo?: GruppoLora): string[] { const c = cartellaLora() assicura(c) const fatti: string[] = [] for (const p of percorsi) { if (!/\.safetensors$/i.test(p)) continue - const dest = join(c, basename(p)) + const s = scaffale(p, gruppo) + assicura(s.cartella) + const dest = join(s.cartella, basename(p)) if (!existsSync(dest)) copyFileSync(p, dest) - aggiornaLora(basename(p), { aggiunta: Date.now() }) - fatti.push(basename(p)) + const file = relative(c, dest) + aggiornaLora(file, { aggiunta: Date.now(), ...(s.gruppo && !s.riconosciuto ? { gruppo: s.gruppo } : {}) }) + fatti.push(file) } return fatti } @@ -221,12 +245,26 @@ async function risolvi(link: string): Promise<{ url: string; nome?: string; file return { url: u.toString(), file: basename(u.pathname) || 'lora.safetensors', intestazioni } } -export async function scaricaLora(link: string, avanz: (r: number, t: number, v: number) => void, segnale?: AbortSignal): Promise { +/** `gruppo`: lo scaffale della scheda da cui si scarica (Qwen o Flux), come in `importaLora` */ +export async function scaricaLora(link: string, avanz: (r: number, t: number, v: number) => void, segnale?: AbortSignal, gruppo?: GruppoLora): Promise { const d = await risolvi(link) let file = (d.file || 'lora.safetensors').replace(/[<>:"/\\|?*]+/g, '_') if (!/\.safetensors$/i.test(file)) file += '.safetensors' + assicura(cartellaLora()) const dest = join(cartellaLora(), file) await scarica({ url: d.url, destinazione: dest, intestazioni: d.intestazioni, segnale, avanzamento: avanz }) + // arrivato, si guarda di che modello è e lo si mette nel suo scaffale + const s = scaffale(dest, gruppo) + let finale = dest + if (s.gruppo) { + assicura(s.cartella) + const nuovo = join(s.cartella, file) + if (!existsSync(nuovo)) { + renameSync(dest, nuovo) + finale = nuovo + } + } + const relativo = relative(cartellaLora(), finale) let anteprima: string | undefined if (d.anteprima) { try { @@ -237,6 +275,6 @@ export async function scaricaLora(link: string, avanz: (r: number, t: number, v: anteprima = undefined } } - aggiornaLora(file, { nome: d.nome, parole: d.parole, fonte: link, anteprima, aggiunta: Date.now() }) - return file + aggiornaLora(relativo, { nome: d.nome, parole: d.parole, fonte: link, anteprima, aggiunta: Date.now(), ...(s.gruppo && !s.riconosciuto ? { gruppo: s.gruppo } : {}) }) + return relativo } diff --git a/src/main/motore/grafi.ts b/src/main/motore/grafi.ts index 1ff0ccb..bb635c1 100644 --- a/src/main/motore/grafi.ts +++ b/src/main/motore/grafi.ts @@ -91,11 +91,11 @@ function applicaLora(g: Grafo, model: Rif, lora: LoraAttiva[], salta?: string): return m } -/** FLUX 3 Klein e FLUX.2 Klein 4B: UNETLoader (i pesi INT8/FP8/BF16 li riconosce da soli ComfyUI), LoRA, text encoder (Qwen3-VL 4B di tipo flux3; Qwen3 4B di tipo flux2 per la 4B), VAE di FLUX.2. Niente cache KV. */ +/** FLUX 3 Klein: UNETLoader (i pesi INT8/BF16 li riconosce da soli ComfyUI), LoRA, text encoder (Qwen3-VL 4B di tipo flux3), VAE di FLUX.2. Niente cache KV. */ function baseKlein(g: Grafo, imp: Impostazioni, fam: Famiglia, lora: LoraAttiva[]): { model: Rif; clip: Rif; vae: Rif } { const file = fam === 'klein-base' ? imp.kleinBase : imp.kleinModello const model = applicaLora(g, r(g.add('UNETLoader', { unet_name: file, weight_dtype: 'default' }, 'Carico il modello')), lora) - const clip = r(g.add('CLIPLoader', { clip_name: imp.kleinEncoder, type: fam === 'klein4b' ? 'flux2' : 'flux3', device: imp.encoderSuCpu ? 'cpu' : 'default' }, 'Carico il text encoder')) + const clip = r(g.add('CLIPLoader', { clip_name: imp.kleinEncoder, type: 'flux3', device: imp.encoderSuCpu ? 'cpu' : 'default' }, 'Carico il text encoder')) const vae = r(g.add('VAELoader', { vae_name: imp.kleinVae }, 'Carico il VAE')) return { model, clip, vae } } @@ -188,22 +188,6 @@ function latenteVuota(g: Grafo, fam: Famiglia, w: number, h: number): Rif { } function campiona(g: Grafo, q: Richiesta, fam: Famiglia, model: Rif, c: Condizioni, latente: Rif, passi: number, denoise = 1, dim?: { w: number; h: number }): Rif { - if (fam === 'klein4b') { - // FLUX.2 Klein 4B (distillato): come il workflow ufficiale di ComfyUI, scaletta Flux2Scheduler (dipende dalla grandezza) + CFGGuider 1 + euler. - // Con denoise < 1 la scaletta è più lunga e se ne prende la coda (SplitSigmasDenoise): si fanno `passi` passi veri. - const w = dim?.w ?? q.larghezza - const h = dim?.h ?? q.altezza - const totali = denoise < 0.999 ? Math.max(passi, Math.round(passi / Math.max(0.05, denoise))) : passi - const scaletta = g.add('Flux2Scheduler', { steps: totali, width: w, height: h }) - const sigme: Rif = denoise < 0.999 ? r(g.add('SplitSigmasDenoise', { sigmas: r(scaletta), denoise }), 1) : r(scaletta) - const guida = g.add('CFGGuider', { model, positive: c.pos, negative: c.neg, cfg: 1 }) - const out = g.add( - 'SamplerCustomAdvanced', - { noise: r(g.add('RandomNoise', { noise_seed: q.seed })), guider: r(guida), sampler: r(g.add('KSamplerSelect', { sampler_name: 'euler' })), sigmas: sigme, latent_image: latente }, - 'Disegno' - ) - return r(out, 0) - } // Klein veloce è distillato (8 passi, senza CFG); Klein Base vuole CFG 3,5; sampler e scheduler sono quelli del riferimento (euler, e la scaletta del modello) const cfg = fam === 'qwen' ? q.cfg : fam === 'klein' ? 1 : q.cfg > 1 ? q.cfg : 3.5 return r( diff --git a/src/main/motore/klein.ts b/src/main/motore/klein.ts index 000ec4d..162c03a 100644 --- a/src/main/motore/klein.ts +++ b/src/main/motore/klein.ts @@ -1,4 +1,4 @@ -// FLUX 3 Klein e FLUX.2 Klein 4B: quali file usare. Le impostazioni possono dirlo; se sono vuote si usano quelli scaricati dall'app (risolti.json) e, +// FLUX 3 Klein: quali file usare. Le impostazioni possono dirlo; se sono vuote si usano quelli scaricati dall'app (risolti.json) e, // in mancanza, si cerca nelle cartelle dei modelli (chi ha già i file li mette lì a mano). import { existsSync, readdirSync } from 'node:fs' import { join } from 'node:path' @@ -53,9 +53,8 @@ export function encoderDiTesto(imp: Impostazioni): string { return llmUncensoredFile(imp) || encoderKleinFile(imp) } -/** i file di Klein (o di Klein Base, o di FLUX.2 Klein 4B) da usare adesso, o l'errore da mostrare in italiano se mancano */ +/** i file di Klein (o di Klein Base) da usare adesso, o l'errore da mostrare in italiano se mancano */ export function fileKlein(imp: Impostazioni, famiglia: Famiglia): FileKlein | string { - if (famiglia === 'klein4b') return fileKlein4b(imp) const base = famiglia === 'klein-base' const idModelli = base ? ['klein-base-int8', 'klein-base-bf16'] : ['klein-int8', 'klein-bf16'] const scelto = base ? imp.kleinBase : imp.kleinModello @@ -72,22 +71,8 @@ export function fileKlein(imp: Impostazioni, famiglia: Famiglia): FileKlein | st return { modello, encoder, vae } } -/** il VAE di FLUX.2 (lo stesso per Klein 3 e Klein 4B) */ +/** il VAE di FLUX.2 (quello di Klein 3) */ function vaeKlein(imp: Impostazioni): string { const vae = sul(imp, 'vae', imp.kleinVae) || sul(imp, 'vae', risoltoSalvato('klein-vae')?.file) return vae || preferisci(lista(imp, 'vae'), [/^flux2-vae\./i, /klein/i, /flux.?[23]/i, /^ae\./i, /flux/i]) } - -/** FLUX.2 Klein 4B: il denoiser (FP8 ufficiale), il Qwen3 4B di solo testo e il VAE di FLUX.2 */ -function fileKlein4b(imp: Impostazioni): FileKlein | string { - let modello = sul(imp, 'diffusion_models', risoltoSalvato('klein4b-fp8')?.file) - if (!modello) { - const tutti = [...lista(imp, 'diffusion_models'), ...lista(imp, 'unet')].filter((n) => /klein.*4b|4b.*klein/i.test(n) && !/base/i.test(n) && !/vae|text_enc/i.test(n)) - modello = preferisci(tutti, [/fp8/i, /bf16/i, /./]) - } - const encoder = sul(imp, 'text_encoders', risoltoSalvato('klein4b-te')?.file) || preferisci(lista(imp, 'text_encoders'), [/^qwen_3_4b\.safetensors$/i, /^qwen_?3_4b/i]) - const vae = vaeKlein(imp) - const mancano = [!modello && 'il modello FLUX.2 Klein 4B', !encoder && 'il text encoder (Qwen3 4B)', !vae && 'il VAE'].filter(Boolean) - if (mancano.length) return `FLUX.2 Klein 4B non è pronto: mancano ${mancano.join(', ')}. Scaricali dal selettore del modello (o da Opzioni → Scarica altri modelli).` - return { modello, encoder, vae } -} diff --git a/src/main/motore/lavori.ts b/src/main/motore/lavori.ts index ce90c46..25390e7 100644 --- a/src/main/motore/lavori.ts +++ b/src/main/motore/lavori.ts @@ -18,6 +18,7 @@ import { PUP_I2I, PUP_I2I_BREVE, PUP_T2I, PUP_T2I_BREVE } from './pup' import { encoderKleinFile, fileKlein } from './klein' import { cambioDiModello, faseScarico, occupaImmagine, occupaTesto, scriveCol4B, type Occupa } from './spazio' import { elencoLora } from '../lora' +import { vaBeneCon } from '@shared/loraBase' import { scaricaVoce, statoModelli } from '../installa/installatore' const lavori: Lavoro[] = [] @@ -58,8 +59,26 @@ function conParoleLora(q: Richiesta): Richiesta { return extra.length ? { ...q, prompt: `${extra.join(', ')}, ${q.prompt}` } : q } +/** rete di sicurezza: un LoRA fatto per l'altro modello (un Qwen con Klein o al contrario) farebbe fermare il motore, quindi non parte. + * L'interfaccia non li manda (pile separate per Qwen e per Flux), ma una ricetta o una foto vecchia della galleria possono riportarli. */ +function soloLoraDelModello(q: Richiesta): Richiesta { + if (q.modalita === 'descrivi' || !q.lora?.length) return q + let info + try { + info = elencoLora() + } catch { + return q + } + const fam = q.famiglia ?? impostazioni().famiglia ?? 'qwen' + const buoni = q.lora.filter((a) => { + const l = info.find((x) => x.file === a.file) + return !l || vaBeneCon(l, fam) !== 'no' + }) + return buoni.length === q.lora.length ? q : { ...q, lora: buoni } +} + export function accodaRichiesta(q0: Richiesta): Lavoro[] { - const q = conParoleLora(adattaRichiesta(q0)) + const q = conParoleLora(soloLoraDelModello(adattaRichiesta(q0))) const quante = Math.max(1, Math.min(16, q.quante || 1)) const base = q.seed >= 0 ? q.seed : -1 const nuovi: Lavoro[] = [] @@ -320,7 +339,7 @@ async function potenzia(l: Lavoro, ing: Ingressi, impLavoro: Impostazioni, fam: // il testo che l'upsampler vede: con foto, la richiesta di modifica; da zero, la richiesta e la forma della tela (TARGET CANVAS) const mcd = (a: number, b: number): number => (b ? mcd(b, a % b) : a) const g0 = mcd(q.larghezza || 1, q.altezza || 1) - // il Klein 4B capisce solo i prompt di sistema brevi (con quello lungo ricopia le regole): con lui si usano quelli, e la richiesta da foto va nuda + // il Qwen3-VL 4B (l'encoder di Klein) capisce solo i prompt di sistema brevi (con quello lungo ricopia le regole): con lui si usano quelli, e la richiesta da foto va nuda // (con «Reference images follow… Output the caption» risponde «user») const encoderKlein = fam !== 'qwen' && !!imp.kleinEncoder const richiesta = p.replace(//g, (_m, n: string) => `the image number ${n}`) @@ -500,7 +519,7 @@ async function esegui(l: Lavoro): Promise { sampler: q.sampler, scheduler: q.scheduler, lora: q.lora, - modello: usaKlein ? (fam === 'klein-base' ? 'FLUX 3 Klein Base · ' : fam === 'klein4b' ? 'FLUX.2 Klein 4B · ' : 'FLUX 3 Klein · ') + impLavoro.kleinModello : imp.modello, + modello: usaKlein ? (fam === 'klein-base' ? 'FLUX 3 Klein Base · ' : 'FLUX 3 Klein · ') + impLavoro.kleinModello : imp.modello, forza: q.forza, trasparente: q.trasparente, origine: q.origine, diff --git a/src/main/motore/regioni.ts b/src/main/motore/regioni.ts index 12259de..53e74fd 100644 --- a/src/main/motore/regioni.ts +++ b/src/main/motore/regioni.ts @@ -5,7 +5,7 @@ // {"id", "bbox": [y_min, x_min, y_max, x_max], "desc"} su una tela 0–1000 (origine in alto a sinistra) e la didascalia che cita ogni // (formato di black-forest-labs/flux-3-image-klein, src/flux3/bbox.py). Per le modifiche una riga è {"id", "from": "target", "src_bbox": null, "tgt_bbox", "desc"}. // I riquadri sono una guida morbida: l'elemento può uscire un po' dal rettangolo, e non protegge i pixel fuori (per quello c'è la zona con la maschera). -// - Qwen-Image 2.1 e FLUX.2 Klein 4B non hanno i riquadri: la posizione si dice a parole («at the top left, a small …»). +// - Qwen-Image 2.1 non ha i riquadri: la posizione si dice a parole («at the top left, a small …»). import type { Box01, Regione } from '../../shared/tipi' export interface RegionePronta { @@ -69,7 +69,7 @@ export function dove(b: Box01): string { return grande + posto } -/** il prompt per Qwen-Image 2.1 e FLUX.2 Klein 4B: la posizione di ogni elemento a parole */ +/** il prompt per Qwen-Image 2.1: la posizione di ogni elemento a parole */ export function promptConPosizioni(scena: string, regioni: RegionePronta[], modifica = false): string { if (!regioni.length) return scena const testi = regioni.map((r) => (modifica ? `${maiuscola(senzaPunto(r.prompt))} — ${dove(r.box)}.` : `${maiuscola(dove(r.box))}: ${senzaPunto(r.prompt)}.`)) diff --git a/src/main/motore/spazio.ts b/src/main/motore/spazio.ts index ade5e13..dc76b8d 100644 --- a/src/main/motore/spazio.ts +++ b/src/main/motore/spazio.ts @@ -7,10 +7,10 @@ import type { Famiglia } from '../../shared/tipi' * - `testo:4b` / `testo:8b`: l'LLM che scrive (il Qwen3-VL 4B di Klein o l'8B di Qwen); * - `cpu`: il traduttore dedicato, che sta sul processore; * - `leggero`: l'ingranditore (pochi MB): non chiede spazio e non cambia quello che c'è. */ -export type Occupa = 'immagine:qwen' | 'immagine:klein' | 'immagine:klein-base' | 'immagine:klein4b' | 'testo:4b' | 'testo:8b' | 'cpu' | 'leggero' +export type Occupa = 'immagine:qwen' | 'immagine:klein' | 'immagine:klein-base' | 'testo:4b' | 'testo:8b' | 'cpu' | 'leggero' /** la chiave di un lavoro che disegna */ -export const occupaImmagine = (fam: Famiglia): Occupa => (fam === 'klein' ? 'immagine:klein' : fam === 'klein4b' ? 'immagine:klein4b' : fam === 'klein-base' ? 'immagine:klein-base' : 'immagine:qwen') +export const occupaImmagine = (fam: Famiglia): Occupa => (fam === 'klein' ? 'immagine:klein' : fam === 'klein-base' ? 'immagine:klein-base' : 'immagine:qwen') /** la chiave di un lavoro che scrive: col 4B di Klein (`encoderKlein`) o con l'8B di Qwen */ export const occupaTesto = (encoderKlein?: boolean): Occupa => (encoderKlein ? 'testo:4b' : 'testo:8b') @@ -40,8 +40,6 @@ export function faseScarico(prima: Occupa | null): string { return 'Scarico FLUX 3 Klein' case 'immagine:klein-base': return 'Scarico FLUX 3 Klein Base' - case 'immagine:klein4b': - return 'Scarico FLUX.2 Klein 4B' case 'testo:4b': case 'testo:8b': return 'Scarico l\'LLM' diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index d439d64..087b4b9 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -8,6 +8,7 @@ import { Crea } from './pagine/Crea' import { Modifica } from './pagine/Modifica' import { Box } from './pagine/Box' import { Lora } from './pagine/Lora' +import { SelettoreModello } from './componenti/ScegliModello' import { StripModi, PAGINE_STUDIO, vaiModo, type ModoStudio } from './pagine/Studio' import { Album } from './pagine/Album' import { BarraStato } from './componenti/BarraStato' @@ -191,6 +192,8 @@ export function App(): JSX.Element { DAPROD IMAGE + {/* il modello che disegna si sceglie qui, in alto, da qualsiasi pagina */} + {pronto && installato && } {agg.stato === 'scarico' && ( vai('impostazioni')}> diff --git a/src/renderer/src/anteprime.ts b/src/renderer/src/anteprime.ts index 958493f..3dd2a4d 100644 --- a/src/renderer/src/anteprime.ts +++ b/src/renderer/src/anteprime.ts @@ -43,9 +43,10 @@ export function chiaveRicetta(idWorkflow: string): ChiaveAsset | undefined { } /** la copertina generica di un LoRA senza anteprima: «Qwen» o «Klein» dalla famiglia; se serve a più famiglie o non si capisce → niente (resta la sigla colorata) */ -export function chiaveLora(l: { famiglie?: string[]; compatibile?: string }): ChiaveAsset | undefined { +export function chiaveLora(l: { famiglie?: string[]; compatibile?: string; gruppo?: string }): ChiaveAsset | undefined { const f = new Set((l.famiglie || []).map((x) => (x === 'qwen' ? 'qwen' : 'klein'))) if (f.size === 1) return `lora/${[...f][0]}` + if (!f.size && l.gruppo) return l.gruppo === 'qwen' ? 'lora/qwen' : 'lora/klein' return !f.size && l.compatibile === 'si' ? 'lora/qwen' : undefined } diff --git a/src/renderer/src/api.ts b/src/renderer/src/api.ts index e49f8ce..19d19f3 100644 --- a/src/renderer/src/api.ts +++ b/src/renderer/src/api.ts @@ -1,6 +1,6 @@ // L'API del processo principale, con i tipi giusti. import type { - ControlloSistema, Download, Famiglia, FiltroGalleria, Impostazioni, InfoLora, InfoMotore, Lavoro, Opera, PassoSetup, Richiesta, Risorse, StatoAggiornamento, StatoVoce + ControlloSistema, Download, Famiglia, FiltroGalleria, GruppoLora, Impostazioni, InfoLora, InfoMotore, Lavoro, Opera, PassoSetup, Richiesta, Risorse, StatoAggiornamento, StatoVoce } from '@shared/tipi' type Via = () => void @@ -36,9 +36,6 @@ export interface FileModelli { klein: string[] kleinTe: string[] kleinVae: string[] - /** i file di FLUX.2 Klein 4B (modello, text encoder, VAE condiviso) */ - klein4b: string[] - klein4bTe: string[] } export interface ModelloTrovato { percorso: string @@ -84,8 +81,10 @@ export interface Api { lora: { elenco(): Promise; aggiorna(file: string, m: Partial): Promise; /** i LoRA buoni per quel modello (compatibili e «da provare»; mai quelli fatti per un altro) */ - compatibili(famiglia: Famiglia): Promise; importa(p: string[]): Promise - elimina(file: string): Promise; scarica(link: string): Promise; apriCartella(): Promise + compatibili(famiglia: Famiglia): Promise + /** `gruppo`: lo scaffale della scheda da cui si carica (Qwen o Flux); se i nomi dei tensori dicono un altro modello vince quello. Ritorna i nomi dei file (con la sottocartella). */ + importa(p: string[], gruppo?: GruppoLora): Promise + elimina(file: string): Promise; scarica(link: string, gruppo?: GruppoLora): Promise; apriCartella(): Promise } file: { scegliImmagini(multiple: boolean): Promise; scegli(nome: string, est: string[]): Promise diff --git a/src/renderer/src/azioni.ts b/src/renderer/src/azioni.ts index 2f093a6..28d40a9 100644 --- a/src/renderer/src/azioni.ts +++ b/src/renderer/src/azioni.ts @@ -1,6 +1,7 @@ // Le azioni che servono a più pagine: costruire le richieste, aprire una foto in Modifica, rifinire, variare. import { inQualitaPiena, modelloDaOpera, parametriModello, senzaTurbo, type IdModello } from '@shared/modelli' -import type { Opera, Richiesta } from '@shared/tipi' +import { vaBeneCon } from '@shared/loraBase' +import type { Famiglia, LoraAttiva, Opera, Richiesta } from '@shared/tipi' import { api } from './api' import { passiDi, usaStato, type FotoBase, type Qualita } from './stato' import { dimensioniRis } from './util' @@ -23,6 +24,15 @@ export const SISTEMA_MIGLIORA_MODIFICA = export const SISTEMA_DESCRIVI = 'Describe this image as a prompt for a text-to-image model: one English paragraph of 100-150 words covering subject, details, setting, composition, lighting, colors, style and mood. Output only the paragraph.' +/** i LoRA di una lista che vanno bene con quel modello: quelli dell'altro scaffale (Qwen con Klein o al contrario) restano fuori */ +export function loraDelModello(lista: LoraAttiva[] | undefined, famiglia: Famiglia): LoraAttiva[] { + const { lore } = usaStato.getState() + return (lista || []).filter((a) => { + const l = lore.find((x) => x.file === a.file) + return !!l && vaBeneCon(l, famiglia) !== 'no' + }) +} + export function richiestaBase(): Richiesta { const { imp, loraAttive } = usaStato.getState() const d = imp?.predefiniti || { passi: 40, cfg: 1, sampler: 'euler', scheduler: 'simple', megapixel: 1 } @@ -41,12 +51,19 @@ export function richiestaBase(): Richiesta { altezza: 1024, megapixel: d.megapixel, immagini: [], - // i LoRA sono fatti per Qwen: con Klein non valgono - lora: famiglia === 'qwen' ? loraAttive.filter((l) => l.forza !== 0) : [], + // la pila del modello scelto: Qwen e Flux hanno ognuno la sua (`loraPile`), quindi qui ci sono solo LoRA fatti per questo modello + lora: loraAttive.filter((l) => l.forza !== 0), quante: 1 } } +/** i LoRA con cui rifinire o variare un'opera: quelli suoi che vanno col modello scelto; se non ce n'è, la pila accesa adesso */ +const loraDellOpera = (o: Opera): LoraAttiva[] => { + const b = richiestaBase() + const suoi = loraDelModello(o.lora, b.famiglia ?? 'qwen') + return suoi.length ? suoi : b.lora +} + /** passi per una qualità. Il Turbo di Qwen non c'è più (D1 del piano): una qualità «turbo» salvata vale «alta» e non accende il LoRA */ export function perQualita(q: Qualita): Pick & Partial> { return { passi: passiDi(senzaTurbo(q)), turbo: false } @@ -96,7 +113,7 @@ export function rifinisci(o: Opera, fattore?: number, forza?: number): Promise { modalita: 'varia', prompt: o.prompt, negativo: o.negativo, - lora: o.lora.length ? o.lora : richiestaBase().lora, + lora: loraDellOpera(o), immagini: [o.file], forza, quante, @@ -168,8 +185,8 @@ export function rifaiCon(o: Opera, id: IdModello): Promise { immagini: [], trasparente: o.trasparente, ...parametriModello(id), - // i LoRA sono fatti per Qwen: con Klein Base non valgono - lora: stessaFamiglia ? [] : richiestaBase().lora, + // i LoRA dell'opera che vanno bene col modello nuovo (da Klein a Klein Base restano; da Klein a Qwen no) + lora: loraDelModello(o.lora, parametriModello(id).famiglia), origine: o.id, etichetta: stessaFamiglia ? 'Qualità piena' : 'Con Qwen' }, @@ -181,7 +198,7 @@ export function rifaiCon(o: Opera, id: IdModello): Promise { export function riusa(o: Opera): void { const s = usaStato.getState() s.setCrea({ prompt: o.prompt, seed: o.seed, casuale: false, trasparente: !!o.trasparente }) - if (o.lora?.length) s.setLoraAttive(o.lora.filter((l) => s.lore.some((x) => x.file === l.file))) + if (o.lora?.length) s.setLoraAttive(loraDelModello(o.lora, s.imp?.famiglia ?? 'qwen')) s.apriVisore(null) s.vai('crea') s.avvisa('Impostazioni rimesse in Crea (seed fisso)', 'ok') @@ -192,18 +209,22 @@ const FILE_LORA: Record, string> = { vestiti: 'qwen-image-2.1-outfit-swap.safetensors' } -/** il file del LoRA delle angolazioni (akhaliq, "Multiple Angles"), se è fra i LoRA */ +/** il file del LoRA delle angolazioni (akhaliq, "Multiple Angles"), se è fra i LoRA e va col modello scelto (è un LoRA di Qwen: con Klein no, e Camera va a parole) */ export function fileLoraCamera(): string | undefined { - return usaStato.getState().lore.find((l) => /multiple-angles/i.test(l.fonte || '') || /multiple.?angles|(^|[-_.])mva([-_.]|$)/i.test(l.file))?.file + const { lore, imp } = usaStato.getState() + return lore.find((l) => (/multiple-angles/i.test(l.fonte || '') || /multiple.?angles|(^|[-_.])mva([-_.]|$)/i.test(l.file)) && vaBeneCon(l, imp?.famiglia ?? 'qwen') !== 'no')?.file } -/** i LoRA che un'azione rapida accende da sola, se ci sono nella cartella (se no si va col modello da solo) */ +/** i LoRA che un'azione rapida accende da sola, se ci sono nella cartella e vanno col modello scelto (questi sono LoRA di Qwen: + * con Klein non partono e si va col modello da solo, come quando mancano) */ export function loraPer(chiavi: ChiaveLora[] | undefined): { file: string; forza: number }[] { - const { lore } = usaStato.getState() + const { lore, imp } = usaStato.getState() + const famiglia = imp?.famiglia ?? 'qwen' const out: { file: string; forza: number }[] = [] for (const k of chiavi || []) { const file = k === 'camera' ? fileLoraCamera() : FILE_LORA[k] - if (file && lore.some((l) => l.file === file) && !out.some((o) => o.file === file)) out.push({ file, forza: k === 'camera' ? 0.9 : 1 }) + const info = file ? lore.find((l) => l.file === file) : undefined + if (file && info && vaBeneCon(info, famiglia) !== 'no' && !out.some((o) => o.file === file)) out.push({ file, forza: k === 'camera' ? 0.9 : 1 }) } return out } diff --git a/src/renderer/src/box.ts b/src/renderer/src/box.ts index f56f0ec..d16ed67 100644 --- a/src/renderer/src/box.ts +++ b/src/renderer/src/box.ts @@ -4,7 +4,8 @@ import type { BoxRegione } from '@shared/tipi' export const COLORI_BOX = ['#8a5cff', '#3fe0ff', '#ff3df2', '#ffd54a', '#3ee08f', '#ff7a6b', '#4c8dff', '#b6f23f'] -export const MAX_BOX = 8 +/** quanti riquadri al massimo: lo stesso tetto del motore (`regioniPronte`, 16) */ +export const MAX_BOX = 16 export type Rect = BoxRegione['rect'] export const TIPI_BOX: { id: BoxRegione['tipo']; nome: string; titolo: string; guida: string }[] = [ @@ -36,6 +37,15 @@ export const DISPOSIZIONI: { id: string; nome: string; box: { rect: Rect; tipo: const limita = (n: number, a = 0, b = 1): number => Math.max(a, Math.min(b, n)) export const MINIMO = 0.04 +/** quale riquadro sta sotto un punto (0–1): fra quelli che lo contengono il più piccolo, a pari area il più davanti. + * Serve per scegliere con un clic un riquadro che ne copre un altro, o che sta dentro uno grande. */ +export function riquadroSotto(regioni: BoxRegione[], x: number, y: number): BoxRegione | null { + const area = (r: BoxRegione): number => (r.rect[2] - r.rect[0]) * (r.rect[3] - r.rect[1]) + const dentro = regioni.filter((r) => !r.nascosta && x >= r.rect[0] && x <= r.rect[2] && y >= r.rect[1] && y <= r.rect[3]) + dentro.sort((a, b) => area(a) - area(b) || b.ordine - a.ordine) + return dentro[0] ?? null +} + export const idBox = (): string => 'b' + Math.random().toString(36).slice(2, 8) /** un rettangolo da due punti (qualunque ordine), dentro la tela */ diff --git a/src/renderer/src/componenti/AvvisoLora.tsx b/src/renderer/src/componenti/AvvisoLora.tsx index 6fe8f6d..9c9258c 100644 --- a/src/renderer/src/componenti/AvvisoLora.tsx +++ b/src/renderer/src/componenti/AvvisoLora.tsx @@ -38,10 +38,12 @@ export function AvvisoLora(p: { id: string; perche: string }): JSX.Element | nul const [voce, setVoce] = useState(null) const dl = usaScarichi((s) => s.dl) usaStato((s) => s.lore) + // sono LoRA di Qwen: con Klein non si accendono e non servono + const qwen = usaStato((s) => (s.imp?.famiglia || 'qwen') === 'qwen') useEffect(() => { void api.modelli.stato().then((l) => setVoce(l.find((x) => x.id === p.id) || null)).catch(() => undefined) }, [p.id, dl[voce?.nome ?? '']?.stato]) - if (!voce || voce.presente) return null + if (!qwen || !voce || voce.presente) return null const d = dl[voce.nome] const va = d?.stato === 'in corso' || d?.stato === 'verifica' return ( diff --git a/src/renderer/src/componenti/ScegliModello.tsx b/src/renderer/src/componenti/ScegliModello.tsx index dc03f49..857d342 100644 --- a/src/renderer/src/componenti/ScegliModello.tsx +++ b/src/renderer/src/componenti/ScegliModello.tsx @@ -1,4 +1,4 @@ -// Il selettore del modello che disegna, nella barra in alto: tre pillole (Qwen base, Klein veloce, Klein Base) da ORDINE_MODELLI. +// Il selettore del modello che disegna, nella barra in alto dell'app (si sceglie solo da lì, da qualsiasi pagina): Qwen base, Klein veloce e Klein Base da ORDINE_MODELLI. // Salva `modelloScelto` (la famiglia si allinea da sola nel main). Se i file di Klein non ci sono, la pillola ha un puntino // e un clic apre il pannellino che dice quanto pesano e li scarica. import { useEffect, useRef, useState, type JSX } from 'react' @@ -7,19 +7,18 @@ import { MODELLI, ORDINE_MODELLI, modelloDaImpostazioni, type IdModello } from ' import { api } from '../api' import { ricaricaImp, usaStato } from '../stato' import { I } from './Icone' -import { Tessera, Tessere } from './Tessere' /** le voci del catalogo da avere per usare un modello (il modello INT8 sta nelle schede da 8 GB) */ -const SERVONO: Record, string[]> = { klein: ['klein-int8', 'klein-te', 'klein-vae'], 'klein-base': ['klein-base-int8', 'klein-te', 'klein-vae'], klein4b: ['klein4b-fp8', 'klein4b-te', 'klein-vae'] } -const PESO: Record, string> = { klein: 'circa 10 GB', 'klein-base': 'circa 10 GB (4,7 se hai già Klein)', klein4b: 'circa 12,5 GB' } -const ETICHETTA: Record = { 'qwen-base': 'Qwen base', klein: 'Klein veloce', 'klein-base': 'Klein Base', klein4b: 'Klein 4B' } +const SERVONO: Record, string[]> = { klein: ['klein-int8', 'klein-te', 'klein-vae'], 'klein-base': ['klein-base-int8', 'klein-te', 'klein-vae'] } +const PESO: Record, string> = { klein: 'circa 10 GB', 'klein-base': 'circa 10 GB (4,7 se hai già Klein)' } +const ETICHETTA: Record = { 'qwen-base': 'Qwen', klein: 'Klein 3', 'klein-base': 'Klein 3 Base' } -export function SelettoreModello({ variante = 'pillole' }: { variante?: 'pillole' | 'tessere' }): JSX.Element { +export function SelettoreModello(): JSX.Element { const imp = usaStato((s) => s.imp) const avvisa = usaStato((s) => s.avvisa) const inCorso = usaStato((s) => s.lavori.some((l) => l.stato === 'in coda' || l.stato === 'in corso')) const scelto = modelloDaImpostazioni(imp) - const [pronti, setPronti] = useState>({ 'qwen-base': true, klein: false, 'klein-base': false, klein4b: false }) + const [pronti, setPronti] = useState>({ 'qwen-base': true, klein: false, 'klein-base': false }) const [aperto, setAperto] = useState(null) const [scarico, setScarico] = useState<{ id: IdModello; passo: string; perc: number } | null>(null) const radice = useRef(null) @@ -31,8 +30,7 @@ export function SelettoreModello({ variante = 'pillole' }: { variante?: 'pillole setPronti({ 'qwen-base': true, klein: comuni && !!f.klein?.some((n) => !/base/i.test(n)), - 'klein-base': comuni && !!f.klein?.some((n) => /base/i.test(n)), - klein4b: (f.klein4b?.length ?? 0) > 0 && (f.klein4bTe?.length ?? 0) > 0 && (f.kleinVae?.length ?? 0) > 0 + 'klein-base': comuni && !!f.klein?.some((n) => /base/i.test(n)) }) } useEffect(() => { @@ -70,7 +68,7 @@ export function SelettoreModello({ variante = 'pillole' }: { variante?: 'pillole const scarica = async (id: Exclude): Promise => { try { const stato = await api.modelli.stato() - const nomi: Record = { 'klein-int8': 'il modello Klein', 'klein-base-int8': 'il modello Klein Base', 'klein4b-fp8': 'il modello Klein 4B', 'klein4b-te': 'il text encoder (Qwen3 4B)', 'klein-te': 'il text encoder', 'klein-vae': 'il VAE' } + const nomi: Record = { 'klein-int8': 'il modello Klein', 'klein-base-int8': 'il modello Klein Base', 'klein-te': 'il text encoder', 'klein-vae': 'il VAE' } for (const voce of SERVONO[id]) { if (stato.find((v) => v.id === voce)?.presente) continue setScarico({ id, passo: nomi[voce], perc: 0 }) @@ -89,34 +87,17 @@ export function SelettoreModello({ variante = 'pillole' }: { variante?: 'pillole } return ( -
- {variante === 'tessere' ? ( - - {ORDINE_MODELLI.map((id) => { - const m = MODELLI[id] - return ( - : id === 'klein' ? : } - nome={ETICHETTA[id]} - su={id === scelto} - daScaricare={!pronti[id]} - onClick={() => void scegli(id)} - titolo={`${m.nome}: ${m.sotto}${!pronti[id] ? ' (da scaricare)' : ''}${inCorso ? ' · vale dal prossimo lavoro' : ''}`} - /> - ) - })} - - ) : ( +
{ORDINE_MODELLI.map((id) => { const m = MODELLI[id] return (
- )} {aperto && aperto !== 'qwen-base' && !pronti[aperto] && (
{MODELLI[aperto].nome} @@ -142,8 +122,3 @@ export function SelettoreModello({ variante = 'pillole' }: { variante?: 'pillole
) } - -/** Lo stesso selettore di Crea, anche nel pannello di Modifica. */ -export function ScegliModello(): JSX.Element { - return <>

Motore

-} diff --git a/src/renderer/src/componenti/SceltaLora.tsx b/src/renderer/src/componenti/SceltaLora.tsx index fe524c3..63ff32c 100644 --- a/src/renderer/src/componenti/SceltaLora.tsx +++ b/src/renderer/src/componenti/SceltaLora.tsx @@ -5,6 +5,7 @@ import { urlFile } from '../api' import { I } from './Icone' import { ImgAsset } from './Tessere' import { chiaveLora } from '../anteprime' +import { NOME_GRUPPO, gruppoDiFamiglia, vaBeneCon } from '@shared/loraBase' import type { InfoLora } from '@shared/tipi' export function coloreDa(nome: string): string { @@ -35,8 +36,10 @@ export function Copertina({ l, grande }: { l: InfoLora; grande?: boolean }): JSX export function SceltaLora(): JSX.Element { const { lore: tutte, loraAttive, setLoraAttive, toggleLora, vaiOpzioni, imp } = usaStato() - // il Turbo si accende dalla Qualità, non da qui - const lore = tutte.filter((l) => l.file !== imp?.loraTurbo) + // il Turbo si accende dalla Qualità, non da qui; e si vedono solo i LoRA che vanno col modello scelto (Qwen o Flux: due scaffali) + const famiglia = imp?.famiglia || 'qwen' + const gruppo = gruppoDiFamiglia(famiglia) + const lore = tutte.filter((l) => l.file !== imp?.loraTurbo && vaBeneCon(l, famiglia) !== 'no') const [aperto, setAperto] = useState(false) const rif = useRef(null) useEffect(() => { @@ -79,13 +82,13 @@ export function SceltaLora(): JSX.Element { - {lore.length === 0 && Nessun LoRA: aggiungili da Opzioni → LoRA.} + {lore.length === 0 && Nessun LoRA per {NOME_GRUPPO[gruppo]}: aggiungili dalla pagina LoRA.}
{aperto && (
{lore.length === 0 && (
- Non hai ancora LoRA. + Non hai ancora LoRA per {NOME_GRUPPO[gruppo]}.
)} @@ -99,7 +102,7 @@ export function SceltaLora(): JSX.Element {
{l.nome}
- {l.compatibile === 'no' ? '⚠ ' + (l.base || 'altro modello') : l.parole || l.file} + {vaBeneCon(l, famiglia) === '?' ? '? da verificare' : l.parole || l.file}
{su && ✓} @@ -115,7 +118,6 @@ export function SceltaLora(): JSX.Element { /** aggiunge al prompt le parole chiave dei LoRA attivi che non ci sono già */ export function conParoleLora(prompt: string): string { const { lore, loraAttive, imp } = usaStato.getState() - if (imp && imp.famiglia !== 'qwen') return prompt const extra: string[] = [] for (const a of loraAttive) { const p = lore.find((l) => l.file === a.file)?.parole?.trim() diff --git a/src/renderer/src/orbita.css b/src/renderer/src/orbita.css index f4a3eba..ee0d77c 100644 --- a/src/renderer/src/orbita.css +++ b/src/renderer/src/orbita.css @@ -197,7 +197,15 @@ .box-regione { position: absolute; border: 2px solid var(--c); background: color-mix(in srgb, var(--c) 16%, transparent); border-radius: 4px; cursor: move; box-sizing: border-box; animation: nasce 180ms var(--molla); } .box-regione.sel { border-style: dashed; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 0 22px color-mix(in srgb, var(--c) 55%, transparent); background: color-mix(in srgb, var(--c) 24%, transparent); } .box-regione.nascosta { opacity: 0.25; } -.box-regione .numero { position: absolute; left: -2px; top: -2px; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 4px 0 8px 0; background: var(--c); color: #0a0716; font-weight: 800; font-size: 13px; display: grid; place-items: center; pointer-events: none; } +.box-regione .numero { position: absolute; left: -2px; top: -2px; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 4px 0 8px 0; background: var(--c); color: #0a0716; font-weight: 800; font-size: 13px; display: grid; place-items: center; cursor: move; z-index: 2; } +/* strumento «Disegna»: il corpo dei riquadri lascia passare il mouse, così si disegna anche dentro uno grande; restano afferrabili il numero e le maniglie */ +.box-cornice.disegna .box-regione { pointer-events: none; cursor: crosshair; } +.box-cornice.disegna .box-regione .numero, .box-cornice.disegna .box-regione .maniglia { pointer-events: auto; } +.box-strumenti { display: inline-flex; gap: 2px; padding: 2px; border-radius: 12px; background: var(--vetro); border: 1px solid var(--bordo-vetro); } +.box-strumenti button { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 12px; border: 0; border-radius: 10px; background: transparent; color: var(--tenue); font-size: 13px; font-weight: 700; cursor: pointer; } +.box-strumenti button svg { width: 16px; height: 16px; } +.box-strumenti button:hover { color: var(--testo); background: var(--pannello2); } +.box-strumenti button.su { color: #fff; background: linear-gradient(var(--pannello2), var(--pannello2)) padding-box, var(--aurora) border-box; border: 1px solid transparent; } .box-regione .testo-box { position: absolute; left: 4px; right: 4px; top: 26px; bottom: 4px; overflow: hidden; color: #fff; font-size: 13px; font-weight: 600; line-height: 1.2; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9); pointer-events: none; } .box-regione .maniglia { position: absolute; width: 11px; height: 11px; background: #fff; border: 2px solid var(--c); border-radius: 3px; } .box-regione .maniglia.nw { left: -6px; top: -6px; cursor: nwse-resize; } .box-regione .maniglia.ne { right: -6px; top: -6px; cursor: nesw-resize; } diff --git a/src/renderer/src/pagine/Box.tsx b/src/renderer/src/pagine/Box.tsx index 5aa7778..63442bb 100644 --- a/src/renderer/src/pagine/Box.tsx +++ b/src/renderer/src/pagine/Box.tsx @@ -9,7 +9,6 @@ import { Tessera, Tessere, E } from '../componenti/Tessere' import { AdattaFoto } from '../componenti/AdattaFoto' import { Casella } from '../componenti/Casella' import { SezioneChiudibile } from '../componenti/Controlli' -import { SelettoreModello } from '../componenti/ScegliModello' import { HudCrea, SezFormato, SezMisura, SezQuante, SezSfondo } from '../componenti/SezioniCrea' import { IntensitaStile, ScaffaliStili } from '../componenti/ScaffaliStili' import { Hud } from '../componenti/Hud' @@ -23,7 +22,7 @@ import { misuraCrea } from '../misura' import { promptCrea, sfondoRichiesto } from '../creaPrompt' import { stileDa } from '../stili' import { richiestaBase } from '../azioni' -import { DISPOSIZIONI, MAX_BOX, TIPI_BOX, coloreBox, fraseBox, nuovaRegione, promptDaBox, rettDaPunti, trasforma, type Maniglia, type Rect } from '../box' +import { DISPOSIZIONI, MAX_BOX, TIPI_BOX, coloreBox, fraseBox, nuovaRegione, promptDaBox, rettDaPunti, riquadroSotto, trasforma, type Maniglia, type Rect } from '../box' import type { BoxRegione, Opera } from '@shared/tipi' const MANIGLIE: Maniglia[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'] @@ -41,6 +40,9 @@ export function Box(): JSX.Element { const [scelto, setScelto] = useState(null) const [vedoInCorso, setVedoInCorso] = useState(true) const [vistaRis, setVistaRis] = useState(false) + // «disegna»: si trascina ovunque per fare un riquadro nuovo, anche dentro uno grande (si sposta dal numero o con le maniglie); + // «sposta»: si afferra un riquadro dal corpo e lo si muove. Su un punto vuoto della tela si disegna in tutti e due. + const [strumento, setStrumento] = useState<'disegna' | 'sposta'>('disegna') const mis = misuraCrea(crea) const { finale, lav } = mis @@ -101,10 +103,6 @@ export function Box(): JSX.Element { } const giuTela = (e: PE): void => { if (e.button !== 0 || vistaRis) return - if (usaStato.getState().box.regioni.length >= MAX_BOX) { - avvisa(`Al massimo ${MAX_BOX} riquadri`, 'errore') - return - } const p = punto(e) gesto.current = { tipo: 'disegna', ...p } setBozza([p.x, p.y, p.x, p.y]) @@ -139,9 +137,16 @@ export function Box(): JSX.Element { const p = punto(e) const r = rettDaPunti(g.x, g.y, p.x, p.y) setBozza(null) - // un clic senza trascinare non fa un riquadro - if (r[2] - r[0] < 0.03 || r[3] - r[1] < 0.03) return + // un clic senza trascinare non fa un riquadro: sceglie quello che c'è sotto (il più piccolo), o toglie la scelta + if (r[2] - r[0] < 0.03 || r[3] - r[1] < 0.03) { + seleziona(riquadroSotto(usaStato.getState().box.regioni, p.x, p.y)?.id ?? null) + return + } const s = usaStato.getState().box + if (s.regioni.length >= MAX_BOX) { + avvisa(`Al massimo ${MAX_BOX} riquadri`, 'errore') + return + } const n = nuovaRegione(r, s.regioni, r[2] - r[0] > 0.9 && r[3] - r[1] > 0.9 ? 'sfondo' : 'oggetto') setBox({ regioni: [...s.regioni, n], selezionata: n.id }) } @@ -245,9 +250,9 @@ export function Box(): JSX.Element { ) : ( -
+
{regioni.length === 0 && !bozza && ( -
DISEGNA UN RIQUADROTrascina sulla tela e di' cosa ci va dentro. Oppure scegli una disposizione a sinistra.
+
DISEGNA UN RIQUADROTrascina sulla tela e di' cosa ci va dentro (anche dentro un riquadro già fatto). Oppure scegli una disposizione a sinistra.
)} {regioni.map((r, i) => (
giuRegione(e, r, 'corpo')} data-box={r.id} > - {i + 1} + giuRegione(e, r, 'corpo')}>{i + 1}
{r.prompt || (r.rif ? 'foto di riferimento' : '')}
{r.id === selezionata && MANIGLIE.map((m) => giuRegione(e, r, m)} />)}
@@ -294,8 +299,7 @@ export function Box(): JSX.Element {

Motore {modello.nome}

- - {modello.id === 'qwen-base' &&
Con Qwen i riquadri guidano la composizione a parole; con Klein 3 il motore li userà come regioni vere.
} +
Il modello si sceglie nella barra in alto. {modello.id === 'qwen-base' ? 'Con Qwen i riquadri guidano la composizione a parole; con Klein 3 il motore li usa come regioni vere.' : 'Con Klein 3 il motore usa i riquadri come regioni vere.'}
@@ -316,6 +320,11 @@ export function Box(): JSX.Element {

Riquadri {regioni.length} / {MAX_BOX}

+
+ + +
+
{strumento === 'disegna' ? 'Trascina sulla tela per un riquadro nuovo, anche dentro un altro. Sposta un riquadro dal suo numero.' : 'Afferra un riquadro dal corpo per spostarlo. Sui punti vuoti della tela si disegna.'}
{regioni.length === 0 &&
Nessun riquadro: trascina sulla tela per disegnarne uno.
}
{regioni.map((r, i) => { diff --git a/src/renderer/src/pagine/Crea.tsx b/src/renderer/src/pagine/Crea.tsx index 9738355..bb07e16 100644 --- a/src/renderer/src/pagine/Crea.tsx +++ b/src/renderer/src/pagine/Crea.tsx @@ -11,7 +11,6 @@ import { SezioneChiudibile } from '../componenti/Controlli' import { conParoleLora } from '../componenti/SceltaLora' import { percentuale } from '../componenti/Coda' import { VistaZoom } from '../componenti/VistaZoom' -import { SelettoreModello } from '../componenti/ScegliModello' import { AtmosferaGenere, CampiGenere, GrigliaGeneri } from '../componenti/Generi' import { IntensitaStile, ScaffaliStili } from '../componenti/ScaffaliStili' import { HudCrea, SezFormato, SezMisura, SezQuante, SezSfondo } from '../componenti/SezioniCrea' @@ -50,7 +49,7 @@ export function Crea(): JSX.Element { // la misura: la stessa funzione che costruisce la richiesta e che disegna il HUD («la cifra che vedi è quella che esce») const mis = misuraCrea(crea) const { finale, lav } = mis - // il modello si sceglie dalle tessere «Motore» e porta con sé i passi (e il CFG) + // il modello si sceglie dalla barra in alto e porta con sé i passi (e il CFG) const modello = MODELLI[modelloDaImpostazioni(imp)] const passi = modello.passi const quante = Math.max(1, Math.min(8, crea.quante)) @@ -260,11 +259,6 @@ export function Crea(): JSX.Element {
-
-

Motore {modello.nome}

- -
-

Cosa {preset ? preset.nome : 'libero'}

@@ -350,11 +344,9 @@ export function Crea(): JSX.Element { )}
- {qwen && ( - - - - )} + + +
@@ -364,7 +356,7 @@ export function Crea(): JSX.Element { {stileAttivo && } {crea.sfondo !== 'scena' && } {crea.riferimenti.length > 0 && } - {qwen && nLora > 0 && } + {nLora > 0 && }
diff --git a/src/renderer/src/pagine/Impostazioni.tsx b/src/renderer/src/pagine/Impostazioni.tsx index 5a59cf3..53d2135 100644 --- a/src/renderer/src/pagine/Impostazioni.tsx +++ b/src/renderer/src/pagine/Impostazioni.tsx @@ -24,7 +24,7 @@ export function Impostazioni(): JSX.Element { function OpzioniGenerali(): JSX.Element { const { imp, motore, avvisa, agg } = usaStato() - const [file, setFile] = useState({ diffusione: [], encoder: [], vae: [], upscaler: [], klein: [], kleinTe: [], kleinVae: [], klein4b: [], klein4bTe: [] }) + const [file, setFile] = useState({ diffusione: [], encoder: [], vae: [], upscaler: [], klein: [], kleinTe: [], kleinVae: [] }) const [catalogo, setCatalogo] = useState([]) const [download, setDownload] = useState>({}) const [log, setLog] = useState([]) diff --git a/src/renderer/src/pagine/Lora.tsx b/src/renderer/src/pagine/Lora.tsx index e90b95c..4688ffd 100644 --- a/src/renderer/src/pagine/Lora.tsx +++ b/src/renderer/src/pagine/Lora.tsx @@ -1,16 +1,24 @@ -// LoRA: trascinali qui o incolla un link (Hugging Face, Civitai); un clic sulla carta lo accende. +// LoRA: due scaffali, uno per Qwen e uno per Flux (FLUX 3 Klein), ognuno con la sua pila di LoRA accesi. Trascina i file o incolla un link +// (Hugging Face, Civitai) dentro la scheda giusta; un clic sulla carta lo accende. I LoRA che l'app non riconosce stanno in «Altri». import { useEffect, useState, type JSX } from 'react' import { usaStato, ricaricaLore } from '../stato' import { api } from '../api' import { I } from '../componenti/Icone' import { Copertina } from '../componenti/SceltaLora' import { byte } from '../util' -import type { Download, InfoLora, StatoVoce } from '@shared/tipi' +import type { Download, Famiglia, GruppoLora, InfoLora, StatoVoce } from '@shared/tipi' +import { GRUPPI_LORA, NOME_GRUPPO, gruppoDa, gruppoDiFamiglia } from '@shared/loraBase' import { compatibilitaLora, forzaLora, spostaLora } from '../pilaLora' +type Scheda = GruppoLora | 'altri' +/** il modello di riferimento di uno scaffale: per Flux è Klein (Klein veloce e Klein Base hanno gli stessi LoRA) */ +const FAMIGLIA_DI: Record = { qwen: 'qwen', flux: 'klein' } +const CHI: Record = { qwen: 'Qwen-Image 2.1', flux: 'FLUX 3 Klein' } + export function Lora(): JSX.Element { - const { lore, loraAttive, toggleLora, setLoraAttive, avvisa, imp } = usaStato() - const famiglia = imp?.famiglia || 'qwen' + const { lore, loraPile, setLoraPila, toggleLoraPer, avvisa, imp } = usaStato() + const gruppoModello = gruppoDiFamiglia(imp?.famiglia) + const [scheda, setScheda] = useState(gruppoModello) const [link, setLink] = useState('') const [sopra, setSopra] = useState(false) const [download, setDownload] = useState>({}) @@ -23,20 +31,46 @@ export function Lora(): JSX.Element { return api.su.download((d) => setDownload((x) => ({ ...x, [d.id]: d }))) }, []) + const gruppo: GruppoLora | undefined = scheda === 'altri' ? undefined : scheda + const famiglia = gruppo ? FAMIGLIA_DI[gruppo] : (imp?.famiglia || 'qwen') + const pila = gruppo ? loraPile[gruppo] : [] + const conteggio = (g: GruppoLora): number => lore.filter((l) => gruppoDa(l) === g).length + const altri = lore.filter((l) => gruppoDa(l) === undefined) + const importa = async (ps: string[]): Promise => { - const f = await api.lora.importa(ps) + const f = await api.lora.importa(ps, gruppo) await ricaricaLore() - avvisa(f.length ? `${f.length} LoRA aggiunti` : 'Servono file .safetensors', f.length ? 'ok' : 'errore') + if (!f.length) { + avvisa('Servono file .safetensors', 'errore') + return + } + avvisaDove(f) + } + + /** dice dove sono finiti i file: se i nomi dei tensori dicono un altro modello, il LoRA sta nello scaffale giusto, non in quello aperto */ + const avvisaDove = (files: string[]): void => { + const fresche = usaStato.getState().lore.filter((l) => files.includes(l.file)) + const altrove = fresche.filter((l) => gruppoDa(l) !== gruppo) + if (!altrove.length) { + avvisa(`${files.length} LoRA aggiunti`, 'ok') + return + } + const l = altrove[0] + const g = gruppoDa(l) + avvisa(g ? `«${l.nome}» è un LoRA per ${NOME_GRUPPO[g]}: l'ho messo nella scheda ${NOME_GRUPPO[g]}` : `«${l.nome}» non è fatto per Qwen né per Flux (${l.base || 'altro modello'}): sta in «Altri»`, g ? 'ok' : 'errore') + if (g) setScheda(g) } const scarica = async (): Promise => { if (!link.trim()) return const l = link.trim() setLink('') - const d = await api.lora.scarica(l) + const d = await api.lora.scarica(l, gruppo) await ricaricaLore() - if (d.stato === 'fatto') avvisa(`Scaricato ${d.nome}`, 'ok') - else if (d.stato === 'errore') avvisa(d.errore || 'Download non riuscito', 'errore') + if (d.stato === 'fatto') { + if (d.nome) avvisaDove([d.nome]) + else avvisa(`Scaricato ${d.nome}`, 'ok') + } else if (d.stato === 'errore') avvisa(d.errore || 'Download non riuscito', 'errore') } const scaricaConsigliato = async (id: string): Promise => { @@ -47,7 +81,8 @@ export function Lora(): JSX.Element { } const inCorso = Object.values(download).filter((d) => d.stato === 'in corso' || d.stato === 'verifica') - const visibili = lore.filter((l) => !filtro || (l.nome + l.file + l.parole).toLowerCase().includes(filtro.toLowerCase())) + const visibili = (gruppo ? lore.filter((l) => gruppoDa(l) === gruppo) : altri).filter((l) => !filtro || (l.nome + l.file + l.parole).toLowerCase().includes(filtro.toLowerCase())) + const accesi = gruppo ? loraPile[gruppo].length : 0 return (
@@ -65,19 +100,39 @@ export function Lora(): JSX.Element { void importa(Array.from(e.dataTransfer.files).map((f) => api.file.percorso(f))) }} > -
-
-
- Trascina qui i LoRA (.safetensors) o incolla un link -
- setLink(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && scarica()} /> - - - +
+ {GRUPPI_LORA.map((g) => ( + + ))} + {altri.length > 0 && ( + + )} +
+ + {gruppo ? ( +
+
+
+ Trascina qui i LoRA per {NOME_GRUPPO[gruppo]} (.safetensors) o incolla un link +
+ setLink(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && scarica()} /> + + + +
+ Qui vanno i LoRA fatti per {CHI[gruppo]}{gruppo === 'flux' ? ' (valgono per Klein veloce e per Klein Base)' : ''}. Qwen e Flux non sono intercambiabili: se carichi un LoRA dell'altro modello lo metto nella sua scheda. Per Civitai a volte serve il token (Impostazioni).
- La compatibilità dipende dal modello: Qwen, Klein e gli altri Flux non sono intercambiabili. Per Civitai a volte serve il token (Impostazioni). La pila non converte un LoRA incompatibile.
-
+ ) : ( +
+ LoRA che l'app non riconosce + Dai nomi dei tensori non si capisce per quale modello siano, o sono fatti per un altro (Stable Diffusion, SDXL, FLUX.1…). Se sai per quale modello sono, scegli lo scaffale: da lì in poi si accendono con quel modello. Se non vanno, il motore si ferma con un errore. +
+ )} {inCorso.map((d) => (
@@ -86,30 +141,35 @@ export function Lora(): JSX.Element {
))} -
-

Pila LoRA

{famiglia} · {loraAttive.length} slot
-

Dall'alto verso il basso: questo è l'ordine di applicazione. Forza 0 lascia lo slot spento senza cancellarlo.

- {!loraAttive.length && Accendi un LoRA dalla libreria per aggiungerlo alla pila.} - {loraAttive.map((a, i) => { - const l = lore.find((x) => x.file === a.file) - const compatibile = l ? compatibilitaLora(l, famiglia) : '?' - const nome = l?.nome || a.file - return
-
{i + 1}. {nome}{compatibile === 'si' ? 'Compatibile' : compatibile === 'no' ? 'Non compatibile' : 'Da verificare'}
-
- - - setLoraAttive(loraAttive.map((x) => x.file === a.file ? { ...x, forza: forzaLora(Number(e.target.value)) } : x))} /> - {a.forza.toFixed(2)} - + {gruppo && ( +
+

Pila LoRA · {NOME_GRUPPO[gruppo]}

{pila.length} slot
+

+ Dall'alto verso il basso: questo è l'ordine di applicazione. Forza 0 lascia lo slot spento senza cancellarlo.{' '} + {gruppo === gruppoModello ? `Parte con il modello scelto adesso (${NOME_GRUPPO[gruppo]}).` : `Parte quando scegli ${NOME_GRUPPO[gruppo]} fra i modelli della barra in alto (adesso è ${NOME_GRUPPO[gruppoModello]}).`} +

+ {!pila.length && Accendi un LoRA dalla libreria per aggiungerlo alla pila.} + {pila.map((a, i) => { + const l = lore.find((x) => x.file === a.file) + const compatibile = l ? compatibilitaLora(l, famiglia) : '?' + const nome = l?.nome || a.file + return
+
{i + 1}. {nome}{compatibile === 'si' ? 'Compatibile' : compatibile === 'no' ? 'Non compatibile' : 'Da verificare'}
+
+ + + setLoraPila(gruppo, pila.map((x) => x.file === a.file ? { ...x, forza: forzaLora(Number(e.target.value)) } : x))} /> + {a.forza.toFixed(2)} + +
+ {l?.parole && Parole chiave: {l.parole}} + {compatibile !== 'si' && {compatibile === 'no' ? `Spegni questo slot: non è fatto per ${CHI[gruppo]}.` : 'Architettura non riconosciuta: controlla la scheda del LoRA prima di usarlo.'}}
- {l?.parole && Parole chiave: {l.parole}} - {compatibile !== 'si' && {compatibile === 'no' ? 'Spegni questo slot prima di generare con il modello selezionato.' : 'Architettura non riconosciuta: controlla la scheda del LoRA prima di usarlo.'}} -
- })} -
+ })} + + )} - {consigliati.some((c) => !c.presente) && ( + {gruppo === 'qwen' && consigliati.some((c) => !c.presente) && (

CONSIGLIATI PER QWEN-IMAGE 2.1

@@ -127,22 +187,36 @@ export function Lora(): JSX.Element { )}
-

I TUOI LORA · {lore.length} · {loraAttive.length} {loraAttive.length === 1 ? 'ACCESO' : 'ACCESI'}

+

{gruppo ? `LORA PER ${NOME_GRUPPO[gruppo].toUpperCase()}` : 'ALTRI LORA'} · {visibili.length} · {accesi} {accesi === 1 ? 'ACCESO' : 'ACCESI'}

- {loraAttive.length > 0 && } + {gruppo && pila.length > 0 && } setFiltro(e.target.value)} style={{ width: 220 }} />
- {lore.length === 0 &&
Ancora nessun LoRA. Un LoRA aggiunge uno stile, un personaggio o un dettaglio al modello: trascinane uno qui sopra.
} + {visibili.length === 0 && gruppo && ( +
+ {gruppo === 'flux' + ? 'Ancora nessun LoRA per Flux. Carica un LoRA fatto per FLUX 3 Klein: trascinalo qui sopra o incolla il link.' + : 'Ancora nessun LoRA per Qwen. Un LoRA aggiunge uno stile, un personaggio o un dettaglio al modello: trascinane uno qui sopra.'} +
+ )}
{visibili.map((l) => ( - a.file === l.file)} toggle={() => { - if (!loraAttive.some((a) => a.file === l.file) && compatibilitaLora(l, famiglia) === 'no') { - avvisa('Questo LoRA non è compatibile con il modello selezionato.', 'errore') - return - } - toggleLora(l.file, forzaLora(l.forza)) - }} /> + a.file === l.file)} + toggle={() => { + if (!gruppo) return + if (!loraPile[gruppo].some((a) => a.file === l.file) && compatibilitaLora(l, famiglia) === 'no') { + avvisa(`Questo LoRA non è compatibile con ${NOME_GRUPPO[gruppo]}.`, 'errore') + return + } + toggleLoraPer(gruppo, l.file, forzaLora(l.forza)) + }} + /> ))}
@@ -150,9 +224,9 @@ export function Lora(): JSX.Element { ) } -function CartaLora({ l, attiva, toggle }: { l: InfoLora; attiva: boolean; toggle: () => void }): JSX.Element { - const { avvisa, loraAttive, setLoraAttive, imp } = usaStato() - const compatibile = compatibilitaLora(l, imp?.famiglia || 'qwen') +function CartaLora({ l, attiva, toggle, gruppo, famiglia }: { l: InfoLora; attiva: boolean; toggle: () => void; gruppo?: GruppoLora; famiglia: Famiglia }): JSX.Element { + const { avvisa, loraPile, setLoraPila } = usaStato() + const compatibile = compatibilitaLora(l, famiglia) const [parole, setParole] = useState(l.parole) const [forza, setForza] = useState(l.forza) useEffect(() => { @@ -172,21 +246,33 @@ function CartaLora({ l, attiva, toggle }: { l: InfoLora; attiva: boolean; toggle await salva({ anteprima: dentro }) } + /** dice a quale scaffale appartiene un LoRA che l'app non riconosce (da lì in poi si accende con quel modello) */ + const assegna = async (g: GruppoLora): Promise => { + await salva({ gruppo: g }) + avvisa(`«${l.nome}» ora sta nello scaffale ${NOME_GRUPPO[g]}`, 'ok') + } + return (
-
+
- {attiva ? ✓ Acceso : Accendi} + {attiva ? ✓ Acceso : gruppo ? Accendi : null}
{l.nome}
- + {compatibile === 'si' ? '✓ Compatibile' : compatibile === 'no' ? '⚠ ' + (l.base?.split(' (')[0] || 'altro modello') : '? Da verificare'}
+ {!gruppo && ( +
+ Usalo con + {GRUPPI_LORA.map((g) => )} +
+ )} { const v = forzaLora(Number(e.target.value)) setForza(v) - if (attiva) setLoraAttive(loraAttive.map((a) => (a.file === l.file ? { ...a, forza: v } : a))) + if (attiva && gruppo) setLoraPila(gruppo, loraPile[gruppo].map((a) => (a.file === l.file ? { ...a, forza: v } : a))) }} onPointerUp={() => salva({ forza })} onBlur={() => salva({ forza })} @@ -237,4 +323,3 @@ function CartaLora({ l, attiva, toggle }: { l: InfoLora; attiva: boolean; toggle
) } - diff --git a/src/renderer/src/pagine/Studio.tsx b/src/renderer/src/pagine/Studio.tsx index 0c011d0..6ccc80e 100644 --- a/src/renderer/src/pagine/Studio.tsx +++ b/src/renderer/src/pagine/Studio.tsx @@ -3,7 +3,6 @@ // il prompt e i riquadri non si perdono passando da un modo all'altro. import type { JSX } from 'react' import { usaStato, type Pagina } from '../stato' -import { MODELLI, modelloDaImpostazioni } from '@shared/modelli' import { I } from '../componenti/Icone' import { ImgAsset } from '../componenti/Tessere' import { chiaveModo } from '../anteprime' @@ -42,11 +41,9 @@ const MODI: { id: ModoStudio; nome: string; icona: () => JSX.Element; titolo: st export function StripModi(): JSX.Element { const pagina = usaStato((s) => s.pagina) const modo = usaStato((s) => s.modifica.modo) - const imp = usaStato((s) => s.imp) const nLora = usaStato((s) => s.loraAttive.length) const vai = usaStato((s) => s.vai) const attivo: ModoStudio = pagina === 'modifica' ? (modo === 'ricette' ? 'ricette' : 'modifica') : pagina === 'box' ? 'box' : 'crea' - const m = MODELLI[modelloDaImpostazioni(imp)] return (
{MODI.map((x) => ( @@ -55,14 +52,9 @@ export function StripModi(): JSX.Element { ))} - - Motore {m.nome} - - {( - - )} +
) } diff --git a/src/renderer/src/pagine/modifica/Schede.tsx b/src/renderer/src/pagine/modifica/Schede.tsx index 6b4c0d6..f3850a5 100644 --- a/src/renderer/src/pagine/modifica/Schede.tsx +++ b/src/renderer/src/pagine/modifica/Schede.tsx @@ -5,7 +5,6 @@ import { usaStato, type FotoBase } from '../../stato' import { urlFile } from '../../api' import { I } from '../../componenti/Icone' import { Casella } from '../../componenti/Casella' -import { ScegliModello } from '../../componenti/ScegliModello' import { Contatore, Segmenti } from '../../componenti/Controlli' import { SceltaLora } from '../../componenti/SceltaLora' import { Riferimenti } from '../../componenti/Riferimenti' @@ -26,7 +25,7 @@ interface Props extends PropsScrivi { } export function SchedeModifica(p: Props): JSX.Element { - const { modifica: m, setModifica, imp } = usaStato() + const { modifica: m, setModifica } = usaStato() const sp = usaSpeciale() const fusion = sp === 'unisci' const esempioMode = sp === 'esempio' @@ -35,7 +34,6 @@ export function SchedeModifica(p: Props): JSX.Element { return (
-
@@ -56,12 +54,10 @@ export function SchedeModifica(p: Props): JSX.Element { - {(imp?.famiglia || 'qwen') === 'qwen' && ( -
-

LoRA

- -
- )} +
+

LoRA

+ +