voxblog/apps/api/src/ai-generate.ts

226 lines
7.1 KiB
TypeScript

import express from 'express';
import OpenAI from 'openai';
import { db } from './db';
import { settings } from './db/schema';
import { eq } from 'drizzle-orm';
const router = express.Router();
// System prompt that defines how the AI should generate articles
const SYSTEM_PROMPT = `You are an expert content writer creating high-quality blog articles for Ghost CMS.
CRITICAL REQUIREMENTS:
1. Generate production-ready HTML content that can be published directly to Ghost
2. Use semantic HTML5 tags: <h2>, <h3>, <p>, <ul>, <ol>, <blockquote>, <strong>, <em>
3. For images, use this EXACT placeholder format: {{IMAGE:description_of_image}}
- Example: {{IMAGE:screenshot_of_dashboard}}
- Example: {{IMAGE:team_photo_at_conference}}
- Use descriptive, snake_case names that indicate what the image should show
4. Structure articles with clear sections using headings
5. Write engaging, SEO-friendly content with natural keyword integration
6. Include a compelling introduction and conclusion
7. Use lists and formatting to improve readability
8. Do NOT include <html>, <head>, <body> tags - only the article content
9. Do NOT use markdown - use HTML tags only
10. Ensure all HTML is valid and properly closed
OUTPUT FORMAT:
Return only the HTML content, ready to be inserted into Ghost's content editor.`;
router.post('/generate', async (req, res) => {
try {
const {
prompt,
audioTranscriptions,
selectedImageUrls,
} = req.body as {
prompt: string;
audioTranscriptions?: string[];
selectedImageUrls?: string[];
};
if (!prompt) {
return res.status(400).json({ error: 'prompt is required' });
}
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: 'OpenAI API key not configured' });
}
const openai = new OpenAI({ apiKey });
// Get system prompt from settings or use default
let systemPrompt = SYSTEM_PROMPT;
try {
const settingRows = await db
.select()
.from(settings)
.where(eq(settings.key, 'system_prompt'))
.limit(1);
if (settingRows.length > 0) {
systemPrompt = settingRows[0].value;
console.log('[AI Generate] Using custom system prompt from settings');
} else {
console.log('[AI Generate] Using default system prompt');
}
} catch (err) {
console.warn('[AI Generate] Failed to load system prompt from settings, using default:', err);
}
// Build context from audio transcriptions
let contextSection = '';
if (audioTranscriptions && audioTranscriptions.length > 0) {
contextSection += '\n\nAUDIO TRANSCRIPTIONS:\n';
audioTranscriptions.forEach((transcript, idx) => {
contextSection += `\n[Transcript ${idx + 1}]:\n${transcript}\n`;
});
}
// Add information about available images
if (selectedImageUrls && selectedImageUrls.length > 0) {
contextSection += '\n\nAVAILABLE IMAGES:\n';
contextSection += `You have ${selectedImageUrls.length} images available. Use {{IMAGE:description}} placeholders where images should be inserted.\n`;
}
const userPrompt = `${prompt}${contextSection}`;
console.log('[AI Generate] Starting generation with OpenAI...');
console.log('[AI Generate] User prompt length:', userPrompt.length);
const completion = await openai.chat.completions.create({
model: 'gpt-4o', // Using GPT-4 with internet access capability
messages: [
{
role: 'system',
content: systemPrompt,
},
{
role: 'user',
content: userPrompt,
},
],
temperature: 0.7,
max_tokens: 4000,
});
const generatedContent = completion.choices[0]?.message?.content || '';
if (!generatedContent) {
return res.status(500).json({ error: 'No content generated' });
}
console.log('[AI Generate] Generation successful, length:', generatedContent.length);
// Extract image placeholders for tracking
const imagePlaceholderRegex = /\{\{IMAGE:([^}]+)\}\}/g;
const imagePlaceholders: string[] = [];
let match;
while ((match = imagePlaceholderRegex.exec(generatedContent)) !== null) {
imagePlaceholders.push(match[1]);
}
return res.json({
content: generatedContent,
imagePlaceholders,
tokensUsed: completion.usage?.total_tokens || 0,
model: completion.model,
});
} catch (err: any) {
console.error('[AI Generate] Error:', err);
return res.status(500).json({
error: 'AI generation failed',
details: err?.message || 'Unknown error'
});
}
});
// Generate metadata (title, tags, canonical URL) from content
router.post('/generate-metadata', async (req, res) => {
try {
const { contentHtml } = req.body as { contentHtml: string };
if (!contentHtml) {
return res.status(400).json({ error: 'contentHtml is required' });
}
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
return res.status(500).json({ error: 'OpenAI API key not configured' });
}
const openai = new OpenAI({ apiKey });
// Strip HTML tags for better analysis
const textContent = contentHtml.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
const preview = textContent.slice(0, 2000); // First 2000 chars
console.log('[AI Metadata] Generating metadata...');
const completion = await openai.chat.completions.create({
model: 'gpt-4o',
messages: [
{
role: 'system',
content: `You are an SEO expert. Generate metadata for blog posts.
REQUIREMENTS:
1. Title: Compelling, SEO-friendly, 50-60 characters
2. Tags: 3-5 relevant tags, comma-separated
3. Canonical URL: SEO-friendly slug based on title (lowercase, hyphens, no special chars)
OUTPUT FORMAT (JSON):
{
"title": "Your Compelling Title Here",
"tags": "tag1, tag2, tag3",
"canonicalUrl": "your-seo-friendly-slug"
}
Return ONLY valid JSON, no markdown, no explanation.`,
},
{
role: 'user',
content: `Generate metadata for this article:\n\n${preview}`,
},
],
temperature: 0.7,
max_tokens: 300,
});
const response = completion.choices[0]?.message?.content || '';
if (!response) {
return res.status(500).json({ error: 'No metadata generated' });
}
console.log('[AI Metadata] Raw response:', response);
// Parse JSON response
let metadata;
try {
// Remove markdown code blocks if present
const cleaned = response.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
metadata = JSON.parse(cleaned);
} catch (parseErr) {
console.error('[AI Metadata] JSON parse error:', parseErr);
return res.status(500).json({ error: 'Failed to parse metadata response' });
}
console.log('[AI Metadata] Generated:', metadata);
return res.json({
title: metadata.title || '',
tags: metadata.tags || '',
canonicalUrl: metadata.canonicalUrl || '',
});
} catch (err: any) {
console.error('[AI Metadata] Error:', err);
return res.status(500).json({
error: 'Metadata generation failed',
details: err?.message || 'Unknown error'
});
}
});
export default router;