Files
abdulive/docs/03-data-model.md
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

7.3 KiB

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.

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

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.

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.

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

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

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.

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

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

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

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

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

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).