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 import logging
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel from pydantic import BaseModel
from auth import get_current_user, require_social_media from auth import get_current_user, require_social_media
@ -21,13 +21,34 @@ _FORMATS = {"reel", "story", "post", "carousel"}
_STATUSES = {"idea", "draft", "scheduled", "published", "archived"} _STATUSES = {"idea", "draft", "scheduled", "published", "archived"}
_SYSTEM = ( _SYSTEM = (
"Du bist ein erfahrener Social-Media-Stratege für Ban Yaro, " "Du bist Luna, der freundliche Social-Media-Coach von Ban Yaro (banyaro.app). "
"die deutschsprachige Hunde-App (banyaro.app). " "Du begleitest eine junge Content-Creatorin (15 Jahre) dabei, für die Hunde-App "
"Zielgruppe: Hundebesitzer in DACH, 2045 Jahre, Fokus Frauen. " "auf TikTok und Instagram zu wachsen. "
"Ton: warmherzig, authentisch, informativ — nie aufdringlich werblich. " "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." "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 = '''\ _PROMPT_GENERATE = '''\
Erstelle einen vollständigen Social-Media-Content-Plan für folgenden Post. 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", "canva_notes": "Empfehlung für Canva: Template-Typ, Textüberlagerungen, Farbstimmung",
"script": {script_field}, "script": {script_field},
"unsplash_query": "2-3 englische Suchbegriffe für Unsplash-Stockfotos", "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]}") 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 # 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 """INSERT INTO social_content
(created_by, platform, format, topic, caption, hashtags, (created_by, platform, format, topic, caption, hashtags,
visual_brief, image_prompt, canva_notes, script, hook, cta, visual_brief, image_prompt, canva_notes, script, hook, cta,
unsplash_query, ai_score, source, breed_id) unsplash_query, ai_score, source, breed_id, coaching)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
( (
user["id"], req.platform, req.format, req.topic, user["id"], req.platform, req.format, req.topic,
data.get("caption"), data.get("hashtags"), 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("canva_notes"), data.get("script"),
data.get("hook"), data.get("cta"), data.get("hook"), data.get("cta"),
data.get("unsplash_query"), data.get("ai_score"), data.get("unsplash_query"), data.get("ai_score"),
"generated", req.breed_id, "generated", req.breed_id, data.get("coaching"),
), ),
) )
entry_id = cur.lastrowid 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" "SELECT id, name FROM wiki_rassen WHERE ki_enriched=1 ORDER BY name ASC"
).fetchall() ).fetchall()
return [dict(r) for r in rows] 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}