Spaces:
Build error
Build error
File size: 6,255 Bytes
e49c006 a427438 e49c006 a427438 e49c006 a427438 e49c006 a427438 e49c006 a427438 e49c006 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | """
Dream Analysis Service — real Gemini-powered analysis of a user-submitted
dream description: detected emotions, recurring symbols, likely themes,
and a genuine interpretive summary.
Mirrors the established Gemini client pattern from
app/routes/commend/gemini_service.py (same lazy client, same env var
naming convention, same error classification) rather than inventing a
second way to talk to the same API.
"""
from __future__ import annotations
import json
import os
import re
from typing import Any, Dict, List, Optional, Tuple
from google import genai
from .logging_config import get_logger
logger = get_logger(__name__)
_client = None
_MAX_DREAM_CHARS = 4000
_MIN_DREAM_CHARS = 10
# Tried in order; a model Google has retired falls through to the next one.
_MODELS = [m for m in (os.getenv("DREAMS_GEMINI_MODEL"), "gemini-2.5-flash-lite", "gemini-2.5-flash") if m]
_MODEL_GONE = ("not found", "no longer available", "deprecated", "is not supported", "404")
def _get_client():
global _client
if _client is not None:
return _client
api_key = os.getenv("DREAMS_GEMINI_API_KEY") or os.getenv("COMMEND_GEMINI_API_KEY")
if not api_key:
raise ValueError("Dream analysis is not configured: set DREAMS_GEMINI_API_KEY or COMMEND_GEMINI_API_KEY")
try:
_client = genai.Client(api_key=api_key)
logger.info("Gemini initialized for dream analysis")
return _client
except Exception as e:
logger.error(f"Gemini init failed: {e}")
raise ValueError(f"Gemini failed: {e}")
_ALLOWED_EMOTIONS = {
"joy", "fear", "anxiety", "sadness", "anger", "confusion",
"peace", "excitement", "nostalgia", "wonder", "shame", "love",
}
def _extract_json(text: str) -> Optional[Dict[str, Any]]:
"""Gemini sometimes wraps JSON in a ```json fence despite instructions
not to — strip that before parsing rather than fail the whole request."""
cleaned = text.strip()
fence_match = re.match(r"^```(?:json)?\s*(.*?)\s*```$", cleaned, re.DOTALL)
if fence_match:
cleaned = fence_match.group(1)
try:
return json.loads(cleaned)
except json.JSONDecodeError:
return None
def analyze_dream(
dream_text: str,
language: str = "English",
) -> Tuple[Optional[Dict[str, Any]], Optional[str]]:
"""
Analyze a dream description and return structured findings:
{
"emotions": ["fear", "wonder"],
"themes": ["falling", "being chased"],
"symbols": ["water", "flying"],
"interpretation": "...",
"lucidityIndicator": bool
}
Returns (result, None) on success or (None, error_message) on failure.
"""
text = dream_text.strip()
if len(text) < _MIN_DREAM_CHARS:
return None, "Dream description is too short to analyze."
if len(text) > _MAX_DREAM_CHARS:
text = text[:_MAX_DREAM_CHARS]
try:
client = _get_client()
prompt = f"""You are a dream analyst. Analyze this dream description and respond with ONLY a raw JSON object (no markdown fence, no commentary) with this exact shape:
{{"emotions": string[] (2-4 items from: joy, fear, anxiety, sadness, anger, confusion, peace, excitement, nostalgia, wonder, shame, love), "themes": string[] (1-3 short phrases describing narrative themes, e.g. "being chased", "falling", "losing teeth"), "symbols": string[] (1-4 concrete recurring symbols/objects mentioned or implied, e.g. "water", "flying", "a locked door"), "interpretation": string (2-3 sentences, a grounded psychological reading, not mystical fortune-telling, in {language}), "lucidityIndicator": boolean (true only if the dreamer describes realizing they were dreaming)}}
Dream description:
{text}
JSON:"""
response = None
last_error: Optional[Exception] = None
for model in _MODELS:
try:
response = client.models.generate_content(model=model, contents=prompt)
break
except Exception as e: # noqa: BLE001 - classified below
last_error = e
if not any(marker in str(e).lower() for marker in _MODEL_GONE):
raise
logger.warning(f"Gemini model {model} unavailable, trying the next one: {e}")
if response is None:
raise last_error or RuntimeError("no Gemini model answered")
raw = response.text.strip() if response.text else ""
parsed = _extract_json(raw)
if parsed is None:
logger.error(f"Dream analysis returned non-JSON: {raw[:200]}")
return None, "Analysis produced an unexpected response format."
emotions = [e for e in parsed.get("emotions", []) if e in _ALLOWED_EMOTIONS][:4]
themes = [str(t)[:60] for t in parsed.get("themes", [])][:3]
symbols = [str(s)[:40] for s in parsed.get("symbols", [])][:4]
interpretation = str(parsed.get("interpretation", ""))[:800]
lucidity_indicator = bool(parsed.get("lucidityIndicator", False))
if not emotions or not interpretation:
return None, "Analysis was incomplete. Please try again."
return {
"emotions": emotions,
"themes": themes,
"symbols": symbols,
"interpretation": interpretation,
"lucidityIndicator": lucidity_indicator,
}, None
except Exception as e:
logger.error(f"Dream analysis error: {e}")
err = str(e).lower()
# Match "api key", "api_key", and "apikey" — the local ValueError
# from _get_client() reads "..._api_key required" (underscore), and
# Gemini's own errors tend to read "API key not valid" (space), so
# a plain substring check on either form alone misses the other.
if "not configured" in err:
return None, "Dream analysis is not configured"
if "api key" in err or "api_key" in err or "apikey" in err:
logger.error("Gemini refused the configured API key; replace DREAMS_GEMINI_API_KEY / COMMEND_GEMINI_API_KEY")
return None, "Invalid API key"
if "quota" in err or "limit" in err or "resource" in err:
return None, "API quota exceeded"
return None, f"Analysis failed: {str(e)}"
|