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
240 lines
7.3 KiB
Markdown
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).
|