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:
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user