feat: words-Tabelle – Brysbaert-Import + hierarchische Kategorien + Batch-Anreicherung
- categories: parent_id (self-referential) + 49 Unterkategorien geseedet - words: neue Spalten conc_m, dom_pos, level, themenfeld_id + unique index titel_en - enrich_batches + word_generative Tabellen - src/lib/enrichWords.js: Batch-Anreicherung (DE/SV-Übersetzung, Wortart, CEFR, Themenfeld) - src/routes/wordGenerative.js: CRUD für KI-Bild-Pipeline - src/routes/words.js: Filter dom_pos/level/themenfeld_id/has_conc_m + picture_count - scripts/import-brysbaert.js: CSV-Import-Skript (lokal gegen Prod-DB) - POST /api/words/enrich-batch als manueller Trigger Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -771,6 +771,128 @@ async function migrate() {
|
||||
ON CONFLICT (key) DO NOTHING
|
||||
`).catch(() => {});
|
||||
|
||||
// ── Brysbaert-Erweiterungen ─────────────────────────────────────────────────
|
||||
|
||||
// parent_id auf categories (self-referential, Oberkategorie → Unterkategorie)
|
||||
await query(`ALTER TABLE categories ADD COLUMN IF NOT EXISTS parent_id UUID REFERENCES categories(id) ON DELETE SET NULL`).catch(() => {});
|
||||
|
||||
// Unterkategorien seeden. Die bestehenden 22 Einträge sind die Oberkategorien (parent_id = NULL).
|
||||
const SUBCATEGORY_TAXONOMY = [
|
||||
// Lebensmittel
|
||||
['Obst', 'Fruit', 'Frukt', 'Lebensmittel'],
|
||||
['Gemüse', 'Vegetables', 'Grönsaker', 'Lebensmittel'],
|
||||
['Fleisch & Fisch', 'Meat & Fish', 'Kött & fisk', 'Lebensmittel'],
|
||||
['Backwaren & Getreide', 'Baked Goods & Grains', 'Bröd & spannmål', 'Lebensmittel'],
|
||||
['Milchprodukte', 'Dairy', 'Mejeriprodukter', 'Lebensmittel'],
|
||||
['Getränke', 'Drinks', 'Drycker', 'Lebensmittel'],
|
||||
['Gewürze & Kräuter', 'Spices & Herbs', 'Kryddor & örter', 'Lebensmittel'],
|
||||
['Süßigkeiten & Snacks', 'Sweets & Snacks', 'Sötsaker & snacks', 'Lebensmittel'],
|
||||
// Tiere
|
||||
['Haustiere', 'Pets', 'Husdjur', 'Tiere'],
|
||||
['Wildtiere', 'Wild Animals', 'Vilda djur', 'Tiere'],
|
||||
['Vögel', 'Birds', 'Fåglar', 'Tiere'],
|
||||
['Reptilien & Amphibien', 'Reptiles & Amphibians', 'Reptiler & amfibier', 'Tiere'],
|
||||
['Insekten & Spinnen', 'Insects & Spiders', 'Insekter & spindlar', 'Tiere'],
|
||||
['Meerestiere', 'Sea Animals', 'Havsdjur', 'Tiere'],
|
||||
// Körper
|
||||
['Kopf & Gesicht', 'Head & Face', 'Huvud & ansikte', 'Körper'],
|
||||
['Rumpf', 'Torso', 'Bål', 'Körper'],
|
||||
['Arme & Beine', 'Arms & Legs', 'Armar & ben', 'Körper'],
|
||||
['Innere Organe', 'Internal Organs', 'Inre organ', 'Körper'],
|
||||
['Körperpflege', 'Personal Care', 'Kroppsvård', 'Körper'],
|
||||
// Kleidung
|
||||
['Oberbekleidung', 'Tops & Outerwear', 'Överkläder', 'Kleidung'],
|
||||
['Unterbekleidung', 'Underwear', 'Underkläder', 'Kleidung'],
|
||||
['Kopfbedeckung', 'Headwear', 'Huvudbonader', 'Kleidung'],
|
||||
['Schuhe & Socken', 'Shoes & Socks', 'Skor & strumpor', 'Kleidung'],
|
||||
['Accessoires', 'Accessories', 'Accessoarer', 'Kleidung'],
|
||||
// Familie & Menschen
|
||||
['Familienmitglieder', 'Family Members', 'Familjemedlemmar', 'Familie & Menschen'],
|
||||
['Berufe & Titel', 'Professions & Titles', 'Yrken & titlar', 'Familie & Menschen'],
|
||||
['Beziehungen', 'Relationships', 'Relationer', 'Familie & Menschen'],
|
||||
// Haushalt
|
||||
['Küchenutensilien', 'Kitchen Utensils', 'Köksredskap', 'Haushalt'],
|
||||
['Reinigung & Pflege', 'Cleaning & Care', 'Rengöring & vård', 'Haushalt'],
|
||||
['Verpackung & Behälter', 'Packaging & Containers', 'Förpackningar & behållare','Haushalt'],
|
||||
// Wohnen & Möbel
|
||||
['Zimmer & Räume', 'Rooms & Spaces', 'Rum & utrymmen', 'Wohnen & Möbel'],
|
||||
['Möbel', 'Furniture', 'Möbler', 'Wohnen & Möbel'],
|
||||
['Beleuchtung & Elektro', 'Lighting & Electronics', 'Belysning & el', 'Wohnen & Möbel'],
|
||||
// Natur & Pflanzen
|
||||
['Pflanzen & Blumen', 'Plants & Flowers', 'Växter & blommor', 'Natur & Pflanzen'],
|
||||
['Bäume & Sträucher', 'Trees & Shrubs', 'Träd & buskar', 'Natur & Pflanzen'],
|
||||
['Landschaftsmerkmale', 'Landscape Features', 'Landskapsdrag', 'Natur & Pflanzen'],
|
||||
['Gesteine & Böden', 'Rocks & Soils', 'Stenar & jordar', 'Natur & Pflanzen'],
|
||||
// Verkehr & Reisen
|
||||
['Fahrzeuge (Land)', 'Land Vehicles', 'Landfordon', 'Verkehr & Reisen'],
|
||||
['Fahrzeuge (Wasser & Luft)', 'Water & Air Vehicles', 'Vatten- & luftfordon', 'Verkehr & Reisen'],
|
||||
['Straße & Infrastruktur', 'Roads & Infrastructure', 'Vägar & infrastruktur', 'Verkehr & Reisen'],
|
||||
// Stadt & Gebäude
|
||||
['Gebäude & Orte', 'Buildings & Places', 'Byggnader & platser', 'Stadt & Gebäude'],
|
||||
['Innenräume & Bereiche', 'Indoor Spaces & Areas', 'Inomhusutrymmen', 'Stadt & Gebäude'],
|
||||
// Technik & Geräte
|
||||
['Haushaltsgeräte', 'Household Appliances', 'Hushållsapparater', 'Technik & Geräte'],
|
||||
['Elektronik & Computer', 'Electronics & Computers', 'Elektronik & datorer', 'Technik & Geräte'],
|
||||
['Werkzeuge & Maschinen', 'Tools & Machines', 'Verktyg & maskiner', 'Technik & Geräte'],
|
||||
// Sport & Freizeit
|
||||
['Sport & Bewegung', 'Sports & Exercise', 'Sport & rörelse', 'Sport & Freizeit'],
|
||||
['Spiele & Spielzeug', 'Games & Toys', 'Spel & leksaker', 'Sport & Freizeit'],
|
||||
['Kunst & Musik', 'Arts & Music', 'Konst & musik', 'Sport & Freizeit'],
|
||||
];
|
||||
for (const [de, en, sv, parentDe] of SUBCATEGORY_TAXONOMY) {
|
||||
await query(
|
||||
`INSERT INTO categories (titel_de, titel_en, titel_sv, status, published_at, parent_id)
|
||||
SELECT $1, $2, $3, 'published', NOW(),
|
||||
(SELECT id FROM categories WHERE lower(titel_de) = lower($4) AND parent_id IS NULL LIMIT 1)
|
||||
WHERE NOT EXISTS (SELECT 1 FROM categories WHERE lower(titel_de) = lower($1))`,
|
||||
[de, en, sv, parentDe]
|
||||
).catch(() => {});
|
||||
}
|
||||
|
||||
// Neue Spalten auf words (Brysbaert-Import + Anreicherung)
|
||||
await query(`ALTER TABLE words ADD COLUMN IF NOT EXISTS conc_m NUMERIC(4,2)`).catch(() => {});
|
||||
await query(`ALTER TABLE words ADD COLUMN IF NOT EXISTS dom_pos VARCHAR(20)`).catch(() => {});
|
||||
await query(`ALTER TABLE words ADD COLUMN IF NOT EXISTS level VARCHAR(5)`).catch(() => {});
|
||||
await query(`ALTER TABLE words ADD COLUMN IF NOT EXISTS themenfeld_id UUID`).catch(() => {});
|
||||
await query(`ALTER TABLE words ADD CONSTRAINT words_themenfeld_id_fkey FOREIGN KEY (themenfeld_id) REFERENCES categories(id) ON DELETE SET NULL`).catch(() => {});
|
||||
await query(`ALTER TABLE words DROP CONSTRAINT IF EXISTS words_dom_pos_check`).catch(() => {});
|
||||
await query(`ALTER TABLE words ADD CONSTRAINT words_dom_pos_check CHECK (dom_pos IN ('noun', 'verb', 'adjective', 'other'))`).catch(() => {});
|
||||
await query(`ALTER TABLE words DROP CONSTRAINT IF EXISTS words_level_check`).catch(() => {});
|
||||
await query(`ALTER TABLE words ADD CONSTRAINT words_level_check CHECK (level IN ('A1', 'A2', 'B1'))`).catch(() => {});
|
||||
|
||||
// Unique-Index auf titel_en — Voraussetzung für ON CONFLICT im CSV-Import.
|
||||
// Falls bestehende Duplikate den Index verhindern, muss erst bereinigt werden.
|
||||
await query(`CREATE UNIQUE INDEX IF NOT EXISTS words_titel_en_key ON words (titel_en)`).catch(() => {});
|
||||
|
||||
// enrich_batches — Status-Tracking für Wort-Anreicherungs-Batches (analog category_batches)
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS enrich_batches (
|
||||
batch_id TEXT PRIMARY KEY,
|
||||
status TEXT NOT NULL DEFAULT 'submitted',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
|
||||
// word_generative — Pipeline für KI-generierte Wort-Bilder
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS word_generative (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
word_id UUID NOT NULL REFERENCES words(id) ON DELETE CASCADE,
|
||||
prompt TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending'
|
||||
CHECK (status IN ('pending', 'generating', 'generated', 'accepted', 'rejected')),
|
||||
picture_link TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`);
|
||||
await query(`
|
||||
DROP TRIGGER IF EXISTS word_generative_updated_at ON word_generative;
|
||||
CREATE TRIGGER word_generative_updated_at
|
||||
BEFORE UPDATE ON word_generative
|
||||
FOR EACH ROW EXECUTE FUNCTION update_updated_at()
|
||||
`);
|
||||
|
||||
// ── Migrate old {{uuid}} placeholders → new {{label.w:uuid}} / {{label.o:uuid}} ──
|
||||
await migratePlaceholders();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user