Spaces:
Runtime error
Runtime error
| // Enhanced Natural Language Scene Parser | |
| // Converts user descriptions into comprehensive structured scene data | |
| export class SceneParser { | |
| constructor() { | |
| this.keywords = { | |
| environments: { | |
| forest: ['forest', 'woods', 'jungle', 'trees', 'woodland'], | |
| beach: ['beach', 'shore', 'ocean', 'seaside', 'coast'], | |
| desert: ['desert', 'sand', 'dunes', 'arid'], | |
| city: ['city', 'urban', 'street', 'building', 'metropolis'], | |
| lab: ['lab', 'laboratory', 'sci-fi', 'futuristic', 'science'], | |
| room: ['room', 'indoor', 'interior', 'chamber'], | |
| space: ['space', 'planet', 'alien', 'cosmic', 'astronomical'], | |
| underwater: ['underwater', 'ocean floor', 'sea', 'aquatic'], | |
| magical: ['magical', 'enchanted', 'mystical', 'fantasy'] | |
| }, | |
| lighting: { | |
| day: ['day', 'daylight', 'sunny', 'bright'], | |
| night: ['night', 'dark', 'moonlight', 'stars', 'nocturnal'], | |
| sunset: ['sunset', 'dusk', 'evening', 'golden hour', 'twilight'], | |
| sunrise: ['sunrise', 'dawn', 'morning', 'aurora'], | |
| indoor: ['indoor', 'artificial', 'fluorescent', 'lamp'], | |
| blue: ['blue light', 'blue ambient', 'cyan', 'azure'], | |
| warm: ['warm', 'orange', 'amber', 'firelight'] | |
| }, | |
| objects: { | |
| tree: ['tree', 'palm', 'oak', 'pine', 'cedar'], | |
| mushroom: ['mushroom', 'fungus', 'toadstool', 'glowing mushroom'], | |
| fire: ['fire', 'campfire', 'flame', 'bonfire', 'torch'], | |
| rock: ['rock', 'stone', 'boulder', 'pebble'], | |
| water: ['water', 'ocean', 'sea', 'lake', 'river', 'pond'], | |
| building: ['building', 'house', 'structure', 'tower'], | |
| vehicle: ['car', 'vehicle', 'truck', 'bike', 'automobile'], | |
| firefly: ['firefly', 'fireflies', 'lightning bug', 'glowing bug'], | |
| wizard: ['wizard', 'mage', 'sorcerer', 'magician'], | |
| character: ['character', 'person', 'human', 'avatar', 'npc'] | |
| }, | |
| materials: { | |
| metal: ['metal', 'metallic', 'steel', 'iron', 'bronze'], | |
| wood: ['wood', 'wooden', 'timber', 'oak', 'pine'], | |
| glass: ['glass', 'transparent', 'clear', 'crystal'], | |
| fabric: ['fabric', 'cloth', 'textile', 'fabric'], | |
| stone: ['stone', 'rock', 'concrete', 'marble', 'granite'], | |
| glowing: ['glowing', 'emissive', 'luminous', 'radiant', 'shining'], | |
| magical: ['magical', 'enchanted', 'mystical', 'ethereal'] | |
| }, | |
| effects: { | |
| fog: ['fog', 'mist', 'haze', 'vapor', 'cloud'], | |
| rain: ['rain', 'rainy', 'storm', 'precipitation'], | |
| snow: ['snow', 'snowy', 'blizzard', 'flurry'], | |
| bloom: ['bloom', 'glow', 'bright', 'halo'], | |
| particles: ['particles', 'sparkles', 'dust', 'debris'] | |
| }, | |
| audio: { | |
| ambient: ['ambient', 'background', 'music', 'soundtrack', 'atmosphere'], | |
| spatial: ['spatial', 'positional', '3d sound', 'surround'], | |
| nature: ['nature', 'birds', 'wind', 'water', 'forest sounds'], | |
| magical: ['magical', 'mystical', 'enchanted', 'fantasy music'] | |
| }, | |
| animations: { | |
| walk: ['walk', 'walking', 'stroll', 'pace'], | |
| run: ['run', 'running', 'sprint'], | |
| sit: ['sit', 'sitting', 'rest'], | |
| stand: ['stand', 'standing', 'idle'], | |
| fly: ['fly', 'flying', 'hover', 'float'], | |
| glow: ['glow', 'pulse', 'flicker', 'animate'] | |
| } | |
| }; | |
| } | |
| parse(description) { | |
| const lowerDesc = description.toLowerCase(); | |
| const scene = { | |
| sceneName: this.extractSceneName(description), | |
| environment: this.detectEnvironment(lowerDesc), | |
| lighting: this.detectLighting(lowerDesc), | |
| objects: this.extractObjects(lowerDesc, description), | |
| materials: this.detectMaterials(lowerDesc), | |
| effects: this.detectEffects(lowerDesc), | |
| avatar: this.extractAvatar(lowerDesc, description), | |
| audio: this.extractAudio(lowerDesc, description), | |
| camera: this.detectCamera(lowerDesc) | |
| }; | |
| // Fill defaults | |
| this.fillDefaults(scene); | |
| return scene; | |
| } | |
| extractSceneName(description) { | |
| // Try to extract a name from quotes or first few words | |
| const match = description.match(/"([^"]+)"/); | |
| if (match) return match[1]; | |
| const words = description.split(/\s+/).slice(0, 3); | |
| return words.join('_').replace(/[^a-z0-9_]/gi, '') || 'generated_scene'; | |
| } | |
| detectEnvironment(desc) { | |
| for (const [env, keywords] of Object.entries(this.keywords.environments)) { | |
| if (keywords.some(kw => desc.includes(kw))) { | |
| return env; | |
| } | |
| } | |
| return 'room'; // default | |
| } | |
| detectLighting(desc) { | |
| const lighting = { | |
| type: 'day', | |
| hdri: null, | |
| sun_energy: 3.0, | |
| sun_angle: 0.5, | |
| ambient_strength: 0.3, | |
| color_temperature: 5500, | |
| color: [1, 1, 1], | |
| lights: [] | |
| }; | |
| if (this.keywords.lighting.night.some(kw => desc.includes(kw))) { | |
| lighting.type = 'night'; | |
| lighting.sun_energy = 0.5; | |
| lighting.ambient_strength = 0.1; | |
| lighting.color_temperature = 2000; | |
| lighting.hdri = 'night_sky'; | |
| lighting.color = [0.2, 0.2, 0.4]; | |
| } else if (this.keywords.lighting.sunset.some(kw => desc.includes(kw))) { | |
| lighting.type = 'sunset'; | |
| lighting.sun_energy = 2.0; | |
| lighting.color_temperature = 3000; | |
| lighting.hdri = 'sunset_sky'; | |
| lighting.color = [1, 0.6, 0.3]; | |
| } else if (this.keywords.lighting.sunrise.some(kw => desc.includes(kw))) { | |
| lighting.type = 'sunrise'; | |
| lighting.sun_energy = 2.5; | |
| lighting.color_temperature = 4000; | |
| lighting.hdri = 'sunrise_sky'; | |
| lighting.color = [1, 0.8, 0.5]; | |
| } | |
| // Detect blue ambient light | |
| if (this.keywords.lighting.blue.some(kw => desc.includes(kw))) { | |
| lighting.color = [0.3, 0.5, 1.0]; | |
| lighting.ambient_strength = 0.4; | |
| lighting.lights.push({ | |
| type: 'AMBIENT', | |
| color: [0.2, 0.3, 0.8], | |
| energy: 0.5 | |
| }); | |
| } | |
| return lighting; | |
| } | |
| extractObjects(desc, fullDesc) { | |
| const objects = []; | |
| let objIndex = 0; | |
| // Count objects - improved matching | |
| const counts = {}; | |
| for (const [type, keywords] of Object.entries(this.keywords.objects)) { | |
| const matches = keywords.filter(kw => desc.includes(kw)); | |
| if (matches.length > 0) { | |
| // Try to find number before the keyword | |
| const countMatch = fullDesc.match(new RegExp(`(\\d+)\\s+${matches[0]}`, 'i')); | |
| const count = countMatch ? parseInt(countMatch[1]) : | |
| (type === 'firefly' ? 10 : type === 'mushroom' ? 5 : 1); | |
| counts[type] = count; | |
| } | |
| } | |
| // Create objects | |
| for (const [type, count] of Object.entries(counts)) { | |
| for (let i = 0; i < count; i++) { | |
| objects.push(this.createObject(type, objIndex++, objects.length)); | |
| } | |
| } | |
| // Extract custom objects from description | |
| const customMatches = fullDesc.match(/(\d+)\s+(\w+)/gi); | |
| if (customMatches) { | |
| customMatches.forEach(match => { | |
| const parts = match.match(/(\d+)\s+(\w+)/i); | |
| if (parts && !this.keywords.objects[parts[2].toLowerCase()]) { | |
| const count = parseInt(parts[1]); | |
| for (let i = 0; i < count; i++) { | |
| objects.push(this.createObject('cube', objIndex++, objects.length, parts[2])); | |
| } | |
| } | |
| }); | |
| } | |
| return objects; | |
| } | |
| createObject(type, index, position, name = null) { | |
| const base = { | |
| type: 'cube', | |
| name: name || `${type}_${index}`, | |
| location: [ | |
| (position % 5 - 2) * 2 + (Math.random() - 0.5) * 0.5, | |
| Math.floor(position / 5) * 2 + (Math.random() - 0.5) * 0.5, | |
| 0.5 | |
| ], | |
| rotation: [0, 0, 0], | |
| scale: [1, 1, 1], | |
| color: [0.8, 0.8, 0.8, 1], | |
| metallic: 0.0, | |
| roughness: 0.5 | |
| }; | |
| // Type-specific defaults | |
| switch (type) { | |
| case 'tree': | |
| base.type = 'cylinder'; | |
| base.scale = [0.3, 0.3, 3]; | |
| base.location[2] = 1.5; | |
| base.color = [0.2, 0.6, 0.2, 1]; | |
| base.roughness = 0.8; | |
| break; | |
| case 'mushroom': | |
| base.type = 'sphere'; | |
| base.scale = [0.3, 0.3, 0.4]; | |
| base.location[2] = 0.2; | |
| base.color = [0.8, 0.2, 0.8, 1]; | |
| base.emission = [0.5, 0.1, 0.5, 1]; | |
| base.emissionStrength = 2.0; | |
| base.roughness = 0.3; | |
| break; | |
| case 'fire': | |
| base.type = 'cone'; | |
| base.scale = [0.5, 0.5, 1]; | |
| base.location[2] = 0.5; | |
| base.color = [1, 0.3, 0, 1]; | |
| base.emission = [1, 0.5, 0, 1]; | |
| base.emissionStrength = 3.0; | |
| base.roughness = 0.2; | |
| break; | |
| case 'firefly': | |
| base.type = 'sphere'; | |
| base.scale = [0.05, 0.05, 0.05]; | |
| base.location[2] = 1.5 + Math.random() * 0.5; | |
| base.color = [1, 1, 0.5, 1]; | |
| base.emission = [1, 1, 0.3, 1]; | |
| base.emissionStrength = 5.0; | |
| base.roughness = 0.1; | |
| base.animation = { type: 'float', speed: 1.0 }; | |
| break; | |
| case 'rock': | |
| base.type = 'sphere'; | |
| base.scale = [0.8, 0.8, 0.6]; | |
| base.color = [0.4, 0.4, 0.4, 1]; | |
| base.roughness = 0.9; | |
| break; | |
| case 'water': | |
| base.type = 'plane'; | |
| base.scale = [10, 10, 1]; | |
| base.location[2] = 0; | |
| base.color = [0.2, 0.5, 0.8, 0.7]; | |
| base.metallic = 0.1; | |
| base.roughness = 0.1; | |
| break; | |
| } | |
| return base; | |
| } | |
| detectMaterials(desc) { | |
| const materials = {}; | |
| for (const [mat, keywords] of Object.entries(this.keywords.materials)) { | |
| if (keywords.some(kw => desc.includes(kw))) { | |
| materials[mat] = true; | |
| } | |
| } | |
| return materials; | |
| } | |
| detectEffects(desc) { | |
| const effects = {}; | |
| for (const [effect, keywords] of Object.entries(this.keywords.effects)) { | |
| if (keywords.some(kw => desc.includes(kw))) { | |
| effects[effect] = true; | |
| } | |
| } | |
| return effects; | |
| } | |
| extractAvatar(desc, fullDesc) { | |
| const avatar = { | |
| present: desc.includes('avatar') || desc.includes('character') || desc.includes('person') || | |
| desc.includes('wizard') || desc.includes('mage') || desc.includes('sorcerer'), | |
| action: 'stand', | |
| position: [0, 0, 0], | |
| name: 'Avatar', | |
| scale: 1.0 | |
| }; | |
| if (avatar.present) { | |
| // Detect wizard/mage | |
| if (desc.includes('wizard') || desc.includes('mage') || desc.includes('sorcerer')) { | |
| avatar.name = 'Wizard'; | |
| avatar.scale = 1.0; | |
| } | |
| // Detect actions | |
| for (const [action, keywords] of Object.entries(this.keywords.animations)) { | |
| if (keywords.some(kw => desc.includes(kw))) { | |
| avatar.action = action; | |
| break; | |
| } | |
| } | |
| // Position based on action | |
| if (avatar.action === 'sit') { | |
| avatar.position[2] = 0.5; | |
| } else if (avatar.action === 'walk') { | |
| avatar.position = [0, 0, 0]; | |
| avatar.animation = { type: 'walk', speed: 1.0 }; | |
| } | |
| } | |
| return avatar; | |
| } | |
| extractAudio(desc, fullDesc) { | |
| const audio = { | |
| ambient: [], | |
| spatial: [] | |
| }; | |
| // Detect ambient sounds | |
| if (desc.includes('wave') || desc.includes('ocean')) { | |
| audio.ambient.push({ type: 'ocean_waves', volume: 0.5, loop: true }); | |
| } | |
| if (desc.includes('wind') || desc.includes('breeze')) { | |
| audio.ambient.push({ type: 'wind', volume: 0.3, loop: true }); | |
| } | |
| if (desc.includes('music') || desc.includes('soundtrack') || desc.includes('background')) { | |
| const isMagical = desc.includes('magical') || desc.includes('mystical') || desc.includes('enchanted'); | |
| audio.ambient.push({ | |
| type: isMagical ? 'magical_ambient' : 'ambient_music', | |
| volume: 0.4, | |
| loop: true | |
| }); | |
| } | |
| if (this.keywords.audio.magical.some(kw => desc.includes(kw))) { | |
| audio.ambient.push({ type: 'magical_ambient', volume: 0.5, loop: true }); | |
| } | |
| // Detect spatial sounds | |
| if (desc.includes('fire') || desc.includes('campfire')) { | |
| audio.spatial.push({ type: 'fire_crackle', position: [0, 0, 0], volume: 0.4, loop: true }); | |
| } | |
| return audio; | |
| } | |
| detectCamera(desc) { | |
| const camera = { | |
| position: [4, -4, 2.2], | |
| rotation: [1.05, 0, 0.78], | |
| fov: 50 | |
| }; | |
| if (desc.includes('close') || desc.includes('close-up')) { | |
| camera.position = [2, -2, 1.5]; | |
| } else if (desc.includes('far') || desc.includes('wide')) { | |
| camera.position = [8, -8, 4]; | |
| camera.fov = 70; | |
| } | |
| return camera; | |
| } | |
| fillDefaults(scene) { | |
| // Ensure lighting exists | |
| if (!scene.lighting.lights) { | |
| scene.lighting.lights = []; | |
| } | |
| // Add environment-specific objects | |
| if (scene.environment === 'forest' && scene.objects.filter(o => o.name.includes('tree')).length === 0) { | |
| for (let i = 0; i < 5; i++) { | |
| scene.objects.push(this.createObject('tree', i, scene.objects.length)); | |
| } | |
| } | |
| if (scene.environment === 'beach' && scene.objects.filter(o => o.name.includes('water')).length === 0) { | |
| scene.objects.push(this.createObject('water', 0, scene.objects.length)); | |
| } | |
| // Add magical forest elements | |
| if (scene.environment === 'magical' || scene.environment === 'forest') { | |
| const hasMushrooms = scene.objects.some(o => o.name.includes('mushroom')); | |
| const hasFireflies = scene.objects.some(o => o.name.includes('firefly')); | |
| if (!hasMushrooms && scene.effects && !scene.effects.fog) { | |
| // Add some mushrooms if not present | |
| for (let i = 0; i < 3; i++) { | |
| scene.objects.push(this.createObject('mushroom', i, scene.objects.length)); | |
| } | |
| } | |
| } | |
| // Add ground plane | |
| scene.ground = { | |
| type: 'plane', | |
| size: 20, | |
| texture: scene.environment === 'beach' ? 'sand' : | |
| scene.environment === 'forest' || scene.environment === 'magical' ? 'grass' : 'concrete', | |
| color: scene.environment === 'beach' ? [0.9, 0.8, 0.6, 1] : | |
| scene.environment === 'forest' || scene.environment === 'magical' ? [0.2, 0.4, 0.2, 1] : | |
| [0.5, 0.5, 0.5, 1], | |
| roughness: 0.8, | |
| metallic: 0.0 | |
| }; | |
| // Add default sun if not specified | |
| if (scene.lighting.type === 'day' && scene.lighting.lights.filter(l => l.type === 'SUN').length === 0) { | |
| scene.lighting.lights.push({ | |
| type: 'SUN', | |
| location: [10, -10, 10], | |
| energy: scene.lighting.sun_energy, | |
| angle: scene.lighting.sun_angle, | |
| color: scene.lighting.color | |
| }); | |
| } | |
| // Add night lighting if needed | |
| if (scene.lighting.type === 'night' && scene.lighting.lights.length === 0) { | |
| scene.lighting.lights.push({ | |
| type: 'POINT', | |
| location: [0, 0, 3], | |
| energy: 50, | |
| color: [0.3, 0.3, 0.5] | |
| }); | |
| } | |
| } | |
| } | |