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:
parent
ca0ce79815
commit
1cb0c2df77
8 changed files with 1497 additions and 337 deletions
|
|
@ -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,
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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": "3–5 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": "3–5 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 10–15 erfolgreichen Wiederholungen"],
|
||||
"tipp": "Klappt nicht? Leckerli unter dein angewinkeltes Knie halten — Hund kriecht drunter durch."},
|
||||
{"id": "grundkommandos_bleib", "name": "Bleib", "kat": "Grundkommando", "schwierigkeit": "Anfänger–Fortgeschr.", "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 2–3 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": "5–10 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": "3–5 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": "3–5 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": "5–10 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": "5–10 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. 1–2 Stunden, erwachsene Hunde max. 4–6 Stunden allein lassen."},
|
||||
{"id": "problem_bellen", "name": "Weniger Bellen", "kat": "Problemverhalten","schwierigkeit": "Mittel", "alter": "Ab 8 Wochen", "dauer": "5–10 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
|
||||
# ------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue