ki integration

This commit is contained in:
2026-08-15 13:43:52 +02:00
parent 82d98bd8cf
commit 9ecb292c26
78 changed files with 12055 additions and 55 deletions

View File

@@ -10,13 +10,13 @@
| | Stand |
|---|---|
| **Appwrite-Schema** | 15 Tabellen, Indizes, 4 Buckets, Team `internal` angelegt durch `scripts/setup-appwrite.mjs`, idempotent |
| **Appwrite-Schema** | **22 Tabellen, Indizes, 5 Buckets, Team `internal`** live und vollständig, `scripts/setup-appwrite.mjs` reproduziert diesen Stand idempotent (Stand 14.08.2026) |
| **Prompts** | P1P18 als 22 Zeilen geseedet (P8 = Kern + 4 Adapter) via `scripts/seed-prompts.mjs`, versioniert |
| **Regeln** | 4 globale Brand-Regeln in `rules` |
| **Prototyp** | `prototyp-app.html`, 21 Screens, Handy-Ansicht, vollständig ohne Backend |
| **App-Code** | **existiert noch nicht** es gibt einen Expo-QR-Code, aber keine Anwendung |
**Der entscheidende Punkt:** Das Schema im Repo bildet den Stand **vor** dem Bild-/Feed-Konzept ab. Es fehlen alle Tabellen des neuen Produktteils. Wer jetzt anfängt, UI zu bauen, baut gegen Tabellen, die es nicht gibt.
> **Nachtrag 14.08.2026 E0 ist erledigt.** Der Abschnitt unten beschreibt den Stand vom 28.07. und ist ab hier **Historie, keine Aufgabenliste mehr**. Die Live-Datenbank war bereits vollständig migriert (alle 7 neuen Tabellen, alle 4 Enum-Erweiterungen, `attributes` aufgetrennt, 5. Bucket) nur `setup-appwrite.mjs` im Repo hing auf dem alten Stand hinterher und hätte beim Ausführen die Score-Spalten in `attributes` wieder angelegt. Das Skript ist inzwischen auf den Live-Stand gezogen und gegen ihn geprüft: **0 angelegt, 251 existierten schon, 0 Warnungen.** Auch die beiden Enum-Entscheidungen aus §7.1 (Nischen-Liste, `typ_tags`) sind getroffen und stehen als `NISCHEN` und `TYP_TAGS` im Skript.
---
@@ -77,20 +77,39 @@ Dazu die aus dem alten Plan noch offenen: `elo-update`, `favorit-berechnen`, `ad
Jede Etappe hat ein Abnahmekriterium etwas, das man vorführen kann. Ohne das ist nicht entscheidbar, ob sie fertig ist.
### E0 · Schema nachziehen
**Inhalt:** `setup-appwrite.mjs` um die 7 neuen Tabellen, die geänderten Spalten, die Enum-Erweiterungen und den Bucket ergänzen. `attributes` auftrennen. `P_KEYS` auf 22. Skript einmal komplett gegen eine **leere** Datenbank laufen lassen.
**Abnahme:** Skript läuft zweimal hintereinander fehlerfrei durch (idempotent), 22 Tabellen und 5 Buckets stehen in der Appwrite-Konsole.
**Aufwand:** klein. Reine Skriptarbeit, kein UI.
### E0 · Schema nachziehen ✅ **erledigt (14.08.2026)**
**Inhalt:** `setup-appwrite.mjs` um die 7 neuen Tabellen, die geänderten Spalten, die Enum-Erweiterungen und den Bucket ergänzen. `attributes` auftrennen. `P_KEYS` auf 22.
**Abnahme erfüllt:** 22 Tabellen und 5 Buckets stehen live; das Skript läuft ohne eine einzige Änderung durch (`0 angelegt, 251 existierten schon, 0 Warnungen`).
**Datenbestand:** nur Stammdaten 4 Zeilen `rules`, 22 Zeilen `prompt_templates` (P1P18, P8 als Familie). Keine Brand-Daten. **Enum-Änderungen sind ab jetzt trotzdem nicht mehr gefahrlos**, weil `prompt_templates.key` gefüllt ist.
### E1 · Projektgerüst Expo
**Inhalt:** Expo-Projekt anlegen, TypeScript, Navigation (Tabs + Stacks), Appwrite-SDK verdrahten, Theme aus dem Prototyp übernehmen (Farben, Typo, Glas-Effekte **ohne** die Fake-Tastatur und ohne die Shader-Spielerei per `setInterval`).
**Abnahme:** App startet auf Handy und im Browser, drei Tabs sind klickbar, jeder Tab zeigt einen leeren Screen mit seinem Namen.
**Aufwand:** mittel. Hier entscheidet sich die Ordnerstruktur des Codes sorgfältig sein.
> **Lehre aus dieser Etappe:** Die Datenbank war der Quelle voraus, nicht umgekehrt. Wer `setup-appwrite.mjs` in diesem Zustand ausgeführt hätte, hätte die sieben Score-Spalten in `attributes` neu angelegt und die Auftrennung halb zurückgedreht ein idempotentes Skript schützt nur vor doppeltem Anlegen, nicht vor einer veralteten Definition. Deshalb: **nach jeder Schema-Änderung an der Konsole das Skript nachziehen und mit `scripts/check-appwrite.mjs` gegenprüfen.**
### E2 · Auth und Mandant
**Inhalt:** Registrieren, Anmelden, Abmelden. Bei der Registrierung: Appwrite-Team anlegen, `brands`-Zeile anlegen, Zeilenrechte setzen. Session-Persistenz.
**Abnahme:** Zwei Konten anlegen; Konto B sieht **keine** Daten von Konto A geprüft über die API, nicht über das UI.
**Aufwand:** mittel. Die Rechte-Prüfung ist der eigentliche Inhalt, nicht die Formulare.
### E1 · Projektgerüst Expo ✅ **erledigt im Browser (14.08.2026), nativ ungeprüft**
**Inhalt:** Expo-Projekt in `client/`, TypeScript, Expo Router (Stack + Tabs), Appwrite-SDK verdrahtet, Theme aus dem Prototyp. Fake-Tastatur und `setInterval`-Shader sind bewusst nicht übernommen.
**Abnahme:** `npx tsc --noEmit` fehlerfrei · `npx expo export --platform web` erzeugt alle drei Routen · im Browser rendern `/`, `/erstellen` und `/profil` mit Tab-Leiste, keine Konsolen-Fehler. **Offen: der Start auf einem echten Gerät** dafür fehlt ein Testgerät bzw. ein Dev-Build.
**Struktur (`client/src/`):**
| Pfad | Inhalt |
|---|---|
| `app/_layout.tsx` | Wurzel-**Stack**, nicht direkt die Tabs Willkommen/Anmelden liegen laut app-aufbau.md §3 vor den Tabs, das Onboarding als Modal darüber. Beide brauchen eine Ebene ohne Tab-Leiste. |
| `app/(tabs)/` | Feed · Erstellen · Profil |
| `lib/appwrite.ts` + `.web.ts` | plattform-getrennter Client. `react-native-appwrite` läuft nicht im Browser, das Web-SDK `appwrite` kennt kein React Native beide exportieren dieselben Klassen, der Rest der App importiert nur `@/lib/appwrite`. |
| `lib/config.ts` | Endpoint, Projekt-ID, DB-ID aus `app.json``expo.extra.appwrite`. **Kein Server-Key** der landet sonst im Bundle. |
| `theme/tokens.ts` | Farben, Radien, Abstände aus `prototyp-app.html`. Dark-only, weil der Prototyp kein Light-Theme hat. |
**Zwei bewusste Abweichungen:** Das ist vorerst ein normaler Tab statt des Popovers aus §3 der kommt mit den drei Erstellen-Abläufen in E6. Und der Profil-Screen zeigt provisorisch Endpoint und Projekt-ID, damit belegt ist, dass der Client wirklich lädt; das fliegt in E2 raus.
### E2 · Auth und Mandant ✅ **erledigt (14.08.2026)**
**Inhalt:** Willkommen/Registrieren/Anmelden/Abmelden, Session-Persistenz, bei der Registrierung Team + `brands`-Zeile mit Zeilenrechten.
**Abnahme erfüllt:** `scripts/test-mandanten.mjs` legt zwei echte Konten an und prüft **über die API**: B listet nur die eigene Zeile · Direktzugriff B→A `404 row_not_found` · Schreibzugriff B→A `401 user_unauthorized` · Gegenprobe, dass der Server-Key beide Zeilen sieht. Räumt sich selbst auf.
**Der eigentliche Inhalt war das Rechte-Modell, nicht die Formulare.** Zwei Dinge, die vorher niemandem aufgefallen waren:
1. **Kein Client konnte irgendetwas anlegen.** Bei `rowSecurity: true` regeln Zeilenrechte lesen/ändern/löschen aber eine Zeile, die es noch nicht gibt, hat keine Rechte. Ohne Tabellen-Recht zum Anlegen war jede Client-Schreiboperation blockiert. 16 Tabellen haben jetzt `create("users")`; `score_events`, `video_metrics`, `post_metrics` und `usage_records` bewusst **nicht** sonst könnte ein Nutzer seine eigenen Elo-Werte und Feed-Signale fälschen (§11, „Manipulation des Feed-Scores").
2. **`setup-appwrite.mjs` hat Rechte nie abgeglichen.** Bei bestehenden Tabellen lieferte `POST` nur ein 409, die Rechte blieben unangetastet. Das Skript vergleicht jetzt und zieht per `PUT` nach sonst driftet das Rechte-Modell genauso wie zuvor das Schema.
**Nebenfund: SDK-Version passte nicht zum Server.** Appwrite läuft in **1.8.1**, `create-expo-app` hatte SDKs mit Response-Format 1.9.5 gezogen. Gepinnt auf `appwrite@23.0.0` und `react-native-appwrite@0.25.0` (beide Response-Format 1.8.0), **exakt statt Caret** der Sinn der Pinnung ist ja gerade der Gleichstand mit dem Server.
> Das ist die Etappe, bei der man am ehesten schludert und es am teuersten wird. Die Mandantentrennung ist das Fundament des Rechte-Modells aus `datenbank-aufbau.md` §5.
@@ -99,20 +118,51 @@ Jede Etappe hat ein Abnahmekriterium etwas, das man vorführen kann. Ohne da
**Abnahme:** Nach dem Onboarding stehen in `brands` sechs ausgefüllte Felder und in `attribute_scores` die ersten Attribute auf der brand-weiten Ebene (`folder_id = null`).
**Aufwand:** mittel.
### E4 · Modelle
### E4 · Modelle 🟡 **Kern steht (14.08.2026), Verbessern-Strecke offen**
**Inhalt:** Modell anlegen (Person, Produkt, **Kulisse**), echte Referenzbild-Uploads, Versionierung mit Release-Prinzip, Modelle-Übersicht im Profil.
**Abnahme:** Ein Produkt-Modell mit 4 Referenzbildern anlegen, freigeben, in der Übersicht sehen. `assets.released_version_id` zeigt auf die richtige Version.
**Aufwand:** mittel. Ohne Modelle kann nichts generiert werden deshalb vor der Generierung.
**Erledigt:** Anlegen mit Typwahl und Mehrfach-Bildauswahl (`modell/neu`), Upload nach `asset-references` mit Team-Rechten, Version 1 wird angelegt und sofort freigegeben, `assets.released_version_id` zeigt darauf. Übersicht im Profil nach Typ gruppiert, mit Titelbild aus der freigegebenen Version.
**Belegt:** Vier Modelle (2 Produkte, 2 Kulissen) über `scripts/seed-demo.mjs` als **Client** angelegt das beweist nebenbei, dass die Tabellen- und Bucket-Rechte aus E2 ausreichen. Alle vier Bilder laden in der App in Originalgröße.
**Offen:** weitere Versionen anlegen und freigeben (Verbessern-Strecke), `merkmale` als Token-Lock, Einwilligungs-Upload für Personen.
### E5 · Ordner
### E5 · Ordner ✅ **erledigt (14.08.2026)**
**Inhalt:** Ordner-Liste, Anlegen mit den zwei Schaltern, Ordner-Detail, aktiver Ordner als App-Zustand. Function `ordner-initialisieren`.
**Abnahme:** Zwei Ordner anlegen einen mit `erben`, einen mit `aus_posts`. In `attribute_scores` stehen danach unterschiedliche Startwerte mit korrekt gesetzter `start_quelle`.
**Aufwand:** mittel. Fachlich der anspruchsvollste Teil bis hierher.
**Abnahme erfüllt** nach dem Anlegen über die Oberfläche steht in `attribute_scores`:
### E6 · Post erstellen (erste echte Generierung)
| Ordner | Modus | Scores | `start_quelle` |
|---|---|---|---|
| (brand-weit, `folder_id = null`) | | 12 | `neutral` |
| Sommerkampagne | `erben` | 12 | `geerbt` |
| Cleane Studioshots | `aus_posts` | 0 | |
| Herbstlinie (über die App angelegt) | `erben` | 12 | `geerbt` |
Zweck und Startwerte sind als Auswahl **mit Erklärung** umgesetzt, nicht als nackter Schalter: wer sie missversteht, baut sich einen Ordner, der nicht tut, was er erwartet.
**Beim Erben wird der Wert kopiert, aber nicht die Sicherheit** `k_factor` geht zurück auf 32, weil im neuen Scope noch nichts belegt ist.
**Abweichung:** `ordner-initialisieren` läuft vorerst im Client statt als Appwrite-Function. Vertretbar, weil nur eigene Zeilen kopiert werden und die Zeilenrechte das ohnehin begrenzen; beim Umzug in eine Function ändert sich nur der Ort.
### E6 · Post erstellen 🟡 **Rezept-Hälfte steht (14.08.2026), Generierung extern blockiert**
**Inhalt:** Create-Screen mit Ordner, Format, Kette, Slots. `jobs` + `job-dispatcher` + Realtime-Wartezustand. P7 auf das Slot-System umbauen. P22 als Bild-Tagger. Post-Ergebnis mit privat/veröffentlichen.
**Abnahme:** Ein Prompt erzeugt eine dreiteilige Bilderkette, die im Entwürfe-Screen auftaucht, auch wenn man die App zwischendurch schließt.
**Aufwand:** groß. Hier hängt die Auswahl des Bildmodells dran **offener Punkt 1**.
**Erledigt:** `erstellen/post` mit Ordnerwahl in der Kopfzeile, Format, Kettenlänge und Slot-Auswahl für Kulisse, Produkt und Person als Bildkacheln, weil bei Kulissen und Produkten das Aussehen die Information ist und ein Name wie „Halle, Metallwand" nichts darüber sagt, ob es passt. Der Post entsteht **sofort** mit `status: entwurf`, bevor irgendetwas generiert wird; ohne diese Zeile gäbe es keinen Ort, an den man nach dem Warten zurückkehrt. Je Kettenbild eine `jobs`-Zeile. Ordner-Detail zeigt die Posts mit aufgelösten Slot-Namen.
**Nachweis über die Oberfläche:** Post „Serum auf Waschtisch" → Ordner Herbstlinie, `4:5`, 3 Bilder, `privat`, Slots Halle-Metallwand + Refine-&-Renew-Serum, dazu **3 Jobs `bild_gen/wartend` mit `prompt_template_key = P7`**.
**Job-Dispatcher steht** `scripts/job-dispatcher.mjs`, serverseitig, weil der Anbieter-Schlüssel nicht ins Client-Bundle darf. Er baut aus Slot-Rezept, den Beschreibungen der freigegebenen Asset-Versionen, den Top-Attributen **des gewählten Ordners** und den globalen Regeln einen Prompt, erzeugt das Bild, legt es in `generated-images` mit Team-Rechten ab, schreibt `post_images` und setzt den Post auf `generiert`, sobald die Kette vollständig ist.
Drei Anbieter über `BILD_ANBIETER`:
| Wert | Ergebnis (geprüft 14.08.2026) |
|---|---|
| `stub` | ✅ erzeugt Platzhalterbilder **die ganze Kette ist damit prüfbar, ohne einen Cent auszugeben** |
| `ark` | ✗ `ModelNotOpen: account 3003959567 has not activated seedream-5-0` |
| `openrouter` | ✗ `Insufficient credits. This account never purchased credits` |
**Durchgespielt mit `stub`:** Post „Serum auf Waschtisch" → 3 Jobs → 3 Bilder erzeugt, hochgeladen, Post auf `generiert`, Kette im Ordner-Detail sichtbar (3/3 geladen). Bei den echten Anbietern greift der Fehlerpfad: Job auf `fehler` mit der Anbieter-Meldung, Post auf `fehler`.
**Damit fehlt nur noch die Freischaltung.** Sobald eines der beiden Konten offen ist, liefert `BILD_ANBIETER=ark node scripts/job-dispatcher.mjs` echte Bilder ohne Codeänderung.
**Offen:** Umzug des Dispatchers in eine Appwrite-Function (läuft jetzt lokal/per Cron), P7 als versionierter Prompt statt der Vorstufe im Skript, P22 als Tagger, Wartezustand mit Realtime, Ergebnis-Screen mit Veröffentlichen.
### E7 · Feed
**Inhalt:** Veröffentlichen mit Zeilen-Permission, Feed-Deck mit den drei Reitern, Post-Detail, `post_metrics` schreiben, Function `feed-score-berechnen` mit Decay und Explorations-Slot.
@@ -187,6 +237,6 @@ E0 Schema
## 7. Was vor E0 zu entscheiden ist
1. **Nischen-Liste** und **`typ_tags`-Liste** beide sind Enums und wandern in E0 ins Schema. Später zu ändern heißt: dieselbe Migrationsfalle wie oben.
2. **Bildmodell** blockiert nicht E0, aber E6. Der Test dafür sollte parallel zu E1E5 laufen.
1. ~~**Nischen-Liste** und **`typ_tags`-Liste**~~ ✅ entschieden und live. 20 Nischen, 17 Motiv-Tags, stehen als `NISCHEN` und `TYP_TAGS` in `scripts/setup-appwrite.mjs`.
2. **Bildmodell** blockiert nicht E0, aber E6. Der **Anbieter steht** (OpenRouter, siehe `projekt-uebersicht.md` §9); offen ist nur, welches der dortigen Bildmodelle die Multi-Referenz-Komposition trifft. Der Wegwerf-Test dafür sollte parallel zu E1E5 laufen.
3. **Mindestalter und Moderationsweg** blockiert nicht den Code, aber E7 im Livebetrieb.

View File

@@ -166,11 +166,26 @@ Homescreen → **Feed** · **Erstellen** (Szene / Post / Modell) · **Ordner**
| Hosting | Hetzner Cloud (MVP CX32 ~€7 → CX42 ~€16 ab ~25 Brands) |
| Video-Speicher | Hetzner Object Storage (S3-kompatibel) |
| Backend/DB | **Appwrite self-hosted** (Auth+Teams, TablesDB, Functions, Realtime, Storage mit S3-Adapter, Messaging) €0 Lizenz |
| KI-Gateway | **OpenRouter** (eine API für LLM *und* Video) |
| LLM | Claude (Scripts, Tagging, Vergleiche) · Gemini für P9 |
| Videomodelle | Seedance 2.0 + Veo 3.1 + Sora 2 / Wan |
| KI-Gateway | **Zwei Anbieter, klar getrennt:** **OpenRouter** für LLM *und* Bild · **BytePlus ModelArk** für Video |
| LLM | Claude (Scripts, Vergleiche) · Gemini für P9 **beides über OpenRouter**, Gemini dort mit nativem Video-Input |
| Bildmodelle | **noch offen** Kandidaten für den Test vor E6: `google/gemini-3-pro-image`, `openai/gpt-5-image` (OpenRouter) und `seedream-5-0-260128` (Ark **eu-west**, DSGVO-nah) |
| Videomodelle | **Seedance-Familie über BytePlus ModelArk** `dreamina-seedance-2-5` / `-2-0` / `-2-0-fast` / `-2-0-mini` · `seedance-1-5-pro` · `seedance-1-0-pro` / `-pro-fast` |
| Payment | Stripe (Abo + metered) |
> **Warum zwei Anbieter statt einem Gateway:** OpenRouter hat **kein** Modell mit Video-*Output* (geprüft am 14.08.2026 über `/api/v1/models`, 411 Modelle, keines mit `output_modalities: video`). Video-*Input* und Bild-*Output* kann OpenRouter dagegen sehr wohl deshalb läuft dort alles außer der Videogenerierung.
>
> **Ark-Regionen Keys sind regionsgebunden, jeder Key funktioniert nur in seiner eigenen Region** (geprüft am 14.08.2026):
>
> | Region | Endpoint | Modelle | Video? |
> |---|---|---|---|
> | **ap-southeast** (Singapur) | `https://ark.ap-southeast.bytepluses.com/api/v3` | 42 aktiv | **ja einzige Region mit Seedance** |
> | **eu-west** | `https://ark.eu-west.bytepluses.com/api/v3` | 5 aktiv (1× Bild `seedream-5-0`, 4× VLM) | **nein** |
> | cn-beijing | `ark.cn-beijing.volces.com` | | weist BytePlus-Keys mit `AuthenticationError` ab |
>
> **Entscheidung:** Videogenerierung läuft über **ap-southeast**, weil die EU-Region keine Videomodelle anbietet. Der EU-Key bleibt für den Bildmodell-Test und als spätere Migrationsoption bestehen falls BytePlus Seedance in eu-west ausrollt, ist der Wechsel eine Endpoint- und Key-Änderung, sonst nichts. **Deshalb gehören Endpoint und Key in die Konfiguration, nicht in den Code.**
>
> **Folge für den Vote-Loop:** Die 36 konkurrierenden Videos kommen jetzt aus **einer Modellfamilie in verschiedenen Generationen**, nicht mehr von verschiedenen Anbietern. Für die Elo-Attribution (§5) ist das eher ein Vorteil bei konstanter Modellfamilie stammt der Unterschied zwischen zwei Videos aus den Attributen, nicht aus dem Modell. Verloren geht die Absicherung, dass ein anderer Anbieter bestimmte Motive besser trifft. Die Adapter für Veo/Sora/Wan bleiben deshalb in `prompts/` liegen (siehe §7) sie sind ungenutzt, aber nicht gelöscht.
**Kostenrechnung 25 Brands (~1.000 Videos/Monat):** ~€1.300 gesamt, davon **>90 % Videogenerierung** (~€1.200). ≈ €52/Brand/Monat → Abo ab €150300/Brand lässt gesunde Marge. Genau deshalb funktioniert „Preis pro Video".
### Datenbank-Tabellen (Appwrite)
@@ -185,6 +200,7 @@ Homescreen → **Feed** · **Erstellen** (Szene / Post / Modell) · **Ordner**
## 10. Rechtliches
- **AVV** mit Hetzner (Standard, kostenlos)
- **Drittlandtransfer BytePlus (Singapur)** die Videogenerierung läuft über `ap-southeast`, weil die EU-Region keine Videomodelle hat (§9). Damit verlassen Prompt, Referenzbilder und generiertes Video die EU. Zu klären: AVV mit BytePlus inklusive **Standardvertragsklauseln**, Nennung in der Datenschutzerklärung, Aufnahme ins Verarbeitungsverzeichnis. **Entschärfend:** nach der Regel unten gehen ohnehin nur KI-generierte Gesichter raus überwiegend Geschäfts-, keine biometrischen Personendaten. Alles außerhalb der Videogenerierung bleibt in der EU bzw. bei OpenRouter.
- **EU AI Act:** KI-generierte Werbevideos müssen gekennzeichnet werden
- **Gesichter echter Personen** nur mit dokumentierter Einwilligung (auch beim Brand Owner) → **rein KI-generierte Gesichter sind der sichere Standard**
- Referenzen fremder Inhalte: Merkmale beschreiben und lernen, **nie 1:1 kopieren**
@@ -256,7 +272,7 @@ Es gibt jetzt **zwei Stränge**, die parallel laufen können der Bild-Strang
4. **Appwrite aufsetzen** und das Schema aus `datenbank-aufbau.md` anlegen **inklusive `attribute_scores`-Auftrennung von Anfang an** (siehe Abhängigkeit in §12).
5. **Elo-Function implementieren** (deterministisch, gut testbar guter erster Code), scope-bewusst.
6. **Nischen- und Typ-Tag-Enums festlegen** blockiert P19, P22 und die Kopier-Kompatibilität.
7. **Bildmodell auswählen** Kriterium ist Multi-Referenz-Komposition (Person + Produkt + Kulisse konsistent), nicht reine Bildqualität.
7. **Bildmodell auswählen** Kriterium ist Multi-Referenz-Komposition (Person + Produkt + Kulisse konsistent), nicht reine Bildqualität. Vier Kandidaten stehen bereit: `google/gemini-3-pro-image`, `google/gemini-3.1-flash-image`, `openai/gpt-5-image` (OpenRouter) und `seedream-5-0-260128` (Ark eu-west). **Seedream hat den Standortvorteil** bei gleichwertigem Ergebnis bliebe der gesamte Bild-Strang in der EU und §10 wäre nur für Video zu klären. Der Test entscheidet, nicht die Herkunft.
8. **Prototyp erweitern:** Feed, Post-Detail, Kopier-Screen mit Slot-Chips, Ordner-Ansicht, Ordner anlegen (zwei Schalter).
9. Entscheidungen nachziehen: Name, Preisstufen **für beide Zielgruppen**, Ads- **und Social-**API-Anträge starten.