diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c869af..4782a3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,53 @@ # Changelog +## 1.4.0 — 2026-10-28 + +Qwen molto più veloce, e Klein che finalmente fa quello che gli chiedi (cambi di persona, vestiti, unire più foto): era un problema di come gli si parlava, non del modello. Tutto misurato sul tuo PC (RTX 4060 8 GB, 1024×1024). + +### Perché Klein «non faceva niente» + +- **BFL lo scrive nella scheda del modello: «non usare Klein senza un prompt upsampler»**, cioè un LLM che prima legge la richiesta e la riscrive in una descrizione completa. L'app lo faceva, ma l'LLM **vedeva solo la prima foto**: scriveva «sostituisci la donna con l'uomo della seconda foto» senza sapere com'è l'uomo, e Klein restituiva la seconda foto com'era. Provato: con la frase corta il risultato era l'uomo della foto 2 identico; con tutte le foto in vista all'LLM, l'uomo prende il posto della donna con la sua posa, la tazza «MORNING», il muro e la luce. +- Ora l'LLM guarda **tutte le foto** (in un lotto solo, come nel flusso ufficiale) e scrive in 3-6 secondi. Con le foto usa il prompt di sistema ufficiale di BFL. +- Il negativo di Klein è il positivo azzerato (come nei flussi di BFL), non più il testo vuoto: conta con Klein Base (CFG 3,5). +- Provato anche: la cache KV di FLUX (`FluxKVCache`) su Klein 3 non fa niente (stesso tempo), quindi non c'è. + +### Klein: una pagina sola + +- Scegli **Klein 3** o **Klein 3 Base** in alto e l'interfaccia diventa semplice: **scrivi** cosa vuoi; se vuoi, **aggiungi foto** (fino a 10: la prima è quella che cambia, le altre sono riferimenti). Con foto è una modifica e l'immagine esce grande come la foto 1; con «Scelgo io» scegli formato e misura e ne esce una nuova con quei riferimenti. Niente Genere, Stile, Box, Ricette né schede: Klein fa tutto da solo. Restano **Quante**, **Seed** e **LoRA** (dentro «Altro» e «LoRA», chiusi). +- Le foto trascinate sulla finestra, e «Cambia» sotto un risultato, vanno nelle Foto di Klein. +- Sotto il risultato puoi aprire **«Quello che ha visto il modello»**: il testo che l'LLM ha scritto. + +### L'LLM che legge il prompt: a scelta, per tutti e due i modelli + +- Un interruttore **«L'LLM legge il prompt»** sotto la casella del prompt (Crea e Modifica) e in Opzioni. **Klein: acceso di serie** (ne ha bisogno). **Qwen: spento di serie** (capisce bene anche i prompt brevi), ma si può accendere: con Qwen l'LLM scrive col suo stile (i tag ``, `` che vuole il suo enhancer ufficiale) e aiuta le modifiche difficili. +- Scrive il **Qwen3-VL 4B** (tutto nella VRAM, ~3-6 s); se non c'è, l'encoder di Qwen. + +### Qwen veloce + +- **Nuovo modello «Qwen veloce»** (la seconda pillola in alto): Qwen-Image 2.1 col **LoRA Lightning di LightX2V** (distillazione a 4 passi, CFG 1, per testo→immagine e modifiche; 67 MB, si scarica con un clic). Misurato: **1 MP in 15 s invece di 73** (a 25 passi), testi sull'immagine compresi (la tazza «MORNING» esce giusta). A 2,25 MP: 58 s a freddo; a 4 MP (2048×2048): 72 s. Il LoRA Fun-Acc di Alibaba PAI, provato, non si carica nel motore (chiavi non riconosciute: le immagini escono sfocate): non c'è. +- **Qwen base fa 25 passi invece di 40**: sono quelli dei workflow ufficiali di ComfyUI per Qwen-Image 2.1. 1 MP: 73 s a freddo e a caldo (2,8 s a passo). +- **Cache KV a Q4 di serie.** La cache del prompt e delle foto di Qwen costa un quarto della memoria. Misurato su una modifica a 25 passi: **3,5 → 3,1 s a passo (−11%)**, immagine uguale a occhio (40,7 dB di somiglianza con la cache piena; int8 50 dB); spenta la cache va il doppio più piano (8,3 s a passo). Con i 4 passi di Lightning la composizione può cambiare un po' (stesso seme, stessa qualità). Si cambia in Opzioni → Avanzate. Le impostazioni esistenti passano a Q4, a Lightning e a 25 passi una volta sola. + +### Misure sulla tua scheda (RTX 4060 8 GB, caldo, 1 MP se non diversamente detto) + +| | tempo | +| --- | --- | +| Qwen veloce (4 passi) | 15 s | +| Qwen (25 passi) | 73 s (2,8 s a passo) | +| Qwen, modifica con una foto, 25 passi | 3,1 s a passo con Q4 (3,5 con la cache piena) | +| Klein 3 da zero (16 passi) | 9-12 s | +| Klein 3 da zero a 4 MP (2048×2048) | 58 s | +| Klein 3, modifica con 1 foto / 2 foto / 3 foto | 22 s / 38 s / 56 s | +| Klein 3 Base (50 passi, CFG 3,5), da zero / modifica | 56 s / 116 s | +| LLM che legge il prompt (4B) | 3-6 s | + +A 4 MP, nelle zone sfocate dello sfondo di Klein si vede un po' di banding (a gradini): non indagato, a 1 MP non c'è. + +### Dentro + +- `test/bench-motore.mjs`: misure di velocità e immagini sul motore vero (un ComfyUI a parte, la tua app non c'entra). +- Prove nuove: `test/interfaccia-1.4.mjs` (quattro modelli, Qwen coi suoi modi, Klein con una pagina sola, foto → modifica), `test/grafi_v2.mjs` (Lightning in tutte le modalità, negativo di Klein, LLM che guarda più foto), `test/modelli.mjs`. + ## 1.3.0 — 2026-10-28 Un giro di pulizia: i LoRA di Klein funzionano davvero, Klein parte dai 16 passi indicati da Black Forest Labs, Crea si usa subito (scrivi, scegli il formato, premi CREA) e le Opzioni sono corte, con tutto il resto in «Avanzate». diff --git a/CLAUDE.md b/CLAUDE.md index bb9a1e2..fc3e3dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,17 +50,17 @@ interfaccia**, installato e guidato dall'app. ## FLUX 3 Klein (famiglie di modelli) - `Famiglia = 'qwen' | 'klein' | 'klein-base'` (`shared/tipi.ts`, `FAMIGLIE` = passi per qualità), `Richiesta.famiglia`, `impostazioni.famiglia`. -- **Tre modelli, niente Turbo di Qwen** (1.1, `src/shared/modelli.ts`, la mappa unica, provata da `node test/modelli.mjs`): `qwen-base` (40 passi), `klein` (veloce, 16 passi: quelli di BFL, fino alla 1.2.1 erano 8; 16 costa 4 s in più a 1 MP e dà dettagli più puliti), `klein-base` (50, CFG 3,5). `impostazioni.modelloScelto` va sempre d'accordo con `famiglia` (`allineaScelta` in `salvaImpostazioni`); le impostazioni vecchie si migrano una volta (`migra`: Turbo GGUF → UC dello stesso quant). La qualità «turbo» salvata vale «alta» (`senzaTurbo`): l'interfaccia non manda più `turbo: true`, i grafi del Turbo restano per i PNG vecchi. +- **Quattro modelli** (`src/shared/modelli.ts`, la mappa unica, provata da `node test/modelli.mjs`): `qwen-base` (**25 passi**, quelli dei workflow ufficiali di ComfyUI; fino alla 1.3 erano 40), `qwen-veloce` (**1.4**: LoRA **Lightning** di LightX2V, 4 passi, `turbo: true` + `imp.loraTurbo` = `qwen-image-2.1-lightning-4step.safetensors`, voce `lora-lightning` del catalogo; 15 s invece di 73 a 1 MP), `klein` (veloce, 16 passi: quelli di BFL), `klein-base` (50, CFG 3,5). Il vecchio Turbo8 a 8 passi non si usa più (resta per i PNG vecchi: `shiftDi`). `impostazioni.modelloScelto` va sempre d'accordo con `famiglia` (`allineaScelta` in `salvaImpostazioni`); le impostazioni vecchie si migrano una volta (`migra`: Turbo GGUF → UC dello stesso quant). La qualità «turbo» salvata vale «alta» (`senzaTurbo`): l'interfaccia non manda più `turbo: true`, i grafi del Turbo restano per i PNG vecchi. Il selettore è `componenti/ScegliModello.tsx` (3 schede, "da scaricare", download di modello + `klein-te` + `klein-vae`), la qualità `ScegliQualita.tsx`. - Catalogo: `klein-int8/bf16`, `klein-base-int8/bf16`, `klein-te` (Qwen3-VL 4B), `klein-vae` dal repo `black-forest-labs/flux-3-image-klein-comfy` (`repo` + `cerca` regex, risolti a runtime da `installa/risolvi.ts`: **i nomi vengono dai template ufficiali, vanno confermati con i file veri**). `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 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. + Niente Turbo né cache KV (i LoRA invece sì: `applicaLora` vale per tutte le famiglie; `FluxKVCache` non fa niente su Klein 3: provato, stesso tempo); i tag `` diventano ordinali. Il negativo è il positivo azzerato (`ConditioningZeroOut`, come i workflow di BFL) se non ne scrivi uno. **I grafi Qwen non devono cambiare** (a parte Lightning): si controlla con `node test/confronta_grafi.mjs` 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`). +- Provato su RTX 4060 8 GB (1.4): 16 passi a 1 MP in 13 s, modifica con 1 foto 22 s, con 2 foto 38 s; `ReferenceLatent` in catena = `ReferenceMultiLatent` (fanno lo stesso). - **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`. **Formato dei LoRA di Klein (1.3):** i LoRA FLUX 3 esistono in due formati. AI Toolkit usa i nomi di ComfyUI (`diffusion_model.content_mode_blocks.image.N.q_proj`, `single_blocks.N.mlp_in`…) e il motore li carica; Diffusers/SimpleTuner usano `transformer.mode_blocks.image.N.attn.to_q`, `single_transformer_blocks.N.ff.linear_in`, e ComfyUI **non li conosce** (tutte le chiavi «not loaded», 0 patch: il LoRA non fa niente). `shared/loraBase.ts` li riconosce (`formato: 'diffusers'`), `shared/loraConverti.ts` ha la mappa dei nomi e `main/loraConverti.ts` riscrive solo l'intestazione del .safetensors (l'originale resta come `.diffusers.bak`; se alpha ≠ rango aggiunge i tensori `.alpha`). Si converte in `importaLora`, `scaricaLora` e in `elencoLora` per i file messi a mano; la forza di partenza di un LoRA di Klein è 0,75 (BFL). Prova: `node test/lora-converti.mjs`. Verificato sul motore vero con il LoRA delle angolazioni di akhaliq: prima 0 patch, dopo 172. @@ -166,3 +166,10 @@ interfaccia**, installato e guidato dall'app. - `index.ts`: istanza unica, `errori.log` in DATI, pulizia dei temp vecchi; `processo.ts`: `motore.pid` + `chiudiOrfano`, log a rotazione. `impostazioni.scaricaDopo` = secondi di quiete prima di liberare la VRAM. - Renderer: `main.tsx` ha il `Salvagente` (ErrorBoundary) e il gestore `unhandledrejection`; `leggiLocale`/`leggiLista` tollerano localStorage rovinato. - `test/finto.js` deve avere gli stessi campi del vero main (es. `modelli.file` con `klein*`): se mancano, l'avviso di errore compare e si vede. + +## 1.4: veloce, e Klein che funziona (misurato sulla 4060) +- **Perché Klein «non faceva niente»**: BFL lo dice nella scheda («do not use the model without a prompt upsampler») e l'app lo faceva, ma l'upsampler vedeva **solo la prima foto** (`immagini.slice(0, 1)`): scriveva «sostituisci la donna con l'uomo della seconda foto» senza sapere com'è l'uomo, e Klein restituiva la seconda foto com'era. Ora `fotoPerTesto()` (`grafi.ts`) manda **tutte** le foto al `TextGenerate` in un lotto (`BatchImagesNode`, ognuna a ~0,4 MP e alla forma della prima, come nel flusso ufficiale) e il cambio personaggio riesce. Il 4B scrive in 3-6 s. Con le foto si usa il prompt di sistema ufficiale lungo (`PUP_I2I`); da zero la versione breve (`PUP_T2I_BREVE`) perché il 4B con quello lungo lo ricopia. +- **L'LLM che legge il prompt è un interruttore per modello** (`potenziaPer(imp, fam)`: `potenziaKlein` di serie acceso, `potenziaQwen` spento), sotto la casella del prompt di Crea e di Modifica (`componenti/InterruttoreLLM.tsx`) e in Opzioni. `lavori.ts` `potenzia()` serve tutti e due: con Qwen scrive il 4B se `testoCon4B` (di serie sì) e chiede i tag `…` (`PUP_QWEN_I2I`: lo dice il prompt ufficiale del suo enhancer); con Klein cita le foto a parole. Il testo scritto sta in `Lavoro.promptInglese` e si vede sotto il risultato di Crea. +- **Klein ha una pagina sola** (`Crea.tsx`, `klein = !qwen`): prompt, Foto (fino a 10: la prima è quella che cambia), misura «come la foto 1» (modalità `modifica`, esce a ≤ 1 MP) o «Scelgo io» (`StatoCrea.misuraLibera`: `crea` con quei riferimenti), Altro (quante, seed), LoRA. Niente Genere, Stile, Box, Ricette, Estrai né la barra dei modi (`App.tsx` non monta `StripModi`; `vaiModo` e `apriInModifica` rimandano a Crea; una foto trascinata sulla finestra si aggiunge alle Foto). +- **Cache KV di Qwen a Q4 di serie** (`cacheTipo: 'int4'`, migrata una volta da `migra14` in `impostazioni.ts`, che mette anche Lightning e 25 passi): a 25 passi su una modifica 3,5 → 3,1 s/passo (−11%), immagine identica a occhio (40,7 dB contro la cache piena; int8 50 dB). Con 4 passi (Lightning) la composizione può cambiare un po' (22-26 dB) ma non la qualità. Spenta la cache: 8,3 s/passo (il doppio). +- `test/bench-motore.mjs`: misure sul motore vero (costruisce i grafi come l'app, li manda a un ComfyUI a parte sulla porta 8899 e dà le fasi): vedi l'intestazione. **Non lanciare `test/vetrina.mjs`, `gui.mjs`, `console.mjs`, `modifica.mjs`, `setup.mjs`, `foto.mjs`: aprono l'app vera sui dati veri** (la 1.4 ha migrato le impostazioni di chi le ha provate). Per l'interfaccia: `node test/interfaccia-1.4.mjs` e gli altri test con Chromium e il finto `window.daprod`. diff --git a/README.md b/README.md index 05ac5da..3955c92 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.4 + +- **Qwen veloce**: Qwen-Image 2.1 col LoRA Lightning (4 passi), **15 secondi a 1 MP invece di 73**. È la seconda pillola in alto. Qwen base ora fa 25 passi (erano 40) e la cache KV è a Q4 (modifiche con foto circa il 10% più veloci, stessa qualità a occhio). +- **Klein ora funziona**: cambi di persona, vestiti, più foto unite. Il problema era che l'LLM che riscrive la richiesta vedeva solo la prima foto; ora le vede tutte. Con Klein l'interfaccia è una pagina sola: scrivi, aggiungi foto se vuoi, CREA. +- **«L'LLM legge il prompt»**: un interruttore sotto il prompt, per Klein (acceso di serie) e per Qwen (spento di serie). + ## 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ù. @@ -119,8 +125,8 @@ Modifica → Estrai e nelle ricette. Diventa un manichino grigio che il modello ## Tre modelli: Qwen e FLUX 3 Klein -Nella barra in alto scegli **chi disegna**: **Qwen base** (Qwen-Image 2.1, 40 passi, con i LoRA), **Klein veloce** (16 passi) o **Klein Base** (la versione normale, 50 passi). I file di Klein si scaricano con un clic dal repository ufficiale di Black Forest Labs. -Il Turbo di Qwen non c'è più: Klein veloce è il modo rapido. +Nella barra in alto scegli **chi disegna**: **Qwen** (Qwen-Image 2.1, 25 passi, con i LoRA), **Qwen veloce** (lo stesso col LoRA Lightning, 4 passi: 5 volte più veloce), **Klein 3** (16 passi) o **Klein 3 Base** (la versione normale, 50 passi). I file di Klein e il LoRA Lightning si scaricano con un clic. +Con Qwen hai Genere, Stile, Modifica (anche solo una zona), Box e Ricette. Con Klein c'è una pagina sola: scrivi e aggiungi foto se vuoi cambiarle o unirle. 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. I LoRA hanno due scaffali, uno per Qwen e uno per Klein: ognuno si vede solo col suo modello. @@ -165,15 +171,15 @@ se la scheda è da 6 GB, il profilo **6 GB** in Opzioni. L'app ti avvisa da sola - Il motore è **ComfyUI 0.42.3**, a una versione fissata, senza la sua interfaccia, installato con **uv** in `%LOCALAPPDATA%\DaProdImage`. Il modello è [Qwen-Image 2.1 Uncensored GGUF](https://huggingface.co/abenzerps/Qwen-Image-2.1-Uncensored-GGUF) - (Q4_K_M per 6-8 GB) con i passi della pipeline ufficiale (40, euler, CFG 1). In alternativa **FLUX 3 Klein** (`black-forest-labs/flux-3-image-klein-comfy`), con il suo + (Q4_K_M per 6-8 GB) con i passi della pipeline ufficiale (25, euler, CFG 1; 4 col LoRA Lightning). In alternativa **FLUX 3 Klein** (`black-forest-labs/flux-3-image-klein-comfy`), con il suo encoder Qwen3-VL 4B e il VAE FLUX.2. - **ComfyUI-GGUF** viaggia dentro l'app: il fork di leejet, l'unico che conosce `qwen_image21`, con una nostra correzione per ComfyUI 0.39 e successive (`engine/custom_nodes/ComfyUI-GGUF/DAPROD.md`). I **DaProd-Nodi** riempiono la zona prima di "rimuovi" e accordano i colori dopo il VAE, così l'incollaggio non si vede. - Anteprime dal vivo con **TAEQI 2.1** di madebyollin; traduzione dei prompt con **Opus-MT** (un modello di sola traduzione, sul processore, ~300 MB, scaricato da solo); foto convertite con WIC di Windows, come in [DaP Convertitore](https://github.com/cammo22/DaP-Convertitore). -- Su una RTX 4060 da 8 GB, a 1 MP: Qwen base circa 3 secondi per passo (40 passi in poco meno di 3 minuti a freddo), **Klein veloce in circa 10 secondi a 16 passi (6 a 8 passi)**, - Klein Base 50 passi in circa un minuto. Le misure complete sono in [docs/misure-1.1.0.md](docs/misure-1.1.0.md). +- Su una RTX 4060 da 8 GB, a 1 MP: **Qwen veloce 15 secondi** (4 passi), Qwen base 73 secondi (25 passi, 2,8 s a passo), **Klein 3 in circa 10 secondi** a 16 passi + (22 s per modificare una foto, 38 s con due foto). Le misure della 1.4 sono nel [CHANGELOG](CHANGELOG.md), quelle della 1.1 in [docs/misure-1.1.0.md](docs/misure-1.1.0.md). ## Sviluppare diff --git a/package-lock.json b/package-lock.json index 9c2861f..e5d404e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "daprod-image", - "version": "1.3.0", + "version": "1.4.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "daprod-image", - "version": "1.3.0", + "version": "1.4.0", "license": "MIT", "dependencies": { "electron-updater": "^6.8.9" diff --git a/package.json b/package.json index 0a0f876..721771f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "daprod-image", "productName": "DaProd Image", - "version": "1.3.0", + "version": "1.4.0", "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", diff --git a/src/main/impostazioni.ts b/src/main/impostazioni.ts index 6111a34..a51b985 100644 --- a/src/main/impostazioni.ts +++ b/src/main/impostazioni.ts @@ -2,7 +2,7 @@ import { readFileSync, writeFileSync, renameSync, readdirSync } from 'node:fs' import { join } from 'node:path' import type { Impostazioni } from '@shared/tipi' -import { allineaScelta, denoiserSenzaTurbo, famigliaDi, modelloDaImpostazioni } from '@shared/modelli' +import { LIGHTNING_FILE, allineaScelta, denoiserSenzaTurbo, famigliaDi, modelloDaImpostazioni } from '@shared/modelli' import { DATI, GALLERIA_PREDEFINITA, MODELLI_PREDEFINITI, assicura } from './percorsi' const FILE = join(DATI, 'impostazioni.json') @@ -24,18 +24,19 @@ const PREDEFINITE: Impostazioni = { scaricaInIdle: true, porta: 8818, cacheKV: 'auto', - cacheTipo: 'default', + // Q4: la cache KV di Qwen costa un quarto della memoria e fa le modifiche con foto circa il 12% più veloci (misurato sulla 4060, stessa qualità a occhio) + cacheTipo: 'int4', encoderSuCpu: false, anteprimaLive: true, riservaVram: 0, argomentiExtra: '', - // gli step "standard" della pipeline ufficiale (40, euler, cfg 1): la qualità piena - predefiniti: { passi: 40, cfg: 1, sampler: 'euler', scheduler: 'simple', megapixel: 1 }, + // gli step dei workflow ufficiali di ComfyUI per Qwen-Image 2.1 (25, euler/simple, cfg 1) + predefiniti: { passi: 25, cfg: 1, sampler: 'euler', scheduler: 'simple', megapixel: 1 }, installato: false, tokenCivitai: '', tokenHF: '', tempi: {}, - loraTurbo: 'turbo8_lora_step2500.safetensors', + loraTurbo: LIGHTNING_FILE, memoria: 'auto', vramMB: 0, veloce: false, @@ -43,6 +44,8 @@ const PREDEFINITE: Impostazioni = { traduttore: 'dedicato', unaCosaAllaVolta: true, potenziaKlein: true, + potenziaQwen: false, + migrata: 14, testoCon4B: true, // misurato in T12 (docs/misure-1.1.0.md): Migliora prompt + Crea con Qwen passa da 186 s a 134 s (encoder 8B int8) o da 446 s a 150 s (8B GGUF) scaricaDopo: 120, uncensored: false, @@ -74,6 +77,17 @@ function migra(letto: Partial, imp: Impostazioni): boolean { return true } +/** 1.4: la cache KV di Qwen passa a Q4 (se era quella piena di serie) e il LoRA di distillazione a Lightning (se era il Turbo8 di serie o vuoto); + * i passi di serie scendono a 25. Una volta sola. */ +function migra14(letto: Partial, imp: Impostazioni): boolean { + if ((letto.migrata ?? 0) >= 14) return false + if (!letto.cacheTipo || letto.cacheTipo === 'default') imp.cacheTipo = 'int4' + if (!letto.loraTurbo || /^turbo8/i.test(letto.loraTurbo)) imp.loraTurbo = LIGHTNING_FILE + if (!letto.predefiniti || letto.predefiniti.passi === 40) imp.predefiniti = { ...imp.predefiniti, passi: 25 } + imp.migrata = 14 + return true +} + export function impostazioni(): Impostazioni { if (attuali) return attuali try { @@ -83,7 +97,8 @@ export function impostazioni(): Impostazioni { imp.modelloScelto = modelloDaImpostazioni(letto) imp.famiglia = famigliaDi(imp.modelloScelto) attuali = imp - if (migra(letto, imp)) { + const m14 = migra14(letto, imp) + if (migra(letto, imp) || m14) { try { scrivi(imp) } catch { diff --git a/src/main/index.ts b/src/main/index.ts index eec7792..7da7c16 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -223,7 +223,9 @@ function registraIpc(): void { upscaler: leggi('upscale_models', /\.(pth|safetensors)$/i), klein: tutti.filter(isKlein), kleinTe: enc.filter(isKleinTe), - kleinVae: vae.filter(isKleinVae) + kleinVae: vae.filter(isKleinVae), + // il LoRA di «Qwen veloce» (Lightning): sta in loras/, col nome che dicono le impostazioni + lightning: existsSync(join(c, 'loras', impostazioni().loraTurbo)) } }) gestisci('modelli:inutilizzati', () => fileInutilizzati()) diff --git a/src/main/installa/catalogo.ts b/src/main/installa/catalogo.ts index 88dc312..11aa2fb 100644 --- a/src/main/installa/catalogo.ts +++ b/src/main/installa/catalogo.ts @@ -164,10 +164,18 @@ const GREZZO: VoceCatalogo[] = [ byte: 66961958, sha256: 'a5812231fc936b42af08a5edba784195495d303d5b3248c24489ef0c4021fe01', consigliato: true, necessario: true }, // --- LoRA consigliati --- + { + // LightX2V: Qwen-Image 2.1 in 4 passi con CFG 1 (distillazione DMD), per testo→immagine e modifiche. Misurato sulla 4060: 1 MP in 15 s invece di 73 a 25 passi + id: 'lora-lightning', tipo: 'lora', nome: 'Lightning 4 passi (LightX2V)', uso: 'Qwen veloce', + descrizione: 'Fa le immagini di Qwen in 4 passi invece di 25: circa 5 volte più veloce, con qualità quasi uguale. È quello che usa il modello «Qwen veloce».', + file: 'qwen-image-2.1-lightning-4step.safetensors', cartella: 'loras', + url: 'https://huggingface.co/lightx2v/Qwen-Image-2.1-Lightning/resolve/main/pytorch_lora_weights.safetensors', + byte: 67144280, sha256: '53c69320d4163da51032f90275074028cb098556039c72b45f8dce9f0f9e34a3', consigliato: true + }, { // Turbo8 (chriswritescode): distillazione a 8 passi, CFG 1, euler/simple. Niente sha256 pubblicato: basta che ci sia id: 'lora-turbo', tipo: 'lora', nome: 'Turbo 8 passi (Turbo8)', - descrizione: 'Fa le immagini in 8 passi invece di 40: circa 5 volte più veloce, con poca perdita. Dalla 1.1 non serve più: il Turbo di Qwen non esiste più.', + descrizione: 'Fa le immagini in 8 passi invece di 40: circa 5 volte più veloce, con poca perdita. Dalla 1.4 non serve più: c\'è Lightning (4 passi), più veloce e più pulito.', file: 'turbo8_lora_step2500.safetensors', cartella: 'loras', url: 'https://huggingface.co/chriswritescode/Turbo8-LoRA-Qwen-Image-2.1/resolve/main/turbo8_lora_step2500.safetensors', // dalla 1.1 il Turbo di Qwen non si usa più (D1 del piano): resta per chi ce l'ha, ma non si scarica da solo diff --git a/src/main/motore/grafi.ts b/src/main/motore/grafi.ts index bb635c1..12a936e 100644 --- a/src/main/motore/grafi.ts +++ b/src/main/motore/grafi.ts @@ -1,7 +1,8 @@ // Da una Richiesta al grafo di ComfyUI (formato API). Ogni modalità usa gli stessi pezzi: // modello GGUF → (Turbo) → LoRA → cache KV di Qwen-Image 2.1 · text encoder Qwen3-VL · VAE -// e i parametri della pipeline ufficiale (euler/simple, cfg 1, 40 passi; 8 col Turbo). +// e i parametri della pipeline ufficiale (euler/simple, cfg 1, 25 passi; 4 col LoRA Lightning). import { modelloTurbo } from '@shared/tipi' +import { shiftDi } from '@shared/modelli' import type { Famiglia, Impostazioni, LoraAttiva, Richiesta, Riquadro } from '@shared/tipi' import type { ChiaviRtx } from './cliente' import { promptConPosizioni, promptKleinConBox, regioniPronte } from './regioni' @@ -57,9 +58,16 @@ export function dimensioniPer(mp: number, larghezza: number, altezza: number): { return { w: m32(larghezza * s), h: m32(altezza * s) } } -/** il Turbo è acceso e il suo LoRA c'è? */ +/** il Turbo (dalla 1.4 il LoRA Lightning a 4 passi) è acceso e il suo LoRA c'è? */ const conTurbo = (q: Richiesta, imp: Impostazioni): boolean => !!q.turbo && (modelloTurbo(imp.modello) || !!imp.loraTurbo) +/** Lightning (e il vecchio Turbo8) sono allenati con la scaletta del rumore spostata come nel loro workflow, e lo spostamento dipende dalla grandezza + * della latente: `ModelSamplingFlux` davanti al campionatore, in tutte le modalità (nella scheda del modello vale per testo→immagine e per le modifiche) */ +function conShiftTurbo(g: Grafo, q: Richiesta, imp: Impostazioni, model: Rif, w: number, h: number): Rif { + if (famigliaDi(q, imp) !== 'qwen' || !conTurbo(q, imp) || modelloTurbo(imp.modello)) return model + return r(g.add('ModelSamplingFlux', { model, max_shift: shiftDi(imp.loraTurbo), base_shift: 0.5, width: w, height: h })) +} + /** il text encoder: safetensors col CLIPLoader, GGUF (Q4/Q6/Q8) col suo loader, che trova da solo l'mmproj accanto */ function caricaClip(g: Grafo, imp: Impostazioni): Rif { const titolo = 'Carico il text encoder' @@ -77,6 +85,20 @@ function caricaClipPerTesto(g: Grafo, imp: Impostazioni, kleinEncoder: boolean | return r(g.add('CLIPLoader', { clip_name: imp.kleinEncoder, type: 'krea2', device: imp.encoderSuCpu ? 'cpu' : 'default' }, 'Carico il text encoder')) } +/** le foto che l'LLM guarda mentre scrive. Una sola: com'è (i grafi di prima non cambiano). Più foto (i riferimenti di Klein): vanno tutte a un solo + * lotto, come nel flusso ufficiale (BatchImagesNode → TextGenerate), ognuna a ~0,4 MP e alla forma della prima, così l'upsampler vede chi c'è in ogni foto. */ +function fotoPerTesto(g: Grafo, immagini: Ingressi['immagini']): Rif { + if (immagini.length === 1) return r(g.add('LoadImage', { image: immagini[0].nome }, 'Guardo la foto')) + const prima = dimensioniPer(0.4, immagini[0].larghezza, immagini[0].altezza) + const pronte = immagini.slice(0, 10).map((im, i) => { + const caricata = r(g.add('LoadImage', { image: im.nome }, i === 0 ? 'Guardo le foto' : undefined)) + return r(g.add('ImageScale', { image: caricata, upscale_method: 'lanczos', width: prima.w, height: prima.h, crop: 'disabled' })) + }) + const inputs: Record = {} + pronte.forEach((p, i) => (inputs[`images.image${i}`] = p)) + return r(g.add('BatchImagesNode', inputs)) +} + /** il modello che disegna questa richiesta */ export const famigliaDi = (q: Richiesta, imp: Impostazioni): Famiglia => q.famiglia ?? imp.famiglia ?? 'qwen' @@ -159,10 +181,11 @@ function condiziona( const enc = g.add('TextEncodeQwenImage21', inputs, o.fase) return { pos: r(enc, 0), neg: r(enc, 1), lat: r(enc, 2) } } - // Klein non ha i tag : le foto si citano a parole («the second image»), come fa il suo prompt upsampler. Il negativo è il testo vuoto, o quello scritto + // Klein non ha i tag : le foto si citano a parole («the second image»), come fa il suo prompt upsampler. Il negativo è quello scritto, + // o (come nei workflow ufficiali di BFL) il positivo azzerato: con Klein Base (CFG 3,5) il testo vuoto codificato non è la stessa cosa const parole = o.prompt.replace(//g, (_m, n: string) => `the ${ORDINALI[Number(n) - 1] || 'last'} image`) let pos = r(g.add('CLIPTextEncode', { clip: ctx.clip, text: parole }, o.fase)) - let neg = r(g.add('CLIPTextEncode', { clip: ctx.clip, text: o.neg || '' })) + let neg = o.neg ? r(g.add('CLIPTextEncode', { clip: ctx.clip, text: o.neg })) : r(g.add('ConditioningZeroOut', { conditioning: pos })) o.immagini.slice(0, 10).forEach((im, i) => { const pronta = i === 0 && o.misuraPrima @@ -355,7 +378,7 @@ export function costruisci(q: Richiesta, imp: Impostazioni, ing: Ingressi, prefi system_prompt: q.sistema || '' } if (q.presenza !== undefined) inputs['sampling_mode.presence_penalty'] = q.presenza - if (ing.immagini[0]) inputs.image = r(g.add('LoadImage', { image: ing.immagini[0].nome }, 'Guardo la foto')) + if (ing.immagini.length) inputs.image = fotoPerTesto(g, ing.immagini) const gen = g.add('TextGenerate', inputs, 'Scrivo') const uscita = g.add('PreviewAny', { source: r(gen, 0) }) return { prompt: g.nodi, uscita, fasi: g.fasi, megapixel: 0, passi: q.maxToken || 400 } @@ -406,11 +429,7 @@ export function costruisci(q: Richiesta, imp: Impostazioni, ing: Ingressi, prefi const caricate = ing.immagini.slice(0, fam === 'qwen' ? 16 : 10).map((im) => r(g.add('LoadImage', { image: im.nome }, 'Carico i riferimenti'))) const cond = condiziona(g, fam, { clip, vae }, { prompt: testo, neg, resolution: 1024, conVae: caricate.length > 0, immagini: caricate, mpRiferimenti: 1, scalaDa: 0, fase: leggo }) const lat = latenteVuota(g, fam, q.larghezza, q.altezza) - // il Turbo è stato allenato con la scaletta del rumore che dipende dalla grandezza (come nel suo workflow) - const m = fam === 'qwen' && conTurbo(q, imp) && !modelloTurbo(imp.modello) - ? r(g.add('ModelSamplingFlux', { model, max_shift: 0.6935, base_shift: 0.5, width: q.larghezza, height: q.altezza })) - : model - const ks = campiona(g, q, fam, m, cond, lat, q.passi, 1, { w: q.larghezza, h: q.altezza }) + const ks = campiona(g, q, fam, conShiftTurbo(g, q, imp, model, q.larghezza, q.altezza), cond, lat, q.passi, 1, { w: q.larghezza, h: q.altezza }) const mp = (q.larghezza * q.altezza) / 1048576 const uscita = salva(g, allaMisura(g, q, imp, sviluppa(g, q, ks, vae, mp, fam !== 'qwen'), q.larghezza, q.altezza, rtx), prefisso) return { prompt: g.nodi, uscita, fasi: g.fasi, megapixel: mp, passi: q.passi } @@ -423,7 +442,7 @@ export function costruisci(q: Richiesta, imp: Impostazioni, ing: Ingressi, prefi const caricate = ing.immagini.slice(0, fam === 'qwen' ? 16 : 10).map((im) => r(g.add('LoadImage', { image: im.nome }, 'Carico le foto'))) const cond = condiziona(g, fam, { clip, vae }, { prompt: conRegioni(q.prompt, q, fam, true), neg, resolution: res, conVae: true, immagini: caricate, misuraPrima: d, fase: leggo }) const lat = cond.lat ?? latenteVuota(g, fam, d.w, d.h) - const ks = campiona(g, q, fam, model, cond, lat, q.passi, 1, d) + const ks = campiona(g, q, fam, conShiftTurbo(g, q, imp, model, d.w, d.h), cond, lat, q.passi, 1, d) const mp = (d.w * d.h) / 1048576 const uscita = salva(g, allaMisura(g, q, imp, sviluppa(g, q, ks, vae, mp, fam !== 'qwen'), d.w, d.h, rtx), prefisso) const area = { x: 0, y: 0, w: ing.immagini[0].larghezza, h: ing.immagini[0].altezza } @@ -454,7 +473,7 @@ export function costruisci(q: Richiesta, imp: Impostazioni, ing: Ingressi, prefi const lat = codifica(g, img, vae, mp) const forza = q.forza ?? (q.modalita === 'rifinisci' ? 0.45 : 0.65) const passi = passiPer(q.passi, forza, minimo) - const ks = campiona(g, q, fam, model, cond, lat, passi, forza, { w: W, h: H }) + const ks = campiona(g, q, fam, conShiftTurbo(g, q, imp, model, W, H), cond, lat, passi, forza, { w: W, h: H }) const uscita = salva(g, allaMisura(g, q, imp, sviluppa(g, q, ks, vae, mp, fam !== 'qwen'), W, H, rtx), prefisso) return { prompt: g.nodi, uscita, fasi: g.fasi, megapixel: mp, passi } } @@ -564,7 +583,7 @@ export function costruisci(q: Richiesta, imp: Impostazioni, ing: Ingressi, prefi const mp = (box.tw * box.th) / 1048576 const lat0 = codifica(g, sorgente, vae, mp) const lat = r(g.add('SetLatentNoiseMask', { samples: lat0, mask: mLatente })) - const dd = r(g.add('DifferentialDiffusion', { model, strength: 1 })) + const dd = r(g.add('DifferentialDiffusion', { model: conShiftTurbo(g, q, imp, model, box.tw, box.th), strength: 1 })) const forza = q.forza ?? 1 const passi = passiPer(q.passi, forza, minimo) const ks = campiona(g, q, fam, dd, cond, lat, passi, forza, { w: box.tw, h: box.th }) diff --git a/src/main/motore/lavori.ts b/src/main/motore/lavori.ts index 25390e7..1a4528e 100644 --- a/src/main/motore/lavori.ts +++ b/src/main/motore/lavori.ts @@ -2,9 +2,11 @@ // Per ogni lavoro: carica le foto, costruisce il grafo, segue avanzamento e anteprime, // e alla fine porta l'immagine in galleria con dentro le impostazioni. import { nativeImage } from 'electron' +import { existsSync } from 'node:fs' import { join } from 'node:path' import { randomInt, randomUUID } from 'node:crypto' import type { Famiglia, Impostazioni, Lavoro, Richiesta } from '@shared/tipi' +import { potenziaPer } from '@shared/modelli' import { USCITA } from '../percorsi' import { impostazioni, profiloMemoria, registraTempo, stimaPasso } from '../impostazioni' import { costruisci, type GrafoPronto, type Ingressi } from './grafi' @@ -14,7 +16,7 @@ import { accoda, caricaImmagine, chiaviRtx, collegato, connetti, interrompi, nod import { avviaMotore, infoMotore, aggiornaVram } from './processo' import { aggiungi } from '../galleria' import { pulisciDidascalia, senzaTitolo } from './didascalia' -import { PUP_I2I, PUP_I2I_BREVE, PUP_T2I, PUP_T2I_BREVE } from './pup' +import { PUP_I2I, PUP_QWEN_I2I, 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' @@ -302,6 +304,23 @@ const SEGMENTATORI: Record = { segmenta: { id: 'seg-sam2', nome: 'SAM 2' } } +/** «Qwen veloce» (richiesta con `turbo`) vuole il LoRA Lightning: se non c'è, lo scarica (67 MB) invece di far fallire il lavoro con un errore del motore */ +async function lightningPronto(l: Lavoro, imp: Impostazioni): Promise { + const c = join(imp.cartellaModelli, 'loras', imp.loraTurbo) + if (existsSync(c)) return + l.fase = 'Scarico il LoRA Lightning' + avvisa(l) + try { + await scaricaVoce('lora-lightning', (r, t) => { + l.fase = `Scarico il LoRA Lightning${t ? ` · ${Math.round((r / t) * 100)}%` : ''}` + avvisa(l, false) + }) + } catch (e) { + throw new Error(`Non riesco a scaricare il LoRA Lightning (serve a «Qwen veloce»): ${(e as Error).message}`) + } + if (!existsSync(c)) throw new Error('Il LoRA Lightning non è sul disco: scegli «Qwen» invece di «Qwen veloce», o scaricalo dalla pagina LoRA.') +} + /** il segmentatore è sul disco? se manca lo scarica (BiRefNet ~450 MB, SAM 2 ~185 MB); se non ci riesce il lavoro fallisce con un messaggio chiaro */ async function segmentatorePronto(l: Lavoro, s: { id: string; nome: string }): Promise { const stato = (): boolean => !!statoModelli().find((v) => v.id === s.id)?.presente @@ -322,38 +341,42 @@ async function segmentatorePronto(l: Lavoro, s: { id: string; nome: string }): P // le didascalie già scritte (più immagini dallo stesso prompt = un'espansione sola) const potenziati = new Map() -/** Klein: espande una richiesta breve in una didascalia dettagliata (come l'upsampler ufficiale); in inglese anche se la richiesta era in italiano. +/** «L'LLM legge il prompt prima di disegnare» (Klein di serie, Qwen a scelta): espande una richiesta breve in una didascalia o in un'istruzione dettagliata, + * GUARDANDO TUTTE LE FOTO (non solo la prima: altrimenti un «sostituisci la donna con l'uomo della seconda foto» non può funzionare); in inglese anche se la richiesta era in italiano. * Se non riesce si ripiega sulla traduzione normale. - * `impLavoro` sono le impostazioni del lavoro (con i file di Klein già risolti): l'upsampler usa l'encoder della famiglia, - * così con Klein scrive col suo Qwen3-VL 4B, che è molto più scattante del Qwen 8B. */ + * `impLavoro` sono le impostazioni del lavoro (con i file di Klein già risolti): scrive il Qwen3-VL 4B (tutto in VRAM, decine di token al secondo) se c'è; + * con Qwen solo se `testoCon4B` è acceso, se no il suo encoder (8B, molto più lento). */ async function potenzia(l: Lavoro, ing: Ingressi, impLavoro: Impostazioni, fam: Famiglia): Promise { const q = l.richiesta const p = q.prompt.trim() const conFoto = ing.immagini.length > 0 - const chiave = `${fam}|${conFoto}|${q.larghezza}x${q.altezza}|${p}` + const chiave = `${fam}|${ing.immagini.map((i) => i.nome).join(',')}|${q.larghezza}x${q.altezza}|${p}` const gia = potenziati.get(chiave) if (gia) return gia - const imp = impLavoro - l.fase = 'Potenzio il prompt' + // chi scrive: il 4B (con Qwen solo se è acceso `testoCon4B`) o l'encoder della famiglia + const file4B = encoderKleinFile(impLavoro) + const encoderKlein = scriveCol4B('descrivi', fam, undefined, !!impLavoro.testoCon4B, !!file4B) + const imp = encoderKlein ? { ...impLavoro, kleinEncoder: file4B } : impLavoro + l.fase = 'Leggo il prompt' avvisa(l) // 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 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}`) + // il 4B vuole la richiesta da foto «nuda» (con «Reference images follow… Output the caption» risponde «user»): le foto le vede dal nodo. Il prompt di sistema ufficiale + // (lungo) funziona anche con lui quando vede tutte le foto (misurato in 1.4); da zero il 4B si perde col prompt lungo, per lui c'è la versione breve. + // Klein cita le foto a parole («the second image»), Qwen coi tag : la richiesta si adatta. + const richiesta = fam === 'qwen' ? p : p.replace(//g, (_m, n: string) => `the image number ${n}`) const utente = conFoto ? encoderKlein ? richiesta : `Reference images follow in order (the first image, the second image, ...).\nUser request: ${richiesta}\nOutput the caption.` : `${q.larghezza > 0 && q.modalita === 'crea' ? `TARGET CANVAS: ${Math.round(q.larghezza / g0)}:${Math.round(q.altezza / g0)}\n` : ''}${p}` - const sistema = conFoto ? (encoderKlein ? PUP_I2I_BREVE : PUP_I2I) : encoderKlein ? PUP_T2I_BREVE : PUP_T2I + const sistema = conFoto ? (fam === 'qwen' ? PUP_QWEN_I2I : PUP_I2I) : encoderKlein ? PUP_T2I_BREVE : PUP_T2I const g = costruisci( { ...q, modalita: 'descrivi', encoderKlein, prompt: utente, sistema, maxToken: conFoto ? 320 : 600, temperatura: 0.15, presenza: conFoto ? 0 : 1.5, seed: q.seed >= 0 ? q.seed : randomInt(1, 2 ** 31), immagini: [] }, - imp, { immagini: ing.immagini.slice(0, 1) }, 'pup' + imp, { immagini: ing.immagini.slice(0, 10) }, 'pup' ) - g.fasi = Object.fromEntries(Object.keys(g.fasi).map((k) => [k, 'Potenzio il prompt'])) + g.fasi = Object.fromEntries(Object.keys(g.fasi).map((k) => [k, 'Leggo il prompt'])) const u = await lancia(g, occupaTesto(encoderKlein), l, true) const t = pulisciDidascalia(u.testo || '').replace(/^["“]+|["”]+$/g, '').replace(/^(caption|instruction)\s*:\s*/i, '') if (t.length < 20) throw new Error('didascalia vuota') @@ -458,6 +481,7 @@ async function esegui(l: Lavoro): Promise { } if (!collegato()) await connetti() if (l.richiesta.modalita in SEGMENTATORI) await segmentatorePronto(l, SEGMENTATORI[l.richiesta.modalita]) + if (l.richiesta.turbo && (l.richiesta.famiglia ?? imp.famiglia) === 'qwen' && !/turbo/i.test(imp.modello) && l.richiesta.modalita !== 'descrivi' && l.richiesta.modalita !== 'traduci') await lightningPronto(l, imp) const ing = await prepara(l.richiesta) // FLUX 3 Klein: i suoi file (scelti, scaricati dall'app o messi a mano) e, se serve, il prompt espanso const fam = l.richiesta.famiglia ?? imp.famiglia @@ -472,7 +496,7 @@ async function esegui(l: Lavoro): Promise { impLavoro = { ...imp, kleinModello: f.modello, kleinBase: f.modello, kleinEncoder: f.encoder, kleinVae: f.vae } } let inglese: string - if (usaKlein && imp.potenziaKlein && l.richiesta.potenzia && l.richiesta.prompt.trim() && (l.richiesta.modalita === 'crea' || l.richiesta.modalita === 'modifica')) { + if ((usaKlein || fam === 'qwen') && potenziaPer(imp, fam) && l.richiesta.potenzia && l.richiesta.prompt.trim() && (l.richiesta.modalita === 'crea' || l.richiesta.modalita === 'modifica')) { try { inglese = await potenzia(l, ing, impLavoro, fam) } catch (e) { diff --git a/src/main/motore/pup.ts b/src/main/motore/pup.ts index f0705d1..55897a8 100644 --- a/src/main/motore/pup.ts +++ b/src/main/motore/pup.ts @@ -2,6 +2,9 @@ // perché è allenato sulle didascalie lunghe che l'upsampler scrive). L'upsampler vero è un Qwen3.5 da 9 miliardi; qui si usano gli stessi // prompt di sistema (dal repo ufficiale github.com/black-forest-labs/flux-3-image-klein, Apache 2.0) col Qwen3-VL che l'app ha già. // t2i = da zero, i2i = con foto da modificare o da usare come riferimenti. +// IMPORTANTE (misurato sulla 4060, 1.4): chi scrive deve VEDERE TUTTE le foto, non solo la prima. Con una foto sola l'upsampler non sa chi c'è nella seconda, +// scrive «sostituisci la donna con l'uomo» senza dire com'è l'uomo, e Klein restituisce la seconda foto com'è: il «cambio personaggio» non riesce. +// Con tutte le foto in vista (BatchImagesNode → TextGenerate, come nel flusso ufficiale) anche il 4B scrive istruzioni che funzionano. // // Klein ha DUE encoder: il Qwen3-VL 4B (suo, entra tutto nella VRAM: ~49 tk/s) e il Qwen3-VL 8B (quello di Qwen-Image: 8,9 GB non entrano nella VRAM da 8 GB, // gira quasi tutto sul processore a 0,5-3 tk/s). L'upsampler di Klein usa il 4B, caricato come `krea2` (con `flux3` il tokenizer riempie il prompt di token @@ -21,3 +24,8 @@ export const PUP_T2I_BREVE = export const PUP_I2I_BREVE = "You are the prompt upsampler of an image-editing model. Look at the reference image. Write ONE detailed, self-contained English instruction for an image-editing model that will edit this image as the user asks. Ground everything in what is really visible: identify each element by its real appearance and never mention content that is not there. State every end state concretely (which color, how large, which pose, which material) and cover the knock-on effects of the change (shadows, reflections, occlusion, perspective, scale, lighting). Output only the instruction, as one continuous prose block of 50 to 150 words, with no greetings, no explanations, no Markdown." + +// Qwen-Image 2.1 non vuole ordinali a parole ma i tag , … (lo dice il prompt ufficiale del suo «prompt enhancer»: «MUST use , … Do not use +// natural language references like "the first image"»). Versione corta, come le altre, per il 4B. +export const PUP_QWEN_I2I = + "You write instructions for an image-editing model. You see the input images, attached in order, and the user's short request (it may be in any language). Rewrite it as ONE clear, detailed English instruction for the editing model. When there are several images, refer to them ONLY with the tags , , ... in the order they are attached, never with words like \"the first image\"; with a single image just say \"the image\". Say what to take from each image and what is newly generated, and ground everything in what is really visible: identify each element by its real appearance. State every end state concretely (color, size, pose, material). Close with one short clause naming what stays unchanged; transferred people keep their identity, face and accessories unless asked otherwise. Transcribe on-image text verbatim inside double quotes. Output only the instruction, as one continuous prose block of 40 to 120 words, with no greetings, no explanations, no Markdown." diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 087b4b9..5927fa6 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -123,7 +123,7 @@ export function App(): JSX.Element { const foto = percorsi.find((p) => !/\.(safetensors|ckpt|pt|gguf|json|txt|zip)$/i.test(p)) if (foto) { try { - apriInModifica(await normalizza(foto)) + apriInModifica(await normalizza(foto), true) } catch (er) { avvisa((er as Error).message, 'errore') } @@ -181,6 +181,12 @@ export function App(): JSX.Element { } }, [risorse, lavori, motore, scarico]) + // Klein: una pagina sola. Se si cambia modello mentre si è in Modifica o Box, si torna a Crea + const conKlein = !!imp && imp.famiglia !== 'qwen' + useEffect(() => { + if (conKlein && (pagina === 'modifica' || pagina === 'box')) vai('crea') + }, [conKlein, pagina, vai]) + const attivi = lavori.filter((l) => l.stato === 'in coda' || l.stato === 'in corso').length const statoMotore = motore.stato === 'pronto' ? 'pronto' : motore.stato === 'avvio' ? 'avvio' : motore.stato === 'errore' ? 'errore' : '' const testoMotore = { pronto: 'Motore pronto', avvio: 'Avvio del motore…', errore: 'Motore fermo', spento: 'Motore spento', 'non installato': 'Da installare' }[motore.stato] @@ -248,7 +254,8 @@ export function App(): JSX.Element {
{/* le pagine restano montate: la maschera disegnata o il prompt non si perdono cambiando pagina */}
- + {/* con Klein c'è una pagina sola (Crea: scrivi, aggiungi foto se vuoi): niente modi, niente schede */} + {!conKlein && }
@@ -279,7 +286,7 @@ export function App(): JSX.Element {
{rilascio && (
-
{pagina === 'galleria' ? 'Rilascia: le foto entrano in Galleria (diventano PNG)' : pagina === 'lora' ? 'Rilascia: i .safetensors vanno fra i LoRA' : 'Rilascia: la foto si apre in Modifica (diventa PNG)'}
+
{pagina === 'galleria' ? 'Rilascia: le foto entrano in Galleria (diventano PNG)' : pagina === 'lora' ? 'Rilascia: i .safetensors vanno fra i LoRA' : imp?.famiglia && imp.famiglia !== 'qwen' ? 'Rilascia: la foto si aggiunge alle Foto di Klein (diventa PNG)' : 'Rilascia: la foto si apre in Modifica (diventa PNG)'}
)}
diff --git a/src/renderer/src/api.ts b/src/renderer/src/api.ts index 19d19f3..61afed1 100644 --- a/src/renderer/src/api.ts +++ b/src/renderer/src/api.ts @@ -36,6 +36,8 @@ export interface FileModelli { klein: string[] kleinTe: string[] kleinVae: string[] + /** il LoRA Lightning (Qwen veloce) è nella cartella dei LoRA */ + lightning?: boolean } export interface ModelloTrovato { percorso: string diff --git a/src/renderer/src/azioni.ts b/src/renderer/src/azioni.ts index 28d40a9..7988008 100644 --- a/src/renderer/src/azioni.ts +++ b/src/renderer/src/azioni.ts @@ -3,7 +3,7 @@ import { inQualitaPiena, modelloDaOpera, parametriModello, senzaTurbo, type IdMo 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 { modelloCorrente, passiDi, usaStato, type FotoBase, type Qualita } from './stato' import { dimensioniRis } from './util' import type { ChiaveLora } from './rapide' @@ -64,9 +64,9 @@ const loraDellOpera = (o: Opera): LoraAttiva[] => { 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 */ +/** passi per una qualità. Con «Qwen veloce» sono i 4 del LoRA Lightning (e si accende il LoRA); una qualità «turbo» salvata prima della 1.1 vale «alta» */ export function perQualita(q: Qualita): Pick & Partial> { - return { passi: passiDi(senzaTurbo(q)), turbo: false } + return { passi: passiDi(senzaTurbo(q)), turbo: modelloCorrente() === 'qwen-veloce' } } /** la qualità per i lavori "di servizio" (rifinisci, varia) */ @@ -74,8 +74,18 @@ const qualitaServizio = (): Qualita => 'alta' export const fotoDaOpera = (o: Opera): FotoBase => ({ percorso: o.file, larghezza: o.larghezza, altezza: o.altezza, operaId: o.id, prompt: o.prompt }) -export function apriInModifica(f: FotoBase): void { +/** Con Klein non c'è una pagina Modifica: la foto diventa la prima «Foto» di Crea (con `aggiungi`, si mette in fondo alle altre: foto trascinate sulla finestra) */ +const conKlein = (): boolean => (usaStato.getState().imp?.famiglia ?? 'qwen') !== 'qwen' + +export function apriInModifica(f: FotoBase, aggiungi = false): void { const s = usaStato.getState() + if (conKlein()) { + const altre = s.crea.riferimenti.filter((x) => x.percorso !== f.percorso) + s.setCrea({ riferimenti: (aggiungi ? [...altre, f] : [f, ...(s.crea.misuraLibera ? altre : [])]).slice(0, 10), misuraLibera: false }) + s.apriVisore(null) + s.vai('crea') + return + } s.setModifica({ base: f, versioni: [f] }) s.apriVisore(null) s.vai('modifica') @@ -83,6 +93,7 @@ export function apriInModifica(f: FotoBase): void { /** apre la foto in Modifica direttamente sul modo Estrai (persone e oggetti) */ export function apriInEstrai(f: FotoBase): void { + if (conKlein()) return apriInModifica(f) const s = usaStato.getState() s.setModifica({ base: f, versioni: [f], modo: 'estrai' }) s.apriVisore(null) diff --git a/src/renderer/src/componenti/InterruttoreLLM.tsx b/src/renderer/src/componenti/InterruttoreLLM.tsx new file mode 100644 index 0000000..1c6f214 --- /dev/null +++ b/src/renderer/src/componenti/InterruttoreLLM.tsx @@ -0,0 +1,31 @@ +// «L'LLM legge il prompt»: un interruttore per modello (Klein di serie acceso, Qwen spento). Sta sotto la casella del prompt di Crea e di Modifica. +// Con lui acceso, prima di disegnare l'LLM (Qwen3-VL) guarda le foto e riscrive la richiesta in una descrizione completa in inglese: BFL dice di non +// usare Klein senza (con i prompt corti i cambi di persona non riescono), e anche con Qwen aiuta le modifiche difficili. +import type { JSX } from 'react' +import { potenziaPer } from '@shared/modelli' +import { api } from '../api' +import { ricaricaImp, usaStato } from '../stato' +import { Interruttore } from './Controlli' + +export function InterruttoreLLM(): JSX.Element | null { + const imp = usaStato((s) => s.imp) + if (!imp) return null + const fam = imp.famiglia + const acceso = potenziaPer(imp, fam) + const cambia = async (v: boolean): Promise => { + await api.impostazioni.salva(fam === 'qwen' ? { potenziaQwen: v } : { potenziaKlein: v }) + await ricaricaImp() + } + return ( +
+ void cambia(v)} + sotto={acceso ? (fam === 'qwen' ? "L'LLM guarda le foto e riscrive la richiesta (qualche secondo in più)" : "Klein rende molto meglio con descrizioni complete: lo fa l'LLM, guardando le foto") : 'Il modello riceve il tuo testo com\'è'} + titolo="Prima di disegnare, l'LLM legge il prompt (e guarda le foto) e lo riscrive in una descrizione completa in inglese" + > + L'LLM legge il prompt + +
+ ) +} diff --git a/src/renderer/src/componenti/ScegliModello.tsx b/src/renderer/src/componenti/ScegliModello.tsx index 857d342..f3d3935 100644 --- a/src/renderer/src/componenti/ScegliModello.tsx +++ b/src/renderer/src/componenti/ScegliModello.tsx @@ -1,6 +1,6 @@ -// 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. +// Il selettore del modello che disegna, nella barra in alto dell'app (si sceglie solo da lì, da qualsiasi pagina): Qwen, Qwen veloce, Klein e Klein Base da ORDINE_MODELLI. +// Salva `modelloScelto` (la famiglia si allinea da sola nel main). Se i file che servono (Klein, o il LoRA Lightning di Qwen veloce) 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' import type { Download } from '@shared/tipi' import { MODELLI, ORDINE_MODELLI, modelloDaImpostazioni, type IdModello } from '@shared/modelli' @@ -9,26 +9,32 @@ import { ricaricaImp, usaStato } from '../stato' import { I } from './Icone' /** 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'] } -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' } +const SERVONO: Record, string[]> = { 'qwen-veloce': ['lora-lightning'], klein: ['klein-int8', 'klein-te', 'klein-vae'], 'klein-base': ['klein-base-int8', 'klein-te', 'klein-vae'] } +const PESO: Record, string> = { 'qwen-veloce': 'circa 67 MB', klein: 'circa 10 GB', 'klein-base': 'circa 10 GB (4,7 se hai già Klein)' } +const ETICHETTA: Record = { 'qwen-base': 'Qwen', 'qwen-veloce': 'Qwen veloce', klein: 'Klein 3', 'klein-base': 'Klein 3 Base' } +const COSA_SERVE: Record, string> = { + 'qwen-veloce': 'Serve il LoRA Lightning', + klein: 'Servono il modello (INT8, adatto a 8 GB), il text encoder e il VAE', + 'klein-base': 'Servono il modello (INT8, adatto a 8 GB), il text encoder e il VAE' +} 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 }) + const [pronti, setPronti] = useState>({ 'qwen-base': true, 'qwen-veloce': 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) - /** i file di Klein ci sono? (si guardano le cartelle: vale anche per chi li ha messi a mano) */ + /** i file di Klein e il LoRA Lightning ci sono? (si guardano le cartelle: vale anche per chi li ha messi a mano) */ const ricarica = async (): Promise => { const f = await api.modelli.file() const comuni = (f.kleinTe?.length ?? 0) > 0 && (f.kleinVae?.length ?? 0) > 0 setPronti({ 'qwen-base': true, + 'qwen-veloce': !!f.lightning, klein: comuni && !!f.klein?.some((n) => !/base/i.test(n)), 'klein-base': comuni && !!f.klein?.some((n) => /base/i.test(n)) }) @@ -68,7 +74,7 @@ export function SelettoreModello(): JSX.Element { 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', 'klein-te': 'il text encoder', 'klein-vae': 'il VAE' } + const nomi: Record = { 'lora-lightning': 'il LoRA Lightning', '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 }) @@ -111,7 +117,7 @@ export function SelettoreModello(): JSX.Element { {aperto && aperto !== 'qwen-base' && !pronti[aperto] && (
{MODELLI[aperto].nome} -
{MODELLI[aperto].sotto}. Servono il modello (INT8, adatto a 8 GB), il text encoder e il VAE: {PESO[aperto]}, una volta sola.
+
{MODELLI[aperto].sotto}. {COSA_SERVE[aperto]}: {PESO[aperto]}, una volta sola.
{scarico?.id === aperto ? (
Scarico {scarico.passo}… {scarico.perc}%
) : ( diff --git a/src/renderer/src/orbita.css b/src/renderer/src/orbita.css index f753593..fe78fa1 100644 --- a/src/renderer/src/orbita.css +++ b/src/renderer/src/orbita.css @@ -362,3 +362,13 @@ body.meno-movimento .palco::before { animation: none; } .impostazioni details.avanzate[open] > summary::before { content: '▾ '; } .impostazioni details.avanzate[open] > summary { margin-bottom: 14px; } .impostazioni .voce .distintivo { justify-self: start; } + +/* ---------- 1.4: l'interruttore dell'LLM sotto il prompt ---------- */ +.llm-int { margin-top: 10px; padding: 8px 10px; border-radius: 12px; border: 1px solid var(--bordo-vetro); background: rgba(255,255,255,.03); } +.llm-int[data-llm='si'] { border-color: rgba(168, 120, 255, .45); background: rgba(138, 92, 246, .08); } +.llm-int .interruttore { padding: 0; } +.llm-int .et { font-size: 13.5px; font-weight: 600; } +.llm-int .et small { font-weight: 400; margin-top: 2px; } +.llm-scritto { margin: 6px 14px 0; font-size: 12.5px; color: var(--tenue); } +.llm-scritto summary { cursor: pointer; user-select: none; } +.llm-scritto p { margin: 6px 0 0; max-height: 120px; overflow: auto; user-select: text; color: var(--testo2, var(--tenue)); line-height: 1.45; } diff --git a/src/renderer/src/pagine/Crea.tsx b/src/renderer/src/pagine/Crea.tsx index cec930a..6175546 100644 --- a/src/renderer/src/pagine/Crea.tsx +++ b/src/renderer/src/pagine/Crea.tsx @@ -18,6 +18,7 @@ import { Hud } from '../componenti/Hud' import { SceltaLora } from '../componenti/SceltaLora' import { Riferimenti } from '../componenti/Riferimenti' import { Casella } from '../componenti/Casella' +import { InterruttoreLLM } from '../componenti/InterruttoreLLM' import { durata, normalizza, stimaSecondi, FORMATI } from '../util' import { SISTEMA_DESCRIVI, SISTEMA_IDEA, SISTEMA_MIGLIORA, apriInEstrai, apriInModifica, fotoDaOpera, richiestaBase, rifaiCon, rifacibileIn, rifinisci, riusa, varia, ingrandisci } from '../azioni' import { presetDa } from '../preset' @@ -56,6 +57,11 @@ export function Crea(): JSX.Element { const preset = presetDa(crea.preset) const stima = stimaSecondi(imp?.tempi, (lav.w * lav.h) / 1048576, passi) * quante const qwen = (imp?.famiglia || 'qwen') === 'qwen' + // Klein ha un'interfaccia più semplice: niente generi, stili, box e ricette. Una sola pagina: scrivi, e se vuoi aggiungi foto + // (la prima è quella che cambia, le altre sono riferimenti). Senza foto disegna da zero. + const klein = !qwen + const conFoto = klein && crea.riferimenti.length > 0 + const comeLaFoto = conFoto && !crea.misuraLibera // i lavori di questa pagina (creati, rifiniti, variati) const miei = useMemo(() => lavori.filter((l) => ['crea', 'rifinisci', 'varia'].includes(l.richiesta.modalita)), [lavori]) @@ -86,6 +92,9 @@ export function Crea(): JSX.Element { avvisa('Scrivi prima cosa vuoi vedere', 'errore') return } + // Klein con foto, misura «come la foto 1»: è una modifica (esce grande come la prima foto, a ~1 MP al massimo) + const prima = crea.riferimenti[0] + const mpFoto = prima ? Math.round(Math.max(0.3, Math.min(1, (prima.larghezza * prima.altezza) / 1048576)) * 100) / 100 : 1 const q = { ...richiestaBase(), modalita: 'crea' as const, @@ -108,6 +117,7 @@ export function Crea(): JSX.Element { trasparente: crea.sfondo === 'trasparente', sfondo: sfondoRichiesto(crea), ...parametriModello(modello.id), + ...(comeLaFoto && prima ? { modalita: 'modifica' as const, megapixel: mpFoto, larghezza: prima.larghezza, altezza: prima.altezza, uscita: undefined } : {}), etichetta: preset ? preset.nome : stileDa(crea.stileId)?.nome } await api.lavori.accoda(q) @@ -202,8 +212,17 @@ export function Crea(): JSX.Element {
COSA COSTRUIAMO OGGI?
-

Scrivi a sinistra cosa vuoi vedere e premi Crea (Ctrl+Invio).

-

Puoi accodare quante immagini vuoi: partono una dopo l'altra.

+ {klein ? ( + <> +

Scrivi a destra cosa vuoi vedere e premi Crea (Ctrl+Invio).

+

Per cambiare una foto, o unire persone e cose di più foto, aggiungile sotto il testo: Klein fa tutto da solo.

+ + ) : ( + <> +

Scrivi a sinistra cosa vuoi vedere e premi Crea (Ctrl+Invio).

+

Puoi accodare quante immagini vuoi: partono una dopo l'altra.

+ + )} {recenti.length > 0 && (
Riparti da una delle ultime (rimette prompt e impostazioni) @@ -218,15 +237,16 @@ export function Crea(): JSX.Element { )}
)} - + {/* con foto «come la foto 1» la misura la decide la foto: il riquadro con 1024 × 1024 non direbbe il vero */} + {!comeLaFoto && }
{op && !mostraInCorso && (
- - + + {!klein && } {rifacibileIn(op).includes('klein-base') && } - {rifacibileIn(op).includes('qwen-base') && } + {rifacibileIn(op).includes('qwen-base') && } @@ -238,6 +258,12 @@ export function Crea(): JSX.Element { {op.larghezza}×{op.altezza} · seed {op.seed} · {durata(op.durata)}
)} + {op && !mostraInCorso && op.promptInglese && ( +
+ Quello che ha visto il modello (in inglese, scritto dall'LLM o tradotto) +

{op.promptInglese}

+
+ )}
{miei.filter((l) => l.stato === 'in corso' || l.stato === 'in coda').map((l) => (
setVedoInCorso(true)}> @@ -259,6 +285,8 @@ export function Crea(): JSX.Element {
+ {!klein && ( + <> {preset && (

Genere {preset.nome}

@@ -295,6 +323,7 @@ export function Crea(): JSX.Element { )}
+
@@ -342,6 +371,73 @@ export function Crea(): JSX.Element { + + )} + + {klein && ( + <> +
+

+ {conFoto ? 'Cosa cambio?' : 'Cosa vuoi vedere'} + {crea.prompt.length > 0 && `${crea.prompt.length} car.`} +

+ setCrea({ prompt: p })} + minimo={110} + disabled={scrivendo} + /> + +
+ +
+ setCrea({ riferimenti })} + inizio={1} + max={10} + inserisci={inserisci} + esempio="Aggiungi una foto per cambiarla; due o più per unire persone, vestiti e oggetti. La prima è quella che cambia, le altre sono riferimenti." + /> +
+ + {comeLaFoto ? ( +
+

Misura come la foto 1

+
+ Esce grande come la prima foto. + +
+
+ ) : ( +
+

Formato {finale.w} × {finale.h} px

+ +

Misura lato corto {Math.min(finale.w, finale.h)} px

+ + {conFoto && } +
+ )} + + 1 ? `${quante} immagini` : 'quante, seed'}> +

Quante {quante > 1 ? `${quante}, una dopo l'altra` : ''}

+ + {!crea.casuale && ( +
+ Seed + setCrea({ seed: Math.max(0, Number(e.target.value) || 0) })} style={{ width: 130 }} /> +
+ )} +
+ + + + + + )}
@@ -353,7 +449,7 @@ export function Crea(): JSX.Element { {crea.riferimenti.length > 0 && } {nLora > 0 && }
- + {!comeLaFoto && }
{voceInterruttore('Traduci in inglese', 'I prompt in italiano si traducono da soli', imp.traduci, (traduci) => salva({ traduci }))} - {voceInterruttore('Klein: arricchisci il prompt', 'Klein rende meglio con descrizioni lunghe (circa 5 s in più)', imp.potenziaKlein ?? true, (potenziaKlein) => salva({ potenziaKlein }))} + {voceInterruttore("Klein: l'LLM legge il prompt", 'Lo riscrive guardando le foto: Klein ne ha bisogno (BFL lo chiede)', imp.potenziaKlein ?? true, (potenziaKlein) => salva({ potenziaKlein }))} + {voceInterruttore("Qwen: l'LLM legge il prompt", 'Aiuta le modifiche difficili; qualche secondo in più', imp.potenziaQwen ?? false, (potenziaQwen) => salva({ potenziaQwen }))} {voceInterruttore('Anteprima dal vivo', "Vedi l'immagine nascere", imp.anteprimaLive, (anteprimaLive) => salva({ anteprimaLive }))}
Libera la scheda video dopoSe non fai niente per un po'
@@ -323,8 +325,8 @@ function OpzioniGenerali(): JSX.Element { salva({ cacheKV })} voci={[{ id: 'auto', nome: 'Auto' }, { id: 'gpu', nome: 'GPU' }, { id: 'cpu', nome: 'RAM' }, { id: 'off', nome: 'Spenta' }]} />
-
Precisione della cacheint8 dimezza la memoria
- salva({ cacheTipo })} voci={[{ id: 'default', nome: 'Piena' }, { id: 'int8', nome: 'int8' }, { id: 'int4', nome: 'int4' }]} /> +
Precisione della cache KVQ4 = un quarto della memoria, circa il 10% più veloce, stessa qualità a occhio
+ salva({ cacheTipo })} voci={[{ id: 'default', nome: 'Piena' }, { id: 'int8', nome: 'Q8' }, { id: 'int4', nome: 'Q4' }]} />
{voceInterruttore('Text encoder sul processore', 'Lascia la VRAM al disegno; il prompt si legge più piano', imp.encoderSuCpu, (encoderSuCpu) => salva({ encoderSuCpu }))}
diff --git a/src/renderer/src/pagine/Studio.tsx b/src/renderer/src/pagine/Studio.tsx index 6ccc80e..00c0b32 100644 --- a/src/renderer/src/pagine/Studio.tsx +++ b/src/renderer/src/pagine/Studio.tsx @@ -22,6 +22,11 @@ export function modoStudio(): ModoStudio | null { /** apre un modo dello Studio */ export function vaiModo(modo: ModoStudio): void { const s = usaStato.getState() + // con Klein c'è solo Crea + if ((s.imp?.famiglia ?? 'qwen') !== 'qwen') { + s.vai('crea') + return + } if (modo === 'ricette') { s.setModifica({ modo: 'ricette' }) s.vai('modifica') diff --git a/src/renderer/src/pagine/modifica/SchedaScrivi.tsx b/src/renderer/src/pagine/modifica/SchedaScrivi.tsx index c664679..9a4c304 100644 --- a/src/renderer/src/pagine/modifica/SchedaScrivi.tsx +++ b/src/renderer/src/pagine/modifica/SchedaScrivi.tsx @@ -5,6 +5,7 @@ import type { PosizioneCamera } from '../../componenti/Camera' import { usaStato, type FotoBase } from '../../stato' import { I } from '../../componenti/Icone' import { Casella } from '../../componenti/Casella' +import { InterruttoreLLM } from '../../componenti/InterruttoreLLM' import { SchedaIdee } from './SchedaIdee' import { usaSpeciale } from '../../workflow' import type { Rapida, CategoriaRapide } from '../../rapide' @@ -89,6 +90,8 @@ function SchedaTesto({ prompt, base, griglia, zona, scrivendo, migliora, setCame ))}
+ {/* i modi speciali (Unisci foto, Come l'esempio) hanno già un LLM che pianifica: l'interruttore serve alle modifiche scritte a mano */} + {!fusion && !esempioMode && m.modo !== 'espandi' && } {mancaRif && !griglia && (
Serve {rapida?.rifNome || 'una foto di riferimento'}: aggiungila qui sotto in Riferimenti (nel testo è <image2>). diff --git a/src/renderer/src/stato.ts b/src/renderer/src/stato.ts index 3df5a05..9a0a738 100644 --- a/src/renderer/src/stato.ts +++ b/src/renderer/src/stato.ts @@ -1,7 +1,7 @@ // Lo stato dell'interfaccia (zustand). Le scelte di Crea/Modifica e i LoRA attivi restano fra un avvio e l'altro. import { create } from 'zustand' import { FAMIGLIE, type BoxRegione, type Download, type Famiglia, type GruppoLora, type Impostazioni, type InfoLora, type InfoMotore, type Lavoro, type LoraAttiva, type Opera, type Bordi, type Risorse, type StatoAggiornamento } from '@shared/tipi' -import { senzaTurbo } from '@shared/modelli' +import { MODELLI, modelloDaImpostazioni, senzaTurbo, type IdModello } from '@shared/modelli' import { gruppoDa, gruppoDiFamiglia } from '@shared/loraBase' import { api } from './api' import type { Misura } from './componenti/Misura' @@ -19,8 +19,10 @@ export const PASSI_QUALITA: Record = { bozza: 20, alta: 40, mas /** il modello che disegna adesso (Qwen, Klein veloce, Klein Base) */ export const famigliaCorrente = (): Famiglia => usaStato.getState().imp?.famiglia || 'qwen' -/** i passi per una qualità col modello scelto (il Turbo di Qwen non c'è più) */ -export const passiDi = (q: Qualita, f: Famiglia = famigliaCorrente()): number => FAMIGLIE[f].passi[q] +/** il modello scelto in alto (Qwen, Qwen veloce, Klein, Klein Base) */ +export const modelloCorrente = (): IdModello => modelloDaImpostazioni(usaStato.getState().imp) +/** i passi per una qualità col modello scelto (Qwen veloce ha sempre i suoi 4: è il LoRA Lightning) */ +export const passiDi = (q: Qualita, f: Famiglia = famigliaCorrente()): number => (f === 'qwen' && modelloCorrente() === 'qwen-veloce' ? MODELLI['qwen-veloce'].passi : FAMIGLIE[f].passi[q]) export interface FotoBase { percorso: string @@ -57,6 +59,8 @@ export interface StatoCrea { sfondo: 'scena' | 'trasparente' | 'tinta' tinta: string anteSfondo: 'scacchi' | 'bianco' | 'nero' | 'colore' + /** Klein con foto: false = la misura è quella della prima foto (come nei flussi ufficiali), true = scelgo formato e misura io (si crea una foto nuova con quei riferimenti) */ + misuraLibera?: boolean } /** L'Editor box: i riquadri disegnati sulla tela di Crea (stesso formato, stessa misura) e la scena generale */ diff --git a/src/shared/modelli.ts b/src/shared/modelli.ts index 808323b..0d6ea52 100644 --- a/src/shared/modelli.ts +++ b/src/shared/modelli.ts @@ -1,21 +1,21 @@ -// I modelli che si scelgono nell'interfaccia: due famiglie, tre voci (D1 del piano, rivista da Cammo il 21/10/2026). -// qwen-base = Qwen-Image 2.1 pieno, 40 passi, coi LoRA: la qualità piena di Qwen. -// klein = FLUX 3 Klein distillato, 16 passi senza CFG (quelli indicati da BFL; fino alla 1.2.1 erano 8): il modello veloce. -// klein-base = FLUX 3 Klein Base, 50 passi con CFG 3,5: la qualità piena di Klein. -// Niente Qwen veloce col LoRA Turbo8: a Cammo non piace. Dalla 1.2.1 niente FLUX.2 Klein 4B: sono rimasti Qwen e FLUX 3 Klein. +// I modelli che si scelgono nell'interfaccia: due famiglie, quattro voci. +// qwen-base = Qwen-Image 2.1 pieno, 25 passi (quelli dei workflow ufficiali di ComfyUI; fino alla 1.3 erano 40), coi LoRA. +// qwen-veloce = Qwen-Image 2.1 col LoRA Lightning di LightX2V (distillato a 4 passi, CFG 1): misurato sulla 4060, 1 MP in 15 s invece di 73. +// klein = FLUX 3 Klein distillato, 16 passi senza CFG (quelli indicati da BFL): il modello veloce di Klein. +// klein-base = FLUX 3 Klein Base, 50 passi con CFG 3,5: la qualità piena di Klein. +// (Il vecchio Turbo8 a 8 passi non c'è più; dalla 1.2.1 niente FLUX.2 Klein 4B.) // -// Il motore continua a ragionare per `Famiglia` (+ `turbo`, che resta nella Richiesta solo per i PNG e le ricette vecchie): -// questa è l'unica mappa fra le due cose. È pura (niente Electron, niente React), così `test/modelli.mjs` la prova con Node. -// I grafi di Qwen non cambiano: qwen-base manda la stessa richiesta di prima con la qualità «Alta». +// Il motore ragiona per `Famiglia` + `turbo` (= il LoRA Lightning acceso, `Impostazioni.loraTurbo`): questa è l'unica mappa fra le due cose. +// È pura (niente Electron, niente React), così `test/modelli.mjs` la prova con Node. import type { Famiglia } from './tipi' -export type IdModello = 'qwen-base' | 'klein' | 'klein-base' +export type IdModello = 'qwen-base' | 'qwen-veloce' | 'klein' | 'klein-base' export interface InfoModello { id: IdModello famiglia: Famiglia - /** sempre false: il Turbo di Qwen non c'è più (il campo c'è per dirlo chiaro a chi costruisce la richiesta) */ - turbo: false + /** acceso il LoRA Lightning (4 passi): solo `qwen-veloce` */ + turbo: boolean passi: number /** il CFG che vuole il modello (null = quello delle impostazioni, 1 per Qwen) */ cfg: number | null @@ -29,13 +29,14 @@ export interface InfoModello { } export const MODELLI: Record = { - 'qwen-base': { id: 'qwen-base', famiglia: 'qwen', turbo: false, passi: 40, cfg: null, nome: 'Qwen-Image 2.1', corto: 'Qwen', sotto: '40 passi · LoRA', lora: true, veloce: false }, + 'qwen-base': { id: 'qwen-base', famiglia: 'qwen', turbo: false, passi: 25, cfg: null, nome: 'Qwen-Image 2.1', corto: 'Qwen', sotto: '25 passi · LoRA', lora: true, veloce: false }, + 'qwen-veloce': { id: 'qwen-veloce', famiglia: 'qwen', turbo: true, passi: 4, cfg: null, nome: 'Qwen-Image 2.1 Lightning', corto: 'Qwen veloce', sotto: '4 passi · LoRA · 5 volte più veloce', lora: true, veloce: true }, klein: { id: 'klein', famiglia: 'klein', turbo: false, passi: 16, cfg: 1, nome: 'FLUX 3 Klein', corto: 'Klein veloce', sotto: '16 passi · senza CFG · il più veloce', lora: true, veloce: true }, 'klein-base': { id: 'klein-base', famiglia: 'klein-base', turbo: false, passi: 50, cfg: 3.5, nome: 'FLUX 3 Klein Base', corto: 'Klein Base', sotto: '50 passi · CFG 3,5 · la qualità piena', lora: true, veloce: false } } /** l'ordine delle pillole nell'interfaccia */ -export const ORDINE_MODELLI: IdModello[] = ['qwen-base', 'klein', 'klein-base'] +export const ORDINE_MODELLI: IdModello[] = ['qwen-base', 'qwen-veloce', 'klein', 'klein-base'] export const MODELLO_PREDEFINITO: IdModello = 'qwen-base' export const eModello = (x: unknown): x is IdModello => typeof x === 'string' && Object.prototype.hasOwnProperty.call(MODELLI, x) @@ -76,17 +77,28 @@ export function modelloDaOpera(o: { modello?: string | null } | null | undefined } /** quello che il modello mette nella Richiesta: famiglia, passi, niente Turbo (CFG e sampler di Klein li decide il motore) */ -export function parametriModello(id: IdModello): { famiglia: Famiglia; passi: number; turbo: false } { +export function parametriModello(id: IdModello): { famiglia: Famiglia; passi: number; turbo: boolean } { const m = MODELLI[id] - return { famiglia: m.famiglia, passi: m.passi, turbo: false } + return { famiglia: m.famiglia, passi: m.passi, turbo: m.turbo } } -/** «Rifai in qualità piena» (stesso seed, stesso prompt): da Klein veloce si va a Klein Base (il più vicino) o a Qwen base. +/** «Rifai in qualità piena» (stesso seed, stesso prompt): da Klein veloce si va a Klein Base (il più vicino) o a Qwen base, da Qwen veloce a Qwen base. * I modelli già pieni non hanno niente sopra: lista vuota. */ export function inQualitaPiena(id: IdModello): IdModello[] { + if (id === 'qwen-veloce') return ['qwen-base'] return MODELLI[id].veloce ? ['klein-base', 'qwen-base'] : [] } +/** il LoRA Lightning (4 passi) di LightX2V: il file e quanto vale lo spostamento della scaletta del rumore (max_shift, come nella scheda del modello) */ +export const LIGHTNING_FILE = 'qwen-image-2.1-lightning-4step.safetensors' +export const SHIFT_LIGHTNING = 0.9 +/** il vecchio Turbo8 (8 passi) usava 0,6935: resta per i PNG e le ricette vecchie */ +export const SHIFT_TURBO8 = 0.6935 +export const shiftDi = (file: string): number => (/turbo8/i.test(file) ? SHIFT_TURBO8 : SHIFT_LIGHTNING) + +/** «L'LLM legge il prompt prima di disegnare» (`potenziaKlein` / `potenziaQwen` nelle impostazioni): ognuno dei due modelli ha il suo interruttore */ +export const potenziaPer = (imp: { potenziaKlein?: boolean; potenziaQwen?: boolean }, fam: Famiglia): boolean => (fam === 'qwen' ? !!imp.potenziaQwen : imp.potenziaKlein ?? true) + /** le qualità salvate (localStorage, ricette tue, lavori vecchi): «turbo» non c'è più e diventa «alta» */ export function senzaTurbo(q: Q | 'turbo'): Exclude | 'alta' { return (q === 'turbo' ? 'alta' : q) as Exclude | 'alta' diff --git a/src/shared/tipi.ts b/src/shared/tipi.ts index 5903401..c24adc5 100644 --- a/src/shared/tipi.ts +++ b/src/shared/tipi.ts @@ -85,7 +85,7 @@ const famiglia = (id: IdModello, bozza: number, massima: number): { nome: string return { nome: m.nome, corto: m.corto, sotto: m.sotto, passi: { turbo: m.passi, bozza, alta: m.passi, massima }, cfg: m.cfg, lora: m.lora } } export const FAMIGLIE: Record> = { - qwen: famiglia('qwen-base', 20, 50), + qwen: famiglia('qwen-base', 15, 40), klein: famiglia('klein', 8, 24), 'klein-base': famiglia('klein-base', 25, 64) } @@ -267,7 +267,7 @@ export interface Impostazioni { upscaler: string /** la famiglia che disegna adesso (va sempre d'accordo con `modelloScelto`: `allineaScelta` in `modelli.ts`) */ famiglia: Famiglia - /** il modello scelto nell'interfaccia (qwen-base / klein / klein-base); `modello` invece è il file del denoiser di Qwen */ + /** il modello scelto nell'interfaccia (qwen-base / qwen-veloce / klein / klein-base); `modello` invece è il file del denoiser di Qwen */ modelloScelto: IdModello /** i file di FLUX 3 Klein (vuoto = li trova da solo nelle cartelle dei modelli) */ kleinModello: string @@ -291,7 +291,7 @@ export interface Impostazioni { tokenHF: string /** secondi per passo misurati, per megapixel (chiave = mp arrotondati a 0.25) */ tempi: Record - /** il LoRA Turbo (8 passi) dentro models/loras */ + /** il LoRA di distillazione di Qwen dentro models/loras: dalla 1.4 è Lightning (4 passi); prima era il Turbo8 (8 passi) */ loraTurbo: string /** profilo della memoria: auto = dalla VRAM della scheda; bassa = 6 GB; normale = 8-12 GB; alta = 16 GB e più */ memoria: 'auto' | 'bassa' | 'normale' | 'alta' @@ -305,8 +305,12 @@ export interface Impostazioni { traduttore: 'dedicato' | 'llm' /** una cosa alla volta in VRAM: quando passa dal modello immagine all'LLM (o viceversa) scarica quello che non serve */ unaCosaAllaVolta: boolean - /** Klein: espande i prompt brevi in didascalie dettagliate col Qwen3-VL (come l'upsampler ufficiale; +circa mezzo minuto per immagine) */ + /** Klein: l'LLM legge il prompt e lo riscrive in una didascalia dettagliata guardando anche le foto (come l'upsampler ufficiale di BFL, che chiede di non usare Klein senza) */ potenziaKlein: boolean + /** Qwen: lo stesso, a scelta (di serie spento: Qwen capisce bene anche i prompt brevi) */ + potenziaQwen: boolean + /** versione delle impostazioni: serve a fare una volta sola le correzioni di una nuova versione dell'app (1.4: cache KV Q4, LoRA Lightning) */ + migrata?: number /** i lavori di testo (migliora prompt, descrivi foto, Idee, Estrai, traduzione LLM…) scrivono col Qwen3-VL 4B (veloce, tutto in VRAM) anche con Qwen. * Spento di serie: con Qwen scrive il suo encoder 8B. Con Klein il 4B scrive sempre. */ testoCon4B: boolean diff --git a/test/bench-motore.mjs b/test/bench-motore.mjs new file mode 100644 index 0000000..48d84cd --- /dev/null +++ b/test/bench-motore.mjs @@ -0,0 +1,162 @@ +// Misure di velocità (e immagini) sul motore vero: costruisce i grafi come l'app (src/main/motore/grafi.ts), li manda a un ComfyUI che +// gira già (di solito uno a parte, sulla porta 8899, così la tua app non c'entra) e dice quanto ci mette ogni fase. +// +// node test/bench-motore.mjs casi.json [--porta 8899] [--esci cartella] +// +// casi.json = [{ "nome": "qwen-25", "richiesta": { ...campi di Richiesta... }, "imp": { ...impostazioni da cambiare... }, "immagini": ["C:/foto.png"], +// "sistemaDa": "PUP_I2I_BREVE" (il prompt di sistema da src/main/motore/pup.ts), "ripeti": 2 (il seed sale di uno a ogni giro), "ritocca": [{ "classe": "KSampler", "campo": "cfg", "valore": 1.5 } | { "classe": "KSampler", "campo": "model", "avvolgi": "FluxKVCache" }] }] +// Le impostazioni di base sono quelle vere dell'utente (%APPDATA%/DaProd Image/impostazioni.json) più quello che metti in "imp". +// Prima di ogni caso non si scarica niente: il primo giro include il caricamento dei pesi, quindi i casi si ripetono con "ripeti". +import { fileURLToPath, pathToFileURL } from 'node:url' +import { dirname, join, basename } from 'node:path' +import { mkdtempSync, readFileSync, writeFileSync, mkdirSync, copyFileSync, existsSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { buildSync } from 'esbuild' + +const QUI = dirname(fileURLToPath(import.meta.url)) +const arg = (n, d) => { + const i = process.argv.indexOf('--' + n) + return i > 0 ? process.argv[i + 1] : d +} +const PORTA = Number(arg('porta', '8899')) +const ESCI = arg('esci', join(QUI, '.out', 'bench')) +const FILE = process.argv[2] +if (!FILE || FILE.startsWith('--')) { + console.log('uso: node test/bench-motore.mjs casi.json [--porta 8899] [--esci cartella]') + process.exit(1) +} +mkdirSync(ESCI, { recursive: true }) + +const fuori = join(mkdtempSync(join(tmpdir(), 'daprod-bench-')), 'grafi.mjs') +buildSync({ + entryPoints: [join(QUI, '..', 'src', 'main', 'motore', 'grafi.ts')], + bundle: true, + format: 'esm', + platform: 'node', + outfile: fuori, + alias: { '@shared': join(QUI, '..', 'src', 'shared') }, + logLevel: 'error' +}) +const G = await import(pathToFileURL(fuori).href) +// i prompt di sistema dell'upsampler: un caso può dire "sistemaDa": "PUP_I2I_BREVE" invece di ricopiarlo +const fuoriPup = join(mkdtempSync(join(tmpdir(), 'daprod-bench-')), 'pup.mjs') +buildSync({ entryPoints: [join(QUI, '..', 'src', 'main', 'motore', 'pup.ts')], bundle: true, format: 'esm', platform: 'node', outfile: fuoriPup, logLevel: 'error' }) +const PUP = await import(pathToFileURL(fuoriPup).href) + +const IMP_VERE = join(process.env.APPDATA || '', 'DaProd Image', 'impostazioni.json') +const impBase = existsSync(IMP_VERE) ? JSON.parse(readFileSync(IMP_VERE, 'utf8')) : {} +const base = { + modalita: 'crea', prompt: '', negativo: '', seed: 12345, passi: 16, cfg: 1, sampler: 'euler', scheduler: 'simple', megapixel: 1, + larghezza: 1024, altezza: 1024, immagini: [], lora: [], quante: 1, trasparente: false +} +const URL = `http://127.0.0.1:${PORTA}` +const s = (ms) => Math.round(ms / 100) / 10 + +async function carica(percorso) { + const f = new FormData() + f.append('image', new Blob([readFileSync(percorso)]), basename(percorso)) + f.append('overwrite', 'true') + const r = await fetch(`${URL}/upload/image`, { method: 'POST', body: f }) + const j = await r.json() + return j.name +} + +function dimensioniPng(percorso) { + const b = readFileSync(percorso) + return { larghezza: b.readUInt32BE(16), altezza: b.readUInt32BE(20) } +} + +async function esegui(caso, giro) { + const imp = { ...impBase, ...(caso.imp || {}) } + const q = { ...base, ...(caso.richiesta || {}) } + q.seed += giro + if (caso.sistemaDa) q.sistema = PUP[caso.sistemaDa] + const ing = { immagini: [] } + for (const p of caso.immagini || []) { + const nome = await carica(p) + ing.immagini.push({ nome, ...dimensioniPng(p) }) + } + const g = G.costruisci(q, imp, ing, `bench_${caso.nome}`, imp.memoria === 'bassa' ? 'bassa' : 'normale') + // ritocchi al grafo già costruito, per provare un valore senza toccare il codice: [{ "classe": "KSampler", "campo": "cfg", "valore": 1.5 }] + for (const r of caso.ritocca || []) { + for (const n of Object.values(g.prompt)) { + if (n.class_type !== r.classe) continue + // "avvolgi": mette un nodo nuovo fra quello che c'era e questo campo (es. FluxKVCache fra il modello e il KSampler) + if (r.avvolgi) { + const id = String(1000 + Object.keys(g.prompt).length) + g.prompt[id] = { class_type: r.avvolgi, inputs: { [r.campo]: n.inputs[r.campo], ...(r.extra || {}) } } + n.inputs[r.campo] = [id, 0] + } else n.inputs[r.campo] = r.valore + } + } + const idPrompt = crypto.randomUUID() + const ws = new WebSocket(`ws://127.0.0.1:${PORTA}/ws?clientId=${idPrompt}`) + ws.binaryType = 'arraybuffer' + const fasi = {} + let ultimo = null + let t0 = 0 + let passoPrimo = 0 + let passoUltimo = 0 + let passi = 0 + let fatto + const finito = new Promise((ok, ko) => (fatto = { ok, ko })) + const nodo = (n) => g.fasi[n] || `nodo ${n} (${g.prompt[n]?.class_type})` + const chiudiFase = (t) => { + if (ultimo !== null) fasi[ultimo.f] = (fasi[ultimo.f] || 0) + (t - ultimo.t) + } + ws.onmessage = (e) => { + if (typeof e.data !== 'string') return + const m = JSON.parse(e.data) + const t = performance.now() + if (m.type === 'execution_start') t0 = t + if (m.type === 'executing' && m.data.prompt_id === idPrompt) { + chiudiFase(t) + ultimo = m.data.node ? { f: nodo(m.data.node), t } : null + if (!m.data.node) fatto.ok(t) + } + if (m.type === 'progress' && m.data.prompt_id === idPrompt && g.prompt[m.data.node]?.class_type === 'KSampler') { + if (!passoPrimo) passoPrimo = t + passoUltimo = t + passi = m.data.max + } + if (m.type === 'execution_error') fatto.ko(new Error(m.data.exception_message || 'errore del motore')) + } + await new Promise((ok) => (ws.onopen = ok)) + const tInvio = performance.now() + const r = await fetch(`${URL}/prompt`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ prompt: g.prompt, client_id: idPrompt, prompt_id: idPrompt }) }) + if (!r.ok) throw new Error('il motore rifiuta il grafo: ' + (await r.text()).slice(0, 600)) + let tFine + try { + tFine = await finito + } finally { + ws.close() + } + const h = await (await fetch(`${URL}/history/${idPrompt}`)).json() + const usc = h[idPrompt]?.outputs?.[g.uscita] + let file = '' + if (usc?.images?.[0]) { + const im = usc.images[0] + const bin = await fetch(`${URL}/view?filename=${encodeURIComponent(im.filename)}&subfolder=${encodeURIComponent(im.subfolder)}&type=${im.type}`) + file = join(ESCI, `${caso.nome}${giro ? '_' + giro : ''}.png`) + writeFileSync(file, Buffer.from(await bin.arrayBuffer())) + } + if (usc?.text) file = String(usc.text).slice(0, 4000) + const campiona = passi > 1 ? s((passoUltimo - passoPrimo) / (passi - 1)) : null + return { totale: s(tFine - tInvio), fasi: Object.fromEntries(Object.entries(fasi).map(([k, v]) => [k, s(v)])), passi, sPerPasso: campiona, file } +} + +const casi = JSON.parse(readFileSync(FILE, 'utf8')) +const righe = [] +for (const caso of casi) { + for (let giro = 0; giro < (caso.ripeti || 1); giro++) { + try { + const r = await esegui(caso, giro) + righe.push({ caso: caso.nome, giro, ...r }) + console.log(`${caso.nome}#${giro}: ${r.totale} s | passi ${r.passi} a ${r.sPerPasso} s/passo | ${JSON.stringify(r.fasi)}${r.file ? '\n → ' + r.file : ''}`) + } catch (e) { + console.log(`${caso.nome}#${giro}: ERRORE ${e.message}`) + righe.push({ caso: caso.nome, giro, errore: String(e.message) }) + } + } +} +writeFileSync(join(ESCI, 'ultimo.json'), JSON.stringify(righe, null, 2)) diff --git a/test/box-lora-ui.mjs b/test/box-lora-ui.mjs index 8c11690..b989ef2 100644 --- a/test/box-lora-ui.mjs +++ b/test/box-lora-ui.mjs @@ -13,7 +13,7 @@ const lore = [ await provaBrowser(async (pg) => { // ---- 1. il modello si sceglie dalla barra in alto: Qwen, Klein 3 e Klein 3 Base, niente Klein 4B ---- const pillole = pg.locator('.barra-titolo [data-selettore-modello] [data-modello]') - assert.deepEqual(await pillole.evaluateAll((b) => b.map((x) => x.getAttribute('data-modello'))), ['qwen-base', 'klein', 'klein-base']) + assert.deepEqual(await pillole.evaluateAll((b) => b.map((x) => x.getAttribute('data-modello'))), ['qwen-base', 'qwen-veloce', 'klein', 'klein-base']) assert.equal(await pg.locator('[data-modello="klein4b"], [data-id="modello-klein4b"]').count(), 0, 'niente Klein 4B') for (const modo of ['crea', 'modifica', 'box']) { await pg.click(`button[data-modo="${modo}"]`) diff --git a/test/browser-prova.mjs b/test/browser-prova.mjs index 705c832..a34ff4d 100644 --- a/test/browser-prova.mjs +++ b/test/browser-prova.mjs @@ -29,7 +29,7 @@ export async function provaBrowser(fn, init) { await pg.addInitScript(await readFile('test/finto.js', 'utf8')) if (init) await pg.addInitScript(init) await pg.goto(server.url) - await pg.locator('.strip-modi').waitFor() + await pg.locator('.studio-corpo').waitFor() await fn(pg) if (errori.length) throw Error('Errori pagina: ' + errori.join('\n')) } finally { await browser?.close(); await server.close() } diff --git a/test/finto.js b/test/finto.js index 8c1f0df..f164734 100644 --- a/test/finto.js +++ b/test/finto.js @@ -26,14 +26,15 @@ const asc = {} const su = new Proxy({}, { get: (_, k) => (f) => { (asc[k] ||= []).push(f); return () => {} } }) const emetti = (k, ...a) => (asc[k] || []).forEach((f) => f(...a)) - const imp = { cartellaModelli: 'C:\\m', cartellaGalleria: 'C:\\g', modello: 'qwen-image-2.1-UC-Q4_K_M.gguf', famiglia: 'qwen', modelloScelto: 'qwen-base', encoder: 'qwen3vl_8b_int8_convrot.safetensors', vae: 'texture_fix.safetensors', upscaler: '4x-UltraSharp.pth', porta: 8818, cacheKV: 'auto', cacheTipo: 'default', encoderSuCpu: false, anteprimaLive: true, riservaVram: 0, argomentiExtra: '', predefiniti: { passi: 40, cfg: 1, sampler: 'euler', scheduler: 'simple', megapixel: 1 }, installato: true, tokenCivitai: '', tokenHF: '', tempi: { '1.00': 3.1 }, loraTurbo: 'turbo8_lora_step2500.safetensors', memoria: 'auto', vramMB: 6144, veloce: false } + const imp = { cartellaModelli: 'C:\\m', cartellaGalleria: 'C:\\g', modello: 'qwen-image-2.1-UC-Q4_K_M.gguf', famiglia: 'qwen', modelloScelto: 'qwen-base', encoder: 'qwen3vl_8b_int8_convrot.safetensors', vae: 'texture_fix.safetensors', upscaler: '4x-UltraSharp.pth', porta: 8818, cacheKV: 'auto', cacheTipo: 'int4', encoderSuCpu: false, anteprimaLive: true, riservaVram: 0, argomentiExtra: '', predefiniti: { passi: 25, cfg: 1, sampler: 'euler', scheduler: 'simple', megapixel: 1 }, installato: true, tokenCivitai: '', tokenHF: '', tempi: { '1.00': 3.1 }, loraTurbo: 'qwen-image-2.1-lightning-4step.safetensors', potenziaKlein: true, potenziaQwen: false, memoria: 'auto', vramMB: 6144, veloce: false } let n = 0 const v = (x) => async () => x window.daprod = { - impostazioni: { leggi: v(imp), salva: async (m) => Object.assign(imp, m) }, + // leggi() dà una copia: con lo stesso oggetto lo store non si accorge del cambio (come nel vero main, che ogni volta manda JSON nuovo) + impostazioni: { leggi: async () => ({ ...imp }), salva: async (m) => Object.assign(imp, m, m.modelloScelto ? { famiglia: m.modelloScelto === 'klein' || m.modelloScelto === 'klein-base' ? m.modelloScelto : 'qwen' } : {}) }, sistema: { controllo: v({}) }, setup: { cerca: v([]), stato: v({ installato: true, motore: true, modelli: [] }), avvia: v(true), annulla: v() }, - modelli: { stato: v([]), segmentatori: v([]), inutilizzati: v([]), elimina: v(), file: v({ diffusione: [imp.modello], encoder: [imp.encoder], vae: [imp.vae], upscaler: [imp.upscaler], klein: ['flux3_image_klein_sd_int8_convrot.safetensors', 'flux3_image_klein_base_int8_convrot.safetensors'], kleinTe: ['qwen3vl_4b_int8_convrot.safetensors'], kleinVae: ['flux2-vae.safetensors'] }), scarica: v({ stato: 'errore', errore: 'finto' }), annulla: v() }, + modelli: { stato: v([]), segmentatori: v([]), inutilizzati: v([]), elimina: v(), file: v({ diffusione: [imp.modello], encoder: [imp.encoder], vae: [imp.vae], upscaler: [imp.upscaler], klein: ['flux3_image_klein_sd_int8_convrot.safetensors', 'flux3_image_klein_base_int8_convrot.safetensors'], kleinTe: ['qwen3vl_4b_int8_convrot.safetensors'], kleinVae: ['flux2-vae.safetensors'], lightning: true }), scarica: v({ stato: 'errore', errore: 'finto' }), annulla: v() }, motore: { info: v({ stato: 'pronto', porta: 8818, vramTotale: 6442450944, vramLibera: 1500000000, gpu: 'NVIDIA GeForce RTX 3060 Laptop GPU', versione: '0.39.0' }), avvia: v(), riavvia: v(), ferma: v(), log: v(['motore pronto']), libera: v(), apriComfy: v() }, lavori: { elenco: v([]), annulla: v(), svuota: v(), pulisci: v(), diff --git a/test/grafi_v2.mjs b/test/grafi_v2.mjs index 276c8e9..606b9e3 100644 --- a/test/grafi_v2.mjs +++ b/test/grafi_v2.mjs @@ -96,5 +96,35 @@ const rq = costruisci({ modalita: 'crea', famiglia: 'qwen', passi: 20, regioni } uguale('Qwen + riquadri: a parole', /Compose the image with these elements/.test(testoDi(rq)) && !testoDi(rq).includes('bbox'), true) uguale('senza riquadri il prompt è quello', testoDi(costruisci({ modalita: 'crea', famiglia: 'klein', passi: 8 })), base.prompt) +// --- 1.4: Qwen veloce (LoRA Lightning a 4 passi): la scaletta spostata (max_shift 0,9) in TUTTE le modalità, non solo in Crea --- +const impL = { ...imp, loraTurbo: 'qwen-image-2.1-lightning-4step.safetensors' } +const turbo = { turbo: true, passi: 4 } +const shift = (g) => nodi(g, 'ModelSamplingFlux').map((n) => [n.inputs.max_shift, n.inputs.base_shift]) +const conLightning = (q, ing = ing1) => costruisci({ ...base, ...q, ...turbo }, impL, ing) +uguale('Lightning: il LoRA a forza 1', nodi(conLightning({ modalita: 'crea', famiglia: 'qwen' }), 'LoraLoaderModelOnly').map((n) => [n.inputs.lora_name, n.inputs.strength_model]), [['qwen-image-2.1-lightning-4step.safetensors', 1]]) +uguale('Lightning Crea: shift 0,9', shift(conLightning({ modalita: 'crea', famiglia: 'qwen' })), [[0.9, 0.5]]) +uguale('Lightning Modifica: shift 0,9', shift(conLightning({ modalita: 'modifica', famiglia: 'qwen', immagini: ['a.png'] })), [[0.9, 0.5]]) +uguale('Lightning Varia: shift 0,9', shift(conLightning({ modalita: 'varia', famiglia: 'qwen', immagini: ['a.png'] })), [[0.9, 0.5]]) +uguale('Lightning Zona: shift 0,9', shift(conLightning({ modalita: 'zona', famiglia: 'qwen', immagini: ['a.png'], maschera: 'm.png', riquadro: { x: 10, y: 10, w: 50, h: 50 } }, { ...ing1, maschera: 'm.png' })), [[0.9, 0.5]]) +uguale('senza Turbo niente ModelSamplingFlux', shift(costruisci({ ...base, modalita: 'modifica', famiglia: 'qwen', immagini: ['a.png'] }, impL, ing1)), []) +uguale('Klein non prende il Lightning', [nodi(costruisci({ ...base, modalita: 'crea', famiglia: 'klein', ...turbo }, impL), 'ModelSamplingFlux').length, nodi(costruisci({ ...base, modalita: 'crea', famiglia: 'klein', ...turbo }, impL), 'LoraLoaderModelOnly').length], [0, 0]) +uguale('la cache KV di Qwen col tipo scelto', nodi(costruisci({ ...base, modalita: 'crea', famiglia: 'qwen' }, { ...imp, cacheTipo: 'int4' }), 'QwenImage21Cache').map((n) => n.inputs.dtype), ['int4']) + +// --- 1.4: negativo di Klein = positivo azzerato (come BFL); se lo scrivi tu, quello --- +const kz = costruisci({ ...base, modalita: 'crea', famiglia: 'klein' }) +uguale('Klein: negativo = ConditioningZeroOut del positivo', [nodi(kz, 'ConditioningZeroOut').length, nodi(kz, 'CLIPTextEncode').length], [1, 1]) +const kn = costruisci({ ...base, modalita: 'crea', famiglia: 'klein-base', negativo: 'blurry' }) +uguale('Klein Base: negativo scritto da te', [nodi(kn, 'ConditioningZeroOut').length, nodi(kn, 'CLIPTextEncode').map((n) => n.inputs.text).sort()], [0, ['a red fox in a forest', 'blurry']]) + +// --- 1.4: l'LLM che scrive il prompt guarda TUTTE le foto (BatchImagesNode → TextGenerate) --- +const desc = (immagini) => costruisci({ ...base, modalita: 'descrivi', famiglia: 'klein', encoderKlein: true, prompt: 'x', sistema: 's', maxToken: 100, immagini: [] }, imp, { immagini }) +const foto = (n, l = 1024, a = 1024) => ({ nome: n, larghezza: l, altezza: a }) +const d1 = desc([foto('a.png')]) +uguale('descrivi con una foto: LoadImage diretto, niente lotto', [nodi(d1, 'BatchImagesNode').length, nodi(d1, 'LoadImage').length], [0, 1]) +const d3 = desc([foto('a.png', 1024, 1024), foto('b.png', 800, 1200), foto('c.png', 500, 500)]) +uguale('descrivi con tre foto: un lotto di tre, alla forma della prima', [nodi(d3, 'BatchImagesNode').length, Object.keys(nodi(d3, 'BatchImagesNode')[0].inputs).sort(), nodi(d3, 'ImageScale').every((n) => n.inputs.width === nodi(d3, 'ImageScale')[0].inputs.width && n.inputs.height === nodi(d3, 'ImageScale')[0].inputs.height)], [1, ['images.image0', 'images.image1', 'images.image2'], true]) +uguale('descrivi: il lotto va al TextGenerate', nodi(d3, 'TextGenerate')[0].inputs.image[0], Object.keys(d3.prompt).find((k) => d3.prompt[k].class_type === 'BatchImagesNode')) +uguale('descrivi senza foto: niente image', 'image' in nodi(desc([]), 'TextGenerate')[0].inputs, false) + console.log(falliti ? `${falliti} FALLITI su ${fatti}` : `TUTTI OK (${fatti} controlli)`) process.exit(falliti ? 1 : 0) diff --git a/test/gui-v2-flussi.mjs b/test/gui-v2-flussi.mjs index c32cb3e..03dde14 100644 --- a/test/gui-v2-flussi.mjs +++ b/test/gui-v2-flussi.mjs @@ -21,16 +21,18 @@ await provaBrowser(async (pg) => { await pg.screenshot({ path: 'test/.out/v2-box-contratto.png' }) return } - await pg.click('[data-modo="modifica"]') if (caso === 'modello') { - // dalla 1.2.1 il modello si sceglie solo dalla barra in alto: Qwen e Klein 3 (veloce e Base), niente Klein 4B, niente selettore nel pannello + // dalla 1.2.1 il modello si sceglie solo dalla barra in alto; dalla 1.4 sono quattro (Qwen, Qwen veloce, Klein 3, Klein 3 Base) e Klein ha una pagina sola const barra = pg.locator('.barra-titolo [data-selettore-modello] [data-modello]') - assert.deepEqual(await barra.evaluateAll((b) => b.map((x) => x.getAttribute('data-modello'))), ['qwen-base', 'klein', 'klein-base'], 'la barra in alto ha tre modelli') - assert.equal(await pannello.locator('[data-id^="modello-"]').count(), 0, 'nel pannello non c\'è più il selettore del modello') - assert.match(await pg.locator('.barra-titolo [data-modello][aria-checked="true"]').innerText(), /Klein/, 'fixture usa Klein') - assert.equal(await pg.locator('.strip-modi button[title^="La pila"]').count(), 1, 'LoRA accessibili anche con Klein') + assert.deepEqual(await barra.evaluateAll((b) => b.map((x) => x.getAttribute('data-modello'))), ['qwen-base', 'qwen-veloce', 'klein', 'klein-base'], 'la barra in alto ha quattro modelli') + assert.equal(await pannello.locator('[data-id^="modello-"]').count(), 0, "nel pannello non c'è più il selettore del modello") + await pg.click('[data-modello="klein"]') + await pg.locator('.strip-modi').waitFor({ state: 'detached' }) + assert.match(await pg.locator('.barra-titolo [data-modello][aria-checked="true"]').innerText(), /Klein/, 'Klein scelto') + assert.equal(await pg.locator('.colonna.destra:visible h3.chiudibile:has-text("LoRA")').count(), 1, 'LoRA accessibili anche con Klein, dentro la pagina') return } + await pg.click('[data-modo="modifica"]') await pg.click('.attrezzi button:has-text("Dal PC")') await pg.click('[data-id="cat-vestiti"]') const capo = pannello.locator('[data-controllo="capo"]') @@ -51,5 +53,5 @@ await provaBrowser(async (pg) => { await titolo.getByRole('textbox').fill('CIAO "MONDO"') assert.equal(await titolo.getByRole('textbox').inputValue(), 'CIAO "MONDO"') await pg.screenshot({ path: 'test/.out/v2-controlli.png' }) -}, () => { window.daprod.impostazioni.salva({ famiglia: 'klein', modelloScelto: 'klein' }) }) +}) console.log('OK: regressioni GUI v2 / ' + caso) diff --git a/test/interfaccia-1.4.mjs b/test/interfaccia-1.4.mjs new file mode 100644 index 0000000..50c9f28 --- /dev/null +++ b/test/interfaccia-1.4.mjs @@ -0,0 +1,64 @@ +// La 1.4 nel browser di prova (finto window.daprod): quattro modelli in alto, Qwen col suo Studio a quattro modi, Klein con una pagina sola +// (scrivi, aggiungi foto), l'interruttore «L'LLM legge il prompt» in tutti e due. Foto in test/.out/. +// node test/interfaccia-1.4.mjs +import assert from 'node:assert/strict' +import { mkdirSync } from 'node:fs' +import { provaBrowser } from './browser-prova.mjs' +mkdirSync('test/.out', { recursive: true }) + +// --- Qwen (di serie) --- +await provaBrowser(async (pg) => { + const pillole = pg.locator('.barra-titolo [data-selettore-modello] [data-modello]') + assert.deepEqual(await pillole.evaluateAll((b) => b.map((x) => x.getAttribute('data-modello'))), ['qwen-base', 'qwen-veloce', 'klein', 'klein-base'], 'quattro modelli in alto') + assert.equal(await pg.locator('.strip-modi [data-modo]').count(), 4, 'Qwen: Crea, Modifica, Box, Ricette') + assert.equal(await pg.locator('.llm-int[data-llm="no"]').count() >= 1, true, 'Qwen: l\'LLM che legge il prompt è spento di serie') + await pg.screenshot({ path: 'test/.out/v14-qwen-crea.png' }) + // Qwen veloce: 4 passi, LoRA Lightning + await pg.click('[data-modello="qwen-veloce"]') + await pg.locator('.studio-corpo .colonna.destra:visible textarea').first().fill('una volpe nella nebbia') + await pg.keyboard.press('Control+Enter') + await pg.waitForFunction(() => !!window.__ultima) + const q = await pg.evaluate(() => window.__ultima) + assert.equal(q.famiglia, 'qwen'); assert.equal(q.passi, 4, 'Qwen veloce: 4 passi'); assert.equal(q.turbo, true, 'Qwen veloce: LoRA Lightning acceso') + // l'interruttore si accende e il motore lo sa + await pg.click('.llm-int label') + assert.equal(await pg.evaluate(async () => (await window.daprod.impostazioni.leggi()).potenziaQwen), true, 'interruttore Qwen salvato') +}, undefined) + +// --- Klein: una pagina sola --- +await provaBrowser(async (pg) => { + assert.equal(await pg.locator('.strip-modi').count(), 0, 'Klein: niente modi dello Studio') + const pannello = pg.locator('.studio-corpo .colonna.destra:visible') + assert.equal(await pannello.locator('h3.chiudibile').filter({ hasText: /Genere|Stile/ }).count(), 0, 'Klein: niente Genere né Stile') + assert.equal(await pannello.locator('.llm-int[data-llm="si"]').count(), 1, 'Klein: l\'LLM legge il prompt, acceso di serie') + assert.equal(await pannello.locator('h3:has-text("Foto")').count(), 1, 'Klein: la sezione Foto') + await pg.screenshot({ path: 'test/.out/v14-klein-crea.png' }) + await pannello.locator('textarea').first().fill('una volpe nella nebbia') + await pg.keyboard.press('Control+Enter') + await pg.waitForFunction(() => !!window.__ultima) + const q = await pg.evaluate(() => window.__ultima) + assert.equal(q.famiglia, 'klein'); assert.equal(q.passi, 16, 'Klein: 16 passi'); assert.equal(q.modalita, 'crea'); assert.equal(q.potenzia, true) + // con una foto: è una modifica, la misura è quella della foto 1; con «Scelgo io» torna una creazione con quei riferimenti + await pg.evaluate(() => { window.__ultima = null }) + await pannello.locator('.rif .aggiungi').click() + await pg.getByRole('button', { name: 'Dal PC…' }).click() + await pg.waitForFunction(() => document.querySelectorAll('.colonna.destra .rif .r').length === 1) + assert.equal(await pannello.locator('h3:has-text("Cosa cambio?")').count(), 1, 'con una foto la domanda diventa «Cosa cambio?»') + assert.equal(await pannello.locator('h3:has-text("Formato")').count(), 0, 'con una foto niente Formato: esce come la foto 1') + await pg.screenshot({ path: 'test/.out/v14-klein-foto.png' }) + await pannello.locator('textarea').first().fill('metti un cappello rosso') + await pg.keyboard.press('Control+Enter') + await pg.waitForFunction(() => !!window.__ultima) + const m = await pg.evaluate(() => window.__ultima) + assert.equal(m.modalita, 'modifica'); assert.equal(m.immagini.length, 1); assert.equal(m.famiglia, 'klein'); assert.equal(m.potenzia, true) + await pg.evaluate(() => { window.__ultima = null }) + await pannello.locator('button:has-text("Scelgo io")').click() + assert.equal(await pannello.locator('h3:has-text("Formato")').count(), 1, 'Scelgo io: torna il Formato') + await pg.keyboard.press('Control+Enter') + await pg.waitForFunction(() => !!window.__ultima) + assert.equal((await pg.evaluate(() => window.__ultima)).modalita, 'crea', 'Scelgo io: foto nuova con i riferimenti') + // le scorciatoie dei modi non portano via da Crea + await pg.keyboard.press('2') + assert.equal(await pg.locator('.studio-corpo .colonna.destra:visible h3:has-text("Foto")').count(), 1, 'Klein: il tasto 2 resta su Crea') +}, () => { window.daprod.impostazioni.salva({ famiglia: 'klein', modelloScelto: 'klein' }) }) +console.log('interfaccia 1.4: ok (foto in test/.out/v14-*.png)') diff --git a/test/modelli.mjs b/test/modelli.mjs index 14117f9..a43e252 100644 --- a/test/modelli.mjs +++ b/test/modelli.mjs @@ -22,12 +22,13 @@ function uguale(nome, avuto, atteso) { } // la mappa -uguale('tre modelli, in ordine (niente Klein 4B dalla 1.2.1)', M.ORDINE_MODELLI, ['qwen-base', 'klein', 'klein-base']) -uguale('le chiavi della mappa', Object.keys(M.MODELLI).sort(), ['klein', 'klein-base', 'qwen-base']) -uguale('qwen-base', [M.MODELLI['qwen-base'].famiglia, M.MODELLI['qwen-base'].passi, M.MODELLI['qwen-base'].turbo, M.MODELLI['qwen-base'].lora], ['qwen', 40, false, true]) +uguale('quattro modelli, in ordine (niente Klein 4B dalla 1.2.1)', M.ORDINE_MODELLI, ['qwen-base', 'qwen-veloce', 'klein', 'klein-base']) +uguale('le chiavi della mappa', Object.keys(M.MODELLI).sort(), ['klein', 'klein-base', 'qwen-base', 'qwen-veloce']) +uguale('qwen-base', [M.MODELLI['qwen-base'].famiglia, M.MODELLI['qwen-base'].passi, M.MODELLI['qwen-base'].turbo, M.MODELLI['qwen-base'].lora], ['qwen', 25, false, true]) +uguale('qwen-veloce: Lightning, 4 passi', [M.MODELLI['qwen-veloce'].famiglia, M.MODELLI['qwen-veloce'].passi, M.MODELLI['qwen-veloce'].turbo, M.MODELLI['qwen-veloce'].lora], ['qwen', 4, true, true]) uguale('klein', [M.MODELLI.klein.famiglia, M.MODELLI.klein.passi, M.MODELLI.klein.cfg, M.MODELLI.klein.veloce], ['klein', 16, 1, true]) uguale('klein-base', [M.MODELLI['klein-base'].famiglia, M.MODELLI['klein-base'].passi, M.MODELLI['klein-base'].cfg], ['klein-base', 50, 3.5]) -uguale('nessun modello col Turbo', Object.values(M.MODELLI).some((m) => m.turbo), false) +uguale('solo qwen-veloce ha il LoRA Lightning', Object.values(M.MODELLI).filter((m) => m.turbo).map((m) => m.id), ['qwen-veloce']) for (const id of M.ORDINE_MODELLI) uguale(`id dentro la voce ${id}`, M.MODELLI[id].id, id) uguale('predefinito', M.MODELLO_PREDEFINITO, 'qwen-base') @@ -49,7 +50,8 @@ uguale('imp 1.0 klein-base', M.modelloDaImpostazioni({ famiglia: 'klein-base' }) uguale('imp 0.x senza famiglia', M.modelloDaImpostazioni({}), 'qwen-base') uguale('imp nulle', M.modelloDaImpostazioni(null), 'qwen-base') uguale('imp 1.1: vince modelloScelto', M.modelloDaImpostazioni({ famiglia: 'qwen', modelloScelto: 'klein' }), 'klein') -uguale('imp con modelloScelto rovinato', M.modelloDaImpostazioni({ famiglia: 'klein-base', modelloScelto: 'qwen-veloce' }), 'klein-base') +uguale('imp con modelloScelto rovinato', M.modelloDaImpostazioni({ famiglia: 'klein-base', modelloScelto: 'sdxl' }), 'klein-base') +uguale('imp con qwen-veloce', M.modelloDaImpostazioni({ famiglia: 'qwen', modelloScelto: 'qwen-veloce' }), 'qwen-veloce') // salvataggio: modelloScelto e famiglia restano d'accordo uguale('salva modelloScelto', M.allineaScelta({ modelloScelto: 'klein' }), { modelloScelto: 'klein', famiglia: 'klein' }) @@ -66,13 +68,23 @@ uguale('opera senza modello (PNG da fuori)', M.modelloDaOpera({ modello: '' }), uguale('opera nulla', M.modelloDaOpera(undefined), 'qwen-base') // quello che va nella Richiesta -uguale('parametri qwen-base', M.parametriModello('qwen-base'), { famiglia: 'qwen', passi: 40, turbo: false }) +uguale('parametri qwen-base', M.parametriModello('qwen-base'), { famiglia: 'qwen', passi: 25, turbo: false }) +uguale('parametri qwen-veloce', M.parametriModello('qwen-veloce'), { famiglia: 'qwen', passi: 4, turbo: true }) uguale('parametri klein', M.parametriModello('klein'), { famiglia: 'klein', passi: 16, turbo: false }) uguale('parametri klein-base', M.parametriModello('klein-base'), { famiglia: 'klein-base', passi: 50, turbo: false }) // Rifai in qualità piena uguale('piena da klein', M.inQualitaPiena('klein'), ['klein-base', 'qwen-base']) uguale('piena da qwen-base', M.inQualitaPiena('qwen-base'), []) +uguale('piena da qwen-veloce', M.inQualitaPiena('qwen-veloce'), ['qwen-base']) + +// il LoRA Lightning e l'interruttore «l'LLM legge il prompt» +uguale('shift Lightning', M.shiftDi(M.LIGHTNING_FILE), 0.9) +uguale('shift Turbo8 (PNG vecchi)', M.shiftDi('turbo8_lora_step2500.safetensors'), 0.6935) +uguale('LLM: Klein di serie acceso', M.potenziaPer({}, 'klein'), true) +uguale('LLM: Qwen di serie spento', M.potenziaPer({}, 'qwen'), false) +uguale('LLM: Qwen acceso a mano', M.potenziaPer({ potenziaQwen: true }, 'qwen'), true) +uguale('LLM: Klein spento a mano', M.potenziaPer({ potenziaKlein: false }, 'klein-base'), false) uguale('piena da klein-base', M.inQualitaPiena('klein-base'), []) // le qualità salvate (localStorage, ricette)