diff --git a/CHANGELOG.md b/CHANGELOG.md index 2de47d3..9db17e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,18 @@ Il motore impara cose nuove (la versione dell'app non cambia qui: la alza chi pu - Correzioni: Klein 4B ha la sua chiave VRAM nella regola «una cosa alla volta»; le parole chiave dei LoRA entrano nel prompt anche con Klein. - Prove: `npm test` (modelli, catalogo, spazio, lora, regioni, adatta, grafi v2, grafi Qwen identici). +## Non rilasciato — interfaccia v2 «Orbita» + +L'interfaccia nuova (spec in `docs/gui-v2-spec.md`) sopra l'engine v2, più gli asset grafici. + +- **Studio**: Crea e Modifica nella stessa schermata, pannello dei controlli a sinistra, pulsante CREA fisso in fondo, risoluzione finale sempre visibile (HUD). +- **Crea a pulsanti** (tessere al posto dei menu a tendina) e **28 stili** con icona in 4 scaffali. +- **Editor box** con un prompt per riquadro (va al motore come regioni), **Estrai in due clic** (SAM 2), opzione **senza sfondo**. +- **Gestore LoRA**: pila con ordine e pesi. **Sei ricette** (Foglio, Mondi, Mix, Set, Formati, Rinnova) al posto del catalogo lungo. +- **Modelli uncensored**: interruttore spento di serie, avviso da accettare, etichetta sulle voci. +- **Asset** in `resources/gui` (111 WebP, con manifest: prompt, seed, modello). 60 sono scontornati per colore, da rifare con BiRefNet. +- Non ancora provati con la GPU: qualità di SAM 2 e BiRefNet, riquadri dal vivo, LoRA su Klein. + ## 1.1.0 — 2026-10-21 Un'app più semplice da usare: tre modelli al posto delle «qualità», una sola scheda per scrivere cosa cambiare (tutta la foto o solo una zona), le ricette dentro Modifica e il testo scritto dal Qwen3-VL 4B anche con Qwen. Il motore passa a ComfyUI 0.42.3. diff --git a/docs/gui-v2-spec.md b/docs/gui-v2-spec.md new file mode 100644 index 0000000..f9e3b7e --- /dev/null +++ b/docs/gui-v2-spec.md @@ -0,0 +1,675 @@ +# DaProd Image — GUI v2 «Orbita» · specifica di progetto + +**Stato:** bozza 1 per la revisione di Opus · **Base:** `375ac96` (v1.1.0, `feat/klein-upsampler-veloce`) · **Branch:** `design/gui-v2` · **Autore:** claude-sonnet (task t_4a7955f9, subentrato ad Astra per la quota GPT). +**Letti per scriverla:** `CLAUDE.md`, `rapide.ts`, `ricetteBase.ts`, `preset.ts`, `stile.css`, `Crea.tsx`, `Modifica.tsx` / `SchedaScrivi.tsx`, `ScegliModello.tsx`, `Lora.tsx`, `Impostazioni.tsx`, `shared/modelli.ts`, `util.ts`, e la ricerca di Sol (`docs/ricerca-modelli-2026-10.md`, branch `research/modelli-2026-10`). +**Questo documento non cambia `src/`.** Tutto ciò che qui è «proposta di interfaccia» (campi, IPC) va concordato nel thread dei task engine (t_22f93113) e UI (t_3f06c5f1) prima di scriverlo nel codice. +**Mockup:** `docs/mockup/index.html` (statico, si apre con doppio clic; 4 schermate navigabili). + +--- + +## 0. In una pagina + +| Cosa chiede l'utente | Come lo risolve la v2 | +|---|---| +| GUI «spaziale», moderna, divertente | Tema **Orbita**: spazio profondo, vetro scuro, stelle in parallasse, anelli di luce al posto dei bordi, microcopy da missione. Sempre leggibile: l'effetto sta nello sfondo e nei bordi, mai sul testo (§8, §9) | +| Crea: pulsanti, niente tendine | Ogni scelta è una **tessera** (icona grande + nome corto). Zero `` residui di `Impostazioni.tsx` (`voceScelta`) diventano **segmenti a pulsanti** (profilo memoria: Auto · Bassa · Normale · Alta; traduttore: Opus-MT · LLM; ecc.). + +### 9.2 Laboratorio Uncensored + +``` +┌ LABORATORIO UNCENSORED ───────────────────────────────────────────────────┐ +│ 🔥 Modelli senza filtri di rifiuto [ ◯──── Disattivato ] │ +│ Per scrittura libera e immagini mature fra adulti consenzienti. │ +│ Sono più liberi, non “permessi”: la legge e le licenze valgono sempre. │ +└────────────────────────────────────────────────────────────────────────────┘ +(attivato) CATALOGO PER RUOLO + Ruolo Modello Licenza Peso Stato + Testo (LLM) Qwen3-VL 4B abliterated Apache-2.0 ~4 GB [Scarica] (in valutazione) + Encoder Qwen3-VL 8B Uncensored (GGUF) Apache-2.0 ~5 GB [Scarica] + Immagine Qwen-Image 2.1 Uncensored Q4/6/8 Qwen Research 4–7 GB [Scarica] ⚠ solo uso non commerciale + Immagine FLUX / Klein 3 Kommunity — [Non disponibile] ℹ la licenza vieta di aggirare le protezioni +``` + +**Flusso** +1. L'interruttore apre il **disclaimer** in modale (non si attiva finché non lo si accetta): + > **Prima di continuare.** Questi componenti non rifiutano le richieste e possono produrre contenuti sensibili o inesatti. Usali solo per scopi leciti, con i diritti e il consenso necessari. Sono vietati — e non verranno mai supportati — contenuti sessuali che coinvolgono minori, immagini intime non consensuali, deepfake ingannevoli di persone reali, violenza reale o altri usi illegali. «Uncensored» non significa «senza regole»: restano valide le leggi del tuo Paese e le licenze di ogni modello (che possono vietare l'uso commerciale o la ridistribuzione). Rispondi tu di ciò che crei e pubblichi. + Tre caselle **obbligatorie** (non preselezionate): *Ho almeno 18 anni* · *Userò questi modelli solo per scopi leciti e nel rispetto delle licenze* · *Capisco che chi crea i contenuti ne è responsabile*. Pulsante **Attiva** disabilitato finché non sono tutte accese. Si registra `uncensored.accettatoIl` (data) e `versioneDisclaimer`. +2. Attivato: compare il catalogo. Ogni riga ha **ruolo, licenza (link), peso, stato, pulsante**. Per i modelli con licenza non commerciale (Qwen Research) il download chiede una **seconda conferma** con il testo della licenza e non parte da solo. **Niente download automatici** dell'idle (`idle.ts` li esclude esplicitamente). +3. Nessun file illegale o d'abuso nel catalogo; le voci le approva il team (lista chiusa in `catalogo.ts`, `categoria: 'uncensored'`, con `repo`, `revisione`, `licenza`, `ruolo`). +4. **Dopo il download**, il modello compare nel selettore Motore (Studio e Impostazioni) come tessera con **fascetta Uncensored**. **Non è mai scelto da solo**: al riavvio il motore torna a un modello standard (`modelloScelto` non può essere un uncensored al caricamento), le ricette/lavori in coda non lo usano se non è stato scelto esplicitamente; il tooltip dice *«Scelto da te: nessun filtro di rifiuto»*. +5. **Segnali visivi** quando è in uso: fascetta fissa accanto al pulsante Crea, bordo console in `--corona`, `uncensored: true` nelle impostazioni dentro il PNG (e filtro «solo uncensored» in Galleria per trovarli). +6. **Disattiva:** l'interruttore spento nasconde le tessere (i file restano), e il pulsante **Rimuovi i file** li cancella dopo conferma. + +### 9.3 Decisione aperta (D1 — serve Opus/utente) +La ricerca di Sol (§3) dice che **FLUX 3 Klein** ha licenza Kommunity con §4(d) che vieta di aggirare/rimuovere le protezioni; l'encoder Klein uncensored (AtomicChat) non è utilizzabile nel sampler e non esiste un denoiser Klein «de-safety» legittimo da distribuire. **Proposta per la v2:** il catalogo Uncensored contiene **LLM** (testo/Idee/Migliora) e **Qwen-Image 2.1** (con avviso licenza Research). La riga **Klein/FLUX** compare come *«Non disponibile»* con spiegazione cliccabile. Se l'utente vuole comunque una voce Flux, l'unica via pulita è **FLUX.2 Klein 4B (Apache-2.0)** + un encoder uncensored dalla licenza chiara (oggi nessuno verificato): da valutare da Sol. **Rispondere nel thread** prima che Sonnet scriva il catalogo. + +--- + +## 10. Gestione LoRA (Studio + pagina LoRA) + +**Obiettivi:** caricare più LoRA con forza e ordine per **entrambe** le famiglie, capire a colpo d'occhio cosa è compatibile col modello scelto, provare «con e senza». + +### 10.1 Cassetto «Pila LoRA» (da Studio, pulsante `LoRA N` nella strip) + +``` +┌ PILA LORA · per Klein veloce ───────────────── [+ Aggiungi] ┐ +│ ① ⠿ [thumb] Cinema grain Klein 3 ✓ ━━●━━ 0,70 [S] [👁] ⋯ │ +│ parole chiave: [cngrain] ← clic le inserisce nel prompt │ +│ ② ⠿ [thumb] Visi coerenti Klein 3 ✓ ━●━━━ 0,50 [S] [👁] ⋯ │ +│ ③ ⠿ [thumb] Acquerello XL ⚠ non per Klein (spenta, grigia) │ +│ Suggeriti da te in Modifica: (+) Coerenza ← un clic = in pila │ +│ [ ◧ Prova A/B ] [ Salva come look ▾ ] Forza totale 1,20 ⚠│ +└────────────────────────────────────────────────────────────────────┘ +``` + +- **Slot ordinabili** (trascina `⠿`; l'ordine è l'ordine di applicazione dei `LoraLoaderModelOnly`). Max 8 slot attivi (avviso oltre 4). +- **Compatibilità per architettura**, non per «Flux»: badge `Qwen 2.1` · `Klein 3` · `Klein 2 4B/9B` (se aggiunti) · `Altro (SD/SDXL/Flux 1)` · `Sconosciuto`. Si legge dall'intestazione safetensors (come oggi `lora:info`) e dal sidecar `.json` se c'è. Il LoRA incompatibile col modello in uso è **grigio, con motivo** («fatto per Qwen 2.1, qui usi Klein»), non nascosto; si può comunque tenere in pila per l'altro modello (la pila ha **una vista per modello**: cambiando motore cambia la pila; i LoRA condivisi tra famiglie non esistono). +- **Forza** 0–1,5 (avanzate: −1,0…2,0) con valore di partenza dalla scheda o **0,7** (la ricerca lo propone come default UI). Doppio clic sul cursore = ripristina. Nessuna normalizzazione automatica; la **forza totale > 1,8** mostra solo un avviso. +- **S (solo)** = prova questo LoRA da solo (gli altri in pausa per un lancio); **occhio** = spegni senza rimuovere. +- **Parole chiave** come chip (qui servono davvero): un clic le inserisce una sola volta nel prompt; il motore le aggiunge comunque (`conParoleLora`). Il testo da disegnare e le parole chiave restano separati. +- **Gruppo esclusivo «Acceleratori»** (Turbo/Lightning/Distillazione): se ne può accendere uno solo; i conflitti sono spiegati. +- **Suggeriti** (`ChiaveLora` di `rapide.ts`: coerenza, vestiti, camera): compaiono come slot fantasma quando una categoria di Modifica li chiede, un clic li mette in pila. +- **Prova A/B:** lancia 2 immagini con lo stesso seed, una con la pila e una senza (o con/senza il LoRA selezionato), le mostra affiancate con cursore di confronto (`PrimaDopo`). Costa 2 immagini: l'HUD lo dice. +- **Look:** salva la pila (con forze e ordine) come preset nominato; tessere a icona per richiamarli. +- **Nel PNG** (`png.ts`): per ogni LoRA `hash, file, peso, ordine, base, trigger`; cambiare un peso/ordine invalida le cache (lavoro del motore). + +### 10.2 Pagina LoRA (libreria) +- Griglia di **carte con copertina** (immagine generata in app: l'ultima dove è stato usato + miniatura del creatore, se c'è), filtro a **tessere**: *Tutti · Per questo modello · Qwen · Klein · Stili · Persone · Dettagli · Acceleratori*; ordinamento recenti/nome. +- **Aggiungi:** trascina `.safetensors` ovunque nella finestra (già esiste) · incolla link Hugging Face/Civitai · «Scansiona cartella». Dopo l'import: **assistente a 2 passi** (1. «Per quale modello l'hai preso?» con riconoscimento automatico ma correggibile; 2. parole chiave e forza). I pickle (`.pt/.ckpt`) vengono rifiutati con spiegazione. +- **Consigliati** (`catalogo.ts`): tessere per modello (Qwen 2.1: coerenza, vestiti, camera; Klein 3: da aggiungere dopo il test del loader di Sol). +- **Dettaglio carta:** metadati (base, formato, hash, licenza), storico di uso, pulsanti *Prova A/B*, *Apri la pagina*, *Mostra nella cartella*, *Elimina*. +- **Dati richiesti dall'engine (proposta a Sol):** `lora:info(file) → { architettura, baseRevisione?, formato, sha256, trigger[], pesoConsigliato?, distillazione?: 'turbo'|'lightning'|null, compatibili: IdModello[] }`; `Richiesta.lora: { file, peso, ordine }[]` (esiste in forma più semplice); test del loader Klein prima di abilitare il caricamento. + +--- + +## 11. Cassetto «Ingranaggi» (ispirato a ComfyUI, ma semplice) + +Piccolo pulsante `⚙` accanto al pulsante primario. Apre un cassetto a destra con il **piano di volo**: la catena di passi che l'app farà per questa richiesta, disegnata come **grafo verticale di nodi** (6–10 nodi), solo lettura per la struttura ma con i **valori modificabili sul nodo**. + +``` + ◉ Foto base 1024×768 + │ + ▢ Zona (maschera morbida, contesto 0,8) + │ + ◇ Motore: Klein veloce · 8 passi · CFG 1 [seed 123456 ⚄] + │ └─ LoRA ① Cinema grain 0,70 + ◇ Testo: tradotto in inglese · migliorato + │ + ✦ Campionatore euler/simple + │ + ⤢ Ingrandisce ×1,2 (RTX) → 1536×1024 + │ + ⬒ Salva PNG (alpha) + Galleria +``` +- I nodi sono *derivati dalla richiesta*, non un editor libero: niente cavi da collegare. Ogni nodo mostra 1–3 valori modificabili (passi, CFG, seed, megapixel, forza LoRA, denoise). +- **Modifica ricetta:** «Salva questa catena come ricetta» (Le mie). L'Editor ricette diventa una versione dello stesso grafo con passi riordinabili e ingressi esposti (testo, foto, scelta). +- **Esporta grafo ComfyUI** (`.json` workflow) per chi vuole aprirlo altrove — fase 3, utile per i power user. +- Vale come strumento di diagnosi: se un lavoro fallisce, il nodo in errore è evidenziato in `--errore`. + +--- + +## 12. Micro-interazioni + +Tutte rispettano «Meno movimento» (le durate vanno a ~0 e si spegnono rotazioni e parallasse). + +| Dove | Interazione | Dettaglio | +|---|---|---| +| Tessere | **Hover** | sollevamento 2 px, alone tenue `--alone-nebulosa`, icona che «respira» (scala 1→1,06, 600 ms) | +| Tessere | **Selezione** | anello `--aurora` che compare con un'espansione a molla e poi ruota lentamente (12 s/giro); l'icona passa a `--stella` | +| Tessere | **Pallina valore** | quando una categoria ha scelte attive, una pallina luminosa pulsa una volta e mostra il numero | +| Interruttori | **Accendi** | il pianeta scivola e l'anello si accende; click sonoro opzionale (OFF di default) | +| Cursori | **Trascina** | il pomello-stella allunga una scia; il valore appare in una bolla vetro sopra; rilascio = piccolo «ping» luminoso | +| Strip modi | **Cambio modo** | il pannello a sinistra scorre di 24 px con dissolvenza (220 ms); il palco non salta (stessa tela) | +| Palco | **Rilascio foto** | l'anello «wormhole» si apre dal punto del rilascio e la foto vi entra; invito Modifica/Riferimento | +| Palco | **Cambio formato** | la cornice cambia forma con una molla (240 ms); l'HUD fa rotolare i numeri | +| HUD | **Cambio stato** (nativa/ingrandita/ridotta) | l'icona e il colore cambiano con un lampo breve; la seconda riga si espande/collassa | +| Box | **Nuovo box** | scala .9→1 con alone; bordo tratteggiato che avanza («formiche in marcia») finché è selezionato | +| Box | **Elimina** | si ritrae verso il centro e svanisce (140 ms) | +| Box | **Aggancio** | linee guida ciano a comparsa istantanea, un tick a 60 Hz (nessuna latenza) | +| Pulsante primario | **In corso** | un anello di luce gira attorno al bordo, il testo diventa fase+percentuale («Disegno… 42%») | +| Palco | **Anteprima live** | l'anteprima taesd si sfuoca in ingresso e si «mette a fuoco» con ogni passo (blur 8→0) | +| Risultato | **Finito** | piccola **supernova** (anello che si espande e svanisce, 500 ms) attorno alla miniatura; l'immagine si deposita nella striscia con rimbalzo | +| Errore | **Fallito** | la miniatura trema 2 volte, bordo `--errore`, toast con la causa in italiano e azione «Riprova» | +| Estrai | **Sagoma** | il bordo della maschera scorre come un'aurora (gradiente animato); dopo il ritaglio la cosa «esce» dalla foto con ombra e scorre nel pannello Estratti | +| Impostazioni | **Attiva Uncensored** | la modale fa una breve scossa di avvertimento sul pulsante se le caselle non sono tutte accese; dopo l'attivazione la tessera appare con la fascetta fiamma | +| LoRA | **Trascina slot** | lo slot si solleva, gli altri scorrono; sul rilascio fa un «clic» di aggancio | +| Generale | **Parallasse** | lo starfield si sposta di 2–4 px verso il mouse (rAF, throttle 30 fps) | +| Generale | **Stati vuoti** | l'astronauta fluttua; cliccandolo fa una piroetta (easter egg) | +| Generale | **Easter egg** | digitando «cometa» o ↑↑↓↓ passa una cometa sullo schermo (una volta per sessione) | + +**Microcopy spaziale (ma sempre chiaro)** + +| Momento | Testo | +|---|---| +| Stato vuoto | «Cosa costruiamo oggi?» | +| Fasi del lavoro | «Leggo la foto…» · «Traduco il prompt…» · «Preparo i motori…» · «Disegno…» · «Rifinisco i bordi…» · «Metto in orbita…» | +| Fine | «Atterrato.» (+ tempo) | +| Coda | «In attesa di decollo (n°)» | +| Errore | «Intoppo in orbita: . [Riprova]» | +| Pulsanti | CREA ✦ · MODIFICA ✦ · COMPONI ✦ · LANCIA ✦ (nomi chiari prima del colore) | + +**Budget:** ≤ 4 animazioni continue contemporanee; starfield ≤ 3% CPU; nessun `box-shadow` animato su più di 12 elementi (animare `opacity` di uno pseudo-elemento); mai animare `width/height/top/left`. + +--- + +## 13. Elenco asset da generare (per Sol / Haiku — Flux Klein, modalità veloce) + +**Scopo:** dare un'identità visiva coerente senza appesantire l'installer (< 6 MB totali aggiunti ai `resources/`; WebP qualità 82; PNG con alpha solo dove serve). +**Cartella:** `resources/gui//.webp` (o `.png`); un `resources/gui/README.md` con prompt, seed, modello e data di ogni asset. +**Motore:** Klein veloce 8 passi (stesso modello dell'app), seed fissato, un seed per gruppo. **Stile comune** (prefisso di ogni prompt): *«polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text»* (per i gruppi con trasparenza: aggiungere *«isolated on plain dark background»* e poi **togliere lo sfondo con BiRefNet** quando l'engine v2 lo permette, altrimenti con il modello Qwen «Rimuovi sfondo» esistente). + +### 13.1 Set di prova (4–6, da mostrare a Opus/Astra PRIMA di generare il resto) +| id | File | Misura | Prompt (dopo il prefisso comune) | +|---|---|---|---| +| `prova-persone` | `tessera/persone.png` | 256² alpha | an astronaut helmet visor reflecting a smiling face, three-quarter view | +| `prova-estrai` | `tessera/estrai.png` | 256² alpha | scissors cutting a glowing outline around a small planet, sparks | +| `prova-sfondo` | `sfondi/nebulosa.webp` | 2560×1440 | deep space nebula in violet and magenta with faint stars, large empty dark area on the right, cinematic, no planet | +| `prova-stile-1` | `stili/anime-film.webp` | 320² | a young woman in a red scarf holding a coffee cup on a city street at sunset, anime movie still style | +| `prova-stile-2` | `stili/vetro.webp` | 320² | the same woman rendered as liquid glass and crystal, refractive translucent material | +| `prova-locandina` | `ricette/foglio.webp` | 512×320 | a character turnaround sheet floating in space, multiple poses of an astronaut fox, neat grid | + +### 13.2 Gruppi completi + +| Gruppo | Cartella | Quanti | Misura | Contenuto | Alpha | +|---|---|---|---|---|---| +| **Sfondi** | `sfondi/` | 6 | 2560×1440 (WebP ≤ 350 kB) | `nebulosa` (principale) · `nebulosa-calda` (oro/magenta) · `stelle` (tile 1024², trasparente, stelle sparse) · `aurora` (banner Impostazioni 1600×400) · `hero-pianeta` (stato vuoto, pianeta in basso a sinistra) · `setup-pianeta` | `stelle`, `hero-pianeta` sì | +| **Tessere Modifica** | `tessera/` | 10 | 256² | Estrai · Persone · Vestiti · Situazioni · Prospettiva · Luce e colore · Stili · Testi · Ritocco · Espandi — un oggetto simbolo 3D per ognuna | sì | +| **Modi** | `modi/` | 4 | 128² | Crea · Modifica · Box · Ricette (versione piccola, per la strip) | sì | +| **Generi di Crea** | `generi/` | 12 | 256² | Foto · Ritratto · Prodotto · Poster · Infografica · Logo · Social · Miniatura · Fumetto · Invito · Menù · Libero | sì | +| **Anteprime stili** | `stili/` | 28 | 320² | **stessa scena** per tutti (*ragazza con sciarpa rossa che beve un caffè in una strada di città al tramonto*, seed fisso), un file per stile con il frammento del §7.4 come prompt | no | +| **Ricette** | `ricette/` | 6 | 512×320 | locandine di Foglio · Mondi · Mix · Set · Formati · Rinnova | no | +| **Luoghi** | `luoghi/` | 10 | 160² | miniature per Situazioni: città, spiaggia, montagna, bosco, deserto, interno, studio bianco, spazio, sott'acqua, futuro | no | +| **Meteo/stagioni** | `meteo/` | 10 | 128² | sole, nuvole, pioggia, neve, nebbia, temporale · primavera, estate, autunno, inverno | sì | +| **Costumi** | `costumi/` | 9 | 128² | astronauta, cavaliere, pirata, supereroe, samurai, mago, chef, medico, sposa (busto stilizzato) | sì | +| **Animali** | `animali/` | 5 | 128² | cane, gatto, cavallo, uccello, drago | sì | +| **Espressioni** | `facce/` | 6 | 128² | sorriso, ridere, serio, sorpreso, triste, occhiolino (faccia 3D tonda) | sì | +| **Disposizioni Box** | `box/` | 9 | 160×100 | *non da generare*: SVG disegnati a mano (schemi dei box) | — | +| **Personaggi** | `mascotte/` | 3 | 512² | astronauta che fluttua (stato vuoto), astronauta con cartello (errore), astronauta che saluta (setup) | sì | +| **LoRA** | `lora/` | 2 | 256² | copertina generica «Qwen» e «Klein» per i LoRA senza anteprima | no | +| **Uncensored** | `uncensored/` | 1 | 256² | icona fiamma arancio (fascetta e tessera) — meglio SVG | sì | + +**Totale:** circa 125 immagini (di cui 8 sono SVG scritti a mano e non vanno in coda). +**Priorità:** P0 = set di prova + sfondi + tessere Modifica + modi (indispensabili alla fase 1); P1 = anteprime stili + generi + ricette; P2 = luoghi, meteo, costumi, animali, facce, mascotte. + +### 13.3 Regole +- Niente testo dentro le immagini (le scritte le fa l'interfaccia). Niente volti di persone reali riconoscibili. Niente loghi o personaggi con diritti (per «stili» usare nomi generici: vedi §7.4, es. «Anime da film», non marchi). +- Stile coerente: stessa luce (rim light ciano a sinistra, magenta a destra), stessa famiglia di colori. Ogni asset 3D su fondo scuro neutro (poi alpha). +- Per ogni asset il README riporta: `id, file, prompt, negative, modello (nome file), passi, seed, dimensioni originali, dimensioni finali, data`. +- Se un asset non arriva, l'interfaccia mostra un **fallback a icona SVG + gradiente** (nessuna tessera vuota). + +### 13.4 Icone SVG nuove (le disegna Sonnet, `Icone.tsx`) +`estrai, box, ricette, studio, pila, pennello-magico, bacchetta-sam, orbita, sole-dir, sole-qualita, temperatura, eta, sorriso, ridere, serio, sorpreso, triste, occhiolino, occhiali, barba, capelli, cappello, scarpe, giacca, abito, pantaloni, maglia, cappotto, ora-giorno, epoca, folla, meteo-*, stagione-*, lingua, titolo, cartello, restaura, dettagli, pelle, bianco-nero, grading, ritaglio, ricrea, su-sfondo, fiamma, lucchetto, occhio-barrato, trascina, dado, quadrante, wormhole, supernova, disposizione-*`. + +--- + +## 14. Piano di consegna (per i task successivi) + +| Fase | Contenuto | Responsabile | Cosa si vede | +|---|---|---|---| +| **F0** | Token + `stile.css` «Orbita», componenti base (`Tessera`, interruttore, cursore, segnaposto), starfield, `test/interfaccia.mjs` verde con i nuovi stili | Sonnet UI | Nuovo aspetto su tutte le pagine senza cambiare funzioni | +| **F1** | `Studio` con 4 modi, strip, console, **HUD misura**, Crea a tessere (Motore, Cosa, Atmosfera, Stile, Formato, Misura, Sfondo, Quante), Senza sfondo (engine: RGBA Qwen / genera+BiRefNet Klein) | Sonnet UI + Sol (`Richiesta.sfondo`, `bg:rimuovi`) | Crea completo, risoluzione chiara | +| **F2** | Modifica: griglia categorie + `controlli.ts` (sostituisce `rapide.ts`), Estrai (Ricrea + Trova già pronti; Ritaglio con SAM2/BiRefNet), stili nuovi, ricette 6 | Sonnet UI + Sol (`estrai:ritaglia`) | Modifica a pulsanti, Estrai in 2 clic | +| **F3** | Box (disegno, pannello, disposizioni, adattatore Klein 3 / maschere Qwen), Bozza veloce | Sonnet UI + Sol (`Richiesta.box`) | Editor regionale | +| **F4** | Pila LoRA + pagina LoRA nuova, Klein LoRA (dopo test loader), Laboratorio Uncensored, Ingranaggi, A/B | Sonnet UI + Sol (`lora:info`, catalogo uncensored) | Gestione completa | +| **F5** | Rifiniture: microcopy, easter egg, audio opzionale, test visivi `test/foto.mjs`, README/CHANGELOG | Sonnet | Release 1.2.0 | + +**Interfacce da fissare nel thread di t_22f93113 (proposta):** +1. `Richiesta.sfondo?: 'scena' | 'trasparente' | { tinta: string }` + `Opera.alpha: boolean` nelle impostazioni dentro il PNG. +2. `Richiesta.box?: BoxRegione[]` e `Richiesta.promptScena?: string` (§5). +3. `estrai:ritaglia`, `estrai:maschera` (clic/box → maschera SAM2), IPC di progresso (fase, percentuale). +4. `lora:info` esteso, `Richiesta.lora` ordinato con peso, supporto Klein (se il loader funziona). +5. `modelli.catalogo({categoria:'uncensored'})` → voci con `ruolo, licenza, licenzaUrl, peso, stato ('disponibile'|'in-valutazione'|'bloccato-licenza'), richiedeConferma` ; `impostazioni.uncensored = { abilitato, accettatoIl, versioneDisclaimer }`. +6. `MODELLI` (in `shared/modelli.ts`) estendibile con `uncensored?: true` e `ruolo` senza rompere `modelloDa`. +7. `test/finto.js` allineato a ogni nuovo campo (CLAUDE.md lo impone). + +**Criteri di accettazione trasversali:** `npm run typecheck`, `npm run build`, `node test/interfaccia.mjs`, `node test/modelli.mjs`, `node test/ricette.mjs` (adattato), foto di ogni schermata in `test/.out/`; nessun ``/`Segmenti`) | Studio · Crea (tessere) | +| Pagina Modifica (Scrivi/Espandi/Ricette/Estrai) | Studio · Modifica (3 tessere ambito + categorie), Espandi è una tessera, Estrai in cima | +| Scheda Ricette (Laboratorio) | Studio · Ricette (6 locandine + Mie) | +| Idee/azioni rapide (chip) | Controlli interattivi (§6.2) | +| Preset (16) con stili per genere | Generi (11 + Libero) + Atmosfera (icone) + Stile (28) | +| `ScegliModello` (3 pillole nella barra) | Tessere Motore (+ Uncensored) | +| Misura, ScegliMp, Uscita in «Altro» | HUD misura sempre visibile + popover | +| `Lora.tsx` sub-tab di Impostazioni, `SceltaLora` | Pagina LoRA + Pila LoRA nel cassetto | +| `Impostazioni` con ` +
+ + + + + + +
+
+

PILA LORA per Klein veloce

+

Trascina per cambiare l'ordine. La forza si regola per ogni slot; ⚠ avvisa se la somma è alta.

+
⠿
+
① Cinema grain Klein 3 ✓
+
◔
0,70
+
parola chiave: un clic la inserisce nel prompt
+
⠿
+
② Visi coerenti Klein 3 ✓
+
◔
0,50
+
⠿
+
③ Acquerello XL ⚠ non per Klein
+
Fatto per SDXL: qui non funziona. Resta in pila per un altro modello.
+
Forza totale 1,20
+
+
+ + +
+ +
+
+

🔥 LABORATORIO UNCENSORED

Modelli che non rifiutano le richieste. Più liberi, non “permessi”: legge e licenze valgono sempre.

+
+
+ +

PRESTAZIONI E MEMORIA

+

Profilo memoria

+
+

I vecchi menu a tendina diventano pulsanti.

+
+
+ + +
+ ● Disegno… 42%
Barra di stato: clic = apre la coda
+ + + +
+
+

🔥 Prima di continuare

+

Questi componenti non rifiutano le richieste e possono produrre contenuti sensibili o inesatti. Usali solo per scopi leciti, con i diritti e il consenso necessari. Sono vietati — e non verranno mai supportati — contenuti sessuali che coinvolgono minori, immagini intime non consensuali, deepfake ingannevoli di persone reali, violenza reale o altri usi illegali. «Uncensored» non significa «senza regole»: restano valide le leggi del tuo Paese e le licenze di ogni modello. Rispondi tu di ciò che crei e pubblichi.

+ + + +
+
+
+ + + + diff --git a/resources/gui/README.md b/resources/gui/README.md new file mode 100644 index 0000000..96b4a66 --- /dev/null +++ b/resources/gui/README.md @@ -0,0 +1,136 @@ +# Asset GUI v2 «Orbita» (generati con FLUX 3 Klein veloce) + +Generati con **FLUX 3 Klein veloce** (`flux3_image_klein_sd_int8_convrot.safetensors`, 8 passi, CFG 1, euler/simple), +encoder **Qwen3-VL 4B** (`qwen3vl_4b_int8_convrot.safetensors`, tipo `flux3`), VAE **FLUX.2** (`flux2-vae.safetensors`), +con l'engine ComfyUI di DaProdImage. Script: `scripts/gui-v2/genera_klein.py`. Spec: `docs/gui-v2-spec.md` §13. + +Stile comune: icone/tessere con il prefisso «polished 3D stylized icon …» (spec §13); sfondi, luoghi e ricette con +«cinematic digital art» o «stylized 3D illustration» (stessa palette, niente «icon»); stili (§7.4) con la scena fissa +«a young woman in a red scarf holding a coffee cup on a city street at sunset» + frammento dello stile + «full frame, no black bars», +senza prefisso. Seed fisso per gruppo (campo `seed`). + +Trasparenza: sfondo scuro uniforme → alpha dalla distanza di colore dal fondo (rampa 70→140, sotto 0,12 zero), buchi chiusi +del soggetto riempiti; stelle → alpha dalla luminosità su nero. **BiRefNet non è stato usato**: la scontornatura è una stima, +non una segmentazione. Le righe con `BiRefNet: sì` vanno rifatte con BiRefNet quando sarà disponibile nell'engine usato. +Gli stili con bande nere (cinema 2,39:1) sono ritagliati al centro quadrato. + +Il visore e le parti interne aperte verso il bordo possono restare trasparenti: limite approvato nel set di prova. +Negativo vuoto per tutti gli asset (CFG 1). `Originale` indica la misura generata prima del ritaglio/ridimensionamento. +Rifiniture CPU: quattro anteprime (moda-editoriale, synthwave, spaziale60, svizzero) ritagliate per togliere scritte/loghi spuri; +coordinate nel manifest (`postproduzione`). Tile stelle: alpha a 4 livelli e tinta bianco caldo per stare sotto 350 kB. + +| ID | File | Gruppo | Originale | Misura | Passi | Seed | Modello (UNET) | Alpha | BiRefNet | Prompt (completo) | Negativo | Data | Secondi | +|---|---|---|---|---|---:|---:|---|---|---|---|---|---|---:| +| `animali/cane` | `animali/cane.webp` | animali | 1024×1024 | 128×128 | 8 | 8181 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a friendly 3D stylized dog head, isolated on plain dark background | — | 2026-10-22T19:06:32 | 7.0 | +| `animali/cavallo` | `animali/cavallo.webp` | animali | 1024×1024 | 128×128 | 8 | 8181 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a 3D stylized horse head, isolated on plain dark background | — | 2026-10-22T19:06:49 | 7.0 | +| `animali/drago` | `animali/drago.webp` | animali | 1024×1024 | 128×128 | 8 | 8181 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a 3D stylized dragon head, isolated on plain dark background | — | 2026-10-22T19:07:05 | 7.0 | +| `animali/gatto` | `animali/gatto.webp` | animali | 1024×1024 | 128×128 | 8 | 8181 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a friendly 3D stylized cat head, isolated on plain dark background | — | 2026-10-22T19:06:41 | 7.2 | +| `animali/uccello` | `animali/uccello.webp` | animali | 1024×1024 | 128×128 | 8 | 8181 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a 3D stylized bird head with colorful feathers, isolated on plain dark background | — | 2026-10-22T19:06:57 | 7.2 | +| `costumi/astronauta` | `costumi/astronauta.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of an astronaut, isolated on plain dark background | — | 2026-10-22T19:05:19 | 7.1 | +| `costumi/cavaliere` | `costumi/cavaliere.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a medieval knight in helmet, isolated on plain dark background | — | 2026-10-22T19:05:27 | 7.0 | +| `costumi/chef` | `costumi/chef.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a chef with toque, isolated on plain dark background | — | 2026-10-22T19:06:08 | 7.3 | +| `costumi/mago` | `costumi/mago.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a wizard with pointed hat and beard, isolated on plain dark background | — | 2026-10-22T19:06:00 | 7.0 | +| `costumi/medico` | `costumi/medico.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a doctor with stethoscope, isolated on plain dark background | — | 2026-10-22T19:06:16 | 7.1 | +| `costumi/pirata` | `costumi/pirata.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a pirate with hat and eyepatch, isolated on plain dark background | — | 2026-10-22T19:05:35 | 7.2 | +| `costumi/samurai` | `costumi/samurai.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a samurai warrior, isolated on plain dark background | — | 2026-10-22T19:05:52 | 7.2 | +| `costumi/sposa` | `costumi/sposa.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a bride with a veil, isolated on plain dark background | — | 2026-10-22T19:06:24 | 7.2 | +| `costumi/supereroe` | `costumi/supereroe.webp` | costumi | 1024×1024 | 128×128 | 8 | 7171 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a superhero with mask and cape, isolated on plain dark background | — | 2026-10-22T19:05:43 | 7.1 | +| `facce/occhiolino` | `facce/occhiolino.webp` | facce | 1024×1024 | 128×128 | 8 | 9191 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face winking, isolated on plain dark background | — | 2026-10-22T19:07:57 | 7.4 | +| `facce/ridere` | `facce/ridere.webp` | facce | 1024×1024 | 128×128 | 8 | 9191 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face laughing with eyes closed, isolated on plain dark background | — | 2026-10-22T19:07:22 | 7.1 | +| `facce/serio` | `facce/serio.webp` | facce | 1024×1024 | 128×128 | 8 | 9191 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face with a serious look, isolated on plain dark background | — | 2026-10-22T19:07:30 | 7.2 | +| `facce/sorpreso` | `facce/sorpreso.webp` | facce | 1024×1024 | 128×128 | 8 | 9191 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face surprised with open mouth, isolated on plain dark background | — | 2026-10-22T19:07:39 | 8.1 | +| `facce/sorriso` | `facce/sorriso.webp` | facce | 1024×1024 | 128×128 | 8 | 9191 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face smiling, isolated on plain dark background | — | 2026-10-22T19:07:14 | 7.3 | +| `facce/triste` | `facce/triste.webp` | facce | 1024×1024 | 128×128 | 8 | 9191 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face looking sad with a tear, isolated on plain dark background | — | 2026-10-22T19:07:48 | 8.0 | +| `generi/foto` | `generi/foto.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a vintage film camera, isolated on plain dark background | — | 2026-10-22T18:55:10 | 8.0 | +| `generi/fumetto` | `generi/fumetto.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a comic speech bubble with a burst, isolated on plain dark background | — | 2026-10-22T18:56:33 | 8.0 | +| `generi/infografica` | `generi/infografica.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a pie chart and a bar chart floating, isolated on plain dark background | — | 2026-10-22T18:55:52 | 8.1 | +| `generi/invito` | `generi/invito.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, an envelope with a wax seal, isolated on plain dark background | — | 2026-10-22T18:56:44 | 8.0 | +| `generi/libero` | `generi/libero.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a blank canvas with a glowing sparkle, isolated on plain dark background | — | 2026-10-22T18:57:06 | 8.0 | +| `generi/logo` | `generi/logo.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing abstract emblem shield, isolated on plain dark background | — | 2026-10-22T18:56:03 | 8.1 | +| `generi/menu` | `generi/menu.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a restaurant menu card with a fork and a spoon, isolated on plain dark background | — | 2026-10-22T18:56:55 | 8.0 | +| `generi/miniatura` | `generi/miniatura.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a play button over a video frame, isolated on plain dark background | — | 2026-10-22T18:56:23 | 7.3 | +| `generi/poster` | `generi/poster.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a rolled poster with a film reel, isolated on plain dark background | — | 2026-10-22T18:55:42 | 8.0 | +| `generi/prodotto` | `generi/prodotto.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glossy perfume bottle, isolated on plain dark background | — | 2026-10-22T18:55:31 | 8.2 | +| `generi/ritratto` | `generi/ritratto.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a classic oval portrait frame with a silhouette, isolated on plain dark background | — | 2026-10-22T18:55:20 | 8.1 | +| `generi/social` | `generi/social.webp` | generi | 1024×1024 | 256×256 | 8 | 1212 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a smartphone with a heart and a thumbs up, isolated on plain dark background | — | 2026-10-22T18:56:13 | 8.0 | +| `lora/klein` | `lora/klein.webp` | lora | 1024×1024 | 256×256 | 8 | 1414 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a glowing crystal spark, cyan and gold | — | 2026-10-22T19:08:50 | 7.1 | +| `lora/qwen` | `lora/qwen.webp` | lora | 1024×1024 | 256×256 | 8 | 1414 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a glowing spiral of layered light, violet and magenta | — | 2026-10-22T19:08:43 | 8.0 | +| `luoghi/bosco` | `luoghi/bosco.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized forest with glowing fireflies | — | 2026-10-22T19:02:58 | 8.1 | +| `luoghi/citta` | `luoghi/citta.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized city at dusk | — | 2026-10-22T19:02:34 | 8.1 | +| `luoghi/deserto` | `luoghi/deserto.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized desert with dunes | — | 2026-10-22T19:03:06 | 8.0 | +| `luoghi/futuro` | `luoghi/futuro.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized futuristic city of glass towers | — | 2026-10-22T19:03:46 | 8.1 | +| `luoghi/interno` | `luoghi/interno.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized cozy room interior | — | 2026-10-22T19:03:14 | 8.0 | +| `luoghi/montagna` | `luoghi/montagna.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized snowy mountain | — | 2026-10-22T19:02:50 | 7.4 | +| `luoghi/sott-acqua` | `luoghi/sott-acqua.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized underwater coral reef, light rays | — | 2026-10-22T19:03:38 | 7.3 | +| `luoghi/spazio` | `luoghi/spazio.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized space station view | — | 2026-10-22T19:03:30 | 8.0 | +| `luoghi/spiaggia` | `luoghi/spiaggia.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized tropical beach | — | 2026-10-22T19:02:42 | 8.0 | +| `luoghi/studio-bianco` | `luoghi/studio-bianco.webp` | luoghi | 1024×1024 | 160×160 | 8 | 5151 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, an empty white photo studio with a softbox light | — | 2026-10-22T19:03:22 | 8.1 | +| `mascotte/cartello` | `mascotte/cartello.webp` | mascotte | 1024×1024 | 512×512 | 8 | 1313 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cute astronaut holding a sign with an error warning symbol, isolated on plain dark background | — | 2026-10-22T19:08:22 | 8.0 | +| `mascotte/fluttua` | `mascotte/fluttua.webp` | mascotte | 1024×1024 | 512×512 | 8 | 1313 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cute astronaut floating in zero gravity, relaxed pose, arms out, isolated on plain dark background | — | 2026-10-22T19:08:10 | 8.0 | +| `mascotte/saluta` | `mascotte/saluta.webp` | mascotte | 1024×1024 | 512×512 | 8 | 1313 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cute astronaut waving hello, isolated on plain dark background | — | 2026-10-22T19:08:35 | 7.3 | +| `meteo/autunno` | `meteo/autunno.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a falling maple leaf, isolated on plain dark background | — | 2026-10-22T19:05:03 | 7.1 | +| `meteo/estate` | `meteo/estate.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a sunflower, isolated on plain dark background | — | 2026-10-22T19:04:55 | 8.0 | +| `meteo/inverno` | `meteo/inverno.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a snowflake crystal, isolated on plain dark background | — | 2026-10-22T19:05:11 | 7.2 | +| `meteo/nebbia` | `meteo/nebbia.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a foggy cloud bank, isolated on plain dark background | — | 2026-10-22T19:04:29 | 7.1 | +| `meteo/neve` | `meteo/neve.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cloud with snowflakes, isolated on plain dark background | — | 2026-10-22T19:04:20 | 8.0 | +| `meteo/nuvole` | `meteo/nuvole.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, fluffy clouds, isolated on plain dark background | — | 2026-10-22T19:04:03 | 7.0 | +| `meteo/pioggia` | `meteo/pioggia.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cloud with rain drops, isolated on plain dark background | — | 2026-10-22T19:04:11 | 7.4 | +| `meteo/primavera` | `meteo/primavera.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a blossom branch, isolated on plain dark background | — | 2026-10-22T19:04:45 | 7.0 | +| `meteo/sole` | `meteo/sole.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a bright sun, isolated on plain dark background | — | 2026-10-22T19:03:55 | 8.0 | +| `meteo/temporale` | `meteo/temporale.webp` | meteo | 1024×1024 | 128×128 | 8 | 6161 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a dark storm cloud with lightning, isolated on plain dark background | — | 2026-10-22T19:04:37 | 7.1 | +| `modi/box` | `modi/box.webp` | modi | 1024×1024 | 128×128 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a floating glowing rectangle frame with corner handles, isolated on plain dark background | — | 2026-10-22T18:54:50 | 7.3 | +| `modi/crea` | `modi/crea.webp` | modi | 1024×1024 | 128×128 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing four-point star sparkle, isolated on plain dark background | — | 2026-10-22T18:54:33 | 8.0 | +| `modi/modifica` | `modi/modifica.webp` | modi | 1024×1024 | 128×128 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing pen nib drawing a light trail, isolated on plain dark background | — | 2026-10-22T18:54:41 | 7.0 | +| `modi/ricette` | `modi/ricette.webp` | modi | 1024×1024 | 128×128 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing lightning bolt inside a rounded tile, isolated on plain dark background | — | 2026-10-22T18:54:59 | 8.0 | +| `ricette/foglio` | `ricette/foglio.webp` | ricette | 1024×640 | 512×320 | 8 | 3131 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, a character turnaround sheet floating in space, multiple poses of an astronaut fox, neat grid | — | 2026-10-22T18:31:14 | 6.0 | +| `ricette/formati` | `ricette/formati.webp` | ricette | 1024×640 | 512×320 | 8 | 3131 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, a poster frame, a phone frame and a square frame side by side, glowing outlines | — | 2026-10-22T18:59:46 | 6.1 | +| `ricette/mix` | `ricette/mix.webp` | ricette | 1024×640 | 512×320 | 8 | 3131 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, several floating photo cards merging into one glowing card, collage of mixed elements | — | 2026-10-22T18:59:34 | 6.0 | +| `ricette/mondi` | `ricette/mondi.webp` | ricette | 1024×640 | 512×320 | 8 | 3131 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, the same city street in several parallel worlds, a grid of four glowing portal windows | — | 2026-10-22T18:59:28 | 6.0 | +| `ricette/rinnova` | `ricette/rinnova.webp` | ricette | 1024×640 | 512×320 | 8 | 3131 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, an old faded photograph turning into a sharp glowing photo, restoration sparkle | — | 2026-10-22T18:59:52 | 6.0 | +| `ricette/set` | `ricette/set.webp` | ricette | 1024×640 | 512×320 | 8 | 3131 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, a neat row of sticker icons: a rocket, a planet, a star and a heart | — | 2026-10-22T18:59:40 | 6.0 | +| `sfondi/aurora` | `sfondi/aurora.webp` | sfondi | 2048×512 | 1600×400 | 8 | 2026 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, aurora borealis ribbons in cyan, violet and gold over a dark horizon, wide panoramic banner | — | 2026-10-22T18:58:09 | 8.0 | +| `sfondi/hero-pianeta` | `sfondi/hero-pianeta.webp` | sfondi | 2560×1440 | 2560×1440 | 8 | 2026 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a large planet with glowing atmosphere in the lower left corner, dark space, cinematic, isolated on plain dark background | — | 2026-10-22T18:58:49 | 32.3 | +| `sfondi/nebulosa` | `sfondi/nebulosa.webp` | sfondi | 2560×1440 | 2560×1440 | 8 | 2026 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, deep space nebula in violet and magenta with faint stars, large empty dark area on the right, cinematic, no planet | — | 2026-10-22T18:30:56 | 32.2 | +| `sfondi/nebulosa-calda` | `sfondi/nebulosa-calda.webp` | sfondi | 2560×1440 | 2560×1440 | 8 | 2026 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, warm nebula in gold and magenta with faint stars, open space in the center-left | — | 2026-10-22T18:57:39 | 32.2 | +| `sfondi/setup-pianeta` | `sfondi/setup-pianeta.webp` | sfondi | 2560×1440 | 2560×1440 | 8 | 2026 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a planet with rings in the lower left, glowing atmosphere, dark space, cinematic | — | 2026-10-22T18:59:21 | 32.2 | +| `sfondi/stelle` | `sfondi/stelle.webp` | sfondi | 1024×1024 | 1024×1024 | 8 | 2026 | `flux3_image_klein_sd_int8_convrot.safetensors` | luce | no | sparse small white and gold stars, tiny bright points, seamless | — | 2026-10-22T18:58:01 | 8.1 | +| `stili/acquerello-film` | `stili/acquerello-film.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, hand-painted watercolor animation background, soft gradients, gentle clouds, full frame, no black bars | — | 2026-10-22T19:00:39 | 6.0 | +| `stili/anime-film` | `stili/anime-film.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, high quality anime movie still, painterly backgrounds, soft cinematic lighting, full frame, no black bars | — | 2026-10-22T18:35:40 | 12.0 | +| `stili/aura` | `stili/aura.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, soft airbrushed aura gradients, grain, glowing blurred shapes, dreamy abstract, full frame, no black bars | — | 2026-10-22T19:02:02 | 6.0 | +| `stili/chrome` | `stili/chrome.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, chrome blobject, reflective metal, hyperreal studio HDRI, early-2000s futurism, full frame, no black bars | — | 2026-10-22T19:01:44 | 6.0 | +| `stili/cinema-anam` | `stili/cinema-anam.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, cinematic still, anamorphic lens flares, oval bokeh, rich color grade, 2.39:1 feel, full frame, no black bars | — | 2026-10-22T18:59:58 | 6.1 | +| `stili/collage` | `stili/collage.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, cut-paper collage, layered torn paper edges, mixed textures, full frame, no black bars | — | 2026-10-22T19:01:01 | 6.0 | +| `stili/cyberpunk` | `stili/cyberpunk.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, dense neon cityscape, holograms, rain, high contrast magenta/teal, full frame, no black bars | — | 2026-10-22T19:01:50 | 6.1 | +| `stili/diorama` | `stili/diorama.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, isometric miniature diorama, tilt-shift, clean cube base, soft shadows, full frame, no black bars | — | 2026-10-22T19:01:32 | 6.0 | +| `stili/documentario` | `stili/documentario.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, gritty documentary photograph, available light, slightly desaturated, imperfect framing, full frame, no black bars | — | 2026-10-22T19:00:33 | 5.4 | +| `stili/flash-y2k` | `stili/flash-y2k.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, direct on-camera flash, early-2000s compact digital camera look, harsh highlights, candid party snapshot, full frame, no black bars | — | 2026-10-22T19:00:03 | 5.1 | +| `stili/gouache` | `stili/gouache.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, contemporary gouache illustration, flat shapes, rich matte colors, subtle paper texture, full frame, no black bars | — | 2026-10-22T19:00:49 | 5.0 | +| `stili/linea` | `stili/linea.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, minimal single continuous line drawing on white, a touch of color, full frame, no black bars | — | 2026-10-22T19:01:07 | 6.1 | +| `stili/manga-inchiostro` | `stili/manga-inchiostro.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, black and white manga, screentones, dynamic inked lines, full frame, no black bars | — | 2026-10-22T19:00:44 | 5.4 | +| `stili/mattoncini` | `stili/mattoncini.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, scene built of interlocking plastic bricks and minifigures, full frame, no black bars | — | 2026-10-22T19:01:38 | 6.0 | +| `stili/moda-editoriale` | `stili/moda-editoriale.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, high-fashion editorial photography, studio strobes, bold color blocking, magazine cover composition, full frame, no black bars | — | 2026-10-22T19:00:16 | 6.0 | +| `stili/notte-neon` | `stili/notte-neon.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, night street photography, wet asphalt reflections, magenta and cyan neon signage, full frame, no black bars | — | 2026-10-22T19:00:27 | 6.0 | +| `stili/pellicola35` | `stili/pellicola35.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, shot on 35mm film, visible grain, soft halation, faded blacks, full frame, no black bars | — | 2026-10-22T19:00:10 | 6.0 | +| `stili/pixel` | `stili/pixel.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, detailed 32-bit pixel art, limited palette, dithering, full frame, no black bars | — | 2026-10-22T19:02:09 | 6.0 | +| `stili/plastilina` | `stili/plastilina.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, stop-motion claymation, visible fingerprints, handmade imperfections, full frame, no black bars | — | 2026-10-22T19:01:19 | 6.0 | +| `stili/polaroid` | `stili/polaroid.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, instant film photo, white frame, soft focus, washed warm tones, light leaks, full frame, no black bars | — | 2026-10-22T19:00:21 | 5.5 | +| `stili/risograph` | `stili/risograph.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, two-color risograph print, grainy overprint, misregistration, limited palette, full frame, no black bars | — | 2026-10-22T19:00:55 | 5.4 | +| `stili/soft3d` | `stili/soft3d.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, soft 3D render, rounded shapes, pastel studio light, subsurface scattering, friendly character look, full frame, no black bars | — | 2026-10-22T19:01:13 | 6.0 | +| `stili/solarpunk` | `stili/solarpunk.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, solarpunk utopia, lush green architecture, warm sunlight, art nouveau curves, full frame, no black bars | — | 2026-10-22T19:02:21 | 6.0 | +| `stili/spaziale60` | `stili/spaziale60.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, 1960s space-age illustration, atomic shapes, retro NASA poster palette, full frame, no black bars | — | 2026-10-22T19:02:15 | 6.1 | +| `stili/svizzero` | `stili/svizzero.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, Swiss international typographic poster, strong grid, flat color, bold sans-serif, full frame, no black bars | — | 2026-10-22T19:02:26 | 5.0 | +| `stili/synthwave` | `stili/synthwave.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, retro-futuristic 80s sunset grid, chrome lettering, purple-pink gradient sky, full frame, no black bars | — | 2026-10-22T19:01:56 | 6.0 | +| `stili/vetro` | `stili/vetro.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, liquid glass and crystal, refractive translucent material, caustics, iridescent edges, full frame, no black bars | — | 2026-10-22T18:35:52 | 12.1 | +| `stili/vinile` | `stili/vinile.webp` | stili | 768×768 | 320×320 | 8 | 4242 | `flux3_image_klein_sd_int8_convrot.safetensors` | — | no | a young woman in a red scarf holding a coffee cup on a city street at sunset, designer vinyl toy, glossy plastic, seam lines, studio product shot, full frame, no black bars | — | 2026-10-22T19:01:26 | 6.0 | +| `tessera/espandi` | `tessera/espandi.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing frame expanding outward with arrows, stars around it, isolated on plain dark background | — | 2026-10-22T18:54:24 | 7.2 | +| `tessera/estrai` | `tessera/estrai.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, scissors cutting a glowing outline around a small planet, sparks, isolated on plain dark background | — | 2026-10-22T18:37:47 | 22.1 | +| `tessera/luce-colore` | `tessera/luce-colore.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glass prism splitting white light into a rainbow of violet, magenta, cyan and gold, isolated on plain dark background | — | 2026-10-22T18:53:42 | 8.0 | +| `tessera/persone` | `tessera/persone.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, an astronaut helmet visor reflecting a smiling face, three-quarter view, isolated on plain dark background | — | 2026-10-22T18:37:22 | 11.1 | +| `tessera/prospettiva` | `tessera/prospettiva.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a camera lens with orbiting rings around it, perspective grid lines, isolated on plain dark background | — | 2026-10-22T18:53:32 | 8.1 | +| `tessera/ritocco` | `tessera/ritocco.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a retouching brush with a soft glowing sparkle, healing light, isolated on plain dark background | — | 2026-10-22T18:54:14 | 8.0 | +| `tessera/situazioni` | `tessera/situazioni.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing globe showing mountains and a sunrise on its edge, tiny landscape, isolated on plain dark background | — | 2026-10-22T18:53:21 | 8.0 | +| `tessera/stili` | `tessera/stili.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a paint palette with a brush and a swirl of paint, floating, isolated on plain dark background | — | 2026-10-22T18:53:53 | 8.1 | +| `tessera/testi` | `tessera/testi.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, floating letter blocks, a glowing letter T and an O, typographic cubes, isolated on plain dark background | — | 2026-10-22T18:54:03 | 8.1 | +| `tessera/vestiti` | `tessera/vestiti.webp` | tessera | 1024×1024 | 256×256 | 8 | 1111 | `flux3_image_klein_sd_int8_convrot.safetensors` | chiave | sì | polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a folded jacket and a hanger floating in space, fabric with soft gloss, isolated on plain dark background | — | 2026-10-22T18:53:10 | 10.1 | + +Totale: 111 asset. Modello: `flux3_image_klein_sd_int8_convrot.safetensors`, encoder `qwen3vl_4b_int8_convrot.safetensors`, VAE `flux2-vae.safetensors`. diff --git a/resources/gui/animali/cane.webp b/resources/gui/animali/cane.webp new file mode 100644 index 0000000..053b09c Binary files /dev/null and b/resources/gui/animali/cane.webp differ diff --git a/resources/gui/animali/cavallo.webp b/resources/gui/animali/cavallo.webp new file mode 100644 index 0000000..b644a13 Binary files /dev/null and b/resources/gui/animali/cavallo.webp differ diff --git a/resources/gui/animali/drago.webp b/resources/gui/animali/drago.webp new file mode 100644 index 0000000..0ece90e Binary files /dev/null and b/resources/gui/animali/drago.webp differ diff --git a/resources/gui/animali/gatto.webp b/resources/gui/animali/gatto.webp new file mode 100644 index 0000000..c98328a Binary files /dev/null and b/resources/gui/animali/gatto.webp differ diff --git a/resources/gui/animali/uccello.webp b/resources/gui/animali/uccello.webp new file mode 100644 index 0000000..e7be856 Binary files /dev/null and b/resources/gui/animali/uccello.webp differ diff --git a/resources/gui/costumi/astronauta.webp b/resources/gui/costumi/astronauta.webp new file mode 100644 index 0000000..811ef4c Binary files /dev/null and b/resources/gui/costumi/astronauta.webp differ diff --git a/resources/gui/costumi/cavaliere.webp b/resources/gui/costumi/cavaliere.webp new file mode 100644 index 0000000..d0e5ad8 Binary files /dev/null and b/resources/gui/costumi/cavaliere.webp differ diff --git a/resources/gui/costumi/chef.webp b/resources/gui/costumi/chef.webp new file mode 100644 index 0000000..ceb36a2 Binary files /dev/null and b/resources/gui/costumi/chef.webp differ diff --git a/resources/gui/costumi/mago.webp b/resources/gui/costumi/mago.webp new file mode 100644 index 0000000..3d27301 Binary files /dev/null and b/resources/gui/costumi/mago.webp differ diff --git a/resources/gui/costumi/medico.webp b/resources/gui/costumi/medico.webp new file mode 100644 index 0000000..88a1dd2 Binary files /dev/null and b/resources/gui/costumi/medico.webp differ diff --git a/resources/gui/costumi/pirata.webp b/resources/gui/costumi/pirata.webp new file mode 100644 index 0000000..9347c68 Binary files /dev/null and b/resources/gui/costumi/pirata.webp differ diff --git a/resources/gui/costumi/samurai.webp b/resources/gui/costumi/samurai.webp new file mode 100644 index 0000000..91ff9e4 Binary files /dev/null and b/resources/gui/costumi/samurai.webp differ diff --git a/resources/gui/costumi/sposa.webp b/resources/gui/costumi/sposa.webp new file mode 100644 index 0000000..c09d25f Binary files /dev/null and b/resources/gui/costumi/sposa.webp differ diff --git a/resources/gui/costumi/supereroe.webp b/resources/gui/costumi/supereroe.webp new file mode 100644 index 0000000..cfdd7b9 Binary files /dev/null and b/resources/gui/costumi/supereroe.webp differ diff --git a/resources/gui/facce/occhiolino.webp b/resources/gui/facce/occhiolino.webp new file mode 100644 index 0000000..ab6e49c Binary files /dev/null and b/resources/gui/facce/occhiolino.webp differ diff --git a/resources/gui/facce/ridere.webp b/resources/gui/facce/ridere.webp new file mode 100644 index 0000000..20a87e4 Binary files /dev/null and b/resources/gui/facce/ridere.webp differ diff --git a/resources/gui/facce/serio.webp b/resources/gui/facce/serio.webp new file mode 100644 index 0000000..c6f62a3 Binary files /dev/null and b/resources/gui/facce/serio.webp differ diff --git a/resources/gui/facce/sorpreso.webp b/resources/gui/facce/sorpreso.webp new file mode 100644 index 0000000..9417dc5 Binary files /dev/null and b/resources/gui/facce/sorpreso.webp differ diff --git a/resources/gui/facce/sorriso.webp b/resources/gui/facce/sorriso.webp new file mode 100644 index 0000000..527278f Binary files /dev/null and b/resources/gui/facce/sorriso.webp differ diff --git a/resources/gui/facce/triste.webp b/resources/gui/facce/triste.webp new file mode 100644 index 0000000..610f921 Binary files /dev/null and b/resources/gui/facce/triste.webp differ diff --git a/resources/gui/generi/foto.webp b/resources/gui/generi/foto.webp new file mode 100644 index 0000000..0d11fae Binary files /dev/null and b/resources/gui/generi/foto.webp differ diff --git a/resources/gui/generi/fumetto.webp b/resources/gui/generi/fumetto.webp new file mode 100644 index 0000000..51a28c9 Binary files /dev/null and b/resources/gui/generi/fumetto.webp differ diff --git a/resources/gui/generi/infografica.webp b/resources/gui/generi/infografica.webp new file mode 100644 index 0000000..5b57ae7 Binary files /dev/null and b/resources/gui/generi/infografica.webp differ diff --git a/resources/gui/generi/invito.webp b/resources/gui/generi/invito.webp new file mode 100644 index 0000000..399b702 Binary files /dev/null and b/resources/gui/generi/invito.webp differ diff --git a/resources/gui/generi/libero.webp b/resources/gui/generi/libero.webp new file mode 100644 index 0000000..0c8e85b Binary files /dev/null and b/resources/gui/generi/libero.webp differ diff --git a/resources/gui/generi/logo.webp b/resources/gui/generi/logo.webp new file mode 100644 index 0000000..499d605 Binary files /dev/null and b/resources/gui/generi/logo.webp differ diff --git a/resources/gui/generi/menu.webp b/resources/gui/generi/menu.webp new file mode 100644 index 0000000..6c83b26 Binary files /dev/null and b/resources/gui/generi/menu.webp differ diff --git a/resources/gui/generi/miniatura.webp b/resources/gui/generi/miniatura.webp new file mode 100644 index 0000000..286186f Binary files /dev/null and b/resources/gui/generi/miniatura.webp differ diff --git a/resources/gui/generi/poster.webp b/resources/gui/generi/poster.webp new file mode 100644 index 0000000..3b4c6ec Binary files /dev/null and b/resources/gui/generi/poster.webp differ diff --git a/resources/gui/generi/prodotto.webp b/resources/gui/generi/prodotto.webp new file mode 100644 index 0000000..3ae90a6 Binary files /dev/null and b/resources/gui/generi/prodotto.webp differ diff --git a/resources/gui/generi/ritratto.webp b/resources/gui/generi/ritratto.webp new file mode 100644 index 0000000..03a7185 Binary files /dev/null and b/resources/gui/generi/ritratto.webp differ diff --git a/resources/gui/generi/social.webp b/resources/gui/generi/social.webp new file mode 100644 index 0000000..bfde99a Binary files /dev/null and b/resources/gui/generi/social.webp differ diff --git a/resources/gui/lora/klein.webp b/resources/gui/lora/klein.webp new file mode 100644 index 0000000..950b122 Binary files /dev/null and b/resources/gui/lora/klein.webp differ diff --git a/resources/gui/lora/qwen.webp b/resources/gui/lora/qwen.webp new file mode 100644 index 0000000..e8e2cc8 Binary files /dev/null and b/resources/gui/lora/qwen.webp differ diff --git a/resources/gui/luoghi/bosco.webp b/resources/gui/luoghi/bosco.webp new file mode 100644 index 0000000..e1d2eb8 Binary files /dev/null and b/resources/gui/luoghi/bosco.webp differ diff --git a/resources/gui/luoghi/citta.webp b/resources/gui/luoghi/citta.webp new file mode 100644 index 0000000..b59b760 Binary files /dev/null and b/resources/gui/luoghi/citta.webp differ diff --git a/resources/gui/luoghi/deserto.webp b/resources/gui/luoghi/deserto.webp new file mode 100644 index 0000000..405a3ed Binary files /dev/null and b/resources/gui/luoghi/deserto.webp differ diff --git a/resources/gui/luoghi/futuro.webp b/resources/gui/luoghi/futuro.webp new file mode 100644 index 0000000..ed2af6f Binary files /dev/null and b/resources/gui/luoghi/futuro.webp differ diff --git a/resources/gui/luoghi/interno.webp b/resources/gui/luoghi/interno.webp new file mode 100644 index 0000000..06527cd Binary files /dev/null and b/resources/gui/luoghi/interno.webp differ diff --git a/resources/gui/luoghi/montagna.webp b/resources/gui/luoghi/montagna.webp new file mode 100644 index 0000000..5f74fbd Binary files /dev/null and b/resources/gui/luoghi/montagna.webp differ diff --git a/resources/gui/luoghi/sott-acqua.webp b/resources/gui/luoghi/sott-acqua.webp new file mode 100644 index 0000000..e2d2491 Binary files /dev/null and b/resources/gui/luoghi/sott-acqua.webp differ diff --git a/resources/gui/luoghi/spazio.webp b/resources/gui/luoghi/spazio.webp new file mode 100644 index 0000000..bdcc5b5 Binary files /dev/null and b/resources/gui/luoghi/spazio.webp differ diff --git a/resources/gui/luoghi/spiaggia.webp b/resources/gui/luoghi/spiaggia.webp new file mode 100644 index 0000000..d31eb6b Binary files /dev/null and b/resources/gui/luoghi/spiaggia.webp differ diff --git a/resources/gui/luoghi/studio-bianco.webp b/resources/gui/luoghi/studio-bianco.webp new file mode 100644 index 0000000..7d09be1 Binary files /dev/null and b/resources/gui/luoghi/studio-bianco.webp differ diff --git a/resources/gui/manifest.json b/resources/gui/manifest.json new file mode 100644 index 0000000..02a2ac8 --- /dev/null +++ b/resources/gui/manifest.json @@ -0,0 +1,3392 @@ +{ + "modello": { + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple" + }, + "asset": { + "tessera/persone": { + "id": "tessera/persone", + "gruppo": "tessera", + "file": "tessera/persone.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, an astronaut helmet visor reflecting a smiling face, three-quarter view, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:37:22", + "secondi": 11.1, + "sha256_16": "e87b0d31cd852523", + "byte": 22774, + "da_rifare_con_birefnet": true + }, + "tessera/estrai": { + "id": "tessera/estrai", + "gruppo": "tessera", + "file": "tessera/estrai.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, scissors cutting a glowing outline around a small planet, sparks, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:37:47", + "secondi": 22.1, + "sha256_16": "debbca2d5e6cb1c7", + "byte": 14916, + "da_rifare_con_birefnet": true + }, + "sfondi/nebulosa": { + "id": "sfondi/nebulosa", + "gruppo": "sfondi", + "file": "sfondi/nebulosa.webp", + "misura": [ + 2560, + 1440 + ], + "generato": [ + 2560, + 1440 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, deep space nebula in violet and magenta with faint stars, large empty dark area on the right, cinematic, no planet", + "negativo": "", + "seme": 2026, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:30:56", + "secondi": 32.2, + "sha256_16": "2c9fbac619c0c395", + "byte": 99208, + "da_rifare_con_birefnet": false + }, + "stili/anime-film": { + "id": "stili/anime-film", + "gruppo": "stili", + "file": "stili/anime-film.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, high quality anime movie still, painterly backgrounds, soft cinematic lighting, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:35:40", + "secondi": 12.0, + "sha256_16": "b6fc603334ae2535", + "byte": 17362, + "da_rifare_con_birefnet": false + }, + "stili/vetro": { + "id": "stili/vetro", + "gruppo": "stili", + "file": "stili/vetro.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, liquid glass and crystal, refractive translucent material, caustics, iridescent edges, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:35:52", + "secondi": 12.1, + "sha256_16": "e62b0be0adae51f8", + "byte": 20852, + "da_rifare_con_birefnet": false + }, + "ricette/foglio": { + "id": "ricette/foglio", + "gruppo": "ricette", + "file": "ricette/foglio.webp", + "misura": [ + 512, + 320 + ], + "generato": [ + 1024, + 640 + ], + "prompt": "stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, a character turnaround sheet floating in space, multiple poses of an astronaut fox, neat grid", + "negativo": "", + "seme": 3131, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:31:14", + "secondi": 6.0, + "sha256_16": "4730af2b50ed2d23", + "byte": 31778, + "da_rifare_con_birefnet": false + }, + "tessera/vestiti": { + "id": "tessera/vestiti", + "gruppo": "tessera", + "file": "tessera/vestiti.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a folded jacket and a hanger floating in space, fabric with soft gloss, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:53:10", + "secondi": 10.1, + "sha256_16": "11bc552c633b3188", + "byte": 18520, + "da_rifare_con_birefnet": true + }, + "tessera/situazioni": { + "id": "tessera/situazioni", + "gruppo": "tessera", + "file": "tessera/situazioni.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing globe showing mountains and a sunrise on its edge, tiny landscape, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:53:21", + "secondi": 8.0, + "sha256_16": "70ea369586acdbfc", + "byte": 18928, + "da_rifare_con_birefnet": true + }, + "tessera/prospettiva": { + "id": "tessera/prospettiva", + "gruppo": "tessera", + "file": "tessera/prospettiva.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a camera lens with orbiting rings around it, perspective grid lines, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:53:32", + "secondi": 8.1, + "sha256_16": "79fc6ef637e81975", + "byte": 25346, + "da_rifare_con_birefnet": true + }, + "tessera/luce-colore": { + "id": "tessera/luce-colore", + "gruppo": "tessera", + "file": "tessera/luce-colore.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glass prism splitting white light into a rainbow of violet, magenta, cyan and gold, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:53:42", + "secondi": 8.0, + "sha256_16": "664ed5b8393adb06", + "byte": 13824, + "da_rifare_con_birefnet": true + }, + "tessera/stili": { + "id": "tessera/stili", + "gruppo": "tessera", + "file": "tessera/stili.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a paint palette with a brush and a swirl of paint, floating, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:53:53", + "secondi": 8.1, + "sha256_16": "d6948bbcfdf18a99", + "byte": 21006, + "da_rifare_con_birefnet": true + }, + "tessera/testi": { + "id": "tessera/testi", + "gruppo": "tessera", + "file": "tessera/testi.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, floating letter blocks, a glowing letter T and an O, typographic cubes, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:54:03", + "secondi": 8.1, + "sha256_16": "18c192fb1b78a69a", + "byte": 16638, + "da_rifare_con_birefnet": true + }, + "tessera/ritocco": { + "id": "tessera/ritocco", + "gruppo": "tessera", + "file": "tessera/ritocco.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a retouching brush with a soft glowing sparkle, healing light, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:54:14", + "secondi": 8.0, + "sha256_16": "36e4d8a2b7287e98", + "byte": 21276, + "da_rifare_con_birefnet": true + }, + "tessera/espandi": { + "id": "tessera/espandi", + "gruppo": "tessera", + "file": "tessera/espandi.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing frame expanding outward with arrows, stars around it, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:54:24", + "secondi": 7.2, + "sha256_16": "b966a8feacc0d942", + "byte": 22092, + "da_rifare_con_birefnet": true + }, + "modi/crea": { + "id": "modi/crea", + "gruppo": "modi", + "file": "modi/crea.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing four-point star sparkle, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:54:33", + "secondi": 8.0, + "sha256_16": "3ccf1e26f9946ef7", + "byte": 8164, + "da_rifare_con_birefnet": true + }, + "modi/modifica": { + "id": "modi/modifica", + "gruppo": "modi", + "file": "modi/modifica.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing pen nib drawing a light trail, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:54:41", + "secondi": 7.0, + "sha256_16": "64acf81a932de8e6", + "byte": 6604, + "da_rifare_con_birefnet": true + }, + "modi/box": { + "id": "modi/box", + "gruppo": "modi", + "file": "modi/box.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a floating glowing rectangle frame with corner handles, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:54:50", + "secondi": 7.3, + "sha256_16": "ec2df88b4d3ed2eb", + "byte": 6808, + "da_rifare_con_birefnet": true + }, + "modi/ricette": { + "id": "modi/ricette", + "gruppo": "modi", + "file": "modi/ricette.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing lightning bolt inside a rounded tile, isolated on plain dark background", + "negativo": "", + "seme": 1111, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:54:59", + "secondi": 8.0, + "sha256_16": "a63a3cb472715685", + "byte": 4822, + "da_rifare_con_birefnet": true + }, + "generi/foto": { + "id": "generi/foto", + "gruppo": "generi", + "file": "generi/foto.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a vintage film camera, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:55:10", + "secondi": 8.0, + "sha256_16": "8d4c22b35f8ff514", + "byte": 19442, + "da_rifare_con_birefnet": true + }, + "generi/ritratto": { + "id": "generi/ritratto", + "gruppo": "generi", + "file": "generi/ritratto.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a classic oval portrait frame with a silhouette, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:55:20", + "secondi": 8.1, + "sha256_16": "dbb9b7b50d14ffd9", + "byte": 16104, + "da_rifare_con_birefnet": true + }, + "generi/prodotto": { + "id": "generi/prodotto", + "gruppo": "generi", + "file": "generi/prodotto.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glossy perfume bottle, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:55:31", + "secondi": 8.2, + "sha256_16": "3d2ec218f22797e3", + "byte": 13466, + "da_rifare_con_birefnet": true + }, + "generi/poster": { + "id": "generi/poster", + "gruppo": "generi", + "file": "generi/poster.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a rolled poster with a film reel, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:55:42", + "secondi": 8.0, + "sha256_16": "3ff60af1f0025dab", + "byte": 17080, + "da_rifare_con_birefnet": true + }, + "generi/infografica": { + "id": "generi/infografica", + "gruppo": "generi", + "file": "generi/infografica.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a pie chart and a bar chart floating, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:55:52", + "secondi": 8.1, + "sha256_16": "1e0ea88b310ed4a5", + "byte": 9212, + "da_rifare_con_birefnet": true + }, + "generi/logo": { + "id": "generi/logo", + "gruppo": "generi", + "file": "generi/logo.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a glowing abstract emblem shield, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:56:03", + "secondi": 8.1, + "sha256_16": "791c0621b3b7f5f4", + "byte": 16550, + "da_rifare_con_birefnet": true + }, + "generi/social": { + "id": "generi/social", + "gruppo": "generi", + "file": "generi/social.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a smartphone with a heart and a thumbs up, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:56:13", + "secondi": 8.0, + "sha256_16": "d0e755c731d7961e", + "byte": 12020, + "da_rifare_con_birefnet": true + }, + "generi/miniatura": { + "id": "generi/miniatura", + "gruppo": "generi", + "file": "generi/miniatura.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a play button over a video frame, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:56:23", + "secondi": 7.3, + "sha256_16": "f7e24213fec5b87d", + "byte": 15494, + "da_rifare_con_birefnet": true + }, + "generi/fumetto": { + "id": "generi/fumetto", + "gruppo": "generi", + "file": "generi/fumetto.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a comic speech bubble with a burst, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:56:33", + "secondi": 8.0, + "sha256_16": "cc6499ed6e450b5b", + "byte": 10166, + "da_rifare_con_birefnet": true + }, + "generi/invito": { + "id": "generi/invito", + "gruppo": "generi", + "file": "generi/invito.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, an envelope with a wax seal, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:56:44", + "secondi": 8.0, + "sha256_16": "396238896ae9faa1", + "byte": 13818, + "da_rifare_con_birefnet": true + }, + "generi/menu": { + "id": "generi/menu", + "gruppo": "generi", + "file": "generi/menu.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a restaurant menu card with a fork and a spoon, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:56:55", + "secondi": 8.0, + "sha256_16": "83df4f02c06489d3", + "byte": 17262, + "da_rifare_con_birefnet": true + }, + "generi/libero": { + "id": "generi/libero", + "gruppo": "generi", + "file": "generi/libero.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a blank canvas with a glowing sparkle, isolated on plain dark background", + "negativo": "", + "seme": 1212, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:57:06", + "secondi": 8.0, + "sha256_16": "09a464b55e918a58", + "byte": 17100, + "da_rifare_con_birefnet": true + }, + "sfondi/nebulosa-calda": { + "id": "sfondi/nebulosa-calda", + "gruppo": "sfondi", + "file": "sfondi/nebulosa-calda.webp", + "misura": [ + 2560, + 1440 + ], + "generato": [ + 2560, + 1440 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, warm nebula in gold and magenta with faint stars, open space in the center-left", + "negativo": "", + "seme": 2026, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:57:39", + "secondi": 32.2, + "sha256_16": "4a20a35fc8768c3a", + "byte": 124162, + "da_rifare_con_birefnet": false + }, + "sfondi/stelle": { + "id": "sfondi/stelle", + "gruppo": "sfondi", + "file": "sfondi/stelle.webp", + "misura": [ + 1024, + 1024 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "sparse small white and gold stars, tiny bright points, seamless", + "negativo": "", + "seme": 2026, + "alpha": "luce", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:58:01", + "secondi": 8.1, + "sha256_16": "a494ce62ef204e72", + "byte": 147362, + "da_rifare_con_birefnet": false, + "postproduzione": "Alpha quantizzato a 4 livelli e tinta bianco caldo uniforme per eliminare il rumore e rispettare il budget di 350 kB" + }, + "sfondi/aurora": { + "id": "sfondi/aurora", + "gruppo": "sfondi", + "file": "sfondi/aurora.webp", + "misura": [ + 1600, + 400 + ], + "generato": [ + 2048, + 512 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, aurora borealis ribbons in cyan, violet and gold over a dark horizon, wide panoramic banner", + "negativo": "", + "seme": 2026, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:58:09", + "secondi": 8.0, + "sha256_16": "0a6a786d7e7bb86f", + "byte": 28574, + "da_rifare_con_birefnet": false + }, + "sfondi/hero-pianeta": { + "id": "sfondi/hero-pianeta", + "gruppo": "sfondi", + "file": "sfondi/hero-pianeta.webp", + "misura": [ + 2560, + 1440 + ], + "generato": [ + 2560, + 1440 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a large planet with glowing atmosphere in the lower left corner, dark space, cinematic, isolated on plain dark background", + "negativo": "", + "seme": 2026, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:58:49", + "secondi": 32.3, + "sha256_16": "d796fd62bdd47abf", + "byte": 328254, + "da_rifare_con_birefnet": true + }, + "sfondi/setup-pianeta": { + "id": "sfondi/setup-pianeta", + "gruppo": "sfondi", + "file": "sfondi/setup-pianeta.webp", + "misura": [ + 2560, + 1440 + ], + "generato": [ + 2560, + 1440 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a planet with rings in the lower left, glowing atmosphere, dark space, cinematic", + "negativo": "", + "seme": 2026, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:59:21", + "secondi": 32.2, + "sha256_16": "371c8ee82f09187b", + "byte": 65696, + "da_rifare_con_birefnet": false + }, + "ricette/mondi": { + "id": "ricette/mondi", + "gruppo": "ricette", + "file": "ricette/mondi.webp", + "misura": [ + 512, + 320 + ], + "generato": [ + 1024, + 640 + ], + "prompt": "stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, the same city street in several parallel worlds, a grid of four glowing portal windows", + "negativo": "", + "seme": 3131, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:59:28", + "secondi": 6.0, + "sha256_16": "e0e6b563960bd1bf", + "byte": 36384, + "da_rifare_con_birefnet": false + }, + "ricette/mix": { + "id": "ricette/mix", + "gruppo": "ricette", + "file": "ricette/mix.webp", + "misura": [ + 512, + 320 + ], + "generato": [ + 1024, + 640 + ], + "prompt": "stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, several floating photo cards merging into one glowing card, collage of mixed elements", + "negativo": "", + "seme": 3131, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:59:34", + "secondi": 6.0, + "sha256_16": "6cee19ead1ada93d", + "byte": 24792, + "da_rifare_con_birefnet": false + }, + "ricette/set": { + "id": "ricette/set", + "gruppo": "ricette", + "file": "ricette/set.webp", + "misura": [ + 512, + 320 + ], + "generato": [ + 1024, + 640 + ], + "prompt": "stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, a neat row of sticker icons: a rocket, a planet, a star and a heart", + "negativo": "", + "seme": 3131, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:59:40", + "secondi": 6.0, + "sha256_16": "85cc540fde1fd7aa", + "byte": 18514, + "da_rifare_con_birefnet": false + }, + "ricette/formati": { + "id": "ricette/formati", + "gruppo": "ricette", + "file": "ricette/formati.webp", + "misura": [ + 512, + 320 + ], + "generato": [ + 1024, + 640 + ], + "prompt": "stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, a poster frame, a phone frame and a square frame side by side, glowing outlines", + "negativo": "", + "seme": 3131, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:59:46", + "secondi": 6.1, + "sha256_16": "3e64e04bc9b7d676", + "byte": 17976, + "da_rifare_con_birefnet": false + }, + "ricette/rinnova": { + "id": "ricette/rinnova", + "gruppo": "ricette", + "file": "ricette/rinnova.webp", + "misura": [ + 512, + 320 + ], + "generato": [ + 1024, + 640 + ], + "prompt": "stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, clean composition, no text, an old faded photograph turning into a sharp glowing photo, restoration sparkle", + "negativo": "", + "seme": 3131, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:59:52", + "secondi": 6.0, + "sha256_16": "4771134a64a35c43", + "byte": 20462, + "da_rifare_con_birefnet": false + }, + "stili/cinema-anam": { + "id": "stili/cinema-anam", + "gruppo": "stili", + "file": "stili/cinema-anam.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, cinematic still, anamorphic lens flares, oval bokeh, rich color grade, 2.39:1 feel, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T18:59:58", + "secondi": 6.1, + "sha256_16": "24a64d58e08d83b3", + "byte": 12816, + "da_rifare_con_birefnet": false + }, + "stili/flash-y2k": { + "id": "stili/flash-y2k", + "gruppo": "stili", + "file": "stili/flash-y2k.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, direct on-camera flash, early-2000s compact digital camera look, harsh highlights, candid party snapshot, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:03", + "secondi": 5.1, + "sha256_16": "05a154c3067ef7cf", + "byte": 14578, + "da_rifare_con_birefnet": false + }, + "stili/pellicola35": { + "id": "stili/pellicola35", + "gruppo": "stili", + "file": "stili/pellicola35.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, shot on 35mm film, visible grain, soft halation, faded blacks, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:10", + "secondi": 6.0, + "sha256_16": "c8e4d05b9809bfa0", + "byte": 13612, + "da_rifare_con_birefnet": false + }, + "stili/moda-editoriale": { + "id": "stili/moda-editoriale", + "gruppo": "stili", + "file": "stili/moda-editoriale.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, high-fashion editorial photography, studio strobes, bold color blocking, magazine cover composition, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:16", + "secondi": 6.0, + "sha256_16": "78541344ee2c5a78", + "byte": 11844, + "da_rifare_con_birefnet": false, + "postproduzione": { + "ritaglio_senza_scritte": [ + 65, + 0, + 250, + 255 + ], + "uscita": [ + 320, + 320 + ] + } + }, + "stili/polaroid": { + "id": "stili/polaroid", + "gruppo": "stili", + "file": "stili/polaroid.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, instant film photo, white frame, soft focus, washed warm tones, light leaks, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:21", + "secondi": 5.5, + "sha256_16": "fde6ee533c2b254b", + "byte": 7670, + "da_rifare_con_birefnet": false + }, + "stili/notte-neon": { + "id": "stili/notte-neon", + "gruppo": "stili", + "file": "stili/notte-neon.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, night street photography, wet asphalt reflections, magenta and cyan neon signage, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:27", + "secondi": 6.0, + "sha256_16": "71baee80ebf3918c", + "byte": 24802, + "da_rifare_con_birefnet": false + }, + "stili/documentario": { + "id": "stili/documentario", + "gruppo": "stili", + "file": "stili/documentario.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, gritty documentary photograph, available light, slightly desaturated, imperfect framing, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:33", + "secondi": 5.4, + "sha256_16": "892cb3eba233d36b", + "byte": 13344, + "da_rifare_con_birefnet": false + }, + "stili/acquerello-film": { + "id": "stili/acquerello-film", + "gruppo": "stili", + "file": "stili/acquerello-film.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, hand-painted watercolor animation background, soft gradients, gentle clouds, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:39", + "secondi": 6.0, + "sha256_16": "cd5563ecb147d2a6", + "byte": 18160, + "da_rifare_con_birefnet": false + }, + "stili/manga-inchiostro": { + "id": "stili/manga-inchiostro", + "gruppo": "stili", + "file": "stili/manga-inchiostro.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, black and white manga, screentones, dynamic inked lines, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:44", + "secondi": 5.4, + "sha256_16": "a004e5a3e448bbac", + "byte": 37172, + "da_rifare_con_birefnet": false + }, + "stili/gouache": { + "id": "stili/gouache", + "gruppo": "stili", + "file": "stili/gouache.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, contemporary gouache illustration, flat shapes, rich matte colors, subtle paper texture, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:49", + "secondi": 5.0, + "sha256_16": "e6ac3ccde775a052", + "byte": 21256, + "da_rifare_con_birefnet": false + }, + "stili/risograph": { + "id": "stili/risograph", + "gruppo": "stili", + "file": "stili/risograph.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, two-color risograph print, grainy overprint, misregistration, limited palette, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:00:55", + "secondi": 5.4, + "sha256_16": "de51142242046827", + "byte": 20276, + "da_rifare_con_birefnet": false + }, + "stili/collage": { + "id": "stili/collage", + "gruppo": "stili", + "file": "stili/collage.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, cut-paper collage, layered torn paper edges, mixed textures, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:01", + "secondi": 6.0, + "sha256_16": "d99e344203e4c90c", + "byte": 21290, + "da_rifare_con_birefnet": false + }, + "stili/linea": { + "id": "stili/linea", + "gruppo": "stili", + "file": "stili/linea.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, minimal single continuous line drawing on white, a touch of color, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:07", + "secondi": 6.1, + "sha256_16": "baa2102383618f53", + "byte": 18994, + "da_rifare_con_birefnet": false + }, + "stili/soft3d": { + "id": "stili/soft3d", + "gruppo": "stili", + "file": "stili/soft3d.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, soft 3D render, rounded shapes, pastel studio light, subsurface scattering, friendly character look, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:13", + "secondi": 6.0, + "sha256_16": "b2bfcf5364a9c404", + "byte": 15016, + "da_rifare_con_birefnet": false + }, + "stili/plastilina": { + "id": "stili/plastilina", + "gruppo": "stili", + "file": "stili/plastilina.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, stop-motion claymation, visible fingerprints, handmade imperfections, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:19", + "secondi": 6.0, + "sha256_16": "08fc493b0588be82", + "byte": 14878, + "da_rifare_con_birefnet": false + }, + "stili/vinile": { + "id": "stili/vinile", + "gruppo": "stili", + "file": "stili/vinile.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, designer vinyl toy, glossy plastic, seam lines, studio product shot, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:26", + "secondi": 6.0, + "sha256_16": "3b023deaba6e7197", + "byte": 20350, + "da_rifare_con_birefnet": false + }, + "stili/diorama": { + "id": "stili/diorama", + "gruppo": "stili", + "file": "stili/diorama.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, isometric miniature diorama, tilt-shift, clean cube base, soft shadows, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:32", + "secondi": 6.0, + "sha256_16": "c8bad91a212f9eb1", + "byte": 10356, + "da_rifare_con_birefnet": false + }, + "stili/mattoncini": { + "id": "stili/mattoncini", + "gruppo": "stili", + "file": "stili/mattoncini.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, scene built of interlocking plastic bricks and minifigures, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:38", + "secondi": 6.0, + "sha256_16": "84bb1d2473a7a588", + "byte": 26882, + "da_rifare_con_birefnet": false + }, + "stili/chrome": { + "id": "stili/chrome", + "gruppo": "stili", + "file": "stili/chrome.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, chrome blobject, reflective metal, hyperreal studio HDRI, early-2000s futurism, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:44", + "secondi": 6.0, + "sha256_16": "8bdc029c3cfc253c", + "byte": 21750, + "da_rifare_con_birefnet": false + }, + "stili/cyberpunk": { + "id": "stili/cyberpunk", + "gruppo": "stili", + "file": "stili/cyberpunk.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, dense neon cityscape, holograms, rain, high contrast magenta/teal, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:50", + "secondi": 6.1, + "sha256_16": "30ac282d323b276e", + "byte": 29260, + "da_rifare_con_birefnet": false + }, + "stili/synthwave": { + "id": "stili/synthwave", + "gruppo": "stili", + "file": "stili/synthwave.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, retro-futuristic 80s sunset grid, chrome lettering, purple-pink gradient sky, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:01:56", + "secondi": 6.0, + "sha256_16": "901074435d531758", + "byte": 17060, + "da_rifare_con_birefnet": false, + "postproduzione": { + "ritaglio_senza_scritte": [ + 0, + 82, + 320, + 320 + ], + "uscita": [ + 320, + 320 + ] + } + }, + "stili/aura": { + "id": "stili/aura", + "gruppo": "stili", + "file": "stili/aura.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, soft airbrushed aura gradients, grain, glowing blurred shapes, dreamy abstract, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:02", + "secondi": 6.0, + "sha256_16": "6016c192a38fbc83", + "byte": 16320, + "da_rifare_con_birefnet": false + }, + "stili/pixel": { + "id": "stili/pixel", + "gruppo": "stili", + "file": "stili/pixel.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, detailed 32-bit pixel art, limited palette, dithering, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:09", + "secondi": 6.0, + "sha256_16": "354f30e28c30b95f", + "byte": 31740, + "da_rifare_con_birefnet": false + }, + "stili/spaziale60": { + "id": "stili/spaziale60", + "gruppo": "stili", + "file": "stili/spaziale60.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, 1960s space-age illustration, atomic shapes, retro NASA poster palette, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:15", + "secondi": 6.1, + "sha256_16": "08a0190c2edc556a", + "byte": 20466, + "da_rifare_con_birefnet": false, + "postproduzione": { + "ritaglio_senza_scritte": [ + 65, + 52, + 270, + 320 + ], + "uscita": [ + 320, + 320 + ] + } + }, + "stili/solarpunk": { + "id": "stili/solarpunk", + "gruppo": "stili", + "file": "stili/solarpunk.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, solarpunk utopia, lush green architecture, warm sunlight, art nouveau curves, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:21", + "secondi": 6.0, + "sha256_16": "333456a26cea05cd", + "byte": 24116, + "da_rifare_con_birefnet": false + }, + "stili/svizzero": { + "id": "stili/svizzero", + "gruppo": "stili", + "file": "stili/svizzero.webp", + "misura": [ + 320, + 320 + ], + "generato": [ + 768, + 768 + ], + "prompt": "a young woman in a red scarf holding a coffee cup on a city street at sunset, Swiss international typographic poster, strong grid, flat color, bold sans-serif, full frame, no black bars", + "negativo": "", + "seme": 4242, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:26", + "secondi": 5.0, + "sha256_16": "eb17aaa088dded0b", + "byte": 14762, + "da_rifare_con_birefnet": false, + "postproduzione": { + "ritaglio_senza_scritte": [ + 105, + 55, + 235, + 245 + ], + "uscita": [ + 320, + 320 + ] + } + }, + "luoghi/citta": { + "id": "luoghi/citta", + "gruppo": "luoghi", + "file": "luoghi/citta.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized city at dusk", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:34", + "secondi": 8.1, + "sha256_16": "9515c0082818310f", + "byte": 2950, + "da_rifare_con_birefnet": false + }, + "luoghi/spiaggia": { + "id": "luoghi/spiaggia", + "gruppo": "luoghi", + "file": "luoghi/spiaggia.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized tropical beach", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:42", + "secondi": 8.0, + "sha256_16": "ebc18eb853fe5b9b", + "byte": 4574, + "da_rifare_con_birefnet": false + }, + "luoghi/montagna": { + "id": "luoghi/montagna", + "gruppo": "luoghi", + "file": "luoghi/montagna.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized snowy mountain", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:50", + "secondi": 7.4, + "sha256_16": "a93833bd5fbc5d78", + "byte": 3906, + "da_rifare_con_birefnet": false + }, + "luoghi/bosco": { + "id": "luoghi/bosco", + "gruppo": "luoghi", + "file": "luoghi/bosco.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized forest with glowing fireflies", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:02:58", + "secondi": 8.1, + "sha256_16": "ad2e5e892efeda8d", + "byte": 4742, + "da_rifare_con_birefnet": false + }, + "luoghi/deserto": { + "id": "luoghi/deserto", + "gruppo": "luoghi", + "file": "luoghi/deserto.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized desert with dunes", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:03:06", + "secondi": 8.0, + "sha256_16": "814f5f087544763f", + "byte": 2474, + "da_rifare_con_birefnet": false + }, + "luoghi/interno": { + "id": "luoghi/interno", + "gruppo": "luoghi", + "file": "luoghi/interno.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized cozy room interior", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:03:14", + "secondi": 8.0, + "sha256_16": "3f27e1004abc21b8", + "byte": 4068, + "da_rifare_con_birefnet": false + }, + "luoghi/studio-bianco": { + "id": "luoghi/studio-bianco", + "gruppo": "luoghi", + "file": "luoghi/studio-bianco.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, an empty white photo studio with a softbox light", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:03:22", + "secondi": 8.1, + "sha256_16": "bbf1bd6aa23dbb9f", + "byte": 2138, + "da_rifare_con_birefnet": false + }, + "luoghi/spazio": { + "id": "luoghi/spazio", + "gruppo": "luoghi", + "file": "luoghi/spazio.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized space station view", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:03:30", + "secondi": 8.0, + "sha256_16": "c5e15054ab53aeff", + "byte": 3450, + "da_rifare_con_birefnet": false + }, + "luoghi/sott-acqua": { + "id": "luoghi/sott-acqua", + "gruppo": "luoghi", + "file": "luoghi/sott-acqua.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized underwater coral reef, light rays", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:03:38", + "secondi": 7.3, + "sha256_16": "10a75f362398a67c", + "byte": 5134, + "da_rifare_con_birefnet": false + }, + "luoghi/futuro": { + "id": "luoghi/futuro", + "gruppo": "luoghi", + "file": "luoghi/futuro.webp", + "misura": [ + 160, + 160 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a miniature 3D stylized futuristic city of glass towers", + "negativo": "", + "seme": 5151, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:03:46", + "secondi": 8.1, + "sha256_16": "82f1a6abad44c788", + "byte": 4382, + "da_rifare_con_birefnet": false + }, + "meteo/sole": { + "id": "meteo/sole", + "gruppo": "meteo", + "file": "meteo/sole.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a bright sun, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:03:55", + "secondi": 8.0, + "sha256_16": "c9c839625af4121c", + "byte": 6868, + "da_rifare_con_birefnet": true + }, + "meteo/nuvole": { + "id": "meteo/nuvole", + "gruppo": "meteo", + "file": "meteo/nuvole.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, fluffy clouds, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:04:03", + "secondi": 7.0, + "sha256_16": "d5685d0336b95d19", + "byte": 7758, + "da_rifare_con_birefnet": true + }, + "meteo/pioggia": { + "id": "meteo/pioggia", + "gruppo": "meteo", + "file": "meteo/pioggia.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cloud with rain drops, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:04:11", + "secondi": 7.4, + "sha256_16": "04349875f700d14d", + "byte": 4816, + "da_rifare_con_birefnet": true + }, + "meteo/neve": { + "id": "meteo/neve", + "gruppo": "meteo", + "file": "meteo/neve.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cloud with snowflakes, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:04:20", + "secondi": 8.0, + "sha256_16": "0a8a840eab176879", + "byte": 5440, + "da_rifare_con_birefnet": true + }, + "meteo/nebbia": { + "id": "meteo/nebbia", + "gruppo": "meteo", + "file": "meteo/nebbia.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a foggy cloud bank, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:04:29", + "secondi": 7.1, + "sha256_16": "42b83dbcd9c056e3", + "byte": 10990, + "da_rifare_con_birefnet": true + }, + "meteo/temporale": { + "id": "meteo/temporale", + "gruppo": "meteo", + "file": "meteo/temporale.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a dark storm cloud with lightning, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:04:37", + "secondi": 7.1, + "sha256_16": "63aba8632f8dfd71", + "byte": 7472, + "da_rifare_con_birefnet": true + }, + "meteo/primavera": { + "id": "meteo/primavera", + "gruppo": "meteo", + "file": "meteo/primavera.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a blossom branch, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:04:45", + "secondi": 7.0, + "sha256_16": "c4e37c3eb7a6cf53", + "byte": 7074, + "da_rifare_con_birefnet": true + }, + "meteo/estate": { + "id": "meteo/estate", + "gruppo": "meteo", + "file": "meteo/estate.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a sunflower, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:04:55", + "secondi": 8.0, + "sha256_16": "fddf899a376784be", + "byte": 8824, + "da_rifare_con_birefnet": true + }, + "meteo/autunno": { + "id": "meteo/autunno", + "gruppo": "meteo", + "file": "meteo/autunno.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a falling maple leaf, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:05:03", + "secondi": 7.1, + "sha256_16": "ced54192fa8ee05f", + "byte": 4902, + "da_rifare_con_birefnet": true + }, + "meteo/inverno": { + "id": "meteo/inverno", + "gruppo": "meteo", + "file": "meteo/inverno.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a snowflake crystal, isolated on plain dark background", + "negativo": "", + "seme": 6161, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:05:11", + "secondi": 7.2, + "sha256_16": "0ff76abc555b1624", + "byte": 8240, + "da_rifare_con_birefnet": true + }, + "costumi/astronauta": { + "id": "costumi/astronauta", + "gruppo": "costumi", + "file": "costumi/astronauta.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of an astronaut, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:05:19", + "secondi": 7.1, + "sha256_16": "a2f0ccd9c89841ee", + "byte": 7842, + "da_rifare_con_birefnet": true + }, + "costumi/cavaliere": { + "id": "costumi/cavaliere", + "gruppo": "costumi", + "file": "costumi/cavaliere.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a medieval knight in helmet, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:05:27", + "secondi": 7.0, + "sha256_16": "17d996093a58413b", + "byte": 6838, + "da_rifare_con_birefnet": true + }, + "costumi/pirata": { + "id": "costumi/pirata", + "gruppo": "costumi", + "file": "costumi/pirata.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a pirate with hat and eyepatch, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:05:35", + "secondi": 7.2, + "sha256_16": "48024c7cfb895c1d", + "byte": 7968, + "da_rifare_con_birefnet": true + }, + "costumi/supereroe": { + "id": "costumi/supereroe", + "gruppo": "costumi", + "file": "costumi/supereroe.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a superhero with mask and cape, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:05:43", + "secondi": 7.1, + "sha256_16": "9b24d77d05e76b05", + "byte": 8414, + "da_rifare_con_birefnet": true + }, + "costumi/samurai": { + "id": "costumi/samurai", + "gruppo": "costumi", + "file": "costumi/samurai.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a samurai warrior, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:05:52", + "secondi": 7.2, + "sha256_16": "04fd35eb2f97d560", + "byte": 11220, + "da_rifare_con_birefnet": true + }, + "costumi/mago": { + "id": "costumi/mago", + "gruppo": "costumi", + "file": "costumi/mago.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a wizard with pointed hat and beard, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:06:00", + "secondi": 7.0, + "sha256_16": "3466ce1a83bc4fc6", + "byte": 8568, + "da_rifare_con_birefnet": true + }, + "costumi/chef": { + "id": "costumi/chef", + "gruppo": "costumi", + "file": "costumi/chef.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a chef with toque, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:06:08", + "secondi": 7.3, + "sha256_16": "4a6e528ae87cb107", + "byte": 5512, + "da_rifare_con_birefnet": true + }, + "costumi/medico": { + "id": "costumi/medico", + "gruppo": "costumi", + "file": "costumi/medico.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a doctor with stethoscope, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:06:16", + "secondi": 7.1, + "sha256_16": "a889b744cc1e3537", + "byte": 6908, + "da_rifare_con_birefnet": true + }, + "costumi/sposa": { + "id": "costumi/sposa", + "gruppo": "costumi", + "file": "costumi/sposa.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, stylized 3D bust portrait of a bride with a veil, isolated on plain dark background", + "negativo": "", + "seme": 7171, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:06:24", + "secondi": 7.2, + "sha256_16": "79c8c1d05fca84e4", + "byte": 7216, + "da_rifare_con_birefnet": true + }, + "animali/cane": { + "id": "animali/cane", + "gruppo": "animali", + "file": "animali/cane.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a friendly 3D stylized dog head, isolated on plain dark background", + "negativo": "", + "seme": 8181, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:06:32", + "secondi": 7.0, + "sha256_16": "99b56db5dd5f9484", + "byte": 6828, + "da_rifare_con_birefnet": true + }, + "animali/gatto": { + "id": "animali/gatto", + "gruppo": "animali", + "file": "animali/gatto.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a friendly 3D stylized cat head, isolated on plain dark background", + "negativo": "", + "seme": 8181, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:06:41", + "secondi": 7.2, + "sha256_16": "342ff1665a89ce49", + "byte": 7792, + "da_rifare_con_birefnet": true + }, + "animali/cavallo": { + "id": "animali/cavallo", + "gruppo": "animali", + "file": "animali/cavallo.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a 3D stylized horse head, isolated on plain dark background", + "negativo": "", + "seme": 8181, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:06:49", + "secondi": 7.0, + "sha256_16": "1ec1f81b31785874", + "byte": 9528, + "da_rifare_con_birefnet": true + }, + "animali/uccello": { + "id": "animali/uccello", + "gruppo": "animali", + "file": "animali/uccello.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a 3D stylized bird head with colorful feathers, isolated on plain dark background", + "negativo": "", + "seme": 8181, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:06:57", + "secondi": 7.2, + "sha256_16": "278e2dfe9b9b5e2f", + "byte": 7436, + "da_rifare_con_birefnet": true + }, + "animali/drago": { + "id": "animali/drago", + "gruppo": "animali", + "file": "animali/drago.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a 3D stylized dragon head, isolated on plain dark background", + "negativo": "", + "seme": 8181, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:07:05", + "secondi": 7.0, + "sha256_16": "0bba676d16fe7fb3", + "byte": 9626, + "da_rifare_con_birefnet": true + }, + "facce/sorriso": { + "id": "facce/sorriso", + "gruppo": "facce", + "file": "facce/sorriso.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face smiling, isolated on plain dark background", + "negativo": "", + "seme": 9191, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:07:14", + "secondi": 7.3, + "sha256_16": "77169126779a7f8a", + "byte": 7058, + "da_rifare_con_birefnet": true + }, + "facce/ridere": { + "id": "facce/ridere", + "gruppo": "facce", + "file": "facce/ridere.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face laughing with eyes closed, isolated on plain dark background", + "negativo": "", + "seme": 9191, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:07:22", + "secondi": 7.1, + "sha256_16": "e0e4d762901497fb", + "byte": 6244, + "da_rifare_con_birefnet": true + }, + "facce/serio": { + "id": "facce/serio", + "gruppo": "facce", + "file": "facce/serio.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face with a serious look, isolated on plain dark background", + "negativo": "", + "seme": 9191, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:07:30", + "secondi": 7.2, + "sha256_16": "4437e01c9ffe5631", + "byte": 8304, + "da_rifare_con_birefnet": true + }, + "facce/sorpreso": { + "id": "facce/sorpreso", + "gruppo": "facce", + "file": "facce/sorpreso.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face surprised with open mouth, isolated on plain dark background", + "negativo": "", + "seme": 9191, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:07:39", + "secondi": 8.1, + "sha256_16": "aec4db2af23d41d2", + "byte": 6354, + "da_rifare_con_birefnet": true + }, + "facce/triste": { + "id": "facce/triste", + "gruppo": "facce", + "file": "facce/triste.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face looking sad with a tear, isolated on plain dark background", + "negativo": "", + "seme": 9191, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:07:48", + "secondi": 8.0, + "sha256_16": "bd27f865b0c37630", + "byte": 7866, + "da_rifare_con_birefnet": true + }, + "facce/occhiolino": { + "id": "facce/occhiolino", + "gruppo": "facce", + "file": "facce/occhiolino.webp", + "misura": [ + 128, + 128 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a round 3D face winking, isolated on plain dark background", + "negativo": "", + "seme": 9191, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:07:57", + "secondi": 7.4, + "sha256_16": "47bab7353807e386", + "byte": 7248, + "da_rifare_con_birefnet": true + }, + "mascotte/fluttua": { + "id": "mascotte/fluttua", + "gruppo": "mascotte", + "file": "mascotte/fluttua.webp", + "misura": [ + 512, + 512 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cute astronaut floating in zero gravity, relaxed pose, arms out, isolated on plain dark background", + "negativo": "", + "seme": 1313, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:08:10", + "secondi": 8.0, + "sha256_16": "465c5e895a936ace", + "byte": 48044, + "da_rifare_con_birefnet": true + }, + "mascotte/cartello": { + "id": "mascotte/cartello", + "gruppo": "mascotte", + "file": "mascotte/cartello.webp", + "misura": [ + 512, + 512 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cute astronaut holding a sign with an error warning symbol, isolated on plain dark background", + "negativo": "", + "seme": 1313, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:08:22", + "secondi": 8.0, + "sha256_16": "f27305559eebe6ff", + "byte": 63378, + "da_rifare_con_birefnet": true + }, + "mascotte/saluta": { + "id": "mascotte/saluta", + "gruppo": "mascotte", + "file": "mascotte/saluta.webp", + "misura": [ + 512, + 512 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, dark navy background, clean composition, no text, a cute astronaut waving hello, isolated on plain dark background", + "negativo": "", + "seme": 1313, + "alpha": "chiave", + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:08:35", + "secondi": 7.3, + "sha256_16": "85066267ed08a150", + "byte": 59640, + "da_rifare_con_birefnet": true + }, + "lora/qwen": { + "id": "lora/qwen", + "gruppo": "lora", + "file": "lora/qwen.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a glowing spiral of layered light, violet and magenta", + "negativo": "", + "seme": 1414, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:08:43", + "secondi": 8.0, + "sha256_16": "bb15fb9a44296e0c", + "byte": 8178, + "da_rifare_con_birefnet": false + }, + "lora/klein": { + "id": "lora/klein", + "gruppo": "lora", + "file": "lora/klein.webp", + "misura": [ + 256, + 256 + ], + "generato": [ + 1024, + 1024 + ], + "prompt": "cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, clean composition, no text, a glowing crystal spark, cyan and gold", + "negativo": "", + "seme": 1414, + "alpha": null, + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", + "encoder": "qwen3vl_4b_int8_convrot.safetensors", + "vae": "flux2-vae.safetensors", + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", + "modello_file": "flux3_image_klein_sd_int8_convrot.safetensors", + "data": "2026-10-22T19:08:50", + "secondi": 7.1, + "sha256_16": "711f068334b38a52", + "byte": 7304, + "da_rifare_con_birefnet": false + } + } +} diff --git a/resources/gui/mascotte/cartello.webp b/resources/gui/mascotte/cartello.webp new file mode 100644 index 0000000..959f1cb Binary files /dev/null and b/resources/gui/mascotte/cartello.webp differ diff --git a/resources/gui/mascotte/fluttua.webp b/resources/gui/mascotte/fluttua.webp new file mode 100644 index 0000000..9d2da7a Binary files /dev/null and b/resources/gui/mascotte/fluttua.webp differ diff --git a/resources/gui/mascotte/saluta.webp b/resources/gui/mascotte/saluta.webp new file mode 100644 index 0000000..576e8f2 Binary files /dev/null and b/resources/gui/mascotte/saluta.webp differ diff --git a/resources/gui/meteo/autunno.webp b/resources/gui/meteo/autunno.webp new file mode 100644 index 0000000..acb59de Binary files /dev/null and b/resources/gui/meteo/autunno.webp differ diff --git a/resources/gui/meteo/estate.webp b/resources/gui/meteo/estate.webp new file mode 100644 index 0000000..f014275 Binary files /dev/null and b/resources/gui/meteo/estate.webp differ diff --git a/resources/gui/meteo/inverno.webp b/resources/gui/meteo/inverno.webp new file mode 100644 index 0000000..0baee4d Binary files /dev/null and b/resources/gui/meteo/inverno.webp differ diff --git a/resources/gui/meteo/nebbia.webp b/resources/gui/meteo/nebbia.webp new file mode 100644 index 0000000..e48aac4 Binary files /dev/null and b/resources/gui/meteo/nebbia.webp differ diff --git a/resources/gui/meteo/neve.webp b/resources/gui/meteo/neve.webp new file mode 100644 index 0000000..8b18cb8 Binary files /dev/null and b/resources/gui/meteo/neve.webp differ diff --git a/resources/gui/meteo/nuvole.webp b/resources/gui/meteo/nuvole.webp new file mode 100644 index 0000000..67bb779 Binary files /dev/null and b/resources/gui/meteo/nuvole.webp differ diff --git a/resources/gui/meteo/pioggia.webp b/resources/gui/meteo/pioggia.webp new file mode 100644 index 0000000..b135477 Binary files /dev/null and b/resources/gui/meteo/pioggia.webp differ diff --git a/resources/gui/meteo/primavera.webp b/resources/gui/meteo/primavera.webp new file mode 100644 index 0000000..bd21be2 Binary files /dev/null and b/resources/gui/meteo/primavera.webp differ diff --git a/resources/gui/meteo/sole.webp b/resources/gui/meteo/sole.webp new file mode 100644 index 0000000..142c83a Binary files /dev/null and b/resources/gui/meteo/sole.webp differ diff --git a/resources/gui/meteo/temporale.webp b/resources/gui/meteo/temporale.webp new file mode 100644 index 0000000..37171f8 Binary files /dev/null and b/resources/gui/meteo/temporale.webp differ diff --git a/resources/gui/modi/box.webp b/resources/gui/modi/box.webp new file mode 100644 index 0000000..ed2d1fc Binary files /dev/null and b/resources/gui/modi/box.webp differ diff --git a/resources/gui/modi/crea.webp b/resources/gui/modi/crea.webp new file mode 100644 index 0000000..6b1f44b Binary files /dev/null and b/resources/gui/modi/crea.webp differ diff --git a/resources/gui/modi/modifica.webp b/resources/gui/modi/modifica.webp new file mode 100644 index 0000000..2737fe6 Binary files /dev/null and b/resources/gui/modi/modifica.webp differ diff --git a/resources/gui/modi/ricette.webp b/resources/gui/modi/ricette.webp new file mode 100644 index 0000000..3195843 Binary files /dev/null and b/resources/gui/modi/ricette.webp differ diff --git a/resources/gui/ricette/foglio.webp b/resources/gui/ricette/foglio.webp new file mode 100644 index 0000000..76867cb Binary files /dev/null and b/resources/gui/ricette/foglio.webp differ diff --git a/resources/gui/ricette/formati.webp b/resources/gui/ricette/formati.webp new file mode 100644 index 0000000..4dde3cd Binary files /dev/null and b/resources/gui/ricette/formati.webp differ diff --git a/resources/gui/ricette/mix.webp b/resources/gui/ricette/mix.webp new file mode 100644 index 0000000..ef7deb9 Binary files /dev/null and b/resources/gui/ricette/mix.webp differ diff --git a/resources/gui/ricette/mondi.webp b/resources/gui/ricette/mondi.webp new file mode 100644 index 0000000..d135ed0 Binary files /dev/null and b/resources/gui/ricette/mondi.webp differ diff --git a/resources/gui/ricette/rinnova.webp b/resources/gui/ricette/rinnova.webp new file mode 100644 index 0000000..9174449 Binary files /dev/null and b/resources/gui/ricette/rinnova.webp differ diff --git a/resources/gui/ricette/set.webp b/resources/gui/ricette/set.webp new file mode 100644 index 0000000..acf3ad4 Binary files /dev/null and b/resources/gui/ricette/set.webp differ diff --git a/resources/gui/sfondi/aurora.webp b/resources/gui/sfondi/aurora.webp new file mode 100644 index 0000000..1fbf224 Binary files /dev/null and b/resources/gui/sfondi/aurora.webp differ diff --git a/resources/gui/sfondi/hero-pianeta.webp b/resources/gui/sfondi/hero-pianeta.webp new file mode 100644 index 0000000..13e905d Binary files /dev/null and b/resources/gui/sfondi/hero-pianeta.webp differ diff --git a/resources/gui/sfondi/nebulosa-calda.webp b/resources/gui/sfondi/nebulosa-calda.webp new file mode 100644 index 0000000..ca493eb Binary files /dev/null and b/resources/gui/sfondi/nebulosa-calda.webp differ diff --git a/resources/gui/sfondi/nebulosa.webp b/resources/gui/sfondi/nebulosa.webp new file mode 100644 index 0000000..f9c20b0 Binary files /dev/null and b/resources/gui/sfondi/nebulosa.webp differ diff --git a/resources/gui/sfondi/setup-pianeta.webp b/resources/gui/sfondi/setup-pianeta.webp new file mode 100644 index 0000000..a2675c1 Binary files /dev/null and b/resources/gui/sfondi/setup-pianeta.webp differ diff --git a/resources/gui/sfondi/stelle.webp b/resources/gui/sfondi/stelle.webp new file mode 100644 index 0000000..a79bb4e Binary files /dev/null and b/resources/gui/sfondi/stelle.webp differ diff --git a/resources/gui/stili/acquerello-film.webp b/resources/gui/stili/acquerello-film.webp new file mode 100644 index 0000000..4782e12 Binary files /dev/null and b/resources/gui/stili/acquerello-film.webp differ diff --git a/resources/gui/stili/anime-film.webp b/resources/gui/stili/anime-film.webp new file mode 100644 index 0000000..96aab3f Binary files /dev/null and b/resources/gui/stili/anime-film.webp differ diff --git a/resources/gui/stili/aura.webp b/resources/gui/stili/aura.webp new file mode 100644 index 0000000..f552232 Binary files /dev/null and b/resources/gui/stili/aura.webp differ diff --git a/resources/gui/stili/chrome.webp b/resources/gui/stili/chrome.webp new file mode 100644 index 0000000..54e4e0a Binary files /dev/null and b/resources/gui/stili/chrome.webp differ diff --git a/resources/gui/stili/cinema-anam.webp b/resources/gui/stili/cinema-anam.webp new file mode 100644 index 0000000..2ae7f88 Binary files /dev/null and b/resources/gui/stili/cinema-anam.webp differ diff --git a/resources/gui/stili/collage.webp b/resources/gui/stili/collage.webp new file mode 100644 index 0000000..96e6b76 Binary files /dev/null and b/resources/gui/stili/collage.webp differ diff --git a/resources/gui/stili/cyberpunk.webp b/resources/gui/stili/cyberpunk.webp new file mode 100644 index 0000000..904e96d Binary files /dev/null and b/resources/gui/stili/cyberpunk.webp differ diff --git a/resources/gui/stili/diorama.webp b/resources/gui/stili/diorama.webp new file mode 100644 index 0000000..51d3760 Binary files /dev/null and b/resources/gui/stili/diorama.webp differ diff --git a/resources/gui/stili/documentario.webp b/resources/gui/stili/documentario.webp new file mode 100644 index 0000000..a883208 Binary files /dev/null and b/resources/gui/stili/documentario.webp differ diff --git a/resources/gui/stili/flash-y2k.webp b/resources/gui/stili/flash-y2k.webp new file mode 100644 index 0000000..ad9b0c8 Binary files /dev/null and b/resources/gui/stili/flash-y2k.webp differ diff --git a/resources/gui/stili/gouache.webp b/resources/gui/stili/gouache.webp new file mode 100644 index 0000000..66208c9 Binary files /dev/null and b/resources/gui/stili/gouache.webp differ diff --git a/resources/gui/stili/linea.webp b/resources/gui/stili/linea.webp new file mode 100644 index 0000000..9bdba92 Binary files /dev/null and b/resources/gui/stili/linea.webp differ diff --git a/resources/gui/stili/manga-inchiostro.webp b/resources/gui/stili/manga-inchiostro.webp new file mode 100644 index 0000000..2d66ad9 Binary files /dev/null and b/resources/gui/stili/manga-inchiostro.webp differ diff --git a/resources/gui/stili/mattoncini.webp b/resources/gui/stili/mattoncini.webp new file mode 100644 index 0000000..d965123 Binary files /dev/null and b/resources/gui/stili/mattoncini.webp differ diff --git a/resources/gui/stili/moda-editoriale.webp b/resources/gui/stili/moda-editoriale.webp new file mode 100644 index 0000000..1691e41 Binary files /dev/null and b/resources/gui/stili/moda-editoriale.webp differ diff --git a/resources/gui/stili/notte-neon.webp b/resources/gui/stili/notte-neon.webp new file mode 100644 index 0000000..bc86856 Binary files /dev/null and b/resources/gui/stili/notte-neon.webp differ diff --git a/resources/gui/stili/pellicola35.webp b/resources/gui/stili/pellicola35.webp new file mode 100644 index 0000000..a0c3f79 Binary files /dev/null and b/resources/gui/stili/pellicola35.webp differ diff --git a/resources/gui/stili/pixel.webp b/resources/gui/stili/pixel.webp new file mode 100644 index 0000000..72530c1 Binary files /dev/null and b/resources/gui/stili/pixel.webp differ diff --git a/resources/gui/stili/plastilina.webp b/resources/gui/stili/plastilina.webp new file mode 100644 index 0000000..788cb7b Binary files /dev/null and b/resources/gui/stili/plastilina.webp differ diff --git a/resources/gui/stili/polaroid.webp b/resources/gui/stili/polaroid.webp new file mode 100644 index 0000000..4002f97 Binary files /dev/null and b/resources/gui/stili/polaroid.webp differ diff --git a/resources/gui/stili/risograph.webp b/resources/gui/stili/risograph.webp new file mode 100644 index 0000000..e504d19 Binary files /dev/null and b/resources/gui/stili/risograph.webp differ diff --git a/resources/gui/stili/soft3d.webp b/resources/gui/stili/soft3d.webp new file mode 100644 index 0000000..201fcb3 Binary files /dev/null and b/resources/gui/stili/soft3d.webp differ diff --git a/resources/gui/stili/solarpunk.webp b/resources/gui/stili/solarpunk.webp new file mode 100644 index 0000000..c170c4f Binary files /dev/null and b/resources/gui/stili/solarpunk.webp differ diff --git a/resources/gui/stili/spaziale60.webp b/resources/gui/stili/spaziale60.webp new file mode 100644 index 0000000..f0674c1 Binary files /dev/null and b/resources/gui/stili/spaziale60.webp differ diff --git a/resources/gui/stili/svizzero.webp b/resources/gui/stili/svizzero.webp new file mode 100644 index 0000000..89454dc Binary files /dev/null and b/resources/gui/stili/svizzero.webp differ diff --git a/resources/gui/stili/synthwave.webp b/resources/gui/stili/synthwave.webp new file mode 100644 index 0000000..a8ec64c Binary files /dev/null and b/resources/gui/stili/synthwave.webp differ diff --git a/resources/gui/stili/vetro.webp b/resources/gui/stili/vetro.webp new file mode 100644 index 0000000..54d6032 Binary files /dev/null and b/resources/gui/stili/vetro.webp differ diff --git a/resources/gui/stili/vinile.webp b/resources/gui/stili/vinile.webp new file mode 100644 index 0000000..2fb8a30 Binary files /dev/null and b/resources/gui/stili/vinile.webp differ diff --git a/resources/gui/tessera/espandi.webp b/resources/gui/tessera/espandi.webp new file mode 100644 index 0000000..44cfdfc Binary files /dev/null and b/resources/gui/tessera/espandi.webp differ diff --git a/resources/gui/tessera/estrai.webp b/resources/gui/tessera/estrai.webp new file mode 100644 index 0000000..7ec9d42 Binary files /dev/null and b/resources/gui/tessera/estrai.webp differ diff --git a/resources/gui/tessera/luce-colore.webp b/resources/gui/tessera/luce-colore.webp new file mode 100644 index 0000000..2806b6e Binary files /dev/null and b/resources/gui/tessera/luce-colore.webp differ diff --git a/resources/gui/tessera/persone.webp b/resources/gui/tessera/persone.webp new file mode 100644 index 0000000..b5128d6 Binary files /dev/null and b/resources/gui/tessera/persone.webp differ diff --git a/resources/gui/tessera/prospettiva.webp b/resources/gui/tessera/prospettiva.webp new file mode 100644 index 0000000..56a4820 Binary files /dev/null and b/resources/gui/tessera/prospettiva.webp differ diff --git a/resources/gui/tessera/ritocco.webp b/resources/gui/tessera/ritocco.webp new file mode 100644 index 0000000..af5262c Binary files /dev/null and b/resources/gui/tessera/ritocco.webp differ diff --git a/resources/gui/tessera/situazioni.webp b/resources/gui/tessera/situazioni.webp new file mode 100644 index 0000000..15ca94f Binary files /dev/null and b/resources/gui/tessera/situazioni.webp differ diff --git a/resources/gui/tessera/stili.webp b/resources/gui/tessera/stili.webp new file mode 100644 index 0000000..95c8cd6 Binary files /dev/null and b/resources/gui/tessera/stili.webp differ diff --git a/resources/gui/tessera/testi.webp b/resources/gui/tessera/testi.webp new file mode 100644 index 0000000..5426eb2 Binary files /dev/null and b/resources/gui/tessera/testi.webp differ diff --git a/resources/gui/tessera/vestiti.webp b/resources/gui/tessera/vestiti.webp new file mode 100644 index 0000000..a6d904f Binary files /dev/null and b/resources/gui/tessera/vestiti.webp differ diff --git a/scripts/gui-v2/genera_klein.py b/scripts/gui-v2/genera_klein.py new file mode 100644 index 0000000..2c8ab30 --- /dev/null +++ b/scripts/gui-v2/genera_klein.py @@ -0,0 +1,473 @@ +#!/usr/bin/env python3 +"""Genera gli asset grafici della GUI v2 («Orbita») con FLUX 3 Klein veloce (8 passi, senza CFG). + +Parla con l'engine ComfyUI già installato da DaProdImage (stesso modello, stesso encoder, stesso VAE +che usa l'app per «Klein veloce»). Il grafo è quello che costruisce `costruisci()` in +src/main/motore/grafi.ts per la famiglia klein (UNETLoader + CLIPLoader flux3 + VAELoader, +CLIPTextEncode, EmptyFlux2LatentImage, KSampler euler/simple cfg 1, VAEDecode, SaveImage). + +Uso (con il Python dell'engine, che ha Pillow e numpy): + "%LOCALAPPDATA%/DaProdImage/engine/venv/Scripts/python.exe" scripts/gui-v2/genera_klein.py --server 127.0.0.1:8818 --set prova + ... --gruppo tessera + ... --id tessera/persone + ... --tutti (tutti i gruppi, salta quelli già fatti) + ... --readme (rigenera resources/gui/README.md dal manifest) + +Output: + resources/gui//.webp|png (asset finali) + resources/gui/manifest.json (prompt, seed, modello, misure, data per ogni asset) + resources/gui/README.md (tabella generata dal manifest) + grezzi (scratch) (PNG grezzi di ComfyUI, fuori dal repo) +""" +from __future__ import annotations + +import argparse +import datetime as dt +import hashlib +import json +import os +import sys +import time +import urllib.parse +import urllib.request +import uuid +from pathlib import Path + +import numpy as np +from PIL import Image + +HERE = Path(__file__).resolve().parent +REPO = HERE.parent.parent # radice del worktree +RISORSE = REPO / "resources" / "gui" +MANIFEST = RISORSE / "manifest.json" +GREZZI = Path(os.environ.get("GUI_GREZZI", str(Path(os.environ.get("TMPDIR", str(REPO / "scratch"))) / "gui-v2" / "grezzi"))) + +# --- modelli: gli stessi file del catalogo di DaProdImage (src/main/installa/catalogo.ts) --- +MODELLO = { + "unet": "flux3_image_klein_sd_int8_convrot.safetensors", # klein-int8: FLUX 3 Klein veloce + "encoder": "qwen3vl_4b_int8_convrot.safetensors", # klein-te: Qwen3-VL 4B, tipo flux3 + "vae": "flux2-vae.safetensors", # klein-vae + "passi": 8, + "cfg": 1, + "sampler": "euler", + "scheduler": "simple", +} + +# --- prefissi (dalla spec §13). Le scene (sfondi, luoghi, stili, ricette) non sono icone: stesso palette, altra frase --- +PREFISSO_ICONA = ("polished 3D stylized icon, soft glossy materials, deep-space color palette of violet, magenta, cyan and gold, " + "subtle rim light, dark navy background, clean composition, no text, ") +PREFISSO_SCENA = ("cinematic digital art, deep-space color palette of violet, magenta, cyan and gold, soft glow, " + "clean composition, no text, ") +PREFISSO_LOCANDINA = ("stylized 3D illustration, deep-space color palette of violet, magenta, cyan and gold, subtle rim light, " + "clean composition, no text, ") +ISOLATO = ", isolated on plain dark background" +ISOLATO_NERO = ", isolated on pure black background" + +# Seed: uno per gruppo (spec §13). Gli stili usano la stessa scena e lo stesso seed per il confronto. +SEME = {"prova": 2026, "sfondi": 2026, "tessera": 1111, "modi": 1111, "generi": 1212, "stili": 4242, + "ricette": 3131, "luoghi": 5151, "meteo": 6161, "costumi": 7171, "animali": 8181, "facce": 9191, + "mascotte": 1313, "lora": 1414} + +SCENA_STILI = "a young woman in a red scarf holding a coffee cup on a city street at sunset" + +# asset: id, gruppo, file (relativo a resources/gui), gen (w,h), misura finale (w,h), prompt, alpha ('chiave'|'luce'|None), prefisso +def A(gruppo, nome, misura, prompt, alpha=None, prefisso=PREFISSO_ICONA, gen=None, file=None, prefix_isolato=True): + w, h = misura + if gen is None: + # icone piccole: si genera a ~1 MP e si scala giù (bordi più puliti); il resto alla misura finale (multipli di 16) + scala = 1024 / max(w, h) if max(w, h) <= 512 else 1.0 + gen = (round(w * scala / 16) * 16, round(h * scala / 16) * 16) + gw, gh = gen + full = (prefisso + prompt + (ISOLATO if alpha == "chiave" and prefix_isolato else ISOLATO_NERO if alpha == "luce" else "")) if prefisso else prompt + return {"id": f"{gruppo}/{nome}", "gruppo": gruppo, "file": file or f"{gruppo}/{nome}.webp", + "misura": [w, h], "gen": [gw, gh], "prompt": full, "alpha": alpha, "seme": SEME[gruppo]} + + +ASSET = [] +# --- set di prova (spec §13.1): persone, estrai, nebulosa, stile 1 e 2 (dal file degli stili), locandina --- +ASSET += [ + A("tessera", "persone", (256, 256), "an astronaut helmet visor reflecting a smiling face, three-quarter view", alpha="chiave"), + A("tessera", "estrai", (256, 256), "scissors cutting a glowing outline around a small planet, sparks", alpha="chiave"), + A("sfondi", "nebulosa", (2560, 1440), "deep space nebula in violet and magenta with faint stars, large empty dark area on the right, cinematic, no planet", prefisso=PREFISSO_SCENA), + A("ricette", "foglio", (512, 320), "a character turnaround sheet floating in space, multiple poses of an astronaut fox, neat grid", prefisso=PREFISSO_LOCANDINA, gen=(1024, 640)), +] +PROVA = ["tessera/persone", "tessera/estrai", "sfondi/nebulosa", "stili/anime-film", "stili/vetro", "ricette/foglio"] + +# --- tessere Modifica (10), modi (4): oggetti simbolo 3D --- +for nome, p in [("estrai", "scissors cutting a glowing outline around a small planet, sparks"), + ("persone", "an astronaut helmet visor reflecting a smiling face, three-quarter view"), + ("vestiti", "a folded jacket and a hanger floating in space, fabric with soft gloss"), + ("situazioni", "a glowing globe showing mountains and a sunrise on its edge, tiny landscape"), + ("prospettiva", "a camera lens with orbiting rings around it, perspective grid lines"), + ("luce-colore", "a glass prism splitting white light into a rainbow of violet, magenta, cyan and gold"), + ("stili", "a paint palette with a brush and a swirl of paint, floating"), + ("testi", "floating letter blocks, a glowing letter T and an O, typographic cubes"), + ("ritocco", "a retouching brush with a soft glowing sparkle, healing light"), + ("espandi", "a glowing frame expanding outward with arrows, stars around it")]: + if nome in ("estrai", "persone"): + continue + ASSET.append(A("tessera", nome, (256, 256), p, alpha="chiave")) +for nome, p in [("crea", "a glowing four-point star sparkle"), + ("modifica", "a glowing pen nib drawing a light trail"), + ("box", "a floating glowing rectangle frame with corner handles"), + ("ricette", "a glowing lightning bolt inside a rounded tile")]: + ASSET.append(A("modi", nome, (128, 128), p, alpha="chiave")) + +# --- generi di Crea (12) --- +for nome, p in [("foto", "a vintage film camera"), ("ritratto", "a classic oval portrait frame with a silhouette"), + ("prodotto", "a glossy perfume bottle"), ("poster", "a rolled poster with a film reel"), + ("infografica", "a pie chart and a bar chart floating"), ("logo", "a glowing abstract emblem shield"), + ("social", "a smartphone with a heart and a thumbs up"), ("miniatura", "a play button over a video frame"), + ("fumetto", "a comic speech bubble with a burst"), ("invito", "an envelope with a wax seal"), + ("menu", "a restaurant menu card with a fork and a spoon"), ("libero", "a blank canvas with a glowing sparkle")]: + ASSET.append(A("generi", nome, (256, 256), p, alpha="chiave")) + +# --- sfondi (6) --- +ASSET += [ + A("sfondi", "nebulosa-calda", (2560, 1440), "warm nebula in gold and magenta with faint stars, open space in the center-left", prefisso=PREFISSO_SCENA), + A("sfondi", "stelle", (1024, 1024), "sparse small white and gold stars, tiny bright points, seamless", prefisso="", alpha="luce", gen=(1024, 1024)), + A("sfondi", "aurora", (1600, 400), "aurora borealis ribbons in cyan, violet and gold over a dark horizon, wide panoramic banner", prefisso=PREFISSO_SCENA, gen=(2048, 512)), + A("sfondi", "hero-pianeta", (2560, 1440), "a large planet with glowing atmosphere in the lower left corner, dark space, cinematic", prefisso=PREFISSO_SCENA, alpha="chiave"), + A("sfondi", "setup-pianeta", (2560, 1440), "a planet with rings in the lower left, glowing atmosphere, dark space, cinematic", prefisso=PREFISSO_SCENA), +] + +# --- ricette (6): locandine 512×320 --- +for nome, p in [("mondi", "the same city street in several parallel worlds, a grid of four glowing portal windows"), + ("mix", "several floating photo cards merging into one glowing card, collage of mixed elements"), + ("set", "a neat row of sticker icons: a rocket, a planet, a star and a heart"), + ("formati", "a poster frame, a phone frame and a square frame side by side, glowing outlines"), + ("rinnova", "an old faded photograph turning into a sharp glowing photo, restoration sparkle")]: + ASSET.append(A("ricette", nome, (512, 320), p, prefisso=PREFISSO_LOCANDINA, gen=(1024, 640))) + +# --- stili (28): scena fissa + frammento della spec §7.4 (letti dal file stili.json) --- +STILI_JSON = HERE / "stili.json" +if STILI_JSON.exists(): + for s in json.loads(STILI_JSON.read_text(encoding="utf-8")): + ASSET.append(A("stili", s["id"], (320, 320), f"{SCENA_STILI}, {s['frammento']}, full frame, no black bars", prefisso="", gen=(768, 768))) + +# --- luoghi (10) --- +for nome, p in [("citta", "a miniature 3D stylized city at dusk"), ("spiaggia", "a miniature 3D stylized tropical beach"), + ("montagna", "a miniature 3D stylized snowy mountain"), ("bosco", "a miniature 3D stylized forest with glowing fireflies"), + ("deserto", "a miniature 3D stylized desert with dunes"), ("interno", "a miniature 3D stylized cozy room interior"), + ("studio-bianco", "an empty white photo studio with a softbox light"), ("spazio", "a miniature 3D stylized space station view"), + ("sott-acqua", "a miniature 3D stylized underwater coral reef, light rays"), ("futuro", "a miniature 3D stylized futuristic city of glass towers")]: + ASSET.append(A("luoghi", nome, (160, 160), p, prefisso=PREFISSO_SCENA)) + +# --- meteo e stagioni (10) --- +for nome, p in [("sole", "a bright sun"), ("nuvole", "fluffy clouds"), ("pioggia", "a cloud with rain drops"), + ("neve", "a cloud with snowflakes"), ("nebbia", "a foggy cloud bank"), ("temporale", "a dark storm cloud with lightning"), + ("primavera", "a blossom branch"), ("estate", "a sunflower"), ("autunno", "a falling maple leaf"), ("inverno", "a snowflake crystal")]: + ASSET.append(A("meteo", nome, (128, 128), p, alpha="chiave")) + +# --- costumi (9): busti stilizzati --- +for nome, p in [("astronauta", "an astronaut"), ("cavaliere", "a medieval knight in helmet"), ("pirata", "a pirate with hat and eyepatch"), + ("supereroe", "a superhero with mask and cape"), ("samurai", "a samurai warrior"), ("mago", "a wizard with pointed hat and beard"), + ("chef", "a chef with toque"), ("medico", "a doctor with stethoscope"), ("sposa", "a bride with a veil")]: + ASSET.append(A("costumi", nome, (128, 128), f"stylized 3D bust portrait of {p}", alpha="chiave")) + +# --- animali (5), facce (6), mascotte (3), lora (2) --- +for nome, p in [("cane", "a friendly 3D stylized dog head"), ("gatto", "a friendly 3D stylized cat head"), + ("cavallo", "a 3D stylized horse head"), ("uccello", "a 3D stylized bird head with colorful feathers"), + ("drago", "a 3D stylized dragon head")]: + ASSET.append(A("animali", nome, (128, 128), p, alpha="chiave")) +for nome, p in [("sorriso", "a round 3D face smiling"), ("ridere", "a round 3D face laughing with eyes closed"), + ("serio", "a round 3D face with a serious look"), ("sorpreso", "a round 3D face surprised with open mouth"), + ("triste", "a round 3D face looking sad with a tear"), ("occhiolino", "a round 3D face winking")]: + ASSET.append(A("facce", nome, (128, 128), p, alpha="chiave")) +for nome, p in [("fluttua", "a cute astronaut floating in zero gravity, relaxed pose, arms out"), + ("cartello", "a cute astronaut holding a sign with an error warning symbol"), + ("saluta", "a cute astronaut waving hello")]: + ASSET.append(A("mascotte", nome, (512, 512), p, alpha="chiave", gen=(1024, 1024))) +ASSET.append(A("lora", "qwen", (256, 256), "a glowing spiral of layered light, violet and magenta", prefisso=PREFISSO_SCENA)) +ASSET.append(A("lora", "klein", (256, 256), "a glowing crystal spark, cyan and gold", prefisso=PREFISSO_SCENA)) + +PER_ID = {a["id"]: a for a in ASSET} + + +# ---------------- ComfyUI ---------------- + +def grafo(a: dict) -> dict: + w, h = a["gen"] + tiled = w * h > 2.2 * 1024 * 1024 # stessa soglia dell'app («normale»): oltre 2,2 MP il VAE a tessere + decode = ( + {"class_type": "VAEDecodeTiled", "inputs": {"samples": ["7", 0], "vae": ["3", 0], "tile_size": 512, "overlap": 64, + "temporal_size": 64, "temporal_overlap": 8}} + if tiled else {"class_type": "VAEDecode", "inputs": {"samples": ["7", 0], "vae": ["3", 0]}} + ) + return { + "1": {"class_type": "UNETLoader", "inputs": {"unet_name": MODELLO["unet"], "weight_dtype": "default"}}, + "2": {"class_type": "CLIPLoader", "inputs": {"clip_name": MODELLO["encoder"], "type": "flux3", "device": "default"}}, + "3": {"class_type": "VAELoader", "inputs": {"vae_name": MODELLO["vae"]}}, + "4": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": a["prompt"]}}, + "5": {"class_type": "CLIPTextEncode", "inputs": {"clip": ["2", 0], "text": ""}}, + "6": {"class_type": "EmptyFlux2LatentImage", "inputs": {"width": w, "height": h, "batch_size": 1}}, + "7": {"class_type": "KSampler", "inputs": { + "model": ["1", 0], "seed": a["seme"], "steps": MODELLO["passi"], "cfg": MODELLO["cfg"], + "sampler_name": MODELLO["sampler"], "scheduler": MODELLO["scheduler"], + "positive": ["4", 0], "negative": ["5", 0], "latent_image": ["6", 0], "denoise": 1}}, + "8": decode, + "9": {"class_type": "SaveImage", "inputs": {"images": ["8", 0], "filename_prefix": "gui/" + a["id"].replace("/", "_")}}, + } + + +def chiama(server: str, path: str, dati: dict | None = None, timeout: int = 60): + url = f"http://{server}{path}" + req = urllib.request.Request(url, data=json.dumps(dati).encode() if dati is not None else None, + headers={"Content-Type": "application/json"} if dati is not None else {}) + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode() or "null") + + +def genera_grezzo(server: str, a: dict, timeout_s: int = 900) -> tuple[Path, float]: + client = str(uuid.uuid4()) + t0 = time.time() + risposta = chiama(server, "/prompt", {"prompt": grafo(a), "client_id": client}) + if not risposta or "prompt_id" not in risposta: + raise RuntimeError(f"ComfyUI ha rifiutato il grafo: {risposta}") + pid = risposta["prompt_id"] + while True: + if time.time() - t0 > timeout_s: + raise TimeoutError(f"{a['id']}: troppo lento") + storia = chiama(server, f"/history/{pid}") or {} + if pid in storia: + stato = storia[pid].get("status", {}) + if stato.get("status_str") == "error": + msgs = [m for m in stato.get("messages", []) if m and m[0] == "execution_error"] + raise RuntimeError(f"{a['id']}: errore del motore {msgs[-1][1] if msgs else stato}") + out = storia[pid].get("outputs", {}).get("9", {}).get("images", []) + if out: + img = out[0] + q = urllib.parse.urlencode({"filename": img["filename"], "subfolder": img.get("subfolder", ""), "type": img.get("type", "output")}) + with urllib.request.urlopen(f"http://{server}/view?{q}", timeout=120) as r: + dest = GREZZI / f"{a['id'].replace('/', '_')}.png" + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_bytes(r.read()) + return dest, time.time() - t0 + time.sleep(1.0) + + +# ---------------- trasparenza ---------------- + +def fondo_da_bordo(rgb: np.ndarray) -> np.ndarray: + bordo = np.concatenate([rgb[:4].reshape(-1, 3), rgb[-4:].reshape(-1, 3), rgb[:, :4].reshape(-1, 3), rgb[:, -4:].reshape(-1, 3)]) + return np.median(bordo, axis=0) + + +def buchi_chiusi(a: np.ndarray, area_min: float = 0.02) -> np.ndarray: + """Regioni trasparenti chiuse dentro il soggetto (es. il visore dell'astronauta) grandi almeno `area_min` dell'immagine: + da riempire. Le zone trasparenti che toccano il bordo restano fuori. Lavora a 512 px (basta per le aree grandi).""" + scala = 512 + piccola = Image.fromarray(((a < 0.5) * 255).astype(np.uint8)).resize((scala, scala), Image.Resampling.NEAREST) + m = np.asarray(piccola) > 0 + visto = np.zeros_like(m, dtype=bool) + da_riempire = np.zeros_like(m, dtype=bool) + h, w = m.shape + for y0 in range(h): + for x0 in range(w): + if not m[y0, x0] or visto[y0, x0]: + continue + pila = [(y0, x0)] + visto[y0, x0] = True + zona = [] + tocca_bordo = False + while pila: + y, x = pila.pop() + zona.append((y, x)) + if y in (0, h - 1) or x in (0, w - 1): + tocca_bordo = True + for ny, nx in ((y - 1, x), (y + 1, x), (y, x - 1), (y, x + 1)): + if 0 <= ny < h and 0 <= nx < w and m[ny, nx] and not visto[ny, nx]: + visto[ny, nx] = True + pila.append((ny, nx)) + if not tocca_bordo and len(zona) >= area_min * h * w: + for y, x in zona: + da_riempire[y, x] = True + if not da_riempire.any(): + return np.zeros(a.shape, dtype=bool) + grande = Image.fromarray((da_riempire * 255).astype(np.uint8)).resize((a.shape[1], a.shape[0]), Image.Resampling.NEAREST) + return np.asarray(grande) > 0 + + +def scontorna_chiave(img: Image.Image) -> Image.Image: + """Sfondo scuro uniforme: alpha dalla distanza di colore dal fondo (rampa 30→100), buchi chiusi del soggetto + riempiti (il visore), colore sbiancato dal fondo sui bordi.""" + rgb = np.asarray(img.convert("RGB")).astype(np.float32) + fondo = fondo_da_bordo(rgb) + d = np.sqrt(((rgb - fondo) ** 2).sum(axis=2)) + a = np.clip((d - 70.0) / (140.0 - 70.0), 0.0, 1.0) + a = np.where(a < 0.12, 0.0, a) # l'alone grigio del fondo sparisce; i bordi morbidi restano + pieno = buchi_chiusi(a) + a = np.where(pieno, 1.0, a) + colore = np.where(a[..., None] > 0.02, (rgb - (1 - a[..., None]) * fondo) / np.maximum(a[..., None], 1e-3), 0.0) + colore = np.where(pieno[..., None], rgb, colore) + colore = np.clip(colore, 0, 255) + out = np.dstack([colore, a * 255.0]).astype(np.uint8) + return Image.fromarray(out, "RGBA") + + +def scontorna_luce(img: Image.Image) -> Image.Image: + """Fondo nero: l'alpha è la luminosità (le stelle restano, il nero sparisce).""" + rgb = np.asarray(img.convert("RGB")).astype(np.float32) + a = rgb.max(axis=2) / 255.0 + a = np.clip((a - 0.06) / 0.5, 0, 1) + colore = np.clip(rgb / np.maximum(a[..., None], 1e-3), 0, 255) + colore = np.where(a[..., None] > 0.02, colore, 0) + return Image.fromarray(np.dstack([colore, a * 255.0]).astype(np.uint8), "RGBA") + + +# ---------------- salvataggio ---------------- + +def toglie_barre(img: Image.Image) -> Image.Image: + """Il modello «anime movie still» a volte disegna le bande nere del cinema (2,39:1): le toglie in alto e in basso + e ritaglia al centro un quadrato, così la misura resta quella delle altre anteprime.""" + a = np.asarray(img.convert("L")).astype(np.float32) + h, w = a.shape + righe = a.mean(axis=1) + top, bot = 0, h - 1 + while top < h // 4 and righe[top] < 14: + top += 1 + while bot > 3 * h // 4 and righe[bot] < 14: + bot -= 1 + if top == 0 and bot == h - 1: + return img + alt = bot - top + 1 + lato = min(w, alt) + x0 = (w - lato) // 2 + y0 = top + (alt - lato) // 2 + return img.crop((x0, y0, x0 + lato, y0 + lato)) + + +def salva_finale(a: dict, grezzo: Path) -> Path: + img = Image.open(grezzo).convert("RGB") + if a["gruppo"] == "stili": + img = toglie_barre(img) + if a["alpha"] == "chiave": + img = scontorna_chiave(img) + elif a["alpha"] == "luce": + img = scontorna_luce(img) + w, h = a["misura"] + if img.size != (w, h): + img = img.resize((w, h), Image.Resampling.LANCZOS) + dest = RISORSE / a["file"] + dest.parent.mkdir(parents=True, exist_ok=True) + if dest.suffix == ".webp": + img.save(dest, "WEBP", quality=82, method=6) + else: + img.save(dest, "PNG", optimize=True) + return dest + + +def carica_manifest() -> dict: + if MANIFEST.exists(): + return json.loads(MANIFEST.read_text(encoding="utf-8")) + return {"modello": MODELLO, "asset": {}} + + +def scrivi_manifest(m: dict) -> None: + MANIFEST.parent.mkdir(parents=True, exist_ok=True) + MANIFEST.write_text(json.dumps(m, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + + +def sha(p: Path) -> str: + return hashlib.sha256(p.read_bytes()).hexdigest()[:16] + + +def fai(server: str, lista: list[dict], forza: bool) -> None: + m = carica_manifest() + for a in lista: + dest = RISORSE / a["file"] + ent = m["asset"].get(a["id"]) + if not forza and dest.exists() and ent and ent.get("prompt") == a["prompt"] and ent.get("seme") == a["seme"]: + print(f"= {a['id']} già fatto", flush=True) + continue + print(f"> {a['id']} {a['gen'][0]}x{a['gen'][1]} seed {a['seme']}", flush=True) + grezzo, secondi = genera_grezzo(server, a) + finale = salva_finale(a, grezzo) + m["asset"][a["id"]] = { + "id": a["id"], "gruppo": a["gruppo"], "file": a["file"].replace("\\", "/"), "misura": a["misura"], "generato": a["gen"], + "prompt": a["prompt"], "negativo": "", "seme": a["seme"], "alpha": a["alpha"], **MODELLO, + "modello_file": MODELLO["unet"], "data": dt.datetime.now().isoformat(timespec="seconds"), + "secondi": round(secondi, 1), "sha256_16": sha(finale), "byte": finale.stat().st_size, + "da_rifare_con_birefnet": a["alpha"] == "chiave", + } + scrivi_manifest(m) + print(f" ok {finale.name} {finale.stat().st_size // 1024} KB in {secondi:.0f}s", flush=True) + + +def scrivi_readme() -> None: + m = carica_manifest() + righe = [ + "# Asset GUI v2 «Orbita» (generati con FLUX 3 Klein veloce)", + "", + "Generati con **FLUX 3 Klein veloce** (`flux3_image_klein_sd_int8_convrot.safetensors`, 8 passi, CFG 1, euler/simple),", + "encoder **Qwen3-VL 4B** (`qwen3vl_4b_int8_convrot.safetensors`, tipo `flux3`), VAE **FLUX.2** (`flux2-vae.safetensors`),", + "con l'engine ComfyUI di DaProdImage. Script: `scripts/gui-v2/genera_klein.py`. Spec: `docs/gui-v2-spec.md` §13.", + "", + "Stile comune: icone/tessere con il prefisso «polished 3D stylized icon …» (spec §13); sfondi, luoghi e ricette con", + "«cinematic digital art» o «stylized 3D illustration» (stessa palette, niente «icon»); stili (§7.4) con la scena fissa", + "«a young woman in a red scarf holding a coffee cup on a city street at sunset» + frammento dello stile + «full frame, no black bars»,", + "senza prefisso. Seed fisso per gruppo (campo `seed`).", + "", + "Trasparenza: sfondo scuro uniforme → alpha dalla distanza di colore dal fondo (rampa 70→140, sotto 0,12 zero), buchi chiusi", + "del soggetto riempiti; stelle → alpha dalla luminosità su nero. **BiRefNet non è stato usato**: la scontornatura è una stima,", + "non una segmentazione. Le righe con `BiRefNet: sì` vanno rifatte con BiRefNet quando sarà disponibile nell'engine usato.", + "Gli stili con bande nere (cinema 2,39:1) sono ritagliati al centro quadrato.", + "", + "Il visore e le parti interne aperte verso il bordo possono restare trasparenti: limite approvato nel set di prova.", + "Negativo vuoto per tutti gli asset (CFG 1). `Originale` indica la misura generata prima del ritaglio/ridimensionamento.", + "Rifiniture CPU: quattro anteprime (moda-editoriale, synthwave, spaziale60, svizzero) ritagliate per togliere scritte/loghi spuri;", + "coordinate nel manifest (`postproduzione`). Tile stelle: alpha a 4 livelli e tinta bianco caldo per stare sotto 350 kB.", + "", + "| ID | File | Gruppo | Originale | Misura | Passi | Seed | Modello (UNET) | Alpha | BiRefNet | Prompt (completo) | Negativo | Data | Secondi |", + "|---|---|---|---|---|---:|---:|---|---|---|---|---|---|---:|", + ] + # Completa i manifest preesistenti e le generazioni future con la lista esplicita dei ritagli da rifare. + for e in m["asset"].values(): + e["da_rifare_con_birefnet"] = e.get("alpha") == "chiave" + scrivi_manifest(m) + for id_, e in sorted(m["asset"].items(), key=lambda kv: (kv[1]["gruppo"], kv[0])): + modello = e.get("modello_file", m["modello"]["unet"]) + birefnet = "sì" if e["da_rifare_con_birefnet"] else "no" + righe.append(f"| `{id_}` | `{e['file']}` | {e['gruppo']} | {e['generato'][0]}×{e['generato'][1]} | {e['misura'][0]}×{e['misura'][1]} | {e['passi']} | {e['seme']} | `{modello}` | {e['alpha'] or '—'} | {birefnet} | {e['prompt']} | {e['negativo'] or '—'} | {e['data']} | {e['secondi']} |") + righe.append("") + righe.append(f"Totale: {len(m['asset'])} asset. Modello: `{m['modello']['unet']}`, encoder `{m['modello']['encoder']}`, VAE `{m['modello']['vae']}`.") + (RISORSE / "README.md").write_text("\n".join(righe) + "\n", encoding="utf-8") + + +def main() -> int: + p = argparse.ArgumentParser() + p.add_argument("--server", default="127.0.0.1:8818") + p.add_argument("--set", choices=["prova"]) + p.add_argument("--gruppo") + p.add_argument("--id", action="append", default=[]) + p.add_argument("--tutti", action="store_true") + p.add_argument("--forza", action="store_true", help="rigenera anche se c'è già") + p.add_argument("--readme", action="store_true") + p.add_argument("--elenco", action="store_true") + a = p.parse_args() + if a.elenco: + for x in ASSET: + print(x["id"], x["misura"], x["gen"], x["alpha"] or "-") + print("totale", len(ASSET)) + return 0 + if a.readme: + scrivi_readme() + print("README scritto") + return 0 + if a.set == "prova": + lista = [PER_ID[i] for i in PROVA] + elif a.gruppo: + lista = [x for x in ASSET if x["gruppo"] == a.gruppo] + elif a.id: + lista = [PER_ID[i] for i in a.id] + elif a.tutti: + lista = ASSET + else: + p.error("serve --set, --gruppo, --id o --tutti") + fai(a.server, lista, a.forza) + scrivi_readme() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/gui-v2/stili.json b/scripts/gui-v2/stili.json new file mode 100644 index 0000000..9a96891 --- /dev/null +++ b/scripts/gui-v2/stili.json @@ -0,0 +1,142 @@ +[ + { + "id": "cinema-anam", + "nome": "Cinema anamorfico", + "frammento": "cinematic still, anamorphic lens flares, oval bokeh, rich color grade, 2.39:1 feel" + }, + { + "id": "flash-y2k", + "nome": "Flash 2000", + "frammento": "direct on-camera flash, early-2000s compact digital camera look, harsh highlights, candid party snapshot" + }, + { + "id": "pellicola35", + "nome": "Pellicola 35mm", + "frammento": "shot on 35mm film, visible grain, soft halation, faded blacks" + }, + { + "id": "moda-editoriale", + "nome": "Moda editoriale", + "frammento": "high-fashion editorial photography, studio strobes, bold color blocking, magazine cover composition" + }, + { + "id": "polaroid", + "nome": "Polaroid", + "frammento": "instant film photo, white frame, soft focus, washed warm tones, light leaks" + }, + { + "id": "notte-neon", + "nome": "Notte al neon", + "frammento": "night street photography, wet asphalt reflections, magenta and cyan neon signage" + }, + { + "id": "documentario", + "nome": "Reportage", + "frammento": "gritty documentary photograph, available light, slightly desaturated, imperfect framing" + }, + { + "id": "anime-film", + "nome": "Anime da film", + "frammento": "high quality anime movie still, painterly backgrounds, soft cinematic lighting" + }, + { + "id": "acquerello-film", + "nome": "Acquerello animato", + "frammento": "hand-painted watercolor animation background, soft gradients, gentle clouds" + }, + { + "id": "manga-inchiostro", + "nome": "Manga a inchiostro", + "frammento": "black and white manga, screentones, dynamic inked lines" + }, + { + "id": "gouache", + "nome": "Gouache moderna", + "frammento": "contemporary gouache illustration, flat shapes, rich matte colors, subtle paper texture" + }, + { + "id": "risograph", + "nome": "Risograph", + "frammento": "two-color risograph print, grainy overprint, misregistration, limited palette" + }, + { + "id": "collage", + "nome": "Collage di carta", + "frammento": "cut-paper collage, layered torn paper edges, mixed textures" + }, + { + "id": "linea", + "nome": "Linea unica", + "frammento": "minimal single continuous line drawing on white, a touch of color" + }, + { + "id": "soft3d", + "nome": "3D morbido", + "frammento": "soft 3D render, rounded shapes, pastel studio light, subsurface scattering, friendly character look" + }, + { + "id": "plastilina", + "nome": "Plastilina", + "frammento": "stop-motion claymation, visible fingerprints, handmade imperfections" + }, + { + "id": "vinile", + "nome": "Vinile da collezione", + "frammento": "designer vinyl toy, glossy plastic, seam lines, studio product shot" + }, + { + "id": "diorama", + "nome": "Diorama isometrico", + "frammento": "isometric miniature diorama, tilt-shift, clean cube base, soft shadows" + }, + { + "id": "mattoncini", + "nome": "Mattoncini", + "frammento": "scene built of interlocking plastic bricks and minifigures" + }, + { + "id": "vetro", + "nome": "Vetro liquido", + "frammento": "liquid glass and crystal, refractive translucent material, caustics, iridescent edges" + }, + { + "id": "chrome", + "nome": "Cromo Y2K", + "frammento": "chrome blobject, reflective metal, hyperreal studio HDRI, early-2000s futurism" + }, + { + "id": "cyberpunk", + "nome": "Cyberpunk", + "frammento": "dense neon cityscape, holograms, rain, high contrast magenta/teal" + }, + { + "id": "synthwave", + "nome": "Synthwave", + "frammento": "retro-futuristic 80s sunset grid, chrome lettering, purple-pink gradient sky" + }, + { + "id": "aura", + "nome": "Aura gradient", + "frammento": "soft airbrushed aura gradients, grain, glowing blurred shapes, dreamy abstract" + }, + { + "id": "pixel", + "nome": "Pixel 32-bit", + "frammento": "detailed 32-bit pixel art, limited palette, dithering" + }, + { + "id": "spaziale60", + "nome": "Space age ’60", + "frammento": "1960s space-age illustration, atomic shapes, retro NASA poster palette" + }, + { + "id": "solarpunk", + "nome": "Solarpunk", + "frammento": "solarpunk utopia, lush green architecture, warm sunlight, art nouveau curves" + }, + { + "id": "svizzero", + "nome": "Poster svizzero", + "frammento": "Swiss international typographic poster, strong grid, flat color, bold sans-serif" + } +] diff --git a/scripts/gui-v2/tavola_asset.py b/scripts/gui-v2/tavola_asset.py new file mode 100644 index 0000000..0dd5b40 --- /dev/null +++ b/scripts/gui-v2/tavola_asset.py @@ -0,0 +1,26 @@ +"""Rifiniture CPU e tavola di controllo: non usa la GPU né avvia generazioni.""" +import hashlib +import json +import os +from pathlib import Path +from PIL import Image, ImageDraw + +root = Path(__file__).resolve().parents[2] +base = root / 'resources/gui' +manifest_path = base / 'manifest.json' +m = json.loads(manifest_path.read_text(encoding='utf-8')) +items = [(key, e) for key, e in m['asset'].items() if e['gruppo'] in ('stili', 'ricette') or key in ('sfondi/nebulosa', 'sfondi/nebulosa-calda')] +cols, w, h = 6, 180, 200 +sheet = Image.new('RGB', (cols*w, ((len(items)+cols-1)//cols)*h), '#10172e') +draw = ImageDraw.Draw(sheet) +for i, (key, entry) in enumerate(items): + x, y = (i % cols)*w, (i // cols)*h + with Image.open(base / entry['file']) as im: + im = im.convert('RGBA') + im.thumbnail((w-8, h-25)) + sheet.paste(im, (x+(w-im.width)//2, y+23), im) + draw.text((x+4, y+4), key, fill='white') +out = Path(os.environ['TMPDIR']) / 'gui-v2/controllo-stili-ricette.webp' +out.parent.mkdir(parents=True, exist_ok=True) +sheet.save(out, 'WEBP', quality=82) +print(json.dumps({'tavola': str(out), 'immagini': len(items)}, ensure_ascii=False)) diff --git a/scripts/gui-v2/verifica_asset.py b/scripts/gui-v2/verifica_asset.py new file mode 100644 index 0000000..6060f6e --- /dev/null +++ b/scripts/gui-v2/verifica_asset.py @@ -0,0 +1,111 @@ +from pathlib import Path +from collections import Counter +import hashlib, json, os, sys +from PIL import Image, ImageDraw, ImageFont + +root = Path(__file__).resolve().parents[2] +assets_dir = root / 'resources' / 'gui' +manifest = json.loads((assets_dir / 'manifest.json').read_text(encoding='utf-8')) +entries = manifest['asset'] +files = sorted(p for p in assets_dir.rglob('*.webp') if p.name != 'prova-tavola.webp') +errors = [] +if len(entries) != 111: + errors.append(f'manifest count {len(entries)} != 111') +if len(files) != len(entries): + errors.append(f'webp count {len(files)} != manifest count {len(entries)}') +seen = set() +alpha_counts = Counter() +total_bytes = 0 +max_file = (0, '') +for asset_id, e in entries.items(): + path = assets_dir / e['file'] + if not path.is_file(): + errors.append(f'missing {asset_id}: {path}') + continue + if path in seen: + errors.append(f'duplicate manifest file: {e["file"]}') + seen.add(path) + raw = path.read_bytes() + total_bytes += len(raw) + if len(raw) > max_file[0]: + max_file = (len(raw), e['file']) + if e.get('byte') != len(raw): + errors.append(f'byte mismatch {asset_id}: manifest={e.get("byte")} actual={len(raw)}') + digest = hashlib.sha256(raw).hexdigest()[:16] + if e.get('sha256_16') != digest: + errors.append(f'hash mismatch {asset_id}: manifest={e.get("sha256_16")} actual={digest}') + try: + with Image.open(path) as im: + im.load() + if im.format != 'WEBP': + errors.append(f'not WEBP: {asset_id} format={im.format}') + if list(im.size) != e['misura']: + errors.append(f'dimension mismatch {asset_id}: manifest={e["misura"]} actual={im.size}') + alpha = e.get('alpha') + alpha_counts[str(alpha or 'none')] += 1 + if alpha and 'A' not in im.getbands(): + errors.append(f'alpha requested but missing channel: {asset_id}') + if alpha and im.getchannel('A').getextrema()[0] == 255: + errors.append(f'alpha is fully opaque: {asset_id}') + if e.get('da_rifare_con_birefnet') != (alpha == 'chiave'): + errors.append(f'BiRefNet flag mismatch: {asset_id}') + except Exception as ex: + errors.append(f'decode failed {asset_id}: {ex}') + +from genera_klein import PER_ID +expected = set(PER_ID) +if set(entries) != expected: + errors.append(f'catalogo e manifest diversi: mancanti={sorted(expected-set(entries))}, extra={sorted(set(entries)-expected)}') +if set(files) != {assets_dir / e['file'] for e in entries.values()}: + errors.append('file WebP non presenti nel manifest o mancanti') +if sum(e['byte'] for e in entries.values()) >= 6000000: + errors.append('superato budget totale 6 MB') +for e in entries.values(): + if e['gruppo'] == 'sfondi' and e['byte'] > 350000: + errors.append(f'sfondo sopra 350 kB: {e["file"]}') +readme = (assets_dir / 'README.md').read_text(encoding='utf-8') +if '| Modello (UNET) |' not in readme or '| BiRefNet |' not in readme: + errors.append('README missing per-asset model or BiRefNet columns') +if readme.count(' | sì | ') != sum(1 for e in entries.values() if e.get('da_rifare_con_birefnet')): + errors.append('README BiRefNet yes count differs from manifest') + +# Tavola leggera del set di prova già approvato, per allegarla al task. +items = [ + ('Icona · Persone', 'tessera/persone.webp'), + ('Icona · Estrai', 'tessera/estrai.webp'), + ('Sfondo · Nebulosa', 'sfondi/nebulosa.webp'), + ('Stile · Anime film', 'stili/anime-film.webp'), + ('Stile · Vetro', 'stili/vetro.webp'), + ('Ricetta · Foglio', 'ricette/foglio.webp'), +] +W, H = 900, 560 +cell_w, cell_h, header_h = 300, 280, 26 +sheet = Image.new('RGB', (W, H), '#10172e') +draw = ImageDraw.Draw(sheet) +font = ImageFont.load_default() +for i, (label, rel) in enumerate(items): + x, y = (i % 3) * cell_w, (i // 3) * cell_h + draw.rectangle((x, y, x + cell_w - 1, y + header_h - 1), fill='#1b2444') + draw.text((x + 8, y + 7), label, fill='white', font=font) + im = Image.open(assets_dir / rel).convert('RGBA') + im.thumbnail((cell_w - 16, cell_h - header_h - 14), Image.Resampling.LANCZOS) + # sfondi scuri per anteprima coerente con la GUI; alpha compositato sul navy. + bg = Image.new('RGBA', im.size, '#10172e') + bg.alpha_composite(im) + dx = x + (cell_w - im.width) // 2 + dy = y + header_h + (cell_h - header_h - im.height) // 2 + sheet.paste(bg.convert('RGB'), (dx, dy)) + +scratch = Path(os.environ.get('TMPDIR') or os.environ.get('TEMP') or root) +out = scratch / 'gui-v2' / 'prova-tavola.webp' +out.parent.mkdir(parents=True, exist_ok=True) +sheet.save(out, 'WEBP', quality=48, method=6) + +print(json.dumps({ + 'asset_count': len(entries), 'webp_count': len(files), 'total_bytes': total_bytes, + 'max_file': {'bytes': max_file[0], 'file': max_file[1]}, 'alpha_counts': dict(alpha_counts), + 'birefnet_revisit_count': sum(1 for e in entries.values() if e.get('da_rifare_con_birefnet')), + 'contact_sheet': str(out), 'contact_sheet_bytes': out.stat().st_size, 'errors': errors, +}, ensure_ascii=False, indent=2)) +if errors: + sys.exit(1) diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index c718675..d439d64 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -6,9 +6,11 @@ import { I } from './componenti/Icone' import { Coda } from './componenti/Coda' import { Crea } from './pagine/Crea' import { Modifica } from './pagine/Modifica' +import { Box } from './pagine/Box' +import { Lora } from './pagine/Lora' +import { StripModi, PAGINE_STUDIO, vaiModo, type ModoStudio } from './pagine/Studio' import { Album } from './pagine/Album' import { BarraStato } from './componenti/BarraStato' -import { SelettoreModello } from './componenti/ScegliModello' import { SelettoreFoto } from './componenti/SelettoreFoto' import { Galleria } from './pagine/Galleria' import { Impostazioni } from './pagine/Impostazioni' @@ -16,12 +18,15 @@ import { Setup } from './pagine/Setup' import { normalizza } from './util' import { apriInModifica } from './azioni' -const VOCI: { id: Pagina; nome: string; icona: () => JSX.Element }[] = [ - { id: 'crea', nome: 'Crea', icona: I.crea }, - { id: 'modifica', nome: 'Modifica', icona: I.modifica }, +/** la rotaia: Studio (con i suoi modi in cima), Galleria, Album, LoRA */ +const VOCI: { id: Pagina | 'studio'; nome: string; icona: () => JSX.Element }[] = [ + { id: 'studio', nome: 'Studio', icona: I.studio }, { id: 'galleria', nome: 'Galleria', icona: I.galleria }, - { id: 'album', nome: 'Album', icona: I.album } + { id: 'album', nome: 'Album', icona: I.album }, + { id: 'lora', nome: 'LoRA', icona: I.pila } ] +/** l'ultimo modo dello Studio, per tornarci dalla rotaia */ +let ultimoModo: ModoStudio = 'crea' const gb = (b: number): string => (b / 1073741824).toFixed(b >= 10 * 1073741824 ? 0 : 1) @@ -56,7 +61,7 @@ function Indicatori(): JSX.Element | null { } export function App(): JSX.Element { - const { pagina, schedaOpzioni, vai, motore, lavori, codaAperta, avvisi, avvisa, agg, imp } = usaStato() + const { pagina, vai, motore, lavori, codaAperta, avvisi, avvisa, agg, imp } = usaStato() const [pronto, setPronto] = useState(false) const [installato, setInstallato] = useState(true) // il motore era installato ma la sua versione è cambiata (aggiornamento dell'app): si aggiorna da solo @@ -139,6 +144,27 @@ export function App(): JSX.Element { } }, []) + // l'ultimo modo dello Studio (per tornarci dalla rotaia) e le scorciatoie 1–7 e «,» (non quando si scrive) + useEffect(() => { + const t = (e: KeyboardEvent): void => { + if (e.ctrlKey || e.metaKey || e.altKey) return + if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) return + const mappa: Record void> = { + '1': () => vaiModo('crea'), '2': () => vaiModo('modifica'), '3': () => vaiModo('box'), '4': () => vaiModo('ricette'), + '5': () => usaStato.getState().vai('galleria'), '6': () => usaStato.getState().vai('album'), '7': () => usaStato.getState().vai('lora'), ',': () => usaStato.getState().vai('impostazioni') + } + const f = mappa[e.key] + if (f && !usaStato.getState().visore) { + e.preventDefault() + f() + } + } + window.addEventListener('keydown', t) + return () => window.removeEventListener('keydown', t) + }, []) + const modoCorrente = usaStato((s) => (s.pagina === 'modifica' ? (s.modifica.modo === 'ricette' ? 'ricette' : 'modifica') : s.pagina === 'box' ? 'box' : s.pagina === 'crea' ? 'crea' : null)) + if (modoCorrente) ultimoModo = modoCorrente + // Monitoraggio VRAM: se resta sopra il 95% per più di 10 s a coda ferma, avvisa. useEffect(() => { const attiviNow = lavori.some((l) => l.stato === 'in coda' || l.stato === 'in corso') @@ -165,7 +191,6 @@ export function App(): JSX.Element { DAPROD IMAGE - {pronto && installato && } {agg.stato === 'scarico' && ( vai('impostazioni')}> @@ -197,12 +222,15 @@ export function App(): JSX.Element { ) : ( <>
{/* le pagine restano montate: la maschera disegnata o il prompt non si perdono cambiando pagina */} -
-
+
+ +
+
+
+
+
+
{pagina === 'album' && }
{pagina === 'galleria' && }
+
{pagina === 'lora' && }
{pagina === 'impostazioni' && }
@@ -241,7 +276,7 @@ export function App(): JSX.Element { {rilascio && (
-
{pagina === 'galleria' ? 'Rilascia: le foto entrano in Galleria (diventano PNG)' : pagina === 'impostazioni' && schedaOpzioni === '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' : 'Rilascia: la foto si apre in Modifica (diventa PNG)'}
)} diff --git a/src/renderer/src/azioni.ts b/src/renderer/src/azioni.ts index 971dec6..2f093a6 100644 --- a/src/renderer/src/azioni.ts +++ b/src/renderer/src/azioni.ts @@ -64,6 +64,14 @@ export function apriInModifica(f: FotoBase): void { s.vai('modifica') } +/** apre la foto in Modifica direttamente sul modo Estrai (persone e oggetti) */ +export function apriInEstrai(f: FotoBase): void { + const s = usaStato.getState() + s.setModifica({ base: f, versioni: [f], modo: 'estrai' }) + s.apriVisore(null) + s.vai('modifica') +} + async function accoda(q: Richiesta, messaggio: string): Promise { try { await api.lavori.accoda(q) diff --git a/src/renderer/src/box.ts b/src/renderer/src/box.ts new file mode 100644 index 0000000..f56f0ec --- /dev/null +++ b/src/renderer/src/box.ts @@ -0,0 +1,132 @@ +// L'Editor box: i riquadri disegnati sulla tela, ognuno col suo testo. Qui la parte senza interfaccia: colori, disposizioni pronte, +// geometria (sposta/ridimensiona nei limiti 0–1) e la descrizione a parole che si aggiunge al prompt finché il motore non ha +// l'adattatore dei riquadri (Klein 3 / Qwen): così funziona già con qualunque modello, e il motore riceve comunque `box` per fare meglio. +import type { BoxRegione } from '@shared/tipi' + +export const COLORI_BOX = ['#8a5cff', '#3fe0ff', '#ff3df2', '#ffd54a', '#3ee08f', '#ff7a6b', '#4c8dff', '#b6f23f'] +export const MAX_BOX = 8 +export type Rect = BoxRegione['rect'] + +export const TIPI_BOX: { id: BoxRegione['tipo']; nome: string; titolo: string; guida: string }[] = [ + { id: 'persona', nome: 'Persona', titolo: 'Una persona o un personaggio', guida: 'a person' }, + { id: 'oggetto', nome: 'Oggetto', titolo: 'Un oggetto, un animale, una cosa', guida: 'an object' }, + { id: 'testo', nome: 'Testo', titolo: 'Una scritta: il testo va fra virgolette e resta identico', guida: 'text' }, + { id: 'sfondo', nome: 'Sfondo', titolo: 'L\'ambiente dietro: occupa tutto il riquadro', guida: 'background' } +] + +export const RIGIDITA: { id: BoxRegione['rigidita']; nome: string; titolo: string }[] = [ + { id: 'guida', nome: 'Suggerita', titolo: 'Il riquadro è una guida per la composizione: il modello può uscire un po\' dai bordi' }, + { id: 'decisa', nome: 'Decisa', titolo: 'Il modello resta dentro il riquadro' }, + { id: 'blocca', nome: 'Blocca', titolo: 'Solo lì dentro: maschera e compositing (serve il motore v2)' } +] + +/** le disposizioni pronte: (x0, y0, x1, y1) e il tipo di ogni riquadro */ +export const DISPOSIZIONI: { id: string; nome: string; box: { rect: Rect; tipo: BoxRegione['tipo'] }[] }[] = [ + { id: 'uno', nome: 'Uno grande', box: [{ rect: [0.08, 0.08, 0.92, 0.92], tipo: 'oggetto' }] }, + { id: 'due', nome: 'Due affiancati', box: [{ rect: [0.04, 0.12, 0.48, 0.88], tipo: 'persona' }, { rect: [0.52, 0.12, 0.96, 0.88], tipo: 'persona' }] }, + { id: 'tre', nome: 'Tre colonne', box: [{ rect: [0.03, 0.1, 0.32, 0.9], tipo: 'oggetto' }, { rect: [0.355, 0.1, 0.645, 0.9], tipo: 'oggetto' }, { rect: [0.68, 0.1, 0.97, 0.9], tipo: 'oggetto' }] }, + { id: 'due-uno', nome: 'Due sopra, uno sotto', box: [{ rect: [0.04, 0.04, 0.48, 0.48], tipo: 'oggetto' }, { rect: [0.52, 0.04, 0.96, 0.48], tipo: 'oggetto' }, { rect: [0.04, 0.53, 0.96, 0.96], tipo: 'sfondo' }] }, + { id: 'terzi', nome: 'Terzi', box: [{ rect: [0.04, 0.06, 0.64, 0.94], tipo: 'persona' }, { rect: [0.69, 0.06, 0.96, 0.46], tipo: 'oggetto' }, { rect: [0.69, 0.54, 0.96, 0.94], tipo: 'oggetto' }] }, + { id: 'griglia', nome: 'Griglia 2×2', box: [{ rect: [0.04, 0.04, 0.48, 0.48], tipo: 'oggetto' }, { rect: [0.52, 0.04, 0.96, 0.48], tipo: 'oggetto' }, { rect: [0.04, 0.52, 0.48, 0.96], tipo: 'oggetto' }, { rect: [0.52, 0.52, 0.96, 0.96], tipo: 'oggetto' }] }, + { id: 'primo-piano', nome: 'Primo piano + sfondo', box: [{ rect: [0, 0, 1, 1], tipo: 'sfondo' }, { rect: [0.24, 0.16, 0.76, 0.96], tipo: 'persona' }] }, + { id: 'titolo', nome: 'Titolo + immagine', box: [{ rect: [0.08, 0.04, 0.92, 0.2], tipo: 'testo' }, { rect: [0.08, 0.25, 0.92, 0.96], tipo: 'oggetto' }] }, + { id: 'polaroid', nome: 'Polaroid', box: [{ rect: [0.12, 0.08, 0.88, 0.76], tipo: 'oggetto' }, { rect: [0.12, 0.8, 0.88, 0.94], tipo: 'testo' }] } +] + +const limita = (n: number, a = 0, b = 1): number => Math.max(a, Math.min(b, n)) +export const MINIMO = 0.04 + +export const idBox = (): string => 'b' + Math.random().toString(36).slice(2, 8) + +/** un rettangolo da due punti (qualunque ordine), dentro la tela */ +export function rettDaPunti(ax: number, ay: number, bx: number, by: number): Rect { + return [limita(Math.min(ax, bx)), limita(Math.min(ay, by)), limita(Math.max(ax, bx)), limita(Math.max(ay, by))] +} + +export function nuovaRegione(rect: Rect, regioni: BoxRegione[], tipo: BoxRegione['tipo'] = 'oggetto'): BoxRegione { + const ordine = regioni.reduce((m, r) => Math.max(m, r.ordine), 0) + 1 + return { id: idBox(), rect, tipo, prompt: '', rigidita: 'decisa', ordine } +} + +/** il colore di un riquadro dal suo posto nell'elenco: stabile finché non si cancella quello prima */ +export const coloreBox = (i: number): string => COLORI_BOX[i % COLORI_BOX.length] + +export type Maniglia = 'nw' | 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'corpo' + +/** sposta o ridimensiona un rettangolo: `dx` e `dy` in frazioni della tela */ +export function trasforma(r: Rect, m: Maniglia, dx: number, dy: number): Rect { + let [x0, y0, x1, y1] = r + if (m === 'corpo') { + const w = x1 - x0 + const h = y1 - y0 + x0 = limita(x0 + dx, 0, 1 - w) + y0 = limita(y0 + dy, 0, 1 - h) + return [x0, y0, x0 + w, y0 + h] + } + if (m.includes('w')) x0 = limita(x0 + dx, 0, x1 - MINIMO) + if (m.includes('e')) x1 = limita(x1 + dx, x0 + MINIMO, 1) + if (m.includes('n')) y0 = limita(y0 + dy, 0, y1 - MINIMO) + if (m.includes('s')) y1 = limita(y1 + dy, y0 + MINIMO, 1) + return [x0, y0, x1, y1] +} + +const COL = ['left', 'center', 'right'] +const RIGA = ['top', 'middle', 'bottom'] +/** dove sta il riquadro, a parole (in inglese: va dentro il prompt) */ +export function posizioneParole(r: Rect): string { + const cx = (r[0] + r[2]) / 2 + const cy = (r[1] + r[3]) / 2 + const w = r[2] - r[0] + const h = r[3] - r[1] + if (w > 0.85 && h > 0.85) return 'the whole frame' + const c = COL[cx < 0.36 ? 0 : cx > 0.64 ? 2 : 1] + const g = RIGA[cy < 0.36 ? 0 : cy > 0.64 ? 2 : 1] + if (c === 'center' && g === 'middle') return 'the center' + if (w > 0.85) return `the ${g} of the frame, spanning the full width` + if (h > 0.85) return `the ${c} side of the frame, spanning the full height` + return g === 'middle' ? `the ${c} side` : c === 'center' ? `the ${g} center` : `the ${g}-${c}` +} + +const percentuale = (n: number): number => Math.round(n * 100) + +/** la descrizione del riquadro a parole. `tag` è l' della foto di riferimento, se c'è */ +export function fraseBox(r: BoxRegione, tag?: string): string { + const w = r.rect[2] - r.rect[0] + const h = r.rect[3] - r.rect[1] + const dove = posizioneParole(r.rect) + const testo = r.prompt.trim().replace(/[.;\s]+$/, '') + const rif = tag ? ` (use ${tag} for how it looks)` : '' + const misura = `about ${percentuale(w)}% of the width and ${percentuale(h)}% of the height` + if (r.tipo === 'sfondo') return `Background${dove === 'the whole frame' ? '' : ` (${dove})`}: ${testo || 'a fitting environment'}${rif}.` + if (r.tipo === 'testo') return `In ${dove}, the text ${/["“«]/.test(testo) ? testo : `"${testo}"`} written in crisp, perfectly legible lettering${rif}.` + const cosa = r.tipo === 'persona' ? 'a person' : 'an object' + return `In ${dove} (${misura}): ${testo || cosa}${rif}.` +} + +export interface BoxPronti { + prompt: string + /** le foto di riferimento, nell'ordine dei riquadri: , … */ + immagini: string[] + /** i riquadri che partono (gli occhi spenti restano fuori), con l'ordine rimesso a posto */ + regioni: BoxRegione[] +} + +/** dal disegno al prompt: scena + un periodo per ogni riquadro (dal fondo al davanti) */ +export function promptDaBox(scena: string, tutti: BoxRegione[], nascosti: ReadonlySet): BoxPronti { + const regioni = tutti + .filter((r) => !nascosti.has(r.id)) + .sort((a, b) => a.ordine - b.ordine) + .map((r, i) => ({ ...r, ordine: i + 1 })) + const immagini: string[] = [] + const frasi = regioni.map((r) => { + let tag: string | undefined + if (r.rif) { + immagini.push(r.rif) + tag = `` + } + return fraseBox(r, tag) + }) + const testa = scena.trim() ? `${scena.trim().replace(/[.;\s]+$/, '')}. ` : '' + const corpo = frasi.length ? `The image is composed of ${frasi.length} part${frasi.length > 1 ? 's' : ''}, placed exactly as described. ${frasi.join(' ')}` : '' + return { prompt: (testa + corpo).trim(), immagini, regioni } +} diff --git a/src/renderer/src/componenti/ControlliCategoria.tsx b/src/renderer/src/componenti/ControlliCategoria.tsx new file mode 100644 index 0000000..bf87d3f --- /dev/null +++ b/src/renderer/src/componenti/ControlliCategoria.tsx @@ -0,0 +1,79 @@ +// I pezzi grafici dei controlli di Modifica (catalogo in controlli.ts): cursori con le icone agli estremi, griglie a icona, +// campioni colore, il quadrante 3×3 e le righe di testo. Il valore sta nell'id dell'idea attiva: qui non c'è stato proprio. +import { useEffect, useState, type JSX } from 'react' +import { Tessera, Tessere } from './Tessere' +import { conControllo, controlliDi, leggiMulti, scriviMulti, valoreDa, type Controllo, type Parte } from '../controlli' +import type { Rapida } from '../rapide' + +type Props = { categoria: string; idee: Rapida[]; setIdee: (i: Rapida[]) => void } + +export function ControlliCategoria({ categoria, idee, setIdee }: Props): JSX.Element | null { + const lista = controlliDi(categoria) + if (!lista.length) return null + const metti = (c: Controllo, valore: string): void => setIdee(conControllo(idee, c, valore) as Rapida[]) + return ( +
+ {lista.map((c) => ( +
+
{c.icona} {c.nome}
+ {c.tipo === 'cursore' ? metti(c, String(v))} /> : metti(c, scriviMulti(v))} />} +
+ ))} +
+ ) +} + +function Cursore(p: { c: Extract; valore: number; cambia: (v: number) => void }): JSX.Element { + const { c } = p + const et = c.etichette?.[p.valore - c.min] ?? (p.valore === c.neutro ? '—' : p.valore > 0 && c.min < 0 ? `+${p.valore}` : String(p.valore)) + return ( +
+ {c.estremi[0]} + p.cambia(Number(e.target.value))} onDoubleClick={() => p.cambia(c.neutro)} /> + {c.estremi[1]} + {c.etichette ? c.etichette[p.valore - c.min] : et} +
+ ) +} + +function Multi(props: { c: Extract; valore: Record; cambia: (v: Record) => void }): JSX.Element { + // Una scelta incompleta (colore prima del capo, posizione prima del testo) non è ancora un'idea, + // ma non deve sparire. La bozza vive qui; solo le istruzioni complete entrano nel lavoro. + const esterno = scriviMulti(props.valore) + const [bozza, setBozza] = useState(props.valore) + useEffect(() => { setBozza(leggiMulti(esterno)) }, [esterno]) + const p = { ...props, valore: bozza, cambia: (v: Record): void => { setBozza(v); props.cambia(v) } } + const imposta = (parte: string, v: string): void => p.cambia({ ...p.valore, [parte]: p.valore[parte] === v ? '' : v }) + return ( +
+ {p.c.parti.map((x: Parte) => { + if (x.tipo === 'testo') { + return p.cambia({ ...p.valore, [x.id]: e.target.value })} /> + } + if (x.tipo === 'colori') { + return ( +
+ {x.opzioni.map((o) =>
+ ) + } + if (x.tipo === 'quadrante') { + // 3×3: le direzioni/posizioni stanno attorno; se sono 8 il centro è il soggetto + const celle = x.opzioni.length === 8 ? [...x.opzioni.slice(0, 4), null, ...x.opzioni.slice(4)] : x.opzioni + return ( +
+ {celle.map((o, i) => o + ? + : ●)} +
+ ) + } + return ( + + {x.opzioni.map((o) => {o.icona}} nome={o.nome} su={p.valore[x.id] === o.id} titolo={`${o.nome} · un clic la accende, un altro la spegne`} onClick={() => imposta(x.id, o.id)} />)} + + ) + })} +
+ ) +} diff --git a/src/renderer/src/componenti/Estrai.tsx b/src/renderer/src/componenti/Estrai.tsx index 184fdfd..cae0acd 100644 --- a/src/renderer/src/componenti/Estrai.tsx +++ b/src/renderer/src/componenti/Estrai.tsx @@ -16,7 +16,7 @@ import { apriInModifica, fotoDaOpera } from '../azioni' import { durata, stimaSecondi } from '../util' import { FORMATI_ESTRAI, INQUADRATURE, misuraEstrai, POSE_ESTRAI, SFONDI_ESTRAI, VISTE_OGGETTO, attesiEstrai, dimensioniEstrai, impostaEstrai, proporzioniEstrai, richiestaEstrai, - trovaElementi, usaEstrai, type Elemento + richiestaRitaglio, trovaElementi, usaEstrai, type Elemento } from '../estrai' const corto = (e: Elemento, i: number): string => (e.nome ? e.nome.split(/[,.;]/)[0].slice(0, 28) : e.tipo === 'persona' ? `Persona ${i + 1}` : `Oggetto ${i + 1}`) @@ -63,7 +63,12 @@ function FotoElementi(p: { foto: FotoBase }): JSX.Element { // un clic sceglie la cosa più piccola lì sotto; cliccando ancora si passa a quella più grande che la contiene, e poi si toglie la scelta const l = sotto(punto(e)) const i = l.findIndex((x) => x.id === scelto) - impostaEstrai({ scelto: !l.length ? null : i < 0 ? l[0].id : i + 1 < l.length ? l[i + 1].id : null }) + if (!l.length) { + // un clic nel vuoto: SAM 2 trova la cosa da quel punto (un solo punto alla volta; si vede un segno attorno) + const q = punto(e) + const n: Elemento = { id: 'p-' + crypto.randomUUID().slice(0, 6), box: [Math.max(0, q.x - 0.05), Math.max(0, q.y - 0.05), Math.min(1, q.x + 0.05), Math.min(1, q.y + 0.05)], nome: '', tipo: 'oggetto', manuale: true, punto: q } + impostaEstrai({ elementi: [...usaEstrai.getState().elementi.filter((x) => !x.punto), n], scelto: n.id }) + } else impostaEstrai({ scelto: i < 0 ? l[0].id : i + 1 < l.length ? l[i + 1].id : null }) } setBozza(null) } @@ -105,7 +110,7 @@ export function PalcoEstrai(p: { base: FotoBase | null; daGalleria: () => void;
ESTRAI DA UNA FOTO
-

Dai una foto e premi Trova persone e oggetti: clicchi quello che vuoi e lo ottieni da solo, su sfondo pulito.

+

Dai una foto, clicca la cosa che vuoi e premi Ritaglia: la ottieni su sfondo trasparente, con i pixel della foto (due clic). Con «Trova persone e oggetti» il modello le riquadra per te.

Una persona a figura intera (T-pose, A-pose, una posa disegnata), un busto o un volto; un oggetto intero, da davanti, di lato o dall'alto.

@@ -127,7 +132,7 @@ export function PalcoEstrai(p: { base: FotoBase | null; daGalleria: () => void; {inAttesa > 0 && {inCorso ? `si sta creando${percentuale(inCorso) !== null ? ` · ${percentuale(inCorso)}%` : ''}` : 'in coda'}{inAttesa > 1 ? ` · +${inAttesa - 1}` : ''}}
{!ris.length && !inAttesa ? ( -
Scegli una persona o un oggetto sulla foto e premi Estrai: compare qui, e finisce anche in Galleria.
+
Clicca una cosa sulla foto e premi Ritaglia (o Ricrea): compare qui, e finisce anche in Galleria.
) : (
@@ -167,6 +172,20 @@ export function ControlliEstrai(p: { base: FotoBase | null }): JSX.Element { const dim = dimensioniEstrai(m.mp, r) const fin = misuraEstrai(s.lato, dim) const persona = el?.tipo === 'persona' + const [stretto, setStretto] = useState(true) + const dalPunto = !!el?.punto + const trovati = s.elementi.filter((x) => !x.punto).length + + /** il ritaglio fedele: i pixel sono quelli della foto, lo sfondo diventa trasparente (SAM 2) */ + const ritaglio = async (): Promise => { + if (!p.base || !el) return + try { + const [id] = await api.lavori.accoda(richiestaRitaglio(p.base, el, stretto)) + attesiEstrai.set(id, { etichetta: `Ritaglio · ${corto(el, s.elementi.indexOf(el))}` }) + } catch (e) { + avvisa((e as Error).message, 'errore') + } + } const estrai = async (): Promise => { if (!p.base || !el) return @@ -189,7 +208,7 @@ export function ControlliEstrai(p: { base: FotoBase | null }): JSX.Element {
-

1 Cosa estrarre {s.elementi.length ? `${s.elementi.length} trovati` : ''}

+

1 Cosa estrarre {trovati ? `${trovati} trovati` : ''}

{el ? (
{persona ? : } @@ -197,7 +216,7 @@ export function ControlliEstrai(p: { base: FotoBase | null }): JSX.Element {
) : ( -
{s.cercando ? 'Il modello sta guardando la foto…' : !p.base ? 'Prima scegli una foto.' : s.elementi.length ? 'Clicca sulla foto la persona o l\'oggetto che vuoi (cliccando ancora nello stesso punto passi a quello più grande che lo contiene).' : 'Premi il pulsante: il modello trova le persone e gli oggetti nella foto. Puoi anche riquadrare a mano trascinando.'}
+
{s.cercando ? 'Il modello sta guardando la foto…' : !p.base ? 'Prima scegli una foto.' : s.elementi.length ? 'Clicca sulla foto la persona o l\'oggetto che vuoi (cliccando ancora nello stesso punto passi a quello più grande che lo contiene).' : 'Clicca sulla foto la cosa da ritagliare (SAM 2 la trova da solo), oppure premi il pulsante: il modello trova persone e oggetti. Puoi anche riquadrare a mano trascinando.'}
)} {el && (
@@ -208,12 +227,12 @@ export function ControlliEstrai(p: { base: FotoBase | null }): JSX.Element {
)} -
- {el && ( + {el && !dalPunto && (

2 Come lo vuoi

{persona ? ( @@ -252,7 +271,7 @@ export function ControlliEstrai(p: { base: FotoBase | null }): JSX.Element {
)} -
+

{el ? 3 : 2} Dimensioni e qualità

Proporzioni
@@ -283,10 +302,14 @@ export function ControlliEstrai(p: { base: FotoBase | null }): JSX.Element {
- +
Ritaglio stretto attorno alla cosa
+ -
{fin.w} × {fin.h} px · ~{durata(stima)}
+
{fin.w} × {fin.h} px · ~{durata(stima)}
) diff --git a/src/renderer/src/componenti/Fusion.tsx b/src/renderer/src/componenti/Fusion.tsx index 710740f..575ffa4 100644 --- a/src/renderer/src/componenti/Fusion.tsx +++ b/src/renderer/src/componenti/Fusion.tsx @@ -112,7 +112,7 @@ export function Fusion(p: { {p.inAttesa > 0 && {p.inCorso ? `si sta creando${percentuale(p.inCorso) !== null ? ` · ${percentuale(p.inCorso)}%` : ''}` : 'in coda'}{p.inAttesa > 1 ? ` · +${p.inAttesa - 1}` : ''}}
{!p.risultati.length && !p.inAttesa ? ( -
Scrivi cosa vuoi a destra e premi il pulsante: la foto nuova compare qui e finisce anche in Galleria.
+
Scrivi cosa vuoi a sinistra e premi il pulsante: la foto nuova compare qui e finisce anche in Galleria.
) : (
diff --git a/src/renderer/src/componenti/Generi.tsx b/src/renderer/src/componenti/Generi.tsx new file mode 100644 index 0000000..12b5976 --- /dev/null +++ b/src/renderer/src/componenti/Generi.tsx @@ -0,0 +1,106 @@ +// I generi di Crea (ex «preset») come tessere, e l'atmosfera: una tessera per ogni stile di luce del genere scelto. +// Scegliere un genere mette formato, risoluzione e sfondo giusti e scrive il prompt dagli esempi; +// cambiando un campo o l'atmosfera il prompt si riscrive (e si può sempre ritoccare a mano). +// Dalla GUI v2 il genere non è più un menu a tendina: 11 tessere + «Libero». Copertina, slide, app, arte e sticker non hanno la tessera +// (stanno nei Formati, nello Stile e nel Set delle Ricette) ma restano nell'elenco per leggere i lavori salvati. +import type { JSX } from 'react' +import { usaStato } from '../stato' +import { I } from './Icone' +import { Casella } from './Casella' +import { Tessera, Tessere } from './Tessere' +import { PRESET, campiCon, componiPreset, presetDa } from '../preset' + +const icona = (nome: string): JSX.Element => { + const Ic = (I as Record JSX.Element>)[nome] || I.crea + return +} + +/** i generi che hanno la tessera, nell'ordine della spec */ +const GENERI = ['foto', 'ritratto', 'prodotto', 'poster', 'infografica', 'logo', 'social', 'miniatura', 'fumetto', 'invito', 'menu'] + +/** sceglie un genere (o «Libero» con '') */ +export function scegliGenere(id: string, prima: (prompt: string) => void): void { + const { crea, setCrea } = usaStato.getState() + const p = presetDa(id) + if (!p) { + setCrea({ preset: '' }) + return + } + if (!crea.preset && crea.prompt.trim()) prima(crea.prompt) + const stile = p.stili[0]?.id || '' + setCrea({ + preset: p.id, stile, campi: {}, formato: p.formato, ris: p.ris, trasparente: !!p.trasparente, sfondo: p.trasparente ? 'trasparente' : 'scena', + prompt: componiPreset(p, {}, stile) + }) +} + +/** «Cosa»: la griglia dei generi 4×3 */ +export function GrigliaGeneri({ prima }: { prima: (prompt: string) => void }): JSX.Element { + const crea = usaStato((s) => s.crea) + const attivo = presetDa(crea.preset) + const nascosto = !!attivo && !GENERI.includes(attivo.id) + return ( + + {GENERI.map((id) => { + const p = presetDa(id)! + return scegliGenere(id, prima)} /> + })} + scegliGenere('', prima)} /> + + ) +} + +const ATMOSFERE: [RegExp, string][] = [ + [/naturale|luce/i, '☀️'], [/oro/i, '🌅'], [/notte|neon/i, '🌙'], [/pellicola/i, '🎞️'], [/finestra/i, '🪟'], [/studio/i, '💡'], [/strada/i, '🛣️'], + [/vetro/i, '🔮'], [/pop/i, '💥'], [/lusso|elegante/i, '💎'], [/minimal|pulita|chiaro/i, '⚪'], [/scuro/i, '🌑'], [/acquerello/i, '🎨'], [/pixel/i, '👾'] +] +const emojiAtmosfera = (nome: string, i: number): string => ATMOSFERE.find(([r]) => r.test(nome))?.[1] || ['✨', '🌈', '🎯', '🔆'][i % 4] +const SFONDI = ['linear-gradient(135deg,#3a2c7a,#f2a65a)', 'linear-gradient(135deg,#f6c453,#c0392b)', 'linear-gradient(135deg,#0b1d4a,#7b2cbf)', 'linear-gradient(135deg,#3b6b5e,#d8c3a5)'] + +/** «Atmosfera»: gli stili del genere come tessere con icona, una sola scelta */ +export function AtmosferaGenere(): JSX.Element | null { + const { crea, setCrea } = usaStato() + const p = presetDa(crea.preset) + if (!p || p.stili.length < 2) return null + const valori = campiCon(p, crea.campi) + return ( + + {p.stili.map((s, i) => ( + {emojiAtmosfera(s.nome, i)}} nome={s.nome} fondo={SFONDI[i % SFONDI.length]} titolo={`${s.nome}: ${s.testo}`} su={crea.stile === s.id} onClick={() => setCrea({ stile: s.id, prompt: componiPreset(p, valori, s.id) })} /> + ))} + + ) +} + +/** i campi del genere scelto (1–3 righe in italiano, con esempi in grigio) */ +export function CampiGenere(): JSX.Element | null { + const { crea, setCrea } = usaStato() + const p = presetDa(crea.preset) + if (!p) return null + const valori = campiCon(p, crea.campi) + const cambiaCampo = (id: string, valore: string): void => { + const campi = { ...valori, [id]: valore } + setCrea({ campi, prompt: componiPreset(p, campi, crea.stile) }) + } + return ( +
+
+ {p.campi.map((c) => ( + + ))} +
+
+ Scrivi in italiano: i testi da mettere nell'immagine restano identici (anche gli accenti). Il prompt qui sotto si aggiorna da solo. +
+
+ ) +} + +export { PRESET } diff --git a/src/renderer/src/componenti/Hud.tsx b/src/renderer/src/componenti/Hud.tsx new file mode 100644 index 0000000..9eb146b --- /dev/null +++ b/src/renderer/src/componenti/Hud.tsx @@ -0,0 +1,35 @@ +// HUD misura: la striscia fissa sul palco (e la versione corta accanto al pulsante) con i pixel che escono davvero. +// Prima riga: forma in scala, pixel finali, rapporto, megapixel. Seconda riga solo quando c'è una conversione (verde nativa, ciano ingrandita, ambra ridotta). +import type { JSX, ReactNode } from 'react' +import type { StatoMisura } from '../misura' + +export interface DatiHud { + w: number + h: number + rapporto: string + mp: number + stato: StatoMisura + nota: string +} + +const testoMp = (mp: number): string => (Math.round(mp * 100) / 100).toFixed(2).replace('.', ',') + +export function Hud(p: { d: DatiHud; corto?: boolean; onClick?: () => void; extra?: ReactNode; titolo?: string }): JSX.Element { + const k = 26 / Math.max(p.d.w, p.d.h) + return ( +
e.key === 'Enter' && p.onClick?.()}> + + + + {p.d.w} × {p.d.h}px + {p.d.rapporto.replace(':', ' : ')} + {testoMp(p.d.mp)} MP + + {!p.corto && p.d.stato !== 'nativa' && {p.d.nota}} + {!p.corto && p.d.stato === 'nativa' && {p.d.nota}} + + {p.extra} +
+ ) +} diff --git a/src/renderer/src/componenti/Icone.tsx b/src/renderer/src/componenti/Icone.tsx index 1c900d3..01633f9 100644 --- a/src/renderer/src/componenti/Icone.tsx +++ b/src/renderer/src/componenti/Icone.tsx @@ -79,5 +79,26 @@ export const I = { catena: () => , freccia: () => , casa: () => , - gioca: () => + gioca: () => , + // --- GUI v2 «Orbita» --- + studio: () => , + estrai: () => , + box: () => , + ricette: () => , + pila: () => , + fiamma: () => , + lucchetto: () => , + scacchi: () => , + scena: () => , + tinta: () => , + ritaglio: () => , + ricrea: () => , + susfondo: () => , + ingranaggi: () => , + interruttore: () => , + cartello: () => , + occhioBarrato: () => , + sposta: () => , + cursore: () => , + duplica: () => } diff --git a/src/renderer/src/componenti/LabVista.tsx b/src/renderer/src/componenti/LabVista.tsx index 06a31c2..dd645de 100644 --- a/src/renderer/src/componenti/LabVista.tsx +++ b/src/renderer/src/componenti/LabVista.tsx @@ -28,7 +28,7 @@ export function Catalogo(p: { cerca: string; scelto: string | null; scegli: (id:

{icona(c.icona)} {c.nome} {c.testo}

{voci.map((w) => ( -
+
} + {acceso && <> + {!p.catalogo.some((v) => v.uncensored) &&

Nessun componente Uncensored approvato disponibile nel catalogo di questa versione.

} + {p.catalogo.filter((v) => v.uncensored).map((v) => { + const d = p.download[v.nome] + const inCorso = d && (d.stato === 'in corso' || d.stato === 'verifica') + return
+
{v.nome} Uncensored
{v.ruolo || v.tipo} · {byte(v.byte)} · {v.licenza || 'Licenza da verificare'}
+
{v.licenzaUrl && }{v.presente ? Presente : }
+ {v.avviso || v.descrizione} +
+ })} + {p.catalogo.some((v) => v.id === 'llm-huihui-4b' && v.presente) && void p.salva({ testoUncensored: v })}>Con Klein, scrivi i testi con l’LLM Uncensored (Qwen3-VL 4B)} +

FLUX / Klein 3 Uncensored: non disponibile. Non distribuiamo varianti che aggirano protezioni vietate dalla licenza Kommunity.

+ } +
+ +} diff --git a/src/renderer/src/componenti/Preset.tsx b/src/renderer/src/componenti/Preset.tsx deleted file mode 100644 index b9e976d..0000000 --- a/src/renderer/src/componenti/Preset.tsx +++ /dev/null @@ -1,86 +0,0 @@ -// Il menu degli stili (in cima alla colonna di Crea) e la scheda coi campi del preset scelto. -// Scegliere un preset mette formato, risoluzione e sfondo giusti e scrive il prompt dagli esempi; -// cambiando un campo o uno stile il prompt si riscrive (e si può sempre ritoccare a mano). -import type { JSX } from 'react' -import { usaStato } from '../stato' -import { I } from './Icone' -import { Casella } from './Casella' -import { PRESET, campiCon, componiPreset, presetDa } from '../preset' - -const icona = (nome: string): JSX.Element => { - const Ic = (I as Record JSX.Element>)[nome] || I.crea - return -} - -/** gli stili in un menu solo: «Libero» (scrivi tu) o uno dei preset */ -export function MenuStili({ prima }: { prima: (prompt: string) => void }): JSX.Element { - const { crea, setCrea } = usaStato() - const scegli = (id: string): void => { - const p = presetDa(id) - if (!p) { - setCrea({ preset: '' }) - return - } - if (!crea.preset && crea.prompt.trim()) prima(crea.prompt) - const stile = p.stili[0]?.id || '' - setCrea({ preset: p.id, stile, campi: {}, formato: p.formato, ris: p.ris, trasparente: !!p.trasparente, prompt: componiPreset(p, {}, stile) }) - } - const attivo = presetDa(crea.preset) - return ( - - ) -} - -export function SchedaPreset(): JSX.Element | null { - const { crea, setCrea } = usaStato() - const p = presetDa(crea.preset) - if (!p) return null - const valori = campiCon(p, crea.campi) - const cambiaCampo = (id: string, valore: string): void => { - const campi = { ...valori, [id]: valore } - setCrea({ campi, prompt: componiPreset(p, campi, crea.stile) }) - } - const cambiaStile = (stile: string): void => setCrea({ stile, prompt: componiPreset(p, valori, stile) }) - return ( -
-

- {icona(p.icona)} {p.nome} - - - -

-
- {p.stili.map((s) => ( - - ))} -
-
- {p.campi.map((c) => ( - - ))} -
-
- Scrivi in italiano: i testi da mettere nell'immagine restano identici (anche gli accenti). Il prompt qui sotto si aggiorna da solo. -
-
- ) -} diff --git a/src/renderer/src/componenti/ScaffaliStili.tsx b/src/renderer/src/componenti/ScaffaliStili.tsx new file mode 100644 index 0000000..893d109 --- /dev/null +++ b/src/renderer/src/componenti/ScaffaliStili.tsx @@ -0,0 +1,29 @@ +// Gli stili nuovi come scaffali di tessere + l'intensità a tre pianeti. Si usano in Crea (si aggiungono al prompt) e in Modifica (trasformano la foto). +import type { JSX } from 'react' +import { INTENSITA, SCAFFALI, STILI, type Intensita } from '../stili' +import { Pianeti, Tessera } from './Tessere' + +export function ScaffaliStili(p: { valore: string; scegli: (id: string) => void }): JSX.Element { + return ( +
+ {SCAFFALI.map((sc) => ( +
+
{sc.nome}
+
+ {STILI.filter((s) => s.scaffale === sc.id).map((s) => ( + {s.icona}} nome={s.nome} fondo={s.fondo} titolo={`${s.nome}: ${s.frammento}`} su={p.valore === s.id} onClick={() => p.scegli(p.valore === s.id ? '' : s.id)} /> + ))} +
+
+ ))} +
+ ) +} + +export function IntensitaStile(p: { valore: Intensita; cambia: (i: Intensita) => void }): JSX.Element { + return ( +
+ ({ id: i.id, nome: i.nome, titolo: i.titolo }))} /> +
+ ) +} diff --git a/src/renderer/src/componenti/ScegliModello.tsx b/src/renderer/src/componenti/ScegliModello.tsx index 1bde24e..dc03f49 100644 --- a/src/renderer/src/componenti/ScegliModello.tsx +++ b/src/renderer/src/componenti/ScegliModello.tsx @@ -7,13 +7,14 @@ import { MODELLI, ORDINE_MODELLI, modelloDaImpostazioni, type IdModello } from ' import { api } from '../api' import { ricaricaImp, usaStato } from '../stato' import { I } from './Icone' +import { Tessera, Tessere } from './Tessere' /** le voci del catalogo da avere per usare un modello (il modello INT8 sta nelle schede da 8 GB) */ const SERVONO: Record, string[]> = { klein: ['klein-int8', 'klein-te', 'klein-vae'], 'klein-base': ['klein-base-int8', 'klein-te', 'klein-vae'], klein4b: ['klein4b-fp8', 'klein4b-te', 'klein-vae'] } const PESO: Record, string> = { klein: 'circa 10 GB', 'klein-base': 'circa 10 GB (4,7 se hai già Klein)', klein4b: 'circa 12,5 GB' } const ETICHETTA: Record = { 'qwen-base': 'Qwen base', klein: 'Klein veloce', 'klein-base': 'Klein Base', klein4b: 'Klein 4B' } -export function SelettoreModello(): JSX.Element { +export function SelettoreModello({ variante = 'pillole' }: { variante?: 'pillole' | 'tessere' }): 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')) @@ -88,7 +89,26 @@ export function SelettoreModello(): JSX.Element { } return ( -
+
+ {variante === 'tessere' ? ( + + {ORDINE_MODELLI.map((id) => { + const m = MODELLI[id] + return ( + : id === 'klein' ? : } + nome={ETICHETTA[id]} + su={id === scelto} + daScaricare={!pronti[id]} + onClick={() => void scegli(id)} + titolo={`${m.nome}: ${m.sotto}${!pronti[id] ? ' (da scaricare)' : ''}${inCorso ? ' · vale dal prossimo lavoro' : ''}`} + /> + ) + })} + + ) : (
{ORDINE_MODELLI.map((id) => { const m = MODELLI[id] @@ -107,6 +127,7 @@ export function SelettoreModello(): JSX.Element { ) })}
+ )} {aperto && aperto !== 'qwen-base' && !pronti[aperto] && (
{MODELLI[aperto].nome} @@ -122,8 +143,7 @@ export function SelettoreModello(): JSX.Element { ) } -/** Il vecchio selettore a schede dentro Crea, Estrai e Modifica: ora il modello si sceglie nella barra in alto (`SelettoreModello`). - * Resta vuoto (e il `.sezione:empty` in stile.css lo nasconde) finché Modifica.tsx non ne toglie la riga (T6). */ -export function ScegliModello(): null { - return null +/** Lo stesso selettore di Crea, anche nel pannello di Modifica. */ +export function ScegliModello(): JSX.Element { + return <>

Motore

} diff --git a/src/renderer/src/componenti/SezioniCrea.tsx b/src/renderer/src/componenti/SezioniCrea.tsx new file mode 100644 index 0000000..ef1b6aa --- /dev/null +++ b/src/renderer/src/componenti/SezioniCrea.tsx @@ -0,0 +1,141 @@ +// Le sezioni di Crea fatte a tessere/pulsanti, condivise con il modo Box (stesso formato, stessa misura, stesso sfondo): +// Formato (la forma del rettangolo in scala), Misura (S · M · L · XL · MAX coi pixel veri), Sfondo, Quante + Seed, e il HUD con il suo popover. +import { useState, type JSX } from 'react' +import { usaStato } from '../stato' +import { I } from './Icone' +import { Hud } from './Hud' +import { Tessera, Tessere, PulsanteS } from './Tessere' +import { SceltaMisura, ScegliMp, MISURA0, Uscita } from './Misura' +import { FORMATI, dimensioniRis } from '../util' +import { misuraCrea } from '../misura' + +export const TAGLIE: { ris: number; nome: string; titolo: string }[] = [ + { ris: 512, nome: 'S', titolo: 'Lato corto 512 px: veloce, per provare' }, + { ris: 768, nome: 'M', titolo: 'Lato corto 768 px' }, + { ris: 1024, nome: 'L', titolo: 'Lato corto 1024 px: l\'equilibrio giusto' }, + { ris: 1440, nome: 'XL', titolo: 'Lato corto 1440 px: per testi piccoli, poster e infografiche' }, + { ris: 2048, nome: 'MAX', titolo: 'Il massimo nativo (4,2 MP): lento su 6-8 GB' } +] +/** le misure «bozza» di prima (256 … 1080), dietro il lampo */ +const BOZZE: number[] = [256, 384, 480, 576, 720, 1080] + +export function SezFormato(): JSX.Element { + const { crea, setCrea } = usaStato() + return ( +
+ {FORMATI.map((f) => { + const k = 24 / Math.max(f.w, f.h) + return ( + + ) + })} +
+ ) +} + +export function SezMisura(): JSX.Element { + const { crea, setCrea } = usaStato() + const [bozze, setBozze] = useState(false) + const bozzaAttiva = BOZZE.includes(crea.ris) || !TAGLIE.some((t) => t.ris === crea.ris) + return ( +
+
+ {TAGLIE.map((t) => { + const d = dimensioniRis(crea.formato, t.ris) + return setCrea({ ris: t.ris, misura: crea.misura?.tipo === 'custom' ? MISURA0 : crea.misura })} /> + })} + setBozze(!bozze)} titolo="Bozze veloci: 256p … 1080p" /> +
+ {(bozze || (bozzaAttiva && BOZZE.includes(crea.ris))) && ( +
+ {BOZZE.map((r) => { + const d = dimensioniRis(crea.formato, r) + return setCrea({ ris: r, misura: crea.misura?.tipo === 'custom' ? MISURA0 : crea.misura })} /> + })} +
+ )} +
+ ) +} + +const TINTE = ['#ffffff', '#f2f2f2', '#000000', '#e63946', '#f4a261', '#ffd54a', '#2a9d8f', '#3fe0ff', '#8a5cff', '#ff3df2'] + +export function SezSfondo(): JSX.Element { + const { crea, setCrea, imp } = usaStato() + const klein = (imp?.famiglia || 'qwen') !== 'qwen' + const scegli = (sfondo: 'scena' | 'trasparente' | 'tinta'): void => setCrea({ sfondo, trasparente: sfondo === 'trasparente' }) + return ( +
+ + } nome="Scena" titolo="L'immagine con il suo sfondo" su={crea.sfondo === 'scena'} onClick={() => scegli('scena')} /> + } nome="Senza sfondo" titolo="PNG con trasparenza vera: solo il soggetto" su={crea.sfondo === 'trasparente'} onClick={() => scegli('trasparente')} /> + } nome="Tinta unita" titolo="Il soggetto su un colore pieno (prodotti, sticker)" su={crea.sfondo === 'tinta'} onClick={() => scegli('tinta')} /> + + {crea.sfondo === 'tinta' && ( +
+ {TINTE.map((c) => ( +
+ )} + {crea.sfondo === 'trasparente' && ( +
+ {([['scacchi', 'Scacchi'], ['bianco', 'Bianco'], ['nero', 'Nero'], ['colore', 'Colore']] as const).map(([id, nome]) => ( + {nome}} su={crea.anteSfondo === id} onClick={() => setCrea({ anteSfondo: id })} titolo="Cambia solo la vista, non il file" /> + ))} +
+ )} + {crea.sfondo === 'trasparente' && klein && ( +
Klein non disegna la trasparenza da sé: crea il soggetto su fondo pulito e il motore toglie lo sfondo dopo (serve il motore v2). Con Qwen il PNG è già trasparente.
+ )} +
+ ) +} + +export function SezQuante(): JSX.Element { + const { crea, setCrea } = usaStato() + return ( +
+ {[1, 2, 4, 8].map((n) => ( + setCrea({ quante: n })} titolo={n === 1 ? 'Una immagine' : `${n} immagini, una dopo l'altra`} /> + ))} + +
+ ) +} + +/** Il HUD con il popover «Misura»: la cifra che vedi è quella che esce, e un clic apre le scelte */ +export function HudCrea(): JSX.Element { + const { crea, setCrea } = usaStato() + const [aperto, setAperto] = useState(false) + const m = misuraCrea(crea) + const mis = crea.misura?.tipo === 'custom' ? crea.misura : null + return ( + <> + setAperto(!aperto)} titolo="Clic per cambiare formato e misura" /> + {aperto && ( +
undefined}> +
MISURA
+ + +
+ Megapixel a cui disegna il modello + setCrea({ mpLavoro })} ar={{ w: m.finale.w, h: m.finale.h }} auto="come la misura" /> +
+
+ Misura precisa {mis ? '(attiva)' : ''} + setCrea({ misura })} base={null} mp={Math.round((m.lav.w * m.lav.h) / 1e5) / 10} ammetti={['auto', 'custom']} auto={{ w: m.lav.w, h: m.lav.h }} senzaUscita /> +
+ +
+ )} + + ) +} diff --git a/src/renderer/src/componenti/Tessere.tsx b/src/renderer/src/componenti/Tessere.tsx new file mode 100644 index 0000000..40267ad --- /dev/null +++ b/src/renderer/src/componenti/Tessere.tsx @@ -0,0 +1,93 @@ +// I pezzi base della GUI v2 «Orbita»: la tessera (icona grande + una parola), la griglia, la fascetta Uncensored e i pulsanti di misura. +// Regola d'oro: una tessera ha un'icona e una parola; il nome lungo sta nel tooltip (`titolo`). Niente chip di testo sotto le tessere. +import type { CSSProperties, JSX, ReactNode } from 'react' +import { I } from './Icone' + +export interface PropsTessera { + /** un'icona SVG (di `I`), o un emoji/carattere */ + icona: ReactNode + nome: string + su?: boolean + onClick?: () => void + /** il nome lungo e la spiegazione: compare al passaggio del mouse */ + titolo?: string + disabilitata?: boolean + /** il puntino ambra: file da scaricare */ + daScaricare?: boolean + /** la pallina luminosa con il numero di scelte attive */ + pallina?: number + /** tessera larga: icona a sinistra, nome e sottotitolo */ + sotto?: string + /** tessera con anteprima: un gradiente di fondo (il fallback delle immagini che non ci sono ancora) */ + fondo?: string + uncensored?: boolean + id?: string +} + +export function Tessera(p: PropsTessera): JSX.Element { + const larga = p.sotto !== undefined + const classe = ['tessera-o', larga ? 'larga' : '', p.fondo ? 'anteprima' : '', p.su ? 'su' : '', p.daScaricare ? 'da-scaricare' : '', p.uncensored ? 'fiamma' : '', p.disabilitata ? 'spenta' : ''].filter(Boolean).join(' ') + return ( + + ) +} + +/** una griglia di tessere con N colonne */ +export function Tessere(p: { colonne?: 2 | 3 | 4 | 5 | 6; children: ReactNode; stile?: CSSProperties }): JSX.Element { + const c = p.colonne ?? 4 + return ( +
+ {p.children} +
+ ) +} + +/** la fascetta arancio dei modelli senza filtri di rifiuto: riservata a questo scopo */ +export function Fascetta(): JSX.Element { + return ( + + Uncensored + + ) +} + +/** un pulsante di misura/quantità: un valore grande e (sotto) i pixel o la nota */ +export function PulsanteS(p: { nome: ReactNode; sotto?: ReactNode; su?: boolean; onClick: () => void; titolo?: string }): JSX.Element { + return ( + + ) +} + +/** tre pianeti di grandezza crescente: per l'intensità, la fantasia */ +export function Pianeti(p: { valore: T; voci: { id: T; nome: string; titolo?: string }[]; cambia: (v: T) => void }): JSX.Element { + return ( +
+
+ {p.voci.map((v) => ( + + ))} +
+
{p.voci.map((v) => {v.nome})}
+
+ ) +} + +/** l'emoji o il carattere in una tessera: stesso spazio di un'icona */ +export const E = (s: string): JSX.Element => {s} diff --git a/src/renderer/src/controlli.ts b/src/renderer/src/controlli.ts new file mode 100644 index 0000000..f3729b3 --- /dev/null +++ b/src/renderer/src/controlli.ts @@ -0,0 +1,274 @@ +// I controlli interattivi di Modifica, per categoria (GUI v2 §6.2): cursori, griglie a icona, campioni colore, quadranti. +// Ogni controllo ha un valore (stringa) e lo trasforma in una frase in inglese (`compila`): `null` = spento, non entra nel lavoro. +// Il valore sta nell'id dell'idea attiva (`ctl-:`): niente stato in più, le Idee sono la fonte di verità. +// Qui sta solo il catalogo (puro: si prova senza interfaccia); i pezzi grafici sono in componenti/ControlliCategoria.tsx. +import type { ChiaveLora, Rapida } from './rapide' + +export interface Opzione { id: string; nome: string; icona: string; frase: string } + +/** una riga di un controllo: una griglia di icone, i campioni colore, il quadrante 3×3 o una riga di testo */ +export type Parte = + | { id: string; tipo: 'griglia' | 'colori' | 'quadrante'; nome: string; opzioni: Opzione[] } + | { id: string; tipo: 'testo'; nome: string; esempio: string } + +export interface Compilato { frase: string; lora?: ChiaveLora[] } + +export type Controllo = + | { id: string; categoria: string; tipo: 'cursore'; nome: string; icona: string; tooltip: string; min: number; max: number; neutro: number; estremi: [string, string]; etichette?: string[]; compila: (v: number) => Compilato | null } + | { id: string; categoria: string; tipo: 'multi'; nome: string; icona: string; tooltip: string; parti: Parte[]; compila: (v: Record) => Compilato | null } + +const INVARIATO = 'Keep the identity, face, pose and everything else unchanged.' +/** [id, nome, icona, frase in inglese] */ +type Voce = [string, string, string, string] +const op = (l: Voce[]): Opzione[] => l.map(([id, nome, icona, frase]) => ({ id, nome, icona, frase })) +const frase = (o: Opzione[], id: string | undefined): string => o.find((x) => x.id === id)?.frase || '' + +const COLORI = op([ + ['#111111', 'Nero', '#111111', 'black'], ['#5a3a22', 'Castano', '#5a3a22', 'dark brown'], ['#e6c880', 'Biondo', '#e6c880', 'blonde'], ['#b5361f', 'Rosso', '#b5361f', 'copper red'], + ['#aaaaaa', 'Grigio', '#aaaaaa', 'silver gray'], ['#f2f2f2', 'Bianco', '#f2f2f2', 'white'], ['#2563eb', 'Blu', '#2563eb', 'electric blue'], ['#16a34a', 'Verde', '#16a34a', 'green'], + ['#ec4899', 'Rosa', '#ec4899', 'pink'], ['#9333ea', 'Viola', '#9333ea', 'purple'] +]) +const ESPRESSIONI = op([ + ['sorriso', 'Sorriso', '😊', 'a warm natural smile'], ['ride', 'Ride', '😂', 'laughing out loud'], ['serio', 'Serio', '😐', 'a serious, neutral look'], + ['sorpreso', 'Sorpreso', '😮', 'a surprised look with wide eyes'], ['triste', 'Triste', '😢', 'a sad look'], ['occhiolino', 'Occhiolino', '😉', 'a playful wink'] +]) +const BARBE = op([ + ['rasata', 'Rasata', '🪒', 'a clean-shaven face, with no beard or stubble'], ['ombra', 'Ombra', '😶', 'a light stubble'], ['corta', 'Corta', '🧔‍♂️', 'a short trimmed beard'], ['folta', 'Folta', '🧔', 'a full thick beard'] +]) +const CAPI = op([ + ['giacca', 'Giacca', '🧥', 'a jacket'], ['abito', 'Abito', '👗', 'a dress'], ['maglia', 'Maglia', '👕', 'a sweater'], ['pantaloni', 'Pantaloni', '👖', 'trousers'], + ['cappotto', 'Cappotto', '🧣', 'a long coat'], ['cappello', 'Cappello', '🎩', 'a hat'], ['scarpe', 'Scarpe', '👟', 'a pair of shoes'] +]) +const MATERIALI = op([['jeans', 'Jeans', '🔷', 'denim'], ['seta', 'Seta', '🎀', 'silk'], ['pelle', 'Pelle', '🟫', 'leather'], ['lana', 'Lana', '🧶', 'wool'], ['metallo', 'Metallo', '⚙️', 'metallic fabric']]) +const OBIETTIVI = op(['14', '24', '35', '50', '85', '200'].map((n): Voce => [n, `${n}mm`, n, `${n}mm`])) +/** il quadrante 3×3 attorno al soggetto: 8 direzioni (il centro non c'è) */ +const DIREZIONI = op([ + ['tl', 'Alto sinistra', '↖', 'the upper left'], ['t', 'Sopra', '↑', 'directly above'], ['tr', 'Alto destra', '↗', 'the upper right'], + ['l', 'Sinistra', '←', 'the left side'], ['r', 'Destra', '→', 'the right side'], + ['bl', 'Basso sinistra', '↙', 'the lower left'], ['b', 'Sotto', '↓', 'directly below'], ['br', 'Basso destra', '↘', 'the lower right'] +]) +const QUALITA_LUCE = op([['morbida', 'Morbida', '☁️', 'soft diffused light with gentle shadows'], ['dura', 'Dura', '☀️', 'hard direct light with crisp defined shadows']]) +const LINGUE = op([ + ['en', 'Inglese', '🇬🇧', 'English'], ['es', 'Spagnolo', '🇪🇸', 'Spanish'], ['fr', 'Francese', '🇫🇷', 'French'], ['de', 'Tedesco', '🇩🇪', 'German'], ['ja', 'Giapponese', '🇯🇵', 'Japanese'], ['zh', 'Cinese', '🇨🇳', 'Simplified Chinese'] +]) +const POSIZIONI = op([ + ['tl', 'Alto sinistra', '◤', 'in the top left corner'], ['t', 'In alto', '▀', 'across the top'], ['tr', 'Alto destra', '◥', 'in the top right corner'], + ['l', 'A sinistra', '▌', 'on the left side'], ['c', 'Al centro', '■', 'in the center'], ['r', 'A destra', '▐', 'on the right side'], + ['bl', 'Basso sinistra', '◣', 'in the bottom left corner'], ['b', 'In basso', '▄', 'across the bottom'], ['br', 'Basso destra', '◢', 'in the bottom right corner'] +]) +const FORMATI_TESTO = op([ + ['titolo', 'Titolo', '🅰️', 'as a huge bold sans-serif headline'], ['cartello', 'Cartello', '🪧', 'on a wooden sign placed naturally in the scene'], + ['meme', 'Meme', '😂', 'as bold white Impact-style letters with a black outline'], ['firma', 'Firma', '✍️', 'as a small elegant handwritten signature'] +]) +const SFONDI = op([['bianco', 'Bianco', '⬜', 'a plain pure white'], ['grigio', 'Grigio', '◻️', 'a neutral light gray studio'], ['sfumato', 'Sfumato', '🌫️', 'a soft blurred gradient']]) + +const ORE = ['', 'at dawn with soft pink and orange light on the horizon', 'in the bright clear morning', 'at noon under harsh overhead sun', 'at sunset with a glowing golden-red sky', 'at blue hour just after sunset, deep blue sky with warm lights', 'at night with a dark sky and artificial lights'] +const EPOCHE = ['', 'ancient Rome: marble columns, togas, Mediterranean sun', 'the Middle Ages: stone buildings, period clothes, torch light', 'the 1920s: art deco style, period clothes and cars, sepia tones', 'the 1980s: neon, big hair, retro tech and film grain', 'a far future: sleek technology, holograms, silver architecture'] + +const griglia = (id: string, categoria: string, nome: string, icona: string, voci: Voce[], compila: (frase: string) => string, lora?: ChiaveLora[]): Controllo => { + const opzioni = op(voci) + return { id, categoria, nome, icona, tooltip: nome, tipo: 'multi', parti: [{ id: 'scelta', tipo: 'griglia', nome, opzioni }], compila: (v) => { + const f = frase(opzioni, v.scelta) + return f ? { frase: compila(f), lora } : null + } } +} + +export const CONTROLLI: Controllo[] = [ + griglia('capelli', 'persone', 'Acconciatura', '💇', [ + ['corti', 'Corti', '👨', 'short cropped hair'], ['lunghi', 'Lunghi', '👩', 'long flowing hair'], ['ricci', 'Ricci', '🧑‍🦱', 'curly hair'], ['lisci', 'Lisci', '👩‍🦰', 'straight hair'], + ['coda', 'Coda', '🎀', 'a ponytail'], ['trecce', 'Trecce', '🪢', 'braided hair'], ['bob', 'Caschetto', '👩‍🦳', 'a bob haircut'], ['rasati', 'Rasati', '🪒', 'a shaved head'] + ], (f) => `Give the person ${f}. Keep the face, expression, clothing and everything else unchanged.`), + griglia('animale', 'persone', 'Animale', '🐾', [ + ['cane', 'Cane', '🐕', 'a dog'], ['gatto', 'Gatto', '🐈', 'a cat'], ['cavallo', 'Cavallo', '🐎', 'a horse'], ['uccello', 'Uccello', '🐦', 'a bird'], ['drago', 'Drago', '🐉', 'a friendly dragon'] + ], (f) => `Add ${f} next to the subject, naturally integrated with matching light, shadows and scale. Keep everything else unchanged.`), + griglia('costume', 'vestiti', 'Costume', '🎭', [ + ['astronauta', 'Astronauta', '🧑‍🚀', 'an astronaut'], ['cavaliere', 'Cavaliere', '🛡️', 'a medieval knight'], ['pirata', 'Pirata', '🏴‍☠️', 'a pirate'], ['supereroe', 'Supereroe', '🦸', 'a superhero'], + ['samurai', 'Samurai', '⚔️', 'a samurai'], ['mago', 'Mago', '🧙', 'a wizard'], ['chef', 'Chef', '👨‍🍳', 'a chef'], ['medico', 'Medico', '🩺', 'a doctor'], ['sposa', 'Sposa', '👰', 'a bride'] + ], (f) => `Dress the person as ${f}. Keep face, hair, pose and background unchanged.`, ['vestiti']), + griglia('luogo', 'situazioni', 'Luogo', '🌍', [ + ['citta', 'Città', '🏙️', 'a lively city street'], ['spiaggia', 'Spiaggia', '🏖️', 'a tropical beach'], ['montagna', 'Montagna', '🏔️', 'a mountain landscape'], ['bosco', 'Bosco', '🌲', 'a lush forest'], ['deserto', 'Deserto', '🏜️', 'a sandy desert'], + ['interno', 'Interno', '🏠', 'a cozy interior'], ['studio-bianco', 'Studio', '⬜', 'a white photography studio'], ['spazio', 'Spazio', '🪐', 'outer space with stars and planets'], ['sott-acqua', 'Sott’acqua', '🐠', 'an underwater scene'], ['futuro', 'Futuro', '🛸', 'a futuristic city'] + ], (f) => `Place the subject in ${f}, matching lighting, perspective and scale. Keep the subject exactly the same.`), + griglia('meteo', 'situazioni', 'Meteo', '🌦️', [ + ['sole', 'Sole', '☀️', 'bright sunshine'], ['nuvole', 'Nuvole', '☁️', 'an overcast sky'], ['pioggia', 'Pioggia', '🌧️', 'rain with wet reflections'], ['neve', 'Neve', '❄️', 'fresh falling snow'], ['nebbia', 'Nebbia', '🌫️', 'thick atmospheric fog'], ['temporale', 'Temporale', '⛈️', 'a dramatic thunderstorm'] + ], (f) => `Show the same scene with ${f}. Keep the composition and subjects identical.`, ['coerenza']), + griglia('stagione', 'situazioni', 'Stagione', '🍂', [ + ['primavera', 'Primavera', '🌸', 'spring with blooming flowers'], ['estate', 'Estate', '☀️', 'summer with lush foliage'], ['autunno', 'Autunno', '🍁', 'autumn with orange leaves'], ['inverno', 'Inverno', '❄️', 'winter with bare trees and frost'] + ], (f) => `Change the season to ${f}. Keep the composition and subjects identical.`, ['coerenza']), + griglia('inquadratura', 'prospettiva', 'Inquadratura', '📷', [ + ['dettaglio', 'Dettaglio', '🔎', 'an extreme detail close-up'], ['primo', 'Primo piano', '🙂', 'a close-up portrait'], ['busto', 'Busto', '👤', 'a waist-up portrait'], ['intera', 'Intera', '🧍', 'a full-body shot'], ['largo', 'Campo largo', '🌄', 'a wide establishing shot'] + ], (f) => `Reframe the photograph as ${f}. Keep the same subject, outfit, colors and lighting.`, ['camera']), + griglia('mood', 'luce', 'Atmosfera luce', '💡', [ + ['studio', 'Studio', '💡', 'soft studio lighting'], ['oro', 'Ora d’oro', '🌅', 'warm golden-hour light'], ['controluce', 'Controluce', '🌞', 'strong backlight and glowing rim light'], ['neon', 'Neon', '🌃', 'magenta and cyan neon'], ['drammatica', 'Drammatica', '🎭', 'dramatic low-key lighting'], ['candela', 'Candela', '🕯️', 'warm candlelight'] + ], (f) => `Relight the photo with ${f}. Keep subject, pose and composition identical.`, ['coerenza']), + griglia('grading', 'luce', 'Colore', '🎨', [ + ['pellicola', 'Pellicola', '🎞️', 'a soft film color grade with fine grain'], ['bn', 'B/N', '◐', 'classic black and white tones'], ['vivace', 'Vivace', '🌈', 'rich vibrant but natural colors'], ['cinema', 'Cinema', '🎬', 'a cinematic color grade'], ['colora', 'Colora', '🖌️', 'realistic natural colors instead of black and white'] + ], (f) => `Give the photo ${f}. Keep the composition and all details identical.`, ['coerenza']), + // ---- Persone + { + id: 'eta', categoria: 'persone', tipo: 'cursore', nome: 'Età', icona: '⏳', tooltip: 'Più giovane o più anziano: lo zero lascia com\'è', min: -30, max: 30, neutro: 0, estremi: ['🧒', '🧓'], + compila: (v) => (v === 0 ? null : { lora: ['coerenza'], frase: v > 0 ? `Age the person by about ${v} years: gray hair, wrinkles, same person. ${INVARIATO}` : `Make the person look about ${-v} years younger: smoother skin, fewer wrinkles, same person. ${INVARIATO}` }) + }, + { + id: 'espressione', categoria: 'persone', tipo: 'multi', nome: 'Espressione', icona: '🙂', tooltip: 'Cambia l\'espressione del viso', + parti: [{ id: 'esp', tipo: 'griglia', nome: 'Espressione', opzioni: ESPRESSIONI }], + compila: (v) => (v.esp ? { lora: ['coerenza'], frase: `Change the facial expression of the person to ${frase(ESPRESSIONI, v.esp)}. ${INVARIATO}` } : null) + }, + { + id: 'colore-capelli', categoria: 'persone', tipo: 'multi', nome: 'Capelli', icona: '💇', tooltip: 'Colore dei capelli', + parti: [{ id: 'col', tipo: 'colori', nome: 'Colore', opzioni: COLORI }], + compila: (v) => (v.col ? { lora: ['coerenza'], frase: `Change the hair color to ${frase(COLORI, v.col)}. Keep the hairstyle, face and everything else unchanged.` } : null) + }, + { + id: 'barba', categoria: 'persone', tipo: 'multi', nome: 'Barba', icona: '🧔', tooltip: 'Barba: dalla rasatura alla barba folta', + parti: [{ id: 'barba', tipo: 'griglia', nome: 'Barba', opzioni: BARBE }], + compila: (v) => (v.barba ? { frase: `Give the person ${frase(BARBE, v.barba)}. Keep the face, expression and everything else unchanged.` } : null) + }, + // ---- Vestiti + { + id: 'capo', categoria: 'vestiti', tipo: 'multi', nome: 'Capo su misura', icona: '👗', tooltip: 'Scegli il capo, il colore e il materiale', + parti: [ + { id: 'capo', tipo: 'griglia', nome: 'Capo', opzioni: CAPI }, + { id: 'colore', tipo: 'colori', nome: 'Colore', opzioni: COLORI }, + { id: 'materiale', tipo: 'griglia', nome: 'Materiale', opzioni: MATERIALI } + ], + compila: (v) => { + const capo = frase(CAPI, v.capo) + if (!capo) return null + const stoffa = [frase(COLORI, v.colore), frase(MATERIALI, v.materiale)].filter(Boolean).join(' ') + return { lora: ['vestiti'], frase: `Dress the person in ${capo}${stoffa ? ` in ${stoffa}` : ''}. Keep their face, hair, hands, pose and the background exactly the same.` } + } + }, + // ---- Situazioni + { + id: 'ora', categoria: 'situazioni', tipo: 'cursore', nome: 'Ora del giorno', icona: '🕒', tooltip: 'Dall\'alba alla notte: la scena resta, cambia la luce del giorno', min: 0, max: 6, neutro: 0, estremi: ['🌅', '🌙'], + etichette: ['—', 'Alba', 'Mattino', 'Mezzogiorno', 'Tramonto', 'Ora blu', 'Notte'], + compila: (v) => (ORE[v] ? { lora: ['coerenza'], frase: `Show the exact same scene ${ORE[v]}. Keep the composition and all subjects identical; only the time of day and lighting change.` } : null) + }, + { + id: 'epoca', categoria: 'situazioni', tipo: 'cursore', nome: 'Epoca', icona: '📜', tooltip: 'Porta la scena in un\'altra epoca: dal passato al futuro', min: 0, max: 5, neutro: 0, estremi: ['🏛️', '🛸'], + etichette: ['—', 'Antica Roma', 'Medioevo', 'Anni \'20', 'Anni \'80', 'Futuro'], + compila: (v) => (EPOCHE[v] ? { frase: `Reimagine the photo in ${EPOCHE[v]}. Keep the subject's face and identity recognizable and the composition similar.` } : null) + }, + { + id: 'folla', categoria: 'situazioni', tipo: 'cursore', nome: 'Folla', icona: '👥', tooltip: 'Aggiunge persone sullo sfondo: poche, tante, una folla', min: 0, max: 3, neutro: 0, estremi: ['🧍', '👥'], + etichette: ['—', 'Poche', 'Tante', 'Folla'], + compila: (v) => (v ? { frase: `Add ${['', 'a few passers-by', 'many people', 'a dense crowd'][v]} in the background, naturally integrated with matching lighting, scale and perspective. Keep the main subject and everything else unchanged.` } : null) + }, + // ---- Prospettiva + { + id: 'altezza', categoria: 'prospettiva', tipo: 'cursore', nome: 'Altezza camera', icona: '📐', tooltip: 'Da sotto in su a dall\'alto in giù', min: -2, max: 2, neutro: 0, estremi: ['⬇️', '⬆️'], + compila: (v) => (v === 0 ? null : { lora: ['camera'], frase: `Re-shoot the photo from ${{ '-2': 'a very low angle looking steeply up', '-1': 'a low angle looking up', '1': 'a high angle looking down', '2': 'directly above, a top-down view' }[String(v)]}. Keep the same subject, outfit, colors and lighting.` }) + }, + { + id: 'obiettivo', categoria: 'prospettiva', tipo: 'multi', nome: 'Obiettivo', icona: '🔭', tooltip: 'La lunghezza focale: grandangolo o tele', + parti: [{ id: 'mm', tipo: 'griglia', nome: 'Obiettivo', opzioni: OBIETTIVI }], + compila: (v) => (v.mm ? { frase: `Re-shoot the photo as if taken with a ${v.mm}mm lens: ${Number(v.mm) <= 24 ? 'wide-angle perspective with stretched depth' : Number(v.mm) >= 85 ? 'compressed telephoto perspective with a softly blurred background' : 'natural perspective'}. Keep the same subject, outfit, colors and lighting.` } : null) + }, + // ---- Luce e colore + { + id: 'luce', categoria: 'luce', tipo: 'multi', nome: 'Luce su misura', icona: '💡', tooltip: 'Da dove arriva la luce, com\'è e di che colore', + parti: [ + { id: 'dir', tipo: 'quadrante', nome: 'Direzione', opzioni: DIREZIONI }, + { id: 'qualita', tipo: 'griglia', nome: 'Qualità', opzioni: QUALITA_LUCE }, + { id: 'colore', tipo: 'colori', nome: 'Colore della luce', opzioni: COLORI } + ], + compila: (v) => { + if (!v.dir && !v.qualita && !v.colore) return null + const d = frase(DIREZIONI, v.dir) + const q = frase(QUALITA_LUCE, v.qualita) + const c = frase(COLORI, v.colore) + return { lora: ['coerenza'], frase: `Relight the photo${d ? ` with the main light coming from ${d}` : ''}${q ? `, ${q}` : ''}${c ? `, with a ${c} tint on the light` : ''}. Keep the subject, pose, clothing and composition identical.` } + } + }, + { + id: 'temperatura', categoria: 'luce', tipo: 'cursore', nome: 'Temperatura', icona: '🌡️', tooltip: 'Dal freddo al caldo: la striscia colore della luce', min: -2, max: 2, neutro: 0, estremi: ['🧊', '🔥'], + compila: (v) => (v === 0 ? null : { lora: ['coerenza'], frase: `Shift the color temperature of the photo ${{ '-2': 'to a strongly cold blue tone', '-1': 'to a slightly cold tone', '1': 'to a slightly warm tone', '2': 'to a strongly warm golden-orange tone' }[String(v)]}. Keep everything else identical.` }) + }, + // ---- Testi + { + id: 'lingua', categoria: 'testo', tipo: 'multi', nome: 'Traduci i testi', icona: '🌐', tooltip: 'Traduce le scritte nella foto, con lo stesso stile', + parti: [{ id: 'lingua', tipo: 'griglia', nome: 'Lingua', opzioni: LINGUE }], + compila: (v) => (v.lingua ? { lora: ['coerenza'], frase: `Translate all the text in the photo into ${frase(LINGUE, v.lingua)}, keeping the same fonts, layout, colors and style. Change nothing else.` } : null) + }, + { + id: 'titolo', categoria: 'testo', tipo: 'multi', nome: 'Aggiungi un testo', icona: '🔤', tooltip: 'Scrivi il testo e scegli dove va e come', + parti: [ + { id: 'dove', tipo: 'quadrante', nome: 'Dove', opzioni: POSIZIONI }, + { id: 'come', tipo: 'griglia', nome: 'Come', opzioni: FORMATI_TESTO }, + { id: 'testo', tipo: 'testo', nome: 'Il testo', esempio: 'BUON COMPLEANNO' } + ], + compila: (v) => { + const t = (v.testo || '').trim() + if (!t) return null + return { frase: `Add the text "${t}" ${frase(POSIZIONI, v.dove) || 'in a natural place'}, ${frase(FORMATI_TESTO, v.come) || 'in clean readable letters'}. Spell every letter exactly as written, with good contrast. Keep the rest of the image identical.` } + } + }, + // ---- Ritocco + { + id: 'dettagli', categoria: 'ritocco', tipo: 'cursore', nome: 'Dettagli', icona: '🔎', tooltip: 'Nitidezza e dettagli fini', min: 0, max: 2, neutro: 0, estremi: ['·', '✦'], etichette: ['—', 'Nitida', 'Molto nitida'], + compila: (v) => (v ? { lora: ['coerenza'], frase: `Enhance the image quality: ${v === 1 ? 'sharp fine details' : 'very sharp ultra-fine details'}, clean textures, no noise or compression artifacts. Keep everything identical.` } : null) + }, + { + id: 'pelle', categoria: 'ritocco', tipo: 'cursore', nome: 'Pelle', icona: '✨', tooltip: 'Dalla pelle naturale a quella liscia', min: 0, max: 2, neutro: 0, estremi: ['🧑', '🪞'], etichette: ['—', 'Naturale', 'Liscia'], + compila: (v) => (v ? { lora: ['coerenza'], frase: v === 1 ? 'Retouch the skin naturally: remove blemishes but keep realistic pores and texture. Keep everything else identical.' : 'Retouch the skin to a smooth, polished beauty finish while keeping the person recognizable. Keep everything else identical.' } : null) + }, + { + id: 'sfondo-colore', categoria: 'ritocco', tipo: 'multi', nome: 'Cambia sfondo', icona: '🏞️', tooltip: 'Sfondo pulito: bianco, grigio, sfumato o un colore', + parti: [ + { id: 'tipo', tipo: 'griglia', nome: 'Sfondo', opzioni: SFONDI }, + { id: 'colore', tipo: 'colori', nome: 'Oppure un colore', opzioni: COLORI } + ], + compila: (v) => { + const t = v.colore ? `a plain solid ${frase(COLORI, v.colore)}` : frase(SFONDI, v.tipo) + return t ? { frase: `Replace the background with ${t} background. Keep the subject, its edges and everything in front of the background exactly the same.` } : null + } + } +] + +export const controlliDi = (categoria: string): Controllo[] => CONTROLLI.filter((c) => c.categoria === categoria) +/** Restano le azioni a un clic e i segnaposto foto; le varianti sono nei controlli, non in un secondo catalogo. */ +const AZIONI_DIRETTE = new Set(['persona-rif', 'persona-agg', 'persona-togli', 'occhi', 'occhiali', 'prova', 'svuota', 't-cambia', 't-togli', 'sfondo-via', 'restaura', 'foto-lavoro']) +export const rapidaVisibile = (r: Rapida): boolean => AZIONI_DIRETTE.has(r.id) || r.id.startsWith('z-') +export const prefissoIdea = (id: string): string => `ctl-${id}:` + +/** i valori di una riga multipla stanno in una stringa `parte=valore;parte=valore` (i valori sono codificati) */ +export const scriviMulti = (v: Record): string => + Object.entries(v).filter(([, x]) => x).map(([k, x]) => `${k}=${encodeURIComponent(x)}`).join(';') +export const leggiMulti = (s: string): Record => Object.fromEntries(s.split(';').flatMap((p) => { + const i = p.indexOf('=') + if (i < 1) return [] + try { return [[p.slice(0, i), decodeURIComponent(p.slice(i + 1))]] } catch { return [] } +})) + +/** la «voce» (idea) che un controllo produce con quel valore, o null se spento. Il valore sta nell'id: `ctl-eta:10` */ +export function ideaDa(c: Controllo, valore: string): Rapida | null { + let k: Compilato | null + if (c.tipo === 'cursore') { + const n = Number(valore) + if (!Number.isInteger(n) || n < c.min || n > c.max) return null + k = c.compila(n) + } else { + const valori = leggiMulti(valore) + const validi = Object.fromEntries(c.parti.flatMap((p) => { + const v = valori[p.id] + return v && (p.tipo === 'testo' || p.opzioni.some((o) => o.id === v)) ? [[p.id, v]] : [] + })) + k = c.compila(validi) + } + if (!k || !k.frase.trim()) return null + return { id: prefissoIdea(c.id) + valore, nome: c.nome, prompt: k.frase, lora: k.lora } +} + +/** il valore attuale di un controllo, letto dalle idee attive (il neutro se è spento) */ +export function valoreDa(c: Controllo, idee: Pick[]): string { + const x = idee.find((r) => r.id.startsWith(prefissoIdea(c.id))) + return x ? x.id.slice(prefissoIdea(c.id).length) : c.tipo === 'cursore' ? String(c.neutro) : '' +} + +/** mette (o toglie, se spento) l'idea del controllo nelle idee attive */ +export function conControllo>(idee: T[], c: Controllo, valore: string): (T | Rapida)[] { + const senza = idee.filter((r) => !r.id.startsWith(prefissoIdea(c.id))) + const idea = ideaDa(c, valore) + return idea ? [...senza, idea] : senza +} diff --git a/src/renderer/src/creaPrompt.ts b/src/renderer/src/creaPrompt.ts new file mode 100644 index 0000000..429bcc5 --- /dev/null +++ b/src/renderer/src/creaPrompt.ts @@ -0,0 +1,29 @@ +// Il prompt che parte da Crea: quello scritto (o composto dal genere) + lo stile scelto + lo sfondo. +// Lo stile e lo sfondo non si scrivono nella casella: si aggiungono qui, al momento di mandare, e si vedono come icone sopra il pulsante. +import type { SfondoRichiesto } from '@shared/tipi' +import { fraseStileCrea, stileDa } from './stili' +import type { StatoCrea } from './stato' + +const NOMI_TINTA: [string, string][] = [['#ffffff', 'pure white'], ['#f2f2f2', 'light gray'], ['#000000', 'solid black'], ['#e63946', 'red'], ['#f4a261', 'orange'], ['#ffd54a', 'yellow'], ['#2a9d8f', 'teal'], ['#3fe0ff', 'cyan'], ['#8a5cff', 'violet'], ['#ff3df2', 'magenta']] +export const nomeTinta = (hex: string): string => NOMI_TINTA.find(([h]) => h.toLowerCase() === hex.toLowerCase())?.[1] || `${hex} colored` + +/** quello che si dice del fondo (in inglese) */ +export function fraseSfondo(crea: Pick, qwen: boolean): string { + if (crea.sfondo === 'tinta') return `isolated on a plain flat ${nomeTinta(crea.tinta)} background, no scenery, nothing else in the frame` + // Qwen disegna già il canale alfa (il motore avvolge il prompt); Klein ha bisogno di un fondo pulito da togliere dopo + if (crea.sfondo === 'trasparente' && !qwen) return 'isolated subject on a plain pure white background, no scenery, no shadows on the background, nothing else in the frame' + return '' +} + +export function sfondoRichiesto(crea: Pick): SfondoRichiesto { + return crea.sfondo === 'trasparente' ? 'trasparente' : crea.sfondo === 'tinta' ? { tinta: crea.tinta } : 'scena' +} + +export function promptCrea(crea: Pick, qwen: boolean): string { + const parti = [crea.prompt.trim().replace(/[.,;\s]+$/, '')] + const s = stileDa(crea.stileId) + if (s) parti.push(fraseStileCrea(s, crea.intensita)) + const f = fraseSfondo(crea, qwen) + if (f) parti.push(f) + return parti.filter(Boolean).join(', ') +} diff --git a/src/renderer/src/estrai.ts b/src/renderer/src/estrai.ts index 44adabf..751ff76 100644 --- a/src/renderer/src/estrai.ts +++ b/src/renderer/src/estrai.ts @@ -267,3 +267,20 @@ ascoltatoriLavoro.push((uno) => { }) } else if (uno.stato === 'errore' || uno.stato === 'annullato') attesiEstrai.delete(uno.id) }) + +// --------------------------------------------------------------------------------------------------------------- +// il ritaglio fedele (SAM 2): due clic, i pixel sono quelli della foto +// --------------------------------------------------------------------------------------------------------------- +/** ritaglia la cosa scelta dalla foto: nessuna ricreazione, il PNG ha lo sfondo trasparente (contratto engine §2.3: `modalita: 'segmenta'`) */ +export function richiestaRitaglio(foto: FotoBase, el: Elemento, ritaglia: boolean): Richiesta { + const nome = el.nome ? el.nome.split(/[,.;]/)[0].slice(0, 30) : el.tipo === 'persona' ? 'Persona' : 'Cosa' + return { + ...richiestaBase(), + modalita: 'segmenta', + prompt: '', + immagini: [foto.percorso], + segmenta: el.punto ? { punti: [{ x: el.punto.x, y: el.punto.y, positivo: true }], ritaglia } : { box: el.box, ritaglia }, + quante: 1, + etichetta: `Estrai · ${nome} (ritaglio)` + } +} diff --git a/src/renderer/src/iconeRapide.ts b/src/renderer/src/iconeRapide.ts new file mode 100644 index 0000000..0ed2c65 --- /dev/null +++ b/src/renderer/src/iconeRapide.ts @@ -0,0 +1,30 @@ +// Le azioni rapide di Modifica (rapide.ts) come tessere: ognuna ha un'icona (emoji, in attesa degli asset disegnati) e UNA parola. +// Il nome lungo resta nel tooltip. Niente chip di testo: le tessere sono quadrate, come la griglia delle Idee. +import type { Rapida } from './rapide' + +/** id → [icona, parola] */ +const ICONE: Record = { + 'persona-rif': ['🔁', 'Cambia'], 'persona-agg': ['🧑‍🤝‍🧑', 'Aggiungi'], 'persona-togli': ['🫥', 'Togli'], sorriso: ['😊', 'Sorriso'], occhi: ['👀', 'Occhi'], + giovane: ['🧒', 'Giovane'], anziano: ['🧓', 'Anziano'], capelli: ['💇', 'Capelli'], 'capelli-col': ['🌈', 'Colore'], occhiali: ['🕶️', 'Occhiali'], barba: ['🧔', 'Barba'], animale: ['🐕', 'Animale'], + prova: ['👕', 'Prova'], vestito: ['👗', 'Vestito'], abito: ['🥂', 'Da sera'], elegante: ['🤵', 'Elegante'], casual: ['👖', 'Casual'], invernale: ['🧥', 'Inverno'], costume: ['🎭', 'Costume'], + divisa: ['🎖️', 'Divisa'], armatura: ['🛡️', 'Armatura'], spazio: ['👨‍🚀', 'Astronauta'], + sfondo: ['🏞️', 'Sfondo'], mettilo: ['📍', 'Mettilo in'], notte: ['🌙', 'Notte'], pioggia: ['🌧️', 'Pioggia'], neve: ['❄️', 'Neve'], nebbia: ['🌫️', 'Nebbia'], tramonto: ['🌇', 'Tramonto'], + primavera: ['🌸', 'Primavera'], autunno: ['🍂', 'Autunno'], 'sott-acqua': ['🌊', 'Sott\'acqua'], 'spazio-sit': ['🪐', 'Spazio'], anni80: ['📼', 'Anni 80'], futuro: ['🔮', 'Futuro'], folla: ['👥', 'Folla'], svuota: ['🧹', 'Svuota'], + alto: ['⬆️', 'Dall\'alto'], basso: ['⬇️', 'Dal basso'], 'dall-alto-top': ['🦅', 'Zenitale'], 'primo-piano': ['🔍', 'Primo piano'], largo: ['🏔️', 'Campo largo'], lato: ['↔️', 'Di lato'], dietro: ['🔙', 'Da dietro'], + 'tre-quarti': ['📐', 'Tre quarti'], grandangolo: ['🔭', 'Grandangolo'], tele: ['📷', 'Tele'], + studio: ['💡', 'Studio'], oro: ['🌅', 'Ora d\'oro'], controluce: ['🌄', 'Controluce'], neon: ['🌃', 'Neon'], drammatica: ['🎬', 'Drammatica'], candela: ['🕯️', 'Candela'], 'bn-colora': ['🎨', 'Colora'], bn: ['⚫', 'B/N'], + pellicola: ['🎞️', 'Pellicola'], vivace: ['🌈', 'Vivace'], + anime: ['🌸', 'Anime'], 'pittura-anime': ['🏯', 'Ghibli'], acquerello: ['🖌️', 'Acquerello'], olio: ['🖼️', 'Olio'], cartoon: ['🐭', 'Cartoon'], lego: ['🧱', 'Mattoncini'], creta: ['🏺', 'Plastilina'], + pixel: ['👾', 'Pixel'], matita: ['✏️', 'Matita'], fumetto: ['💬', 'Fumetto'], caricatura: ['🤪', 'Caricatura'], 'foto-real': ['📸', 'Realistica'], action: ['🦸', 'Action figure'], + 't-cambia': ['✍️', 'Cambia'], 't-traduci': ['🌐', 'Traduci'], 't-togli': ['🧽', 'Togli'], 't-aggiungi': ['🔤', 'Aggiungi'], 't-scrivi': ['🪧', 'Scrivi'], 't-cartello': ['🚏', 'Cartello'], 't-meme': ['😂', 'Meme'], + 'sfondo-via': ['✂️', 'Via sfondo'], 'sfondo-bianco': ['⬜', 'Su bianco'], restaura: ['🛠️', 'Restaura'], dettagli: ['🔎', 'Dettagli'], pelle: ['✨', 'Pelle'], 'foto-lavoro': ['💼', 'Foto CV'], + 'z-rimuovi': ['🗑️', 'Rimuovi'], 'z-sostituisci': ['🔄', 'Sostituisci'], 'z-colore': ['🎨', 'Colore'], 'z-aggiungi': ['➕', 'Aggiungi'], 'z-ritocca': ['🪄', 'Ritocca'], 'z-oggetto': ['📦', 'Oggetto'], + 'z-persona': ['🧍', 'Persona'], 'z-vestito': ['👚', 'Vestito'], 'z-sfoca': ['🫧', 'Sfoca'] +} + +/** l'icona e la parola di un'azione rapida (se non c'è nella tabella: la prima parola del nome) */ +export function iconaRapida(r: Rapida, ripiego = '✨'): { icona: string; nome: string } { + const x = ICONE[r.id] + if (x) return { icona: x[0], nome: x[1] } + return { icona: ripiego, nome: r.nome.replace('…', '').split(' ')[0] } +} diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index c4beac9..b12d916 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -1,5 +1,6 @@ import { createRoot } from 'react-dom/client' import './stile.css' +import './orbita.css' import { App } from './App' import { Component, type ReactNode } from 'react' import { usaStato } from './stato' diff --git a/src/renderer/src/misura.ts b/src/renderer/src/misura.ts new file mode 100644 index 0000000..68fd033 --- /dev/null +++ b/src/renderer/src/misura.ts @@ -0,0 +1,69 @@ +// La misura dell'immagine che esce da Crea: UNA funzione sola, usata sia dal pulsante che costruisce la richiesta sia dal HUD +// («la cifra che vedi è quella che esce»). Il modello disegna a `lav` e il motore porta il risultato a `finale`. +import { dimMp } from './componenti/Misura' +import { FORMATI, dimensioniRis } from './util' +import type { StatoCrea } from './stato' + +export type StatoMisura = 'nativa' | 'ingrandita' | 'ridotta' + +export interface MisuraCrea { + /** la foto che esce, in pixel */ + finale: { w: number; h: number } + /** la misura a cui disegna il modello */ + lav: { w: number; h: number } + /** la misura chiesta dal formato e dalla risoluzione, prima del limite del modello (4,2 MP, 2688 px) */ + voluta: { w: number; h: number } + /** nativa = disegna proprio alla misura finale · ingrandita = disegna più piccolo e poi ingrandisce · ridotta = disegna più grande o il limite l'ha abbassata */ + stato: StatoMisura + /** di quanto ingrandisce (>1) o riduce (<1) dal disegno alla misura finale */ + fattore: number + /** la frase per il HUD */ + nota: string + mp: number + rapporto: string +} + +const m16 = (x: number): number => Math.max(256, Math.round(x / 16) * 16) +const mcd = (a: number, b: number): number => (b ? mcd(b, a % b) : a) +/** il rapporto più vicino fra quelli noti, se no w:h ridotto */ +export const rapportoDi = (w: number, h: number): string => { + const noto = FORMATI.find((f) => Math.abs(f.w / f.h - w / h) < 0.012) + if (noto) return noto.id + const d = mcd(Math.round(w), Math.round(h)) + const a = Math.round(w) / d + const b = Math.round(h) / d + return a > 40 || b > 40 ? (w / h).toFixed(2).replace('.', ',') + ':1' : `${a}:${b}` +} + +export function misuraCrea(crea: Pick): MisuraCrea { + const dim = dimensioniRis(crea.formato, crea.ris) + const f = FORMATI.find((x) => x.id === crea.formato) || FORMATI[0] + const ar = f.w / f.h + const corto = crea.ris + const voluta = { w: Math.round(ar >= 1 ? corto * ar : corto), h: Math.round(ar >= 1 ? corto : corto / ar) } + const mis = crea.misura?.tipo === 'custom' ? crea.misura : null + const finale = mis ? { w: mis.w, h: mis.h } : dim + const mpLav = crea.mpLavoro ? Math.min(crea.mpLavoro, 4.19) : 0 + const lav = mpLav + ? dimMp(mpLav, finale.w, finale.h) + : mis + ? { w: m16(Math.sqrt(dim.w * dim.h * (mis.w / mis.h))), h: m16(Math.sqrt((dim.w * dim.h) / (mis.w / mis.h))) } + : dim + const fattore = Math.sqrt((finale.w * finale.h) / (lav.w * lav.h)) + const limitato = !mis && dim.w * dim.h < voluta.w * voluta.h * 0.97 + let stato: StatoMisura = 'nativa' + let nota = `Nativa: il modello disegna proprio a ${finale.w}×${finale.h}` + if (Math.abs(finale.w - lav.w) > 1 || Math.abs(finale.h - lav.h) > 1) { + if (fattore > 1.01) { + stato = 'ingrandita' + nota = `Disegna a ${lav.w}×${lav.h} → ingrandisce ×${fattore.toFixed(2).replace('.', ',')}` + } else { + stato = 'ridotta' + nota = `Disegna a ${lav.w}×${lav.h} → riduce a ${finale.w}×${finale.h}` + } + } else if (limitato) { + stato = 'ridotta' + nota = `Limite del modello: scende a ${finale.w}×${finale.h}` + } + return { finale, lav, voluta, stato, fattore, nota, mp: (finale.w * finale.h) / 1048576, rapporto: rapportoDi(finale.w, finale.h) } +} diff --git a/src/renderer/src/orbita.css b/src/renderer/src/orbita.css new file mode 100644 index 0000000..4de5ff0 --- /dev/null +++ b/src/renderer/src/orbita.css @@ -0,0 +1,312 @@ +/* DaProd Image — GUI v2 «Orbita»: spazio profondo, vetro scuro, tessere con icona. + Sta sopra stile.css (che resta per le pagine non ancora ridisegnate): qui ci sono i token nuovi, il palco con le stelle + e i pezzi nuovi (tessere, HUD misura, strip dei modi, Box, Uncensored, Pila LoRA). Le animazioni usano solo transform e opacity + e si spengono con «Meno movimento» (prefers-reduced-motion). */ +:root { + /* palette: stessi nomi di stile.css, più scuri; i nuovi vengono dalla spec (docs/gui-v2-spec.md §8) */ + --fondo: #06040f; + --fondo2: #0a0716; + --pannello: #110c24; + --pannello2: #1a1338; + --rialzo: #251c4d; + --bordo: #2a2056; + --bordo2: #43397f; + --testo: #f1ecff; + --tenue: #b2a7d8; + --spento: #7a70a3; + --nebulosa: #8a5cff; + --supernova: #ff3df2; + --plasma: #3fe0ff; + --stella: #ffd54a; + --ember: #ff8a3d; + --errore: #ff5470; + --aurora: linear-gradient(135deg, #8a5cff 0%, #ff3df2 55%, #3fe0ff 100%); + --corona: linear-gradient(135deg, #ffd54a, #ff8a3d); + --vetro: rgba(22, 15, 50, 0.66); + --bordo-vetro: rgba(190, 160, 255, 0.18); + --raggio-s: 8px; + --raggio-l: 18px; + --alone-nebulosa: 0 0 0 1px rgba(138, 92, 255, 0.7), 0 0 22px rgba(138, 92, 255, 0.45); + --alone-plasma: 0 0 0 1px rgba(63, 224, 255, 0.7), 0 0 20px rgba(63, 224, 255, 0.35); + --t-rapido: 120ms; + --t-base: 200ms; + --curva: cubic-bezier(0.2, 0.8, 0.2, 1); + --molla: cubic-bezier(0.34, 1.56, 0.64, 1); + --tessera: 84px; +} + +/* ---------- sfondo: nebulosa e stelle (solo CSS, nessun canvas), si vedono nel palco ---------- */ +.palco { + position: relative; + background: + radial-gradient(ellipse at 18% 100%, rgba(138, 92, 255, 0.22) 0%, transparent 55%), + radial-gradient(ellipse at 90% 0%, rgba(255, 61, 242, 0.13) 0%, transparent 50%), + radial-gradient(ellipse at 50% 40%, #150f2c 0%, var(--fondo) 72%); + isolation: isolate; +} +.palco::before { + content: ''; + position: absolute; + inset: -40px; + z-index: -1; + pointer-events: none; + background-image: + radial-gradient(1px 1px at 6% 12%, rgba(255, 255, 255, 0.8), transparent), + radial-gradient(1.4px 1.4px at 14% 64%, rgba(200, 185, 255, 0.8), transparent), + radial-gradient(1px 1px at 22% 33%, rgba(255, 255, 255, 0.55), transparent), + radial-gradient(1.6px 1.6px at 31% 82%, rgba(160, 230, 255, 0.75), transparent), + radial-gradient(1px 1px at 38% 8%, rgba(255, 255, 255, 0.7), transparent), + radial-gradient(1.2px 1.2px at 47% 47%, rgba(255, 200, 250, 0.6), transparent), + radial-gradient(1px 1px at 55% 90%, rgba(255, 255, 255, 0.6), transparent), + radial-gradient(1.5px 1.5px at 63% 22%, rgba(200, 185, 255, 0.8), transparent), + radial-gradient(1px 1px at 71% 71%, rgba(255, 255, 255, 0.55), transparent), + radial-gradient(1.3px 1.3px at 78% 38%, rgba(160, 230, 255, 0.7), transparent), + radial-gradient(1px 1px at 86% 85%, rgba(255, 255, 255, 0.7), transparent), + radial-gradient(1.6px 1.6px at 92% 14%, rgba(255, 200, 250, 0.8), transparent), + radial-gradient(1px 1px at 97% 55%, rgba(255, 255, 255, 0.5), transparent), + radial-gradient(1.2px 1.2px at 3% 90%, rgba(200, 185, 255, 0.6), transparent); + animation: deriva 90s linear infinite alternate; +} +@keyframes deriva { from { transform: translate3d(0, 0, 0); } to { transform: translate3d(-28px, 14px, 0); } } +.palco > * { position: relative; } + +/* ---------- la rotaia e la strip dei modi ---------- */ +.rotaia { background: linear-gradient(180deg, #0b0819, #070512); border-right-color: var(--bordo-vetro); } +.rotaia button.attiva { background: linear-gradient(160deg, rgba(138, 92, 255, 0.28), rgba(255, 61, 242, 0.12)); color: var(--stella); box-shadow: inset 0 0 0 1px rgba(138, 92, 255, 0.55); } +.rotaia button.attiva::before { background: var(--aurora); } + +.studio { position: absolute; inset: 0; display: flex; flex-direction: column; min-height: 0; } +.studio-corpo { position: relative; flex: 1; min-height: 0; } +.strip-modi { display: flex; align-items: center; gap: 8px; padding: 8px 14px; border-bottom: 1px solid var(--bordo-vetro); background: linear-gradient(180deg, #0d0a1d, #090614); flex: none; } +.strip-modi .modo { + position: relative; display: inline-flex; align-items: center; gap: 8px; height: 38px; padding: 0 16px 0 12px; border-radius: 12px; border: 1px solid var(--bordo); background: var(--pannello); + color: var(--tenue); font-weight: 700; letter-spacing: 0.3px; cursor: pointer; transition: transform var(--t-rapido) var(--curva), background var(--t-rapido), color var(--t-rapido); +} +.strip-modi .modo svg { width: 20px; height: 20px; } +.strip-modi .modo:hover { color: var(--testo); background: var(--pannello2); transform: translateY(-1px); } +.strip-modi .modo.su { color: #fff; border-color: transparent; background: linear-gradient(var(--pannello2), var(--pannello2)) padding-box, var(--aurora) border-box; box-shadow: 0 6px 24px -10px rgba(138, 92, 255, 0.8); } +.strip-modi .modo.su svg { color: var(--stella); } +.strip-modi .spazio { flex: 1; } +.strip-modi .pillola-motore { display: inline-flex; align-items: center; gap: 7px; height: 30px; padding: 0 12px; border-radius: 15px; border: 1px solid var(--bordo-vetro); background: var(--vetro); color: var(--tenue); font-size: 13px; font-weight: 700; cursor: pointer; } +.strip-modi .pillola-motore:hover { color: var(--testo); border-color: var(--nebulosa); } +.strip-modi .pillola-motore b { color: var(--stella); font-weight: 700; } + +/* ---------- tessere: icona grande + una parola ---------- */ +.tessere { display: grid; grid-template-columns: repeat(var(--col, 4), minmax(0, 1fr)); gap: 8px; } +.tessere.col2 { --col: 2; } .tessere.col3 { --col: 3; } .tessere.col5 { --col: 5; } .tessere.col6 { --col: 6; } +.tessera-o { + position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; min-height: 66px; padding: 8px 4px 6px; border-radius: 12px; + border: 1px solid var(--bordo); background: var(--pannello2); color: var(--tenue); cursor: pointer; font-size: 12.5px; font-weight: 700; line-height: 1.1; text-align: center; + transition: transform var(--t-rapido) var(--curva), box-shadow var(--t-base), border-color var(--t-rapido), color var(--t-rapido); min-width: 0; +} +.tessera-o .ic { display: grid; place-items: center; width: 30px; height: 30px; font-size: 22px; line-height: 1; transition: transform var(--t-base) var(--molla); } +.tessera-o .ic svg { width: 26px; height: 26px; } +.tessera-o .nome { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.tessera-o:hover { transform: translateY(-2px); color: var(--testo); border-color: var(--nebulosa); box-shadow: 0 8px 22px -12px rgba(138, 92, 255, 0.9); } +.tessera-o:hover .ic { transform: scale(1.1); } +.tessera-o.su { color: #fff; border-color: transparent; background: linear-gradient(#20194a, #20194a) padding-box, var(--aurora) border-box; box-shadow: 0 0 0 1px rgba(138, 92, 255, 0.25), 0 8px 24px -10px rgba(255, 61, 242, 0.7); } +.tessera-o.su .ic { color: var(--stella); } +.tessera-o:disabled, .tessera-o.spenta { opacity: 0.4; cursor: default; transform: none; box-shadow: none; } +.tessera-o.da-scaricare::after { content: ''; position: absolute; top: 6px; right: 6px; width: 8px; height: 8px; border-radius: 50%; background: var(--oro2); box-shadow: 0 0 8px var(--oro2); } +.tessera-o .pallina { position: absolute; top: -5px; right: -5px; min-width: 18px; height: 18px; padding: 0 5px; border-radius: 9px; background: var(--aurora); color: #fff; font-size: 11px; display: grid; place-items: center; box-shadow: 0 0 12px rgba(255, 61, 242, 0.6); } +.tessera-o.larga { flex-direction: row; justify-content: flex-start; gap: 10px; padding: 10px 12px; min-height: 58px; text-align: left; } +.tessera-o.larga .testi { display: flex; flex-direction: column; gap: 2px; min-width: 0; } +.tessera-o.larga small { font-size: 11.5px; font-weight: 500; color: var(--spento); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.tessera-o.larga.su small { color: var(--tenue); } +.tessera-o.anteprima { min-height: 76px; padding: 0; justify-content: flex-end; overflow: hidden; border-color: transparent; } +.tessera-o.anteprima .fondo { position: absolute; inset: 0; opacity: 0.95; } +.tessera-o.anteprima .ic { position: absolute; left: 50%; top: 22px; transform: translate(-50%, -50%); font-size: 26px; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.5); } +.tessera-o.anteprima:hover .ic { transform: translate(-50%, -50%) scale(1.12); } +.tessera-o.anteprima .nome { position: relative; width: 100%; padding: 3px 4px 4px; background: rgba(6, 4, 15, 0.72); color: var(--testo); font-size: 11.5px; } +.tessera-o.anteprima.su { box-shadow: 0 0 0 2px var(--supernova), 0 8px 24px -10px rgba(255, 61, 242, 0.8); background: none; } +.tessera-o.fiamma { border-color: rgba(255, 138, 61, 0.55); } +.fascetta-uc { display: inline-flex; align-items: center; gap: 4px; height: 17px; padding: 0 7px; border-radius: 9px; background: var(--ember); color: #1d0b00; font-size: 9.5px; font-weight: 800; letter-spacing: 0.8px; text-transform: uppercase; vertical-align: 2px; } +.fascetta-uc svg { width: 10px; height: 10px; } + +.scaffale { display: flex; gap: 8px; overflow-x: auto; padding: 2px 2px 8px; scroll-snap-type: x proximity; } +.scaffale .tessera-o { flex: none; width: 76px; scroll-snap-align: start; } +.titolo-scaffale { font-size: 11.5px; letter-spacing: 1.2px; text-transform: uppercase; color: var(--spento); margin: 4px 0 4px; font-weight: 700; } + +.pianeti { display: flex; align-items: flex-end; gap: 10px; } +.pianeti button { display: grid; place-items: center; border-radius: 50%; border: 1px solid var(--bordo2); background: radial-gradient(circle at 32% 30%, #6a5bd6, #241a5a 70%); color: #fff; font-size: 11px; font-weight: 700; cursor: pointer; transition: transform var(--t-base) var(--molla), box-shadow var(--t-base); } +.pianeti button:nth-child(1) { width: 34px; height: 34px; } .pianeti button:nth-child(2) { width: 42px; height: 42px; } .pianeti button:nth-child(3) { width: 50px; height: 50px; } +.pianeti button.su { box-shadow: var(--alone-nebulosa); border-color: transparent; background: radial-gradient(circle at 32% 30%, #ff9bf7, #8a2cff 70%); } +.pianeti button:hover { transform: scale(1.08); } +.pianeti-et { display: flex; gap: 10px; color: var(--spento); font-size: 11.5px; margin-top: 2px; } + +/* ---------- HUD misura: la cifra che vedi è quella che esce ---------- */ +.hud { position: absolute; left: 14px; bottom: 12px; z-index: 6; display: flex; align-items: center; gap: 12px; max-width: calc(100% - 28px); padding: 9px 14px; border-radius: 14px; border: 1px solid var(--bordo-vetro); background: var(--vetro); backdrop-filter: blur(14px) saturate(1.2); cursor: pointer; color: var(--testo); box-shadow: 0 10px 30px -12px #000; transition: border-color var(--t-base), transform var(--t-rapido); } +.hud:hover { border-color: var(--plasma); transform: translateY(-1px); } +.hud .forma { width: 34px; height: 34px; display: grid; place-items: center; flex: none; } +.hud .forma i { display: block; border: 2px solid var(--plasma); border-radius: 3px; background: rgba(63, 224, 255, 0.1); transition: width 240ms var(--molla), height 240ms var(--molla); } +.hud .px { font-family: var(--titolo); font-weight: 700; font-size: 19px; letter-spacing: 0.5px; font-variant-numeric: tabular-nums; white-space: nowrap; } +.hud .px small { font-family: var(--font); font-size: 12px; font-weight: 600; color: var(--tenue); margin-left: 3px; letter-spacing: 0; } +.hud .meta { display: flex; flex-direction: column; gap: 1px; min-width: 0; } +.hud .linea1 { display: flex; align-items: baseline; gap: 12px; white-space: nowrap; } +.hud .rapporto, .hud .mp { color: var(--tenue); font-size: 14px; font-weight: 700; font-variant-numeric: tabular-nums; } +.hud .conv { font-size: 12.5px; font-weight: 600; display: flex; align-items: center; gap: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.hud .conv::before { content: ''; width: 7px; height: 7px; border-radius: 50%; flex: none; background: currentColor; box-shadow: 0 0 8px currentColor; } +.hud.nativa .conv { color: var(--verde); } +.hud.ingrandita .conv { color: var(--plasma); } +.hud.ridotta .conv { color: var(--oro2); } +.hud.corto { position: static; padding: 4px 10px; gap: 8px; box-shadow: none; background: rgba(63, 224, 255, 0.07); } +.hud.corto .px { font-size: 14px; } +.hud-pop { position: absolute; left: 14px; bottom: 76px; z-index: 30; width: 400px; max-height: calc(100% - 110px); overflow-y: auto; padding: 14px; border-radius: 16px; border: 1px solid var(--bordo-vetro); background: rgba(14, 10, 32, 0.94); backdrop-filter: blur(16px); box-shadow: var(--ombra, 0 24px 60px -10px #000); display: flex; flex-direction: column; gap: 12px; } + +/* ---------- colonna del modo + bottone principale ---------- */ +.colonna.destra { background: linear-gradient(180deg, #0b0819, #080612); border-left: 1px solid var(--bordo-vetro); width: 400px; } +/* Studio: i comandi (Motore, Cosa, prompt, CREA) stanno a SINISTRA del palco, per tutti i modi (Crea, Modifica, Box, Ricette). + La classe resta «destra» perché i test la cercano così. Il pulsante CREA è fuori dallo scorrimento: resta in fondo. */ +.studio-corpo .pagina .colonna.destra { order: -1; border-left: 0; border-right: 1px solid var(--bordo-vetro); } +.colonna .sezione { border-bottom-color: rgba(190, 160, 255, 0.08); } +.btn.primario.aurora { background: var(--aurora); color: #fff; border-color: transparent; text-shadow: 0 1px 6px rgba(0, 0, 0, 0.35); box-shadow: 0 10px 30px -10px rgba(255, 61, 242, 0.7); position: relative; } +.btn.primario.aurora:hover { filter: brightness(1.1); } +.btn.primario.aurora:active { transform: scale(0.98); } +.azione-prima { display: flex; flex-direction: column; gap: 6px; } +.azione-prima .riga-info { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 28px; } + +.scelte-attive { display: flex; flex-wrap: wrap; gap: 6px; } +.scelte-attive button { position: relative; width: 32px; height: 32px; border-radius: 50%; border: 1px solid var(--bordo2); background: var(--pannello2); font-size: 16px; display: grid; place-items: center; cursor: pointer; color: var(--stella); } +.scelte-attive button svg { width: 17px; height: 17px; } +.scelte-attive button:hover::after { content: '✕'; position: absolute; inset: 0; border-radius: 50%; background: rgba(255, 84, 112, 0.9); color: #fff; display: grid; place-items: center; font-size: 13px; } + +.sezione h3 .dx.oro { color: var(--stella); } +.riga-s { display: flex; gap: 6px; flex-wrap: wrap; } +.pulsante-s { min-width: 54px; flex: 1; padding: 7px 4px 5px; border-radius: 11px; border: 1px solid var(--bordo); background: var(--pannello2); color: var(--tenue); cursor: pointer; display: flex; flex-direction: column; align-items: center; gap: 1px; font-weight: 800; font-size: 15px; transition: transform var(--t-rapido) var(--curva), border-color var(--t-rapido); } +.pulsante-s small { font-size: 10.5px; font-weight: 600; color: var(--spento); font-variant-numeric: tabular-nums; } +.pulsante-s:hover { color: var(--testo); border-color: var(--nebulosa); transform: translateY(-1px); } +.pulsante-s.su { color: #fff; border-color: transparent; background: linear-gradient(#20194a, #20194a) padding-box, var(--aurora) border-box; } +.pulsante-s.su small { color: var(--tenue); } +.formati .formato { border-radius: 11px; background: var(--pannello2); } +.formati .formato.su { border-color: transparent; background: linear-gradient(#20194a, #20194a) padding-box, var(--aurora) border-box; color: var(--stella); } + +/* tinta unita e anteprima dello sfondo */ +.campioni { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; } +.campioni button { width: 24px; height: 24px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; padding: 0; } +.campioni button.su { border-color: var(--stella); box-shadow: 0 0 0 2px #000 inset; } +.scacchi-btn { background-image: linear-gradient(45deg, #888 25%, transparent 25%), linear-gradient(-45deg, #888 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #888 75%), linear-gradient(-45deg, transparent 75%, #888 75%); background-size: 8px 8px; background-position: 0 0, 0 4px, 4px -4px, -4px 0; background-color: #ccc; } +.vista.sfondo-bianco .scacchi { background: #fff; } +.vista.sfondo-nero .scacchi { background: #000; } +.vista.sfondo-colore .scacchi { background: var(--tinta-vista, #8a5cff); } +.vista.sfondo-bianco .scacchi, .vista.sfondo-nero .scacchi, .vista.sfondo-colore .scacchi { background-image: none; } + +/* ---------- Box: editor regionale ---------- */ +.box-tela { position: absolute; inset: 0; display: grid; place-items: center; padding: 18px 18px 78px; } +.box-cornice { position: relative; border-radius: 10px; background: rgba(12, 8, 28, 0.78); border: 1px solid var(--bordo-vetro); box-shadow: 0 24px 70px -20px #000, 0 0 0 1px rgba(138, 92, 255, 0.15); overflow: hidden; touch-action: none; cursor: crosshair; user-select: none; } +.box-cornice::before { content: ''; position: absolute; inset: 0; pointer-events: none; background-image: linear-gradient(rgba(190, 160, 255, 0.07) 1px, transparent 1px), linear-gradient(90deg, rgba(190, 160, 255, 0.07) 1px, transparent 1px); background-size: 33.333% 33.333%; } +.box-regione { position: absolute; border: 2px solid var(--c); background: color-mix(in srgb, var(--c) 16%, transparent); border-radius: 4px; cursor: move; box-sizing: border-box; animation: nasce 180ms var(--molla); } +.box-regione.sel { border-style: dashed; box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.5), 0 0 22px color-mix(in srgb, var(--c) 55%, transparent); background: color-mix(in srgb, var(--c) 24%, transparent); } +.box-regione.nascosta { opacity: 0.25; } +.box-regione .numero { position: absolute; left: -2px; top: -2px; min-width: 22px; height: 22px; padding: 0 6px; border-radius: 4px 0 8px 0; background: var(--c); color: #0a0716; font-weight: 800; font-size: 13px; display: grid; place-items: center; pointer-events: none; } +.box-regione .testo-box { position: absolute; left: 4px; right: 4px; top: 26px; bottom: 4px; overflow: hidden; color: #fff; font-size: 13px; font-weight: 600; line-height: 1.2; text-shadow: 0 1px 3px rgba(0, 0, 0, 0.9); pointer-events: none; } +.box-regione .maniglia { position: absolute; width: 11px; height: 11px; background: #fff; border: 2px solid var(--c); border-radius: 3px; } +.box-regione .maniglia.nw { left: -6px; top: -6px; cursor: nwse-resize; } .box-regione .maniglia.ne { right: -6px; top: -6px; cursor: nesw-resize; } +.box-regione .maniglia.sw { left: -6px; bottom: -6px; cursor: nesw-resize; } .box-regione .maniglia.se { right: -6px; bottom: -6px; cursor: nwse-resize; } +.box-regione .maniglia.n { left: calc(50% - 5px); top: -6px; cursor: ns-resize; } .box-regione .maniglia.s { left: calc(50% - 5px); bottom: -6px; cursor: ns-resize; } +.box-regione .maniglia.w { left: -6px; top: calc(50% - 5px); cursor: ew-resize; } .box-regione .maniglia.e { right: -6px; top: calc(50% - 5px); cursor: ew-resize; } +.box-regione.disegno { border-style: dotted; pointer-events: none; } +@keyframes nasce { from { transform: scale(0.92); opacity: 0.4; } } +.box-vuoto { position: absolute; inset: 0; display: grid; place-items: center; text-align: center; color: var(--spento); pointer-events: none; padding: 20px; font-size: 14px; } +.box-vuoto b { color: var(--tenue); display: block; font-family: var(--titolo); letter-spacing: 1px; margin-bottom: 4px; } +.box-lista { display: flex; flex-direction: column; gap: 8px; } +.box-scheda { border: 1px solid var(--bordo); border-left: 4px solid var(--c); border-radius: 10px; background: var(--pannello2); padding: 8px 10px; display: flex; flex-direction: column; gap: 8px; } +.box-scheda.sel { border-color: var(--c); box-shadow: 0 0 20px -8px var(--c); } +.box-scheda .testa { display: flex; align-items: center; gap: 8px; cursor: pointer; } +.box-scheda .testa .n { width: 22px; height: 22px; border-radius: 6px; background: var(--c); color: #0a0716; font-weight: 800; display: grid; place-items: center; font-size: 13px; flex: none; } +.box-scheda .testa .riassunto { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13.5px; color: var(--tenue); } +.box-scheda .testa button { width: 26px; height: 26px; border: 0; background: transparent; color: var(--tenue); cursor: pointer; border-radius: 6px; display: grid; place-items: center; } +.box-scheda .testa button:hover { background: var(--rialzo); color: var(--testo); } +.box-scheda .testa button svg { width: 16px; height: 16px; } +.disposizioni { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; } +.disposizioni button { aspect-ratio: 1.6; border-radius: 8px; border: 1px solid var(--bordo); background: var(--pannello2); padding: 4px; cursor: pointer; position: relative; } +.disposizioni button:hover { border-color: var(--nebulosa); } +.disposizioni button i { position: absolute; border: 1.5px solid var(--tenue); border-radius: 2px; background: rgba(138, 92, 255, 0.25); } +.rigidita { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; } + +/* ---------- Estrai: tre uscite ---------- */ +.uscite-estrai { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; } + +/* ---------- Impostazioni: navigazione a sinistra + Laboratorio Uncensored ---------- */ +.opz-nav { width: 168px; flex: none; padding: 16px 10px; display: flex; flex-direction: column; gap: 6px; border-right: 1px solid var(--bordo-vetro); background: linear-gradient(180deg, #0b0819, #080612); overflow-y: auto; } +.opz-nav .tessera-o { flex-direction: row; justify-content: flex-start; gap: 10px; min-height: 42px; padding: 6px 10px; font-size: 13.5px; } +.opz-nav .tessera-o .ic { width: 22px; height: 22px; font-size: 18px; } .opz-nav .tessera-o .ic svg { width: 20px; height: 20px; } +.uc-lab { border: 1px solid rgba(255, 138, 61, 0.45); border-radius: 14px; background: linear-gradient(180deg, rgba(255, 138, 61, 0.1), rgba(255, 138, 61, 0.02)); padding: 16px 18px; } +.uc-lab h3 { margin: 0 0 6px; font-family: var(--titolo); font-size: 15px; letter-spacing: 1.2px; color: var(--ember); display: flex; align-items: center; gap: 8px; } +.uc-riga { display: grid; grid-template-columns: 96px 1fr 120px 78px 120px; gap: 10px; align-items: center; padding: 9px 4px; border-bottom: 1px solid rgba(255, 255, 255, 0.06); font-size: 13.5px; } +.uc-riga.testa { color: var(--spento); font-size: 11.5px; letter-spacing: 1px; text-transform: uppercase; font-weight: 700; } +.uc-riga .nome { font-weight: 700; min-width: 0; } +.uc-riga .nome small { display: block; font-weight: 500; color: var(--spento); font-size: 12px; } +.disclaimer { position: fixed; inset: 0; z-index: 120; display: grid; place-items: center; background: rgba(4, 3, 12, 0.82); backdrop-filter: blur(6px); } +.disclaimer .scheda-d { width: min(640px, calc(100vw - 40px)); max-height: calc(100vh - 60px); overflow-y: auto; padding: 22px 24px; border-radius: 18px; border: 1px solid rgba(255, 138, 61, 0.6); background: #120c24; box-shadow: 0 30px 90px -20px #000, 0 0 40px -10px rgba(255, 138, 61, 0.5); } +.disclaimer h2 { margin: 0 0 10px; font-family: var(--titolo); color: var(--ember); letter-spacing: 1.2px; font-size: 17px; display: flex; align-items: center; gap: 10px; } +.disclaimer p { margin: 0 0 12px; color: var(--tenue); font-size: 14.5px; line-height: 1.45; } +.disclaimer label.spunta { display: flex; gap: 10px; align-items: flex-start; padding: 8px 10px; border-radius: 10px; background: #0f0b1c; border: 1px solid var(--bordo); margin-bottom: 8px; cursor: pointer; font-size: 14.5px; } +.disclaimer label.spunta input { margin-top: 3px; accent-color: var(--ember); width: 17px; height: 17px; } +.disclaimer .scuoti { animation: scuoti 320ms; } +@keyframes scuoti { 20% { transform: translateX(-6px); } 40% { transform: translateX(6px); } 60% { transform: translateX(-4px); } 80% { transform: translateX(4px); } } + +/* ---------- Pila LoRA ---------- */ +.pila { display: flex; flex-direction: column; gap: 8px; } +.slot { display: grid; grid-template-columns: 22px 38px minmax(0, 1fr); gap: 8px 10px; align-items: center; padding: 8px 10px; border-radius: 12px; border: 1px solid var(--bordo); background: var(--pannello2); transition: transform var(--t-base) var(--curva), opacity var(--t-base); } +.slot.spenta { opacity: 0.5; } +.slot.incompatibile { border-style: dashed; opacity: 0.6; } +.slot.trascinato { transform: scale(1.02); box-shadow: var(--alone-nebulosa); z-index: 2; } +.slot .maniglia-s { cursor: grab; color: var(--spento); text-align: center; font-size: 15px; user-select: none; } +.slot .mini-c { width: 38px; height: 38px; border-radius: 8px; overflow: hidden; display: grid; place-items: center; font-family: var(--titolo); font-weight: 900; font-size: 12px; } +.slot .mini-c img { width: 100%; height: 100%; object-fit: cover; } +.slot .nome-s { min-width: 0; display: flex; align-items: center; gap: 8px; font-weight: 700; } +.slot .nome-s span.n { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; } +.slot .corpo-s { grid-column: 2 / -1; display: flex; align-items: center; gap: 8px; } +.slot .corpo-s input[type='range'] { flex: 1; } +.slot .corpo-s output { width: 40px; text-align: right; color: var(--stella); font-weight: 800; font-variant-numeric: tabular-nums; } +.slot .parole-s { grid-column: 2 / -1; display: flex; gap: 6px; flex-wrap: wrap; align-items: center; font-size: 12px; color: var(--spento); } +.slot .motivo { grid-column: 2 / -1; font-size: 12.5px; color: var(--oro2); } +.slot button.mini { width: 28px; height: 28px; border-radius: 8px; border: 1px solid var(--bordo); background: transparent; color: var(--tenue); cursor: pointer; display: grid; place-items: center; font-size: 12px; font-weight: 800; } +.slot button.mini:hover { color: var(--testo); border-color: var(--nebulosa); } +.slot button.mini.su { background: var(--nebulosa); color: #fff; border-color: transparent; } +.slot button.mini svg { width: 16px; height: 16px; } +.cassetto { position: absolute; top: 0; right: 0; bottom: 0; width: 420px; z-index: 40; padding: 16px; display: flex; flex-direction: column; gap: 12px; background: rgba(12, 8, 28, 0.96); backdrop-filter: blur(14px); border-left: 1px solid var(--bordo-vetro); box-shadow: -24px 0 60px -20px #000; overflow-y: auto; animation: scivola-dx 220ms var(--curva); } +@keyframes scivola-dx { from { transform: translateX(24px); opacity: 0; } } +.cassetto h2 { margin: 0; font-family: var(--titolo); font-size: 14px; letter-spacing: 1.5px; color: var(--stella); display: flex; align-items: center; gap: 8px; } +.badge-arch { display: inline-flex; align-items: center; height: 18px; padding: 0 7px; border-radius: 6px; font-size: 10.5px; font-weight: 800; letter-spacing: 0.4px; white-space: nowrap; } +.badge-arch.si { background: rgba(62, 224, 143, 0.14); color: var(--verde); } +.badge-arch.no { background: rgba(255, 84, 112, 0.14); color: var(--rosso); } +.badge-arch.forse { background: rgba(178, 167, 216, 0.14); color: var(--tenue); } +.badge-arch.presto { background: rgba(255, 182, 39, 0.14); color: var(--oro2); } +.filtri-lora { display: flex; gap: 6px; flex-wrap: wrap; } +.filtri-lora .tessera-o { flex-direction: row; min-height: 30px; padding: 4px 10px; gap: 6px; font-size: 12.5px; flex: none; } + +/* ---------- movimento ridotto ---------- */ +@media (prefers-reduced-motion: reduce) { + .palco::before, .box-regione, .cassetto, .disclaimer .scuoti { animation: none !important; } + .tessera-o, .tessera-o .ic, .pulsante-s, .hud, .hud .forma i, .strip-modi .modo, .slot, .pianeti button { transition: none !important; transform: none !important; } +} +body.meno-movimento .palco::before { animation: none; } + +/* ---------- Modifica: i modi (Scrivi · Espandi · Estrai) in cima alla tela ---------- */ +.modi-modifica { display: flex; gap: 6px; } +.modi-modifica .tessera-o { flex-direction: row; min-height: 36px; padding: 0 14px 0 10px; gap: 8px; font-size: 14px; } +.modi-modifica .tessera-o .ic { width: 22px; height: 22px; } .modi-modifica .tessera-o .ic svg { width: 20px; height: 20px; } + +/* ---------- Modifica: i modi (Scrivi · Espandi · Estrai) in cima alla tela ---------- */ +.modi-modifica { display: flex; gap: 6px; } +.modi-modifica .tessera-o { flex-direction: row; min-height: 36px; padding: 0 14px 0 10px; gap: 8px; font-size: 14px; } +.modi-modifica .tessera-o .ic { width: 22px; height: 22px; } .modi-modifica .tessera-o .ic svg { width: 20px; height: 20px; } + +/* ---------- Modifica: i controlli interattivi per categoria (cursori, campioni colore, quadrante) ---------- */ +.controlli-cat { display: flex; flex-direction: column; gap: 14px; margin-top: 12px; } +.ctl { display: flex; flex-direction: column; gap: 6px; padding: 10px; border-radius: 12px; background: rgba(255, 255, 255, 0.03); border: 1px solid var(--bordo-vetro, rgba(255, 255, 255, 0.08)); } +.ctl-testa { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--stella); } +.ctl-cursore { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 8px; } +.ctl-cursore input[type=range] { width: 100%; accent-color: var(--oro, #ffd54a); } +.ctl-cursore .estremo { font-size: 16px; line-height: 1; } +.ctl-cursore .valore { grid-column: 1 / -1; text-align: center; font-size: 12px; } +.campioni { display: flex; flex-wrap: wrap; gap: 6px; } +.campione { width: 26px; height: 26px; border-radius: 50%; border: 2px solid rgba(255, 255, 255, 0.25); cursor: pointer; padding: 0; } +.campione:hover { transform: scale(1.12); } +.campione.su { border-color: #fff; box-shadow: 0 0 0 2px var(--nebulosa, #8b5cf6), 0 0 12px rgba(255, 255, 255, 0.4); } +.quadrante { display: grid; grid-template-columns: repeat(3, 38px); grid-auto-rows: 38px; gap: 4px; align-self: flex-start; } +.quadrante .cella { display: grid; place-items: center; border-radius: 9px; border: 1px solid rgba(255, 255, 255, 0.12); background: rgba(255, 255, 255, 0.04); color: var(--stella); font-size: 15px; cursor: pointer; padding: 0; } +.quadrante .cella.su { background: var(--nebulosa, #8b5cf6); color: #fff; border-color: transparent; } +.quadrante .cella.soggetto { cursor: default; opacity: 0.4; border-style: dashed; } +@media (prefers-reduced-motion: reduce) { .campione:hover { transform: none; } } diff --git a/src/renderer/src/pagine/Box.tsx b/src/renderer/src/pagine/Box.tsx new file mode 100644 index 0000000..5aa7778 --- /dev/null +++ b/src/renderer/src/pagine/Box.tsx @@ -0,0 +1,392 @@ +// Box: l'editor regionale. Disegni dei riquadri sulla tela (stesso formato e stessa misura di Crea), ognuno con il suo testo, +// il suo tipo e il suo testo. Il motore riceve `box` + `promptScena` secondo docs/contratto-engine-gui.md. +// Le regioni sono guide morbide: niente controlli di blocco o riferimenti che il motore ignorerebbe. +import { useEffect, useMemo, useRef, useState, type JSX, type PointerEvent as PE } from 'react' +import { usaStato } from '../stato' +import { api, urlFile } from '../api' +import { I } from '../componenti/Icone' +import { Tessera, Tessere, E } from '../componenti/Tessere' +import { AdattaFoto } from '../componenti/AdattaFoto' +import { Casella } from '../componenti/Casella' +import { SezioneChiudibile } from '../componenti/Controlli' +import { SelettoreModello } from '../componenti/ScegliModello' +import { HudCrea, SezFormato, SezMisura, SezQuante, SezSfondo } from '../componenti/SezioniCrea' +import { IntensitaStile, ScaffaliStili } from '../componenti/ScaffaliStili' +import { Hud } from '../componenti/Hud' +import { InCorso } from './Crea' +import { conParoleLora } from '../componenti/SceltaLora' +import { percentuale } from '../componenti/Coda' +import { VistaZoom } from '../componenti/VistaZoom' +import { MODELLI, modelloDaImpostazioni, parametriModello } from '@shared/modelli' +import { durata, stimaSecondi } from '../util' +import { misuraCrea } from '../misura' +import { promptCrea, sfondoRichiesto } from '../creaPrompt' +import { stileDa } from '../stili' +import { richiestaBase } from '../azioni' +import { DISPOSIZIONI, MAX_BOX, TIPI_BOX, coloreBox, fraseBox, nuovaRegione, promptDaBox, rettDaPunti, trasforma, type Maniglia, type Rect } from '../box' +import type { BoxRegione, Opera } from '@shared/tipi' + +const MANIGLIE: Maniglia[] = ['nw', 'n', 'ne', 'e', 'se', 's', 'sw', 'w'] +const ICONA_TIPO: Record JSX.Element> = { persona: I.ritratto, oggetto: I.prodotto, testo: I.testo, sfondo: I.scena } + +/** quel che succede con il mouse: disegnare un riquadro nuovo, spostarne uno, o tirarne una maniglia */ +type Gesto = { tipo: 'disegna'; x: number; y: number } | { tipo: 'muovi'; id: string; m: Maniglia; x: number; y: number; rect: Rect } + +export function Box(): JSX.Element { + const { crea, setCrea, box, setBox, imp, lavori, avvisa } = usaStato() + const cornice = useRef(null) + const gesto = useRef(null) + const [bozza, setBozza] = useState(null) + const [opere, setOpere] = useState>({}) + const [scelto, setScelto] = useState(null) + const [vedoInCorso, setVedoInCorso] = useState(true) + const [vistaRis, setVistaRis] = useState(false) + + const mis = misuraCrea(crea) + const { finale, lav } = mis + const modello = MODELLI[modelloDaImpostazioni(imp)] + const quante = Math.max(1, Math.min(8, crea.quante)) + const stima = stimaSecondi(imp?.tempi, (lav.w * lav.h) / 1048576, modello.passi) * quante + const stileAttivo = stileDa(crea.stileId) + const { regioni, selezionata, scena } = box + const sel = regioni.find((r) => r.id === selezionata) || null + + // i lavori partiti da qui (hanno `box`): il risultato si guarda sulla tela al posto dei riquadri + const miei = useMemo(() => lavori.filter((l) => l.richiesta.modalita === 'crea' && !!l.richiesta.box), [lavori]) + const inCorso = miei.find((l) => l.stato === 'in corso') + useEffect(() => { + if (inCorso) { + setVedoInCorso(true) + setVistaRis(false) + } + }, [inCorso?.id]) + const risultati = useMemo(() => miei.flatMap((l) => l.risultati).reverse(), [miei]) + useEffect(() => { + const mancano = risultati.filter((id) => !opere[id]) + if (!mancano.length) return + void Promise.all(mancano.map((id) => api.galleria.opera(id))).then((l) => { + const n = { ...opere } + l.forEach((o) => o && (n[o.id] = o)) + setOpere(n) + }) + }, [risultati, opere]) + useEffect(() => { + if (risultati[0]) { + setScelto(risultati[0]) + setVistaRis(true) + setVedoInCorso(false) + } + }, [risultati[0]]) + const op = scelto ? opere[scelto] : undefined + const mostraInCorso = !!inCorso && vedoInCorso + + const cambia = (id: string, m: Partial): void => setBox({ regioni: usaStato.getState().box.regioni.map((r) => (r.id === id ? { ...r, ...m } : r)) }) + const seleziona = (id: string | null): void => setBox({ selezionata: id }) + const togli = (id: string): void => { + const s = usaStato.getState().box + setBox({ regioni: s.regioni.filter((x) => x.id !== id), selezionata: s.selezionata === id ? null : s.selezionata }) + } + const duplica = (id: string): void => { + const s = usaStato.getState().box + const o = s.regioni.find((r) => r.id === id) + if (!o || s.regioni.length >= MAX_BOX) return + const n = { ...nuovaRegione(trasforma(o.rect, 'corpo', 0.04, 0.04), s.regioni, o.tipo), prompt: o.prompt, rigidita: o.rigidita } + setBox({ regioni: [...s.regioni, n], selezionata: n.id }) + } + + // ---- il mouse sulla tela ---- + const punto = (e: PE): { x: number; y: number } => { + const r = cornice.current!.getBoundingClientRect() + return { x: Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)), y: Math.max(0, Math.min(1, (e.clientY - r.top) / r.height)) } + } + const giuTela = (e: PE): void => { + if (e.button !== 0 || vistaRis) return + if (usaStato.getState().box.regioni.length >= MAX_BOX) { + avvisa(`Al massimo ${MAX_BOX} riquadri`, 'errore') + return + } + const p = punto(e) + gesto.current = { tipo: 'disegna', ...p } + setBozza([p.x, p.y, p.x, p.y]) + e.currentTarget.setPointerCapture(e.pointerId) + seleziona(null) + } + const giuRegione = (e: PE, r: BoxRegione, m: Maniglia): void => { + if (e.button !== 0) return + e.stopPropagation() + const p = punto(e) + gesto.current = { tipo: 'muovi', id: r.id, m, x: p.x, y: p.y, rect: r.rect } + cornice.current?.setPointerCapture(e.pointerId) + seleziona(r.id) + } + const muoviTela = (e: PE): void => { + const g = gesto.current + if (!g) return + const p = punto(e) + if (g.tipo === 'disegna') setBozza(rettDaPunti(g.x, g.y, p.x, p.y)) + else cambia(g.id, { rect: trasforma(g.rect, g.m, p.x - g.x, p.y - g.y) }) + } + const suTela = (e: PE): void => { + const g = gesto.current + gesto.current = null + if (!g) return + try { + cornice.current?.releasePointerCapture(e.pointerId) + } catch { + /* già rilasciato */ + } + if (g.tipo !== 'disegna') return + const p = punto(e) + const r = rettDaPunti(g.x, g.y, p.x, p.y) + setBozza(null) + // un clic senza trascinare non fa un riquadro + if (r[2] - r[0] < 0.03 || r[3] - r[1] < 0.03) return + const s = usaStato.getState().box + const n = nuovaRegione(r, s.regioni, r[2] - r[0] > 0.9 && r[3] - r[1] > 0.9 ? 'sfondo' : 'oggetto') + setBox({ regioni: [...s.regioni, n], selezionata: n.id }) + } + + // ---- tastiera: Canc elimina, Ctrl+D duplica, frecce muovono (Maiusc = 10×), Tab passa al successivo, Ctrl+Invio lancia ---- + useEffect(() => { + const t = (e: KeyboardEvent): void => { + if (usaStato.getState().pagina !== 'box') return + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { + e.preventDefault() + void componi() + return + } + if (e.target instanceof HTMLTextAreaElement || e.target instanceof HTMLInputElement) return + const s = usaStato.getState().box + const id = s.selezionata + if (!id) return + if (e.key === 'Delete' || e.key === 'Backspace') togli(id) + else if (e.ctrlKey && e.key.toLowerCase() === 'd') { + e.preventDefault() + duplica(id) + } else if (e.key.startsWith('Arrow')) { + e.preventDefault() + const passo = e.shiftKey ? 0.02 : 0.002 + const r = s.regioni.find((x) => x.id === id) + if (r) cambia(id, { rect: trasforma(r.rect, 'corpo', e.key === 'ArrowLeft' ? -passo : e.key === 'ArrowRight' ? passo : 0, e.key === 'ArrowUp' ? -passo : e.key === 'ArrowDown' ? passo : 0) }) + } else if (e.key === 'Tab' && s.regioni.length) { + e.preventDefault() + const i = s.regioni.findIndex((x) => x.id === id) + seleziona(s.regioni[(i + (e.shiftKey ? s.regioni.length - 1 : 1)) % s.regioni.length].id) + } else if (e.key === 'Escape') seleziona(null) + } + window.addEventListener('keydown', t) + return () => window.removeEventListener('keydown', t) + }) + + // ---- le disposizioni pronte: i riquadri nuovi tengono i testi di quelli che c'erano ---- + const applicaDisposizione = (id: string): void => { + const d = DISPOSIZIONI.find((x) => x.id === id) + if (!d) return + const vecchie = usaStato.getState().box.regioni + const nuove: BoxRegione[] = d.box.map((b, i) => ({ ...nuovaRegione(b.rect, [], b.tipo), ordine: i + 1, prompt: vecchie[i]?.prompt ?? '', rif: vecchie[i]?.rif, rigidita: vecchie[i]?.rigidita ?? 'decisa' })) + setBox({ regioni: nuove, selezionata: nuove[0]?.id ?? null }) + } + + + // ---- il lancio ---- + async function componi(): Promise { + const s = usaStato.getState() + const nascosti = new Set(s.box.regioni.filter((r) => r.nascosta).map((r) => r.id)) + const attive = s.box.regioni.filter((r) => !nascosti.has(r.id)) + if (!attive.length && !s.box.scena.trim()) { + avvisa('Disegna almeno un riquadro (o scrivi la scena)', 'errore') + return + } + if (attive.some((r) => r.tipo !== 'sfondo' && !r.prompt.trim())) { + avvisa('Ogni riquadro ha bisogno di un testo', 'errore') + return + } + const m = misuraCrea(s.crea) + const pronti = promptDaBox(s.box.scena, s.box.regioni.map((r) => ({ ...r, rif: undefined })), nascosti) + const q = { + ...richiestaBase(), + modalita: 'crea' as const, + prompt: conParoleLora(promptCrea({ ...s.crea, prompt: pronti.prompt }, (s.imp?.famiglia || 'qwen') === 'qwen')), + // il disegno vale più di una didascalia: niente espansione del prompt + potenzia: false, + negativo: '', + seed: s.crea.casuale ? -1 : s.crea.seed, + cfg: 1, + sampler: 'euler', + scheduler: 'simple', + larghezza: m.lav.w, + altezza: m.lav.h, + megapixel: (m.lav.w * m.lav.h) / 1048576, + ...(Math.abs(m.finale.w - m.lav.w) > 1 || Math.abs(m.finale.h - m.lav.h) > 1 ? { uscita: { w: m.finale.w, h: m.finale.h } } : {}), + quante: Math.max(1, Math.min(8, s.crea.quante)), + immagini: pronti.immagini, + trasparente: s.crea.sfondo === 'trasparente', + sfondo: sfondoRichiesto(s.crea), + box: pronti.regioni.map((r) => ({ ...r, prompt: fraseBox(r), rigidita: 'guida' as const })), + // L'adattatore sostituisce prompt con promptScena: stile, sfondo e parole LoRA devono esserci anche qui. + promptScena: conParoleLora(promptCrea({ ...s.crea, prompt: s.box.scena.trim() || 'Compose the following scene' }, (s.imp?.famiglia || 'qwen') === 'qwen')), + ...parametriModello(MODELLI[modelloDaImpostazioni(s.imp)].id), + etichetta: `Box · ${pronti.regioni.length} ${pronti.regioni.length === 1 ? 'riquadro' : 'riquadri'}` + } + setVistaRis(false) + await api.lavori.accoda(q) + if (q.quante > 1) avvisa(`${q.quante} immagini in coda`, 'ok') + } + + const attivi = regioni.filter((r) => !r.nascosta).length + + return ( +
+
+
+ {mostraInCorso && inCorso ? ( + + ) : vistaRis && op ? ( + + ) : ( + +
+ {regioni.length === 0 && !bozza && ( +
DISEGNA UN RIQUADROTrascina sulla tela e di' cosa ci va dentro. Oppure scegli una disposizione a sinistra.
+ )} + {regioni.map((r, i) => ( +
giuRegione(e, r, 'corpo')} + data-box={r.id} + > + {i + 1} +
{r.prompt || (r.rif ? 'foto di riferimento' : '')}
+ {r.id === selezionata && MANIGLIE.map((m) => giuRegione(e, r, m)} />)} +
+ ))} + {bozza &&
} +
+ + )} + {(mostraInCorso || (vistaRis && op)) && ( + + )} + +
+
+ {miei.filter((l) => l.stato === 'in corso' || l.stato === 'in coda').map((l) => ( +
{ setVedoInCorso(true); setVistaRis(false) }}> + {l.anteprima ? : null} +
{l.stato === 'in corso' ? (percentuale(l) !== null ? `${percentuale(l)}%` : '…') : 'in coda'}
+
+ ))} + {risultati.map((id) => { + const o = opere[id] + return ( +
{ setScelto(id); setVistaRis(true); setVedoInCorso(false) }}> + {o && } +
+ ) + })} + {risultati.length === 0 && miei.length === 0 && Qui compaiono le composizioni di questa sessione. Tutte finiscono anche in Galleria.} +
+
+ +
+
+
+

Motore {modello.nome}

+ + {modello.id === 'qwen-base' &&
Con Qwen i riquadri guidano la composizione a parole; con Klein 3 il motore li userà come regioni vere.
} +
+ +
+

Disposizioni

+
+ {DISPOSIZIONI.map((d) => ( + + ))} +
+
+ +
+

Scena generale facoltativa

+ setBox({ scena: s })} minimo={64} placeholder="Luce, atmosfera, stile dell'insieme: «al tramonto, colori caldi, fotografia»" /> +
+ +
+

Riquadri {regioni.length} / {MAX_BOX}

+ {regioni.length === 0 &&
Nessun riquadro: trascina sulla tela per disegnarne uno.
} +
+ {regioni.map((r, i) => { + const aperto = r.id === selezionata + const Ic = ICONA_TIPO[r.tipo] + return ( +
+
seleziona(aperto ? null : r.id)}> + {i + 1} + + {r.prompt || (r.rif ? 'foto di riferimento' : da descrivere…)} + + + +
+ {aperto && ( + <> + + {TIPI_BOX.map((t) => { + const T = ICONA_TIPO[t.id] + return } nome={t.nome} titolo={t.titolo} su={r.tipo === t.id} onClick={() => cambia(r.id, { tipo: t.id })} /> + })} + + cambia(r.id, { prompt })} + minimo={64} + senzaCestino + placeholder={r.tipo === 'testo' ? 'Il testo da scrivere (fra virgolette resta identico)' : r.tipo === 'sfondo' ? 'Com\'è lo sfondo' : r.tipo === 'persona' ? 'Chi è e cosa fa: «una ragazza con un cappotto rosso»' : 'Cos\'è: «un cane che dorme»'} + /> +

Il riquadro guida la composizione: non blocca i pixel. Per conservare i pixel fuori da una zona usa Modifica.

+ + )} +
+ ) + })} +
+
+ +
+

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

+ +
+
+

Misura

+ +
+ + setCrea({ stileId })} /> + {stileAttivo && setCrea({ intensita })} />} + + + + +
+

Quante

+ +
+
+ +
+
+ {attivi} {attivi === 1 ? 'riquadro' : 'riquadri'}{sel ? ` · selezionato il ${regioni.indexOf(sel) + 1}` : ''} + +
+ +
Ctrl+Invio · ~{durata(stima)}
+
+
+
+ ) +} diff --git a/src/renderer/src/pagine/Crea.tsx b/src/renderer/src/pagine/Crea.tsx index 691f96a..4687beb 100644 --- a/src/renderer/src/pagine/Crea.tsx +++ b/src/renderer/src/pagine/Crea.tsx @@ -1,24 +1,31 @@ -// Crea: dal testo all'immagine. -import { useEffect, useMemo, useState, type JSX } from 'react' +// Crea: dal testo all'immagine. GUI v2: ogni scelta è una tessera (motore, genere, atmosfera, stile, formato, misura, sfondo, quante), +// la misura finale è sempre sul palco (HUD) e il prompt di stile/sfondo si aggiunge da sé al momento di mandare. +import { useEffect, useMemo, useRef, useState, type JSX } from 'react' import { usaStato } from '../stato' import { api, urlFile } from '../api' import { I } from '../componenti/Icone' import { AdattaFoto } from '../componenti/AdattaFoto' -import { Interruttore, SezioneChiudibile, Segmenti } from '../componenti/Controlli' -import { SceltaMisura, ScegliMp, Uscita, MISURA0, dimMp } from '../componenti/Misura' -import { SceltaLora, conParoleLora } from '../componenti/SceltaLora' +import { SezioneChiudibile } from '../componenti/Controlli' +import { conParoleLora } from '../componenti/SceltaLora' import { percentuale } from '../componenti/Coda' -import { MenuStili, SchedaPreset } from '../componenti/Preset' import { VistaZoom } from '../componenti/VistaZoom' -import { FORMATI, RISOLUZIONI, dimensioniRis, durata, normalizza, stimaSecondi } from '../util' +import { SelettoreModello } from '../componenti/ScegliModello' +import { AtmosferaGenere, CampiGenere, GrigliaGeneri } from '../componenti/Generi' +import { IntensitaStile, ScaffaliStili } from '../componenti/ScaffaliStili' +import { HudCrea, SezFormato, SezMisura, SezQuante, SezSfondo } from '../componenti/SezioniCrea' +import { Hud } from '../componenti/Hud' +import { SceltaLora } from '../componenti/SceltaLora' import { Riferimenti } from '../componenti/Riferimenti' -import { SISTEMA_DESCRIVI, SISTEMA_IDEA, SISTEMA_MIGLIORA, apriInModifica, fotoDaOpera, richiestaBase, rifaiCon, rifacibileIn, rifinisci, riusa, varia, ingrandisci } from '../azioni' +import { Casella } from '../componenti/Casella' +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' +import { stileDa } from '../stili' +import { misuraCrea } from '../misura' +import { promptCrea, sfondoRichiesto } from '../creaPrompt' import { MODELLI, modelloDaImpostazioni, parametriModello } from '@shared/modelli' -import { Casella } from '../componenti/Casella' import type { Lavoro, Opera } from '@shared/tipi' - const ar = (f: string): string => { const x = FORMATI.find((v) => v.id === f) || FORMATI[0] return `${x.w} / ${x.h}` @@ -31,30 +38,23 @@ export function Crea(): JSX.Element { const [scelto, setScelto] = useState(null) const [opere, setOpere] = useState>({}) const [vedoInCorso, setVedoInCorso] = useState(true) + const prompt = useRef(null) // le ultime opere della galleria, per ripartire da lì quando la pagina è vuota const [recenti, setRecenti] = useState([]) useEffect(() => { void api.galleria.elenco({ modalita: 'tutte' }).then((l) => setRecenti(l.slice(0, 8))) }, [lavori.filter((l) => l.stato === 'fatto').length]) - const dim = dimensioniRis(crea.formato, crea.ris) - // la foto che esce (la misura precisa se c'è, se no la risoluzione scelta) e le dimensioni a cui la disegna il modello: con i megapixel - // scelti il modello lavora a quelli (anche 4 MP per una foto da 480p) e poi la riduce alla misura finale - const mis = crea.misura?.tipo === 'custom' ? crea.misura : null - const finale = mis ? { w: mis.w, h: mis.h } : dim - const mpLav = crea.mpLavoro ? Math.min(crea.mpLavoro, 4.19) : 0 - const m16 = (x: number): number => Math.max(256, Math.round(x / 16) * 16) - const lav = mpLav - ? dimMp(mpLav, finale.w, finale.h) - : mis - ? { w: m16(Math.sqrt(dim.w * dim.h * (mis.w / mis.h))), h: m16(Math.sqrt((dim.w * dim.h) / (mis.w / mis.h))) } - : dim - // il modello si sceglie dalla barra in alto e porta con sé i passi (e il CFG): qui non c'è niente da regolare + // la misura: la stessa funzione che costruisce la richiesta e che disegna il HUD («la cifra che vedi è quella che esce») + const mis = misuraCrea(crea) + const { finale, lav } = mis + // il modello si sceglie dalle tessere «Motore» e porta con sé i passi (e il CFG) const modello = MODELLI[modelloDaImpostazioni(imp)] const passi = modello.passi - const quante = crea.quante <= 1 ? 1 : crea.quante <= 2 ? 2 : 4 + const quante = Math.max(1, Math.min(8, crea.quante)) const preset = presetDa(crea.preset) const stima = stimaSecondi(imp?.tempi, (lav.w * lav.h) / 1048576, passi) * quante + const qwen = (imp?.famiglia || 'qwen') === 'qwen' // i lavori di questa pagina (creati, rifiniti, variati) const miei = useMemo(() => lavori.filter((l) => ['crea', 'rifinisci', 'varia'].includes(l.richiesta.modalita)), [lavori]) @@ -88,7 +88,7 @@ export function Crea(): JSX.Element { const q = { ...richiestaBase(), modalita: 'crea' as const, - prompt: conParoleLora(crea.prompt.trim()), + prompt: conParoleLora(promptCrea(crea, qwen)), // Klein: la richiesta breve si espande in una didascalia dettagliata (se non è un preset, che il prompt lo scrive già lungo) potenzia: !preset, // i valori ufficiali del modello (Klein sceglie CFG, sampler e scheduler da sé nel motore) @@ -104,9 +104,10 @@ export function Crea(): JSX.Element { ...(Math.abs(finale.w - lav.w) > 1 || Math.abs(finale.h - lav.h) > 1 ? { uscita: { w: finale.w, h: finale.h } } : {}), quante, immagini: crea.riferimenti.map((r) => r.percorso), - trasparente: crea.trasparente, + trasparente: crea.sfondo === 'trasparente', + sfondo: sfondoRichiesto(crea), ...parametriModello(modello.id), - etichetta: preset ? preset.nome : undefined + etichetta: preset ? preset.nome : stileDa(crea.stileId)?.nome } await api.lavori.accoda(q) if (quante > 1) avvisa(`${quante} immagini in coda`, 'ok') @@ -178,133 +179,27 @@ export function Crea(): JSX.Element { const op = scelto ? opere[scelto] : undefined const inserisci = (testo: string): void => { - const t = document.querySelector('.colonna:not(.destra) textarea') + const t = prompt.current const v = crea.prompt const i = t ? t.selectionStart : v.length setCrea({ prompt: v.slice(0, i) + testo + v.slice(i) }) } + const stileAttivo = stileDa(crea.stileId) + const nLora = usaStato((s) => s.loraAttive.length) + return (
-
-
-
- -
- -
-

- {preset ? 'Prompt (lo scrive il preset)' : 'Cosa vuoi vedere'} - {crea.prompt.length > 0 && `${crea.prompt.length} car.`} -

- setCrea({ prompt })} - minimo={preset ? 110 : 150} - disabled={scrivendo} - /> -
- - - - {precedente !== null && ( - - )} -
- {(imp?.famiglia || 'qwen') === 'qwen' &&
} -
- -
-

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

-
- {FORMATI.map((f) => { - const k = 22 / Math.max(f.w, f.h) - return ( - - ) - })} -
-

Lato corto

-
- {RISOLUZIONI.map((r) => ( - - ))} -
-
- -
- setCrea({ riferimenti })} - inizio={1} - inserisci={inserisci} - esempio={'Una persona, un vestito, un oggetto, uno stile: "una donna come su una spiaggia", "la giacca di addosso a un robot". Fino a 9 foto.'} - /> -
- -
-

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

- setCrea({ quante: q })} /> -
- - -
-
-

Seed

-
- - {!crea.casuale && setCrea({ seed: Math.max(0, Number(e.target.value) || 0) })} style={{ width: 130 }} />} -
-
- setCrea({ trasparente })} sotto="PNG con sfondo trasparente (canale alfa nativo di Qwen-Image 2.1)"> - Sfondo trasparente - -
-

Megapixel del modello {crea.mpLavoro ? `${(lav.w * lav.h / 1048576).toFixed(2).replace('.', ',')} MP` : 'come la misura finale'}

- A quanti megapixel disegna il modello. Con più megapixel della risoluzione finale disegna grande e poi riduce: più dettagli e linee più pulite anche per una foto piccola (es. 480p disegnata a 4 MP). - setCrea({ mpLavoro })} ar={{ w: finale.w, h: finale.h }} auto="come la misura" /> - 1 || Math.abs(finale.h - lav.h) > 1 ? `disegnata a ${lav.w}×${lav.h} e poi portata qui` : undefined} /> -

Misura precisa {crea.misura?.tipo === 'custom' ? 'attiva' : 'no'}

- setCrea({ misura })} base={null} mp={Math.round((dim.w * dim.h) / 1e5) / 10} ammetti={['auto', 'custom']} auto={{ w: dim.w, h: dim.h }} senzaUscita /> -
-
-
-
-
- -
Ctrl+Invio · ~{durata(stima)}
-
-
-
-
+
{mostraInCorso && inCorso ? ( ) : op ? ( ) : (
-
CREA CON QWEN-IMAGE 2.1
-

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

+
COSA COSTRUIAMO OGGI?
+

Scegli a sinistra un genere e uno stile, scrivi cosa vuoi vedere e premi Crea (Ctrl+Invio).

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

{recenti.length > 0 && (
@@ -320,10 +215,12 @@ export function Crea(): JSX.Element { )}
)} +
{op && !mostraInCorso && (
+ {rifacibileIn(op).includes('klein-base') && } {rifacibileIn(op).includes('qwen-base') && } @@ -356,6 +253,123 @@ export function Crea(): JSX.Element { {risultati.length === 0 && miei.length === 0 && Qui compaiono le immagini di questa sessione. Tutte finiscono anche in Galleria.}
+ +
+
+
+

Motore {modello.nome}

+ +
+ +
+

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

+ + {preset &&
} +
+ + {preset && preset.stili.length > 1 && ( +
+

Atmosfera

+ +
+ )} + +
+

+ {preset ? 'Prompt (lo scrive il genere)' : 'Cosa vuoi vedere'} + {crea.prompt.length > 0 && `${crea.prompt.length} car.`} +

+ setCrea({ prompt: p })} + minimo={preset ? 110 : 150} + disabled={scrivendo} + /> +
+ + + + {precedente !== null && ( + + )} +
+
+ + + setCrea({ stileId })} /> + {stileAttivo && setCrea({ intensita })} />} + + +
+

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

+ +
+ +
+

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

+ +
+ +
+

Sfondo

+ +
+ +
+ setCrea({ riferimenti })} + inizio={1} + inserisci={inserisci} + esempio={'Una persona, un vestito, un oggetto, uno stile: "una donna come su una spiaggia", "la giacca di addosso a un robot". Fino a 9 foto.'} + /> +
+ +
+

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

+ + {!crea.casuale && ( +
+ Seed + setCrea({ seed: Math.max(0, Number(e.target.value) || 0) })} style={{ width: 130 }} /> +
+ )} +
+ + {qwen && ( + + + + )} +
+ +
+
+
+ {preset && } + {stileAttivo && } + {crea.sfondo !== 'scena' && } + {crea.riferimenti.length > 0 && } + {qwen && nLora > 0 && } +
+ +
+ +
Ctrl+Invio · ~{durata(stima)}
+
+
) } diff --git a/src/renderer/src/pagine/Impostazioni.tsx b/src/renderer/src/pagine/Impostazioni.tsx index ec6cc19..2744ab7 100644 --- a/src/renderer/src/pagine/Impostazioni.tsx +++ b/src/renderer/src/pagine/Impostazioni.tsx @@ -1,28 +1,23 @@ // Impostazioni: modelli, prestazioni, cartelle, motore. import { useEffect, useRef, useState, type JSX } from 'react' import { usaStato, ricaricaImp } from '../stato' -import { Lora } from './Lora' import { api, type FileModelli } from '../api' import { I } from '../componenti/Icone' import { Interruttore, Segmenti } from '../componenti/Controlli' import { byte } from '../util' import type { Download, Impostazioni as Imp, StatoVoce } from '@shared/tipi' import type { FileInutile } from '../api' +import { LaboratorioUncensored } from '../componenti/LaboratorioUncensored' const GRUPPI_MODELLI: [StatoVoce['tipo'], string][] = [ ['diffusione', 'Modello di diffusione (denoiser)'], ['encoder', 'Text encoder (l\'LLM)'], ['klein', 'FLUX 3 Klein · modelli'], ['klein-te', 'FLUX 3 Klein · text encoder'], ['klein-vae', 'FLUX 3 Klein · VAE'], ['traduttore', 'Traduttore'], ['vae', 'VAE'], ['upscaler', 'Ingrandimento'], ['anteprima', 'Anteprime dal vivo'] ] -/** Opzioni: due schede, le impostazioni di sempre e i LoRA (prima avevano una voce a sinistra, nella rotaia). */ +/** Opzioni: le impostazioni di sempre (i LoRA hanno la pagina loro, nella rotaia). */ export function Impostazioni(): JSX.Element { - const scheda = usaStato((s) => s.schedaOpzioni) return (
-
- - -
-
{scheda === 'lora' ? : }
+
) } @@ -110,6 +105,7 @@ function OpzioniGenerali(): JSX.Element { return (
+

alterna('MODELLI IN USO')}>{chiusi['MODELLI IN USO'] ? '▸' : '▾'} MODELLI IN USO

@@ -136,7 +132,7 @@ function OpzioniGenerali(): JSX.Element {

alterna('SCARICA ALTRI MODELLI')}>{chiusi['SCARICA ALTRI MODELLI'] ? '▸' : '▾'} SCARICA ALTRI MODELLI

{GRUPPI_MODELLI.map(([tipo, titolo]) => { - const voci = catalogo.filter((v) => v.tipo === tipo) + const voci = catalogo.filter((v) => v.tipo === tipo && !v.uncensored) if (!voci.length) return null return (
diff --git a/src/renderer/src/pagine/Lora.tsx b/src/renderer/src/pagine/Lora.tsx index eb9e56b..e90b95c 100644 --- a/src/renderer/src/pagine/Lora.tsx +++ b/src/renderer/src/pagine/Lora.tsx @@ -6,9 +6,11 @@ import { I } from '../componenti/Icone' import { Copertina } from '../componenti/SceltaLora' import { byte } from '../util' import type { Download, InfoLora, StatoVoce } from '@shared/tipi' +import { compatibilitaLora, forzaLora, spostaLora } from '../pilaLora' export function Lora(): JSX.Element { - const { lore, loraAttive, toggleLora, setLoraAttive, avvisa } = usaStato() + const { lore, loraAttive, toggleLora, setLoraAttive, avvisa, imp } = usaStato() + const famiglia = imp?.famiglia || 'qwen' const [link, setLink] = useState('') const [sopra, setSopra] = useState(false) const [download, setDownload] = useState>({}) @@ -73,7 +75,7 @@ export function Lora(): JSX.Element {
- Servono LoRA fatti per Qwen-Image 2.1: quelli per SDXL, Flux o Qwen-Image 1.x vengono segnati e non funzionano. Per Civitai a volte serve il token (Impostazioni). + La compatibilità dipende dal modello: Qwen, Klein e gli altri Flux non sono intercambiabili. Per Civitai a volte serve il token (Impostazioni). La pila non converte un LoRA incompatibile.
@@ -84,6 +86,29 @@ export function Lora(): JSX.Element {
))} +
+

Pila LoRA

{famiglia} · {loraAttive.length} slot
+

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

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

CONSIGLIATI PER QWEN-IMAGE 2.1

@@ -111,7 +136,13 @@ export function Lora(): JSX.Element { {lore.length === 0 &&
Ancora nessun LoRA. Un LoRA aggiunge uno stile, un personaggio o un dettaglio al modello: trascinane uno qui sopra.
}
{visibili.map((l) => ( - a.file === l.file)} toggle={() => toggleLora(l.file, l.forza)} /> + a.file === l.file)} toggle={() => { + if (!loraAttive.some((a) => a.file === l.file) && compatibilitaLora(l, famiglia) === 'no') { + avvisa('Questo LoRA non è compatibile con il modello selezionato.', 'errore') + return + } + toggleLora(l.file, forzaLora(l.forza)) + }} /> ))}
@@ -120,7 +151,8 @@ export function Lora(): JSX.Element { } function CartaLora({ l, attiva, toggle }: { l: InfoLora; attiva: boolean; toggle: () => void }): JSX.Element { - const { avvisa, loraAttive, setLoraAttive } = usaStato() + const { avvisa, loraAttive, setLoraAttive, imp } = usaStato() + const compatibile = compatibilitaLora(l, imp?.famiglia || 'qwen') const [parole, setParole] = useState(l.parole) const [forza, setForza] = useState(l.forza) useEffect(() => { @@ -152,7 +184,7 @@ function CartaLora({ l, attiva, toggle }: { l: InfoLora; attiva: boolean; toggle
{l.nome}
- {l.compatibile === 'si' ? '✓ Qwen 2.1' : l.compatibile === 'no' ? '⚠ ' + (l.base?.split(' (')[0] || 'altro') : '?'} + {compatibile === 'si' ? '✓ Compatibile' : compatibile === 'no' ? '⚠ ' + (l.base?.split(' (')[0] || 'altro modello') : '? Da verificare'}
Forza { - const v = Number(e.target.value) + const v = forzaLora(Number(e.target.value)) setForza(v) if (attiva) setLoraAttive(loraAttive.map((a) => (a.file === l.file ? { ...a, forza: v } : a))) }} onPointerUp={() => salva({ forza })} + onBlur={() => salva({ forza })} /> {forza.toFixed(2)}
diff --git a/src/renderer/src/pagine/Studio.tsx b/src/renderer/src/pagine/Studio.tsx new file mode 100644 index 0000000..c4e0c2a --- /dev/null +++ b/src/renderer/src/pagine/Studio.tsx @@ -0,0 +1,66 @@ +// Studio: Crea, Modifica, Box e Ricette sullo stesso palco. Qui c'è la strip dei modi (in cima) e il pezzo che la lega al resto: +// i modi non sono pagine nuove, sono le pagine di sempre (Crea, Modifica, Box) che restano montate, così la maschera disegnata, +// il prompt e i riquadri non si perdono passando da un modo all'altro. +import type { JSX } from 'react' +import { usaStato, type Pagina } from '../stato' +import { MODELLI, modelloDaImpostazioni } from '@shared/modelli' +import { I } from '../componenti/Icone' + +export type ModoStudio = 'crea' | 'modifica' | 'box' | 'ricette' + +export const PAGINE_STUDIO: Pagina[] = ['crea', 'modifica', 'box'] + +/** in quale modo dello Studio siamo (Ricette è Modifica col catalogo aperto) */ +export function modoStudio(): ModoStudio | null { + const s = usaStato.getState() + if (s.pagina === 'crea' || s.pagina === 'box') return s.pagina + if (s.pagina === 'modifica') return s.modifica.modo === 'ricette' ? 'ricette' : 'modifica' + return null +} + +/** apre un modo dello Studio */ +export function vaiModo(modo: ModoStudio): void { + const s = usaStato.getState() + if (modo === 'ricette') { + s.setModifica({ modo: 'ricette' }) + s.vai('modifica') + return + } + if (modo === 'modifica' && s.modifica.modo === 'ricette') s.setModifica({ modo: 'scrivi' }) + s.vai(modo) +} + +const MODI: { id: ModoStudio; nome: string; icona: () => JSX.Element; titolo: string; tasto: string }[] = [ + { id: 'crea', nome: 'Crea', icona: I.crea, titolo: 'Dal testo all\'immagine (1)', tasto: '1' }, + { id: 'modifica', nome: 'Modifica', icona: I.modifica, titolo: 'Cambia una foto: tutta, una zona, espandila o estrai (2)', tasto: '2' }, + { id: 'box', nome: 'Box', icona: I.box, titolo: 'Disegna dei riquadri e di\' cosa ci va in ognuno (3)', tasto: '3' }, + { id: 'ricette', nome: 'Ricette', icona: I.ricette, titolo: 'Cose già pronte in più passi, anche su più foto (4)', tasto: '4' } +] + +export function StripModi(): JSX.Element { + const pagina = usaStato((s) => s.pagina) + const modo = usaStato((s) => s.modifica.modo) + const imp = usaStato((s) => s.imp) + const nLora = usaStato((s) => s.loraAttive.length) + const vai = usaStato((s) => s.vai) + const attivo: ModoStudio = pagina === 'modifica' ? (modo === 'ricette' ? 'ricette' : 'modifica') : pagina === 'box' ? 'box' : 'crea' + const m = MODELLI[modelloDaImpostazioni(imp)] + return ( +
+ {MODI.map((x) => ( + + ))} + + + Motore {m.nome} + + {( + + )} +
+ ) +} diff --git a/src/renderer/src/pagine/modifica/SchedaIdee.tsx b/src/renderer/src/pagine/modifica/SchedaIdee.tsx new file mode 100644 index 0000000..4ab3e5b --- /dev/null +++ b/src/renderer/src/pagine/modifica/SchedaIdee.tsx @@ -0,0 +1,112 @@ +// Modifica: le Idee come griglia di tessere con icona (nessun chip di testo). Le categorie sono tessere; aprendone una compaiono le sue azioni, +// anch'esse tessere (un'icona e una parola, il nome lungo nel tooltip). Le scelte si sommano e il modello le mescola. +// «Estrai» è la tessera grande in cima; la categoria Stili ha gli scaffali dei 28 stili con l'intensità; il comando Camera resta in Prospettiva. +import { type JSX } from 'react' +import { usaStato } from '../../stato' +import { I } from '../../componenti/Icone' +import { Casella } from '../../componenti/Casella' +import { AvvisoLora } from '../../componenti/AvvisoLora' +import { ControlloCamera } from '../../componenti/Camera' +import { Pianeti, Tessera, Tessere } from '../../componenti/Tessere' +import { IntensitaStile, ScaffaliStili } from '../../componenti/ScaffaliStili' +import { ControlliCategoria } from '../../componenti/ControlliCategoria' +import { controlliDi, prefissoIdea, rapidaVisibile } from '../../controlli' +import { FANTASIE } from '../../regista' +import { iconaRapida } from '../../iconeRapide' +import { frasiStileModifica, stileDa, type Intensita } from '../../stili' +import type { PropsScrivi } from './SchedaScrivi' + +const ID_STILE = 'stile-' + +export function SchedaIdee({ griglia, zona, categorie, cat, setCategoria, idee, setIdee, usaRapida, chiaveIdee, camera, cambiaCamera, loraCamera }: PropsScrivi): JSX.Element | null { + const { modifica: m, setModifica } = usaStato() + if (m.modo === 'espandi' || griglia) return null + + const stileAttivo = idee.find((r) => r.id.startsWith(ID_STILE))?.id.slice(ID_STILE.length) || '' + /** quante idee attive ci sono in una categoria (la pallina luminosa sulla tessera) */ + const quante = (id: string): number => { + if (id === 'stili') return idee.filter((r) => r.id.startsWith(ID_STILE)).length + return idee.filter((r) => categorie.find((c) => c.id === id)?.voci.some((v) => v.id === r.id) || (r.id.startsWith('ctl-') && controlliDi(id).some((c) => r.id.startsWith(prefissoIdea(c.id))))).length + } + const scegliStile = (id: string): void => { + const senza = idee.filter((r) => !r.id.startsWith(ID_STILE)) + const s = stileDa(id) + setIdee(s ? [...senza, { id: ID_STILE + s.id, nome: s.nome, prompt: frasiStileModifica(s, m.intensitaStile) }] : senza) + } + const cambiaIntensita = (i: Intensita): void => { + setModifica({ intensitaStile: i }) + setIdee(idee.map((r) => (r.id.startsWith(ID_STILE) ? { ...r, prompt: frasiStileModifica(stileDa(r.id.slice(ID_STILE.length))!, i) } : r))) + } + + return ( +
+

Idee sommale: il modello le mescola

+ + } nome="Estrai" sotto="persone e oggetti, un clic" titolo="Estrai una persona o un oggetto dalla foto: un clic sulla cosa, poi scegli l'uscita" onClick={() => setModifica({ modo: 'estrai' })} /> + } nome="Espandi" sotto="allarga oltre i bordi" titolo="Allarga la foto oltre i bordi" onClick={() => setModifica({ modo: 'espandi' })} /> + + + {categorie.map((c) => { + const Ic = (I as Record JSX.Element>)[c.icona] || I.crea + return } nome={c.nome} su={cat?.id === c.id} pallina={quante(c.id)} onClick={() => setCategoria(c.id)} /> + })} + + + {cat && cat.id === 'prospettiva' && !zona && ( +
+ + +
+ )} + {cat && cat.id === 'vestiti' &&
} + + {cat && cat.id !== 'stili' && } + + {cat && cat.id === 'stili' ? ( +
+ + {stileAttivo && } +
+ ) : ( +
+ + {cat?.voci.filter(rapidaVisibile).map((r) => { + const x = iconaRapida(r) + return ( + {x.icona}} + nome={x.nome} + su={idee.some((y) => y.id === r.id)} + onClick={() => void usaRapida(r)} + titolo={`${r.nome.replace('…', '')}${r.rif ? ` · serve ${r.rifNome || 'una foto di riferimento'}` : ''} · un clic la accende, un altro la spegne`} + /> + ) + })} + +
+ )} + + {idee.length > 0 && ( +
+
+ {idee.length} {idee.length === 1 ? 'idea' : 'idee'}: {idee.map((r) => r.nome.replace('…', '')).join(' + ')} + +
+ Fantasia del modello + setModifica({ fantasia })} voci={FANTASIE.map((f) => ({ id: f.id, nome: f.nome, titolo: f.sotto }))} /> + {m.pianoIdee && m.pianoIdee.chiave === chiaveIdee ? ( +
+
Il piano del modello (puoi correggerlo)
+ setModifica({ pianoIdee: { chiave: m.pianoIdee!.chiave, testo } })} /> +
+ ) : ( +
Quando premi «Modifica» il modello guarda la foto, mescola le idee e inventa i dettagli. Qui vedrai cosa ha deciso.
+ )} +
+ )} + {cat && ['luce', 'ritocco', 'situazioni'].includes(cat.id) &&
} +
+ ) +} diff --git a/src/renderer/src/pagine/modifica/SchedaRicette.tsx b/src/renderer/src/pagine/modifica/SchedaRicette.tsx index 55d5e3e..cd749b1 100644 --- a/src/renderer/src/pagine/modifica/SchedaRicette.tsx +++ b/src/renderer/src/pagine/modifica/SchedaRicette.tsx @@ -21,7 +21,6 @@ import { specialeOra, POSA_TUA, VISTA_TUA, leggiCamera, WORKFLOW_BASE, avviaLott import { usaRicette, type RicettaUtente } from '../../ricette' import { EditorRicetta } from '../../componenti/EditorRicetta' import { Catalogo, Dettaglio, Risultati, icona } from '../../componenti/LabVista' -import { SelettoreModo } from './SelettoreModo' import type { Opera } from '@shared/tipi' @@ -63,6 +62,8 @@ export function SchedaRicette(): JSX.Element { const run = runs.find((r) => r.id === attiva) const scegli = (id: string): void => { + // Mix si apre sulla tela di Modifica: parte da «Unisci foto» e da lì si passa a «Come l'esempio» + if (id === 'r-mix') id = 'unisci' const nuovo = workflowDa(id)! usaLab.setState({ scelto: id, vista: 'catalogo' }) setValori(valoriIniziali(nuovo)) @@ -164,10 +165,10 @@ export function SchedaRicette(): JSX.Element { return ( <>
-
+ {/* i modi dello Studio stanno nella strip in cima: qui niente selettore */}
- + @@ -216,8 +217,8 @@ export function SchedaRicette(): JSX.Element { {!w ? (

Ricette

-

Ricette già pronte: scegline una a sinistra, dai la foto (o il testo) e premi Avvia. Il motore fa tutti i passi da solo, uno dopo l'altro, e i risultati restano in Galleria.

-

Due tipi: le serie (la stessa foto in tante versioni: pose, stili, luci…) e le catene (ogni passo parte dal risultato del precedente: restauro, il tempo che passa…).

+

Sei ricette già pronte: scegline una a destra, dai la foto (o il testo) e premi Avvia. Il motore fa tutti i passi da solo, uno dopo l'altro, e i risultati restano in Galleria.

+

Due tipi: le serie (la stessa foto in tante versioni: viste, pose, mondi…) e le catene (ogni passo parte dal risultato del precedente: Rinnova).

) : ( <> diff --git a/src/renderer/src/pagine/modifica/SchedaScrivi.tsx b/src/renderer/src/pagine/modifica/SchedaScrivi.tsx index c496011..c664679 100644 --- a/src/renderer/src/pagine/modifica/SchedaScrivi.tsx +++ b/src/renderer/src/pagine/modifica/SchedaScrivi.tsx @@ -1,13 +1,11 @@ // Modifica, scheda Scrivi: il prompt, Migliora, i segnaposto delle foto e le Idee (azioni rapide che si sommano). // Se sulla tela c'è una zona disegnata il lavoro vale solo per la zona, se no per tutta la foto (lo decide Modifica.tsx). import { type JSX, type RefObject } from 'react' +import type { PosizioneCamera } from '../../componenti/Camera' import { usaStato, type FotoBase } from '../../stato' import { I } from '../../componenti/Icone' import { Casella } from '../../componenti/Casella' -import { Segmenti } from '../../componenti/Controlli' -import { AvvisoLora } from '../../componenti/AvvisoLora' -import { ControlloCamera, type PosizioneCamera } from '../../componenti/Camera' -import { FANTASIE } from '../../regista' +import { SchedaIdee } from './SchedaIdee' import { usaSpeciale } from '../../workflow' import type { Rapida, CategoriaRapide } from '../../rapide' @@ -99,58 +97,3 @@ function SchedaTesto({ prompt, base, griglia, zona, scrivendo, migliora, setCame
) } - -/** Idee (le azioni rapide, che si sommano), il comando Camera e il piano delle idee */ -function SchedaIdee({ griglia, zona, categorie, cat, setCategoria, idee, setIdee, usaRapida, chiaveIdee, camera, cambiaCamera, loraCamera }: PropsScrivi): JSX.Element | null { - const { modifica: m, setModifica } = usaStato() - if (m.modo === 'espandi' || griglia) return null - return ( -
-

Idee sommale: il modello le mescola

-
- {categorie.map((c) => { - const Ic = (I as Record JSX.Element>)[c.icona] || I.crea - return ( - - ) - })} -
- {cat && cat.id === 'prospettiva' && !zona && ( -
- - -
- )} - {cat && cat.id === 'vestiti' &&
} -
- {cat?.voci.map((r) => ( - - ))} -
- {idee.length > 0 && ( -
-
- {idee.length} {idee.length === 1 ? 'idea' : 'idee'}: {idee.map((r) => r.nome.replace('…', '')).join(' + ')} - -
- Fantasia del modello - setModifica({ fantasia })} voci={FANTASIE.map((f) => ({ id: f.id, nome: f.nome, sotto: f.sotto }))} /> - {m.pianoIdee && m.pianoIdee.chiave === chiaveIdee ? ( -
-
Il piano del modello (puoi correggerlo)
- setModifica({ pianoIdee: { chiave: m.pianoIdee!.chiave, testo } })} /> -
- ) : ( -
Quando premi «Modifica» il modello guarda la foto, mescola le idee e inventa i dettagli. Qui vedrai cosa ha deciso.
- )} -
- )} - {cat && ['luce', 'ritocco', 'situazioni'].includes(cat.id) &&
} -
- ) -} diff --git a/src/renderer/src/pagine/modifica/SelettoreModo.tsx b/src/renderer/src/pagine/modifica/SelettoreModo.tsx index bfa55ef..affd7ab 100644 --- a/src/renderer/src/pagine/modifica/SelettoreModo.tsx +++ b/src/renderer/src/pagine/modifica/SelettoreModo.tsx @@ -1,22 +1,22 @@ -// I modi di Modifica (Scrivi, Espandi, Ricette, Estrai): sta in cima alla pagina, anche quando si guarda il catalogo delle ricette. +// I modi di Modifica (Scrivi, Espandi, Estrai) come tessere: stanno in cima alla tela. Ricette ha il suo bottone nella strip dello Studio. import { type JSX } from 'react' import { usaStato, type ModoModifica } from '../../stato' -import { Segmenti } from '../../componenti/Controlli' +import { I } from '../../componenti/Icone' +import { Tessera } from '../../componenti/Tessere' + +const MODI: { id: ModoModifica; nome: string; icona: () => JSX.Element; titolo: string }[] = [ + { id: 'scrivi', nome: 'Scrivi', icona: I.modifica, titolo: 'Scrivi cosa cambiare: se segni una zona col pennello cambia solo lì, se no tutta la foto' }, + { id: 'espandi', nome: 'Espandi', icona: I.espandi, titolo: 'Allarga la foto oltre i bordi' }, + { id: 'estrai', nome: 'Estrai', icona: I.estrai, titolo: 'Dalla foto estrai una persona o un oggetto: un clic sulla cosa, poi scegli l\'uscita' } +] export function SelettoreModo(): JSX.Element { const { modifica: m, setModifica } = usaStato() return ( -
- - valore={m.modo} - cambia={(modo) => setModifica({ modo })} - voci={[ - { id: 'scrivi', nome: 'Scrivi', titolo: 'Scrivi cosa cambiare: se segni una zona col pennello cambia solo lì, se no tutta la foto' }, - { id: 'espandi', nome: 'Espandi' }, - { id: 'ricette', nome: 'Ricette', titolo: 'Cose già pronte in più passi (pose, stili, luci…), anche su tante foto; con Unisci foto e Come l\'esempio' }, - { id: 'estrai', nome: 'Estrai', titolo: 'Dalla foto estrai una persona o un oggetto, su sfondo pulito' } - ]} - /> +
+ {MODI.map((x) => ( + } nome={x.nome} titolo={x.titolo} su={m.modo === x.id} onClick={() => setModifica({ modo: x.id })} /> + ))}
) } diff --git a/src/renderer/src/pagine/modifica/Tela.tsx b/src/renderer/src/pagine/modifica/Tela.tsx index b768773..9a123cc 100644 --- a/src/renderer/src/pagine/modifica/Tela.tsx +++ b/src/renderer/src/pagine/modifica/Tela.tsx @@ -56,9 +56,11 @@ export function TelaModifica(p: Props): JSX.Element {
{speciale && ( - + <> + + + + )} {disegna && base && !confrontoAttivo && ( <> diff --git a/src/renderer/src/pilaLora.ts b/src/renderer/src/pilaLora.ts new file mode 100644 index 0000000..5c74dc1 --- /dev/null +++ b/src/renderer/src/pilaLora.ts @@ -0,0 +1,23 @@ +// Operazioni pure della pila: non cambiano la libreria né il modello selezionato. +import type { Famiglia, InfoLora, LoraAttiva } from '@shared/tipi' + +export function forzaLora(v: number): number { + return Number.isFinite(v) ? Math.max(-2, Math.min(2, v)) : 0 +} + +export function spostaLora(pila: LoraAttiva[], file: string, verso: -1 | 1): LoraAttiva[] { + const i = pila.findIndex((x) => x.file === file) + const j = i + verso + if (i < 0 || j < 0 || j >= pila.length) return pila + const nuova = [...pila] + ;[nuova[i], nuova[j]] = [nuova[j], nuova[i]] + return nuova +} + +// I metadati nuovi sono opzionali finché non è integrato il contratto engine v2. +// Nelle librerie vecchie «si» identifica Qwen 2.1, NON tutte le architetture. +export function compatibilitaLora(l: Pick & { famiglie?: Famiglia[] }, famiglia: Famiglia): 'si' | 'no' | '?' { + if (l.famiglie?.length) return l.famiglie.includes(famiglia) ? 'si' : 'no' + if (l.compatibile === '?') return '?' + return famiglia === 'qwen' ? l.compatibile : 'no' +} diff --git a/src/renderer/src/ricetteBase.ts b/src/renderer/src/ricetteBase.ts index dccf49c..83004c8 100644 --- a/src/renderer/src/ricetteBase.ts +++ b/src/renderer/src/ricetteBase.ts @@ -6,19 +6,19 @@ import { componiCamera, conLora } from './componenti/Camera' import { POSA_TUA, VISTA_TUA, leggiCamera, type Ctx, type Ingresso, type PassoWf, type Workflow } from './workflow' export const E = (etichetta: string, prompt: string, extra: Partial = {}): PassoWf => ({ etichetta, prompt, ...extra }) -const FOTO = 'the photo ' +export const FOTO = 'the photo ' /** il comando Camera dentro la ricetta: muovi i cerchi e ottieni il passo "La tua vista" */ -const CAMERA_TUA: Ingresso = { id: 'vista', tipo: 'camera', nome: 'Muovi la camera: una vista tua, oltre a quelle della ricetta' } +export const CAMERA_TUA: Ingresso = { id: 'vista', tipo: 'camera', nome: 'Muovi la camera: una vista tua, oltre a quelle della ricetta' } /** il passo della camera scelta (se l'hai mossa); `intorno` è quello che resta uguale in ogni vista della ricetta */ -const vistaTua = (c: Ctx, intorno: string): PassoWf[] => { +export const vistaTua = (c: Ctx, intorno: string): PassoWf[] => { const pos = leggiCamera(c.v.vista) const cam = pos && componiCamera(pos, c.camera) if (!pos || !cam) return [] return [E(VISTA_TUA, `${cam} ${intorno}`, { lora: c.camera && conLora(pos) ? ['camera'] : [] })] } /** l'omino dentro la ricetta: lo muovi e ottieni il passo "La tua posa" (la posa è un'immagine in più, `{posa}` nel prompt) */ -const POSA_TUA_IN: Ingresso = { id: 'posa', tipo: 'posa', nome: 'Muovi l\'omino: una posa tua (anche mani e dita), oltre a quelle della ricetta' } -const posaTua = (c: Ctx, intorno: string): PassoWf[] => { +export const POSA_TUA_IN: Ingresso = { id: 'posa', tipo: 'posa', nome: 'Muovi l\'omino: una posa tua (anche mani e dita), oltre a quelle della ricetta' } +export const posaTua = (c: Ctx, intorno: string): PassoWf[] => { const po = leggiPosa(c.v.posa) if (!po) return [] // il manichino di posa è la prima immagine ({posa}), la foto del personaggio la seconda ({foto}): vedi richiestaPasso @@ -38,7 +38,7 @@ export const righe = (s: string | undefined): string[] => (s || '').split('\n'). // --------------------------------------------------------------------------------------------------------------- // i workflow fatti a mano // --------------------------------------------------------------------------------------------------------------- -const POSE: [string, string][] = [ +export const POSE: [string, string][] = [ ['In piedi', 'standing straight with arms crossed, confident'], ['Seduto', 'sitting on a chair, relaxed'], ['Cammina', 'walking forward, mid-step'], @@ -49,11 +49,11 @@ const POSE: [string, string][] = [ ['Guarda indietro', 'seen from behind, looking back over the shoulder'], ['Appoggiato', 'leaning casually against a wall'] ] -const ESPRESSIONI: [string, string][] = [ +export const ESPRESSIONI: [string, string][] = [ ['Felice', 'a warm genuine smile'], ['Ride', 'laughing out loud'], ['Triste', 'a sad, teary look'], ['Arrabbiato', 'an angry frown'], ['Sorpreso', 'a surprised look with wide eyes and open mouth'], ['Spaventato', 'a frightened look'], ['Pensieroso', 'a thoughtful, pensive look'], ['Occhiolino', 'a playful wink'] ] -const ARMADIO: [string, string][] = [ +export const ARMADIO: [string, string][] = [ ['Elegante', 'a tailored dark suit with a crisp white shirt'], ['Casual', 'jeans, a white t-shirt and sneakers'], ['Sportivo', 'a modern athletic tracksuit'], ['Invernale', 'a warm winter coat, scarf and wool hat'], ['Estate', 'light summer clothes: linen shirt and shorts'], ['Serata', 'an elegant evening outfit'], ['Business', 'a smart business outfit'], ['Halloween', 'a spooky Halloween costume'] @@ -72,15 +72,15 @@ const INQUADRATURE: [string, string][] = [ ['Primo piano', 'Re-frame as a tight close-up on the face'], ['Mezzo busto', 'Re-frame as a medium shot from the waist up'], ['Figura intera', 'Re-frame as a full-body shot showing the whole figure'], ['Grandangolo', 'Re-shoot with a wide-angle lens showing a lot of the surroundings'], ['Dall\'alto', 'Re-shoot from a high angle looking down'], ['Dal basso', 'Re-shoot from a low angle looking up'] ] -const GIORNO: [string, string][] = [ +export const GIORNO: [string, string][] = [ ['Alba', 'at dawn with soft pink and orange light on the horizon'], ['Mattino', 'in the bright clear morning'], ['Mezzogiorno', 'at noon under harsh overhead sun'], ['Tramonto', 'at sunset with a glowing golden-red sky'], ['Ora blu', 'at blue hour just after sunset, deep blue sky with warm lights'], ['Notte', 'at night with a dark sky and artificial lights'] ] -const STAGIONI: [string, string][] = [ +export const STAGIONI: [string, string][] = [ ['Primavera', 'in spring: blooming flowers and fresh green leaves'], ['Estate', 'in summer: lush green, strong warm sunlight'], ['Autunno', 'in autumn: orange and red leaves, soft golden light'], ['Inverno', 'in winter: covered with snow, cold blue light'] ] -const METEO: [string, string][] = [ +export const METEO: [string, string][] = [ ['Sole', 'on a perfect sunny day'], ['Nuvoloso', 'under a heavy overcast sky'], ['Pioggia', 'in steady rain with wet surfaces and reflections'], ['Neve', 'in falling snow'], ['Nebbia', 'in thick atmospheric fog'], ['Temporale', 'during a dramatic thunderstorm with dark clouds and lightning'] ] @@ -88,18 +88,18 @@ const LUCI: [string, string][] = [ ['Studio morbido', 'soft professional studio lighting'], ['Rembrandt', 'Rembrandt lighting with a triangle of light on the cheek'], ['Controluce', 'strong backlight with a glowing rim light'], ['Neon', 'colorful magenta and cyan neon lights'], ['Candela', 'warm flickering candlelight in a dark room'], ['Drammatica', 'dramatic low-key lighting with deep shadows'] ] -const EPOCHE: [string, string][] = [ +export const EPOCHE: [string, string][] = [ ['Anni \'20', 'the 1920s: art deco style, period clothes and cars, sepia tones'], ['Anni \'50', 'the 1950s: retro diner era, period clothes, pastel colors'], ['Anni \'80', 'the 1980s: neon, big hair, retro tech and film grain'], ['Medioevo', 'the Middle Ages: stone buildings, period clothes, torch light'], ['Antica Roma', 'ancient Rome: marble columns, togas, Mediterranean sun'], ['Western', 'the Wild West: dusty frontier town, cowboy clothes'], ['Steampunk', 'a steampunk world: brass gears, airships, Victorian clothes'], ['Futuro lontano', 'a far future: sleek technology, holograms, silver architecture'] ] -const MONDI: [string, string][] = [ +export const MONDI: [string, string][] = [ ['Sott\'acqua', 'underwater with light rays, bubbles and coral'], ['Nello spazio', 'in outer space with planets and a nebula'], ['Deserto', 'in a vast desert with dunes at golden hour'], ['Giungla', 'in a dense tropical jungle with mist'], ['Cyberpunk', 'in a rainy cyberpunk city full of neon signs'], ['Post-apocalisse', 'in a ruined post-apocalyptic world overgrown with plants'], ['Nel ghiaccio', 'in a frozen arctic landscape with ice and aurora'], ['Fantasy', 'in an epic high-fantasy world with floating islands'] ] -const STILI: [string, string][] = [ +export const STILI: [string, string][] = [ ['Anime', 'a high quality anime illustration'], ['Acquerello', 'a delicate watercolor painting'], ['Olio', 'a classical oil painting with visible brush strokes'], ['3D cartoon', 'a 3D animated movie render'], ['LEGO', 'a scene built of LEGO bricks and minifigures'], ['Creta', 'a stop-motion claymation scene made of modeling clay'], ['Pixel art', 'detailed 32-bit pixel art'], ['Matita', 'a detailed pencil sketch on paper'], ['Fumetto', 'a comic book illustration with bold ink and halftone shading'], @@ -120,12 +120,12 @@ const PRODOTTO: [string, string][] = [ ['Lifestyle', 'in a cozy lifestyle scene on a wooden table with warm morning light and props'], ['Marmo', 'on a white marble slab with soft reflections, luxury look'], ['In mano', 'held in a hand, natural light, shallow depth of field'], ['Flat lay', 'in a top-down flat lay with complementary props on a textured surface'] ] -const MOCKUP: [string, string][] = [ +export const MOCKUP: [string, string][] = [ ['Maglietta', 'printed on the chest of a white cotton t-shirt, worn by a model, studio photo'], ['Tazza', 'printed on a white ceramic mug on a wooden table, soft light'], ['Borsa', 'printed on a natural canvas tote bag hanging on a hook'], ['Poster', 'as a framed poster on a bright living room wall'], ['Cartellone', 'on a large city billboard at dusk'], ['Custodia', 'printed on a phone case held in a hand'] ] -const FORMATI_SOCIAL: [string, string, number][] = [['1:1 Post', '1:1', 1080], ['4:5 Instagram', '4:5', 1080], ['9:16 Storia', '9:16', 1080], ['16:9 Video', '16:9', 1080], ['2:3 Pinterest', '2:3', 1080]] +export const FORMATI_SOCIAL: [string, string, number][] = [['1:1 Post', '1:1', 1080], ['4:5 Instagram', '4:5', 1080], ['9:16 Storia', '9:16', 1080], ['16:9 Video', '16:9', 1080], ['2:3 Pinterest', '2:3', 1080]] const STILI_TESTO: [string, string][] = [ ['Titolo in alto', 'as a huge bold sans-serif headline across the top'], ['Neon', 'as a glowing neon sign effect'], ['Lettere 3D', 'as big 3D chrome letters'], ['Calligrafia', 'in elegant hand-drawn brush calligraphy'], ['Giornale', 'as a newspaper headline'], ['Adesivo', 'as a cute handwritten sticker label'] @@ -351,18 +351,18 @@ export const WORKFLOW_BASE: Workflow[] = [ // ---- sticker e icone di qualsiasi cosa (non solo persone) -const STILI_STICKER: [string, string][] = [ +export const STILI_STICKER: [string, string][] = [ ['Kawaii', 'a cute kawaii chibi style with big shiny eyes and soft pastel colors'], ['Fumetto', 'a bold cartoon style with thick black outlines and flat bright colors'], ['Vettoriale', 'a clean flat vector illustration with simple geometric shapes'], ['Acquerello', 'a delicate watercolor style with soft bleeding colors'], ['Pixel art', 'a detailed 32-bit pixel art style'], ['Creta 3D', 'a cute 3D clay render with soft lighting'], ['Retro anni \'70', 'a retro 1970s style with warm earthy colors and rounded shapes'], ['Neon', 'a glowing neon line style on dark colors'] ] -const AZIONI_STICKER: [string, string][] = [ +export const AZIONI_STICKER: [string, string][] = [ ['Saluta', 'waving hello with a big smile'], ['Dorme', 'sleeping peacefully with little "z" bubbles'], ['Festeggia', 'celebrating with confetti and a party hat'], ['Corre', 'running fast with motion lines'], ['Mangia', 'happily eating a snack'], ['Cuore', 'holding a big red heart'], ['Occhiali da sole', 'wearing cool sunglasses, looking confident'], ['Arrabbiato', 'looking grumpy and angry with a little storm cloud'] ] -const E2 = (etichetta: string, prompt: string): PassoWf => E(etichetta, prompt, { crea: { formato: '1:1', ris: 1024 }, trasparente: true }) +export const E2 = (etichetta: string, prompt: string): PassoWf => E(etichetta, prompt, { crea: { formato: '1:1', ris: 1024 }, trasparente: true }) WORKFLOW_BASE.push( { id: 'sticker-tutto', nome: 'Sticker di qualsiasi cosa', categoria: 'sticker', icona: 'sticker', modo: 'serie', foto: false, rif: 'si', rifNome: 'una foto di riferimento (facoltativa)', diff --git a/src/renderer/src/ricetteV2.ts b/src/renderer/src/ricetteV2.ts new file mode 100644 index 0000000..5b06ed4 --- /dev/null +++ b/src/renderer/src/ricetteV2.ts @@ -0,0 +1,194 @@ +// Le sei ricette della GUI v2 «Orbita»: Foglio · Mondi · Mix · Set · Formati · Rinnova. +// Ognuna sostituisce un gruppo di ricette vecchie (che restano in `WORKFLOW_BASE` solo per leggere i PNG già fatti). +// Tutto passa dai segnaposti e dalle scelte a pulsanti: niente elenchi lunghi di testo. +import { componiCamera } from './componenti/Camera' +import { + AZIONI_STICKER, CAMERA_TUA, E, E2, EPOCHE, ESPRESSIONI, FOTO, MONDI, MOCKUP, ARMADIO, METEO, POSA_TUA_IN, POSE, STAGIONI, STILI, STILI_STICKER, + posaTua, righe, vistaTua +} from './ricetteBase' +import type { Ctx, Ingresso, PassoWf, Workflow } from './workflow' + +/** quante immagini fare: 4, 6 o 9 */ +const QUANTE: Ingresso = { id: 'quante', tipo: 'scelta', nome: 'Quante', predefinita: '6', opzioni: [{ id: '4', nome: '4' }, { id: '6', nome: '6' }, { id: '9', nome: '9' }] } +const quante = (c: Ctx): number => Math.max(1, Math.min(12, Number(c.v.quante) || 6)) +/** le etichette di una ricetta devono essere tutte diverse (sono le chiavi dei passi): se due coincidono, la seconda prende un numero */ +const univoche = (passi: PassoWf[]): PassoWf[] => { + const viste = new Map() + return passi.map((p) => { + const n = (viste.get(p.etichetta) || 0) + 1 + viste.set(p.etichetta, n) + return n === 1 ? p : { ...p, etichetta: `${p.etichetta} ${n}` } + }) +} +const uguale = 'Keep exactly the same face, hairstyle, outfit, colors and art style.' + +// --------------------------------------------------------------------------------------------------------------- +// 1 · Foglio: da un soggetto, una serie che varia UNA dimensione +// --------------------------------------------------------------------------------------------------------------- +const VISTE_FOGLIO: [string, string][] = [ + ['Davanti', 'seen from the front'], ['Tre quarti', 'rotated 45 degrees to the right (three-quarter view)'], ['Di lato', 'in a perfect side profile view'], + ['Tre quarti dietro', 'seen from behind and 45 degrees to the left'], ['Dietro', 'seen directly from behind'], ['Volto', 'as a close-up portrait of the face, looking at the camera'], + ['Dall\'alto', 'seen from a high angle looking down'], ['Dal basso', 'seen from a low angle looking up'], ['Figura intera', 'full body, centered, slightly turned'] +] +const ETA = [8, 20, 30, 40, 50, 60, 70, 80, 90] +const ANGOLI: [string, { az: number; el: number }][] = [ + ['45° destra', { az: 45, el: 0 }], ['90° destra', { az: 90, el: 0 }], ['135° destra', { az: 135, el: 0 }], ['Dietro', { az: 180, el: 0 }], + ['135° sinistra', { az: -135, el: 0 }], ['90° sinistra', { az: -90, el: 0 }], ['45° sinistra', { az: -45, el: 0 }], ['Dall\'alto', { az: 0, el: 35 }], ['Dal basso', { az: 0, el: -25 }] +] +const FOGLIO_VARIA = [ + { id: 'viste', nome: 'Viste' }, { id: 'pose', nome: 'Pose' }, { id: 'emozioni', nome: 'Emozioni' }, { id: 'eta', nome: 'Età' }, + { id: 'vestiti', nome: 'Vestiti' }, { id: 'azioni', nome: 'Azioni' }, { id: 'giro', nome: 'Giro 360°' } +] +const foglio: Workflow = { + id: 'r-foglio', nome: 'Foglio', categoria: 'ricette', icona: 'serie', modo: 'serie', foto: true, rif: 'no', + descrizione: 'Da una foto fa una serie coerente: la stessa persona o cosa che cambia una sola dimensione (viste, pose, emozioni, età, vestiti, azioni o un giro attorno).', + nota: 'Con i LoRA «Coerenza», «Vestiti» e «Camera» il risultato è più fedele.', + ingressi: [{ id: 'varia', tipo: 'scelta', nome: 'Cosa cambia', predefinita: 'viste', opzioni: FOGLIO_VARIA }, QUANTE, CAMERA_TUA, POSA_TUA_IN], + passi: (c) => { + const n = quante(c) + const sfondo = 'Plain white background, flat even lighting.' + const per = (voci: [string, string][], fn: (d: string) => string, extra: Partial = {}): PassoWf[] => voci.slice(0, n).map(([e, d]) => E(e, fn(d), extra)) + let base: PassoWf[] + switch (c.v.varia || 'viste') { + case 'pose': base = per(POSE, (d) => `Show the same character from ${FOTO} ${d}. ${uguale} The full body is visible.`); break + case 'emozioni': base = per([...ESPRESSIONI, ['Neutro', 'a calm neutral look']], (d) => `Change the facial expression of the person in ${FOTO} to ${d}. Keep the same identity, hairstyle, clothing, pose and background.`, { lora: ['coerenza'] }); break + case 'eta': base = ETA.slice(0, n).map((a) => E(`${a} anni`, `Show the same person from ${FOTO} as they would look at age ${a}. Keep the identity recognizable, with the same pose, framing and background, and age-appropriate hair, skin and clothes.`)); break + case 'vestiti': base = per([...ARMADIO, ['Pigiama', 'cozy pajamas']], (d) => `Dress the person in ${FOTO} in ${d}. Keep their face, hair, hands, pose and the background exactly the same.`, { lora: ['vestiti'] }); break + case 'azioni': base = per([...AZIONI_STICKER, ['Beve un caffè', 'enjoying a cup of coffee']], (d) => `Show the same character from ${FOTO} ${d}. ${uguale} The full body is visible.`); break + case 'giro': base = ANGOLI.slice(0, n).map(([e, pos]) => E(e, componiCamera({ ...pos, dist: 0 }, c.camera) || `Show the subject of ${FOTO} from another angle. Keep the same subject, outfit, colors and lighting.`, { lora: c.camera ? ['camera'] : [] })); break + default: base = per(VISTE_FOGLIO, (d) => `Show the same character from ${FOTO} in a neutral standing A-pose, ${d}. ${uguale} ${sfondo}`) + } + return univoche([...base, ...vistaTua(c, `Keep the same subject, outfit, colors and lighting. ${sfondo}`), ...posaTua(c, `${sfondo} Full body visible.`)]) + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 2 · Mondi: «E se…?» — la stessa scena in più mondi +// --------------------------------------------------------------------------------------------------------------- +const LUOGHI: [string, string][] = [...MONDI, ['Città sulle nuvole', 'in a city built on the clouds at sunset']] +const EPOCHE9: [string, string][] = [...EPOCHE, ['Preistoria', 'the Stone Age: caves, furs, a wild primeval landscape']] +const STAGIONI_METEO: [string, string][] = [...STAGIONI, ...METEO] +const CAMBIA_MONDI = [{ id: 'luogo', nome: 'Luogo' }, { id: 'epoca', nome: 'Epoca' }, { id: 'stagione', nome: 'Stagione' }, { id: 'stile', nome: 'Stile' }, { id: 'tutto', nome: 'Tutto' }] +const mondi: Workflow = { + id: 'r-mondi', nome: 'Mondi', categoria: 'ricette', icona: 'situazione', modo: 'serie', foto: true, rif: 'no', + descrizione: '«E se…?»: la stessa foto in più mondi: luoghi, epoche, stagioni, stili. Il soggetto resta riconoscibile. Puoi aggiungere un tuo «E se…».', + ingressi: [ + { id: 'cambia', tipo: 'scelta', nome: 'Cosa cambia', predefinita: 'luogo', opzioni: CAMBIA_MONDI }, QUANTE, + { id: 'efse', tipo: 'testo', nome: 'E se… (facoltativo)', esempio: 'fosse in una città costruita sulle nuvole', opzionale: true } + ], + passi: (c) => { + const n = quante(c) + const luogo = LUOGHI.map(([e, d]): PassoWf => E(e, `Place the subject of ${FOTO} ${d}. Keep the subject exactly the same, with matching light, shadows and perspective.`)) + const epoca = EPOCHE9.map(([e, d]): PassoWf => E(e, `Reimagine ${FOTO} in ${d}. Keep the subject's face and identity recognizable and the composition similar.`)) + const stagione = STAGIONI_METEO.map(([e, d]): PassoWf => E(e, `Show the exact same scene from ${FOTO} ${d}. Keep the composition and subjects identical; only the ${STAGIONI.some((s) => s[0] === e) ? 'season' : 'weather'} changes.`, { lora: ['coerenza'] })) + const stile = STILI.map(([e, d]): PassoWf => E(e, `Transform ${FOTO} into ${d}. Keep the composition and the subject recognizable.`)) + const liste = { luogo, epoca, stagione, stile } + const scelta = c.v.cambia || 'luogo' + let tutti: PassoWf[] + if (scelta === 'tutto') { + // un po' di ognuno, a giro, finché basta + tutti = [] + for (let k = 0; tutti.length < n && k < 12; k++) for (const l of [luogo, epoca, stagione, stile]) if (l[k] && tutti.length < n + 3) tutti.push(l[k]) + } else tutti = liste[scelta as keyof typeof liste] || luogo + const libero = (c.v.efse || '').trim() + const speciali = libero ? [E('E se…', `Reimagine ${FOTO} as if ${libero}. Keep the main subject recognizable.`)] : [] + return univoche([...speciali, ...tutti.slice(0, libero ? Math.max(1, n - 1) : n)]) + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 3 · Mix: più foto con un ruolo (si usa la tela di Modifica; i due modi sono «Unisci» e «Come l'esempio») +// --------------------------------------------------------------------------------------------------------------- +const mix: Workflow = { + id: 'r-mix', nome: 'Mix', categoria: 'ricette', icona: 'foto', modo: 'speciale', foto: false, rif: 'serve', rifNome: 'le foto da unire', + descrizione: 'Unisce più foto, ognuna con il suo ruolo (base, persona, oggetto, vestito) in una immagine nuova, oppure rifà un esempio con i dettagli delle tue foto («Stile da copiare»).', + ingressi: [], + passi: () => [E('Foto nuova', 'Crea una immagine nuova partendo dalle foto di riferimento, unendo ognuna con il suo ruolo.')] +} + +// --------------------------------------------------------------------------------------------------------------- +// 4 · Set: una serie di cose con stile coerente, sfondo trasparente di serie +// --------------------------------------------------------------------------------------------------------------- +const TIPI_SET: Record = { + sticker: 'A die-cut sticker of {x}, in {s}. Thick white die-cut outline, the subject centered and fully visible, no text, on a plain background.', + icone: 'An app icon of {x}, in {s}. Centered, a single object, consistent style and palette, no text, plain background.', + loghi: 'A clean modern logo mark for {x}, in {s}. Simple, memorable, centered, vector look, no extra text, plain background.', + mascotte: 'A friendly mascot character of {x}, in {s}. Full body, consistent character design, centered, no extra text, plain background.' +} +const STILI_SET = [ + ...STILI_STICKER.slice(0, 6).map(([e]) => e), 'Piatto' +].filter((x, i, a) => a.indexOf(x) === i) +const descrizioneStile = (id: string): string => (id === 'Piatto' ? 'a flat design with rounded corners and a soft shadow' : STILI_STICKER.find(([e]) => e === id)?.[1] || STILI_STICKER[0][1]) +const set: Workflow = { + id: 'r-set', nome: 'Set', categoria: 'ricette', icona: 'sticker', modo: 'serie', foto: false, rif: 'si', rifNome: 'una foto di riferimento (facoltativa)', + descrizione: 'Una serie di cose nello stesso stile, tutte con lo sfondo trasparente: sticker, icone, loghi o mascotte. Scrivi un soggetto per riga.', + ingressi: [ + { id: 'tipo', tipo: 'scelta', nome: 'Cosa', predefinita: 'sticker', opzioni: [{ id: 'sticker', nome: 'Sticker' }, { id: 'icone', nome: 'Icone' }, { id: 'loghi', nome: 'Loghi' }, { id: 'mascotte', nome: 'Mascotte' }] }, + { id: 'stile', tipo: 'scelta', nome: 'Stile', predefinita: 'Kawaii', opzioni: STILI_SET.map((s) => ({ id: s, nome: s })) }, + { id: 'soggetti', tipo: 'righe', nome: 'Un soggetto per riga', esempio: 'una pizza\nun gatto astronauta\nuna chitarra elettrica\nun fiore di loto' } + ], + passi: (c) => { + const modello = TIPI_SET[c.v.tipo || 'sticker'] || TIPI_SET.sticker + const stile = descrizioneStile(c.v.stile || 'Kawaii') + const coi = c.nRif > 0 ? ' Match the character and style of the reference .' : '' + return univoche(righe(c.v.soggetti).slice(0, 40).map((r) => E2(r.slice(0, 28), modello.replace('{x}', r).replace('{s}', stile) + coi))) + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 5 · Formati: un'idea → tutti i formati e i mockup +// --------------------------------------------------------------------------------------------------------------- +const FORMATI_V2: [string, string, number][] = [['1:1 Post', '1:1', 1080], ['4:5 Instagram', '4:5', 1080], ['9:16 Storia', '9:16', 1080], ['16:9 Video', '16:9', 1080], ['2:3 Pinterest', '2:3', 1080]] +const STAMPA: [string, string, number][] = [['Stampa verticale', '3:4', 1440], ['Stampa orizzontale', '4:3', 1440]] +const formati: Workflow = { + id: 'r-formati', nome: 'Formati', categoria: 'ricette', icona: 'social', modo: 'serie', foto: false, rif: 'si', rifNome: 'il tuo logo o una foto (facoltativi)', + descrizione: 'Un\'idea in tutti i formati (social, stampa) o su tanti oggetti: maglietta, tazza, borsa, poster, cartellone, custodia. Se aggiungi un logo, lo riporta uguale.', + ingressi: [ + { id: 'idea', tipo: 'testo', nome: 'Cosa vuoi vedere', esempio: 'una Vespa rossa in un vicolo di Positano al tramonto' }, + { id: 'uscita', tipo: 'scelta', nome: 'Dove', predefinita: 'social', opzioni: [{ id: 'social', nome: 'Social' }, { id: 'stampa', nome: 'Stampa' }, { id: 'mockup', nome: 'Mockup' }, { id: 'tutto', nome: 'Tutto' }] } + ], + passi: (c) => { + const idea = (c.v.idea || 'a beautiful scene').trim() + const dove = c.v.uscita || 'social' + const fmt = (l: [string, string, number][]): PassoWf[] => l.map(([e, f, ris]) => E(e, idea, { crea: { formato: f, ris } })) + const mock = (): PassoWf[] => + MOCKUP.map(([e, d]) => E(`Mockup · ${e}`, c.nRif > 0 + ? `A realistic product mockup photograph: the exact design from ${d}. Reproduce the design exactly as it is, with its colors and proportions.` + : `A realistic product mockup photograph: ${idea} ${d}. Reproduce the design exactly as described.`, { crea: { formato: '1:1', ris: 1024 } })) + return univoche(dove === 'stampa' ? fmt(STAMPA) : dove === 'mockup' ? mock() : dove === 'tutto' ? [...fmt(FORMATI_V2), ...fmt(STAMPA), ...mock()] : fmt(FORMATI_V2)) + } +} + +// --------------------------------------------------------------------------------------------------------------- +// 6 · Rinnova: una trasformazione prima/dopo, a scelta +// --------------------------------------------------------------------------------------------------------------- +const rinnova: Workflow = { + id: 'r-rinnova', nome: 'Rinnova', categoria: 'ricette', icona: 'bacchetta', modo: 'catena', foto: true, rif: 'no', + descrizione: 'Una foto, una trasformazione a scelta: restaura una foto vecchia, dal disegno al render, svuota e arreda una stanza, rifinisci i dettagli o colora. Ogni passo parte dal precedente.', + ingressi: [ + { id: 'cosa', tipo: 'scelta', nome: 'Cosa', predefinita: 'restaura', opzioni: [{ id: 'restaura', nome: 'Restaura' }, { id: 'render', nome: 'Dal disegno al render' }, { id: 'arreda', nome: 'Svuota e arreda' }, { id: 'rifinisci', nome: 'Rifinisci' }, { id: 'colora', nome: 'Colora' }] }, + { id: 'stile', tipo: 'testo', nome: 'Come arredarla (solo per «Svuota e arreda»)', esempio: 'un salotto moderno con divano grigio, tappeto e piante', opzionale: true } + ], + passi: (c) => { + switch (c.v.cosa || 'restaura') { + case 'render': return [ + E('Linea pulita', `Redraw ${FOTO} as clean, confident ink line art with consistent line weight on a white background. Keep the composition and every element.`), + E('A colori', 'Color this line art as a vibrant, polished digital illustration with soft shading. Keep the composition and every element.'), + E('Realistico', 'Turn this illustration into a photorealistic image with natural lighting and real textures. Keep the composition and every element.') + ] + case 'arreda': return [ + E('Stanza vuota', `Remove all the furniture, rugs and decorations from the room in ${FOTO}, leaving an empty room with clean walls and floor. Keep the architecture, windows, doors and lighting.`), + E('Arredata', `Furnish this empty room as ${(c.v.stile || 'a cozy modern living room').trim()}. Keep the architecture, windows, doors and lighting unchanged.`) + ] + case 'rifinisci': return [E('Nitida', `Enhance the image quality of ${FOTO}: sharp fine details, clean textures, natural skin, no noise or compression artifacts. Keep everything identical.`, { lora: ['coerenza'] })] + case 'colora': return [E('A colori', `Colorize ${FOTO} with natural, realistic colors, accurate skin tones and clothes. Keep everything else identical.`, { lora: ['coerenza'] })] + default: return [ + E('Pulita', 'Restore this old photo: remove scratches, dust, stains, noise and blur, and fix the faded contrast. Keep the people and the composition identical.', { lora: ['coerenza'] }), + E('A colori', 'Colorize this black and white photo with natural, realistic colors, accurate skin tones and clothes. Keep everything else identical.', { lora: ['coerenza'] }), + E('Nitida', 'Enhance the image quality: sharp fine details, clean textures, no noise or compression artifacts. Keep everything identical.', { lora: ['coerenza'] }) + ] + } + } +} + +export const WORKFLOW_V2: Workflow[] = [foglio, mondi, mix, set, formati, rinnova] diff --git a/src/renderer/src/riquadri.ts b/src/renderer/src/riquadri.ts index 9c73ffa..01fb027 100644 --- a/src/renderer/src/riquadri.ts +++ b/src/renderer/src/riquadri.ts @@ -11,6 +11,8 @@ export interface Elemento { nome: string tipo: TipoElemento manuale?: boolean + /** un clic sulla foto (frazioni 0–1): la cosa la trova SAM 2 da quel punto (il riquadro è solo un segno per l'occhio) */ + punto?: { x: number; y: number } } const PAROLE_PERSONA = /\b(person|people|man|men|woman|women|boy|girl|child|children|kid|baby|toddler|guy|lady|human|player|dancer|bride|groom)\b/i diff --git a/src/renderer/src/stato.ts b/src/renderer/src/stato.ts index f647b19..164db83 100644 --- a/src/renderer/src/stato.ts +++ b/src/renderer/src/stato.ts @@ -1,13 +1,14 @@ // 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 Download, type Famiglia, type Impostazioni, type InfoLora, type InfoMotore, type Lavoro, type LoraAttiva, type Opera, type Bordi, type Risorse, type StatoAggiornamento } from '@shared/tipi' +import { FAMIGLIE, type BoxRegione, type Download, type Famiglia, 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 { api } from './api' import type { Misura } from './componenti/Misura' // 'laboratorio' è sparito dalla rotaia (T7): T9 ha spostato le sue ricette nella scheda 'ricette' di Modifica. // I LoRA stanno dentro Opzioni (`SchedaOpzioni`). -export type Pagina = 'crea' | 'modifica' | 'album' | 'galleria' | 'impostazioni' +/** 'crea', 'modifica' e 'box' sono i tre modi dello Studio (con Ricette = modifica + modo 'ricette'); 'lora' ha la pagina sua */ +export type Pagina = 'crea' | 'modifica' | 'box' | 'album' | 'galleria' | 'lora' | 'impostazioni' export type SchedaOpzioni = 'generali' | 'lora' export type Qualita = 'bozza' | 'alta' | 'massima' /** Scrivi = tutta la foto, o solo la zona se ne hai disegnata una (si decide al momento dell'invio) */ @@ -48,6 +49,20 @@ export interface StatoCrea { campi: Record /** le dimensioni precise (se scelte): il modello lavora ai megapixel, poi si porta a questa misura */ misura: Misura + /** GUI v2: lo stile nuovo (id di `stili.ts`, '' = nessuno) e quanto forte: 0 leggero · 1 pieno · 2 estremo */ + stileId: string + intensita: 0 | 1 | 2 + /** GUI v2: lo sfondo (`trasparente` qui sotto resta il flag che il motore già conosce e va sempre d'accordo con questo), la tinta unita e come si guarda la trasparenza */ + sfondo: 'scena' | 'trasparente' | 'tinta' + tinta: string + anteSfondo: 'scacchi' | 'bianco' | 'nero' | 'colore' +} + +/** L'Editor box: i riquadri disegnati sulla tela di Crea (stesso formato, stessa misura) e la scena generale */ +export interface StatoBox { + scena: string + regioni: BoxRegione[] + selezionata: string | null } export interface StatoModifica { @@ -81,6 +96,8 @@ export interface StatoModifica { /** le azioni rapide sono idee che si sommano: quanta fantasia ha il modello nel mescolarle, e il piano che ne esce */ fantasia: 'fedele' | 'normale' | 'tanta' pianoIdee: { chiave: string; testo: string } | null + /** GUI v2: quanto forte è lo stile scelto fra le idee (0 leggero · 1 pieno · 2 estremo) */ + intensitaStile: 0 | 1 | 2 } export interface Avviso { @@ -100,6 +117,8 @@ interface Stato { loraAttive: LoraAttiva[] crea: StatoCrea modifica: StatoModifica + /** l'Editor box (modo Box dello Studio) */ + box: StatoBox codaAperta: boolean avvisi: Avviso[] /** l'opera aperta nel visore (galleria) */ @@ -118,6 +137,7 @@ interface Stato { setImp(i: Impostazioni): void setCrea(m: Partial): void setModifica(m: Partial): void + setBox(m: Partial): void setLoraAttive(l: LoraAttiva[]): void toggleLora(file: string, forza?: number): void avvisa(testo: string, tipo?: Avviso['tipo']): void @@ -145,12 +165,14 @@ const leggiLista = (k: string): T[] => { const CREA0: StatoCrea = { prompt: '', formato: '1:1', ris: 1024, mpLavoro: 0, quante: 1, seed: 0, casuale: true, trasparente: false, - riferimenti: [], preset: '', stile: '', campi: {}, misura: { tipo: 'auto' } + riferimenti: [], preset: '', stile: '', campi: {}, misura: { tipo: 'auto' }, + stileId: '', intensita: 1, sfondo: 'scena', tinta: '#ffffff', anteSfondo: 'scacchi' } +const BOX0: StatoBox = { scena: '', regioni: [], selezionata: null } const MOD0: StatoModifica = { base: null, versioni: [], riferimenti: [], esempio: null, modo: 'scrivi', prompt: '', mp: 1, qualita: 'alta', quante: 1, forza: 1, contesto: 0.6, ritaglia: true, segnaZona: false, riempi: false, sfuma: 10, allarga: 8, bordi: { sinistra: 0, sopra: 0, destra: 0, sotto: 0 }, trasparente: false, misura: { tipo: 'auto' }, formatoFusion: 'prima', - pianoEsempio: null, fantasia: 'normale', pianoIdee: null + pianoEsempio: null, fantasia: 'normale', pianoIdee: null, intensitaStile: 1 } /** le scelte salvate dalla 0.1 avevano i megapixel al posto della risoluzione; fino alla 1.0 c'era la qualità «turbo» (ora «alta») */ @@ -181,6 +203,7 @@ export const usaStato = create((set, get) => ({ loraAttive: leggiLista('dpi-lora'), crea: { ...daVecchio(leggiLocale('dpi-crea', CREA0)), riferimenti: [] }, modifica: daVecchioMod({ ...leggiLocale('dpi-modifica', MOD0), base: null, versioni: [], riferimenti: [], esempio: null }), + box: { ...BOX0, ...leggiLocale>('dpi-box', {}), selezionata: null }, codaAperta: false, avvisi: [], visore: null, @@ -189,7 +212,7 @@ export const usaStato = create((set, get) => ({ scarico: null, selettore: null, vai: (pagina) => set({ pagina }), - vaiOpzioni: (schedaOpzioni) => set({ pagina: 'impostazioni', schedaOpzioni }), + vaiOpzioni: (schedaOpzioni) => (schedaOpzioni === 'lora' ? set({ pagina: 'lora' }) : set({ pagina: 'impostazioni', schedaOpzioni })), setImp: (imp) => set({ imp }), setCrea: (m) => { const crea = { ...get().crea, ...m } @@ -211,6 +234,16 @@ export const usaStato = create((set, get) => ({ /* niente */ } }, + setBox: (m) => { + const box = { ...get().box, ...m } + set({ box }) + try { + const { selezionata: _s, ...resto } = box + localStorage.setItem('dpi-box', JSON.stringify(resto)) + } catch { + /* niente */ + } + }, setLoraAttive: (loraAttive) => { set({ loraAttive }) try { diff --git a/src/renderer/src/stili.ts b/src/renderer/src/stili.ts new file mode 100644 index 0000000..871328e --- /dev/null +++ b/src/renderer/src/stili.ts @@ -0,0 +1,82 @@ +// Gli stili della GUI v2: 28, in 4 scaffali, ognuno con un id stabile, un frammento di prompt in inglese e tre intensità. +// Le anteprime vere (immagini generate con Klein, stessa scena per tutti) arrivano con gli asset; finché non ci sono la tessera ha un gradiente e un'icona. +// Vale sia per Crea (si aggiunge al prompt) sia per Modifica (la categoria «Stili» trasforma la foto). + +export type Intensita = 0 | 1 | 2 +export const INTENSITA: { id: Intensita; nome: string; titolo: string }[] = [ + { id: 0, nome: 'Leggero', titolo: 'Un\'influenza lieve: la scena resta in primo piano' }, + { id: 1, nome: 'Pieno', titolo: 'Lo stile scelto, senza esagerare' }, + { id: 2, nome: 'Estremo', titolo: 'Tutto in quello stile, senza compromessi' } +] + +export interface Stile { + id: string + nome: string + scaffale: string + /** in inglese: è la lingua che il modello capisce meglio */ + frammento: string + /** gradiente di ripiego per la tessera e un'icona a emoji */ + fondo: string + icona: string +} + +const S = (id: string, nome: string, scaffale: string, frammento: string, a: string, b: string, icona: string): Stile => ({ + id, nome, scaffale, frammento, icona, fondo: `linear-gradient(135deg, ${a}, ${b})` +}) + +export const SCAFFALI: { id: string; nome: string }[] = [ + { id: 'foto', nome: 'Foto d\'oggi' }, + { id: 'disegnati', nome: 'Disegnati' }, + { id: '3d', nome: 'In 3D' }, + { id: 'grafica', nome: 'Grafica spaziale e oltre' } +] + +export const STILI: Stile[] = [ + // --- Foto d'oggi --- + S('cinema-anam', 'Cinema', 'foto', 'cinematic still, anamorphic lens flares, oval bokeh, rich color grade, 2.39:1 feel', '#0f2b46', '#e08a3c', '🎬'), + S('flash-y2k', 'Flash 2000', 'foto', 'direct on-camera flash, early-2000s compact digital camera look, harsh highlights, candid party snapshot', '#2b2b2b', '#ffe9a8', '📸'), + S('pellicola35', 'Pellicola', 'foto', 'shot on 35mm film, visible grain, soft halation, faded blacks', '#5a3b22', '#d9a066', '🎞️'), + S('moda-editoriale', 'Moda', 'foto', 'high-fashion editorial photography, studio strobes, bold color blocking, magazine cover composition', '#6b0f3a', '#f3b0c8', '👗'), + S('polaroid', 'Polaroid', 'foto', 'instant film photo, white frame, soft focus, washed warm tones, light leaks', '#c9a27a', '#fff2dc', '🖼️'), + S('notte-neon', 'Notte neon', 'foto', 'night street photography, wet asphalt reflections, magenta and cyan neon signage', '#150a3a', '#ff3df2', '🌃'), + S('documentario', 'Reportage', 'foto', 'gritty documentary photograph, available light, slightly desaturated, imperfect framing', '#3a3d40', '#9aa0a6', '📰'), + // --- Disegnati --- + S('anime-film', 'Anime', 'disegnati', 'high quality anime movie still, painterly backgrounds, soft cinematic lighting', '#2a6fdb', '#ffb7d5', '🌸'), + S('acquerello-film', 'Acquerello', 'disegnati', 'hand-painted watercolor animation background, soft gradients, gentle clouds', '#7ec8e3', '#fbe7c6', '🎨'), + S('manga-inchiostro', 'Manga', 'disegnati', 'black and white manga, screentones, dynamic inked lines', '#111111', '#e8e8e8', '✒️'), + S('gouache', 'Gouache', 'disegnati', 'contemporary gouache illustration, flat shapes, rich matte colors, subtle paper texture', '#d1495b', '#edae49', '🖌️'), + S('risograph', 'Risograph', 'disegnati', 'two-color risograph print, grainy overprint, misregistration, limited palette', '#ff4d8d', '#2d5bff', '🖨️'), + S('collage', 'Collage', 'disegnati', 'cut-paper collage, layered torn paper edges, mixed textures', '#e9c46a', '#e76f51', '✂️'), + S('linea', 'Linea unica', 'disegnati', 'minimal single continuous line drawing on white, a touch of color', '#f5f5f5', '#c9c9d6', '〰️'), + // --- In 3D --- + S('soft3d', '3D morbido', '3d', 'soft 3D render, rounded shapes, pastel studio light, subsurface scattering, friendly character look', '#ffb5e8', '#b5deff', '🧸'), + S('plastilina', 'Plastilina', '3d', 'stop-motion claymation, visible fingerprints, handmade imperfections', '#f4a261', '#2a9d8f', '🧱'), + S('vinile', 'Vinile', '3d', 'designer vinyl toy, glossy plastic, seam lines, studio product shot', '#7b2cbf', '#ff9e00', '🪀'), + S('diorama', 'Diorama', '3d', 'isometric miniature diorama, tilt-shift, clean cube base, soft shadows', '#06d6a0', '#118ab2', '🏝️'), + S('mattoncini', 'Mattoncini', '3d', 'scene built of interlocking plastic bricks and minifigures', '#e63946', '#ffd60a', '🧩'), + S('vetro', 'Vetro liquido', '3d', 'liquid glass and crystal, refractive translucent material, caustics, iridescent edges', '#3fe0ff', '#b388ff', '🔮'), + S('chrome', 'Cromo Y2K', '3d', 'chrome blobject, reflective metal, hyperreal studio HDRI, early-2000s futurism', '#8d99ae', '#edf2f4', '💿'), + // --- Grafica spaziale e oltre --- + S('cyberpunk', 'Cyberpunk', 'grafica', 'dense neon cityscape, holograms, rain, high contrast magenta/teal', '#00b8d4', '#d500f9', '🤖'), + S('synthwave', 'Synthwave', 'grafica', 'retro-futuristic 80s sunset grid, chrome lettering, purple-pink gradient sky', '#ff006e', '#3a0ca3', '🌆'), + S('aura', 'Aura', 'grafica', 'soft airbrushed aura gradients, grain, glowing blurred shapes, dreamy abstract', '#f72585', '#4cc9f0', '🫧'), + S('pixel', 'Pixel 32-bit', 'grafica', 'detailed 32-bit pixel art, limited palette, dithering', '#2d6a4f', '#95d5b2', '👾'), + S('spaziale60', 'Space age', 'grafica', '1960s space-age illustration, atomic shapes, retro NASA poster palette', '#e85d04', '#023e8a', '🚀'), + S('solarpunk', 'Solarpunk', 'grafica', 'solarpunk utopia, lush green architecture, warm sunlight, art nouveau curves', '#2d9a4a', '#ffd166', '🌿'), + S('svizzero', 'Poster svizzero', 'grafica', 'Swiss international typographic poster, strong grid, flat color, bold sans-serif', '#d00000', '#ffffff', '🔠') +] + +export const stileDa = (id: string): Stile | undefined => STILI.find((s) => s.id === id) + +/** la frase da aggiungere al prompt di Crea (descrive la scena, poi lo stile) */ +export function fraseStileCrea(s: Stile, i: Intensita): string { + if (i === 0) return `with a subtle ${s.frammento} influence` + if (i === 1) return `in this style: ${s.frammento}` + return `fully rendered as ${s.frammento}, extreme, uncompromising commitment to this style` +} + +/** l'istruzione di Modifica: trasforma la foto tenendo composizione e identità */ +export function frasiStileModifica(s: Stile, i: Intensita): string { + const forza = i === 0 ? 'Apply it subtly, keeping the original look mostly intact.' : i === 1 ? 'Apply it clearly.' : 'Apply it fully and boldly, with an extreme commitment to the style.' + return `Transform the image into this style: ${s.frammento}. Keep the composition, subjects, pose and identity unchanged. ${forza}` +} diff --git a/src/renderer/src/workflow.ts b/src/renderer/src/workflow.ts index 3ef7f15..b6d0646 100644 --- a/src/renderer/src/workflow.ts +++ b/src/renderer/src/workflow.ts @@ -69,6 +69,7 @@ export interface Workflow { } export const CATEGORIE_WF: { id: string; nome: string; icona: string; testo: string }[] = [ + { id: 'ricette', nome: 'Le sei ricette', icona: 'laboratorio', testo: 'Poche, potenti e unite: scegli una locandina' }, { id: 'personaggio', nome: 'Personaggio', icona: 'ritratto', testo: 'Una foto, tutte le pose, le espressioni, le età' }, { id: 'look', nome: 'Vestiti e look', icona: 'vestiti', testo: 'Armadio, acconciature, ruoli e costumi' }, { id: 'camera', nome: 'Camera e angoli', icona: 'camera', testo: 'Giri attorno al soggetto, inquadrature, viste' }, @@ -84,6 +85,7 @@ export const CATEGORIE_WF: { id: string; nome: string; icona: string; testo: str export { WORKFLOW_BASE } from './ricetteBase' import { E, righe, WORKFLOW_BASE } from './ricetteBase' +import { WORKFLOW_V2 } from './ricetteV2' /** il passo che nasce dal comando Camera di una ricetta (la posizione che hai scelto muovendo i cerchi) */ export const VISTA_TUA = 'La tua vista' @@ -139,10 +141,10 @@ export function inUtente(w: Workflow, id = 'u-' + crypto.randomUUID().slice(0, 8 /** tutte le ricette che si vedono: quelle di serie (meno le nascoste) e le tue */ export function elencoWorkflow(): Workflow[] { const { utente, nascoste } = usaRicette.getState() - return [...WORKFLOW_BASE.filter((w) => !nascoste.includes(w.id)), ...utente.map(daUtente)] + return [...WORKFLOW_V2.filter((w) => !nascoste.includes(w.id)), ...utente.map(daUtente)] } -export const workflowDa = (id: string): Workflow | undefined => elencoWorkflow().find((w) => w.id === id) || WORKFLOW_BASE.find((w) => w.id === id) +export const workflowDa = (id: string): Workflow | undefined => elencoWorkflow().find((w) => w.id === id) || WORKFLOW_V2.find((w) => w.id === id) || WORKFLOW_BASE.find((w) => w.id === id) /** il valore di partenza di ogni campo (gli esempi si scrivono da soli, si ritoccano) */ export function valoriIniziali(w: Workflow): Record { @@ -278,7 +280,7 @@ async function richiestaPasso(run: Run, i: number, baseCorrente: FotoBase | null const d = dimensioniRis(passo.crea.formato, Math.round((passo.crea.ris || 1024) * Math.sqrt(run.opzioni.mp))) return { ...comune, modalita: 'crea', prompt, larghezza: d.w, altezza: d.h, megapixel: (d.w * d.h) / 1048576, - immagini, seed: wf.id === 'formati' ? run.seed : -1 + immagini, seed: wf.id === 'formati' || wf.id === 'r-formati' ? run.seed : -1 } } return { diff --git a/test/box.mjs b/test/box.mjs new file mode 100644 index 0000000..543fda8 --- /dev/null +++ b/test/box.mjs @@ -0,0 +1,97 @@ +// Prova dell'Editor box (SENZA Electron né motore): disegna riquadri col mouse, scrivi i testi, scegli una disposizione, componi, +// e controlla la richiesta che arriva al motore (box, scena, prompt a parole, misura). +// npm run build, poi un server statico su out/renderer (porta 8765), poi: node test/box.mjs +import { chromium } from 'playwright' +import { readFileSync, mkdirSync } from 'node:fs' + +const url = process.argv[2] || 'http://localhost:8765/index.html' +const out = 'test/.out' +mkdirSync(out, { recursive: true }) +const b = await chromium.launch() +const pg = await b.newPage({ viewport: { width: 1480, height: 960 } }) +let errori = 0 +pg.on('pageerror', (e) => { errori++; console.log('[errore pagina]', e.message) }) +await pg.addInitScript(readFileSync('test/finto.js', 'utf8')) +await pg.goto(url) +const pausa = (ms) => pg.waitForTimeout(ms) +let male = 0 +const prova = (nome, ok, extra = '') => { console.log(ok ? 'OK ' : 'MALE', nome, extra); if (!ok) male++ } +await pausa(1200) + +await pg.click('.strip-modi button[data-modo="box"]') +await pausa(300) +prova('Box: la tela c\'è e non ci sono riquadri', (await pg.locator('[data-box-tela]').count()) === 1 && (await pg.locator('.box-regione').count()) === 0) + +// formato 16:9 per vedere la tela larga +await pg.click('.colonna.destra:visible button[data-formato="16:9"]') +await pausa(200) +const tela = await pg.locator('[data-box-tela]').boundingBox() +const trascina = async (fx0, fy0, fx1, fy1) => { + await pg.mouse.move(tela.x + tela.width * fx0, tela.y + tela.height * fy0) + await pg.mouse.down() + await pg.mouse.move(tela.x + tela.width * ((fx0 + fx1) / 2), tela.y + tela.height * ((fy0 + fy1) / 2), { steps: 4 }) + await pg.mouse.move(tela.x + tela.width * fx1, tela.y + tela.height * fy1, { steps: 4 }) + await pg.mouse.up() +} +await trascina(0.05, 0.1, 0.45, 0.9) +await trascina(0.55, 0.1, 0.95, 0.5) +prova('due riquadri disegnati', (await pg.locator('.box-regione:not(.disegno)').count()) === 2) + +// un clic senza trascinare non crea niente +await pg.mouse.click(tela.x + tela.width * 0.5, tela.y + tela.height * 0.95) +prova('un clic secco non crea un riquadro', (await pg.locator('.box-regione:not(.disegno)').count()) === 2) + +// il secondo: il testo (il clic secco ha deselezionato) +await pg.locator('.box-scheda .testa').nth(1).click() +await pg.locator('.box-scheda.sel textarea').fill('un cane che dorme') +await pg.locator('.box-scheda.sel button[data-id="tipo-oggetto"]').click() +// il primo +await pg.locator('.box-scheda .testa').first().click() +await pg.locator('.box-scheda.sel textarea').fill('una ragazza con un cappotto rosso') +await pg.locator('.box-scheda.sel button[data-id="tipo-persona"]').click() +prova('non propone un blocco dei pixel che il motore non supporta', (await pg.locator('[data-id="rigidita-blocca"]:enabled').count()) === 0) +await pg.fill('.colonna.destra:visible textarea >> nth=0', 'al tramonto, colori caldi') +await pg.screenshot({ path: `${out}/b1-box.png` }) + +await pg.click('button[data-azione="componi"]') +await pausa(500) +const q = await pg.evaluate(() => window.__ultima) +prova('richiesta: modalità crea con box', q.modalita === 'crea' && Array.isArray(q.box) && q.box.length === 2, JSON.stringify(q.box?.map((r) => [r.tipo, r.rigidita]))) +prova('richiesta: la scena', q.promptScena === 'al tramonto, colori caldi') +prova('richiesta: il prompt descrive i riquadri a parole', /ragazza con un cappotto rosso/.test(q.prompt) && /cane che dorme/.test(q.prompt) && /left side/.test(q.prompt) && /right side|top-right/.test(q.prompt), q.prompt.slice(0, 140)) +prova('richiesta: misura 16:9 coerente (multipli di 16)', q.larghezza % 16 === 0 && Math.abs(q.larghezza / q.altezza - 16 / 9) < 0.06, `${q.larghezza}×${q.altezza}`) +prova('richiesta: niente espansione del prompt', q.potenzia === false) + +// il lavoro è in corso: la tela mostra l'anteprima, e c'è il ritorno ai riquadri +prova("in corso: c'è «Torna ai riquadri»", (await pg.locator('button[data-azione="torna-riquadri"]').count()) === 1) +await pg.click('button[data-azione="torna-riquadri"]') +prova('tornati ai riquadri', (await pg.locator('.box-regione:not(.disegno)').count()) === 2) +await pg.screenshot({ path: `${out}/b2-box-in-corso.png` }) + +// disposizioni: tre colonne tiene i testi di prima +await pausa(300) +await pg.click('.strip-modi button[data-modo="box"]') +await pg.click('button[data-disposizione="tre"]') +await pausa(200) +prova('disposizione «tre colonne»: 3 riquadri', (await pg.locator('.box-regione:not(.disegno)').count()) >= 3) +await pg.screenshot({ path: `${out}/b3-box-tre.png` }) + +// riquadri senza testo: l'invio si ferma con un avviso +await pg.evaluate(() => { window.__ultimaPrima = window.__ultima }) +await pg.click('button[data-azione="componi"]') +await pausa(300) +prova('l\'avviso «ogni riquadro ha bisogno di un testo» compare', (await pg.locator('.avviso.errore:has-text("riquadro")').count()) >= 1) + +// i tasti: Canc toglie il selezionato +const prima = await pg.locator('.box-regione:not(.disegno)').count() +await pg.locator('.box-regione:not(.disegno)').first().click({ position: { x: 5, y: 5 } }) +await pg.keyboard.press('Delete') +prova('Canc elimina il riquadro selezionato', (await pg.locator('.box-regione:not(.disegno)').count()) === prima - 1) +await pg.click('.strip-modi button[data-modo="crea"]') +await pausa(200) +await pg.click('.strip-modi button[data-modo="box"]') +prova('i riquadri restano cambiando modo', (await pg.locator('.box-regione:not(.disegno)').count()) === prima - 1) + +await b.close() +console.log(male || errori ? `PROBLEMI: ${male} prove, ${errori} errori` : 'TUTTO BENE') +process.exit(male || errori ? 1 : 0) diff --git a/test/browser-prova.mjs b/test/browser-prova.mjs new file mode 100644 index 0000000..705c832 --- /dev/null +++ b/test/browser-prova.mjs @@ -0,0 +1,36 @@ +// Harness locale: serve SOLO la build, su una porta libera, e chiude Chromium/server anche se una prova fallisce. +import { createServer } from 'node:http' +import { readFile } from 'node:fs/promises' +import { resolve, extname, sep } from 'node:path' +import { chromium } from 'playwright' +export async function serverBuild() { + const root = resolve('out/renderer') + const tipi = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css', '.webp': 'image/webp', '.png': 'image/png', '.woff2': 'font/woff2' } + const server = createServer(async (req, res) => { + try { + const file = resolve(root, '.' + decodeURIComponent(new URL(req.url, 'http://localhost').pathname)) + if (!file.startsWith(root + sep)) { res.writeHead(403).end(); return } + const data = await readFile(file) + res.writeHead(200, { 'Content-Type': tipi[extname(file)] || 'application/octet-stream' }).end(data) + } catch { res.writeHead(404).end() } + }) + await new Promise((ok) => server.listen(0, '127.0.0.1', ok)) + return { url: `http://127.0.0.1:${server.address().port}/index.html`, close: () => new Promise((ok) => server.close(ok)) } +} +export async function provaBrowser(fn, init) { + const server = await serverBuild() + let browser + try { + browser = await chromium.launch() + const pg = await browser.newPage({ viewport: { width: 1480, height: 1000 }, reducedMotion: 'reduce' }) + pg.setDefaultTimeout(8000) + const errori = [] + pg.on('pageerror', (e) => errori.push(e.message)) + 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 fn(pg) + if (errori.length) throw Error('Errori pagina: ' + errori.join('\n')) + } finally { await browser?.close(); await server.close() } +} diff --git a/test/controlli.mjs b/test/controlli.mjs new file mode 100644 index 0000000..15ce53d --- /dev/null +++ b/test/controlli.mjs @@ -0,0 +1,101 @@ +// Prova del catalogo dei controlli di Modifica (controlli.ts) e dei 28 stili: ogni valore produce una frase non vuota, in inglese, +// con al massimo una foto di riferimento; i valori stanno nell'id dell'idea e tornano uguali; i controlli spenti non producono nulla. +import assert from 'node:assert/strict' +import { buildSync } from 'esbuild' +import { createRequire } from 'node:module' +import { resolve } from 'node:path' +import { mkdirSync } from 'node:fs' +mkdirSync('test/.out', { recursive: true }) +buildSync({ + stdin: { contents: "export * from './src/renderer/src/controlli'; export * from './src/renderer/src/stili'; export { CATEGORIE } from './src/renderer/src/rapide'", resolveDir: process.cwd(), loader: 'ts' }, + bundle: true, platform: 'node', format: 'cjs', alias: { '@shared': resolve('src/shared') }, outfile: 'test/.out/controlli.cjs' +}) +const m = createRequire(import.meta.url)('./.out/controlli.cjs') +const { CONTROLLI, controlliDi, ideaDa, valoreDa, conControllo, scriviMulti, leggiMulti, STILI, SCAFFALI, CATEGORIE } = m + +// ---- ogni controllo, ogni valore +let frasi = 0 +const buona = (f, dove) => { + assert.ok(f && f.trim().length > 12, `${dove}: frase troppo corta`) + assert.ok((f.match(//g) || []).length <= 1, `${dove}: più di una foto di riferimento`) + assert.ok(!/[àèéìòù]/i.test(f), `${dove}: la frase deve essere in inglese`) + frasi++ +} +const idCategorie = new Set(CATEGORIE.map((c) => c.id)) +for (const c of CONTROLLI) { + assert.ok(idCategorie.has(c.categoria), `${c.id}: categoria ${c.categoria} che non esiste`) + assert.ok(c.nome && c.tooltip && c.icona, `${c.id}: manca nome, tooltip o icona`) + if (c.tipo === 'cursore') { + assert.equal(c.compila(c.neutro), null, `${c.id}: il neutro deve essere spento`) + for (let v = c.min; v <= c.max; v++) if (v !== c.neutro) buona(c.compila(v)?.frase, `${c.id}=${v}`) + if (c.etichette) assert.equal(c.etichette.length, c.max - c.min + 1, `${c.id}: un'etichetta per ogni valore`) + } else { + assert.equal(c.compila({}), null, `${c.id}: vuoto = spento`) + const prima = c.parti[0] + if (prima.tipo === 'testo') buona(c.compila({ [prima.id]: 'CIAO' })?.frase, c.id) + for (const parte of c.parti) { + if (parte.tipo === 'testo') { + // il testo da solo basta (le altre righe sono facoltative) + const base = Object.fromEntries(c.parti.filter((x) => x.tipo === 'testo').map((x) => [x.id, 'CIAO "bello"'])) + buona(c.compila(base)?.frase, `${c.id}/testo`) + assert.ok(/"bello"/.test(c.compila(base).frase), `${c.id}: i testi tra virgolette restano identici`) + continue + } + const vistiId = new Set() + for (const o of parte.opzioni) { + assert.ok(!vistiId.has(o.id), `${c.id}/${parte.id}: id doppio ${o.id}`) + vistiId.add(o.id) + assert.ok(o.nome && o.icona && o.frase, `${c.id}/${parte.id}/${o.id}: manca nome, icona o frase`) + // le righe che non bastano da sole (es. materiale senza capo) si provano con una base che le attiva + const base = c.id === 'capo' && parte.id !== 'capo' ? { capo: 'giacca' } : {} + const testi = c.parti.filter((x) => x.tipo === 'testo') + const extra = Object.fromEntries(testi.map((x) => [x.id, 'CIAO'])) + buona(c.compila({ ...base, ...extra, [parte.id]: o.id })?.frase, `${c.id}/${parte.id}/${o.id}`) + } + } + } +} + +// ---- il valore sta nell'id dell'idea e torna uguale +const eta = CONTROLLI.find((c) => c.id === 'eta') +const luce = CONTROLLI.find((c) => c.id === 'luce') +let idee = conControllo([{ id: 'altra' }], eta, '10') +assert.equal(idee.length, 2) +assert.equal(valoreDa(eta, idee), '10') +idee = conControllo(idee, eta, '-20') +assert.equal(idee.length, 2, 'cambiare il valore sostituisce, non aggiunge') +assert.equal(valoreDa(eta, idee), '-20') +assert.match(idee.find((r) => r.id.startsWith('ctl-eta:')).prompt, /younger/) +idee = conControllo(idee, eta, '0') +assert.equal(idee.length, 1, 'il neutro toglie l\'idea') +assert.equal(valoreDa(eta, idee), '0') +const v = { dir: 'tl', qualita: 'dura', colore: '#ec4899' } +idee = conControllo(idee, luce, scriviMulti(v)) +assert.deepEqual(leggiMulti(valoreDa(luce, idee)), v, 'i valori multipli tornano uguali') +assert.match(ideaDa(luce, scriviMulti(v)).prompt, /upper left.*hard direct light.*pink/s) +assert.equal(ideaDa(luce, ''), null) +// un testo con ; e = non deve rompere la lettura +const t = CONTROLLI.find((c) => c.id === 'titolo') +assert.equal(leggiMulti(scriviMulti({ testo: 'a;b=c', dove: 'c' })).testo, 'a;b=c') +assert.ok(controlliDi('luce').length >= 2 && controlliDi('persone').length >= 3) +assert.ok(ideaDa(t, scriviMulti({ testo: 'CIAO', dove: 't', come: 'meme' })).prompt.includes('"CIAO" across the top')) + +// ---- gli stili: 28, 4 scaffali da 7, id unici, frammenti in inglese e le tre intensità +assert.equal(STILI.length, 28) +assert.equal(new Set(STILI.map((s) => s.id)).size, 28, 'id degli stili doppi') +for (const sc of SCAFFALI) assert.equal(STILI.filter((s) => s.scaffale === sc.id).length, 7, `scaffale ${sc.id}`) +for (const s of STILI) { + assert.ok(s.frammento.length > 20 && s.nome && s.icona && s.fondo, `stile ${s.id} incompleto`) + for (const i of [0, 1, 2]) { buona(m.fraseStileCrea(s, i), `stile ${s.id} Crea ${i}`); buona(m.frasiStileModifica(s, i), `stile ${s.id} Modifica ${i}`) } +} +assert.notEqual(m.fraseStileCrea(STILI[0], 0), m.fraseStileCrea(STILI[0], 2), 'le intensità devono differire') +// Le scelte non sono più campi «…» da completare: tutte le categorie hanno controlli tipizzati. +for (const id of ['capelli', 'animale', 'costume', 'luogo', 'meteo', 'stagione', 'inquadratura', 'mood', 'grading']) assert.ok(CONTROLLI.some((c) => c.id === id), `manca il controllo ${id}`) +console.log(`OK: ${CONTROLLI.length} controlli, ${frasi} frasi controllate, 28 stili in 4 scaffali, valori nell'id che tornano uguali`) + +// LocalStorage rovinato e input fuori catalogo non devono produrre prompt NaN o bloccare la pagina. +assert.equal(ideaDa(eta, 'NaN'), null) +assert.equal(ideaDa(eta, '1000'), null) +assert.deepEqual(leggiMulti('testo=%ZZ;senzaSeparatore'), {}) +assert.equal(ideaDa(luce, 'dir=inesistente'), null) +assert.ok(ideaDa(t, scriviMulti({ testo: 'CIAO "MONDO"' })).prompt.includes('CIAO "MONDO"'), 'i testi da disegnare restano letterali') diff --git a/test/foto.mjs b/test/foto.mjs index 2f01e7c..72a2c14 100644 --- a/test/foto.mjs +++ b/test/foto.mjs @@ -48,7 +48,7 @@ for (const [n, nome] of [['11-modifica', 'Modifica'], ['12-galleria', 'Galleria' } if (genera) { - await win.click('.rotaia button:has-text("Crea")') + await win.click('.strip-modi button[data-modo="crea"]') await win.fill('textarea', 'Una tazzina di caffè espresso su un tavolino di marmo in un bar di Napoli, mattina, luce morbida dalla vetrina, fotografia realistica, profondità di campo') await win.click('.segmenti button:has-text("Bozza")') await win.click('.formato:has-text("3:2")') diff --git a/test/gui-v2-flussi.mjs b/test/gui-v2-flussi.mjs new file mode 100644 index 0000000..78e63fd --- /dev/null +++ b/test/gui-v2-flussi.mjs @@ -0,0 +1,52 @@ +// Regressioni GUI v2: controlli composti, scelte modello e nessun campo fittizio. +import assert from 'node:assert/strict' +import { provaBrowser } from './browser-prova.mjs' +const caso = process.argv[2] || 'controlli' +await provaBrowser(async (pg) => { + const pannello = pg.locator('.studio-corpo .colonna.destra:visible') + if (caso === 'box') { + await pg.click('[data-modo="box"]') + await pg.click('[data-disposizione="uno"]') + await pannello.locator('textarea').first().fill('una piazza al tramonto') + await pg.locator('.box-scheda.sel textarea').fill('CIAO MONDO') + await pg.click('[data-id="tipo-testo"]') + await pannello.locator('h3.chiudibile').filter({ hasText: 'Stile' }).click() + await pannello.locator('[data-id="stile-cyberpunk"]').click() + await pg.click('[data-azione="componi"]') + await pg.waitForFunction(() => !!window.__ultima) + const q = await pg.evaluate(() => window.__ultima) + assert.match(q.promptScena, /neon cityscape/i, 'stile conservato dopo adattaRichiesta') + assert.match(q.box[0].prompt, /text.*"CIAO MONDO"/, 'il tipo Testo arriva al motore con istruzioni letterali') + assert.equal(await pg.locator('[data-id="rigidita-blocca"]:enabled').count(), 0, 'nessun controllo blocca senza implementazione engine') + await pg.screenshot({ path: 'test/.out/v2-box-contratto.png' }) + return + } + await pg.click('[data-modo="modifica"]') + if (caso === 'modello') { + assert.equal(await pannello.locator('[data-id="modello-klein4b"]').count(), 1, 'Modifica deve permettere di cambiare modello') + assert.match(await pg.locator('.pillola-motore').first().innerText(), /Klein/, 'fixture usa Klein') + assert.equal(await pg.locator('.strip-modi button[title^="La pila"]').count(), 1, 'LoRA accessibili anche con Klein') + return + } + await pg.click('.attrezzi button:has-text("Dal PC")') + await pg.click('[data-id="cat-vestiti"]') + const capo = pannello.locator('[data-controllo="capo"]') + await capo.getByRole('button', { name: 'Rosa', exact: true }).click() + assert.equal(await capo.getByRole('button', { name: 'Rosa', exact: true }).getAttribute('aria-pressed'), 'true', 'il colore resta selezionato prima del capo') + await capo.locator('[data-id="ctl-capo-giacca"]').click() + await capo.locator('[data-id="ctl-capo-seta"]').click() + await pannello.getByRole('radio', { name: 'Fedele', exact: true }).click() + await pannello.locator('button.primario.grande').click() + await pg.waitForFunction(() => !!window.__ultima) + const q = await pg.evaluate(() => window.__ultima) + assert.match(q.prompt, /jacket.*pink silk/, 'la richiesta contiene capo, colore e materiale') + assert.equal(q.modalita, 'modifica') + await pg.click('[data-id="cat-testo"]') + const titolo = pannello.locator('[data-controllo="titolo"]') + await titolo.getByRole('button', { name: 'In alto', exact: true }).click() + assert.equal(await titolo.getByRole('button', { name: 'In alto', exact: true }).getAttribute('aria-pressed'), 'true', 'posizione persistente prima del testo') + 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/gui.mjs b/test/gui.mjs new file mode 100644 index 0000000..5b2cd2c --- /dev/null +++ b/test/gui.mjs @@ -0,0 +1,21 @@ +// Foto veloci della GUI v2 (SENZA Electron né motore): Studio nei vari modi. +// npm run build, poi un server statico su out/renderer (porta 8765), poi: node test/gui.mjs +import { chromium } from 'playwright' +import { readFileSync, mkdirSync } from 'node:fs' + +const url = process.argv[2] || 'http://localhost:8765/index.html' +const out = 'test/.out' +mkdirSync(out, { recursive: true }) +const b = await chromium.launch() +const pg = await b.newPage({ viewport: { width: 1480, height: 1000 } }) +pg.on('pageerror', (e) => console.log('[errore pagina]', e.message)) +await pg.addInitScript(readFileSync('test/finto.js', 'utf8')) +await pg.goto(url) +const pausa = (ms) => pg.waitForTimeout(ms) +await pausa(1200) +await pg.click('button[data-id="genere-poster"]') +await pg.click('button[data-id="stile-synthwave"]') +await pg.click('button[data-id="sfondo-trasparente"]') +await pausa(300) +await pg.screenshot({ path: `${out}/g1-crea.png` }) +await b.close() diff --git a/test/interfaccia.mjs b/test/interfaccia.mjs index b6c3b67..064053e 100644 --- a/test/interfaccia.mjs +++ b/test/interfaccia.mjs @@ -16,16 +16,28 @@ const pausa = (ms) => pg.waitForTimeout(ms) const ultima = (campo) => pg.evaluate((c) => window.__ultima[c], campo) await pausa(1200) -await pg.click('.rotaia button:has-text("Modifica")') +await pg.click('.strip-modi button[data-modo="modifica"]') await pg.click('.attrezzi button:has-text("Dal PC")') await pausa(600) -await pg.fill('.colonna.destra textarea', 'sostituisci il cielo con un tramonto molto drammatico, nuvole rosse, luce dorata sulla casetta') +await pg.fill('.colonna.destra textarea:visible', 'sostituisci il cielo con un tramonto molto drammatico, nuvole rosse, luce dorata sulla casetta') await pg.screenshot({ path: `${out}/i1-casella-prompt.png` }) -await pg.click('.segmenti button:has-text("Estrai")') +await pg.click('button[data-id="modo-estrai"]') await pausa(1000) console.log('ricerca partita da sola?', await pg.evaluate(() => !!window.__testo), '(deve essere false)') await pg.screenshot({ path: `${out}/i2a-estrai-prima.png` }) +// Estrai in due clic (SAM 2): un clic sulla foto, poi «Ritaglia»; nessuna ricerca col modello di testo +{ + const fo = await pg.locator('.pers-foto').boundingBox() + await pg.mouse.click(fo.x + fo.width * 0.4, fo.y + fo.height * 0.5) + await pausa(300) + await pg.screenshot({ path: `${out}/i2b-estrai-punto.png` }) + await pg.click('button:has-text("Ritaglia")') + await pausa(500) + const q = await pg.evaluate(() => window.__ultima) + if (q.modalita !== 'segmenta' || !q.segmenta?.punti?.length || !q.segmenta.punti[0].positivo || q.segmenta.ritaglia !== true || q.immagini.length !== 1) throw new Error('ritaglio a due clic: richiesta sbagliata ' + JSON.stringify(q.segmenta)) + console.log('RITAGLIO a due clic: OK', JSON.stringify(q.segmenta)) +} await pg.click('button:has-text("Trova persone e oggetti")') await pausa(1200) console.log('ricerca col pulsante:', await pg.evaluate(() => window.__testo?.modalita), '(deve essere descrivi)') @@ -34,7 +46,7 @@ const f = await pg.locator('.pers-foto').boundingBox() await pg.mouse.click(f.x + f.width * 0.45, f.y + f.height * 0.7) await pausa(400) await pg.screenshot({ path: `${out}/i3-estrai-oggetto.png` }) -await pg.click('button:has-text("Estrai l\'oggetto")') +await pg.click('button:has-text("Ricrea l\'oggetto in studio")') await pausa(500) console.log('OGGETTO', await ultima('larghezza'), '×', await ultima('altezza'), '\n', await ultima('prompt')) @@ -42,7 +54,7 @@ await pg.mouse.click(f.x + f.width * 0.12, f.y + f.height * 0.85) await pg.click('.segmenti button:has-text("Disegnata")') await pausa(300) await pg.screenshot({ path: `${out}/i4-estrai-persona.png` }) -await pg.click('button:has-text("Estrai la persona")') +await pg.click('button:has-text("Ricrea la persona in studio")') await pausa(700) console.log('PERSONA', await ultima('larghezza'), '×', await ultima('altezza'), 'immagini:', (await pg.evaluate(() => window.__ultima.immagini.length)), '\n', await ultima('prompt')) await b.close() diff --git a/test/lora-ssr.mjs b/test/lora-ssr.mjs new file mode 100644 index 0000000..bbb8792 --- /dev/null +++ b/test/lora-ssr.mjs @@ -0,0 +1,25 @@ +// Rendering React lato server: niente browser, mouse, tastiera o motore. +import assert from 'node:assert/strict' +import { buildSync } from 'esbuild' +import { createRequire } from 'node:module' +import { resolve } from 'node:path' +import { mkdirSync } from 'node:fs' +const require = createRequire(import.meta.url) +const { renderToStaticMarkup } = require('react-dom/server') +const React = require('react') +// Zustand usa altrimenti lo snapshot iniziale nel rendering lato server. +React.useSyncExternalStore = (_subscribe, getSnapshot) => getSnapshot() +globalThis.window = { daprod: {} } +globalThis.localStorage = { getItem: () => null, setItem() {} } +mkdirSync('test/.out', { recursive: true }) +buildSync({ stdin: { contents: "export { Lora } from './src/renderer/src/pagine/Lora'; export { usaStato } from './src/renderer/src/stato'", resolveDir: process.cwd() }, bundle: true, platform: 'node', format: 'cjs', jsx: 'automatic', external: ['react', 'react-dom'], alias: { '@shared': resolve('src/shared') }, outfile: 'test/.out/lora-ssr.cjs' }) +const { Lora, usaStato } = require('./.out/lora-ssr.cjs') +usaStato.setState({ lore: [{ file: 'test.safetensors', nome: 'Test Qwen', forza: 1, parole: '', dimensione: 1, compatibile: 'si', aggiunta: 0 }], loraAttive: [{ file: 'test.safetensors', forza: 0.5 }] }) +usaStato.getInitialState = usaStato.getState +const html = renderToStaticMarkup(React.createElement(Lora)) +assert.match(html, /Pila LoRA/, 'la pila ordinata è separata dalla libreria') +assert.match(html, /Sposta prima/) +assert.match(html, /Sposta dopo/) +assert.match(html, /Forza di Test Qwen/) +assert.match(html, /min="-2"/) +console.log('OK: gestore LoRA renderizzato con pila, ordine e pesi accessibili') diff --git a/test/misura-run.mjs b/test/misura-run.mjs new file mode 100644 index 0000000..1d2a438 --- /dev/null +++ b/test/misura-run.mjs @@ -0,0 +1,30 @@ +// Esegue UN comando esplicito e registra tempo reale, esito e log, anche quando fallisce. +// Esempio: node test/misura-run.mjs node test/controlli.mjs +import { spawn } from 'node:child_process' +import { mkdirSync, appendFileSync, createWriteStream } from 'node:fs' +import { performance } from 'node:perf_hooks' +import { resolve } from 'node:path' +const args = process.argv.slice(2) +if (!args.length) throw new Error('Indica il comando da misurare') +const vietati = ['foto', 'setup', 'modifica', 'console', 'vetrina', 'impacchettata'] +if (args.some((x) => vietati.some((n) => x.replaceAll('\\', '/').endsWith(`test/${n}.mjs`)))) throw new Error('Test Electron con dati reali vietato: usa avvio-isolato.mjs') +const dir = resolve('test/.out/verifiche') +mkdirSync(dir, { recursive: true }) +const inizio = new Date().toISOString() +const nome = inizio.replaceAll(/[:.]/g, '-') + '-' + args.join('-').replaceAll(/[^a-zA-Z0-9_-]/g, '_') +const log = resolve(dir, nome + '.log') +const stream = createWriteStream(log) +const prima = performance.now() +// npm.cmd è uno script di Windows: il solo wrapper npm usa cmd; gli argomenti sono limitati ai task noti. +const npm = args[0] === 'npm' +if (npm && !['run typecheck', 'run build', 'test', 'run test:gui'].includes(args.slice(1).join(' '))) throw new Error('Task npm non ammesso') +const child = spawn(npm && process.platform === 'win32' ? 'cmd.exe' : args[0], npm && process.platform === 'win32' ? ['/d', '/s', '/c', 'npm.cmd', ...args.slice(1)] : args.slice(1), { stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }) +for (const s of [child.stdout, child.stderr]) s.on('data', (b) => { stream.write(b); process.stdout.write(b) }) +child.on('error', (e) => stream.write(e.stack + '\n')) +child.on('close', (code, signal) => { + const voce = { comando: args.join(' '), inizio, fine: new Date().toISOString(), wall_s: Number(((performance.now() - prima) / 1000).toFixed(3)), exit_code: code, signal, log } + appendFileSync(resolve(dir, 'durate.jsonl'), JSON.stringify(voce) + '\n') + stream.end() + console.log('\nMISURA ' + JSON.stringify(voce)) + process.exitCode = code ?? 1 +}) diff --git a/test/modifica.mjs b/test/modifica.mjs index 3f5b52b..66cdb2d 100644 --- a/test/modifica.mjs +++ b/test/modifica.mjs @@ -29,7 +29,7 @@ await win.click('.visore button:has-text("Modifica")') await pausa(1500) // Espandi a 16:9 -await win.click('.segmenti button:has-text("Espandi")') +await win.click('.modi-modifica button[data-id="modo-espandi"]') await win.click('.chip:has-text("+25% ovunque")') await win.fill('.colonna.destra textarea', 'the rest of a cozy Neapolitan café interior') await pausa(500) diff --git a/test/pila-lora.mjs b/test/pila-lora.mjs new file mode 100644 index 0000000..188a20d --- /dev/null +++ b/test/pila-lora.mjs @@ -0,0 +1,18 @@ +// Prove senza desktop: ordine, limiti della forza e compatibilità della pila LoRA. +import assert from 'node:assert/strict' +import { spostaLora, forzaLora, compatibilitaLora } from '../src/renderer/src/pilaLora.ts' +const pila = [{ file: 'a', forza: 1 }, { file: 'b', forza: 0.5 }, { file: 'c', forza: 0 }] +assert.deepEqual(spostaLora(pila, 'b', -1).map(x => x.file), ['b', 'a', 'c']) +assert.deepEqual(spostaLora(pila, 'b', 1).map(x => x.file), ['a', 'c', 'b']) +assert.deepEqual(spostaLora(pila, 'a', -1), pila) +assert.deepEqual(spostaLora(pila, 'manca', 1), pila) +assert.deepEqual(pila.map(x => x.file), ['a', 'b', 'c'], 'non muta lo stato precedente') +assert.equal(forzaLora(3), 2) +assert.equal(forzaLora(-3), -2) +assert.equal(forzaLora(NaN), 0) +assert.equal(compatibilitaLora({ compatibile: 'si' }, 'qwen'), 'si') +assert.equal(compatibilitaLora({ compatibile: 'si' }, 'klein'), 'no', 'il vecchio si significa solo Qwen') +assert.equal(compatibilitaLora({ compatibile: '?', famiglie: ['klein', 'klein-base'] }, 'klein'), 'si') +assert.equal(compatibilitaLora({ compatibile: 'si', famiglie: ['qwen'] }, 'klein'), 'no') +assert.equal(compatibilitaLora({ compatibile: '?', famiglie: [] }, 'klein'), '?') +console.log('OK: pila LoRA (ordine immutabile, forza, compatibilità esplicita)') diff --git a/test/ricette-v2.mjs b/test/ricette-v2.mjs new file mode 100644 index 0000000..c05009b --- /dev/null +++ b/test/ricette-v2.mjs @@ -0,0 +1,27 @@ +// Le sei ricette nuove, i loro passi veri e la lettura delle ricette storiche. +import assert from 'node:assert/strict' +import { buildSync } from 'esbuild' +import { createRequire } from 'node:module' +import { resolve } from 'node:path' +import { mkdirSync } from 'node:fs' +globalThis.window = { daprod: {} } +globalThis.localStorage = { getItem: () => null, setItem() {} } +mkdirSync('test/.out', { recursive: true }) +buildSync({ entryPoints: ['src/renderer/src/workflow.ts'], bundle: true, platform: 'node', format: 'cjs', jsx: 'automatic', alias: { '@shared': resolve('src/shared') }, outfile: 'test/.out/ricette-v2.cjs' }) +const { elencoWorkflow, workflowDa, valoriIniziali } = createRequire(import.meta.url)('./.out/ricette-v2.cjs') +const elenco = elencoWorkflow() +assert.deepEqual(elenco.map(w => w.nome), ['Foglio', 'Mondi', 'Mix', 'Set', 'Formati', 'Rinnova']) +for (const w of elenco) { + const v = valoriIniziali(w) + const passi = w.passi({ v, nRif: 2, camera: false }) + assert.ok(passi.length, `${w.nome}: almeno un passo`) + assert.equal(new Set(passi.map(p => p.etichetta)).size, passi.length, `${w.nome}: etichette univoche`) + assert.ok(passi.every(p => p.prompt.trim().length > 10)) + for (const g of w.ingressi.filter(g => g.tipo === 'scelta')) for (const o of g.opzioni) { + assert.ok(w.passi({ v: { ...v, [g.id]: o.id }, nRif: 2, camera: false }).length, `${w.nome}/${o.id}`) + } +} +assert.ok(workflowDa('turnaround'), 'il PNG vecchio trova ancora il suo workflow') +assert.ok(workflowDa('esempio'), 'il percorso Esempio rimane accessibile da Mix') +assert.ok(elenco.find(w => w.nome === 'Set').passi({ v: { soggetti: 'gatto\ncane', tipo: 'icone' }, nRif: 0, camera: false }).every(p => p.trasparente)) +console.log('OK: sei ricette, tutte le varianti producono passi, Set trasparente, compatibilità storica') diff --git a/test/ricette.mjs b/test/ricette.mjs index d32b431..e5918c4 100644 --- a/test/ricette.mjs +++ b/test/ricette.mjs @@ -19,17 +19,17 @@ await pausa(1200) // la rotaia non ha più il Laboratorio prova('rotaia senza Laboratorio', (await pg.locator('.rotaia button:has-text("Laboratorio")').count()) === 0) -await pg.click('.rotaia button:has-text("Modifica")') +await pg.click('.strip-modi button[data-modo="modifica"]') await pausa(300) -const voci = (await pg.locator('.attrezzi .segmenti button:visible').allInnerTexts()).map((x) => x.trim()).join('|') -prova(`selettore: ${voci}`, voci === 'Scrivi|Espandi|Ricette|Estrai') +const voci = (await pg.locator('.modi-modifica button:visible').allInnerTexts()).map((x) => x.trim()).join('|') +prova(`selettore: ${voci}`, voci === 'Scrivi|Espandi|Estrai') // il catalogo -await pg.click('.attrezzi .segmenti button:has-text("Ricette")') +await pg.click('.strip-modi button[data-modo="ricette"]') await pausa(500) await pg.screenshot({ path: `${out}/r1-catalogo.png` }) -prova('catalogo con le carte', (await pg.locator('.carta-wf').count()) > 20) -prova("Unisci foto e Come l'esempio nel catalogo", (await pg.locator('.carta-wf:has-text("Unisci foto")').count()) === 1 && (await pg.locator(".carta-wf:has-text(\"Come l'esempio\")").count()) === 1) +prova('catalogo: le sei ricette', (await pg.locator('.carta-wf').count()) === 6) +prova('Mix nel catalogo (Unisci e Come esempio ci stanno dentro)', (await pg.locator('.carta-wf:has-text("Mix")').count()) === 1) // una ricetta normale: colonna di destra con i passi e Avvia await pg.locator('.carta-wf').first().click() @@ -38,29 +38,28 @@ await pg.screenshot({ path: `${out}/r2-ricetta.png` }) prova('ricetta normale: colonna destra con Avvia', (await pg.locator('.colonna.destra button:has-text("Avvia")').count()) === 1) // Unisci foto: la griglia di Modifica e il pulsante di sempre -await pg.locator('.carta-wf:has-text("Unisci foto")').click() +await pg.locator('.carta-wf:has-text("Mix")').click() await pausa(500) await pg.screenshot({ path: `${out}/r3-unisci.png` }) prova('Unisci: c\'è il ritorno a "tutte le ricette"', (await pg.locator('.attrezzi button:has-text("tutte le ricette")').count()) === 1) prova('Unisci: pulsante "crea la foto nuova"', (await pg.locator('.colonna.destra button:has-text("crea la foto nuova")').count()) === 1) await pg.locator('.fusion button:has-text("Dal PC")').first().click() await pausa(500) -await pg.fill('.colonna.destra textarea', 'la ragazza di in una strada di Tokyo') +await pg.fill('.colonna.destra textarea:visible', 'la ragazza di in una strada di Tokyo') await pg.click('.colonna.destra button:has-text("crea la foto nuova")') await pausa(500) const q = await pg.evaluate(() => window.__ultima) prova('Unisci: accodato un lavoro "crea" con la foto', !!q && q.modalita === 'crea' && q.immagini?.length === 1) // Come l'esempio -await pg.click('.attrezzi button:has-text("tutte le ricette")') +await pg.click(".attrezzi button:has-text(\"Come l'esempio\")") await pausa(300) -await pg.locator(".carta-wf:has-text(\"Come l'esempio\")").click() await pausa(500) await pg.screenshot({ path: `${out}/r4-esempio.png` }) prova("Esempio: pulsante \"Crea come l'esempio\"", (await pg.locator(".colonna.destra button:has-text(\"Crea come l'esempio\")").count()) === 1) // tornando a Scrivi la pagina è quella di sempre -await pg.click('.attrezzi .segmenti button:has-text("Scrivi")') +await pg.click('.strip-modi button[data-modo="modifica"]') await pausa(300) prova('Scrivi: pulsante Modifica', (await pg.locator('.colonna.destra button:has-text("Modifica")').count()) >= 1) await b.close() diff --git a/test/studio-layout.mjs b/test/studio-layout.mjs new file mode 100644 index 0000000..377cd38 --- /dev/null +++ b/test/studio-layout.mjs @@ -0,0 +1,49 @@ +// Layout dello Studio: il pannello dei controlli sta a SINISTRA del palco in tutti i modi (Crea, Modifica, Box, Ricette), +// e in Crea il pulsante CREA sta fuori dallo scorrimento, in fondo al pannello. +// npm run build, poi un server statico su out/renderer (porta 8765), poi: node test/studio-layout.mjs +import { chromium } from 'playwright' +import { readFileSync } from 'node:fs' +import assert from 'node:assert/strict' + +const url = process.argv[2] || 'http://localhost:8765/index.html' +const b = await chromium.launch() +const pg = await b.newPage({ viewport: { width: 1480, height: 1000 } }) +pg.on('pageerror', (e) => console.log('[errore pagina]', e.message)) +await pg.addInitScript(readFileSync('test/finto.js', 'utf8')) +await pg.goto(url) +await pg.waitForTimeout(1200) + +const misure = {} +for (const modo of ['crea', 'modifica', 'box', 'ricette']) { + await pg.click(`button[data-modo="${modo}"]`) + await pg.waitForTimeout(400) + misure[modo] = await pg.evaluate(() => { + const visibile = (el) => el.getBoundingClientRect().width > 0 + const colonna = [...document.querySelectorAll('.studio-corpo .colonna.destra')].find(visibile) + const pagina = colonna && colonna.closest('.pagina') + if (!colonna || !pagina) return null + const c = colonna.getBoundingClientRect() + const p = pagina.getBoundingClientRect() + // il pannello è il più a sinistra della pagina (l'ordine visivo, non quello del DOM: il CSS lo mette per primo) + const aSinistra = Math.abs(c.left - p.left) < 2 && c.right < p.right + const azione = colonna.querySelector('.azione-prima') + const scorri = colonna.querySelector(':scope > .scorri') + return { + pannelloASinistra: aSinistra, + pannelloLarghezza: Math.round(c.width), + scorrevole: !!scorri && getComputedStyle(scorri).overflowY === 'auto', + azioneFuoriDalloScorrimento: !!azione && !scorri.contains(azione), + } + }) + console.log(modo, JSON.stringify(misure[modo])) + assert.ok(misure[modo], `${modo}: pannello e palco visibili`) + assert.equal(misure[modo].pannelloASinistra, true, `${modo}: il pannello dei controlli sta a sinistra del palco`) +} +// in Crea il pulsante CREA è fuori dallo scorrimento (resta in fondo anche con il pannello scorso) +await pg.click('button[data-modo="crea"]') +await pg.waitForTimeout(300) +assert.equal(misure.crea.azioneFuoriDalloScorrimento, true, 'Crea: CREA fisso in fondo al pannello') +assert.equal(misure.crea.scorrevole, true, 'Crea: il pannello scorre') + +await b.close() +console.log('OK: Studio con pannello a sinistra in Crea, Modifica, Box e Ricette; CREA fisso in fondo') diff --git a/test/uncensored-ssr.mjs b/test/uncensored-ssr.mjs new file mode 100644 index 0000000..2ea7e1f --- /dev/null +++ b/test/uncensored-ssr.mjs @@ -0,0 +1,30 @@ +// Verifica del gate Uncensored senza avviare browser o scaricare modelli. +import assert from 'node:assert/strict' +import { buildSync } from 'esbuild' +import { createRequire } from 'node:module' +import { resolve } from 'node:path' +import { mkdirSync } from 'node:fs' +const require = createRequire(import.meta.url) +const React = require('react') +const { renderToStaticMarkup } = require('react-dom/server') +globalThis.window = { daprod: {} } +globalThis.localStorage = { getItem: () => null, setItem() {} } +mkdirSync('test/.out', { recursive: true }) +buildSync({ stdin: { contents: "export { LaboratorioUncensored } from './src/renderer/src/componenti/LaboratorioUncensored'; export { Impostazioni } from './src/renderer/src/pagine/Impostazioni'; export { usaStato } from './src/renderer/src/stato'", resolveDir: process.cwd() }, bundle: true, platform: 'node', format: 'cjs', jsx: 'automatic', external: ['react', 'react-dom'], alias: { '@shared': resolve('src/shared') }, outfile: 'test/.out/uncensored-ssr.cjs' }) +const { LaboratorioUncensored, Impostazioni, usaStato } = require('./.out/uncensored-ssr.cjs') +const voce = { id: 'fixture', nome: 'Componente di prova', tipo: 'encoder', uncensored: true, licenza: 'Qwen Research', byte: 1, presente: false } +const props = { imp: {}, catalogo: [voce], download: {}, salva: async () => {}, scarica: async () => { throw Error('mai scaricare nel rendering') } } +const render = imp => renderToStaticMarkup(React.createElement(LaboratorioUncensored, { ...props, imp })) +assert.doesNotMatch(render({}), /Componente di prova/, 'catalogo nascosto di serie') +assert.doesNotMatch(render({ uncensored: true }), /Componente di prova/, 'flag senza consenso insufficiente') +assert.doesNotMatch(render({ uncensored: true, uncensoredAccettato: false }), /Componente di prova/, 'consenso obsoleto insufficiente') +const acceso = render({ uncensored: true, uncensoredAccettato: true }) +assert.match(acceso, /Componente di prova/) +assert.match(acceso, /Uncensored/) +assert.match(acceso, /Qwen Research/) +assert.match(acceso, /Scarica/) +React.useSyncExternalStore = (_subscribe, getSnapshot) => getSnapshot() +usaStato.setState({ imp: { modello: '', encoder: '', vae: '', kleinModello: '', kleinBase: '', kleinEncoder: '', kleinVae: '', famiglia: 'qwen', predefiniti: { passi: 40, cfg: 1, megapixel: 1 } } }) +const pagina = renderToStaticMarkup(React.createElement(Impostazioni)) +assert.match(pagina, /LABORATORIO UNCENSORED/, 'laboratorio raggiungibile dalla pagina Impostazioni') +console.log('OK: laboratorio Uncensored, gate, licenza e integrazione nella pagina')