feat: rich character profiles — brand voice, target, rules, hashtag profiles

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) <noreply@anthropic.com>
This commit is contained in:
Michele
2026-04-04 16:34:25 +02:00
parent 8629d145a8
commit befa8b4adc
5 changed files with 290 additions and 14 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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

View File

@@ -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 = {