#!/usr/bin/env python3 """ Dream Weaver — Local LLM Prompt Expander ======================================== Converts user keywords + room type into a photorealistic interior design prompt using a local Ollama model (default: qwen3.5:27b). Cloud API calls (Gemini, OpenAI) have been completely removed for data privacy and local inference requirements. Usage: from prompt_expander import expand_prompt result = expand_prompt( keywords=["blue marble", "gold veins", "renaissance", "sharp contours"], room_type="bedroom" ) """ import os import json import logging import requests import re logger = logging.getLogger(__name__) # ── Room-type context injected into every LLM call ─────────────────────────── ROOM_CONTEXTS = { "bedroom": { "description": "a private sleeping space", "key_elements": ["bed", "bedside tables", "wardrobe", "soft lighting", "textiles", "headboard"], "must_haves": "bed linen, pillows, bedside lighting", "avoid": "office furniture, dining elements, cooking equipment" }, "living_room": { "description": "a social gathering and relaxation space", "key_elements": ["sofa", "coffee table", "TV unit", "accent chairs", "rugs"], "must_haves": "seating arrangement, focal point", "avoid": "beds, cooking equipment, clinical elements" }, "bathroom": { "description": "a private hygiene and wellness space", "key_elements": ["vanity", "bathtub", "shower", "tiles", "mirrors"], "must_haves": "wet-area materials, luxury fixtures", "avoid": "soft furnishings, carpet, beds" }, "kitchen": { "description": "a functional cooking space", "key_elements": ["cabinetry", "countertops", "backsplash", "appliances", "island"], "must_haves": "work surfaces, storage", "avoid": "beds, lounge furniture" }, "dining_room": { "description": "an eating and entertaining space", "key_elements": ["dining table", "chairs", "sideboard", "pendant lighting"], "must_haves": "central dining table, seating", "avoid": "beds, cooking appliances" }, "home_office": { "description": "a workspace within a home", "key_elements": ["desk", "ergonomic chair", "shelving", "task lighting"], "must_haves": "functional desk setup", "avoid": "beds in foreground, dining furniture" }, "hallway": { "description": "an entrance or transitional corridor", "key_elements": ["console table", "mirror", "coat storage", "lighting"], "must_haves": "welcoming entrance elements", "avoid": "beds, large seating" }, "balcony": { "description": "an outdoor living extension", "key_elements": ["outdoor furniture", "planters", "lighting", "railings"], "must_haves": "weather-resistant materials", "avoid": "indoor bedding, non-weather-resistant elements" }, } FEW_SHOT_EXAMPLES = """ EXAMPLE 1: Keywords: ["light oak", "white walls", "hygge", "natural light", "minimalist"] Room type: bedroom Positive prompt: scandinavian minimalist interior design, light oak wood flooring, neutral beige textiles, abundant natural light streaming through large windows, clean white walls, simple functional furniture, cozy hygge atmosphere, soft cream and warm gray tones, organic cotton fabrics, potted green plants, minimalist pendant lighting, decluttered space, architectural photography, 8k resolution, photorealistic, global illumination, soft shadows, natural materials, sustainable design Negative prompt: (worst quality, low quality, illustration, 3d render, 2d, painting, cartoon, sketch), blurry, distorted, deformed, extra windows, unrealistic lighting, structural changes, heavy ornamentation, dark colors, cluttered space EXAMPLE 2: Keywords: ["gold brass", "marble", "velvet", "emerald green", "1920s", "geometric"] Room type: living_room Positive prompt: art deco luxury interior design, geometric chevron patterns, gold brass accents, rich velvet upholstery in emerald green and sapphire blue, sunburst mirrors, polished marble flooring with brass inlay, crystal chandeliers, lacquered wood furniture, bold symmetrical arrangements, 1920s glamour, warm ambient lighting, architectural photography, 8k resolution, photorealistic, global illumination, elegant reflections, geometric motifs, stepped forms Negative prompt: (worst quality, low quality, illustration, 3d render, 2d, painting, cartoon, sketch), blurry, distorted, deformed, extra windows, unrealistic lighting, structural changes, rustic elements, farmhouse style, minimalism, cheap materials """ SYSTEM_PROMPT = """You are Dream Weaver's interior design prompt engineer. Convert user-provided keywords and a room type into a high-quality prompt for image generation. TASK: Generate JSON containing: 1. "positive_prompt" (rich, photorealistic, 80-120 words) 2. "negative_prompt" (preventing artifacts, 30-50 words) 3. "cfg" (float 6.0-9.0) 4. "denoise" (float 0.5-0.85) 5. "steps" (int 25-40) RULES FOR POSITIVE PROMPT: - Focus on the core aesthetic derived from keywords - Include architecture, furniture, and lighting suitable for the room type - End with: "architectural photography, 8k resolution, photorealistic" RULES FOR NEGATIVE PROMPT: - Start with: (worst quality, low quality, illustration, 3d render, 2d, painting, cartoon, sketch), blurry, distorted, deformed, extra windows, unrealistic lighting, structural changes OUTPUT FORMAT: Provide valid JSON only, with keys: "style_name", "positive_prompt", "negative_prompt", "cfg", "denoise", "steps", "reasoning". FEW-SHOT EXAMPLES: """ + FEW_SHOT_EXAMPLES class ExpandedPrompt: def __init__(self, style_name, positive_prompt, negative_prompt, cfg, denoise, steps, reasoning, source): self.style_name = style_name self.positive_prompt = positive_prompt self.negative_prompt = negative_prompt self.cfg = cfg self.denoise = denoise self.steps = steps self.reasoning = reasoning self.source = source def _call_ollama(user_message: str) -> str: ollama_url = os.environ.get("OLLAMA_URL", "http://localhost:11434") # Using Qwen 3.5 27B as requested model = os.environ.get("OLLAMA_MODEL", "qwen3.5:27b") full_prompt = f"{SYSTEM_PROMPT}\n\nUSER REQUEST:\n{user_message}\n\nReturn JSON ONLY. No markdown wrapping." r = requests.post( f"{ollama_url}/api/generate", json={ "model": model, "prompt": full_prompt, "stream": False, "format": "json", "options": {"temperature": 0.5} }, timeout=180 # Large models take time ) r.raise_for_status() return r.json()["response"] def expand_prompt(keywords: list[str], room_type: str = "living_room", additional_notes: str = "") -> ExpandedPrompt: if not keywords: raise ValueError("Keywords required") room_type = room_type.lower().replace(" ", "_") if room_type not in ROOM_CONTEXTS: room_type = "living_room" ctx = ROOM_CONTEXTS[room_type] user_message = f"""KEYWORDS: {', '.join(keywords)} ROOM TYPE: {room_type} ({ctx['description']}) MUST HAVE: {ctx['must_haves']} AVOID: {ctx['avoid']} {f'NOTES: {additional_notes}' if additional_notes else ''}""" try: logger.info("Calling local Ollama LLM...") raw = _call_ollama(user_message).strip() json_match = re.search(r'\{[\s\S]*\}', raw) if json_match: raw_json = json_match.group(0) else: raw_json = raw data = json.loads(raw_json) return ExpandedPrompt( style_name=data.get("style_name", "custom_local"), positive_prompt=data["positive_prompt"], negative_prompt=data["negative_prompt"], cfg=float(data.get("cfg", 7.5)), denoise=float(data.get("denoise", 0.72)), steps=int(data.get("steps", 30)), reasoning=data.get("reasoning", ""), source="ollama_local" ) except Exception as e: logger.warning(f"Ollama failed, using sync fallback: {e}") return expand_prompt_simple(keywords, room_type) def expand_prompt_simple(keywords: list[str], room_type: str = "living_room") -> ExpandedPrompt: ctx = ROOM_CONTEXTS.get(room_type.replace(" ", "_"), ROOM_CONTEXTS["living_room"]) kw_str = ", ".join(keywords) positive = f"{kw_str} interior design, {', '.join(ctx['key_elements'][:4])}, photorealistic {room_type.replace('_', ' ')} interior, architectural photography, 8k resolution, photorealistic" negative = "(worst quality, low quality, illustration, 3d render, 2d, painting, cartoon, sketch), blurry, distorted, extra windows, unrealistic lighting, structural changes" return ExpandedPrompt( style_name="fallback", positive_prompt=positive, negative_prompt=negative, cfg=7.5, denoise=0.72, steps=30, reasoning="No LLM", source="fallback" ) if __name__ == "__main__": import sys logging.basicConfig(level=logging.INFO) ans = expand_prompt(["blue marble", "gold"], "bathroom") print(ans.positive_prompt)