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
119 lines
3.8 KiB
JavaScript
119 lines
3.8 KiB
JavaScript
// Minimaler YAML-Leser fuer unsere eigenen Konfigurationsdateien.
|
|
//
|
|
// Deckt genau das ab, was config/*.yaml benutzt: verschachtelte Maps ueber
|
|
// Einrueckung, Skalare, Inline-Maps `{a: 1, b: 2}`, Listen aus Skalaren und
|
|
// Blockskalare (`>` / `|`). Bewusst kein vollstaendiges YAML - sobald in S1
|
|
// echte Abhaengigkeiten dazukommen, wird das durch `yaml` aus npm ersetzt.
|
|
|
|
function scalar(raw) {
|
|
const s = raw.trim();
|
|
if (s === '' || s === '~' || s === 'null') return null;
|
|
if (s === 'true') return true;
|
|
if (s === 'false') return false;
|
|
if (/^-?\d+$/.test(s)) return Number.parseInt(s, 10);
|
|
if (/^-?\d*\.\d+$/.test(s)) return Number.parseFloat(s);
|
|
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
return s.slice(1, -1);
|
|
}
|
|
return s;
|
|
}
|
|
|
|
function flowMap(raw) {
|
|
const out = {};
|
|
for (const part of raw.slice(1, -1).split(',')) {
|
|
if (!part.trim()) continue;
|
|
const i = part.indexOf(':');
|
|
if (i === -1) continue;
|
|
out[part.slice(0, i).trim()] = scalar(part.slice(i + 1));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function stripComment(line) {
|
|
let quote = null;
|
|
for (let i = 0; i < line.length; i++) {
|
|
const c = line[i];
|
|
if (quote) {
|
|
if (c === quote) quote = null;
|
|
} else if (c === '"' || c === "'") {
|
|
quote = c;
|
|
} else if (c === '#' && (i === 0 || /\s/.test(line[i - 1]))) {
|
|
return line.slice(0, i);
|
|
}
|
|
}
|
|
return line;
|
|
}
|
|
|
|
export function parseYaml(text) {
|
|
const lines = text.split(/\r?\n/);
|
|
const root = {};
|
|
// Stapel aus [Einrueckung, Zielobjekt]
|
|
const stack = [[-1, root]];
|
|
let i = 0;
|
|
|
|
while (i < lines.length) {
|
|
const rawLine = lines[i];
|
|
const line = stripComment(rawLine);
|
|
if (!line.trim()) { i++; continue; }
|
|
|
|
const indent = line.length - line.trimStart().length;
|
|
const body = line.trim();
|
|
|
|
while (stack.length > 1 && indent <= stack[stack.length - 1][0]) stack.pop();
|
|
const parent = stack[stack.length - 1][1];
|
|
|
|
// Listeneintrag
|
|
if (body.startsWith('- ')) {
|
|
const key = stack[stack.length - 1][2];
|
|
if (key && Array.isArray(parent[key])) parent[key].push(scalar(body.slice(2)));
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
const colon = body.indexOf(':');
|
|
if (colon === -1) { i++; continue; }
|
|
const key = body.slice(0, colon).trim();
|
|
const rest = body.slice(colon + 1).trim();
|
|
|
|
if (rest === '') {
|
|
// Verschachtelte Map, leere Liste oder leerer Wert - anhand der
|
|
// naechsten inhaltstragenden Zeile entscheiden
|
|
let j = i + 1;
|
|
while (j < lines.length && !stripComment(lines[j]).trim()) j++;
|
|
const next = j < lines.length ? stripComment(lines[j]) : '';
|
|
const nextIndent = next ? next.length - next.trimStart().length : -1;
|
|
if (next && nextIndent > indent && next.trim().startsWith('- ')) {
|
|
parent[key] = [];
|
|
stack.push([indent, parent, key]);
|
|
} else if (next && nextIndent > indent) {
|
|
parent[key] = {};
|
|
stack.push([indent, parent[key]]);
|
|
} else {
|
|
parent[key] = null;
|
|
}
|
|
} else if (rest === '>' || rest === '|' || rest === '>-' || rest === '|-') {
|
|
// Blockskalar: alle staerker eingerueckten Zeilen einsammeln
|
|
const parts = [];
|
|
let j = i + 1;
|
|
for (; j < lines.length; j++) {
|
|
if (!lines[j].trim()) { parts.push(''); continue; }
|
|
const ind = lines[j].length - lines[j].trimStart().length;
|
|
if (ind <= indent) break;
|
|
parts.push(lines[j].trim());
|
|
}
|
|
parent[key] = rest.startsWith('>') ? parts.join(' ').trim() : parts.join('\n');
|
|
i = j;
|
|
continue;
|
|
} else if (rest.startsWith('{')) {
|
|
parent[key] = flowMap(rest);
|
|
} else if (rest.startsWith('[')) {
|
|
parent[key] = rest.slice(1, -1).split(',').map(scalar).filter((v) => v !== null);
|
|
} else {
|
|
parent[key] = scalar(rest);
|
|
}
|
|
i++;
|
|
}
|
|
|
|
return root;
|
|
}
|