diff --git a/.env.example b/.env.example deleted file mode 100644 index 6964366142b75192c5c126b34554df5347036032..0000000000000000000000000000000000000000 --- a/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -GOOGLE_API_KEY=your-gemini-api-key-here - -# OCR Mode: development = Stitch & Strip engine (new, single-pass) -# production = Triple-Pass legacy (safe, proven) -OCR_STRIP_MODE=development diff --git a/.gcloudignore b/.gcloudignore deleted file mode 100644 index c38c377617a6bf0e972f4fb618b7c7f7bc61b336..0000000000000000000000000000000000000000 --- a/.gcloudignore +++ /dev/null @@ -1,16 +0,0 @@ -.gcloudignore -.git -.gitignore -venv/ -.venv/ -.venv_fix/ -__pycache__/ -*.pyc -*.pyo -*.pyd -.pytest_cache/ -tests/ -test_results/ -logs/ -server.log -deploy_hf/ diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b3b91b31ea5e55a2322a4334fc1606d4b030ea5a..0000000000000000000000000000000000000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -__pycache__/ -*.pyc -*.pyo -serviceAccountKey*.json -*credentials.json -.env -output_locus.txt -*.log -.DS_Store diff --git a/Dockerfile b/Dockerfile index 86392be9c8898bb3823a21789e6bae3757e2628d..16d8c5bf250720a03f2b0f072cbd1b7696eaba07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,31 +1,22 @@ -# Dockerfile - V8.0 FIXED (Uvicorn + SymPy Support) -FROM python:3.11-slim +FROM python:3.10-slim -WORKDIR /app +RUN apt-get update && apt-get install -y wget build-essential && rm -rf /var/lib/apt/lists/* -# התקנת תלויות מערכת (פונטים לגרפים) -RUN apt-get update && apt-get install -y \ - libgl1 \ - libglib2.0-0 \ - fonts-dejavu-core \ - fontconfig \ - && rm -rf /var/lib/apt/lists/* \ - && fc-cache -f -v +RUN useradd -m -u 1000 user +USER user +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH -# התקנת ספריות פייתון -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt +WORKDIR $HOME/app -# העתקת קבצי האפליקציה -COPY . . +COPY --chown=user requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt -# יצירת משתמש לא-root (דרישת אבטחה) -RUN useradd -m -u 1000 user -USER user -ENV PATH="/home/user/.local/bin:$PATH" +# Download DictaLM 2.0 Q4_K_M GGUF +RUN wget -q -O model.gguf "https://huggingface.co/dicta-il/dictalm2.0-instruct-GGUF/resolve/main/dictalm2.0-instruct.Q4_K_M.gguf" -# חשיפת פורט 7860 -EXPOSE 7860 +COPY --chown=user . . -# --- התיקון הקריטי: הרצה עם uvicorn --- -CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"] \ No newline at end of file +# Run llama-cpp-python server +# Port 7860 is required for HF Spaces +CMD ["python", "-m", "llama_cpp.server", "--model", "model.gguf", "--host", "0.0.0.0", "--port", "7860", "--n_ctx", "4096", "--chat_format", "llama-2"] diff --git a/README.md b/README.md index 46002a38ab1cb6e17cbdb60250f26abdfe8655ca..0583ea13f0b0b45b400c7c24997d359effba4616 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,9 @@ --- -title: BuddyMath +title: BuddyMath Dicta API emoji: 🧮 colorFrom: blue -colorTo: green +colorTo: purple sdk: docker -pinned: false +app_port: 7860 --- - -# BuddyMath Server V3.1 - -מורה פרטי AI למתמטיקה לתלמידים ישראליים 🇮🇱 - -## API Endpoints - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/` | GET | Health check | -| `/health` | GET | Server status | -| `/solve_stream` | POST | פתרון בעיה (SSE) | - -## Modes - -- `solve` - פתור לי -- `teach_me` - למד אותי -- `check` - בדוק אותי - -## Parameters - -``` -image: File (required) -grade: string (required) -mode: string (default: "solve") -student_name: string (default: "תלמיד/ה") -student_gender: string (default: "male") -user_note: string (optional) -``` +API Server for DictaLM 2.0 diff --git a/all_users.json b/all_users.json deleted file mode 100644 index bf0860eb98eabb9d00d726bc0ce0f523414f4ee3..0000000000000000000000000000000000000000 --- a/all_users.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "test_user_123": { - "last_seen": "2026-02-15T12:49:04.304849", - "id": "test_user_123" - }, - "\u05d3\u05d5\u05ea\u05df": { - "last_seen": "2026-03-08T15:18:08.252094", - "id": "\u05d3\u05d5\u05ea\u05df" - }, - "stress_user_1": { - "last_seen": "2026-02-27T12:03:10.171108", - "id": "stress_user_1" - }, - "stress_user_2": { - "last_seen": "2026-02-27T12:03:10.221285", - "id": "stress_user_2" - }, - "stress_user_3": { - "last_seen": "2026-02-27T12:03:10.263698", - "id": "stress_user_3" - }, - "stress_user_4": { - "last_seen": "2026-02-27T12:03:10.332969", - "id": "stress_user_4" - }, - "stress_user_0": { - "last_seen": "2026-02-27T12:03:10.382946", - "id": "stress_user_0" - }, - "diagnostic": { - "last_seen": "2026-02-28T11:19:56.770735", - "id": "diagnostic" - } -} \ No newline at end of file diff --git a/analytics.py b/analytics.py deleted file mode 100644 index 566cfb0a2070856c429f62819ecac6fe6625e146..0000000000000000000000000000000000000000 --- a/analytics.py +++ /dev/null @@ -1,77 +0,0 @@ -import datetime -import logging -from firebase_admin import firestore -from firebase_manager import firebase_manager - -logger = logging.getLogger("HamoraServer") - -class AnalyticsManager: - """ - Manages AI Assessment Telemetry and Weekly Reports. - """ - def _get_db(self): - return firebase_manager.get_db() - - def update_weekly_analytics(self, uid: str, assessment: dict): - """ - Updates the weekly analytics document for a user using atomic operations. - Runs silently. - """ - if not uid or not assessment: - return - - db = self._get_db() - if not db: - logger.error("❌ [ANALYTICS] Firestore DB not available.") - return - - try: - # 1. Parse assessment data - primary_skill = assessment.get("primary_skill", "Unknown") - sub_skill = assessment.get("sub_skill", "") - mastery_score = assessment.get("mastery_score", 0) - parent_note = assessment.get("parent_note", "") - - # Ensure score is an integer - try: - mastery_score = int(mastery_score) - except (ValueError, TypeError): - mastery_score = 0 - - # 2. Compute week_id (e.g., week_11_2026) - today = datetime.date.today() - year, week_num, _ = today.isocalendar() - week_id = f"week_{week_num:02d}_{year}" - - # 3. Construct the document reference - # Path: users/{uid}/analytics/{week_id} - doc_ref = db.collection("users").document(uid).collection("analytics").document(week_id) - - # 4. Construct payload with FieldValue operations for atomic updates - # Create the nested skill payload - skill_payload = { - "sum_scores": firestore.Increment(mastery_score), - "count": firestore.Increment(1) - } - if sub_skill: - skill_payload["sub_skills"] = firestore.ArrayUnion([sub_skill]) - - # Combine into final payload - payload = { - "total_exercises": firestore.Increment(1), - f"skills_data.{primary_skill}": skill_payload, - "last_updated": firestore.SERVER_TIMESTAMP - } - - if parent_note: - payload["parent_notes"] = firestore.ArrayUnion([parent_note]) - - # 5. Set with merge=True - doc_ref.set(payload, merge=True) - logger.info(f"📊 [ANALYTICS] Successfully updated telemetry for {uid} in {week_id}.") - - except Exception as e: - logger.error(f"❌ [ANALYTICS] Error updating analytics for {uid}: {e}") - -# Global instance -analytics_manager = AnalyticsManager() diff --git a/audio_generator.py b/audio_generator.py deleted file mode 100644 index 7c3db3279b0155e3ae19f8130eba285864a1858e..0000000000000000000000000000000000000000 --- a/audio_generator.py +++ /dev/null @@ -1,267 +0,0 @@ -# audio_generator.py - V273.0 (Google Cloud TTS - High Quality Hebrew) -import asyncio -import base64 -import os -import tempfile -import logging - -# Configure Logging -logger = logging.getLogger(__name__) - -# ═══════════════════════════════════════════════════════════════ -# 🎙️ Google Cloud TTS Configuration -# ═══════════════════════════════════════════════════════════════ -# -# קולות עבריים זמינים: -# - he-IL-Wavenet-A (נקבה, איכות גבוהה) ⭐ מומלץ -# - he-IL-Wavenet-B (זכר, איכות גבוהה) -# - he-IL-Standard-A (נקבה, איכות רגילה) -# - he-IL-Standard-B (זכר, איכות רגילה) -# -# Free Tier: 1 מיליון תווים/חודש (WaveNet: 1M, Standard: 4M) -# ═══════════════════════════════════════════════════════════════ - -GOOGLE_VOICE_NAME = "he-IL-Wavenet-A" # Female, high quality -GOOGLE_LANGUAGE_CODE = "he-IL" -SPEAKING_RATE = 0.95 # מעט יותר איטי לבהירות -PITCH = 1.0 # גובה קול רגיל - -# Fallback to edge-tts if Google Cloud not configured -USE_EDGE_TTS_FALLBACK = True -EDGE_TTS_VOICE = "he-IL-HilaNeural" - -from firebase_manager import firebase_manager # V261.17 - - -def _is_google_cloud_configured() -> bool: - """בדיקה אם Google Cloud מוגדר""" - # Option 1: Environment variable - if os.environ.get("GOOGLE_APPLICATION_CREDENTIALS"): - return True - # Option 2: Check for credentials file in common locations - common_paths = [ - "/app/google-credentials.json", - "./google-credentials.json", - os.path.expanduser("~/.config/gcloud/application_default_credentials.json") - ] - for path in common_paths: - if os.path.exists(path): - os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = path - return True - return False - - -async def _generate_with_google_cloud(text: str, output_path: str) -> bool: - """ - יצירת אודיו עם Google Cloud TTS - מחזיר True אם הצליח, False אם נכשל - """ - try: - from google.cloud import texttospeech - - # Create client - client = texttospeech.TextToSpeechClient() - - # Build the voice request - voice = texttospeech.VoiceSelectionParams( - language_code=GOOGLE_LANGUAGE_CODE, - name=GOOGLE_VOICE_NAME, - ) - - # Select the audio format - audio_config = texttospeech.AudioConfig( - audio_encoding=texttospeech.AudioEncoding.MP3, - speaking_rate=SPEAKING_RATE, - pitch=PITCH, - ) - - # Build the synthesis input - synthesis_input = texttospeech.SynthesisInput(text=text) - - # Perform the text-to-speech request - logger.info(f"🎙️ Google Cloud TTS: Generating audio for {len(text)} chars...") - - # Run in thread pool to not block async - loop = asyncio.get_running_loop() - response = await loop.run_in_executor( - None, - lambda: client.synthesize_speech( - input=synthesis_input, - voice=voice, - audio_config=audio_config - ) - ) - - # Write the audio content to file - with open(output_path, "wb") as out: - out.write(response.audio_content) - - logger.info(f"✅ Google Cloud TTS: Audio saved to {output_path}") - return True - - except ImportError: - logger.warning("⚠️ google-cloud-texttospeech not installed. Run: pip install google-cloud-texttospeech") - return False - except Exception as e: - logger.error(f"❌ Google Cloud TTS failed: {e}") - return False - - -async def _generate_with_edge_tts(text: str, output_path: str) -> bool: - """ - יצירת אודיו עם edge-tts (Fallback) - """ - try: - import edge_tts - - logger.info(f"🎙️ Edge TTS (Fallback): Generating audio...") - communicate = edge_tts.Communicate(text, EDGE_TTS_VOICE) - await communicate.save(output_path) - - logger.info(f"✅ Edge TTS: Audio saved to {output_path}") - return True - - except Exception as e: - logger.error(f"❌ Edge TTS failed: {e}") - return False - - -async def generate_teacher_audio(text: str, output_path: str = None) -> str: - """ - V273.0: יצירת אודיו עם Google Cloud TTS (איכות גבוהה) - - מנסה קודם Google Cloud TTS, אם לא מוגדר/נכשל → edge-tts fallback - - Returns: - - Public URL (if Firebase upload success) - - Base64 string (fallback) - - None (if all failed) - """ - try: - if not text: - return None - - # Clean text for TTS (remove emojis and special chars that cause issues) - clean_text = _clean_text_for_tts(text) - - if not clean_text: - return None - - logger.info(f"🎙️ TTS Request: {clean_text[:50]}...") - - # Determine output path - if output_path: - os.makedirs(os.path.dirname(output_path), exist_ok=True) - final_path = output_path - else: - timestamp = int(asyncio.get_event_loop().time() * 1000) - final_path = os.path.join(tempfile.gettempdir(), f"audio_{timestamp}.mp3") - - # Try Google Cloud TTS first - success = False - if _is_google_cloud_configured(): - success = await _generate_with_google_cloud(clean_text, final_path) - else: - logger.info("ℹ️ Google Cloud not configured, using Edge TTS") - - # Fallback to edge-tts - if not success and USE_EDGE_TTS_FALLBACK: - success = await _generate_with_edge_tts(clean_text, final_path) - - if not success: - logger.error("❌ All TTS methods failed") - return None - - # Try Firebase Upload - try: - blob_name = f"audio/{os.path.basename(final_path)}" - loop = asyncio.get_running_loop() - - public_url = await loop.run_in_executor( - None, - lambda: firebase_manager.upload_file(final_path, blob_name) - ) - - if public_url: - logger.info(f"☁️ Firebase URL: {public_url}") - # Clean up local file - if not output_path: - os.remove(final_path) - return public_url - except Exception as fb_err: - logger.warning(f"⚠️ Firebase upload failed ({fb_err}). Using Base64.") - - # Fallback: Return Base64 - with open(final_path, "rb") as audio_file: - audio_bytes = audio_file.read() - audio_base64 = base64.b64encode(audio_bytes).decode('utf-8') - - # Clean up temp file - if not output_path: - os.remove(final_path) - - return audio_base64 - - except Exception as e: - logger.error(f"❌ TTS Generation Failed: {e}") - return None - - -def _clean_text_for_tts(text: str) -> str: - """ - ניקוי טקסט לפני TTS - הסרת אימוג'ים וסימנים בעייתיים - """ - import re - - if not text: - return "" - - # Remove emojis - emoji_pattern = re.compile("[" - u"\U0001F600-\U0001F64F" # emoticons - u"\U0001F300-\U0001F5FF" # symbols & pictographs - u"\U0001F680-\U0001F6FF" # transport & map symbols - u"\U0001F1E0-\U0001F1FF" # flags - u"\U00002702-\U000027B0" - u"\U000024C2-\U0001F251" - "]+", flags=re.UNICODE) - - clean = emoji_pattern.sub('', text) - - # Remove multiple spaces - clean = re.sub(r'\s+', ' ', clean) - - # Remove LaTeX remnants that might have slipped through - clean = clean.replace('$', '').replace('\\', '') - - return clean.strip() - - -# ═══════════════════════════════════════════════════════════════ -# 🧪 Testing -# ═══════════════════════════════════════════════════════════════ - -if __name__ == "__main__": - async def main(): - text = """ - איזה יופי של תרגיל! היינו צריכים למצוא את נקודות הקיצון של הפונקציה. - השתמשנו בנגזרת ראשונה כדי למצוא איפה השיפוע מתאפס. - הטריק לזכור - נגזרת אפס תמיד מסמנת נקודת קיצון אפשרית. - כל הכבוד על ההתמדה! - """ - - print(f"🎙️ Testing TTS...") - print(f"📝 Text length: {len(text)} chars") - print(f"☁️ Google Cloud configured: {_is_google_cloud_configured()}") - - result = await generate_teacher_audio(text) - - if result: - if result.startswith("http"): - print(f"✅ Got URL: {result}") - else: - print(f"✅ Got Base64: {len(result)} chars") - else: - print("❌ TTS failed") - - asyncio.run(main()) \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py deleted file mode 100644 index 7c7ae39d6c465edc3f4bb484a37c486d5a7ca61a..0000000000000000000000000000000000000000 --- a/backend/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# backend/__init__.py -# BuddyMath V2.0 backend package diff --git a/backend/extractor.py b/backend/extractor.py deleted file mode 100644 index 0510466776f2ccd68ee300906aa6a23699249480..0000000000000000000000000000000000000000 --- a/backend/extractor.py +++ /dev/null @@ -1,127 +0,0 @@ -# backend/extractor.py -# BuddyMath V2.0 — Stage 1: Extraction LLM -# -# RULES (from spec): -# 1. Extract ONLY. Never solve. -# 2. If a value is unclear from OCR/text → return null. NO guessing. -# 3. `find` must list what the question asks, not intermediate steps. -# 4. `constraints` must list verbal/diagram constraints verbatim. -# ───────────────────────────────────────────────────────────────────────── - -from __future__ import annotations - -import json -import logging -import re -from typing import Any, Optional - -import google.generativeai as genai - -from backend.math_schema import MathProblemSchema - -logger = logging.getLogger(__name__) - -# ── Extraction-only system prompt ──────────────────────────────────────── -_EXTRACTION_SYSTEM_PROMPT = """ -You are a MATH DATA EXTRACTOR. Your ONLY job is to parse the problem and output a strict JSON object. - -ABSOLUTE RULES: -1. DO NOT solve the problem. DO NOT calculate anything. -2. If a value is unclear, ambiguous, or the OCR image is blurry → use JSON null for that field. - null ≠ "field not in problem". null = "field exists in problem but value is unreadable/unclear". -3. Every field in `given` that the problem mentions must appear — even if its value is null. -4. `find` must list exactly what the question asks to find/prove/calculate. -5. `constraints` must list verbal/diagram rules (e.g. "M is the midpoint of AD"). - -OUTPUT FORMAT (strict JSON, no markdown, no explanation): -{ - "problem_type": "", - "sub_type": "", - "given": { - "