UNPKG

@gv-sh/specgen-server

Version:

SpecGen Server - API for Speculative Fiction Generator

363 lines (284 loc) 14.3 kB
/* global describe, test, expect */ /** * Unit tests for visual element extraction from story text * These tests verify the new sequential generation logic */ describe('Visual Element Extraction Tests', () => { // Since we're testing the actual implementation, we need to create a real instance // instead of using the mocked version const axios = require('axios'); const { Buffer } = require('buffer'); const settingsService = require('../services/settingsService'); // Mock the dependencies but not the main class jest.mock('axios'); jest.mock('../services/settingsService'); // Create a test instance of AIService without the full module mock class TestAIService { extractVisualElementsFromText(text) { if (!text) return []; const visualElements = []; // Remove the title if present const cleanText = text.replace(/\*\*Title:.*?\*\*/, '').trim(); // Extract characters - improved patterns with better action words const characterPatterns = [ /(Dr\.|Professor|Captain|Agent|Detective)\s+([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/gi, /([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)\s+(stood|walked|ran|sat|looked|gazed|stared|stepped)/gi ]; for (const pattern of characterPatterns) { const matches = cleanText.match(pattern); if (matches) { matches.slice(0, 3).forEach(match => { // Clean up the match to get just the name let cleaned = match.replace(/\s+(stood|walked|ran|sat|looked|gazed|stared|stepped).*$/i, '').trim(); // For patterns with titles, keep the full title + name if (cleaned.match(/^(Dr\.|Professor|Captain|Agent|Detective)/)) { visualElements.push(cleaned); } else { // For action-based patterns, extract just the name before the action const nameMatch = match.match(/^([A-Z][a-z]+(?:\s+[A-Z][a-z]+)?)/); if (nameMatch) { visualElements.push(nameMatch[1]); } } }); } } // Extract locations and settings const locationPatterns = [ /(in|at|on|through)\s+(the\s+)?([A-Z][a-z\s]{3,30}(?:city|planet|station|facility|dome|colony|ship|chamber|laboratory|castle|forest|mountain|desert|ocean|space))/gi, /(starship|spaceship|vessel|craft|vehicle)\s+([A-Z][a-z]+)/gi, /(colony|city|station|outpost|facility)\s+([A-Z][a-z\s]+)/gi ]; for (const pattern of locationPatterns) { const matches = cleanText.match(pattern); if (matches) { matches.slice(0, 2).forEach(match => { const location = match.replace(/^(in|at|on|through)\s+(the\s+)?/i, '').trim(); if (location.length > 3 && location.length < 50) { visualElements.push(location); } }); } } // Extract objects and technology // Pattern 1: adjective + noun combinations let matches = cleanText.match(/(advanced|alien|ancient|glowing|metallic|crystalline)\s+(scanner|device|weapon|tool|helmet|suit|console|terminal|reactor|portal|gateway|chamber|throne|altar|artifact)/gi); if (matches) { matches.forEach(match => { visualElements.push(match.trim()); }); } // Pattern 2: standalone tech objects matches = cleanText.match(/\b(scanner|device|weapon|tool|helmet|suit|console|terminal|reactor|portal|gateway|chamber|throne|altar|artifact)\b/gi); if (matches) { matches.forEach(match => { visualElements.push(match.trim()); }); } // Extract atmospheric elements // Pattern 1: action + atmospheric combinations matches = cleanText.match(/(glittering|shimmering|glowing|pulsing|swirling|drifting)\s+(purple\s+mist|mist|fog|clouds|dust|air)/gi); if (matches) { matches.forEach(match => { visualElements.push(match.trim()); }); } // Pattern 2: color + atmospheric combinations matches = cleanText.match(/(red|blue|green|golden|silver|purple|crimson)\s+(light|glow|aurora|mist|dust|sky)/gi); if (matches) { matches.forEach(match => { visualElements.push(match.trim()); }); } // Pattern 3: standalone atmospheric elements matches = cleanText.match(/\b(chamber|storm|clouds|mist|fog|aurora|lightning)\b/gi); if (matches) { matches.forEach(match => { visualElements.push(match.trim()); }); } // Remove duplicates (case-insensitive) and limit to most important elements const uniqueElements = []; const seenElements = new Set(); for (const element of visualElements) { const lowerElement = element.toLowerCase(); // Additional check to avoid partial matches within longer names let isDuplicate = false; for (const seenElement of seenElements) { if (seenElement === lowerElement || (lowerElement.includes(seenElement) && seenElement.length > 5) || (seenElement.includes(lowerElement) && lowerElement.length > 5)) { isDuplicate = true; break; } } if (!isDuplicate) { seenElements.add(lowerElement); uniqueElements.push(element); } } return uniqueElements.slice(0, 5); // Limit to 5 key visual elements } formatImagePrompt(parameters, year = null, generatedText = null) { let prompt = "Create a detailed, visually striking image"; // If we have generated text, use it to create a more coherent image if (generatedText) { // Extract key visual elements from the generated text const visualElements = this.extractVisualElementsFromText(generatedText); if (visualElements.length > 0) { prompt += ` depicting the following scene: ${visualElements.join(', ')}`; } // Add context from the generated text prompt += "\n\nThis image should complement the following story:\n"; // Use the first 500 characters of the story for context const storyExcerpt = generatedText.substring(0, 500) + (generatedText.length > 500 ? '...' : ''); prompt += `"${storyExcerpt}"\n\n`; } else { // Fallback to parameter-based prompt when no text is provided prompt += " with the following elements:\n\n"; } // If year is provided, add it to the prompt if (year) { prompt += `Set in the year ${year}. `; } // Add parameters... Object.entries(parameters).forEach(([categoryName, categoryParams]) => { prompt += `${categoryName}: `; const paramValues = []; Object.entries(categoryParams).forEach(([paramName, paramValue]) => { if (Array.isArray(paramValue)) { paramValues.push(`${paramName}: ${paramValue.join(', ')}`); } else if (typeof paramValue === 'boolean') { if (paramValue) { paramValues.push(paramName); } } else { paramValues.push(`${paramName}: ${paramValue}`); } }); prompt += paramValues.join(', '); prompt += '.\n'; }); prompt += "\nUse high-quality, photorealistic rendering with attention to lighting, detail, and composition. The image should be visually cohesive and striking."; return prompt; } } test('Should extract character names correctly', () => { const testAI = new TestAIService(); const text = `**Title: Test Story** Dr. Elena Rodriguez stood at the edge of the platform, gazing out into space. Captain Marcus ran towards the console while Professor Chen looked at the readings.`; const elements = testAI.extractVisualElementsFromText(text); // Check that we got the expected character names expect(elements).toContain('Dr. Elena Rodriguez'); expect(elements).toContain('Captain Marcus'); expect(elements).toContain('Professor Chen'); }); test('Should extract locations and settings', () => { const testAI = new TestAIService(); const text = `The team arrived at the lunar station after traveling through the asteroid field. The starship Enterprise docked at the space facility.`; const elements = testAI.extractVisualElementsFromText(text); expect(elements).toContain('lunar station'); expect(elements).toContain('starship Enterprise'); expect(elements).toContain('space facility'); }); test('Should extract objects and technology', () => { const testAI = new TestAIService(); const text = `She picked up the advanced scanner and pointed it at the glowing artifact. The metallic console displayed alien symbols while the ancient portal shimmered.`; const elements = testAI.extractVisualElementsFromText(text); expect(elements).toContain('advanced scanner'); expect(elements).toContain('glowing artifact'); expect(elements).toContain('metallic console'); expect(elements).toContain('ancient portal'); }); test('Should extract atmospheric elements', () => { const testAI = new TestAIService(); const text = `The chamber was filled with swirling purple mist. A crimson aurora danced across the sky while blue light emanated from the crystals.`; const elements = testAI.extractVisualElementsFromText(text); expect(elements).toContain('swirling purple mist'); expect(elements).toContain('crimson aurora'); expect(elements).toContain('blue light'); }); test('Should remove title formatting', () => { const testAI = new TestAIService(); const text = `**Title: The Crystal Chambers** Dr. Elena stepped through the portal.`; const elements = testAI.extractVisualElementsFromText(text); // Should not contain the title text expect(elements.join(' ')).not.toContain('Title: The Crystal Chambers'); // Should still extract the character name const hasElena = elements.some(element => element.includes('Elena')); expect(hasElena).toBe(true); }); test('Should limit to 5 visual elements maximum', () => { const testAI = new TestAIService(); const text = `Dr. Elena Rodriguez stood at the advanced console. Captain Marcus ran through the metallic corridor towards the glowing portal. Professor Chen gazed at the crystalline chamber filled with swirling mist. The ancient artifact pulsed with blue light while the starship Nebula waited at the lunar station.`; const elements = testAI.extractVisualElementsFromText(text); expect(elements.length).toBeLessThanOrEqual(5); expect(elements.length).toBeGreaterThan(0); }); test('Should create coherent image prompt from generated text', () => { const testAI = new TestAIService(); const parameters = { 'science-fiction': { 'technology-level': 'Advanced', 'space-exploration': true } }; const generatedText = `**Title: The Nebula Station** Dr. Elena Rodriguez stepped through the ancient portal, her metallic suit gleaming in the ethereal blue light. The crystalline chamber stretched before her, filled with swirling purple mist and glowing artifacts.`; const prompt = testAI.formatImagePrompt(parameters, 2150, generatedText); // Should include visual elements from the text expect(prompt).toContain('Dr. Elena Rodriguez'); expect(prompt).toContain('ancient portal'); expect(prompt).toContain('metallic suit'); expect(prompt).toContain('crystalline chamber'); expect(prompt).toContain('swirling purple mist'); // Should include story context expect(prompt).toContain('This image should complement the following story'); expect(prompt).toContain('Dr. Elena Rodriguez stepped through'); // Should include year expect(prompt).toContain('Set in the year 2150'); // Should include parameters expect(prompt).toContain('science-fiction'); expect(prompt).toContain('Advanced'); }); test('Should fallback to parameter-based prompt when no text provided', () => { const testAI = new TestAIService(); const parameters = { 'fantasy': { 'magic-system': 'Elemental', 'creatures': ['Dragons', 'Elves'] } }; const prompt = testAI.formatImagePrompt(parameters, 1250); // Should not include story-specific elements expect(prompt).not.toContain('depicting the following scene'); expect(prompt).not.toContain('complement the following story'); // Should include basic prompt structure expect(prompt).toContain('Create a detailed, visually striking image'); expect(prompt).toContain('with the following elements'); // Should include parameters expect(prompt).toContain('fantasy'); expect(prompt).toContain('Elemental'); expect(prompt).toContain('Dragons, Elves'); }); test('Should handle empty or invalid text gracefully', () => { const testAI = new TestAIService(); const emptyElements = testAI.extractVisualElementsFromText(''); expect(emptyElements).toEqual([]); const nullElements = testAI.extractVisualElementsFromText(null); expect(nullElements).toEqual([]); const undefinedElements = testAI.extractVisualElementsFromText(undefined); expect(undefinedElements).toEqual([]); const shortTextElements = testAI.extractVisualElementsFromText('Hi.'); expect(shortTextElements).toEqual([]); }); test('Should remove duplicate visual elements', () => { const testAI = new TestAIService(); const text = `Dr. Elena stood at the console. Dr. Elena looked at the scanner. The advanced console beeped while Dr. Elena stepped back.`; const elements = testAI.extractVisualElementsFromText(text); // Should contain Dr. Elena only once const elenaCount = elements.filter(element => element.includes('Dr. Elena')).length; expect(elenaCount).toBe(1); }); });