# 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/ -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://: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`.