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
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,idnie. Überall wird überidverknüpft,login/display_namebei 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).