Files
videogen/scripts/seed-prompts.mjs

149 lines
7.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* BrandLoop Prompt-Seeding (idempotent, versioniert)
*
* Liest alle prompts/P*.md (Frontmatter + Body) und synchronisiert sie in die
* Appwrite-Tabelle `prompt_templates`:
* - (key, model_adapter) ohne Zeile → anlegen mit version 1
* - höchste Version inhaltsgleich → skip (nur aktiv-Flag ggf. angleichen)
* - Inhalt geändert → neue Zeile version+1, alte aktiv=false
*
* Der Body ist das `static_core_md`. Die DB-Spalte `slots` (JSON-String) trägt
* { name, slots[], modell_hinweis } die Tabelle hat bewusst keine name-Spalte.
*
* Zusätzlich werden die 4 globalen Brand-Regeln aus dem Konzept in `rules`
* geseedet (brand_id=null, Match per titel).
*
* Vorab-Check: Jeder {{SLOT}} im Body muss im Frontmatter deklariert sein und
* umgekehrt sonst Abbruch.
*/
import { readFileSync, readdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const ENDPOINT = process.env.APPWRITE_ENDPOINT || 'https://appwrite.webklar.com/v1';
const PROJECT = process.env.APPWRITE_PROJECT || '6a5cee34002bb8360c34';
const ENV_FILE = process.env.ENV_FILE || '/home/webklar/apps/.env';
const DB_ID = 'brandloop';
const PROMPTS_DIR = join(dirname(fileURLToPath(import.meta.url)), '..', 'prompts');
const API_KEY = process.env.APPWRITE_API_KEY
|| readFileSync(ENV_FILE, 'utf8').match(/^BRANDLOOP_APPWRITE_API_KEY=(.+)$/m)?.[1]?.trim();
if (!API_KEY) { console.error('Kein API-Key gefunden.'); process.exit(1); }
async function api(method, path, body) {
const res = await fetch(`${ENDPOINT}${path}`, {
method,
headers: {
'X-Appwrite-Project': PROJECT,
'X-Appwrite-Key': API_KEY,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
const json = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(`${method} ${path}: HTTP ${res.status} ${json.message}`);
return json;
}
const limit = (n) => `queries[]=${encodeURIComponent(JSON.stringify({ method: 'limit', values: [n] }))}`;
// ---- Prompt-Dateien einlesen ----------------------------------------------
function parseFrontmatter(raw, file) {
const m = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
if (!m) throw new Error(`${file}: kein Frontmatter gefunden`);
const fm = {};
for (const line of m[1].split('\n')) {
const kv = line.match(/^([a-z_]+):\s*(.+)$/);
if (!kv) continue;
let value = kv[2].trim();
if (value.startsWith('[')) value = value.slice(1, -1).split(',').map(s => s.trim()).filter(Boolean);
else if (value === 'true') value = true;
else if (value === 'false') value = false;
fm[kv[1]] = value;
}
return { fm, body: m[2].trim() };
}
const files = readdirSync(PROMPTS_DIR).filter(f => /^P\d+.*\.md$/.test(f)).sort();
const templates = [];
let slotErrors = 0;
for (const file of files) {
const { fm, body } = parseFrontmatter(readFileSync(join(PROMPTS_DIR, file), 'utf8'), file);
const declared = new Set(Array.isArray(fm.slots) ? fm.slots : []);
const used = new Set([...body.matchAll(/\{\{([A-Z_]+)\}\}/g)].map(x => x[1]));
for (const s of used) if (!declared.has(s)) { console.error(`${file}: {{${s}}} im Body, aber nicht im Frontmatter deklariert`); slotErrors++; }
for (const s of declared) if (!used.has(s)) { console.error(`${file}: Slot ${s} deklariert, aber nicht im Body verwendet`); slotErrors++; }
templates.push({
file,
key: fm.key,
model_adapter: fm.model_adapter ?? null,
aktiv: fm.aktiv !== false,
static_core_md: body,
slots: JSON.stringify({ name: fm.name, slots: [...declared], modell_hinweis: fm.modell_hinweis ?? null }),
});
}
if (slotErrors) { console.error(`\nAbbruch: ${slotErrors} Slot-Fehler.`); process.exit(1); }
console.log(`${templates.length} Prompt-Dateien gelesen, Slot-Check ok.\n`);
// ---- prompt_templates synchronisieren -------------------------------------
const existing = (await api('GET', `/tablesdb/${DB_ID}/tables/prompt_templates/rows?${limit(500)}`)).rows;
let created = 0, skipped = 0, superseded = 0;
for (const t of templates) {
const family = existing.filter(r => r.key === t.key && (r.model_adapter ?? null) === t.model_adapter);
const label = t.key + (t.model_adapter ? `/${t.model_adapter}` : '');
const latest = family.sort((a, b) => b.version - a.version)[0];
if (latest && latest.static_core_md === t.static_core_md && latest.slots === t.slots) {
if (latest.aktiv !== t.aktiv) {
await api('PATCH', `/tablesdb/${DB_ID}/tables/prompt_templates/rows/${latest.$id}`, { data: { aktiv: t.aktiv } });
console.log(`~ ${label} v${latest.version}: aktiv → ${t.aktiv}`);
} else {
console.log(`= ${label} v${latest.version} (unverändert)`);
}
skipped++;
continue;
}
const version = latest ? latest.version + 1 : 1;
await api('POST', `/tablesdb/${DB_ID}/tables/prompt_templates/rows`, {
rowId: 'unique()',
data: { key: t.key, version, static_core_md: t.static_core_md, slots: t.slots, model_adapter: t.model_adapter, aktiv: t.aktiv },
});
console.log(`+ ${label} v${version}`);
created++;
for (const old of family.filter(r => r.aktiv)) {
await api('PATCH', `/tablesdb/${DB_ID}/tables/prompt_templates/rows/${old.$id}`, { data: { aktiv: false } });
console.log(` - ${label} v${old.version} → aktiv=false`);
superseded++;
}
}
// ---- Globale Regeln seeden -------------------------------------------------
const GLOBAL_RULES = [
{ titel: 'Hintergrund clean', sortierung: 1, prompt_text:
'Hintergründe neutral und aufgeräumt halten kein visuelles Chaos, nichts, was vom Produkt ablenkt. (EN: clean, uncluttered background; nothing that distracts from the product.)' },
{ titel: 'Nur Verlangtes zeigen', sortierung: 2, prompt_text:
'Nur zeigen, was die Szene verlangt nicht mehr, nicht weniger. Keine ungefragten Extra-Elemente, Personen oder Requisiten. (EN: show only what is asked for — no unrequested extra elements, people, or props.)' },
{ titel: 'Logo-Schutz', sortierung: 3, prompt_text:
'Das Brand-Logo nie verzerren, beschneiden, umfärben oder neu interpretieren nur als freigegebenes Asset verwenden, nie generieren lassen. (EN: never distort, crop, recolor, or reinterpret the brand logo; use the released asset only, never generate it.)' },
{ titel: 'Referenzen nie kopieren', sortierung: 4, prompt_text:
'Aus Referenzen fremder Inhalte nur beschriebene Merkmale lernen, nie 1:1 kopieren (Urheberrecht). Gesichter echter Personen nur mit dokumentierter Einwilligung rein KI-generierte Gesichter sind der Standard. (EN: learn described features from references, never copy 1:1; real faces only with documented consent.)' },
];
const existingRules = (await api('GET', `/tablesdb/${DB_ID}/tables/rules/rows?${limit(100)}`)).rows;
let rulesCreated = 0;
for (const rule of GLOBAL_RULES) {
if (existingRules.some(r => !r.brand_id && r.titel === rule.titel)) { console.log(`= Regel „${rule.titel}" (existiert)`); continue; }
await api('POST', `/tablesdb/${DB_ID}/tables/rules/rows`, {
rowId: 'unique()',
data: { titel: rule.titel, prompt_text: rule.prompt_text, aktiv: true, sortierung: rule.sortierung },
});
console.log(`+ Regel „${rule.titel}"`);
rulesCreated++;
}
console.log(`\nFertig: ${created} Prompt-Version(en) angelegt, ${skipped} unverändert, ${superseded} deaktiviert, ${rulesCreated} Regel(n) angelegt.`);