From ca0ce79815169d7763458ede7070f642d73a5aeb Mon Sep 17 00:00:00 2001 From: rene Date: Fri, 24 Apr 2026 19:35:35 +0200 Subject: [PATCH] =?UTF-8?q?social:=20UploadFile=20Import-Fix=20(ForwardRef?= =?UTF-8?q?-Fehler=20=E2=86=92=20502)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/routes/social.py | 139 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 130 insertions(+), 9 deletions(-) diff --git a/backend/routes/social.py b/backend/routes/social.py index 803aa41..bd60346 100644 --- a/backend/routes/social.py +++ b/backend/routes/social.py @@ -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, 20–45 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, 20–45 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": + "ai_score": , + "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}