Parallele Arbeit (auf Staging mitgetestet): KI-Vision-Model (VISION_MODEL in ki.py/routes, im KI-Status sichtbar), Breed-Scraper-Anpassungen (breed_enricher/breed_evaluator, evaluate_enrichment mit user_id), Karten-/Routen-Änderungen (map.js, routes.js), kleinere UI-Anpassungen (admin.js, components.css), docker-compose, MARKETING, nav-loop-Test. Version-Bump auf 1292 (VERSION, sw.js, app.js, index.html, landing.html).
618 lines
22 KiB
Python
618 lines
22 KiB
Python
"""
|
|
BAN YARO — Rassen-Anreicherung (Wikipedia-grounded)
|
|
|
|
Strategie:
|
|
1. Wikipedia-Einleitungstext abrufen (de → en Fallback + opensearch + Aliasse)
|
|
2. Claude Haiku extrahiert Fakten NUR aus dem Quelltext
|
|
3. Kein Wikipedia-Artikel → ki_enriched=2, ki_source='none' (nicht veröffentlichen)
|
|
|
|
ki_enriched-Werte:
|
|
0 = noch nicht verarbeitet
|
|
1 = angereichert (mit Wikipedia-Quelle)
|
|
2 = kein Wikipedia-Artikel gefunden, übersprungen
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import re
|
|
import sys
|
|
import os
|
|
|
|
import httpx
|
|
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
from database import db
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_HAIKU_MODEL = "claude-haiku-4-5-20251001"
|
|
|
|
_WP_HEADERS = {"User-Agent": "Banyaro/1.0 (https://banyaro.de; mail@banyaro.de) httpx"}
|
|
|
|
# Bekannte Name-Mappings: DB-Name → Liste von Wikipedia-Titeln die zu versuchen sind
|
|
_NAME_ALIASES: dict[str, list[str]] = {
|
|
"Staffordshire Bullterrier": ["Staffordshire Bull Terrier"],
|
|
"Tibet-Spaniel": ["Tibetan Spaniel"],
|
|
"Tschechischer Terrier": ["Český teriér", "Czech Terrier", "Böhmischer Terrier"],
|
|
"Weißer Schweizer Schäferhund": ["White Swiss Shepherd Dog", "Berger Blanc Suisse"],
|
|
"Zwergpinscher": ["Miniature Pinscher"],
|
|
"wire-haired dachshund": ["Wire-haired Dachshund", "Dachshund"],
|
|
"Spinone Italiano": ["Spinone"],
|
|
"Thai Bangkaew Dog": ["Thai Bangkaew"],
|
|
"Terceira-Dogge": ["Fila da Terceira"],
|
|
"Ungarische Bracke - Transylvanischer Laufhund": ["Transylvanian Hound"],
|
|
"anglo-français de moyenne vénerie": ["Anglo-Français de Petite Vénerie", "Anglo-Français de moyenne vénerie"],
|
|
"artésien-normand": ["Basset Artésien Normand"],
|
|
"kanadischer Eskinohund": ["Canadian Eskimo Dog"],
|
|
"Wäller": ["Wäller"],
|
|
"Serbischer Laufhund": ["Srpski gonič", "Serbian Hound"],
|
|
"Serbian sheep dog": ["Sharplaninac", "Šarplaninac"],
|
|
"Slovenský hrubosrstý stavač": ["Slovak Rough-haired Pointer"],
|
|
"Spino siciliano": ["Cirneco dell'Etna"],
|
|
"Svensk vit älghund": ["Jamthund", "Jämthund", "Swedish Elkhound"],
|
|
"Walker Foxhound": ["Treeing Walker Coonhound"],
|
|
}
|
|
|
|
# Übersetzungstabelle für englische TheDogAPI-Temperamentwörter
|
|
_TEMPER_DE: dict[str, str] = {
|
|
"adaptable": "anpassungsfähig",
|
|
"active": "aktiv",
|
|
"affectionate": "liebevoll",
|
|
"agile": "agil",
|
|
"alert": "wachsam",
|
|
"aloof": "distanziert",
|
|
"athletic": "sportlich",
|
|
"bold": "kühn",
|
|
"brave": "mutig",
|
|
"calm": "ruhig",
|
|
"careful": "behutsam",
|
|
"cheerful": "fröhlich",
|
|
"clever": "klug",
|
|
"confident": "selbstbewusst",
|
|
"courageous": "mutig",
|
|
"curious": "neugierig",
|
|
"devoted": "treu",
|
|
"dignified": "würdevoll",
|
|
"docile": "gelehrig",
|
|
"dominant": "dominant",
|
|
"eager": "eifrig",
|
|
"eager to please": "folgsam",
|
|
"energetic": "energisch",
|
|
"even tempered": "ausgeglichen",
|
|
"even-tempered": "ausgeglichen",
|
|
"faithful": "treu",
|
|
"fearless": "furchtlos",
|
|
"feisty": "temperamentvoll",
|
|
"friendly": "freundlich",
|
|
"gentle": "sanft",
|
|
"good-natured": "gutmütig",
|
|
"happy": "fröhlich",
|
|
"hardy": "robust",
|
|
"independent": "selbstständig",
|
|
"industrious": "fleißig",
|
|
"intelligent": "intelligent",
|
|
"intuitive": "intuitiv",
|
|
"joyful": "fröhlich",
|
|
"keen": "eifrig",
|
|
"lively": "lebhaft",
|
|
"loyal": "loyal",
|
|
"obedient": "gehorsam",
|
|
"outgoing": "offen",
|
|
"patient": "geduldig",
|
|
"playful": "verspielt",
|
|
"protective": "beschützend",
|
|
"quiet": "ruhig",
|
|
"reserved": "zurückhaltend",
|
|
"responsive": "aufmerksam",
|
|
"sensitive": "sensibel",
|
|
"smart": "klug",
|
|
"sociable": "gesellig",
|
|
"spirited": "temperamentvoll",
|
|
"stubborn": "eigensinnig",
|
|
"sweet": "sanft",
|
|
"tenacious": "hartnäckig",
|
|
"territorial": "territorial",
|
|
"trainable": "lernfähig",
|
|
"versatile": "vielseitig",
|
|
"vigilant": "wachsam",
|
|
"willful": "eigenwillig",
|
|
"witty": "gewitzt",
|
|
"work-focused": "arbeitsorientiert",
|
|
}
|
|
|
|
_TEMPER_GARBAGE = {
|
|
"hunderasse", "dog breed", "breed of dog", "extinct dog breed",
|
|
"dog", "hund", "rasse",
|
|
}
|
|
|
|
_DIRECT_FIELDS = {
|
|
"beschreibung", "vorkommen_de",
|
|
"groesse", "gewicht_min_kg", "gewicht_max_kg",
|
|
"lebensdauer", "aktivitaet", "erfahrung",
|
|
"kinder_geeignet", "wohnung_geeignet", "temperament",
|
|
}
|
|
|
|
_SYSTEM = (
|
|
"Du bist ein Datenprozessor für eine Hunderassen-Referenz-Datenbank. "
|
|
"Extrahiere Informationen AUSSCHLIESSLICH aus dem gegebenen Quelltext. "
|
|
"Setze null wenn eine Information nicht im Text steht. "
|
|
"Erfinde keine Werte."
|
|
)
|
|
|
|
_PROMPT = '''\
|
|
Extrahiere strukturierte Daten für die Hunderasse "{name}" aus diesem Wikipedia-Text.
|
|
|
|
--- WIKIPEDIA ({lang}) ---
|
|
{wiki_text}
|
|
--- ENDE ---
|
|
|
|
Antworte NUR mit einem JSON-Objekt. Fehlende Informationen = null.
|
|
|
|
{{
|
|
"beschreibung": "3-5 informative Sätze über Charakter, Wesen und Verwendung aus dem Text. Schließe mit: Auf banyaro.app findest du weitere Informationen zu dieser Rasse.",
|
|
"vorkommen_de": "1-2 Sätze zur Verbreitung in Deutschland/DACH, nur wenn im Text erwähnt, sonst null",
|
|
"groesse": "klein|mittel|gross|sehr_gross oder null",
|
|
"gewicht_min_kg": Zahl_oder_null,
|
|
"gewicht_max_kg": Zahl_oder_null,
|
|
"lebensdauer": "X-Y Jahre oder null",
|
|
"aktivitaet": "niedrig|mittel|hoch|sehr_hoch oder null",
|
|
"erfahrung": "anfaenger|fortgeschritten|experte oder null",
|
|
"kinder_geeignet": true|false|null,
|
|
"wohnung_geeignet": true|false|null,
|
|
"temperament": "kommagetrennte Eigenschaften auf Deutsch aus dem Text, oder null"
|
|
}}
|
|
'''
|
|
|
|
|
|
def translate_temperament(text: str) -> str | None:
|
|
if not text:
|
|
return text
|
|
parts = [p.strip() for p in text.split(",")]
|
|
result = []
|
|
for part in parts:
|
|
low = part.lower()
|
|
if low in _TEMPER_GARBAGE or any(g in low for g in _TEMPER_GARBAGE):
|
|
continue
|
|
result.append(_TEMPER_DE.get(low, part))
|
|
return ", ".join(result) if result else None
|
|
|
|
|
|
def _parse_json(raw: str) -> dict:
|
|
try:
|
|
return json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
pass
|
|
match = re.search(r"```(?:json)?\s*([\s\S]+?)\s*```", raw)
|
|
if match:
|
|
try:
|
|
return json.loads(match.group(1))
|
|
except json.JSONDecodeError:
|
|
pass
|
|
match = re.search(r"\{[\s\S]+\}", raw)
|
|
if match:
|
|
try:
|
|
return json.loads(match.group(0))
|
|
except json.JSONDecodeError:
|
|
pass
|
|
raise ValueError(f"Kein gültiges JSON in Antwort gefunden: {raw[:200]}")
|
|
|
|
|
|
def _normalize_name(name: str) -> list[str]:
|
|
"""Erzeugt normalisierte Suchvarianten für einen Rassennamen."""
|
|
variants = []
|
|
# Bindestriche durch Leerzeichen ersetzen
|
|
no_dash = name.replace("-", " ")
|
|
if no_dash != name:
|
|
variants.append(no_dash)
|
|
# Gängige Suffixe abschneiden und Rest behalten
|
|
suffixes = [
|
|
" Sheepdog", " Dog", " Hound", " Terrier", " Spaniel",
|
|
" Shepherd", " Pointer", " Retriever", " Bulldog",
|
|
]
|
|
for suffix in suffixes:
|
|
if name.lower().endswith(suffix.lower()) and len(name) > len(suffix) + 3:
|
|
variants.append(name[: -len(suffix)].strip())
|
|
break
|
|
return variants
|
|
|
|
|
|
async def _wp_direct(client: httpx.AsyncClient, lang: str, title: str) -> tuple[str | None, str | None]:
|
|
"""Holt Artikeltext via titles-Parameter (direkte Suche).
|
|
|
|
Nutzt exsentences=5 statt exintro=1, damit Artikel mit sehr kurzer Einleitung
|
|
(z.B. Zwergpinscher DE: 143 Zeichen) trotzdem genug Text liefern.
|
|
"""
|
|
try:
|
|
resp = await client.get(
|
|
f"https://{lang}.wikipedia.org/w/api.php",
|
|
params={
|
|
"action": "query",
|
|
"titles": title,
|
|
"prop": "extracts",
|
|
"exsentences": 5,
|
|
"explaintext": 1,
|
|
"format": "json",
|
|
"redirects": 1,
|
|
},
|
|
)
|
|
pages = resp.json().get("query", {}).get("pages", {})
|
|
for page in pages.values():
|
|
if page.get("pageid", -1) == -1:
|
|
continue
|
|
text = page.get("extract", "").strip()
|
|
if len(text) > 80:
|
|
return text[:3000], lang
|
|
except Exception as e:
|
|
logger.debug("WP direkt (%s/%s) fehlgeschlagen: %s", lang, title, e)
|
|
return None, None
|
|
|
|
|
|
async def _wp_opensearch(client: httpx.AsyncClient, lang: str, query: str) -> tuple[str | None, str | None]:
|
|
"""Sucht via opensearch und holt dann den ersten Treffer-Artikel."""
|
|
try:
|
|
resp = await client.get(
|
|
f"https://{lang}.wikipedia.org/w/api.php",
|
|
params={
|
|
"action": "opensearch",
|
|
"search": query,
|
|
"limit": 1,
|
|
"format": "json",
|
|
},
|
|
)
|
|
results = resp.json()
|
|
# opensearch gibt [query, [titles], [descriptions], [urls]] zurück
|
|
titles = results[1] if len(results) > 1 else []
|
|
if not titles:
|
|
return None, None
|
|
found_title = titles[0]
|
|
logger.debug("Opensearch (%s) '%s' → '%s'", lang, query, found_title)
|
|
return await _wp_direct(client, lang, found_title)
|
|
except Exception as e:
|
|
logger.debug("Opensearch (%s/%s) fehlgeschlagen: %s", lang, query, e)
|
|
return None, None
|
|
|
|
|
|
async def _fetch_wikipedia_text(
|
|
name: str, name_de: str | None = None
|
|
) -> tuple[str | None, str | None]:
|
|
"""Holt den Einleitungstext eines Wikipedia-Artikels.
|
|
|
|
Fallback-Kette:
|
|
1. Direkte Suche DE (Originalname)
|
|
2. Direkte Suche EN (Originalname)
|
|
3. Direkte Suche DE (name_de, falls vorhanden und verschieden)
|
|
4. Aliasse aus _NAME_ALIASES (DE + EN je Alias)
|
|
5. Normalisierte Namensvarianten direkt DE + EN
|
|
6. Opensearch DE (Originalname)
|
|
7. Opensearch EN (Originalname)
|
|
|
|
Returns: (text, lang) oder (None, None) wenn kein Artikel gefunden.
|
|
"""
|
|
async with httpx.AsyncClient(timeout=10, headers=_WP_HEADERS) as client:
|
|
|
|
# 1+2: Direkte Suche mit Originalnamen
|
|
for lang in ("de", "en"):
|
|
text, lang_found = await _wp_direct(client, lang, name)
|
|
if text:
|
|
return text, lang_found
|
|
|
|
# 3: Direkte Suche mit name_de (falls vorhanden und nicht identisch)
|
|
if name_de and name_de.strip() and name_de.strip() != name:
|
|
for lang in ("de", "en"):
|
|
text, lang_found = await _wp_direct(client, lang, name_de.strip())
|
|
if text:
|
|
logger.debug("name_de-Treffer (%s): %s → %s", lang, name, name_de)
|
|
return text, lang_found
|
|
|
|
# 4: Bekannte Aliasse
|
|
for alias in _NAME_ALIASES.get(name, []):
|
|
for lang in ("de", "en"):
|
|
text, lang_found = await _wp_direct(client, lang, alias)
|
|
if text:
|
|
logger.debug("Alias-Treffer (%s): %s → %s", lang, name, alias)
|
|
return text, lang_found
|
|
|
|
# 5: Normalisierte Namensvarianten
|
|
for variant in _normalize_name(name):
|
|
for lang in ("de", "en"):
|
|
text, lang_found = await _wp_direct(client, lang, variant)
|
|
if text:
|
|
logger.debug("Normalisierungs-Treffer (%s): %s → %s", lang, name, variant)
|
|
return text, lang_found
|
|
|
|
# 6+7: Opensearch als letzter Ausweg
|
|
for lang in ("de", "en"):
|
|
text, lang_found = await _wp_opensearch(client, lang, name)
|
|
if text:
|
|
logger.debug("Opensearch-Treffer (%s): %s", lang, name)
|
|
return text, lang_found
|
|
|
|
return None, None
|
|
|
|
|
|
async def _fetch_wikimedia_photo(name: str) -> str | None:
|
|
"""Sucht ein lizenzfreies Foto via Wikipedia pageimages API (de → en Fallback)."""
|
|
# Auch Aliasse probieren
|
|
names_to_try = [name] + _NAME_ALIASES.get(name, [])
|
|
for lang in ("de", "en"):
|
|
for title in names_to_try:
|
|
try:
|
|
async with httpx.AsyncClient(timeout=8, headers=_WP_HEADERS) as client:
|
|
resp = await client.get(
|
|
f"https://{lang}.wikipedia.org/w/api.php",
|
|
params={
|
|
"action": "query",
|
|
"titles": title,
|
|
"prop": "pageimages",
|
|
"format": "json",
|
|
"pithumbsize": 800,
|
|
"redirects": 1,
|
|
},
|
|
)
|
|
pages = resp.json().get("query", {}).get("pages", {})
|
|
for page in pages.values():
|
|
if "thumbnail" in page:
|
|
return page["thumbnail"]["source"]
|
|
except Exception as e:
|
|
logger.debug("Wikimedia-Foto (%s/%s) fehlgeschlagen: %s", lang, title, e)
|
|
return None
|
|
|
|
|
|
async def _haiku_complete(prompt: str) -> tuple[str, str]:
|
|
"""
|
|
Fakten-Extraktion. Bevorzugt Claude Haiku (günstig + genau); ist kein
|
|
Cloud-Key gesetzt oder die Cloud nicht erreichbar, fällt es sauber auf das
|
|
lokale Modell (LM Studio) zurück, statt hart abzubrechen.
|
|
|
|
Returns (text, model) — model fließt in wiki_rassen.ki_model, damit der
|
|
Evaluator lokal-angereicherte Rassen weiterhin zur QC erkennt.
|
|
"""
|
|
key = os.getenv("ANTHROPIC_API_KEY", "")
|
|
|
|
# 1. Bevorzugt: Claude Haiku direkt (günstigstes Cloud-Modell)
|
|
if key:
|
|
try:
|
|
import anthropic
|
|
|
|
def _call():
|
|
client = anthropic.Anthropic(api_key=key)
|
|
return client.messages.create(
|
|
model=_HAIKU_MODEL,
|
|
max_tokens=700,
|
|
system=[{
|
|
"type": "text",
|
|
"text": _SYSTEM,
|
|
"cache_control": {"type": "ephemeral"},
|
|
}],
|
|
messages=[{"role": "user", "content": prompt}],
|
|
)
|
|
|
|
loop = asyncio.get_event_loop()
|
|
resp = await loop.run_in_executor(None, _call)
|
|
return resp.content[0].text.strip(), _HAIKU_MODEL
|
|
except Exception as e:
|
|
logger.warning("Haiku (Cloud) nicht erreichbar, Fallback lokal: %s", e)
|
|
|
|
# 2. Fallback: lokales Modell über die zentrale KI-Abstraktion
|
|
import ki
|
|
if ki.KI_MODE == "off":
|
|
raise RuntimeError("Kein Cloud-Key und KI_MODE=off — Anreicherung nicht möglich.")
|
|
text = await ki._local_complete(prompt, _SYSTEM, max_tokens=700, json_mode=False)
|
|
return text, ki.LOCAL_MODEL
|
|
|
|
|
|
async def _enrich_one(rasse, dry_run: bool = False) -> bool:
|
|
"""Reichert eine einzelne Rasse an. Gibt True zurück wenn erfolgreich."""
|
|
rasse = dict(rasse)
|
|
name = rasse["name"]
|
|
name_de = rasse.get("name_de")
|
|
rasse_id = rasse["id"]
|
|
|
|
# 1. Wikipedia-Text holen
|
|
wiki_text, wiki_lang = await _fetch_wikipedia_text(name, name_de)
|
|
|
|
if not wiki_text:
|
|
if not dry_run:
|
|
with db() as conn:
|
|
conn.execute(
|
|
"UPDATE wiki_rassen SET ki_enriched=2, ki_source='none' WHERE id=?",
|
|
(rasse_id,),
|
|
)
|
|
logger.info("Kein Wikipedia-Artikel: %s → übersprungen", name)
|
|
await asyncio.sleep(0.5)
|
|
return False
|
|
|
|
if dry_run:
|
|
logger.info("[DRY-RUN] Gefunden: %s (WP-%s, %d Zeichen)", name, wiki_lang.upper(), len(wiki_text))
|
|
return True
|
|
|
|
# 2. KI extrahiert Fakten aus dem Quelltext (Haiku, sonst lokaler Fallback)
|
|
prompt = _PROMPT.format(name=name, lang=wiki_lang.upper(), wiki_text=wiki_text)
|
|
try:
|
|
raw, used_model = await _haiku_complete(prompt)
|
|
except Exception as e:
|
|
logger.error("KI-Anfrage fehlgeschlagen für %s: %s", name, e)
|
|
await asyncio.sleep(3)
|
|
return False
|
|
|
|
try:
|
|
data = _parse_json(raw)
|
|
except ValueError as e:
|
|
logger.warning("JSON-Parsing fehlgeschlagen für %s: %s", name, e)
|
|
await asyncio.sleep(2)
|
|
return False
|
|
|
|
# 3. DB-Update
|
|
updates = {
|
|
k: v for k, v in data.items()
|
|
if k in _DIRECT_FIELDS and v is not None
|
|
}
|
|
if "temperament" in updates:
|
|
updates["temperament"] = translate_temperament(updates["temperament"])
|
|
updates["ki_enriched"] = 1
|
|
updates["ki_model"] = used_model
|
|
updates["ki_source"] = f"wikipedia_{wiki_lang}"
|
|
|
|
cols = ", ".join(f"{k}=?" for k in updates)
|
|
values = list(updates.values()) + [rasse_id]
|
|
|
|
try:
|
|
with db() as conn:
|
|
conn.execute(f"UPDATE wiki_rassen SET {cols} WHERE id=?", values)
|
|
logger.info("Rasse angereichert: %s (%d Felder, WP-%s)",
|
|
name, len(updates) - 2, wiki_lang.upper())
|
|
except Exception as e:
|
|
logger.error("DB-Update fehlgeschlagen für %s: %s", name, e)
|
|
await asyncio.sleep(2)
|
|
return False
|
|
|
|
# 4. Foto holen wenn noch keins vorhanden
|
|
if not rasse.get("foto_url"):
|
|
foto_url = await _fetch_wikimedia_photo(name)
|
|
if foto_url:
|
|
try:
|
|
with db() as conn:
|
|
conn.execute(
|
|
"UPDATE wiki_rassen SET foto_url=? WHERE id=?",
|
|
(foto_url, rasse_id),
|
|
)
|
|
logger.info("Wikimedia-Foto gesetzt: %s", name)
|
|
except Exception as e:
|
|
logger.error("Foto-Update fehlgeschlagen für %s: %s", name, e)
|
|
|
|
await asyncio.sleep(1)
|
|
return True
|
|
|
|
|
|
async def enrich_breeds(limit: int = 10, dry_run: bool = False) -> int:
|
|
"""
|
|
Reichert bis zu `limit` Rassen an (ki_enriched = 0).
|
|
|
|
Strategie: Wikipedia-Text holen → Haiku extrahiert Fakten.
|
|
Kein Wikipedia-Artikel → ki_enriched=2, ki_source='none'.
|
|
|
|
Returns: Anzahl erfolgreich angereicherter Rassen.
|
|
"""
|
|
with db() as conn:
|
|
rassen = conn.execute(
|
|
"""SELECT id, name, slug, herkunft, foto_url, name_de FROM wiki_rassen
|
|
WHERE ki_enriched = 0
|
|
ORDER BY name ASC
|
|
LIMIT ?""",
|
|
(limit,),
|
|
).fetchall()
|
|
|
|
if not rassen:
|
|
logger.info("Keine Rassen zur Anreicherung gefunden.")
|
|
return 0
|
|
|
|
enriched_count = 0
|
|
for rasse in rassen:
|
|
if await _enrich_one(rasse, dry_run=dry_run):
|
|
enriched_count += 1
|
|
|
|
return enriched_count
|
|
|
|
|
|
async def retry_missed(limit: int = 70, dry_run: bool = False) -> int:
|
|
"""
|
|
Versucht erneut, Rassen anzureichern die beim ersten Durchlauf
|
|
keinen Wikipedia-Artikel hatten (ki_enriched = 2).
|
|
|
|
Setzt ki_enriched NICHT zurück — verarbeitet direkt.
|
|
Bei Erfolg wird ki_enriched=1 gesetzt (überschreibt 2).
|
|
Bei erneutem Misserfolg bleibt ki_enriched=2.
|
|
|
|
Returns: Anzahl erfolgreich angereicherter Rassen.
|
|
"""
|
|
with db() as conn:
|
|
rassen = conn.execute(
|
|
"""SELECT id, name, slug, herkunft, foto_url, name_de FROM wiki_rassen
|
|
WHERE ki_enriched = 2
|
|
ORDER BY name ASC
|
|
LIMIT ?""",
|
|
(limit,),
|
|
).fetchall()
|
|
|
|
if not rassen:
|
|
logger.info("Keine übersprungenen Rassen zum Retry gefunden.")
|
|
return 0
|
|
|
|
logger.info("Retry für %d Rassen mit ki_enriched=2 ...", len(rassen))
|
|
enriched_count = 0
|
|
for rasse in rassen:
|
|
if await _enrich_one(rasse, dry_run=dry_run):
|
|
enriched_count += 1
|
|
|
|
return enriched_count
|
|
|
|
|
|
def reset_gemma_entries() -> int:
|
|
"""Setzt alle Gemma-angereicherten Einträge zurück auf ki_enriched=0."""
|
|
with db() as conn:
|
|
cur = conn.execute(
|
|
"UPDATE wiki_rassen SET ki_enriched=0, ki_model=NULL, ki_source=NULL "
|
|
"WHERE ki_model LIKE 'gemma%'",
|
|
)
|
|
count = cur.rowcount
|
|
logger.info("Gemma-Reset: %d Rassen zurückgesetzt", count)
|
|
return count
|
|
|
|
|
|
def translate_existing_temperaments() -> int:
|
|
"""Übersetzt alle englischen Temperament-Felder in der DB ins Deutsche."""
|
|
_english_words = set(_TEMPER_DE.keys())
|
|
updated = 0
|
|
with db() as conn:
|
|
rows = conn.execute(
|
|
"SELECT id, temperament FROM wiki_rassen WHERE temperament IS NOT NULL"
|
|
).fetchall()
|
|
for row in rows:
|
|
original = row["temperament"]
|
|
parts_lower = [p.strip().lower() for p in original.split(",")]
|
|
has_english = any(p in _english_words for p in parts_lower)
|
|
has_garbage = any(
|
|
any(g in p for g in _TEMPER_GARBAGE) for p in parts_lower
|
|
)
|
|
if not has_english and not has_garbage:
|
|
continue
|
|
translated = translate_temperament(original)
|
|
if translated != original:
|
|
conn.execute(
|
|
"UPDATE wiki_rassen SET temperament=? WHERE id=?",
|
|
(translated, row["id"]),
|
|
)
|
|
updated += 1
|
|
logger.info("Temperament-Migration: %d Rassen übersetzt", updated)
|
|
return updated
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import argparse
|
|
|
|
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
|
|
parser = argparse.ArgumentParser(description="Rassen-Anreicherung (Wikipedia-grounded)")
|
|
parser.add_argument("--limit", type=int, default=10,
|
|
help="Maximale Anzahl zu verarbeitender Rassen")
|
|
parser.add_argument("--reset-gemma", action="store_true",
|
|
help="Gemma-Einträge zurücksetzen bevor angereichert wird")
|
|
parser.add_argument("--retry-missed", action="store_true",
|
|
help="Rassen mit ki_enriched=2 erneut versuchen")
|
|
parser.add_argument("--dry-run", action="store_true",
|
|
help="Nur Wikipedia-Suche testen, keine DB-Änderungen")
|
|
args = parser.parse_args()
|
|
|
|
if args.reset_gemma:
|
|
n = reset_gemma_entries()
|
|
print(f"Reset: {n} Gemma-Einträge zurückgesetzt")
|
|
|
|
if args.retry_missed:
|
|
count = asyncio.run(retry_missed(args.limit, dry_run=args.dry_run))
|
|
prefix = "[DRY-RUN] " if args.dry_run else ""
|
|
print(f"{prefix}Retry: {count} von {args.limit} Rassen angereichert")
|
|
else:
|
|
count = asyncio.run(enrich_breeds(args.limit, dry_run=args.dry_run))
|
|
prefix = "[DRY-RUN] " if args.dry_run else ""
|
|
print(f"{prefix}Angereichert: {count} Rassen")
|