From befa8b4adc37d45b848201bc6a742fcbc4366e8f Mon Sep 17 00:00:00 2001 From: Michele Date: Sat, 4 Apr 2026 16:34:25 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20rich=20character=20profiles=20=E2=80=94?= =?UTF-8?q?=20brand=20voice,=20target,=20rules,=20hashtag=20profiles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - Character model: add brand_voice, target_audience, business_goals, products_services, content_rules (JSON do/dont), hashtag_profiles (JSON) - Content generation: inject full character context into LLM system prompt (voice, audience, goals, products, rules) - Hashtag generation: merge always-on tags from profile with AI-generated tags - Schema: update CharacterBase and CharacterUpdate with new fields Frontend: - CharacterForm: new sections "Identità e Voce", "Regole Contenuti", "Profili Hashtag" with dedicated editors - RulesEditor: do/don't list with add/remove - HashtagProfileEditor: per-platform tabs, fixed hashtags + max generated count - All fields loaded on edit, saved on submit DB migration: 6 new columns added to characters table Co-Authored-By: Claude Opus 4.6 (1M context) --- backend/app/models.py | 7 + backend/app/routers/content.py | 20 ++- backend/app/schemas.py | 12 ++ backend/app/services/content.py | 66 +++++-- frontend/src/components/CharacterForm.jsx | 199 ++++++++++++++++++++++ 5 files changed, 290 insertions(+), 14 deletions(-) diff --git a/backend/app/models.py b/backend/app/models.py index 93c122a..7e78b5f 100644 --- a/backend/app/models.py +++ b/backend/app/models.py @@ -48,6 +48,13 @@ class Character(Base): niche = Column(String(200), nullable=False) topics = Column(JSON, default=list) tone = Column(Text) + # Rich profile fields + brand_voice = Column(Text, nullable=True) # how the character communicates (long description) + target_audience = Column(Text, nullable=True) # who reads the content + business_goals = Column(Text, nullable=True) # why they create content + products_services = Column(Text, nullable=True) # what they offer + content_rules = Column(JSON, default=dict) # {"do": [...], "dont": [...]} + hashtag_profiles = Column(JSON, default=dict) # per-platform hashtag config visual_style = Column(JSON, default=dict) social_accounts = Column(JSON, default=dict) affiliate_links = Column(JSON, default=list) diff --git a/backend/app/routers/content.py b/backend/app/routers/content.py index 43c9b5a..b5e200f 100644 --- a/backend/app/routers/content.py +++ b/backend/app/routers/content.py @@ -111,6 +111,12 @@ def generate_content( "niche": character.niche, "topics": character.topics or [], "tone": character.tone or "professional", + "brand_voice": character.brand_voice, + "target_audience": character.target_audience, + "business_goals": character.business_goals, + "products_services": character.products_services, + "content_rules": character.content_rules or {}, + "hashtag_profiles": character.hashtag_profiles or {}, } base_url = _get_setting(db, "llm_base_url", current_user.id) @@ -145,7 +151,19 @@ def generate_content( brief=request.brief, ) - hashtags = generate_hashtags(text, llm, platform) + # Hashtag generation with profile support + ht_profile = char_dict.get("hashtag_profiles", {}).get(platform, {}) + always_tags = ht_profile.get("always", []) + max_generated = ht_profile.get("max_generated", 12) + hashtags_generated = generate_hashtags(text, llm, platform, count=max_generated) + # Merge: always tags first, then generated (no duplicates) + seen = set() + hashtags = [] + for tag in always_tags + hashtags_generated: + normalized = tag.lower() + if normalized not in seen: + seen.add(normalized) + hashtags.append(tag) affiliate_links_used: list[dict] = [] if affiliate_link_dicts: diff --git a/backend/app/schemas.py b/backend/app/schemas.py index d8e94c0..ca153cf 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -24,6 +24,12 @@ class CharacterBase(BaseModel): niche: str topics: list[str] = [] tone: Optional[str] = None + brand_voice: Optional[str] = None + target_audience: Optional[str] = None + business_goals: Optional[str] = None + products_services: Optional[str] = None + content_rules: dict = {} # {"do": [...], "dont": [...]} + hashtag_profiles: dict = {} # per-platform: {"instagram": {"always": [], "pool": [], ...}} visual_style: dict = {} social_accounts: dict = {} affiliate_links: list[dict] = [] @@ -40,6 +46,12 @@ class CharacterUpdate(BaseModel): niche: Optional[str] = None topics: Optional[list[str]] = None tone: Optional[str] = None + brand_voice: Optional[str] = None + target_audience: Optional[str] = None + business_goals: Optional[str] = None + products_services: Optional[str] = None + content_rules: Optional[dict] = None + hashtag_profiles: Optional[dict] = None visual_style: Optional[dict] = None social_accounts: Optional[dict] = None affiliate_links: Optional[list[dict]] = None diff --git a/backend/app/services/content.py b/backend/app/services/content.py index 447467f..975073f 100644 --- a/backend/app/services/content.py +++ b/backend/app/services/content.py @@ -20,7 +20,9 @@ def generate_post_text( """Generate social media post text based on a character profile. Args: - character: Dict with keys: name, niche, topics (list), tone (str). + character: Dict with keys: name, niche, topics (list), tone (str), + and optional rich profile: brand_voice, target_audience, + business_goals, products_services, content_rules. topic_hint: Optional topic suggestion to guide generation. llm_provider: LLM provider instance for text generation. platform: Target platform (e.g. 'instagram', 'facebook', 'tiktok', 'youtube'). @@ -36,18 +38,56 @@ def generate_post_text( topics_str = ", ".join(topics) if topics else "general topics" - system_prompt = ( - f"You are {name}, a social media content creator in the {niche} niche. " - f"Your expertise covers: {topics_str}. " - f"Your communication style is {tone}. " - f"You create authentic, engaging content that resonates with your audience. " - f"Never reveal you are an AI. Write as {name} would naturally write.\n\n" - f"REGOLA CRITICA: Se ti viene indicata una tecnica narrativa (PAS, AIDA, Storytelling, ecc.), " - f"usala SOLO come struttura invisibile del testo. " - f"NON scrivere MAI le etichette del framework nel post (es. non scrivere 'PROBLEMA:', " - f"'AGITAZIONE:', 'SOLUZIONE:', 'ATTENZIONE:', 'INTERESSE:', ecc.). " - f"Il lettore non deve percepire alcun framework — deve sembrare un post naturale e spontaneo." - ) + # Base identity + system_parts = [ + f"You are {name}, a social media content creator in the {niche} niche.", + f"Your expertise covers: {topics_str}.", + f"Your communication style is {tone}.", + ] + + # Rich profile: brand voice + brand_voice = character.get("brand_voice") + if brand_voice: + system_parts.append(f"\nVOCE E STILE DI COMUNICAZIONE:\n{brand_voice}") + + # Rich profile: target audience + target_audience = character.get("target_audience") + if target_audience: + system_parts.append(f"\nPUBBLICO TARGET:\n{target_audience}") + + # Rich profile: business goals + business_goals = character.get("business_goals") + if business_goals: + system_parts.append(f"\nOBIETTIVI BUSINESS:\n{business_goals}") + + # Rich profile: products/services + products_services = character.get("products_services") + if products_services: + system_parts.append(f"\nPRODOTTI/SERVIZI OFFERTI:\n{products_services}") + + # Rich profile: content rules (do/don't) + content_rules = character.get("content_rules") or {} + do_rules = content_rules.get("do", []) + dont_rules = content_rules.get("dont", []) + if do_rules or dont_rules: + rules_text = "\nREGOLE CONTENUTI:" + if do_rules: + rules_text += "\nFA SEMPRE: " + " | ".join(do_rules) + if dont_rules: + rules_text += "\nNON FARE MAI: " + " | ".join(dont_rules) + system_parts.append(rules_text) + + system_parts.extend([ + "\nYou create authentic, engaging content that resonates with your audience.", + "Never reveal you are an AI. Write as {name} would naturally write.", + "\nREGOLA CRITICA: Se ti viene indicata una tecnica narrativa (PAS, AIDA, Storytelling, ecc.), " + "usala SOLO come struttura invisibile del testo. " + "NON scrivere MAI le etichette del framework nel post (es. non scrivere 'PROBLEMA:', " + "'AGITAZIONE:', 'SOLUZIONE:', 'ATTENZIONE:', 'INTERESSE:', ecc.). " + "Il lettore non deve percepire alcun framework — deve sembrare un post naturale e spontaneo.", + ]) + + system_prompt = "\n".join(system_parts) # Platform-specific instructions platform_guidance = { diff --git a/frontend/src/components/CharacterForm.jsx b/frontend/src/components/CharacterForm.jsx index 81c0743..c9089f6 100644 --- a/frontend/src/components/CharacterForm.jsx +++ b/frontend/src/components/CharacterForm.jsx @@ -7,10 +7,18 @@ const EMPTY_FORM = { niche: '', topics: [], tone: '', + brand_voice: '', + target_audience: '', + business_goals: '', + products_services: '', + content_rules: { do: [], dont: [] }, + hashtag_profiles: {}, visual_style: { primary_color: '#E85A4F', secondary_color: '#1A1A1A', font: '' }, is_active: true, } +const HASHTAG_PLATFORMS = ['instagram', 'facebook', 'youtube', 'tiktok'] + const NICHE_CHIPS = [ 'Food & Ricette', 'Fitness & Sport', 'Tech & AI', 'Beauty & Skincare', 'Fashion & Style', 'Travel & Lifestyle', 'Finance & Investimenti', 'Salute & Wellness', @@ -45,6 +53,12 @@ export default function CharacterForm() { niche: data.niche || '', topics: data.topics || [], tone: data.tone || '', + brand_voice: data.brand_voice || '', + target_audience: data.target_audience || '', + business_goals: data.business_goals || '', + products_services: data.products_services || '', + content_rules: data.content_rules || { do: [], dont: [] }, + hashtag_profiles: data.hashtag_profiles || {}, visual_style: { primary_color: data.visual_style?.primary_color || '#E85A4F', secondary_color: data.visual_style?.secondary_color || '#1A1A1A', @@ -222,6 +236,64 @@ export default function CharacterForm() { )} + {/* ── Identità e Voce ──────────────────────────────────── */} +
+

+ Queste informazioni vengono usate automaticamente dall'AI ogni volta che genera contenuti. Compilale una volta sola con cura. +

+ + +