UNPKG

contaigents

Version:

Modular AI Content Ecosystem with Audio Generation

976 lines (956 loc) 42.6 kB
import { BaseTool } from './BaseTool.js'; import { BrowserServiceManager } from './BrowserService.js'; import { LLMFactory } from '../llm/LLMFactory.js'; import path from 'path'; import fs from 'fs/promises'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * TextToImageTool - Render text as images using HTML/CSS layouts and Playwright screenshots * * This tool provides a unified interface for converting text into images using: * - Predefined HTML/CSS templates for common layouts * - Custom HTML/CSS injection for advanced layouts * - Playwright browser automation for high-quality screenshots * - Tailwind CSS integration for modern styling */ export class TextToImageTool extends BaseTool { constructor(baseDir) { super(); this.baseDir = baseDir; } getName() { return 'text_to_image'; } getDescription() { return 'Render text as high-quality images using AI-generated HTML layouts with ONLY Tailwind CSS classes and browser screenshots. Creates custom, responsive designs based on your requirements using pure Tailwind utilities - NO raw CSS or custom styles. SUPPORTS TRANSPARENT BACKGROUNDS: Generate PNG images with transparent backgrounds for overlays and compositing. SUPPORTS ITERATIVE DESIGN: Saves HTML files alongside images for editing and re-rendering. Agents can modify saved HTML files using file tools, then re-render instantly without AI generation. Perfect for creating social media graphics, banners, cards, presentations, logos, and any styled text content with complete creative control and iterative refinement.'; } getAgentGuidance() { return ` ## 🎨 AI-POWERED TEXT-TO-IMAGE GENERATION ### What This Tool Does: - Generates custom HTML layouts using AI based on your design requirements - Uses Tailwind CSS for modern, responsive styling - Creates high-quality images via browser rendering - Perfect for social media, presentations, marketing materials, and creative content ### How to Use: 1. **Describe the design**: Provide clear instructions about the visual style, layout, colors, and mood 2. **Specify content**: Include the text content to be rendered 3. **Set dimensions**: Choose appropriate width/height for your use case 4. **Let AI create**: The tool generates custom HTML/CSS tailored to your requirements ### Design Description Examples: - "Modern minimalist card with gradient background and bold typography" - "Retro 80s neon style with synthwave colors and glowing text effects" - "Professional business card layout with clean typography and subtle shadows" - "Instagram story format with vibrant colors and emoji integration" - "Technical documentation style with code syntax highlighting" - "Vintage poster design with distressed textures and classic fonts" ### Best Practices: - Be specific about visual style, colors, and mood - Mention target platform (Instagram, Twitter, presentation, etc.) - Include any special requirements (gradients, shadows, icons, etc.) - Specify text hierarchy (title, subtitle, body text) - Consider readability and contrast ### AI Capabilities: - Generates complete HTML using ONLY Tailwind CSS classes - Creates responsive, modern layouts with Tailwind utilities - Handles complex styling (gradients, shadows, animations) using Tailwind's built-in classes - Adapts to any design style or theme using Tailwind's comprehensive utility system - Ensures proper text formatting and readability with Tailwind typography classes - Uses Tailwind CDN and web fonts - NO custom CSS or inline styles ### Output: - High-quality PNG images - Custom dimensions as specified - Professional browser rendering - Optimized file sizes - **HTML template files** saved alongside images for reuse ## 🔄 ITERATIVE DESIGN WORKFLOW (IMPORTANT FOR AGENTS!) ### Template Management: - **Every generation saves TWO files**: image.png AND image.html - **HTML files are reusable templates** that can be edited and re-rendered - **Use file tools to modify HTML**, then re-render instantly without AI ### Agent Workflow Example: \`\`\` 1. GENERATE: text_to_image(text="Hello World", design_prompt="blue gradient", output_path="design.png") → Creates: design.png + design.html 2. EDIT: Use file_write_tool to modify design.html: - Change colors: class="bg-blue-500" → class="bg-red-500" - Update fonts: class="font-sans" → class="font-serif" - Modify layout: Add borders, shadows, etc. 3. RE-RENDER: text_to_image(html_file_path="design.html", output_path="variation.png") → Creates: variation.png instantly (no AI, <2 seconds) \`\`\` ### Performance Modes: - **Regular mode**: Loads HTML file, processes text, renders image - **HTML-only mode**: html_only=true for maximum performance (no text processing) ### When to Use Each Mode: - **First generation**: Use design_prompt to create initial template - **Iterations**: Use html_file_path to reuse and modify existing templates - **Maximum speed**: Use html_only=true when you don't need text processing `; } getParameters() { return [ { name: 'text', type: 'string', description: 'The text content to render as an image. Can include basic formatting and line breaks. Not required in html_only mode.', required: false }, { name: 'output_path', type: 'string', description: 'Output file path for the generated image (relative to project root). Should end with .png', required: true }, { name: 'design_prompt', type: 'string', description: 'Detailed description of the desired visual design, style, layout, colors, and mood. Be specific about the visual aesthetic you want. Not required if html_file_path is provided.', required: false }, { name: 'html_file_path', type: 'string', description: 'Path to existing HTML file to use for rendering (relative to project root). Alternative to design_prompt for iterating on existing designs.', required: false }, { name: 'html_only', type: 'boolean', description: 'HTML-only mode: Skip all text processing and render HTML file directly. Requires html_file_path. Maximum performance for pure HTML rendering.', required: false, default: false }, { name: 'width', type: 'number', description: 'Image width in pixels (default: 1200)', required: false, default: 1200 }, { name: 'height', type: 'number', description: 'Image height in pixels (default: 630)', required: false, default: 630 }, { name: 'transparent', type: 'boolean', description: 'Generate PNG with transparent background instead of solid background (default: false)', required: false, default: false } ]; } async execute(parameters) { // Validate parameters const validationError = this.validateParameters(parameters); if (validationError) { return validationError; } const { text, output_path, design_prompt, html_file_path, html_only = false, width = 1200, height = 630, transparent = false } = parameters; try { // Validate text content (not required in HTML-only mode) if (!html_only && (!text || typeof text !== 'string' || !text.trim())) { return { success: false, error: 'Text content cannot be empty (unless using html_only mode)' }; } // Validate that either design_prompt or html_file_path is provided if (!design_prompt && !html_file_path) { return { success: false, error: 'Either design_prompt or html_file_path must be provided' }; } // Validate design prompt if provided if (design_prompt && (typeof design_prompt !== 'string' || !design_prompt.trim())) { return { success: false, error: 'Design prompt must be a non-empty string when provided' }; } // Validate html_file_path if provided if (html_file_path && (typeof html_file_path !== 'string' || !html_file_path.trim())) { return { success: false, error: 'HTML file path must be a non-empty string when provided' }; } // Validate html_only mode if (html_only && !html_file_path) { return { success: false, error: 'HTML-only mode requires html_file_path to be provided' }; } // HTML-only mode: optimized path for pure HTML rendering if (html_only) { return await this.renderHtmlOnly(html_file_path, output_path, width, height, transparent); } // Validate and resolve output path const pathValidation = await this.validateAndResolveOutputPath(output_path); if (!pathValidation.success) { return { success: false, error: pathValidation.error }; } // Get HTML content either from file or generate via AI let htmlContent; let htmlFilePath; let isFromFile = false; if (html_file_path) { // Load HTML from existing file console.log(`📂 Loading HTML from file: "${html_file_path}"`); const htmlLoadResult = await this.loadHtmlFromFile(html_file_path); if (!htmlLoadResult.success) { return { success: false, error: htmlLoadResult.error }; } htmlContent = htmlLoadResult.html; htmlFilePath = htmlLoadResult.resolvedPath; isFromFile = true; } else { // Generate custom HTML using AI console.log(`🤖 Generating custom design: "${design_prompt}"`); const customTemplateResult = await this.generateCustomTemplate(design_prompt, text, width, height, transparent); if (!customTemplateResult.success) { return { success: false, error: customTemplateResult.error }; } htmlContent = customTemplateResult.html; // Save HTML file alongside PNG htmlFilePath = await this.saveHtmlFile(pathValidation.resolvedPath, htmlContent, { text, design_prompt: design_prompt, width, height, timestamp: new Date().toISOString() }); } // Get browser service and render to image console.log(`📝 Generating AI-designed text-to-image`); console.log(`📐 Dimensions: ${width}x${height}`); console.log(`🎨 Design: ${design_prompt}`); const browserService = await BrowserServiceManager.getInstance(); await browserService.renderToImage(htmlContent, { width, height, outputPath: pathValidation.resolvedPath, transparent }); // Get file stats const fileStats = await fs.stat(pathValidation.resolvedPath); const fileSize = fileStats.size; const htmlFileStats = await fs.stat(htmlFilePath); const htmlFileSize = htmlFileStats.size; return { success: true, data: { output_file_path: path.relative(this.baseDir, pathValidation.resolvedPath), absolute_path: pathValidation.resolvedPath, file_size: fileSize, html_file_path: path.relative(this.baseDir, htmlFilePath), html_absolute_path: htmlFilePath, html_file_size: htmlFileSize, design_prompt: design_prompt || 'Loaded from HTML file', html_source: isFromFile ? 'loaded_from_file' : 'ai_generated', dimensions: `${width}x${height}`, text_length: text.length, mime_type: 'image/png' }, message: isFromFile ? `Text image generated successfully from existing HTML template. Image saved to: ${path.relative(this.baseDir, pathValidation.resolvedPath)} (${this.formatFileSize(fileSize)}) using HTML: ${path.relative(this.baseDir, htmlFilePath)} (${this.formatFileSize(htmlFileSize)})` : `Text image generated successfully using AI-generated design. Saved to: ${path.relative(this.baseDir, pathValidation.resolvedPath)} (${this.formatFileSize(fileSize)}) and HTML to: ${path.relative(this.baseDir, htmlFilePath)} (${this.formatFileSize(htmlFileSize)})` }; } catch (error) { return { success: false, error: `Failed to generate text image: ${error.message}` }; } } /** * Generate HTML content based on template and parameters */ async generateHTML(options) { if (options.template === 'custom') { // Use custom HTML directly return this.processTemplate(options.custom_html, options); } // Load template file const templatePath = path.join(__dirname, 'templates', `${options.template}.html`); try { const templateContent = await fs.readFile(templatePath, 'utf-8'); return this.processTemplate(templateContent, options); } catch (error) { throw new Error(`Failed to load template "${options.template}": ${error.message}`); } } /** * Process template by replacing placeholders with actual values */ processTemplate(template, options) { let processedTemplate = template; // Replace all template variables const replacements = { '{{text}}': this.escapeHtml(options.text), '{{width}}': options.width.toString(), '{{height}}': options.height.toString(), '{{background_color}}': options.background_color, '{{text_color}}': options.text_color, '{{font_family}}': options.font_family, '{{font_size}}': options.font_size.toString(), '{{padding}}': options.padding.toString(), '{{custom_css}}': options.custom_css }; // Add custom variables for (const [key, value] of Object.entries(options.variables)) { const placeholder = `{{${key}}}`; replacements[placeholder] = this.escapeHtml(String(value)); } // Replace all placeholders for (const [placeholder, value] of Object.entries(replacements)) { processedTemplate = processedTemplate.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), value); } // Handle simple conditionals like {{#if variable}}content{{/if}} processedTemplate = this.processConditionals(processedTemplate, options.variables); return processedTemplate; } /** * Process simple conditionals in templates */ processConditionals(template, variables) { // Handle {{#if variable}}content{{/if}} patterns const conditionalRegex = /\{\{#if\s+(\w+)\}\}(.*?)\{\{\/if\}\}/gs; return template.replace(conditionalRegex, (_match, variableName, content) => { const variableValue = variables[variableName]; // Show content if variable exists and is truthy if (variableValue && variableValue !== '' && variableValue !== '0' && variableValue !== 'false') { return content; } return ''; }); } /** * Escape HTML special characters to prevent XSS */ escapeHtml(text) { const htmlEscapes = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }; return text.replace(/[&<>"']/g, (match) => htmlEscapes[match]); } /** * Generate custom template using LLM */ async generateCustomTemplate(designPrompt, text, width, height, transparent = false) { try { // Try to get a configured provider, fallback to openai let llm; try { llm = await LLMFactory.getConfiguredProvider(); if (!llm) { llm = LLMFactory.getProvider('openai'); } } catch (error) { llm = LLMFactory.getProvider('openai'); } const prompt = `Create a complete HTML document for text-to-image rendering: DESIGN: ${designPrompt} TEXT: ${text} SIZE: ${width}x${height}px TRANSPARENT: ${transparent ? 'YES - Use transparent background' : 'NO - Use solid background'} Requirements: - Complete HTML5 with DOCTYPE - Include Tailwind CSS CDN - USE ONLY TAILWIND CSS CLASSES - NO RAW CSS OR CUSTOM STYLES - Do NOT write any custom CSS in <style> tags or inline styles - Use only Tailwind utility classes for all styling (colors, fonts, spacing, etc.) - Exact dimensions: ${width}x${height}px using Tailwind classes - Professional typography and layout using Tailwind typography classes - Match the design requirements exactly using only Tailwind utilities - Ensure excellent readability with Tailwind color and contrast classes - If you need custom effects, use Tailwind's built-in utilities like gradients, shadows, transforms ${transparent ? '- TRANSPARENT BACKGROUND: Do NOT add background colors to the body or main container. Use transparent/no background for the main container so the image will have a transparent background.' : '- SOLID BACKGROUND: Use appropriate background colors for the design.'} EXAMPLE - Simple Quote Card (Tailwind-only): <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <script src="https://cdn.tailwindcss.com"></script> </head> <body class="m-0 p-0" style="width: 800px; height: 600px;"> <div class="w-full h-full bg-gradient-to-br from-blue-50 to-indigo-100 p-8 flex items-center justify-center"> <div class="bg-white rounded-xl shadow-lg p-6 max-w-md"> <blockquote class="text-gray-800 text-lg font-medium mb-4"> "Your quote text here" </blockquote> <p class="text-gray-600 text-sm">— Author Name</p> </div> </div> </body> </html> IMPORTANT: Do not include any <style> tags or style="" attributes. Use ONLY Tailwind CSS classes like the example above. Return ONLY the HTML code, no explanations or markdown.`; const response = await llm.executePrompt(prompt, { temperature: 0.7, maxTokens: 8000 // Further increased token limit for complete HTML generation }); // Basic validation and cleanup const cleanedHtml = this.validateAndCleanGeneratedTemplate(response.content); return { success: true, html: cleanedHtml, width, height }; } catch (error) { console.log(`⚠️ LLM generation failed, using fallback template: ${error.message}`); // Fallback: create a simple custom template based on the prompt const fallbackHtml = this.createFallbackTemplate(designPrompt, text, width, height, transparent); return { success: true, html: fallbackHtml, width, height }; } } /** * Create a fallback template when LLM generation fails */ createFallbackTemplate(designPrompt, text, width, height, transparent = false) { // Analyze the prompt for style hints with more comprehensive pattern matching const prompt = designPrompt.toLowerCase(); // Color analysis const hasBlue = /blue/i.test(designPrompt); const hasRed = /red/i.test(designPrompt); const hasGreen = /green/i.test(designPrompt); const hasYellow = /yellow/i.test(designPrompt); const hasPurple = /purple/i.test(designPrompt); const hasPink = /pink/i.test(designPrompt); // Style analysis const isRetro = /retro|80s|synthwave|neon|vintage/i.test(designPrompt); const isMinimal = /minimal|clean|simple|modern/i.test(designPrompt); const isGradient = /gradient|colorful|vibrant/i.test(designPrompt); const isDark = /dark|black|night/i.test(designPrompt); const hasRounded = /rounded|round/i.test(designPrompt); const hasBorder = /border/i.test(designPrompt); const hasShadow = /shadow/i.test(designPrompt); let bgClasses = transparent ? '' : 'bg-gray-100'; let textClasses = transparent ? 'text-gray-900' : 'text-gray-900'; let containerClasses = 'p-8'; let shadowClasses = ''; let borderClasses = ''; // Build background based on colors and style (skip if transparent) if (!transparent) { if (isGradient || hasBlue) { if (hasBlue && hasYellow) { bgClasses = 'bg-gradient-to-br from-blue-500 via-blue-600 to-blue-700'; } else if (hasBlue) { bgClasses = 'bg-gradient-to-br from-blue-400 via-blue-500 to-blue-600'; } else if (isGradient) { bgClasses = 'bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500'; } textClasses = 'text-white'; } else if (isRetro) { bgClasses = 'bg-gradient-to-br from-purple-600 via-pink-600 to-blue-600'; textClasses = 'text-white'; containerClasses = 'p-8 font-mono'; } else if (isMinimal) { bgClasses = 'bg-white'; textClasses = 'text-gray-800'; containerClasses = 'p-12'; } else if (isDark) { bgClasses = 'bg-gray-900'; textClasses = 'text-white'; containerClasses = 'p-8'; } } else { // For transparent backgrounds, ensure good text contrast textClasses = 'text-gray-900'; if (isDark) { textClasses = 'text-white'; } } // Add shadow using Tailwind classes if requested if (hasShadow) { shadowClasses = 'drop-shadow-lg'; } // Add border if requested if (hasBorder) { borderClasses = 'border-2 border-white border-opacity-30'; } // Add rounded corners if requested const roundedClasses = hasRounded ? 'rounded-lg' : ''; return `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI Generated Text Image</title> <script src="https://cdn.tailwindcss.com"></script> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet"> <script> tailwind.config = { theme: { extend: { fontFamily: { 'inter': ['Inter', 'sans-serif'] } } } } </script> </head> <body class="m-0 p-0 font-inter" style="width: ${width}px; height: ${height}px;"> <div class="w-full h-full ${bgClasses} ${containerClasses} ${roundedClasses} ${borderClasses} flex items-center justify-center"> <div class="${textClasses} text-center max-w-full"> <div class="text-2xl md:text-3xl lg:text-4xl font-bold leading-tight whitespace-pre-line ${shadowClasses}"> ${text} </div> </div> </div> </body> </html>`; } /** * Validate and clean generated HTML template */ validateAndCleanGeneratedTemplate(html) { // Remove markdown code blocks if present let cleaned = html.replace(/```html\n?/g, '').replace(/```\n?/g, ''); // Ensure it starts with <!DOCTYPE html> if (!cleaned.includes('<!DOCTYPE html>')) { cleaned = '<!DOCTYPE html>\n' + cleaned; } // Basic security: ensure no script tags (except Tailwind CDN and config) cleaned = cleaned.replace(/<script(?![^>]*tailwind)[^>]*>.*?<\/script>/gis, ''); // GUARDRAIL: Remove any <style> tags to enforce Tailwind-only approach cleaned = cleaned.replace(/<style[^>]*>.*?<\/style>/gis, ''); // GUARDRAIL: Remove inline style attributes (except for body dimensions which are necessary) cleaned = cleaned.replace(/style="[^"]*"/gi, (match) => { // Keep style attributes that only contain width/height for body element if (match.includes('width:') && match.includes('height:') && !match.includes('color') && !match.includes('background') && !match.includes('font')) { return match; } return ''; }); // Log warning if raw CSS was detected and removed if (html !== cleaned) { console.log('⚠️ Raw CSS detected and removed from generated HTML. Using Tailwind-only approach.'); } return cleaned.trim(); } /** * Get preset configuration for social media platforms */ getPresetConfiguration(preset) { const presets = { 'instagram-square': { width: 1080, height: 1080, template: 'card', fontSize: 48 }, 'instagram-story': { width: 1080, height: 1920, template: 'card', fontSize: 52 }, 'twitter-post': { width: 1200, height: 675, template: 'card', fontSize: 36 }, 'linkedin-post': { width: 1200, height: 630, template: 'card', fontSize: 32 }, 'facebook-cover': { width: 1200, height: 315, template: 'banner', fontSize: 28 }, 'youtube-thumbnail': { width: 1280, height: 720, template: 'card', fontSize: 42 }, 'pinterest-pin': { width: 1000, height: 1500, template: 'card', fontSize: 38 } }; if (!presets[preset]) { return { success: false, error: `Invalid preset "${preset}". Available presets: ${Object.keys(presets).join(', ')}` }; } return { success: true, ...presets[preset] }; } /** * Format file size in human readable format */ formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } /** * Validate and resolve output path with security checks * (Copied from ImageGenerationTool for consistency) */ async validateAndResolveOutputPath(outputPath) { try { // Normalize the path to prevent directory traversal const normalizedPath = path.normalize(outputPath); // Check for directory traversal attempts if (normalizedPath.includes('..') || path.isAbsolute(normalizedPath)) { return { success: false, error: 'Output path must be relative and cannot contain ".." or be absolute for security reasons' }; } // Resolve full path const fullPath = path.join(this.baseDir, normalizedPath); // Ensure the resolved path is still within the base directory const relativePath = path.relative(this.baseDir, fullPath); if (relativePath.startsWith('..')) { return { success: false, error: 'Output path resolves outside of project directory' }; } // Create parent directories if they don't exist const parentDir = path.dirname(fullPath); await fs.mkdir(parentDir, { recursive: true }); return { success: true, resolvedPath: fullPath }; } catch (error) { return { success: false, error: `Failed to resolve output path: ${error.message}` }; } } /** * Save HTML file alongside the PNG output with metadata comments */ async saveHtmlFile(pngFilePath, htmlContent, metadata) { // Generate HTML file path by replacing .png extension with .html const htmlFilePath = pngFilePath.replace(/\.png$/i, '.html'); // Add metadata comments to the HTML content const htmlWithMetadata = this.addMetadataToHtml(htmlContent, metadata); // Save the HTML file await fs.writeFile(htmlFilePath, htmlWithMetadata, 'utf-8'); console.log(`💾 HTML template saved: ${path.relative(this.baseDir, htmlFilePath)}`); return htmlFilePath; } /** * Add metadata comments to HTML content */ addMetadataToHtml(htmlContent, metadata) { const metadataComment = `<!-- Generated by Contaigents TextToImageTool Timestamp: ${metadata.timestamp} Text Content: ${metadata.text} Design Prompt: ${metadata.design_prompt} Dimensions: ${metadata.width}x${metadata.height} --> `; // Insert metadata comment after DOCTYPE but before <html> if (htmlContent.includes('<!DOCTYPE html>')) { return htmlContent.replace('<!DOCTYPE html>', `<!DOCTYPE html>\n${metadataComment}`); } else { // If no DOCTYPE, add at the beginning return metadataComment + htmlContent; } } /** * Load HTML content from existing file */ async loadHtmlFromFile(htmlFilePath) { try { // Validate and resolve HTML file path const pathValidation = await this.validateAndResolveInputPath(htmlFilePath); if (!pathValidation.success) { return { success: false, error: pathValidation.error }; } // Read HTML file content const htmlContent = await fs.readFile(pathValidation.resolvedPath, 'utf-8'); // Basic validation that it's HTML content if (!htmlContent.includes('<html') && !htmlContent.includes('<!DOCTYPE')) { return { success: false, error: 'File does not appear to contain valid HTML content' }; } console.log(`📂 Loaded HTML template: ${path.relative(this.baseDir, pathValidation.resolvedPath)} (${htmlContent.length} chars)`); return { success: true, html: htmlContent, resolvedPath: pathValidation.resolvedPath }; } catch (error) { return { success: false, error: `Failed to load HTML file: ${error.message}` }; } } /** * Validate and resolve input file path (similar to output path but for reading) */ async validateAndResolveInputPath(inputPath) { try { // Normalize the path to prevent directory traversal const normalizedPath = path.normalize(inputPath); // Check for directory traversal attempts if (normalizedPath.includes('..') || path.isAbsolute(normalizedPath)) { return { success: false, error: 'Input path must be relative and cannot contain ".." or be absolute for security reasons' }; } // Resolve full path const fullPath = path.join(this.baseDir, normalizedPath); // Ensure the resolved path is still within the base directory const relativePath = path.relative(this.baseDir, fullPath); if (relativePath.startsWith('..')) { return { success: false, error: 'Input path resolves outside of project directory' }; } // Check if file exists try { await fs.access(fullPath, fs.constants.R_OK); } catch (error) { return { success: false, error: `HTML file does not exist or is not readable: ${inputPath}` }; } return { success: true, resolvedPath: fullPath }; } catch (error) { return { success: false, error: `Failed to resolve input path: ${error.message}` }; } } /** * HTML-only rendering mode: optimized path for pure HTML-to-image conversion * Skips all text processing and LLM generation for maximum performance */ async renderHtmlOnly(htmlFilePath, outputPath, width, height, transparent = false) { try { console.log(`⚡ HTML-only mode: Maximum performance rendering`); console.log(`📂 HTML: ${htmlFilePath}`); console.log(`📐 Dimensions: ${width}x${height}`); // Validate and resolve HTML file path const htmlPathValidation = await this.validateAndResolveInputPath(htmlFilePath); if (!htmlPathValidation.success) { return { success: false, error: htmlPathValidation.error }; } // Validate and resolve output path const outputPathValidation = await this.validateAndResolveOutputPath(outputPath); if (!outputPathValidation.success) { return { success: false, error: outputPathValidation.error }; } // Load HTML content directly const htmlContent = await fs.readFile(htmlPathValidation.resolvedPath, 'utf-8'); // Basic HTML validation if (!htmlContent.includes('<html') && !htmlContent.includes('<!DOCTYPE')) { return { success: false, error: 'File does not appear to contain valid HTML content' }; } // Optimized browser rendering console.log(`🚀 Rendering HTML to image (optimized path)`); const browserService = await BrowserServiceManager.getInstance(); await browserService.renderToImage(htmlContent, { width, height, outputPath: outputPathValidation.resolvedPath, transparent }); // Get file stats const fileStats = await fs.stat(outputPathValidation.resolvedPath); const fileSize = fileStats.size; const htmlFileStats = await fs.stat(htmlPathValidation.resolvedPath); const htmlFileSize = htmlFileStats.size; return { success: true, data: { output_file_path: path.relative(this.baseDir, outputPathValidation.resolvedPath), absolute_path: outputPathValidation.resolvedPath, file_size: fileSize, html_file_path: path.relative(this.baseDir, htmlPathValidation.resolvedPath), html_absolute_path: htmlPathValidation.resolvedPath, html_file_size: htmlFileSize, design_prompt: 'HTML-only mode (no text processing)', html_source: 'html_only_mode', dimensions: `${width}x${height}`, text_length: 0, mime_type: 'image/png', rendering_mode: 'html_only' }, message: `HTML-only rendering complete! Image saved to: ${path.relative(this.baseDir, outputPathValidation.resolvedPath)} (${this.formatFileSize(fileSize)}) from HTML: ${path.relative(this.baseDir, htmlPathValidation.resolvedPath)} (${this.formatFileSize(htmlFileSize)})` }; } catch (error) { return { success: false, error: `Failed to render HTML-only: ${error.message}` }; } } /** * List available HTML templates in the project directory * Helps agents discover and manage existing templates */ async listAvailableTemplates() { try { console.log(`📋 Discovering HTML templates in project directory...`); // Find all HTML files in the project directory const htmlFiles = await this.findHtmlFiles(this.baseDir); if (htmlFiles.length === 0) { return { success: true, data: { templates: [], count: 0, message: 'No HTML templates found in project directory' }, message: 'No HTML templates found in project directory. Create some templates first using the text-to-image tool.' }; } // Analyze each template for metadata const templates = await Promise.all(htmlFiles.map(async (filePath) => { const metadata = await this.analyzeTemplate(filePath); return { file_path: path.relative(this.baseDir, filePath), absolute_path: filePath, ...metadata }; })); return { success: true, data: { templates, count: templates.length, project_directory: this.baseDir }, message: `Found ${templates.length} HTML template(s) in project directory. Use --html-file-path to render any of these templates.` }; } catch (error) { return { success: false, error: `Failed to list templates: ${error.message}` }; } } /** * Find all HTML files in a directory recursively */ async findHtmlFiles(directory) { const htmlFiles = []; const scanDirectory = async (dir, depth = 0) => { // Limit recursion depth to avoid infinite loops if (depth > 3) return; try { const entries = await fs.readdir(dir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(dir, entry.name); // Skip hidden directories and node_modules if (entry.name.startsWith('.') || entry.name === 'node_modules') { continue; } if (entry.isDirectory()) { await scanDirectory(fullPath, depth + 1); } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) { htmlFiles.push(fullPath); } } } catch (error) { // Skip directories we can't read console.warn(`Warning: Could not read directory ${dir}: ${error.message}`); } }; await scanDirectory(directory); return htmlFiles.sort(); } /** * Analyze an HTML template file for metadata */ async analyzeTemplate(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); const stats = await fs.stat(filePath); // Extract metadata from comments if present const metadataMatch = content.match(/<!--\s*Generated by Contaigents TextToImageTool\s*([\s\S]*?)\s*-->/); let metadata = {}; if (metadataMatch) { const metadataText = metadataMatch[1]; const timestampMatch = metadataText.match(/Timestamp:\s*(.+)/); const designMatch = metadataText.match(/Design Prompt:\s*(.+)/); const dimensionsMatch = metadataText.match(/Dimensions:\s*(.+)/); const textMatch = metadataText.match(/Text Content:\s*(.+)/); metadata = { created_date: timestampMatch ? timestampMatch[1].trim() : undefined, design_prompt: designMatch ? designMatch[1].trim() : undefined, dimensions: dimensionsMatch ? dimensionsMatch[1].trim() : undefined, text_content: textMatch ? textMatch[1].trim() : undefined, has_metadata: true }; } else { metadata = { has_metadata: false }; } return { file_size: stats.size, ...metadata }; } catch (error) { return { file_size: 0, has_metadata: false }; } } }