Refactor: Move SEO content files to root of module
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
from odoo import api, fields, models, _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
class OtkSeoBrandVoice(models.Model):
|
||||
_name = 'otk.seo.brand.voice'
|
||||
_description = 'Brand Voice Guidelines'
|
||||
_order = 'sequence, name'
|
||||
|
||||
name = fields.Char('Brand Name', required=True, translate=True,
|
||||
help='Name of the brand or voice profile (e.g., "Company Main Brand", "Product Line X")')
|
||||
|
||||
sequence = fields.Integer('Sequence', default=10)
|
||||
active = fields.Boolean('Active', default=True)
|
||||
is_default = fields.Boolean('Default Voice',
|
||||
help='If enabled, this voice will be automatically selected for new content.')
|
||||
|
||||
# === Brand Identity ===
|
||||
brand_description = fields.Text('Brand Description', translate=True,
|
||||
help='Brief description of the brand, its values, and target audience. '
|
||||
'Example: "We are a modern tech company targeting young professionals..."')
|
||||
|
||||
target_audience = fields.Text('Target Audience', translate=True,
|
||||
help='Describe the ideal reader/customer. '
|
||||
'Example: "Small business owners aged 30-50, tech-savvy but time-constrained..."')
|
||||
|
||||
# === Voice Characteristics ===
|
||||
voice_tone = fields.Selection([
|
||||
('formal', 'Formal & Professional'),
|
||||
('friendly', 'Friendly & Approachable'),
|
||||
('authoritative', 'Authoritative & Expert'),
|
||||
('conversational', 'Conversational & Casual'),
|
||||
('inspirational', 'Inspirational & Motivating'),
|
||||
('educational', 'Educational & Informative'),
|
||||
('playful', 'Playful & Fun'),
|
||||
('empathetic', 'Empathetic & Supportive'),
|
||||
], string='Voice Tone', default='friendly',
|
||||
help='The overall tone of voice to use in content.')
|
||||
|
||||
voice_personality = fields.Char('Personality Traits',
|
||||
help='Comma-separated personality traits. '
|
||||
'Example: "innovative, trustworthy, approachable, knowledgeable"')
|
||||
|
||||
writing_style = fields.Selection([
|
||||
('concise', 'Concise & Direct'),
|
||||
('detailed', 'Detailed & Thorough'),
|
||||
('storytelling', 'Storytelling & Narrative'),
|
||||
('factual', 'Factual & Data-driven'),
|
||||
('persuasive', 'Persuasive & Sales-oriented'),
|
||||
('conversational', 'Conversational & Casual'),
|
||||
], string='Writing Style', default='concise',
|
||||
help='The preferred writing style for content.')
|
||||
|
||||
# === Vocabulary Guidelines ===
|
||||
preferred_words = fields.Text('Preferred Words & Phrases', translate=True,
|
||||
help='Words and phrases to use frequently. One per line. '
|
||||
'Example:\ninnovative\ncutting-edge\nseamless experience')
|
||||
|
||||
avoided_words = fields.Text('Words to Avoid', translate=True,
|
||||
help='Words and phrases to never use. One per line. '
|
||||
'Example:\ncheap\nbasic\njust')
|
||||
|
||||
industry_terms = fields.Text('Industry Terminology', translate=True,
|
||||
help='Industry-specific terms and their preferred usage. '
|
||||
'Example:\nSaaS (not "software as a service")\nAI-powered (not "artificial intelligence powered")')
|
||||
|
||||
# === Custom Instructions ===
|
||||
custom_instructions = fields.Text('Custom AI Instructions', translate=True,
|
||||
help='Additional instructions for the AI when generating content. '
|
||||
'Example: "Always include a call-to-action at the end. '
|
||||
'Reference sustainability when relevant. Never make claims without evidence."')
|
||||
|
||||
# === Examples ===
|
||||
example_good = fields.Text('Example of Good Content',
|
||||
help='Paste an example of content that represents your brand voice well.')
|
||||
|
||||
example_bad = fields.Text('Example of What to Avoid',
|
||||
help='Paste an example of content that does NOT match your brand voice.')
|
||||
|
||||
# === Usage Tracking ===
|
||||
content_count = fields.Integer('Content Count', compute='_compute_content_count',
|
||||
help='Number of content pieces using this brand voice.')
|
||||
|
||||
@api.depends('name')
|
||||
def _compute_content_count(self):
|
||||
if not self.ids:
|
||||
return
|
||||
data = self.env['otk.seo.content']._read_group(
|
||||
[('brand_voice_id', 'in', self.ids)],
|
||||
['brand_voice_id'],
|
||||
['__count'],
|
||||
)
|
||||
counts = {voice.id: count for voice, count in data}
|
||||
for record in self:
|
||||
record.content_count = counts.get(record.id, 0)
|
||||
|
||||
@api.constrains('is_default')
|
||||
def _check_single_default(self):
|
||||
"""Ensure only one brand voice is set as default."""
|
||||
for record in self:
|
||||
if record.is_default:
|
||||
existing_default = self.search([
|
||||
('is_default', '=', True),
|
||||
('id', '!=', record.id),
|
||||
('active', '=', True)
|
||||
])
|
||||
if existing_default:
|
||||
existing_default.write({'is_default': False})
|
||||
|
||||
@api.model
|
||||
def get_default_voice(self):
|
||||
"""Get the default brand voice, if any."""
|
||||
return self.search([('is_default', '=', True), ('active', '=', True)], limit=1)
|
||||
|
||||
def get_prompt_instructions(self):
|
||||
"""Generate AI instructions from brand voice settings."""
|
||||
self.ensure_one()
|
||||
|
||||
instructions = []
|
||||
|
||||
# Brand identity
|
||||
if self.brand_description:
|
||||
instructions.append(f"Brand Context: {self.brand_description}")
|
||||
|
||||
if self.target_audience:
|
||||
instructions.append(f"Target Audience: {self.target_audience}")
|
||||
|
||||
# Voice characteristics
|
||||
tone_descriptions = {
|
||||
'formal': 'Use a formal, professional tone. Avoid slang and casual expressions.',
|
||||
'friendly': 'Use a friendly, approachable tone. Be warm but professional.',
|
||||
'authoritative': 'Write with authority and expertise. Be confident and assertive.',
|
||||
'conversational': 'Write in a conversational, casual tone. Use contractions and relatable language.',
|
||||
'inspirational': 'Write in an inspirational, motivating tone. Use uplifting language.',
|
||||
'educational': 'Write in an educational, informative tone. Explain concepts clearly.',
|
||||
'playful': 'Write in a playful, fun tone. Use humor where appropriate.',
|
||||
'empathetic': 'Write with empathy and understanding. Acknowledge reader challenges.',
|
||||
}
|
||||
if self.voice_tone:
|
||||
instructions.append(f"Voice Tone: {tone_descriptions.get(self.voice_tone, self.voice_tone)}")
|
||||
|
||||
if self.voice_personality:
|
||||
instructions.append(f"Personality Traits: The content should feel {self.voice_personality}.")
|
||||
|
||||
style_descriptions = {
|
||||
'concise': 'Keep writing concise and direct. Avoid unnecessary words.',
|
||||
'detailed': 'Provide detailed, thorough explanations. Be comprehensive.',
|
||||
'storytelling': 'Use storytelling and narrative techniques. Engage emotionally.',
|
||||
'factual': 'Focus on facts and data. Support claims with evidence.',
|
||||
'persuasive': 'Write persuasively. Focus on benefits and calls-to-action.',
|
||||
}
|
||||
if self.writing_style:
|
||||
instructions.append(f"Writing Style: {style_descriptions.get(self.writing_style, self.writing_style)}")
|
||||
|
||||
# Vocabulary
|
||||
if self.preferred_words:
|
||||
words = [w.strip() for w in self.preferred_words.split('\n') if w.strip()]
|
||||
if words:
|
||||
instructions.append(f"Preferred vocabulary (use these when appropriate): {', '.join(words[:15])}")
|
||||
|
||||
if self.avoided_words:
|
||||
words = [w.strip() for w in self.avoided_words.split('\n') if w.strip()]
|
||||
if words:
|
||||
instructions.append(f"Words to AVOID: {', '.join(words[:15])}")
|
||||
|
||||
if self.industry_terms:
|
||||
instructions.append(f"Industry terminology: {self.industry_terms}")
|
||||
|
||||
# Custom instructions
|
||||
if self.custom_instructions:
|
||||
instructions.append(f"Additional guidelines: {self.custom_instructions}")
|
||||
|
||||
# Examples
|
||||
if self.example_good:
|
||||
instructions.append(f"Example of desired content style:\n{self.example_good[:500]}")
|
||||
|
||||
return '\n\n'.join(instructions)
|
||||
|
||||
def action_view_content(self):
|
||||
"""View content using this brand voice."""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': _('Content with %s Voice') % self.name,
|
||||
'res_model': 'otk.seo.content',
|
||||
'view_mode': 'list,form',
|
||||
'domain': [('brand_voice_id', '=', self.id)],
|
||||
'context': {'default_brand_voice_id': self.id},
|
||||
}
|
||||
Reference in New Issue
Block a user