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}, }