Spaces:
Sleeping
Sleeping
Download app/services/dream_analyzer.py from Rthur2003/crowncode-backend: direct link, hf CLI and curl.
- Browser
- Download file 5.24 kB
-
https://huggingface.co/spaces/Rthur2003/crowncode-backend/resolve/main/app/services/dream_analyzer.py
- Command line
-
hf download hf://spaces/Rthur2003/crowncode-backend/app/services/dream_analyzer.py
-
curl -L -o dream_analyzer.py https://huggingface.co/spaces/Rthur2003/crowncode-backend/resolve/main/app/services/dream_analyzer.py
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)}" | |