UNPKG

@bernierllc/ai-content-generator

Version:

AI-powered content generation service for social posts, blog articles, emails, and multi-platform content

865 lines (864 loc) 32.2 kB
/* Copyright (c) 2025 Bernier LLC This file is licensed to the client under a limited-use license. The client may use and modify this code *only within the scope of the project it was delivered for*. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */ /** * AI Content Generator Service * * Generates AI-powered content for social media posts, blog articles, * emails, and multi-platform content from source material. */ export class AIContentGenerator { provider; fallbackProvider; config; analyticsData = []; constructor(config) { this.config = config; this.provider = config.provider; this.fallbackProvider = config.fallbackProvider; } // ============================================ // SOCIAL MEDIA GENERATION // ============================================ /** * Generate a social media post from source content */ async generateSocialPost(sourceContent, platform, options) { const startTime = Date.now(); // Validate source content if (!sourceContent || sourceContent.trim().length === 0) { return { success: false, error: 'Source content cannot be empty', content: '', }; } const prompt = this.buildSocialPrompt(sourceContent, platform, options); const temperature = options?.creativity ?? this.config.defaultTemperature ?? 0.7; try { const response = await this.executeWithFallback({ messages: [ { role: 'system', content: `You are an expert social media content creator specializing in ${platform}. Create engaging, platform-appropriate content.`, }, { role: 'user', content: prompt }, ], temperature, maxTokens: this.getPlatformMaxTokens(platform), }); if (!response.success || !response.content) { return { success: false, error: response.error || 'Generation failed', content: '', }; } const generatedContent = this.parsePlatformContent(response.content, platform); // Generate alternate versions if requested let alternateVersions; if (options?.alternateVersions && options.alternateVersions > 0) { alternateVersions = await this.generateAlternateVersions(sourceContent, platform, options, options.alternateVersions); } const result = { success: true, content: generatedContent, metadata: { platform, generatedAt: new Date(), model: this.provider.getProviderName(), promptTokens: response.usage?.promptTokens || 0, completionTokens: response.usage?.completionTokens || 0, totalTokens: response.usage?.totalTokens || 0, durationMs: Date.now() - startTime, characterCount: generatedContent.length, }, alternateVersions, }; this.trackGeneration('social', platform, result.metadata?.totalTokens || 0); return result; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Generation failed', content: '', }; } } /** * Generate posts for multiple platforms from a single source */ async generateMultiPlatformContent(sourceContent, platforms, options) { const results = await Promise.all(platforms.map(async (platform) => ({ platform, result: await this.generateSocialPost(sourceContent, platform, options), }))); return { success: true, results, totalGenerated: platforms.length, successCount: results.filter((r) => r.result.success).length, failureCount: results.filter((r) => !r.result.success).length, metadata: { sourceLength: sourceContent.length, platforms, generatedAt: new Date(), }, }; } // ============================================ // BLOG GENERATION // ============================================ /** * Generate a blog post from an outline */ async generateBlogPost(outline, options) { const startTime = Date.now(); // Validate outline if (!outline.topic || outline.topic.trim().length === 0) { return { success: false, error: 'Blog topic cannot be empty', content: '', }; } if (!outline.sections || outline.sections.length === 0) { return { success: false, error: 'Blog outline must have at least one section', content: '', }; } const prompt = this.buildBlogPrompt(outline, options); const temperature = options?.creativity ?? this.config.defaultTemperature ?? 0.6; try { const response = await this.executeWithFallback({ messages: [ { role: 'system', content: 'You are an expert blog writer creating engaging, informative content.', }, { role: 'user', content: prompt }, ], temperature, maxTokens: options?.targetLength ? Math.ceil(options.targetLength * 1.5) : 4000, }); if (!response.success || !response.content) { return { success: false, error: response.error || 'Blog generation failed', content: '', }; } const result = { success: true, content: response.content, metadata: { platform: 'blog', generatedAt: new Date(), model: this.provider.getProviderName(), promptTokens: response.usage?.promptTokens || 0, completionTokens: response.usage?.completionTokens || 0, totalTokens: response.usage?.totalTokens || 0, durationMs: Date.now() - startTime, wordCount: this.countWords(response.content), }, }; this.trackGeneration('blog', undefined, result.metadata?.totalTokens || 0); return result; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Blog generation failed', content: '', }; } } /** * Expand short content into a full blog post */ async expandToBlogPost(shortContent, targetLength = 1000, options) { return this.generateBlogPost({ topic: 'Expansion of content', sections: [ { title: 'Main Content', keyPoints: [shortContent], }, ], targetAudience: options?.targetAudience || 'general', tone: options?.tone || 'informative', }, { ...options, targetLength, }); } // ============================================ // EMAIL GENERATION // ============================================ /** * Generate an email from source content */ async generateEmail(sourceContent, emailType, options) { const startTime = Date.now(); // Validate source content if (!sourceContent || sourceContent.trim().length === 0) { return { success: false, error: 'Source content cannot be empty', content: '', }; } const prompt = this.buildEmailPrompt(sourceContent, emailType, options); try { const response = await this.executeWithFallback({ messages: [ { role: 'system', content: `You are an expert email copywriter specializing in ${emailType} emails. Create compelling, action-oriented content.`, }, { role: 'user', content: prompt }, ], temperature: 0.6, maxTokens: 2000, }); if (!response.success || !response.content) { return { success: false, error: response.error || 'Email generation failed', content: '', }; } const parsed = this.parseEmailContent(response.content); const result = { success: true, content: parsed.body, metadata: { platform: 'email', emailType, subject: parsed.subject, preheader: parsed.preheader, generatedAt: new Date(), model: this.provider.getProviderName(), promptTokens: response.usage?.promptTokens || 0, completionTokens: response.usage?.completionTokens || 0, totalTokens: response.usage?.totalTokens || 0, durationMs: Date.now() - startTime, }, }; this.trackGeneration('email', emailType, result.metadata?.totalTokens || 0); return result; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Email generation failed', content: '', }; } } // ============================================ // SPECIALIZED GENERATION // ============================================ /** * Generate content from git commit data */ async generateFromCommit(commitData, outputFormats) { // Validate commit data if (!commitData.message || commitData.message.trim().length === 0) { return { success: false, commit: commitData, generatedContent: {}, totalGenerated: 0, error: 'Commit message cannot be empty', }; } const results = {}; for (const format of outputFormats) { switch (format) { case 'blog': results[format] = await this.generateBlogFromCommit(commitData); break; case 'social': results[format] = await this.generateSocialFromCommit(commitData); break; case 'email': results[format] = await this.generateEmailFromCommit(commitData); break; } } const successCount = Object.values(results).filter((r) => r?.success).length; return { success: successCount > 0, commit: commitData, generatedContent: results, totalGenerated: successCount, }; } /** * Generate a Twitter/LinkedIn thread from long-form content */ async generateThread(sourceContent, platform, maxPosts = 10) { const startTime = Date.now(); // Validate source content if (!sourceContent || sourceContent.trim().length === 0) { return { success: false, posts: [], error: 'Source content cannot be empty', }; } const prompt = this.buildThreadPrompt(sourceContent, platform, maxPosts); try { const response = await this.executeWithFallback({ messages: [ { role: 'system', content: `You are an expert at creating engaging ${platform} threads that break down complex topics.`, }, { role: 'user', content: prompt }, ], temperature: 0.7, maxTokens: 3000, }); if (!response.success || !response.content) { return { success: false, posts: [], error: response.error || 'Thread generation failed', }; } const posts = this.parseThreadContent(response.content, platform); this.trackGeneration('thread', platform, response.usage?.totalTokens || 0); return { success: true, posts, totalPosts: posts.length, platform, metadata: { generatedAt: new Date(), durationMs: Date.now() - startTime, }, }; } catch (error) { return { success: false, posts: [], error: error instanceof Error ? error.message : 'Thread generation failed', }; } } /** * Generate hashtags for content */ async generateHashtags(content, count = 5, trending) { // Validate content if (!content || content.trim().length === 0) { return { success: false, hashtags: [], error: 'Content cannot be empty', }; } const prompt = `Generate ${count} relevant ${trending ? 'trending ' : ''}hashtags for this content. Return ONLY the hashtags, one per line, with the # symbol:\n\n${content}`; try { const response = await this.executeWithFallback({ messages: [ { role: 'system', content: 'You are a social media hashtag expert. Generate relevant, specific hashtags.', }, { role: 'user', content: prompt }, ], temperature: 0.5, maxTokens: 200, }); if (!response.success || !response.content) { return { success: false, hashtags: [], error: response.error || 'Hashtag generation failed', }; } const hashtags = this.parseHashtags(response.content); return { success: true, hashtags, count: hashtags.length, }; } catch (error) { return { success: false, hashtags: [], error: error instanceof Error ? error.message : 'Hashtag generation failed', }; } } // ============================================ // BATCH OPERATIONS // ============================================ /** * Batch generate content from multiple requests */ async batchGenerate(requests) { const results = await Promise.all(requests.map(async (req) => { if (req.type === 'social' && req.platform) { return { id: req.id, result: await this.generateSocialPost(req.sourceContent, req.platform, req.options), }; } else if (req.type === 'blog' && req.outline) { return { id: req.id, result: await this.generateBlogPost(req.outline, req.blogOptions), }; } else if (req.type === 'email' && req.emailType) { return { id: req.id, result: await this.generateEmail(req.sourceContent, req.emailType, req.emailOptions), }; } else { return { id: req.id, result: { success: false, content: '', error: 'Invalid request: missing required fields for the specified type', }, }; } })); return { success: true, results, totalGenerated: requests.length, successCount: results.filter((r) => r.result.success).length, failureCount: results.filter((r) => !r.result.success).length, }; } // ============================================ // ANALYTICS // ============================================ /** * Get generation analytics for a time range */ getAnalytics(timeRange) { const filteredData = this.analyticsData.filter((d) => d.timestamp >= timeRange.start && d.timestamp <= timeRange.end); const byPlatform = {}; const byType = {}; let totalTokens = 0; for (const data of filteredData) { byType[data.type] = (byType[data.type] || 0) + 1; if (data.platform) { byPlatform[data.platform] = (byPlatform[data.platform] || 0) + 1; } totalTokens += data.tokens; } // Group by date for trend data const trendMap = new Map(); for (const data of filteredData) { const dateKey = data.timestamp.toISOString().split('T')[0]; const existing = trendMap.get(dateKey) || { count: 0, tokens: 0 }; trendMap.set(dateKey, { count: existing.count + 1, tokens: existing.tokens + data.tokens, }); } const trendData = Array.from(trendMap.entries()).map(([dateStr, values]) => ({ date: new Date(dateStr), count: values.count, tokens: values.tokens, })); return { totalGenerations: filteredData.length, byPlatform, byType, averageTokens: filteredData.length > 0 ? totalTokens / filteredData.length : 0, totalCost: totalTokens * 0.00002, // Rough cost estimate trendData, }; } /** * Clear analytics data */ clearAnalytics() { this.analyticsData = []; } // ============================================ // PRIVATE METHODS // ============================================ /** * Execute a completion with fallback to secondary provider */ async executeWithFallback(request) { try { const response = await this.provider.complete(request); if (response.success) { return response; } // Try fallback if primary failed if (this.fallbackProvider) { return await this.fallbackProvider.complete(request); } return response; } catch (error) { // Try fallback on error if (this.fallbackProvider) { try { return await this.fallbackProvider.complete(request); } catch { // Fallback also failed } } throw error; } } /** * Build prompt for social media generation */ buildSocialPrompt(sourceContent, platform, options) { const specs = this.getPlatformSpecifications(platform); let prompt = `Create a ${platform} post from the following content.\n\n`; prompt += `Platform Requirements:\n`; prompt += `- Max length: ${specs.maxLength} characters\n`; prompt += `- Tone: ${specs.tone}\n`; prompt += `- Style: ${specs.style}\n\n`; if (options?.includeHashtags && specs.supportsHashtags) { prompt += `Include ${options.hashtagCount || 3} relevant hashtags\n`; } if (options?.includeEmojis) { prompt += `Use appropriate emojis to enhance engagement\n`; } if (options?.callToAction) { if (options.callToActionText) { prompt += `Include this call to action: "${options.callToActionText}"\n`; } else { prompt += `Include a compelling call to action\n`; } } if (options?.tone) { prompt += `Tone: ${options.tone}\n`; } if (options?.targetAudience) { prompt += `Target audience: ${options.targetAudience}\n`; } prompt += `\nSource Content:\n${sourceContent}\n\n`; prompt += `Provide ONLY the ${platform} post content without any additional explanation or meta-commentary.`; return prompt; } /** * Build prompt for blog generation */ buildBlogPrompt(outline, options) { let prompt = `Write a comprehensive blog post based on this outline:\n\n`; prompt += `Topic: ${outline.topic}\n`; prompt += `Target Audience: ${outline.targetAudience}\n`; prompt += `Tone: ${outline.tone}\n\n`; prompt += `Sections:\n`; outline.sections.forEach((section, i) => { prompt += `\n${i + 1}. ${section.title}\n`; section.keyPoints.forEach((point) => { prompt += ` - ${point}\n`; }); }); if (options?.includeIntro !== false) { prompt += `\nInclude an engaging introduction that hooks the reader\n`; } if (options?.includeConclusion !== false) { prompt += `Include a strong conclusion with key takeaways\n`; } if (options?.targetLength) { prompt += `\nTarget length: approximately ${options.targetLength} words\n`; } if (options?.seoKeywords && options.seoKeywords.length > 0) { prompt += `\nNaturally incorporate these SEO keywords: ${options.seoKeywords.join(', ')}\n`; } prompt += `\nProvide the blog post in markdown format.`; return prompt; } /** * Build prompt for email generation */ buildEmailPrompt(sourceContent, emailType, options) { let prompt = `Create a ${emailType} email from the following content.\n\n`; prompt += `Generate the email with these components:\n`; prompt += `1. Subject line (compelling, ${options?.maxSubjectLength || 60} chars max)\n`; prompt += `2. Preheader text (preview text, 100 chars max)\n`; prompt += `3. Email body (well-structured HTML-friendly content)\n\n`; if (options?.includeCallToAction !== false) { prompt += `Include a clear call to action\n`; } if (options?.tone) { prompt += `Tone: ${options.tone}\n`; } if (options?.targetAudience) { prompt += `Target audience: ${options.targetAudience}\n`; } prompt += `\nSource Content:\n${sourceContent}\n\n`; prompt += `Format your response as JSON: {"subject": "...", "preheader": "...", "body": "..."}`; return prompt; } /** * Build prompt for thread generation */ buildThreadPrompt(sourceContent, platform, maxPosts) { const maxLength = platform === 'twitter' ? 280 : 3000; let prompt = `Create a ${platform} thread (maximum ${maxPosts} posts) from this content.\n\n`; prompt += `Requirements:\n`; prompt += `- Each post must be under ${maxLength} characters\n`; prompt += `- The first post should hook readers\n`; prompt += `- Each post should flow naturally to the next\n`; prompt += `- Use a consistent tone throughout\n`; prompt += `- End with a summary or call to action\n\n`; prompt += `Source Content:\n${sourceContent}\n\n`; prompt += `Format as JSON array of strings: ["post1", "post2", ...]`; return prompt; } /** * Get platform specifications */ getPlatformSpecifications(platform) { const specs = { twitter: { maxLength: 280, tone: 'concise and engaging', style: 'conversational with hashtags', supportsHashtags: true, supportsMedia: true, maxHashtags: 3, }, linkedin: { maxLength: 3000, tone: 'professional', style: 'thought leadership', supportsHashtags: true, supportsMedia: true, maxHashtags: 5, }, facebook: { maxLength: 63206, tone: 'friendly and relatable', style: 'conversational and visual', supportsHashtags: true, supportsMedia: true, maxHashtags: 3, }, instagram: { maxLength: 2200, tone: 'visual-first and aspirational', style: 'storytelling with emojis', supportsHashtags: true, supportsMedia: true, maxHashtags: 30, }, mastodon: { maxLength: 500, tone: 'community-focused', style: 'conversational', supportsHashtags: true, supportsMedia: true, maxHashtags: 5, }, bluesky: { maxLength: 300, tone: 'authentic', style: 'conversational', supportsHashtags: false, supportsMedia: true, }, }; return specs[platform]; } /** * Get max tokens for platform generation */ getPlatformMaxTokens(platform) { const specs = this.getPlatformSpecifications(platform); // Rough estimate: 1 token ≈ 4 characters return Math.ceil(specs.maxLength / 3) + 100; } /** * Parse and clean platform content */ parsePlatformContent(content, _platform) { // Remove any leading/trailing whitespace and quotes let cleaned = content.trim(); // Remove surrounding quotes if present if ((cleaned.startsWith('"') && cleaned.endsWith('"')) || (cleaned.startsWith("'") && cleaned.endsWith("'"))) { cleaned = cleaned.slice(1, -1); } return cleaned; } /** * Parse email content from JSON response */ parseEmailContent(content) { try { // Try to extract JSON from the content const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { const parsed = JSON.parse(jsonMatch[0]); return { subject: parsed.subject || 'Email Subject', preheader: parsed.preheader || '', body: parsed.body || content, }; } } catch { // JSON parsing failed } // Fallback: return content as body return { subject: 'Email Subject', preheader: '', body: content, }; } /** * Parse thread content from JSON response */ parseThreadContent(content, _platform) { try { // Try to extract JSON array from the content const jsonMatch = content.match(/\[[\s\S]*\]/); if (jsonMatch) { const parsed = JSON.parse(jsonMatch[0]); if (Array.isArray(parsed)) { return parsed.map((p) => String(p).trim()); } } } catch { // JSON parsing failed } // Fallback: split by common separators const posts = content.split(/\n\n+/).filter((p) => p.trim().length > 0); return posts.length > 0 ? posts : [content]; } /** * Parse hashtags from content */ parseHashtags(content) { // Match hashtags with the # symbol const hashtagMatches = content.match(/#\w+/g) || []; // Remove # and return unique hashtags const hashtags = hashtagMatches.map((tag) => tag.substring(1)); return [...new Set(hashtags)]; } /** * Generate alternate versions of content */ async generateAlternateVersions(sourceContent, platform, options, count) { const versions = []; for (let i = 0; i < count; i++) { const result = await this.generateSocialPost(sourceContent, platform, { ...options, alternateVersions: 0, // Prevent recursion creativity: Math.min(1, (options.creativity || 0.7) + 0.1), // Slightly more creative }); if (result.success && result.content) { versions.push(result.content); } } return versions; } /** * Generate blog post from commit data */ async generateBlogFromCommit(commitData) { const sourceContent = this.formatCommitForContent(commitData); return this.generateBlogPost({ topic: `Development Update: ${commitData.message.split('\n')[0]}`, sections: [ { title: 'Overview', keyPoints: [sourceContent], }, { title: 'Changes Made', keyPoints: commitData.files.slice(0, 5), }, ], targetAudience: 'developers and technical stakeholders', tone: 'informative and technical', }, { targetLength: 500, includeIntro: true, includeConclusion: true, }); } /** * Generate social post from commit data */ async generateSocialFromCommit(commitData) { const sourceContent = this.formatCommitForContent(commitData); return this.generateSocialPost(sourceContent, 'twitter', { includeHashtags: true, hashtagCount: 3, includeEmojis: true, tone: 'exciting and informative', }); } /** * Generate email from commit data */ async generateEmailFromCommit(commitData) { const sourceContent = this.formatCommitForContent(commitData); return this.generateEmail(sourceContent, 'announcement', { includeCallToAction: true, tone: 'professional', }); } /** * Format commit data for content generation */ formatCommitForContent(commitData) { let content = `Commit: ${commitData.message}\n`; content += `Author: ${commitData.author}\n`; content += `Files changed: ${commitData.files.length}\n`; content += `Changes: +${commitData.additions} -${commitData.deletions}\n`; if (commitData.files.length > 0) { content += `\nFiles:\n${commitData.files.slice(0, 10).join('\n')}`; } if (commitData.diff) { content += `\n\nKey changes:\n${commitData.diff.substring(0, 1000)}`; } return content; } /** * Count words in content */ countWords(content) { return content.trim().split(/\s+/).filter((word) => word.length > 0).length; } /** * Track generation for analytics */ trackGeneration(type, platform, tokens) { if (this.config.enableAnalytics) { this.analyticsData.push({ timestamp: new Date(), type, platform, tokens, }); } } } //# sourceMappingURL=ai-content-generator.js.map