Social: Trainingstipp-Generator, Übungen in DB, 3 Stil-Varianten

- training_exercises Tabelle mit 13 Übungen aus App-Bibliothek
- POST /social/training-tip: Stil-Varianten tutorial/community/aspirational
- exercise_id in social_content für Wiederholungs-Tracking
- Admin-Stats: Social-Media-Sektion mit Status-Übersicht + letzte 10 Posts
- SW by-v349, APP_VER 336
This commit is contained in:
rene 2026-04-24 20:13:22 +02:00
parent ca0ce79815
commit 1cb0c2df77
8 changed files with 1497 additions and 337 deletions

View file

@ -163,6 +163,40 @@ async def stats(user=Depends(require_mod)):
except Exception:
ki_today = ki_month = ki_users_today = 0
# Social Media Tracking
try:
social_total = conn.execute("SELECT COUNT(*) FROM social_content").fetchone()[0]
social_published = conn.execute(
"SELECT COUNT(*) FROM social_content WHERE status='published'"
).fetchone()[0]
social_scheduled = conn.execute(
"SELECT COUNT(*) FROM social_content WHERE status='scheduled'"
).fetchone()[0]
social_ideas = conn.execute(
"SELECT COUNT(*) FROM social_content WHERE status='idea'"
).fetchone()[0]
social_this_week = conn.execute(
"SELECT COUNT(*) FROM social_content WHERE status='published' "
"AND published_at >= datetime('now', '-7 days')"
).fetchone()[0]
social_by_cat = {
row[0]: row[1] for row in conn.execute(
"SELECT category, COUNT(*) FROM social_content "
"WHERE category IS NOT NULL GROUP BY category ORDER BY 2 DESC"
).fetchall()
}
social_recent = [dict(r) for r in conn.execute(
"""SELECT topic, status, platform, format, created_at,
published_at, category, ai_score
FROM social_content
ORDER BY created_at DESC LIMIT 10"""
).fetchall()]
except Exception:
social_total = social_published = social_scheduled = 0
social_ideas = social_this_week = 0
social_by_cat = {}
social_recent = []
return {
"users_total": users_total,
"users_today": users_today,
@ -183,6 +217,13 @@ async def stats(user=Depends(require_mod)):
"ki_today": ki_today,
"ki_month": ki_month,
"ki_users_today": ki_users_today,
"social_total": social_total,
"social_published": social_published,
"social_scheduled": social_scheduled,
"social_ideas": social_ideas,
"social_this_week": social_this_week,
"social_by_cat": social_by_cat,
"social_recent": social_recent,
}

View file

@ -5,6 +5,7 @@ KI-gestützte Content-Erstellung für TikTok & Instagram.
import json
import logging
import random
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
@ -13,6 +14,101 @@ from pydantic import BaseModel
from auth import get_current_user, require_social_media
from database import db
# ------------------------------------------------------------------
# Übungs-Bibliothek (gespiegelt aus uebungen.js)
# ------------------------------------------------------------------
_UEBUNGEN = [
{"id": "grundkommandos_sitz", "name": "Sitz", "kat": "Grundkommando", "schwierigkeit": "Anfänger", "alter": "Ab 8 Wochen", "dauer": "35 Min",
"beschreibung": "Das erste Kommando — Basis für alles weitere. Leckerli über den Kopf führen, Hinterteil senkt sich, sofort belohnen.",
"schritte": ["Leckerli vor die Nase halten", "Langsam nach oben/hinten führen", "Hinterteil senkt sich → sofort Markerwort + Leckerli", "Erst nach 10x Wort 'Sitz' einführen"],
"tipp": "Nie zu früh das Kommandowort einführen — erst wenn die Bewegung sicher klappt."},
{"id": "grundkommandos_platz", "name": "Platz", "kat": "Grundkommando", "schwierigkeit": "Anfänger", "alter": "Ab 10 Wochen", "dauer": "35 Min",
"beschreibung": "Hund legt sich auf Signal hin. Leckerli senkrecht nach unten zwischen Vorderpfoten führen.",
"schritte": ["Hund ins Sitz", "Leckerli nach unten zwischen die Pfoten führen", "Ellenbogen + Hinterteil am Boden → Markerwort + Leckerli", "Wort 'Platz' nach 1015 erfolgreichen Wiederholungen"],
"tipp": "Klappt nicht? Leckerli unter dein angewinkeltes Knie halten — Hund kriecht drunter durch."},
{"id": "grundkommandos_bleib", "name": "Bleib", "kat": "Grundkommando", "schwierigkeit": "AnfängerFortgeschr.", "alter": "Ab 12 Wochen", "dauer": "5 Min",
"beschreibung": "Drei Dimensionen: Dauer, Distanz, Ablenkung — immer nur eine steigern!",
"schritte": ["Hund ins Sitz", "2 Sekunden warten → Markerwort + Leckerli", "Freigabewort einführen: 'Okay'", "Dauer schrittweise: 2 → 5 → 10 → 30 Sekunden", "Erst dann einen Schritt zurücktreten"],
"tipp": "Immer zum Hund zurückkehren und belohnen — nie den Hund zu sich kommen lassen beim 'Bleib'."},
{"id": "grundkommandos_hier", "name": "Hier / Komm", "kat": "Grundkommando", "schwierigkeit": "Anfänger", "alter": "Ab 8 Wochen", "dauer": "5 Min",
"beschreibung": "Lebensrettend wichtig. Niemals rufen und dann etwas Unangenehmes tun.",
"schritte": ["In der Wohnung auf 23 Meter beginnen", "Hinknien, Arme öffnen, freudige Stimme: 'Hier!'", "Ankommen = große Freude + Leckerli", "Nur einmal rufen — wer mehrfach ruft, trainiert Ignorieren"],
"tipp": "Schleppleine im Garten: Hund kann nicht wegbleiben, Erfolg ist garantiert."},
{"id": "grundkommandos_fuss", "name": "Fuß", "kat": "Grundkommando", "schwierigkeit": "Fortgeschr. Anfänger", "alter": "Ab 12 Wochen", "dauer": "510 Min",
"beschreibung": "Hund läuft ruhig neben dir ohne zu ziehen — die Leine hängt locker durch.",
"schritte": ["Hund links positionieren, Leckerli auf Hüfthöhe halten", "Schritt vorwärts → Hund folgt", "Leine locker = belohnen", "Zieht er: stehen bleiben oder Richtung wechseln"],
"tipp": "Nie mitziehen lassen. Leine locker = Belohnung, Leine straff = Stopp."},
{"id": "grundkommandos_aus", "name": "Aus / Lass es", "kat": "Grundkommando", "schwierigkeit": "Anfänger", "alter": "Ab 10 Wochen", "dauer": "35 Min",
"beschreibung": "Hund lässt Gegenstände auf Kommando los — wichtig für Sicherheit.",
"schritte": ["Hund hält Spielzeug", "Leckerli vor Nase → er lässt los", "Markerwort + Leckerli", "Gegenstand zurückgeben → Loslassen lohnt sich!"],
"tipp": "Nie einfach wegnehmen nach 'Aus' — sonst gibt er nächstes Mal nicht mehr her."},
{"id": "tricks_pfote", "name": "Pfote / Handschlag", "kat": "Trick", "schwierigkeit": "Anfänger", "alter": "Ab 12 Wochen", "dauer": "3 Min",
"beschreibung": "Klassischer Trick — sieht toll aus und ist leicht zu lernen.",
"schritte": ["Leckerli in der Faust verstecken, Faust auf Kniehöhe", "Hund schnuppert und kratzt irgendwann → sofort öffnen", "Auf flache Hand umstellen", "Wort 'Pfote' einführen"],
"tipp": "Auf flacher Hand wird aus dem Kratzen ein elegantes Ablegen der Pfote."},
{"id": "tricks_dreh", "name": "Dreh / Kreis", "kat": "Trick", "schwierigkeit": "Anfänger", "alter": "Ab 12 Wochen", "dauer": "35 Min",
"beschreibung": "Hund dreht eine Runde auf Handzeichen — sieht aus wie Tanzen!",
"schritte": ["Leckerli vor Nase", "Kreis in der Luft führen", "Volle Drehung → Markerwort + Leckerli", "'Dreh' = links, 'Runde' = rechts"],
"tipp": "Handbewegung schrittweise kleiner machen bis nur noch ein Fingerzeig reicht."},
{"id": "tricks_such", "name": "Suchspiel / Nasenarbeit", "kat": "Trick", "schwierigkeit": "Anfänger", "alter": "Ab 8 Wochen", "dauer": "510 Min",
"beschreibung": "Mentale Auslastung pur — 10 Min Suchen = 1 Stunde Spaziergang!",
"schritte": ["Leckerli unter Becher verstecken", "Hund findet es → belohnen", "Mehrere Becher, einer hat Leckerli", "Später im Gras oder draußen"],
"tipp": "Perfekt für Regentage oder wenn körperliche Aktivität nicht möglich ist."},
{"id": "tricks_decke", "name": "Platz auf Decke", "kat": "Trick", "schwierigkeit": "Fortgeschr. Anfänger", "alter": "Ab 4 Monate", "dauer": "510 Min",
"beschreibung": "Hund geht auf sein Platz und liegt ruhig — perfekt für Restaurant oder Besuche.",
"schritte": ["Decke hinlegen, Leckerli drauf werfen", "Jedes Betreten belohnen", "Auf das Hinlegen warten → große Belohnung", "Decke schrittweise weiter wegstellen"],
"tipp": "Ideal für den Alltag: Hund hat seinen sicheren Platz überall."},
{"id": "problem_springen", "name": "Nicht springen", "kat": "Problemverhalten","schwierigkeit": "Anfänger", "alter": "Ab 8 Wochen", "dauer": "Bei jeder Begrüßung",
"beschreibung": "Hund begrüßt mit allen vier Pfoten am Boden.",
"schritte": ["Springt er: keine Reaktion", "Alle vier Pfoten unten → sofort Markerwort + Leckerli", "Alle im Haushalt müssen gleich reagieren"],
"tipp": "Konsequenz ist alles — ein Mal hochspringen lassen reicht, um es wieder zu lernen."},
{"id": "problem_alleine", "name": "Alleine bleiben", "kat": "Problemverhalten","schwierigkeit": "Mittel", "alter": "Ab 10 Wochen", "dauer": "Mehrmals täglich",
"beschreibung": "Hund bleibt ruhig allein — ohne Stress, Bellen oder Zerstören.",
"schritte": ["Kong mit Futter füllen", "10 Sekunden rausgehen → ruhig zurückkommen", "Zeit schrittweise erhöhen", "Nie dramatisch verabschieden oder begrüßen"],
"tipp": "Welpen max. 12 Stunden, erwachsene Hunde max. 46 Stunden allein lassen."},
{"id": "problem_bellen", "name": "Weniger Bellen", "kat": "Problemverhalten","schwierigkeit": "Mittel", "alter": "Ab 8 Wochen", "dauer": "510 Min täglich",
"beschreibung": "Hund beruhigt sich auf Signal hin — kein übermäßiges Kläffen mehr.",
"schritte": ["Ursache kennen: Alarm, Frust, Aufmerksamkeit oder Angst?", "Aufmerksamkeits-Bellen nie belohnen", "Kurze Pause abwarten → sofort markieren + belohnen", "'Ruhig' Kommando einführen"],
"tipp": "Nie schreien oder mitbellen — der Hund denkt du machst mit!"},
]
_TRAINING_STILE = [
"tutorial", # "So geht das:"
"community", # "Sind grad dran das zu lernen"
"aspirational",# "Das könnte euer Hund auch"
]
_PROMPT_TRAINING = '''\
Erstelle einen Social-Media-Post über den Hundetraining-Tipp "{name}" für Ban Yaro (banyaro.app).
Kategorie: {kat}
Schwierigkeit: {schwierigkeit}
Alter: {alter}
Dauer: {dauer}
Beschreibung: {beschreibung}
Schritte: {schritte}
Profi-Tipp: {tipp}
Stil dieses Posts: "{stil}"
- "tutorial" "Schaut, so geht das: [Schritte knapp erklärt]"
- "community" "Sind grad dran das zu lernen 🙋 Wer kennt das?"
- "aspirational" "Das könnte euer Hund auch — in nur [Dauer]!"
Antworte NUR als JSON:
{{
"caption": "Post-Text passend zum Stil, mit Emojis, max 600 Zeichen, lehrreich aber locker",
"hashtags": "10 Hashtags kommagetrennt ohne #: hundetraining, hundeschule, positivreinforcement, {name_lower}, banyaro + passende",
"hook": "Erste Zeile die sofort Aufmerksamkeit fängt",
"cta": "Frage ans Publikum: z.B. 'Übt ihr das auch gerade?' oder 'Zeigt uns eure Videos!'",
"visual_brief": "Was im Video/Foto zu sehen sein sollte: Person mit Hund beim Training, Leckerli sichtbar, bestimmte Körperhaltung",
"canva_notes": "Text-Overlay: Übungsname + 1 Key-Tipp als Grafik",
"unsplash_query": "dog training {name_en} positive reinforcement",
"ai_score": <1-5>,
"category": "training",
"coaching": "Tipp für die Creatorin: Warum spricht dieser Post die Zielgruppe an? Wie filmt man das am besten? (1-2 Sätze)"
}}
'''
logger = logging.getLogger(__name__)
router = APIRouter()
@ -56,9 +152,12 @@ Plattform: {platform}
Format: {format}
Thema: {topic}
{breed_info}
Bereits verwendete Themen (nicht wiederholen):
Bereits verwendete Themen NICHT wiederholen, ähnliche Inhalte VERMEIDEN:
{used_topics}
Kategorie-Verteilung der letzten Posts (vermeide überfüllte Kategorien):
{category_stats}
Antworte NUR mit einem JSON-Objekt:
{{
"caption": "Post-Text (plattformgerecht, mit Emojis, max 2200 Zeichen für Instagram / 150 für TikTok)",
@ -71,6 +170,7 @@ Antworte NUR mit einem JSON-Objekt:
"script": {script_field},
"unsplash_query": "2-3 englische Suchbegriffe für Unsplash-Stockfotos",
"ai_score": <Zahl 1-5, geschätztes Engagement-Potenzial>,
"category": "Eine Kategorie aus: tipps|humor|rasse|community|gesundheit|training|saisonal|emotional|behind-the-scenes|quiz",
"coaching": "2-3 Sätze für die Creatorin: Warum dieser Hook? Warum diese Hashtags? Was macht diesen Post stark? Locker und ermutigend formuliert."
}}
'''
@ -134,6 +234,57 @@ def _used_topics(limit: int = 30) -> str:
return "\n".join(f"- {r['topic']}" for r in rows)
def _category_stats(limit: int = 30) -> str:
with db() as conn:
rows = conn.execute(
"""SELECT category, COUNT(*) as n FROM social_content
WHERE category IS NOT NULL
ORDER BY created_at DESC LIMIT ?""",
(limit,),
).fetchall()
if not rows:
return "(noch keine Kategorien)"
from collections import Counter
counts = Counter(r["category"] for r in rows)
total = sum(counts.values())
lines = [f"- {cat}: {n}x ({round(n/total*100)}%)" for cat, n in counts.most_common()]
return "\n".join(lines)
def _diversity_warning(limit: int = 20) -> dict | None:
"""Gibt Warnung zurück wenn eine Kategorie >40% der letzten Posts dominiert."""
with db() as conn:
rows = conn.execute(
"""SELECT category FROM social_content
WHERE category IS NOT NULL
ORDER BY created_at DESC LIMIT ?""",
(limit,),
).fetchall()
if len(rows) < 5:
return None
from collections import Counter
counts = Counter(r["category"] for r in rows)
total = len(rows)
for cat, n in counts.most_common(1):
if n / total > 0.4:
_CAT_DE = {
"tipps": "Tipps", "humor": "Humor/Spaß", "rasse": "Rassen-Info",
"community": "Community", "gesundheit": "Gesundheit",
"training": "Training", "saisonal": "Saison-Content",
"emotional": "Emotionale Posts", "behind-the-scenes": "Behind the Scenes",
"quiz": "Quiz/Fragen",
}
missing = [c for c in _CAT_DE if c not in counts]
return {
"dominant": cat,
"dominant_de": _CAT_DE.get(cat, cat),
"pct": round(n / total * 100),
"suggestions": missing[:3],
"suggestions_de": [_CAT_DE.get(c, c) for c in missing[:3]],
}
return None
def _breed_info(breed_id: int | None) -> str:
if not breed_id:
return ""
@ -183,6 +334,24 @@ def _parse_json(raw: str) -> dict:
raise ValueError(f"Kein JSON in Antwort: {raw[:200]}")
# ------------------------------------------------------------------
# GET /api/social/diversity — Kreativitäts-Check
# ------------------------------------------------------------------
@router.get("/diversity")
async def get_diversity(user=Depends(require_social_media)):
warning = _diversity_warning()
with db() as conn:
rows = conn.execute(
"""SELECT category, COUNT(*) as n FROM social_content
WHERE category IS NOT NULL
ORDER BY n DESC""",
).fetchall()
return {
"warning": warning,
"distribution": [dict(r) for r in rows],
}
# ------------------------------------------------------------------
# GET /api/social/suggestions — KI schlägt 5 Themen vor
# ------------------------------------------------------------------
@ -254,6 +423,7 @@ async def generate_content(req: GenerateRequest, user=Depends(require_social_med
topic=req.topic,
breed_info=_breed_info(req.breed_id),
used_topics=_used_topics(),
category_stats=_category_stats(),
script_field=script_field,
)
@ -269,8 +439,8 @@ async def generate_content(req: GenerateRequest, user=Depends(require_social_med
"""INSERT INTO social_content
(created_by, platform, format, topic, caption, hashtags,
visual_brief, image_prompt, canva_notes, script, hook, cta,
unsplash_query, ai_score, source, breed_id, coaching)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
unsplash_query, ai_score, source, breed_id, coaching, category)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
user["id"], req.platform, req.format, req.topic,
data.get("caption"), data.get("hashtags"),
@ -278,7 +448,7 @@ async def generate_content(req: GenerateRequest, user=Depends(require_social_med
data.get("canva_notes"), data.get("script"),
data.get("hook"), data.get("cta"),
data.get("unsplash_query"), data.get("ai_score"),
"generated", req.breed_id, data.get("coaching"),
"generated", req.breed_id, data.get("coaching"), data.get("category"),
),
)
entry_id = cur.lastrowid
@ -369,7 +539,121 @@ async def delete_content(cid: int, user=Depends(require_social_media)):
# ------------------------------------------------------------------
# GET /api/social/breeds — Rassen für Dropdown
# Übungen in DB seeden (einmalig beim Import)
# ------------------------------------------------------------------
def _seed_exercises():
with db() as conn:
for u in _UEBUNGEN:
conn.execute(
"""INSERT OR IGNORE INTO training_exercises
(exercise_id, name, kategorie, schwierigkeit, alter_ab,
dauer, beschreibung, schritte, tipp)
VALUES (?,?,?,?,?,?,?,?,?)""",
(u["id"], u["name"], u["kat"], u["schwierigkeit"],
u.get("alter"), u.get("dauer"), u.get("beschreibung"),
json.dumps(u.get("schritte", []), ensure_ascii=False),
u.get("tipp")),
)
try:
_seed_exercises()
except Exception:
pass
# ------------------------------------------------------------------
# POST /api/social/training-tip — Trainingstipp-Post generieren
# ------------------------------------------------------------------
@router.post("/training-tip")
async def training_tip(user=Depends(require_social_media)):
# Übung wählen die noch nicht als Social-Post verwendet wurde
with db() as conn:
used = {r["exercise_id"] for r in conn.execute(
"SELECT exercise_id FROM social_content WHERE exercise_id IS NOT NULL"
).fetchall()}
all_ex = conn.execute(
"SELECT * FROM training_exercises ORDER BY RANDOM()"
).fetchall()
unused = [e for e in all_ex if e["exercise_id"] not in used]
pool = unused if unused else list(all_ex) # Reset wenn alle durch
if not pool:
raise HTTPException(404, "Keine Übungen gefunden.")
ex = dict(pool[0])
stil = random.choice(_TRAINING_STILE)
schritte_list = json.loads(ex["schritte"] or "[]")
schritte_text = "\n".join(f" {i+1}. {s}" for i, s in enumerate(schritte_list[:4]))
prompt = _PROMPT_TRAINING.format(
name=ex["name"],
kat=ex["kategorie"],
schwierigkeit=ex["schwierigkeit"] or "Anfänger",
alter=ex["alter_ab"] or "Ab 8 Wochen",
dauer=ex["dauer"] or "5 Min",
beschreibung=ex["beschreibung"] or ex["name"],
schritte=schritte_text,
tipp=ex["tipp"] or "",
stil=stil,
name_lower=ex["name"].lower().replace(" ", ""),
name_en=ex["name"],
)
try:
raw = await _ki_complete(prompt)
data = _parse_json(raw)
except Exception as e:
raise HTTPException(500, f"KI-Fehler: {e}")
stil_label = {
"tutorial": f"So geht's: {ex['name']}",
"community": f"Lernen gerade: {ex['name']} 🙋",
"aspirational": f"Das kann dein Hund auch: {ex['name']}",
}[stil]
with db() as conn:
cur = conn.execute(
"""INSERT INTO social_content
(created_by, platform, format, topic, caption, hashtags,
visual_brief, canva_notes, hook, cta, unsplash_query,
ai_score, source, coaching, category, exercise_id)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(user["id"], "both", "reel" if stil == "tutorial" else "post",
stil_label, data.get("caption"), data.get("hashtags"),
data.get("visual_brief"), data.get("canva_notes"),
data.get("hook"), data.get("cta"), data.get("unsplash_query"),
data.get("ai_score"), "generated", data.get("coaching"),
"training", ex["exercise_id"]),
)
entry_id = cur.lastrowid
with db() as conn:
row = conn.execute("SELECT * FROM social_content WHERE id=?", (entry_id,)).fetchone()
result = dict(row)
result["exercise_name"] = ex["name"]
result["exercise_kat"] = ex["kategorie"]
result["stil"] = stil
return result
# ------------------------------------------------------------------
# GET /api/social/exercises — alle Übungen mit Nutzungsstatistik
# ------------------------------------------------------------------
@router.get("/exercises")
async def get_exercises(user=Depends(require_social_media)):
with db() as conn:
rows = conn.execute(
"""SELECT e.*,
(SELECT COUNT(*) FROM social_content s
WHERE s.exercise_id = e.exercise_id) as posts_count
FROM training_exercises e
ORDER BY e.kategorie, e.name"""
).fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# GET /api/social/breeds — alle Rassen für Autocomplete
# ------------------------------------------------------------------
@router.get("/breeds")
async def social_breeds(user=Depends(require_social_media)):
@ -380,6 +664,131 @@ async def social_breeds(user=Depends(require_social_media)):
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# GET /api/social/unused-breeds — noch nie in Post verwendete Rassen
# ------------------------------------------------------------------
@router.get("/unused-breeds")
async def unused_breeds(limit: int = 6, user=Depends(require_social_media)):
with db() as conn:
rows = conn.execute(
"""SELECT r.id, r.name FROM wiki_rassen r
WHERE r.ki_enriched = 1
AND r.id NOT IN (
SELECT breed_id FROM social_content
WHERE breed_id IS NOT NULL
)
ORDER BY RANDOM()
LIMIT ?""",
(limit,),
).fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# POST /api/social/breed-of-day — "Wusstest du schon?" Post generieren
# ------------------------------------------------------------------
_PROMPT_BREED_DAY = '''\
Erstelle einen begeisternden Social-Media-Post über die Hunderasse "{name}" für Ban Yaro (banyaro.app).
Rassen-Daten aus unserer Datenbank:
- Herkunft: {herkunft}
- Größe: {groesse}
- Gewicht: {gewicht_min}{gewicht_max} kg
- Lebensdauer: {lebensdauer}
- Aktivität: {aktivitaet}
- Erfahrung: {erfahrung}
- Temperament: {temperament}
- Für Kinder geeignet: {kinder}
- Wohnungsgeeignet: {wohnung}
- Beschreibung: {beschreibung}
Antworte NUR als JSON:
{{
"caption": "Post-Text der mit 🐾 Wusstest du schon, wie cool der/die {name} ist? beginnt. Fakten aus den Daten, mit Emojis, max 800 Zeichen.",
"hashtags": "10-12 Hashtags kommagetrennt ohne #: Rassenname, hundeleben, banyaro, passende Eigenschaften",
"hook": "Erste Zeile als Aufmerksamkeitsfänger",
"cta": "Frage ans Publikum passend zur Rasse",
"visual_brief": "Beschreibung: typisches Bild dieser Rasse, Stimmung, Setting",
"canva_notes": "Textüberlagerungen für das Rassenfoto: Rassenname groß, 1-2 Fakten",
"unsplash_query": "2-3 englische Suchbegriffe für diese Rasse",
"ai_score": <1-5>,
"category": "rasse",
"coaching": "Tipp für die Creatorin: Warum ist diese Rasse spannend für die Zielgruppe? (1-2 Sätze)"
}}
'''
@router.post("/breed-of-day")
async def breed_of_day(user=Depends(require_social_media)):
with db() as conn:
rasse = conn.execute(
"""SELECT r.id, r.name, r.herkunft, r.groesse,
r.gewicht_min_kg, r.gewicht_max_kg, r.lebensdauer,
r.aktivitaet, r.erfahrung, r.temperament,
r.kinder_geeignet, r.wohnung_geeignet,
r.beschreibung, r.foto_url
FROM wiki_rassen r
WHERE r.ki_enriched = 1
AND r.beschreibung IS NOT NULL
AND r.id NOT IN (
SELECT breed_id FROM social_content WHERE breed_id IS NOT NULL
)
ORDER BY RANDOM()
LIMIT 1""",
).fetchone()
if not rasse:
raise HTTPException(404, "Alle Rassen wurden bereits verwendet — Reset nötig.")
def _yn(v): return "Ja" if v else "Nein"
prompt = _PROMPT_BREED_DAY.format(
name=rasse["name"],
herkunft=rasse["herkunft"] or "unbekannt",
groesse=rasse["groesse"] or "unbekannt",
gewicht_min=rasse["gewicht_min_kg"] or "?",
gewicht_max=rasse["gewicht_max_kg"] or "?",
lebensdauer=rasse["lebensdauer"] or "unbekannt",
aktivitaet=rasse["aktivitaet"] or "mittel",
erfahrung=rasse["erfahrung"] or "fortgeschritten",
temperament=rasse["temperament"] or "unbekannt",
kinder=_yn(rasse["kinder_geeignet"]),
wohnung=_yn(rasse["wohnung_geeignet"]),
beschreibung=(rasse["beschreibung"] or "")[:400],
)
try:
raw = await _ki_complete(prompt)
data = _parse_json(raw)
except Exception as e:
raise HTTPException(500, f"KI-Fehler: {e}")
with db() as conn:
cur = conn.execute(
"""INSERT INTO social_content
(created_by, platform, format, topic, caption, hashtags,
visual_brief, image_prompt, canva_notes, hook, cta,
unsplash_query, ai_score, source, breed_id, coaching,
category, media_url)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
user["id"], "both", "post",
f"Rasse des Tages: {rasse['name']}",
data.get("caption"), data.get("hashtags"),
data.get("visual_brief"), None,
data.get("canva_notes"), data.get("hook"), data.get("cta"),
data.get("unsplash_query"), data.get("ai_score"),
"generated", rasse["id"], data.get("coaching"),
"rasse", rasse["foto_url"],
),
)
entry_id = cur.lastrowid
with db() as conn:
row = conn.execute("SELECT * FROM social_content WHERE id=?", (entry_id,)).fetchone()
result = dict(row)
result["breed_foto"] = rasse["foto_url"]
return result
# ------------------------------------------------------------------
# GET /api/social/stats — Level / XP
# ------------------------------------------------------------------