LocalAI/PLAN-multimodal.md

228 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Plan: Transkription + Bildgenerierung, Anbindung an Laravel (laravel/ai)
> **Stand der Umsetzung (2026-10-15) – Server fertig, Laravel-Teil dokumentiert:**
> - **Weder A noch B, sondern Variante C:** Vulkan schafft nur 54 % der ROCm-Prompt-Verarbeitung (Messwerte unten). Deshalb baut [`llama-swap/Dockerfile`](llama-swap/Dockerfile) auf dem ROCm-Image auf und kopiert `whisper-server` + `sd-server` aus `unified-vulkan-265` dazu. LLMs laufen weiter auf ROCm, Whisper und Bilder über Vulkan, und alles verwaltet ein llama-swap.
> - Vulkan-Gerät 0 ist die **iGPU**. Die RX 6950 XT ist Gerät 1 → `VK_VISIBLE_DEVICES=1`.
> - **Whisper läuft auf der GPU statt der CPU:** 0,46 s statt 3,3 s (CPU, 12 Threads, `-l de`) bzw. 9,3 s (CPU, 8 Threads, `-l auto`) für 6,6 s Sprache. Es braucht ~1,2 GB VRAM und kostet die Chat-Geschwindigkeit nachweislich nichts (852 t/s mit und ohne).
> - ffmpeg ist im ROCm-Image schon enthalten, `--convert` funktioniert: WAV, WebM/Opus und M4A/AAC werden fehlerfrei transkribiert.
> - Bilder: Z-Image-Turbo Q8, VAE von `Comfy-Org/z_image_turbo` (nicht gated). Die Dauer hängt an der GPU-Rechenleistung (~4 s/Schritt bei 1024²). `--offload-to-cpu` / `--backend te=cpu` ändern daran nichts, `--offload-to-cpu` bleibt (spart VRAM).
> - OpenAI-Format getestet: Alias `gpt-image-1`, `size: 1536x1024`, `quality` (wird ignoriert), ohne `response_format` → `b64_json`.
> - `scripts/start.sh`: baut das Image, lädt Whisper vor, `--pull` aktualisiert die Basis-Images und baut neu.
> - Phase 5 (Laravel) ist als Anleitung in [`docs/laravel.md`](docs/laravel.md) umgesetzt. In einer echten Laravel-App ist sie **noch nicht getestet** (offen: wo die App läuft).
> - Nebenbefund, älter als dieser Plan: Der Startup-Preload von llama-swap meldet für `qwen2.5-coder-1.5b`/`nomic-embed` „status 404“ (llama-server ohne Web-UI antwortet auf `/` mit 404). Die Modelle laden trotzdem.
## Context
Eine Laravel-Anwendung soll über das **Laravel AI SDK** (`laravel/ai`) diesen Server nutzen:
1. **Agent** (Chat, Tools, Streaming, Embeddings). Das geht schon heute ohne Server-Änderung.
2. **Spracheingabe**: Aufnahmetaste im Browser → Transkription (Whisper) → Text als Prompt an den Agenten.
3. **Bildgenerierung**, auch als Tool des Agenten.
Was das SDK laut Doku (https://laravel.com/docs/ai-sdk) kann:
| Funktion | Treiber `openai-compatible` | Bemerkung |
|---|---|---|
| Text, Streaming, Tools, Structured Output | ✅ | |
| Embeddings | ✅ | Standardmodell in der Config nötig |
| Transkription (STT) | ✅ | Standardmodell in der Config nötig, keine Sprecher-Trennung (Diarization) |
| Bilder | ❌ | Umweg: Treiber `openai` mit eigener `url` oder ein eigener HTTP-Aufruf |
| TTS | ❌ | nicht Teil dieses Plans |
## Kernproblem: Image-Wahl (ROCm vs. Vulkan)
Das aktuelle Image `llama-swap:vXXX-rocm-bYYYY` enthält **nur llama-server**.
`whisper-server` und `sd-server` gibt es nur im **unified**-Image, und das für AMD nur als
**`unified-vulkan`** (versionierte Tags: `unified-vulkan-NNN`). Ein ROCm-unified-Image gibt es nicht.
| | A: alles auf `unified-vulkan` (**bevorzugt**) | B: ROCm bleibt, Whisper separat |
|---|---|---|
| Container | 1 × llama-swap | llama-swap (ROCm) + whisper-Container (CPU) |
| VRAM-Koordination Chat ↔ Bildmodell | llama-swap übernimmt das (Gruppe `chat`) | keine. Bildmodell und Chat-Modell würden sich um VRAM streiten, **Bilder fallen in B weg** |
| Routing | alles über llama-swap | Caddy leitet `/v1/audio/*` am llama-swap vorbei |
| Risiko | Vulkan ist evtl. langsamer als ROCm, deshalb Benchmark | mehr Teile, keine Bilder |
**Entscheidung über Benchmark (Phase 1):** Kommt Vulkan bei Token-Generierung **und** Prompt-Verarbeitung
auf ≥ ~85 % von ROCm, dann Variante A. Sonst Variante B, und die Bilder bleiben vorerst weg.
---
## Phase 1: Benchmark Vulkan vs. ROCm (~1 h)
1. Den Tag ermitteln: https://github.com/mostlygeek/llama-swap/pkgs/container/llama-swap → neuestes `unified-vulkan-NNN`.
2. Messen, während der laufende Stack aus ist (`docker compose stop`), mit denselben Modelldateien aus `./models`:
```bash
docker run --rm --device /dev/dri --group-add 987 --group-add 983 \
-e GGML_VK_VISIBLE_DEVICES=0 -v ./models:/models \
--entrypoint llama-bench ghcr.io/mostlygeek/llama-swap:unified-vulkan-NNN \
-m /models/<Pfad zur Qwen3.5-35B-A3B-Q4_K_M.gguf> -p 2048 -n 128 -fa 1
```
Dasselbe mit dem ROCm-Image (`/dev/kfd` dazu, `HIP_VISIBLE_DEVICES=0`). Den GGUF-Pfad unter
`models/models--unsloth--Qwen3.5-35B-A3B-GGUF/snapshots/*/` suchen.
- Ob `llama-bench` in beiden Images vorhanden ist, prüfen. Falls nicht: `llama-server` starten und die t/s aus dem Log einer Test-Anfrage nehmen.
- Wichtig: Für den 35B-MoE muss die Auslagerung in den RAM gleich sein. Notfalls mit `--n-cpu-moe` vergleichbar festlegen.
3. `GGML_VK_VISIBLE_DEVICES=0`: Der 7900X3D hat eine iGPU, die Vulkan sonst als zweites Gerät sieht.
Mit `vulkaninfo --summary` bzw. dem Startlog prüfen, dass Gerät 0 die RX 6950 XT ist.
4. Die Ergebnisse unten unter „Messwerte“ eintragen und A oder B entscheiden.
## Phase 2 (A): Stack auf `unified-vulkan` umstellen
- `.env` / `.env.example`: `LLAMA_SWAP_TAG=unified-vulkan-NNN`. Den alten ROCm-Tag als Kommentar behalten (Rollback).
- `docker-compose.yml` (llama-swap):
- `devices`: nur noch `/dev/dri` nötig (`/dev/kfd` schadet nicht, kann aber weg).
- `environment`: `GGML_VK_VISIBLE_DEVICES: "${VK_VISIBLE_DEVICES:-0}"` statt bzw. zusätzlich zu `HIP_VISIBLE_DEVICES`.
- Prüfen, ob das Image mit `user: 1000:1000` läuft. Es gibt auch `-rootless`-Varianten.
- Prüfen, ob Binärpfade (`/app/llama-server` vs. `llama-server` im PATH) zum `server`-Makro passen.
- `llama-swap/config.yaml`: Bestehende Modelle unverändert lassen. Prüfen, ob `--fit` / `--fit-target` unter Vulkan genauso greift (VRAM mit `rocm-smi` beobachten).
- Gegenprobe: opencode + Continue (Chat, Autocomplete, `@codebase`) funktionieren wie vorher.
- README: Tabelle „Auf einen anderen PC übertragen“ anpassen (unified-vulkan ist jetzt Standard).
## Phase 3: Whisper (Transkription)
1. Modell herunterladen (whisper-server kann kein `-hf`):
```bash
mkdir -p models/whisper
curl -L -o models/whisper/ggml-large-v3-turbo-q8_0.bin \
https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q8_0.bin
```
2. Eintrag in `llama-swap/config.yaml`, **auf der CPU** und in `always-on`, damit er nie das Chat-Modell verdrängt:
```yaml
whisper:
name: Whisper large-v3-turbo (CPU)
checkEndpoint: / # prüfen: hat whisper-server /health?
cmd: >-
whisper-server --host 127.0.0.1 --port ${PORT}
-m /models/whisper/ggml-large-v3-turbo-q8_0.bin
--inference-path /v1/audio/transcriptions
--convert --tmp-dir /tmp
-ng -t 8 -l auto
ttl: 0
```
- Die Flags mit `docker compose exec llama-swap whisper-server --help` prüfen: `--inference-path`, `--convert` (braucht **ffmpeg** im Image, mit `which ffmpeg` prüfen), `-ng`/`--no-gpu`, `--tmp-dir`.
- Ohne ffmpeg: `--convert` weglassen, dann wandelt Laravel vorher in 16-kHz-Mono-WAV um (siehe Phase 5).
- `whisper` zu `routing…groups.always-on.members` und zu `hooks.on_startup.preload` hinzufügen.
3. `scripts/start.sh`: Beim Preload einen Whisper-Request schicken. Dafür eine kurze Test-WAV unter `scripts/` ablegen, z. B. 1 s Stille mit `ffmpeg -f lavfi -i anullsrc=r=16000:cl=mono -t 1 scripts/silence.wav`.
4. Test:
```bash
curl -H "Authorization: Bearer $KEY" http://127.0.0.1:8800/v1/audio/transcriptions \
-F model=whisper -F file=@test.webm -F response_format=json
```
Mit einer echten deutschen Sprachaufnahme testen und die Dauer bei ~10 s Audio notieren (Ziel < 3 s).
5. Caddy: keine Änderung nötig. Es gibt standardmäßig kein Upload-Limit, und der `read_timeout` von 30 min reicht.
## Phase 4: Bildgenerierung (nur Variante A)
1. Modell: **Z-Image-Turbo** (Vorlage aus `docker/config.example.yaml` von llama-swap, 8 Steps, schnell).
Benötigt drei Dateien in `models/sd/`: `z_image_turbo-Q8_0.gguf`, `ae.safetensors` (VAE), `qwen3-4b-instruct-2507-q8_0.gguf` (Text-Encoder).
Die Hugging-Face-Quellen beim Umsetzen heraussuchen. Alternative: FLUX.1-schnell (GGUF).
2. Eintrag in der **`chat`-Gruppe**, damit llama-swap das Chat-Modell entlädt, solange ein Bild generiert wird:
```yaml
z-image:
name: Z-Image Turbo (Bilder)
checkEndpoint: /
cmd: |
sd-server --listen-ip 127.0.0.1 --listen-port ${PORT}
--diffusion-fa
--diffusion-model /models/sd/z_image_turbo-Q8_0.gguf
--vae /models/sd/ae.safetensors
--llm /models/sd/qwen3-4b-instruct-2507-q8_0.gguf
--offload-to-cpu --cfg-scale 1.0 --steps 8
--height 1024 --width 1024
aliases: [gpt-image-1, dall-e-3]
ttl: 600
```
- Die Flags mit `sd-server --help` prüfen, besonders den Namen von `--listen-ip`.
- Die `aliases` sind wichtig: Der OpenAI-Treiber von Laravel schickt seinen eigenen Modellnamen.
3. Test: `curl …/v1/images/generations -d '{"model":"z-image","prompt":"a red bicycle","response_format":"b64_json"}'`.
Dabei messen: Dauer inkl. Modellwechsel, VRAM-Spitze, ob Autocomplete (Continue) weiterläuft.
4. **Hinweis für den Alltag:** Jede Bildgenerierung wirft das Chat-Modell raus (Neuladen dauert einige zehn Sekunden).
Für einen Agenten, der Bilder als Tool erzeugt, heißt das: Chat → Bild → Chat bedeutet zwei Modellwechsel.
## Phase 5: Laravel-Anwendung
### 5.1 Config (`config/ai.php` + `.env`)
```php
'providers' => [
// Text, Agent, Embeddings, Transkription
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'), // http://<server>:8800/v1
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'text' => ['default' => 'qwen3.5-35b-a3b'],
'embeddings' => ['default' => 'nomic-embed', 'dimensions' => 768],
'transcription' => ['default' => 'whisper'],
],
],
// nur für Bilder: OpenAI-Treiber auf den eigenen Server umgebogen
'local-images' => [
'driver' => 'openai',
'key' => env('LOCAL_AI_API_KEY'),
'url' => env('LOCAL_AI_URL'),
],
],
```
- Den Env-Namen für die URL-Überschreibung je nach SDK-Version prüfen (Doku 13.x: `url`/`OPENAI_URL`, 12.x: `OPENAI_BASE_URL`).
- Den Schlüssel prüfen, mit dem `local` zum Standard-Provider für Transkription wird (`Transcription` hat in der Doku keinen `provider:`-Parameter).
- Netz: Der Laravel-Server muss in `ALLOWED_NETS` liegen, im LAN oder als NetBird-Peer.
- Timeouts großzügig setzen (`timeout: 300` beim Prompt), Agent-Läufe mit Tools als Queue-Job.
### 5.2 Agent
- `php artisan make:agent Assistant`: Anweisungen, Tools.
- Smoke-Test: `(new Assistant)->prompt('Hallo', provider: 'local')`, danach ein Tool-Call-Test.
### 5.3 Spracheingabe (Aufnahmetaste)
- **Frontend:** `getUserMedia` + `MediaRecorder` (gedrückt halten = aufnehmen). Bevorzugt `audio/webm;codecs=opus`, Safari liefert `audio/mp4`.
Den Blob per `FormData` an `POST /voice` schicken, die Antwort als Stream lesen.
- **HTTPS ist Pflicht.** Das Mikrofon geht nur in einem sicheren Kontext. Die Laravel-App braucht deshalb HTTPS, lokal geht auch `localhost`.
- **Backend:**
```php
Route::post('/voice/transcribe', fn (Request $r) =>
['text' => (string) Transcription::fromUpload($r->file('audio'))->generate()]);
Route::post('/voice/ask', fn (Request $r) =>
(new Assistant)->stream($r->string('text')));
```
Zwei Schritte, damit der User den erkannten Text **vor** dem Absenden sieht und korrigieren kann.
Das ist wichtig bei Agenten, deren Tools etwas ausführen.
- Wenn `whisper-server` kein `--convert` kann: In Laravel vorher umwandeln mit
`ffmpeg -i in.webm -ar 16000 -ac 1 out.wav` (Symfony Process), dann `Transcription::fromPath(...)`.
- Validierung: Upload-Limit (z. B. 20 MB), Mindestlänge, leere Transkripte abfangen.
### 5.4 Bilder
- Direkt: `Image::of('...')->generate(provider: 'local-images')` (die genaue API in der Doku nachsehen).
Falls der OpenAI-Treiber Parameter schickt, die `sd-server` ablehnt: Fallback auf `Http::withToken(...)->post(.../images/generations)`
und `b64_json` selbst in den Storage schreiben.
- Als Agent-Tool: `GenerateImage`-Tool, das eins von beidem aufruft und eine URL bzw. einen Storage-Pfad zurückgibt.
## Phase 6: Doku im Repo
- `docs/laravel.md`: Config, Netz/Key, Spracheingabe, Bilder, Stolperfallen (HTTPS, Formate, Modellwechsel).
- README: Modelltabelle um `whisper` und `z-image` ergänzen, Images und Rollback beschreiben.
- `PLAN.md`-Stil: Abweichungen oben in diesem Plan unter „Stand der Umsetzung“ festhalten.
## Verifikation
1. opencode + Continue funktionieren unverändert (nach dem Image-Wechsel!), die t/s liegen im erwarteten Bereich.
2. `GET /v1/models` listet `whisper` und `z-image`.
3. Transkription per curl (deutsch, webm und wav) liefert korrekten Text. Im Log wird dabei **kein** Chat-Modell entladen.
4. Bild per curl: PNG kommt zurück, danach lädt das Chat-Modell beim nächsten Request wieder.
5. Laravel: Agent-Prompt mit Tool-Call, Spracheingabe Ende-zu-Ende im Browser (Chrome + Firefox, wenn möglich Safari), Bild über das Agent-Tool.
6. Nach einem Reboot ist Whisper vorgeladen (`/running`).
## Rollback
`LLAMA_SWAP_TAG` in `.env` zurück auf `v265-rocm-b11610`, `/dev/kfd` + `HIP_VISIBLE_DEVICES` wieder rein,
die Einträge `whisper`/`z-image` auskommentieren, `docker compose up -d`.
## Messwerte (in Phase 1 ausfüllen)
| Image | pp2048 t/s | tg128 t/s | Bemerkung |
|---|---|---|---|
| ROCm `v265-rocm-b11610` | ~1030 | ~49,8 | nur 35B geladen, `--fit-target 3072` |
| `unified-vulkan-265` | ~555 | ~43,2 | gleiche Bedingungen |
| ROCm im Betrieb (mit Autocomplete + Embeddings) | ~850 | ~47,5 | identisch mit und ohne Whisper auf der GPU |
Gemessen mit `llama-server` (Betriebs-Parameter), 2302 Prompt-Tokens, 256 generierte Tokens, `cache_prompt: false`.
| Bilder (Z-Image-Turbo Q8, 8 Schritte) | Dauer |
|---|---|
| 512×512 | ~11 s |
| 1024×1024 | ~40 s (Diffusion 32 s, VAE 6 s) |
| 1536×1024 | ~66 s |
| erster Request inkl. Laden | +4 s |
| Chat-Modell danach zurückladen | ~5 s |
| VRAM-Spitze beim Bild | 8,4 GB (mit Autocomplete + Whisper) |
## Offene Punkte
- Wo läuft die Laravel-App (gleiches LAN, NetBird, öffentlicher Server)? Davon hängen `ALLOWED_NETS` und HTTPS ab.
- Bildmodell: Reichen Qualität und Geschwindigkeit von Z-Image-Turbo, oder lieber FLUX.1-schnell?
- Später ggf. TTS (die Antwort vorlesen): audio.cpp ist im unified-Image, aber das Laravel SDK kann TTS nicht über `openai-compatible`.