Files
abdulive/docs/03-data-model.md
T
ZeasyAlexandClaude Opus 5 d09e2b945a Struktur, Doku und S0-Toolchain fuer das abdulive-Toolkit
Monorepo-Geruest fuer neun Streamer-Tools plus den Ingest-Daemon, der
unter sechs davon liegt. Enthaelt:

- docs/: Ueberblick, Architektur, Twitch-Integration, Datenmodell,
  Roadmap, Setup, Hebel-Analyse und den MVP-Plan "Clip-Knopf"
- docs/projects/: je eine Spezifikation pro Tool
- docs/decisions/: vier ADRs (TS-Monorepo, SQLite, EventSub/IRC, Docker)
- docker/: Image mit ffmpeg und whisper.cpp, abhaengigkeitsfreier
  Entrypoint mit check-env, fetch-model und calibrate
- config/layout.yaml: aus einem VOD-Screenshot eingemessener
  Facecam-Ausschnitt (440x330 oben links, Chat-Overlay ab x=1270)

Noch kein Anwendungscode - Scheibe S0 aus docs/07-mvp-plan.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VTGjd92LsDDWF3frd9FTB
2026-09-09 08:56:09 +02:00

240 lines
7.3 KiB
Markdown

# Datenmodell
SQLite, Datei `data/db/abdulive.db`. WAL-Modus, damit mehrere Prozesse
gleichzeitig lesen können, während der Ingest schreibt. Migrationen als
nummerierte SQL-Dateien in `packages/db/migrations/`.
## Kerntabellen
### `events` — der Rohstrom
Append-only. Alles landet hier zuerst. Alle Auswertungen sind ableitbar.
```sql
CREATE TABLE events (
id TEXT PRIMARY KEY, -- ULID
ts TEXT NOT NULL, -- ISO-8601 UTC
type TEXT NOT NULL,
stream_id TEXT, -- FK streams.id, NULL wenn offline
user_id TEXT, -- Twitch user id
payload TEXT NOT NULL -- JSON
);
CREATE INDEX idx_events_ts ON events(ts);
CREATE INDEX idx_events_type ON events(type, ts);
CREATE INDEX idx_events_strm ON events(stream_id, ts);
```
### `streams` — eine Zeile pro Sendung
```sql
CREATE TABLE streams (
id TEXT PRIMARY KEY, -- Twitch stream id
started_at TEXT NOT NULL,
ended_at TEXT,
vod_id TEXT,
peak_viewers INTEGER DEFAULT 0,
avg_viewers REAL,
unique_chatters INTEGER DEFAULT 0,
new_followers INTEGER DEFAULT 0
);
```
### `stream_segments` — Titel/Kategorie über die Zeit
Basis für "welcher Titel hat was gebracht" und für VOD-Kapitel.
```sql
CREATE TABLE stream_segments (
id INTEGER PRIMARY KEY,
stream_id TEXT NOT NULL REFERENCES streams(id),
started_at TEXT NOT NULL,
ended_at TEXT,
title TEXT,
category_id TEXT,
category TEXT,
obs_scene TEXT
);
```
### `viewer_samples` — Viewerzahl im Zeitverlauf
Alle 60 s ein Sample aus `GET /helix/streams`.
```sql
CREATE TABLE viewer_samples (
stream_id TEXT NOT NULL REFERENCES streams(id),
ts TEXT NOT NULL,
viewers INTEGER NOT NULL,
PRIMARY KEY (stream_id, ts)
);
```
### `chat_messages` — jede Nachricht
```sql
CREATE TABLE chat_messages (
id TEXT PRIMARY KEY, -- msg-id aus IRC
stream_id TEXT REFERENCES streams(id),
ts TEXT NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id),
text TEXT NOT NULL,
emotes TEXT, -- JSON: [{id,name,count}]
is_first INTEGER DEFAULT 0, -- first-msg Tag
is_command INTEGER DEFAULT 0,
bits INTEGER DEFAULT 0
);
CREATE INDEX idx_chat_stream_ts ON chat_messages(stream_id, ts);
CREATE INDEX idx_chat_user ON chat_messages(user_id, ts);
```
### `users` — das Community-Gedächtnis
```sql
CREATE TABLE users (
id TEXT PRIMARY KEY, -- Twitch user id (stabil, nicht der Login!)
login TEXT NOT NULL,
display_name TEXT NOT NULL,
first_seen_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
streams_present INTEGER DEFAULT 0, -- an wie vielen Streams anwesend
streams_chatted INTEGER DEFAULT 0, -- an wie vielen geschrieben
message_count INTEGER DEFAULT 0,
is_follower INTEGER DEFAULT 0,
followed_at TEXT,
is_sub INTEGER DEFAULT 0,
sub_tier TEXT,
points INTEGER DEFAULT 0,
watch_minutes INTEGER DEFAULT 0,
greeting TEXT, -- individuelle Begrüßung
notes TEXT, -- freie Notizen des Streamers
pronouns TEXT,
muted_greeting INTEGER DEFAULT 0
);
```
> `login` ändert sich bei Namensänderung, `id` nie. Überall wird über `id`
> verknüpft, `login`/`display_name` bei jedem Kontakt aktualisiert.
### `user_streams` — Anwesenheit pro Stream
Grundlage für "Nummer 15 für dich" und für neue-vs.-wiederkehrende Chatter.
```sql
CREATE TABLE user_streams (
user_id TEXT NOT NULL REFERENCES users(id),
stream_id TEXT NOT NULL REFERENCES streams(id),
first_ts TEXT NOT NULL,
last_ts TEXT NOT NULL,
messages INTEGER DEFAULT 0,
minutes INTEGER DEFAULT 0,
PRIMARY KEY (user_id, stream_id)
);
```
## Tool-Tabellen
### `clips`
```sql
CREATE TABLE clips (
id TEXT PRIMARY KEY, -- interne ULID
twitch_clip_id TEXT,
stream_id TEXT REFERENCES streams(id),
created_at TEXT NOT NULL,
trigger TEXT NOT NULL, -- 'spike'|'command'|'manual'|'hotkey'
trigger_score REAL,
source TEXT NOT NULL, -- 'twitch'|'obs-replay'
raw_path TEXT,
vertical_path TEXT,
subtitle_path TEXT,
duration_s REAL,
title TEXT,
transcript TEXT,
status TEXT NOT NULL, -- 'pending'|'rendered'|'approved'|'uploaded'|'rejected'
score REAL, -- Ranking für Upload-Kandidaten
uploaded_to TEXT -- JSON: {"tiktok": "...", "yt": "..."}
);
```
### `raids`
```sql
CREATE TABLE raids (
id INTEGER PRIMARY KEY,
direction TEXT NOT NULL, -- 'in'|'out'
ts TEXT NOT NULL,
channel_id TEXT NOT NULL,
channel_login TEXT NOT NULL,
viewers INTEGER,
category TEXT,
reciprocated INTEGER DEFAULT 0, -- hat der zurückgeraidet?
notes TEXT
);
```
### `raid_candidates` — Scout-Ergebnisse
```sql
CREATE TABLE raid_candidates (
id INTEGER PRIMARY KEY,
scanned_at TEXT NOT NULL,
channel_id TEXT NOT NULL,
channel_login TEXT NOT NULL,
viewers INTEGER NOT NULL,
category TEXT,
language TEXT,
title TEXT,
uptime_min INTEGER,
score REAL,
history_raids INTEGER DEFAULT 0 -- schon mal Kontakt gehabt?
);
```
### `shop_items` / `redemptions`
```sql
CREATE TABLE shop_items (
id TEXT PRIMARY KEY,
label TEXT NOT NULL,
cost INTEGER NOT NULL,
action TEXT NOT NULL, -- 'set_title'|'obs_hotkey'|'sound'|'emote_rain'|'challenge'
params TEXT, -- JSON
cooldown_s INTEGER DEFAULT 0,
per_stream_limit INTEGER,
enabled INTEGER DEFAULT 1,
requires_approval INTEGER DEFAULT 0
);
CREATE TABLE redemptions (
id TEXT PRIMARY KEY,
ts TEXT NOT NULL,
stream_id TEXT REFERENCES streams(id),
user_id TEXT NOT NULL REFERENCES users(id),
item_id TEXT NOT NULL REFERENCES shop_items(id),
cost INTEGER NOT NULL,
status TEXT NOT NULL, -- 'pending'|'executed'|'refunded'
result TEXT
);
```
### `posts` — ausgehende Social-Posts
```sql
CREATE TABLE posts (
id TEXT PRIMARY KEY,
created_at TEXT NOT NULL,
kind TEXT NOT NULL, -- 'golive'|'recap'|'clip'
platform TEXT NOT NULL, -- 'discord'|'bluesky'|'instagram'
status TEXT NOT NULL, -- 'draft'|'sent'|'failed'
body TEXT,
media_path TEXT,
remote_url TEXT,
error TEXT
);
```
## Event-Typen
```
stream.online stream.offline stream.segment_changed
chat.message chat.first_message chat.cleared
chat.spike ← vom Ingest berechnet
user.arrived user.returning
follow subscribe subgift cheer
raid.in raid.out
points.redeemed shop.redeemed
poll.begin poll.end
obs.scene_changed obs.replay_saved
clip.requested clip.created clip.rendered
```
## Aufbewahrung
Nichts wird gelöscht. Die DB wächst bei ~10 Zuschauern um wenige MB pro Monat.
Nächtliches Backup nach `data/db/backup/` per `scripts/backup.ps1`
(SQLite `VACUUM INTO`, 30 Generationen).