Files
videogen/planung/datenbank-aufbau.md
2026-08-06 12:44:41 +02:00

19 KiB
Raw Permalink Blame History

Datenbank-Aufbau (Appwrite, self-hosted)

Stand: Juli 2026 · basiert auf Appwrite TablesDB (Tabellen/Zeilen/Relationen), Functions, Storage mit S3-Adapter, Realtime, Auth + Teams, Messaging.

Grundsatz: Die brand-knowledge/-Ordnerstruktur aus dem Konzept wird 1:1 in Tabellen abgebildet. Die .md-Textebenen (Beschreibung, Essenz, Details) bleiben als Markdown-Felder erhalten strukturierte Daten (Scores, Zähler, Status) werden eigene Spalten, damit man danach filtern und sortieren kann.

Update Juli 2026 Bild-Modus, Feed & Ordner (Konzept: konzept-bilder-feed.md) Zwei strukturelle Änderungen, keine reinen Ergänzungen:

  1. attributes ist aufgetrennt. Scores und Zähler liegen jetzt in attribute_scores ein Attribut trägt mehrere Scores, einen je Ordner. Ohne diesen Schnitt gäbe es keine ordnerbezogene Wissens-Segmentierung.
  2. assets heißt fachlich „Modelle" und kennt den neuen Typ kulisse. Der Begriff „Modell" meint in diesem Projekt durchgehend das Asset (Person, Produkt, Kulisse) nie das KI-Modell (Seedance, Veo, Sora). Diese Sprachregelung gilt für Doku und Code.

Neu dazu: folders, attribute_scores, posts, post_images, post_folders, post_metrics.


1. Appwrite-Features → unsere Nutzung

Appwrite-Feature Wofür wir es nutzen
Auth + Teams Jede Brand = ein Team. Zeilen-Rechte pro Team → sauberes Multi-Tenant ohne eigene Logik. Internes Admin-Team für den Dev-Modus.
TablesDB + Relationen Alle Tabellen unten; 4 Relationstypen decken Kategorie→Attribut, Attribut→Unter-Attribut (self-relation), Szene→Videos ab.
Storage (S3-Adapter!) Buckets laufen direkt auf Hetzner Object Storage als Backend Appwrite verwaltet Rechte/Uploads, Hetzner speichert. Kein doppeltes System.
Functions Alle deterministischen Abläufe (Elo, Cron-Jobs) + Auslöser für LLM-Jobs. Event-getriggert oder geplant.
Realtime Live-Status in der App: „Script fertig → generiere Video 2/4 → Analyse läuft" ohne Polling.
Messaging Push/E-Mail: „Deine 4 Videos sind fertig wähle das Beste."

2. Tabellen

Mandant & Konfiguration

brands eine Zeile pro Kunde

Spalte Typ Hinweis
team_id string Appwrite-Team (Rechte)
label_name, produkte, zielgruppe, brand_worte[3], am_markt_seit string aus dem Onboarding (6 Pflichtfragen)
nische enum: kosmetik/auto/gastro/fitness/mode/… neu feste Branchenliste, Pflichtfrage im Onboarding. Filtert den Feed und steuert die Kopier-Kompatibilität
stripe_customer_id string Payment
default_video_count int (36) einstellbare Videoanzahl
plan enum: bild/video neu Bild-Modus ist die günstige Einstiegsstufe (Preismodell noch offen)
status enum: trial/aktiv/pausiert

rules feste Regeln (Dev-Modus) 🔒 nur internes Team

Spalte Typ
brand_id rel → brands (null = global für alle Brands)
titel, prompt_text string
aktiv, sortierung bool, int

prompt_templates die statischen Prompts P1P22 (Dev-Modus) 🔒 nur internes Team

Spalte Typ Hinweis
key enum P1…P22 siehe Prompt-Inventar (P19P22 = Bild-Modus & Feed)
version int Prompts sind versioniert man muss zurückrollen können
static_core_md string der statische Kern
slots json welche Slots injiziert werden (REGELN, ASSETS, …)
model_adapter enum: null/seedance/veo/sora/wan P8-Familie: ein Kern, mehrere Dialekte
aktiv bool

Knowledge Base

folders Wissens-Scope (neu)

Spalte Typ Hinweis
brand_id rel → brands privat pro Brand Ordner sind kein soziales Objekt
name, theme_md string, string (Markdown) „Dark Studio"; theme_md schreibt P20 beim Anlegen
zweck enum: sammlung / wissens_scope sammlung = nur kategorisieren, keine Scores, kein Einfluss auf Generierung. wissens_scope = aus dem Ordner heraus wird erstellt
startwert_modus enum: erben / aus_posts nur bei wissens_scope. erben = Kopie der brand-weiten Scores beim Anlegen · aus_posts = nur was in den gesammelten Posts steckt, Startwert = Kategorie-Ø innerhalb des Ordners
ist_default bool Anzeige-Ordner „Allgemein" im UI. Die brand-weite Basisebene ist keine Ordner-Zeile, sondern folder_id = null in attribute_scores der Default-Ordner zeigt sie nur an, er speichert sie nicht
post_count, signal_count int Reifegrad steuert den K-Faktor und die UI-Anzeige „Ordner lernt noch"
created_at datetime

Kaltstart: keine Mindestanzahl an Posts, keine Sperre. Ein Ordner ist ab dem Anlegen nutzbar; die statistische Unsicherheit steckt im K-Faktor (32 → 8), nicht in einer Sperre. Das ist dieselbe Regel wie in projekt-uebersicht.md §5, nur auf Ordner-Ebene.

categories

Spalte Typ Hinweis
brand_id rel
name string (location, licht, farben, kamera, voice, geraeusche, texte-hooks, handlungen)
folder_id rel → folders (null = brand-weit) neu avg_score ist scope-abhängig, also gibt es die Zeile je Ordner
avg_score int Cache für Startwerte neuer Einträge per Function aktualisiert, nicht live berechnet
attribute_count int

attributes nur noch die Definition (Scores siehe unten)

Spalte Typ Hinweis
brand_id, category_id rel
parent_id rel → attributes (self) Unter-Attribute (kaiserslautern--stadion)
name, slug string
status enum: aktiv/archiviert nie löschen
tags string[] Volltext-Index
beschreibung_md, essenz_md, details_md string (Markdown) die drei Textebenen
prompt_bausteine, negativ_prompts string[] gehen direkt in P7/P8-Slots

Warum aufgetrennt: Ein Attribut („badezimmer") ist einmal definiert, trägt aber mehrere Scores einen brand-weit und je einen pro Ordner. „Warmes Abendlicht" kann im Ordner Sommerkampagne auf 6800 stehen und im Ordner Dark Studio gar nicht existieren. Die Textebenen bleiben bewusst am Attribut, nicht am Score: die Beschreibung ändert sich nicht mit dem Scope.

attribute_scores Score je Attribut je Ordner (neu, Herzstück)

Spalte Typ Hinweis
attribute_id rel → attributes
folder_id rel → folders (null = brand-weite Basisebene) Unique-Constraint auf (attribute_id, folder_id)
brand_id, category_id rel denormalisiert. Index: (brand, folder, category, score ↓) das ist die Top-N-Abfrage, die P4 bei jeder Generierung fährt
score int (010.000)
k_factor int 32 neu → 8 etabliert; bleibt hoch, solange folders.signal_count niedrig ist
start_value, start_quelle int, enum: kategorie_avg / geerbt / feed_import dokumentiert, woher der Startwert kam ohne das ist später nicht nachvollziehbar, warum ein Score dort steht
used_count, wins, losses int
last_used_at datetime

score_events das Log (append-only, nie ändern)

Spalte Typ Hinweis
attribute_id rel Index: (attribute, created_at)
folder_id rel → folders (null = brand-weit) neu und zwingend ohne den Scope ist nicht nachvollziehbar, in welcher Wissensinsel ein Punkt vergeben wurde
video_id, post_id, opponent_attribute_id rel
event_type enum: vote / vote_favorit_bestaetigt / performance / gezielte_frage / llm_vergleich / feed_import / post_reichweite die zwei neuen Typen kommen aus dem Bild-Modus
delta, new_score, weight int, int, float weight: ×0,3 / ×1 / ×2 · feed_import und post_reichweite laufen mit ×0,3
kommentar string z. B. P12-Erkenntnis („Stadion war der Unterschied")

Assets / „Modelle" (Release-Prinzip)

assets im UI „Modelle"

Spalte Typ Hinweis
brand_id rel
typ enum: gesicht/produkt/kulisse/logo/sonstiges kulisse ist neu die Umgebung ist ein eigenständiges, einzeln austauschbares Modell
name string „kleine Kerze"
nische enum wie brands.nische Feed-Filter
typ_tags string[] (Enum-Zwang) fahrzeug, getraenk, tube, person_weiblich, innenraum, … gegen diese Liste prüft die Slot-Kompatibilität beim Kopieren. Feste Liste + Code-Validator, keine freie KI-Vergabe
feed_score, feed_score_updated_at float, datetime Modelle werden getrennt von Posts gerankt (siehe post_metrics)
ist_teilbar bool (default false) Modelle werden grundsätzlich nicht geteilt. Das Feld existiert nur, um die Regel explizit und prüfbar zu machen
released_version_id rel → asset_versions (nur diese wird in Videos und Bildern verwendet)

Regel: Ein Kopierer bringt immer eigene Modelle mit. Fremde Modelle erscheinen nur als Beschriftung im Kopier-Screen („Modell: kleine Kerze"), damit klar ist, was das Rezept braucht nie als nutzbares Asset. Begründung in konzept-bilder-feed.md §3 (Marken-/Designrecht bei Produkten, Konsistenzentwertung bei Personen).

asset_versions Versionskette

Spalte Typ Hinweis
asset_id, version_no rel, int
status enum: entwurf/freigegeben/archiviert
beschreibung_md, merkmale string, string[]
change_reason string „Haare dunkler, natürlicher"
won_against_version_id rel Vorher/Nachher-Vote
reference_file_ids string[] → Storage-Bucket
einwilligung_file_id string dokumentierte Einwilligung bei echten Gesichtern

Produktion

scenes

Spalte Typ Hinweis
brand_id rel
folder_id rel → folders (null = brand-weit) neu der Scope wird vor der Generierung gewählt und bestimmt, welches Wissen P4 zieht
user_prompt string Original, unverändert aufheben
selected_context json was P4 injiziert hat (Attribute + Scores zum Zeitpunkt!)
script_md, script_final_md string Original vs. vom Nutzer bearbeitet → Diff = P6-Signal
status enum: entwurf/script/generiert/voting/fertig Realtime-Kanal fürs UI
video_count int

videos

Spalte Typ Hinweis
scene_id, brand_id rel
ki_model enum: seedance/veo/sora/wan
prompt_sent string 🔒 exakt was ans KI-Modell ging (Dev-Modus „Prompt-Einblick")
storage_file_id string → Bucket generated-videos
tags json P9-Analyse
attribute_ids rel many→many für Attribution
asset_version_ids string[] welche Versionen genau (v3? v4?)
is_system_favorite, vote_result bool, enum: gewonnen/verloren/
published, platforms, published_at bool, string[], datetime

video_metrics Zeitreihe (Cron-Function, Ads-APIs)

Spalte Typ
video_id, fetched_at rel, datetime
ctr, thumbstop, roas, follower_gained, spend float

votes

Spalte Typ
scene_id, winner_video_id rel
favorit_bestaetigt bool (→ Gewichtung ×0,3)
created_at datetime

questions gezielte Fragen (P13)

Spalte Typ
brand_id, scene_id rel
frage, attribute_a_id, attribute_b_id string, rel, rel
antwort enum: a/b/egal/offen

Bild-Modus: Posts, Feed & Ordner

posts das Slot-Rezept (neu)

Spalte Typ Hinweis
brand_id rel Ersteller
format enum: 1_1 / 4_5 / 9_16 gilt für die ganze Kette. Beim Kopieren deterministisch übernommen kein LLM
nische enum wie brands.nische Feed-Filter
kulisse_asset_id, person_asset_id, produkt_asset_id rel → assets die Modell-Slots
asset_version_ids string[] welche Versionen genau wie bei videos
attribute_ids rel many→many → attributes Licht / Kamera / Farbe als Slots
werbetext string Inhalt des Text-Overlay-Bildes
prompt_sent string 🔒 exakt was ans Bildmodell ging nur internes Team, nie im Kopier-Screen
slot_summary json die vereinfachte Chip-Darstellung für den Kopier-Screen (Output von P19). Das ist das Einzige, was ein fremder Nutzer sieht eine Abstraktion, keine Offenlegung
copied_from_post_id rel → posts (self) Herkunftskette erlaubt später „dieses Rezept wurde 400× kopiert"
sichtbarkeit enum: privat / oeffentlich (default privat) Veröffentlichen ist ein aktiver Schritt
published_at datetime
social_permalink, social_plattform string, enum: instagram/tiktok/… Grundlage für den Reichweiten-Abgleich
feed_score, feed_score_updated_at float, datetime Cache, per Cron berechnet nie live
ki_kennzeichnung bool EU AI Act im Feed sichtbar, nicht nur beim Export

post_images die Bilderkette

Spalte Typ Hinweis
post_id, position rel, int Reihenfolge im Karussell
typ enum: motiv / text_overlay Das Text-Overlay ist ein eigenes Bild, keine Ebene auf dem Motiv so lässt sich der Text tauschen, ohne die Motive neu zu generieren
storage_file_id string → Bucket generated-images
winkel, position_beschreibung string über die Kette variiert nur Position/Winkel; Modelle, Kulisse, Licht, Farbe und Text bleiben konstant
tags json P22-Analyse (Bild-Tagger)

post_folders Zuordnung (m:n, neu)

Spalte Typ Hinweis
post_id, folder_id rel, rel Unique auf (post_id, folder_id)
ist_fremd bool Post stammt nicht vom Ordner-Besitzer. Ordner nehmen eigene und fremde Posts auf Wissen entsteht durch Sammeln, nicht erst durch Ausgeben
quelle enum: eigen / feed_gespeichert / kopiert
zugeordnet_von enum: nutzer / p20_vorschlag P20 schlägt vor, der Nutzer bestätigt
created_at datetime

post_metrics Feed-Signale (Zeitreihe, append-only)

Spalte Typ Hinweis
post_id, fetched_at rel, datetime
views, likes, kopien int In-App-Signale
social_reach, social_engagement int von verbundenen Accounts
ist_verifiziert bool nur verifizierte Konten zählen voll Manipulationsschutz

Gewichtung Feed-Score: Views ×0,1 · Likes ×0,5 · Kopien ×1 · Social-Reichweite ×2. Eine Kopie wiegt mehr als ein Like ein Like ist eine Meinung, eine Kopie eine Handlung mit Aufwand.

Der Feed-Score ist kein Elo. Elo braucht Duelle; hier werden Zähler aufsummiert. Deshalb eigene Tabelle, eigener Name, eigene Function. Details und Begründung: konzept-bilder-feed.md §8.


Betrieb

jobs jede KI-Aktion als Zeile (Warteschlange + Kostenkontrolle)

Spalte Typ Hinweis
brand_id, typ rel, enum: bild_gen/video_gen/tagging/vergleich/script/slot_analyse/ordner_vorschlag/werbetext die drei neuen Typen kommen aus dem Bild-Modus (P19, P20, P21)
prompt_template_key, prompt_template_version string, int welcher Prompt in welcher Version lief ohne das ist kein Prompt-Debugging möglich
status enum: wartend/läuft/fertig/fehler
cost_usd, tokens float, int speist die Beispielrechnung mit echten Zahlen
error, refs string, json

usage_records für Stripe (metered billing: Preis pro Video)

Spalte Typ
brand_id, periode, videos_generiert, kosten_usd rel, string, int, float

3. Storage-Buckets (Backend: Hetzner Object Storage via S3-Adapter)

Bucket Inhalt Rechte
uploads Onboarding: Logo, Produktbilder, Top-Ads; Referenzen beim Verbessern Team der Brand
asset-references generierte Referenzbilder der Asset-Versionen Team der Brand
generated-videos alle generierten Videos Team der Brand
generated-images alle generierten Bilder (Motive + Text-Overlays) Team der Brand; öffentlich lesbar, sobald posts.sichtbarkeit = oeffentlich
consents Einwilligungs-Dokumente (Gesichter) Team + intern

4. Functions (deterministisch die ⚙️-Punkte aus dem Prompt-Inventar)

Function Trigger Macht
elo-update Event: votes.create / video_metrics.create Elo-Mathe, K-Faktor, Gewichtung, schreibt score_events jetzt scope-bewusst: schreibt in attribute_scores der passenden folder_id
favorit-berechnen Event: alle Videos einer Szene getaggt Score-Summe → is_system_favorite
ads-metriken-holen Cron täglich Meta/TikTok-API → video_metrics
kategorie-durchschnitt Cron täglich avg_score-Cache für Startwerte je Ordner
archivierung Cron wöchentlich Score unter Schwelle + inaktiv → Status archiviert
job-dispatcher Event: jobs.create ruft OpenRouter, schreibt Ergebnis + Kosten zurück
ordner-initialisieren Event: folders.create Bei startwert_modus = erben die brand-weiten Scores nach attribute_scores kopieren; bei aus_posts nur die Attribute der zugeordneten Posts anlegen, Startwert = Kategorie-Ø im Ordner. Setzt start_quelle
feed-score-berechnen Cron stündlich Gewichtete Summe aus post_metrics + exponentieller Zeit-Decay (Halbwertszeit-Parameter, Startwert 30 Tage) → posts.feed_score und aggregiert → assets.feed_score. Rein deterministisch, kein LLM
feed-import Event: post kopiert Legt die Slots des Originals als Attribute in der KB des Kopierers an (Scope = gewählter Ordner), start_quelle = feed_import, schreibt score_events
social-reichweite-holen Cron täglich Instagram/TikTok-API → post_metrics.social_reach; löst Rückfluss mit ×0,3 in die KB aus

5. Rechte-Modell

  • Brand-Team: sieht nur eigene Zeilen (Appwrite Row-Level-Permissions pro Team) Multi-Tenant ohne Extra-Code.
  • Internes Team (Dev-Modus): exklusiver Zugriff auf rules, prompt_templates, jobs.prompt_sent, posts.prompt_sent das App-Geheimnis liegt in den Rechten, nicht nur im UI.
  • append-only: score_events, video_metrics, post_metrics, jobs werden nie geändert oder gelöscht das ist die Log-Anforderung aus dem Konzept.

Neu mit dem Feed die einzige öffentlich lesbare Ebene:

Öffentlich lesbar Bleibt privat
posts bei sichtbarkeit = oeffentlich: Bilder, slot_summary, werbetext, nische, feed_score posts.prompt_sent 🔒 · attribute_scores · folders (immer privat) · rules · alle Modelle als Assets
assets: nur Name, Typ und Typ-Tags als Beschriftung im Kopier-Screen die Asset-Version selbst, Referenzbilder, einwilligung_file_id

Das ist die kritische Grenze des ganzen Features: Der Kopier-Screen zeigt slot_summary, nie prompt_sent. Eine Abstraktion, keine Offenlegung sonst wandert die Knowledge Base fremder Brands nach außen.

Zusätzlich nötig, weil der Feed öffentlich ist: Rate-Limits auf post_metrics-Signale pro Konto und eine Melde-/Moderationsfunktion (DSA). Beim geschlossenen B2B-Produkt gab es beides nicht.