social: UploadFile Import-Fix (ForwardRef-Fehler → 502)

This commit is contained in:
rene 2026-04-24 19:35:35 +02:00
parent 0df6d569c1
commit ca0ce79815

View file

@ -7,7 +7,7 @@ import json
import logging
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from auth import get_current_user, require_social_media
@ -21,13 +21,34 @@ _FORMATS = {"reel", "story", "post", "carousel"}
_STATUSES = {"idea", "draft", "scheduled", "published", "archived"}
_SYSTEM = (
"Du bist ein erfahrener Social-Media-Stratege für Ban Yaro, "
"die deutschsprachige Hunde-App (banyaro.app). "
"Zielgruppe: Hundebesitzer in DACH, 2045 Jahre, Fokus Frauen. "
"Ton: warmherzig, authentisch, informativ — nie aufdringlich werblich. "
"Du bist Luna, der freundliche Social-Media-Coach von Ban Yaro (banyaro.app). "
"Du begleitest eine junge Content-Creatorin (15 Jahre) dabei, für die Hunde-App "
"auf TikTok und Instagram zu wachsen. "
"Erkläre immer kurz WARUM etwas funktioniert — sie soll lernen, nicht nur kopieren. "
"Ton: locker, ermutigend, auf Augenhöhe — nie von oben herab. "
"Zielgruppe der App: Hundebesitzer in DACH, 2045 Jahre, Fokus Frauen. "
"Antworte immer auf Deutsch."
)
_PROMPT_SUGGESTIONS = '''\
Schlage 5 aktuelle, kreative Content-Ideen für Ban Yaro (Hunde-App) vor.
Heute: {datum} ({saison}).
Bereits verwendete Themen (nicht wiederholen):
{used_topics}
Antworte NUR als JSON-Array:
[
{{
"thema": "Konkretes Thema für einen Post (1 Satz)",
"warum": "Kurze Erklärung warum das gerade gut funktioniert (1-2 Sätze, als Coach)",
"format": "reel|post|story|carousel",
"platform": "tiktok|instagram|both",
"emoji": "1 passendes Emoji"
}}
]
Misch Formate und Plattformen. Aktuelle Trends, Saisonales und Evergreen-Themen kombinieren.
'''
_PROMPT_GENERATE = '''\
Erstelle einen vollständigen Social-Media-Content-Plan für folgenden Post.
@ -49,7 +70,8 @@ Antworte NUR mit einem JSON-Objekt:
"canva_notes": "Empfehlung für Canva: Template-Typ, Textüberlagerungen, Farbstimmung",
"script": {script_field},
"unsplash_query": "2-3 englische Suchbegriffe für Unsplash-Stockfotos",
"ai_score": <Zahl 1-5, geschätztes Engagement-Potenzial>
"ai_score": <Zahl 1-5, geschätztes Engagement-Potenzial>,
"coaching": "2-3 Sätze für die Creatorin: Warum dieser Hook? Warum diese Hashtags? Was macht diesen Post stark? Locker und ermutigend formuliert."
}}
'''
@ -161,6 +183,37 @@ def _parse_json(raw: str) -> dict:
raise ValueError(f"Kein JSON in Antwort: {raw[:200]}")
# ------------------------------------------------------------------
# GET /api/social/suggestions — KI schlägt 5 Themen vor
# ------------------------------------------------------------------
@router.get("/suggestions")
async def get_suggestions(user=Depends(require_social_media)):
from datetime import date
import calendar
today = date.today()
month = today.month
saison = (
"Frühling" if month in (3,4,5) else
"Sommer" if month in (6,7,8) else
"Herbst" if month in (9,10,11) else "Winter"
)
prompt = _PROMPT_SUGGESTIONS.format(
datum=today.strftime("%d.%m.%Y"),
saison=saison,
used_topics=_used_topics(20),
)
try:
raw = await _ki_complete(prompt)
import re
m = re.search(r"\[[\s\S]+\]", raw)
data = json.loads(m.group(0)) if m else []
return data
except Exception as e:
logger.error("Suggestions-Fehler: %s", e)
raise HTTPException(500, f"KI-Fehler: {e}")
# ------------------------------------------------------------------
# GET /api/social/content — alle Einträge
# ------------------------------------------------------------------
@ -216,8 +269,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)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
unsplash_query, ai_score, source, breed_id, coaching)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(
user["id"], req.platform, req.format, req.topic,
data.get("caption"), data.get("hashtags"),
@ -225,7 +278,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,
"generated", req.breed_id, data.get("coaching"),
),
)
entry_id = cur.lastrowid
@ -325,3 +378,71 @@ async def social_breeds(user=Depends(require_social_media)):
"SELECT id, name FROM wiki_rassen WHERE ki_enriched=1 ORDER BY name ASC"
).fetchall()
return [dict(r) for r in rows]
# ------------------------------------------------------------------
# GET /api/social/stats — Level / XP
# ------------------------------------------------------------------
_LEVELS = [
(0, 50, "🌱 Rookie", "Anfängerin"),
(50, 150, "✏️ Creator", "Content Creatorin"),
(150, 400, "🌟 Influencer", "Influencerin"),
(400, 800, "🏆 Pro Creator", "Profi"),
(800, 99999,"👑 Star", "Social Media Star"),
]
@router.get("/stats")
async def get_stats(user=Depends(require_social_media)):
with db() as conn:
generated = conn.execute(
"SELECT COUNT(*) FROM social_content WHERE source='generated'"
).fetchone()[0]
published = conn.execute(
"SELECT COUNT(*) FROM social_content WHERE status='published'"
).fetchone()[0]
evaluated = conn.execute(
"SELECT COUNT(*) FROM social_content WHERE source='user'"
).fetchone()[0]
five_star = conn.execute(
"SELECT COUNT(*) FROM social_content WHERE ai_score=5"
).fetchone()[0]
xp = generated * 10 + published * 20 + evaluated * 5 + five_star * 15
lvl = _LEVELS[0]
for l in _LEVELS:
if xp >= l[0]:
lvl = l
idx = _LEVELS.index(lvl)
nxt = _LEVELS[idx + 1] if idx < len(_LEVELS) - 1 else None
return {
"xp": xp, "generated": generated, "published": published,
"evaluated": evaluated, "five_star": five_star,
"level": lvl[2], "level_desc": lvl[3],
"xp_current_min": lvl[0], "xp_next": nxt[0] if nxt else xp,
"next_level": nxt[2] if nxt else None,
}
# ------------------------------------------------------------------
# POST /api/social/media — Medien-Upload (Foto/Video)
# ------------------------------------------------------------------
@router.post("/media")
async def upload_media(
file: UploadFile = File(...),
user=Depends(require_social_media),
):
import shutil, uuid, os
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
social_dir = os.path.join(MEDIA_DIR, "social")
os.makedirs(social_dir, exist_ok=True)
ext = os.path.splitext(file.filename or "")[-1].lower() or ".jpg"
fname = f"{uuid.uuid4().hex}{ext}"
path = os.path.join(social_dir, fname)
with open(path, "wb") as f:
shutil.copyfileobj(file.file, f)
return {"url": f"/media/social/{fname}", "filename": fname}