crowncode-backend / app /services /dream_analyzer.py
Rthur2003's picture
feat: remix engine routes dream analysis servisleri ve dataset organization utilities eklendi
e49c006
Raw History Blame Contribute Delete
5.24 kB
"""
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
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("DREAMS_GEMINI_API_KEY (or COMMEND_GEMINI_API_KEY) required")
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 = client.models.generate_content(
model="gemini-2.5-flash-lite",
contents=prompt,
)
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 "api key" in err or "api_key" in err or "apikey" in err:
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)}"