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
This commit is contained in:
commit
d09e2b945a
98 files changed
+3273
No files matched your search
@@ -0,0 +1,239 @@
|
||||
# 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).
|
||||
Reference in new issue
Block a user