Files
mozempk 0b29c1587d fix(backend): stale classifyImageRunFailure -> classifyRunFailure
Left behind by a rename; broke the img2img (issue #121) crash-detection
path. Typecheck back to 0 errors.
2026-08-27 09:42:01 +02:00

1109 lines
41 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# pi-imgen — installazione per macOS su Apple Silicon.
#
# Ri-eseguibile per costruzione: ogni passo controlla prima di agire, ripara invece di
# duplicare, e NON sovrascrive mai i preset dell'utente in ~/.pi/agent/pi-imgen.json.
#
# NOTE FOR MAINTAINERS (English, per CODE STYLE; all user-visible strings are Italian):
# - macOS ships bash 3.2, so: no associative arrays, no ${var,,}, no `read -i`,
# and empty arrays must be guarded with ${#arr[@]} before expanding under `set -u`.
# - The gRPCServerCLI install location and pinned version MUST stay in sync with
# extensions/imgen/backends/server.ts (GRPC_SERVER_VERSION / serverPaths()), which
# defaults the binary to <piConfigDir>/imgen/gRPCServerCLI-macOS. We deliberately do
# NOT write into a Homebrew prefix we do not own, and we deliberately leave
# `server.binary` unset in the config so the code default keeps the two in step.
# - We never invent CLI flags. `draw-things-cli models ensure` is attempted ONLY if the
# locally installed CLI's own --help confirms both the subcommand and the flags; see
# prefetch_models(). Otherwise we print exact manual instructions and say so.
# - No sudo is ever run by this script. The only privileged step is the official
# Homebrew installer, which asks for its own password and only after we ask first.
set -euo pipefail
# ---------------------------------------------------------------------------
# Costanti
# ---------------------------------------------------------------------------
REPO_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
# Sincronizzati con extensions/imgen/backends/server.ts
GRPC_SERVER_VERSION="v1.20260716.0"
GRPC_SERVER_BINARY="gRPCServerCLI-macOS"
GRPC_SERVER_URL="https://github.com/drawthingsai/draw-things-community/releases/download/${GRPC_SERVER_VERSION}/${GRPC_SERVER_BINARY}"
MODELS_CATALOGUE_URL="https://models.drawthings.ai/models.json"
PI_CONFIG_DIR="${PI_CONFIG_DIR:-$HOME/.pi/agent}"
STATE_DIR="$PI_CONFIG_DIR/imgen"
CFG_PATH="$PI_CONFIG_DIR/pi-imgen.json"
BREW_REQUIRED="draw-things-cli typst"
BREW_OPTIONAL="ghostscript poppler qpdf"
# Spazio consigliato per i modelli (~10 GB di pesi + margine di lavoro).
MIN_MODELS_GB=15
DEFAULT_OUT_DIR="$HOME/Immagini/generated"
# ---------------------------------------------------------------------------
# Interfaccia
# ---------------------------------------------------------------------------
if [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_BLUE=$'\033[34m'
else
C_RESET=""; C_BOLD=""; C_DIM=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""
fi
STEP_NO=0
step() { STEP_NO=$((STEP_NO + 1)); printf '\n%s[%d/8] %s%s\n' "$C_BOLD$C_BLUE" "$STEP_NO" "$1" "$C_RESET"; }
ok() { printf ' %s✓%s %s\n' "$C_GREEN" "$C_RESET" "$1"; }
info() { printf ' %s·%s %s\n' "$C_DIM" "$C_RESET" "$1"; }
warn() { printf ' %s!%s %s\n' "$C_YELLOW" "$C_RESET" "$1"; }
err() { printf ' %s✗%s %s\n' "$C_RED" "$C_RESET" "$1" >&2; }
hint() { printf ' %s%s%s\n' "$C_DIM" "$1" "$C_RESET"; }
die() {
printf '\n%s✗ Installazione interrotta.%s %s\n\n' "$C_RED$C_BOLD" "$C_RESET" "$1" >&2
exit 1
}
# Esiti raccolti per il riepilogo finale.
SUMMARY_LINES=()
ESSENTIAL_MISSING=0
record() { # record <stato: ok|warn|fail> <voce> <dettaglio>
SUMMARY_LINES+=("$1"$'\t'"$2"$'\t'"$3")
[ "$1" = "fail" ] && ESSENTIAL_MISSING=$((ESSENTIAL_MISSING + 1))
return 0
}
# ---------------------------------------------------------------------------
# Opzioni
# ---------------------------------------------------------------------------
ASSUME_YES=0
DOCTOR_ONLY=0
SKIP_MODELS=0
ARG_MODELS_DIR=""
ARG_OUT_DIR=""
usage() {
cat <<'USAGE'
pi-imgen — installazione (macOS, Apple Silicon)
Uso: ./install.sh [opzioni]
-y, --yes non fare domande: usa i valori predefiniti (o quelli già
presenti nella configurazione). Utile per riesecuzioni.
--models-dir DIR cartella dei modelli (deve stare su un disco secondario)
--out-dir DIR cartella dei file generati
--skip-models non prescaricare i modelli
--doctor esegui solo il controllo finale, senza installare nulla
-h, --help mostra questo messaggio
Lo script è ri-eseguibile: ripara ciò che manca e non tocca i tuoi preset.
USAGE
}
while [ $# -gt 0 ]; do
case "$1" in
-y|--yes) ASSUME_YES=1 ;;
--doctor) DOCTOR_ONLY=1 ;;
--skip-models) SKIP_MODELS=1 ;;
--models-dir) [ $# -ge 2 ] || die "Manca il valore per --models-dir."; ARG_MODELS_DIR="$2"; shift ;;
--models-dir=*) ARG_MODELS_DIR="${1#*=}" ;;
--out-dir) [ $# -ge 2 ] || die "Manca il valore per --out-dir."; ARG_OUT_DIR="$2"; shift ;;
--out-dir=*) ARG_OUT_DIR="${1#*=}" ;;
-h|--help) usage; exit 0 ;;
*) usage >&2; die "Opzione sconosciuta: $1" ;;
esac
shift
done
INTERACTIVE=1
if [ ! -t 0 ] || [ "$ASSUME_YES" = "1" ]; then INTERACTIVE=0; fi
# ---------------------------------------------------------------------------
# Utilità
# ---------------------------------------------------------------------------
have() { command -v "$1" >/dev/null 2>&1; }
expand_tilde() { # stampa il percorso con ~ espansa
case "$1" in
"~") printf '%s' "$HOME" ;;
"~/"*) printf '%s/%s' "$HOME" "${1#\~/}" ;;
*) printf '%s' "$1" ;;
esac
}
ask() { # ask <domanda> <predefinito> -> stampa la risposta
local question="$1" default="$2" reply=""
if [ "$INTERACTIVE" = "0" ]; then printf '%s' "$default"; return 0; fi
printf ' %s%s%s\n [%s]: ' "$C_BOLD" "$question" "$C_RESET" "$default" >&2
IFS= read -r reply || reply=""
[ -n "$reply" ] || reply="$default"
printf '%s' "$reply"
}
confirm() { # confirm <domanda> <s|n predefinito> -> 0 = sì
local question="$1" default="$2" reply=""
if [ "$INTERACTIVE" = "0" ]; then [ "$default" = "s" ]; return; fi
local prompt="s/N"; [ "$default" = "s" ] && prompt="S/n"
printf ' %s%s%s [%s]: ' "$C_BOLD" "$question" "$C_RESET" "$prompt" >&2
IFS= read -r reply || reply=""
[ -n "$reply" ] || reply="$default"
case "$reply" in [sSyY]*) return 0 ;; *) return 1 ;; esac
}
free_gb() { # spazio libero in GB; stampa sempre un numero, 0 se non si sa
local gb
gb="$(df -Pk "$1" 2>/dev/null | awk 'NR==2 { printf "%.0f", $4 / 1048576 }')"
case "$gb" in
''|*[!0-9]*) printf '0' ;;
*) printf '%s' "$gb" ;;
esac
}
# Un file scaricato è davvero un eseguibile macOS? Stessi magic number di server.ts:
# difende da una pagina di errore HTML o da un puntatore Git LFS serviti con 200 OK.
is_macho() {
local magic
magic="$(head -c 4 "$1" 2>/dev/null | od -An -tx1 | tr -d ' \n')"
case "$magic" in
feedfacf|cffaedfe|feedface|cefaedfe|cafebabe|bebafeca) return 0 ;;
*) return 1 ;;
esac
}
# Percorso scrivibile? Prova davvero a scrivere: -w mente su volumi di rete e su ACL.
dir_is_writable() {
local probe="$1/.pi-imgen-write-test.$$"
if ( umask 077; : > "$probe" ) 2>/dev/null; then rm -f "$probe"; return 0; fi
return 1
}
# ---------------------------------------------------------------------------
# 0. Configurazione esistente (letta prima di tutto: guida i valori predefiniti)
# ---------------------------------------------------------------------------
EXIST_CFG_VALID=0
EXIST_MODELS_PATH=""
EXIST_OUT_DIR=""
EXIST_SERVER_BINARY=""
read_existing_config() {
[ -f "$CFG_PATH" ] || return 0
have python3 || return 0
local out=""
out="$(CFG_PATH="$CFG_PATH" python3 <<'PY' 2>/dev/null || true
import json, os, shlex
def emit(name, value):
print("%s=%s" % (name, shlex.quote(value if isinstance(value, str) else "")))
try:
with open(os.environ["CFG_PATH"], encoding="utf-8") as fh:
cfg = json.load(fh)
if not isinstance(cfg, dict):
raise ValueError("root is not an object")
except Exception:
raise SystemExit(0)
print("EXIST_CFG_VALID=1")
emit("EXIST_MODELS_PATH", cfg.get("modelsPath", ""))
emit("EXIST_OUT_DIR", cfg.get("outputDir", ""))
server = cfg.get("server") if isinstance(cfg.get("server"), dict) else {}
emit("EXIST_SERVER_BINARY", server.get("binary", ""))
PY
)"
# `out` contiene solo assegnazioni con valori già quotati da shlex.quote().
[ -n "$out" ] && eval "$out"
return 0
}
# ---------------------------------------------------------------------------
# 1. Sistema
# ---------------------------------------------------------------------------
check_system() {
step "Controllo del sistema"
local os arch
os="$(uname -s)"
arch="$(uname -m)"
if [ "$os" != "Darwin" ]; then
die "pi-imgen funziona solo su macOS: qui il sistema è «$os».
Il motore di generazione (Draw Things) esiste soltanto per macOS su Apple Silicon."
fi
# Sotto Rosetta `uname -m` risponde x86_64 anche su un Mac Apple Silicon: distinguere
# i due casi evita di dire a un utente M4 che il suo Mac non è compatibile.
local translated=0
translated="$(sysctl -n sysctl.proc_translated 2>/dev/null || printf '0')"
if [ "$translated" = "1" ]; then
die "Questo terminale sta girando sotto Rosetta (emulazione Intel).
Draw Things richiede un processo nativo arm64.
Apri un terminale nativo, oppure rilancia così:
arch -arm64 /bin/bash \"$REPO_DIR/install.sh\""
fi
if [ "$arch" != "arm64" ]; then
die "pi-imgen richiede un Mac Apple Silicon (M1 o successivi): qui l'architettura è «$arch».
Sui Mac Intel il motore di generazione non è disponibile e non esiste un ripiego."
fi
local osver; osver="$(sw_vers -productVersion 2>/dev/null || printf 'sconosciuta')"
ok "macOS $osver su Apple Silicon ($arch)."
# draw-things-cli richiede macOS 13+.
case "$osver" in
1[0-2].*|[0-9].*) warn "draw-things-cli richiede macOS 13 o successivo: questa è la $osver." ;;
esac
local membytes memgb
membytes="$(sysctl -n hw.memsize 2>/dev/null || printf '0')"
case "$membytes" in ''|*[!0-9]*) membytes=0 ;; esac
memgb="$(( membytes / 1073741824 ))"
if [ "$memgb" -gt 0 ]; then
info "Memoria unificata: ${memgb} GB."
if [ "$memgb" -le 16 ]; then
info "Con 16 GB il decoding a riquadri (tiled) resta attivo: dimezza la memoria di picco."
fi
fi
have python3 || die "Serve python3 (incluso in macOS) per aggiornare la configurazione senza perdere i tuoi preset."
have curl || die "Serve curl (incluso in macOS) per scaricare il server."
record ok "Sistema" "macOS $osver, Apple Silicon"
}
# ---------------------------------------------------------------------------
# 2. Homebrew e pacchetti
# ---------------------------------------------------------------------------
BREW=""
find_brew() {
if have brew; then BREW="$(command -v brew)"; return 0; fi
if [ -x /opt/homebrew/bin/brew ]; then BREW=/opt/homebrew/bin/brew; return 0; fi
if [ -x /usr/local/bin/brew ]; then BREW=/usr/local/bin/brew; return 0; fi
return 1
}
ensure_homebrew() {
step "Homebrew"
if find_brew; then
ok "Homebrew trovato: $BREW"
else
warn "Homebrew non è installato: serve per draw-things-cli e typst."
hint "L'installazione ufficiale chiederà LA TUA PASSWORD (usa sudo per conto suo)."
if ! confirm "Installo Homebrew adesso?" "s"; then
record fail "Homebrew" "assente — installalo da https://brew.sh e rilancia"
err "Salto Homebrew. Senza, draw-things-cli e typst non si possono installare."
return 0
fi
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \
|| die "Installazione di Homebrew fallita. Installalo a mano da https://brew.sh e rilancia questo script."
find_brew || die "Homebrew risulta installato ma non lo trovo nel PATH. Apri un nuovo terminale e rilancia."
ok "Homebrew installato: $BREW"
fi
# Su Apple Silicon il prefisso nativo è /opt/homebrew. Un brew in /usr/local è la
# versione Intel: funziona sotto Rosetta ma non fornirà bottiglie arm64.
case "$BREW" in
/usr/local/*) warn "Questo Homebrew sta in /usr/local: è l'installazione Intel. Per pacchetti arm64 serve quella in /opt/homebrew." ;;
esac
eval "$("$BREW" shellenv)" 2>/dev/null || true
local f
for f in $BREW_REQUIRED; do
if "$BREW" list --formula --versions "$f" >/dev/null 2>&1; then
ok "$f già installato ($("$BREW" list --formula --versions "$f" | head -1))."
record ok "$f" "già presente"
else
info "Installo $f…"
# NB: formula di Homebrew CORE. Niente tap personalizzati, niente --HEAD.
if "$BREW" install "$f"; then
ok "$f installato."
record ok "$f" "installato ora"
else
err "Installazione di $f fallita."
record fail "$f" "installazione fallita — riprova con: brew install $f"
fi
fi
done
# Facoltativi: servono solo al controllo pre-stampa del PDF.
local missing_opt=""
for f in $BREW_OPTIONAL; do
"$BREW" list --formula --versions "$f" >/dev/null 2>&1 || missing_opt="$missing_opt $f"
done
missing_opt="${missing_opt# }"
if [ -n "$missing_opt" ]; then
info "Facoltativi per il controllo pre-stampa (verifica di TrimBox, abbondanza e caratteri incorporati): $missing_opt"
hint "Senza, la locandina si genera lo stesso: salta solo la verifica del PDF."
if confirm "Li installo?" "n"; then
# shellcheck disable=SC2086
if "$BREW" install $missing_opt; then
ok "Strumenti di pre-stampa installati."
record ok "Pre-stampa" "$missing_opt installati"
else
warn "Installazione dei facoltativi fallita: si prosegue senza."
record warn "Pre-stampa" "installazione fallita (facoltativo)"
fi
else
info "Salto i facoltativi. Più avanti: brew install $missing_opt"
record warn "Pre-stampa" "assenti (facoltativo): $missing_opt"
fi
else
ok "Strumenti di pre-stampa già presenti ($BREW_OPTIONAL)."
record ok "Pre-stampa" "completi"
fi
}
# ---------------------------------------------------------------------------
# 3. gRPCServerCLI (asset di release GitHub, NON in Homebrew)
# ---------------------------------------------------------------------------
SERVER_TARGET=""
ensure_grpc_server() {
step "Server residente ($GRPC_SERVER_BINARY)"
# Se la configurazione indica già un percorso per il binario, rispettiamolo: è quello
# che il codice userà (config.server.binary vince su serverPaths()).
if [ -n "$EXIST_SERVER_BINARY" ]; then
SERVER_TARGET="$(expand_tilde "$EXIST_SERVER_BINARY")"
info "Percorso preso dalla configurazione esistente: $SERVER_TARGET"
else
SERVER_TARGET="$STATE_DIR/$GRPC_SERVER_BINARY"
fi
mkdir -p "$(dirname -- "$SERVER_TARGET")"
local stamp="$SERVER_TARGET.version"
info "Non è distribuito da Homebrew: brew install draw-things-cli dà solo il client."
if [ -f "$SERVER_TARGET" ]; then
if [ ! -x "$SERVER_TARGET" ]; then
chmod +x "$SERVER_TARGET" 2>/dev/null || true
info "Permesso di esecuzione ripristinato."
fi
if [ -x "$SERVER_TARGET" ] && is_macho "$SERVER_TARGET"; then
local installed=""
[ -f "$stamp" ] && installed="$(cat "$stamp" 2>/dev/null || printf '')"
if [ "$installed" = "$GRPC_SERVER_VERSION" ]; then
ok "Già presente e aggiornato ($GRPC_SERVER_VERSION): $SERVER_TARGET"
record ok "Server gRPC" "$GRPC_SERVER_VERSION"
return 0
fi
if [ -z "$installed" ]; then
# Installato a mano: è valido, quindi lo teniamo. Riscaricarlo a ogni
# esecuzione sarebbe uno spreco, e questo script deve essere ri-eseguibile.
ok "Già presente (installato a mano): $SERVER_TARGET"
info "Versione non determinabile; quella attesa è $GRPC_SERVER_VERSION."
hint "Per allinearlo: rm \"$SERVER_TARGET\" e rilancia ./install.sh"
record warn "Server gRPC" "presente, versione ignota (attesa $GRPC_SERVER_VERSION)"
return 0
fi
warn "Presente una versione diversa (installata: $installed, attesa: $GRPC_SERVER_VERSION)."
if ! confirm "Lo aggiorno?" "s"; then
record warn "Server gRPC" "versione $installed (attesa $GRPC_SERVER_VERSION)"
return 0
fi
else
warn "Il file esistente non è un eseguibile macOS valido: lo riscarico."
fi
fi
if ! confirm "Scarico $GRPC_SERVER_BINARY $GRPC_SERVER_VERSION (~decine di MB) da GitHub?" "s"; then
record fail "Server gRPC" "assente — scaricalo da $GRPC_SERVER_URL in $SERVER_TARGET"
warn "Salto lo scaricamento. Senza il server ogni immagine ricarica il modello da zero."
return 0
fi
local tmp="$SERVER_TARGET.part.$$"
info "Scarico da $GRPC_SERVER_URL"
if ! curl -fL --progress-bar -o "$tmp" "$GRPC_SERVER_URL"; then
rm -f "$tmp"
err "Scaricamento fallito (rete o URL non raggiungibile)."
hint "A mano: curl -fL -o \"$SERVER_TARGET\" \"$GRPC_SERVER_URL\" && chmod +x \"$SERVER_TARGET\""
record fail "Server gRPC" "scaricamento fallito"
return 0
fi
# Un 200 OK non garantisce un binario: verifichiamo dimensione e magic number.
local size; size="$(wc -c < "$tmp" | tr -d ' ')"
if [ "$size" -lt 1048576 ] || ! is_macho "$tmp"; then
rm -f "$tmp"
err "Il file scaricato non è un eseguibile macOS valido ($size byte): probabilmente una pagina di errore."
record fail "Server gRPC" "file scaricato non valido"
return 0
fi
chmod 755 "$tmp"
# Gatekeeper mette in quarantena i file scaricati: senza questo il primo avvio viene bloccato.
/usr/bin/xattr -d com.apple.quarantine "$tmp" 2>/dev/null || true
mv -f "$tmp" "$SERVER_TARGET"
printf '%s\n' "$GRPC_SERVER_VERSION" > "$stamp"
ok "Installato in $SERVER_TARGET ($GRPC_SERVER_VERSION)."
record ok "Server gRPC" "$GRPC_SERVER_VERSION"
}
# ---------------------------------------------------------------------------
# 4. Percorsi: modelli (disco secondario) e destinazione
# ---------------------------------------------------------------------------
MODELS_PATH=""
OUT_DIR=""
suggest_models_dir() {
# Candidati: volumi montati sotto /Volumes che NON sono il disco di avvio.
local root_dev v dev gb best_gb=0 best=""
root_dev="$(df -P / 2>/dev/null | awk 'NR==2 { print $1 }')"
for v in /Volumes/*; do
[ -d "$v" ] || continue
case "$v" in
*com.apple.TimeMachine*|*.timemachine*) continue ;;
esac
dev="$(df -P "$v" 2>/dev/null | awk 'NR==2 { print $1 }')"
if [ -z "$dev" ] || [ "$dev" = "$root_dev" ]; then continue; fi
dir_is_writable "$v" || continue
gb="$(free_gb "$v")"
[ -n "$gb" ] || gb=0
if [ "$gb" -gt "$best_gb" ]; then best_gb="$gb"; best="$v"; fi
done
if [ -n "$best" ]; then printf '%s/pi-imgen/models' "$best"; return 0; fi
# Nessun disco secondario montato: proponiamo comunque un percorso fuori dalla home
# se esiste, altrimenti la home (chiedendo conferma esplicita più avanti).
printf '%s' "$HOME/Documents/Models"
}
prompt_paths() {
step "Dove tenere i modelli e i file generati"
# -- modelli ---------------------------------------------------------------
local default_models=""
if [ -n "$ARG_MODELS_DIR" ]; then
default_models="$ARG_MODELS_DIR"
elif [ -n "$EXIST_MODELS_PATH" ]; then
default_models="$EXIST_MODELS_PATH"
info "Configurazione attuale: $EXIST_MODELS_PATH"
else
default_models="$(suggest_models_dir)"
fi
printf ' %sI modelli occupano circa 10 GB. Tienili su un disco secondario o esterno,%s\n' "$C_DIM" "$C_RESET"
printf ' %snon nella cartella utente: così i backup di casa restano leggeri.%s\n' "$C_DIM" "$C_RESET"
local candidates="" v
for v in /Volumes/*; do
[ -d "$v" ] || continue
case "$v" in *com.apple.TimeMachine*) continue ;; esac
candidates="$candidates $v ($(free_gb "$v") GB liberi)"
done
[ -n "$candidates" ] && info "Volumi montati:$candidates"
while :; do
local answer
answer="$(ask "Cartella dei modelli" "$default_models")"
answer="$(expand_tilde "$answer")"
case "$answer" in
/*) ;;
*) err "Il percorso deve essere assoluto (deve iniziare con «/»). Ricevuto: «$answer»"
if [ "$INTERACTIVE" = "0" ]; then die "Percorso dei modelli non assoluto: $answer"; fi
continue ;;
esac
case "$answer" in
"$HOME"|"$HOME"/*)
warn "«$answer» sta nella tua cartella utente: finirà nei backup e in iCloud Drive."
if [ "$INTERACTIVE" = "1" ] && ! confirm "Va bene lo stesso?" "n"; then
default_models="$(suggest_models_dir)"
continue
fi
;;
esac
if ! mkdir -p "$answer" 2>/dev/null; then
err "Non riesco a creare «$answer» (permessi, o disco non montato)."
if [ "$INTERACTIVE" = "0" ]; then die "Cartella dei modelli non creabile: $answer"; fi
continue
fi
if ! dir_is_writable "$answer"; then
err "«$answer» non è scrivibile (volume in sola lettura?)."
if [ "$INTERACTIVE" = "0" ]; then die "Cartella dei modelli non scrivibile: $answer"; fi
continue
fi
local gb; gb="$(free_gb "$answer")"; [ -n "$gb" ] || gb=0
if [ "$gb" -lt "$MIN_MODELS_GB" ]; then
warn "Spazio libero: ${gb} GB. Ne servono almeno ${MIN_MODELS_GB} per i modelli."
if [ "$INTERACTIVE" = "1" ] && ! confirm "Proseguo comunque?" "n"; then continue; fi
else
info "Spazio libero: ${gb} GB."
fi
MODELS_PATH="$answer"
ok "Modelli: $MODELS_PATH"
break
done
# -- destinazione ----------------------------------------------------------
local default_out=""
if [ -n "$ARG_OUT_DIR" ]; then default_out="$ARG_OUT_DIR"
elif [ -n "$EXIST_OUT_DIR" ]; then default_out="$EXIST_OUT_DIR"
else default_out="$DEFAULT_OUT_DIR"; fi
while :; do
local answer
answer="$(ask "Cartella dei file generati" "$default_out")"
answer="$(expand_tilde "$answer")"
case "$answer" in
/*) ;;
*) err "Il percorso deve essere assoluto. Ricevuto: «$answer»"
if [ "$INTERACTIVE" = "0" ]; then die "Cartella di destinazione non assoluta: $answer"; fi
continue ;;
esac
if ! mkdir -p "$answer" 2>/dev/null || ! dir_is_writable "$answer"; then
err "«$answer» non è utilizzabile: non riesco a scriverci."
if [ "$INTERACTIVE" = "0" ]; then die "Cartella di destinazione non scrivibile: $answer"; fi
continue
fi
OUT_DIR="$answer"
ok "Destinazione: $OUT_DIR"
break
done
record ok "Cartelle" "modelli in $MODELS_PATH, uscite in $OUT_DIR"
}
# ---------------------------------------------------------------------------
# 5. Caratteri
# ---------------------------------------------------------------------------
FONT_SCRIPT="$REPO_DIR/scripts/fetch-fonts.sh"
fetch_fonts() {
step "Caratteri tipografici (22 famiglie OFL/Apache)"
if [ ! -f "$FONT_SCRIPT" ]; then
warn "Script assente: $FONT_SCRIPT"
hint "Senza i caratteri, l'impaginazione con Typst non parte."
record fail "Caratteri" "scripts/fetch-fonts.sh non trovato"
return 0
fi
[ -x "$FONT_SCRIPT" ] || chmod +x "$FONT_SCRIPT" 2>/dev/null || true
info "Eseguo scripts/fetch-fonts.sh…"
# Fallire qui non deve interrompere l'installazione: i caratteri si riscaricano dopo.
if ( cd "$REPO_DIR" && /usr/bin/env bash "$FONT_SCRIPT" ); then
local n; n="$(find "$REPO_DIR/vendor/fonts" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')"
ok "Caratteri pronti ($n famiglie in vendor/fonts)."
record ok "Caratteri" "$n famiglie"
else
err "Scaricamento dei caratteri fallito."
hint "Riprova più tardi con: \"$FONT_SCRIPT\""
record fail "Caratteri" "scaricamento fallito"
fi
}
# ---------------------------------------------------------------------------
# 6. Modelli
# ---------------------------------------------------------------------------
# ModelsConfig (extensions/imgen/config.ts) dichiara obbligatori SOLO draft e final:
# alt, edit e upscale sono `?` e la loro assenza non deve far fallire nulla.
MODEL_KEYS_REQUIRED="draft final"
MODEL_KEYS_OPTIONAL="alt edit upscale"
# Legge dalla configurazione scritta i modelli per le chiavi indicate, una riga
# "chiave<TAB>file" per ciascuna chiave davvero presente e valorizzata.
config_model_files() { # config_model_files <chiave>...
if [ ! -f "$CFG_PATH" ] || ! have python3; then
return 0
fi
CFG_PATH="$CFG_PATH" MODEL_KEYS="$*" python3 <<'PY' 2>/dev/null || true
import json, os
try:
with open(os.environ["CFG_PATH"], encoding="utf-8") as fh:
models = json.load(fh).get("models", {})
if isinstance(models, dict):
for key in os.environ["MODEL_KEYS"].split():
value = models.get(key)
if isinstance(value, str) and value:
print(f"{key}\t{value}")
except Exception:
pass
PY
}
# Ripiego per una singola chiave: il valore in DEFAULTS.models dentro config.ts,
# così l'elenco non viene duplicato qui dentro.
default_model_file() { # default_model_file <chiave>
sed -n "s/^[[:space:]]*$1:[[:space:]]*\"\([A-Za-z0-9_.-]*\.ckpt\)\".*/\1/p" \
"$REPO_DIR/extensions/imgen/config.ts" 2>/dev/null | head -n 1
}
# I file attesi per un gruppo di chiavi. L'unione è la stessa che fa loadConfig:
# il file dell'utente vince chiave per chiave, il resto arriva da DEFAULTS.
model_files() { # model_files <chiave>...
local pairs key value
pairs="$(config_model_files "$@")"
for key in "$@"; do
value="$(printf '%s\n' "$pairs" | awk -F'\t' -v k="$key" '$1 == k { print $2; exit }')"
if [ -z "$value" ]; then value="$(default_model_file "$key")"; fi
if [ -n "$value" ]; then printf '%s\n' "$value"; fi
done | sort -u
}
required_model_files() { model_files $MODEL_KEYS_REQUIRED; }
# I facoltativi già coperti dai necessari (per impostazione predefinita draft e
# final sono lo stesso file) non vanno riproposti come «mancanti».
optional_model_files() {
local required
required="$(required_model_files)"
# grep -F con più righe = un motivo per riga: scarta i doppioni dei necessari.
model_files $MODEL_KEYS_OPTIONAL | grep -vxF "$required" || true
}
# Avviso, mai errore: senza questi si genera lo stesso, si perdono solo lo stile
# alternativo, le modifiche (img2img) e l'ingrandimento.
report_optional_models() {
local m missing="" have_any=0
for m in $(optional_model_files); do
if [ -f "$MODELS_PATH/$m" ]; then
have_any=1
else
missing="$missing $m"
fi
done
missing="${missing# }"
if [ -z "$missing" ]; then
if [ "$have_any" = "1" ]; then
ok "Anche i modelli facoltativi sono presenti."
record ok "Modelli extra" "presenti"
fi
return 0
fi
info "Facoltativi non presenti (nessun problema): $missing"
hint "Servono solo per lo stile alternativo, le modifiche e l'ingrandimento."
hint "Puoi aggiungerli quando ti servono, dall'app Draw Things o dal catalogo."
record warn "Modelli extra" "facoltativi assenti: $missing"
}
prefetch_models() {
step "Modelli di generazione"
if [ "$SKIP_MODELS" = "1" ]; then
info "Salto lo scaricamento dei modelli (--skip-models)."
record warn "Modelli" "scaricamento saltato"
return 0
fi
local wanted present=0
wanted="$(required_model_files)"
if [ -z "$wanted" ]; then
warn "Non riesco a determinare quali modelli servono."
record warn "Modelli" "elenco non determinato"
return 0
fi
local m missing=""
for m in $wanted; do
if [ -f "$MODELS_PATH/$m" ]; then
ok "Già presente: $m"
present=$((present + 1))
else
missing="$missing $m"
fi
done
missing="${missing# }"
if [ -z "$missing" ]; then
ok "I modelli necessari sono già in $MODELS_PATH."
record ok "Modelli" "$present presenti"
report_optional_models
return 0
fi
info "Mancano: $missing"
if ! have draw-things-cli; then
warn "draw-things-cli non è installato: non posso prescaricare nulla."
document_model_download
record fail "Modelli" "mancanti: $missing"
report_optional_models
return 0
fi
# NON inventiamo flag. Interroghiamo l'aiuto del CLI installato e usiamo il
# sottocomando SOLO se l'aiuto stesso conferma sia «ensure» sia «--models-dir».
local root_help models_help=""
root_help="$(draw-things-cli --help 2>&1 || true)"
if printf '%s' "$root_help" | grep -qE '(^|[[:space:]])models([[:space:]]|$)'; then
models_help="$(draw-things-cli models --help 2>&1 || true)"
fi
if [ -n "$models_help" ] \
&& printf '%s' "$models_help" | grep -qE '(^|[[:space:]])ensure([[:space:]]|$)' \
&& printf '%s' "$models_help" | grep -q -- '--models-dir'; then
info "Il CLI dichiara «models ensure»: uso la forma verificata dal suo --help."
local still=""
for m in $missing; do
info "Scarico $m…"
# Un'uscita a 0 non basta: il file su disco è l'unica prova che serve.
draw-things-cli models ensure --models-dir "$MODELS_PATH" "$m" || true
if [ -s "$MODELS_PATH/$m" ]; then
ok "$m pronto."
else
warn "$m non è arrivato in $MODELS_PATH."
still="$still $m"
fi
done
still="${still# }"
if [ -z "$still" ]; then
record ok "Modelli" "tutti presenti"
else
document_model_download
record fail "Modelli" "mancanti: $still"
fi
report_optional_models
return 0
fi
# Il sottocomando non esiste (o ha un'altra forma) in questa versione del CLI.
warn "Questa versione di draw-things-cli non espone «models ensure» nel suo aiuto."
info "Non tiro a indovinare i parametri: qui sotto trovi le vie sicure."
document_model_download
record fail "Modelli" "mancanti: $missing"
report_optional_models
}
document_model_download() {
cat <<EOF
${C_BOLD}Come procurarsi i modelli (tre strade, tutte verificate)${C_RESET}
1) ${C_BOLD}Dall'app Draw Things${C_RESET} (la più semplice)
Apri Draw Things, e dal gestore modelli scarica:
• Z-Image Turbo — versione «8-bit S» (i8x): usa il Neural Engine dell'M4
e dimezza i tempi
• FLUX.2 klein 4B — per lo stile alternativo e le modifiche
Poi imposta in Draw Things la stessa cartella modelli: $MODELS_PATH
2) ${C_BOLD}Al primo utilizzo${C_RESET}
--model di draw-things-cli accetta anche un riferimento HuggingFace
(hf://autore/repo oppure l'URL), quindi il modello viene risolto da solo
alla prima generazione. Serve la rete, e la prima volta è lenta.
3) ${C_BOLD}Dal catalogo ufficiale${C_RESET}
$MODELS_CATALOGUE_URL elenca i modelli disponibili con i rispettivi file.
Scarica il .ckpt che ti serve e mettilo in: $MODELS_PATH
${C_DIM}Nota: i suffissi _i8x / _i6x («8-bit S» / «6-bit S») usano Int8 con le matmul
hardware del Neural Engine M4 e sono nettamente più veloci. Su questo Mac, preferiscili.${C_RESET}
EOF
}
# ---------------------------------------------------------------------------
# 7. Configurazione (unione, mai sovrascrittura)
# ---------------------------------------------------------------------------
write_config() {
step "Configurazione ($CFG_PATH)"
mkdir -p "$PI_CONFIG_DIR" "$STATE_DIR"
if [ -f "$CFG_PATH" ] && [ "$EXIST_CFG_VALID" != "1" ]; then
local backup="$CFG_PATH.rotto-$(date +%Y%m%d%H%M%S)"
cp -p "$CFG_PATH" "$backup"
warn "Il file esistente non era JSON valido: l'ho messo da parte in $backup"
elif [ -f "$CFG_PATH" ]; then
cp -p "$CFG_PATH" "$CFG_PATH.bak"
info "Copia di sicurezza: $CFG_PATH.bak"
fi
# Unione conservativa: i valori già presenti VINCONO, tranne le due cartelle che
# l'utente ha appena confermato. `presets` e `defaultPreset` non si toccano mai.
# Scriviamo solo ciò che è specifico di questa macchina: config.ts fonde comunque
# il file sopra DEFAULTS, quindi un file parziale è del tutto legittimo e riduce
# il rischio che l'installer resti indietro rispetto al codice.
if CFG_PATH="$CFG_PATH" MODELS_PATH="$MODELS_PATH" OUT_DIR="$OUT_DIR" python3 <<'PY'
import json, os, tempfile
path = os.environ["CFG_PATH"]
try:
with open(path, encoding="utf-8") as fh:
cfg = json.load(fh)
if not isinstance(cfg, dict):
cfg = {}
except Exception:
cfg = {}
# Le due cartelle vengono dal prompt (il cui default era il valore attuale).
cfg["modelsPath"] = os.environ["MODELS_PATH"]
cfg["outputDir"] = os.environ["OUT_DIR"]
def fill(section, values):
"""Riempi solo le chiavi assenti: le scelte dell'utente restano intatte."""
current = cfg.get(section)
if not isinstance(current, dict):
current = {}
for key, value in values.items():
current.setdefault(key, value)
cfg[section] = current
# Leve di memoria, decisive su 16 GB: il decoding a riquadri porta il picco a 1024²
# da 14,03 GB a 7,18 GB. Il picco alla decodifica VAE — non il peso del modello — è
# ciò che fa fuori una macchina da 16 GB.
# ATTENZIONE (non verificato): l'unità di decodingTileWidth/Height è assunta in pixel.
fill("tiling", {
"tiledDecoding": True,
"decodingTileWidth": 512,
"decodingTileHeight": 512,
"decodingTileOverlap": 64,
})
# `binary` resta volutamente non impostato: server.ts lo deduce da serverPaths(), così
# installer e codice non possono divergere. Se l'utente l'ha impostato, lo lasciamo.
fill("server", {"enabled": True, "port": 7859, "cpuOffload": True})
cfg.setdefault("presets", {})
cfg.setdefault("defaultPreset", None)
# Scrittura atomica: un'interruzione non deve lasciare mezza configurazione.
directory = os.path.dirname(path) or "."
fd, tmp = tempfile.mkstemp(dir=directory, prefix=".pi-imgen.", suffix=".json")
try:
with os.fdopen(fd, "w", encoding="utf-8") as fh:
json.dump(cfg, fh, indent=2, ensure_ascii=False)
fh.write("\n")
os.replace(tmp, path)
except Exception:
os.path.exists(tmp) and os.unlink(tmp)
raise
PY
then
ok "Configurazione aggiornata."
local kept; kept="$(C="$CFG_PATH" python3 -c 'import json,os;print(len(json.load(open(os.environ["C"])).get("presets") or {}))' 2>/dev/null || printf '0')"
if [ "$kept" != "0" ]; then ok "Preset conservati: $kept."; else info "Nessun preset salvato (li creerai con /presets)."; fi
record ok "Configurazione" "$CFG_PATH"
else
err "Scrittura della configurazione fallita."
record fail "Configurazione" "scrittura fallita in $CFG_PATH"
fi
}
# ---------------------------------------------------------------------------
# 8. Controllo finale
# ---------------------------------------------------------------------------
doctor() {
step "Controllo finale"
local v
# -- strumenti -------------------------------------------------------------
if have draw-things-cli; then
v="$(draw-things-cli --version 2>&1 | head -1 || true)"
[ -n "$v" ] || v="presente"
ok "draw-things-cli: $v"
else
err "draw-things-cli non trovato nel PATH."
hint "brew install draw-things-cli"
fi
if have typst; then
ok "typst: $(typst --version 2>&1 | head -1)"
else
err "typst non trovato nel PATH."
hint "brew install typst"
fi
if have node; then
local nodemajor; nodemajor="$(node -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
if [ "$nodemajor" -ge 20 ] 2>/dev/null; then
ok "Node $(node -v)."
else
warn "Node $(node -v): pi-imgen richiede Node 20 o successivo."
fi
else
warn "Node non trovato: lo fornisce pi, di solito non è un problema."
fi
# -- server ----------------------------------------------------------------
if [ -n "$SERVER_TARGET" ] && [ -x "$SERVER_TARGET" ] && is_macho "$SERVER_TARGET"; then
ok "Server residente pronto: $SERVER_TARGET"
else
err "Server residente assente o non eseguibile."
hint "curl -fL -o \"${SERVER_TARGET:-$STATE_DIR/$GRPC_SERVER_BINARY}\" \"$GRPC_SERVER_URL\" && chmod +x \"${SERVER_TARGET:-$STATE_DIR/$GRPC_SERVER_BINARY}\""
fi
# Porta 7859: già occupata significa quasi sempre un server già avviato — bene così,
# pi-imgen adotta quello esistente invece di avviarne un secondo (due processi di
# diffusione saturano 16 GB prima di finire una decodifica).
local port=7859
if have python3; then
port="$(CFG_PATH="$CFG_PATH" python3 -c 'import json,os;print((json.load(open(os.environ["CFG_PATH"])).get("server") or {}).get("port",7859))' 2>/dev/null || printf '7859')"
fi
if have nc && nc -z 127.0.0.1 "$port" >/dev/null 2>&1; then
info "Porta $port: qualcosa è già in ascolto (server già avviato: verrà adottato)."
else
info "Porta $port libera: il server partirà all'avvio della sessione pi."
fi
# -- cartelle e modelli ----------------------------------------------------
if [ -d "$MODELS_PATH" ] && dir_is_writable "$MODELS_PATH"; then
ok "Cartella modelli: $MODELS_PATH ($(free_gb "$MODELS_PATH") GB liberi)."
else
err "Cartella modelli non utilizzabile: $MODELS_PATH"
fi
if [ -d "$OUT_DIR" ] && dir_is_writable "$OUT_DIR"; then
ok "Cartella di destinazione: $OUT_DIR"
else
err "Cartella di destinazione non utilizzabile: $OUT_DIR"
fi
# Necessari (draft, final) e facoltativi (alt, edit, upscale) contati a parte:
# i secondi non rendono l'installazione incompleta.
local m found=0 lost=0
for m in $(required_model_files); do
if [ -f "$MODELS_PATH/$m" ]; then found=$((found + 1)); else lost=$((lost + 1)); fi
done
if [ "$lost" = "0" ] && [ "$found" -gt 0 ]; then
ok "Modelli necessari: $found presenti."
else
warn "Modelli necessari: $found presenti, $lost mancanti."
fi
local extra_found=0 extra_lost=0
for m in $(optional_model_files); do
if [ -f "$MODELS_PATH/$m" ]; then
extra_found=$((extra_found + 1))
else
extra_lost=$((extra_lost + 1))
fi
done
if [ "$extra_lost" = "0" ]; then
if [ "$extra_found" -gt 0 ]; then ok "Modelli facoltativi: $extra_found presenti."; fi
else
info "Modelli facoltativi: $extra_found presenti, $extra_lost non scaricati (va bene così)."
fi
# -- caratteri -------------------------------------------------------------
local nf; nf="$(find "$REPO_DIR/vendor/fonts" -mindepth 1 -maxdepth 1 -type d 2>/dev/null | wc -l | tr -d ' ')"
if [ "$nf" -ge 22 ]; then ok "Caratteri: $nf famiglie."
elif [ "$nf" -gt 0 ]; then warn "Caratteri: solo $nf famiglie su 22."
else err "Caratteri assenti: esegui \"$FONT_SCRIPT\"."
fi
# -- configurazione --------------------------------------------------------
if [ -f "$CFG_PATH" ] && C="$CFG_PATH" python3 -c 'import json,os;json.load(open(os.environ["C"]))' >/dev/null 2>&1; then
ok "Configurazione leggibile: $CFG_PATH"
else
err "Configurazione assente o illeggibile: $CFG_PATH"
fi
}
print_summary() {
printf '\n%s────────────────────────────────────────────────────────────%s\n' "$C_DIM" "$C_RESET"
printf '%sRiepilogo%s\n\n' "$C_BOLD" "$C_RESET"
local line status label detail mark color
if [ "${#SUMMARY_LINES[@]}" -gt 0 ]; then
for line in "${SUMMARY_LINES[@]}"; do
status="${line%%$'\t'*}"
label="$(printf '%s' "$line" | cut -f2)"
detail="$(printf '%s' "$line" | cut -f3)"
case "$status" in
ok) mark="pronto" ; color="$C_GREEN" ;;
warn) mark="parziale" ; color="$C_YELLOW" ;;
*) mark="da sistemare" ; color="$C_RED" ;;
esac
printf ' %s%-12s%s %-16s %s\n' "$color" "$mark" "$C_RESET" "$label" "$detail"
done
fi
printf '\n'
if [ "$ESSENTIAL_MISSING" = "0" ]; then
printf '%s✓ Tutto pronto.%s\n\n' "$C_GREEN$C_BOLD" "$C_RESET"
printf ' Apri pi e prova:\n'
printf ' %s/poster%s una locandina completa, dal titolo al PDF per la stampa\n' "$C_BOLD" "$C_RESET"
printf ' %s/presets%s salva i tuoi colori, caratteri e logo\n\n' "$C_BOLD" "$C_RESET"
else
if [ "$ESSENTIAL_MISSING" = "1" ]; then
printf '%s! Manca ancora una cosa.%s\n\n' "$C_YELLOW$C_BOLD" "$C_RESET"
else
printf '%s! Mancano ancora %d cose.%s\n\n' "$C_YELLOW$C_BOLD" "$ESSENTIAL_MISSING" "$C_RESET"
fi
printf ' Sistema le voci «da sistemare» qui sopra e rilancia:\n'
printf ' %s./install.sh%s (ripara ciò che manca, non tocca i tuoi preset)\n\n' "$C_BOLD" "$C_RESET"
fi
printf ' %sConfigurazione:%s %s\n' "$C_DIM" "$C_RESET" "$CFG_PATH"
printf ' %sModelli:%s %s\n' "$C_DIM" "$C_RESET" "${MODELS_PATH:-non impostata}"
printf ' %sDestinazione:%s %s\n\n' "$C_DIM" "$C_RESET" "${OUT_DIR:-non impostata}"
}
# ---------------------------------------------------------------------------
# main
# ---------------------------------------------------------------------------
main() {
printf '\n%spi-imgen — installazione%s\n' "$C_BOLD" "$C_RESET"
printf '%sLocandine, loghi e immagini per eventi. Tutto in locale.%s\n' "$C_DIM" "$C_RESET"
read_existing_config
if [ "$DOCTOR_ONLY" = "1" ]; then
MODELS_PATH="$(expand_tilde "${EXIST_MODELS_PATH:-$HOME/Documents/Models}")"
OUT_DIR="$(expand_tilde "${EXIST_OUT_DIR:-$DEFAULT_OUT_DIR}")"
SERVER_TARGET="${EXIST_SERVER_BINARY:-$STATE_DIR/$GRPC_SERVER_BINARY}"
SERVER_TARGET="$(expand_tilde "$SERVER_TARGET")"
STEP_NO=7
doctor
printf '\n'
exit 0
fi
check_system
ensure_homebrew
ensure_grpc_server
prompt_paths
fetch_fonts
prefetch_models
write_config
doctor
print_summary
[ "$ESSENTIAL_MISSING" = "0" ] || exit 1
}
main "$@"