asnannp commited on
Commit
07c6861
·
1 Parent(s): 2e1605c

deploy: sync backend (real general answers, casual short-circuit, classifier, chat resilience)

Browse files
backend/Dockerfile CHANGED
@@ -5,7 +5,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
5
  UPLOAD_DIR=/app/uploads \
6
  TTS_OUTPUT_DIR=/app/generated/audio \
7
  GENERATED_VIDEO_JOBS_DIR=/app/generated-video-jobs \
8
- GENERATED_VIDEO_OUTPUT_DIR=/app/generated-videos
 
9
 
10
  WORKDIR /app
11
 
 
5
  UPLOAD_DIR=/app/uploads \
6
  TTS_OUTPUT_DIR=/app/generated/audio \
7
  GENERATED_VIDEO_JOBS_DIR=/app/generated-video-jobs \
8
+ GENERATED_VIDEO_OUTPUT_DIR=/app/generated-videos \
9
+ LEARN_LESSON_CACHE_DIR=/app/generated/learn-anything
10
 
11
  WORKDIR /app
12
 
backend/app/main.py CHANGED
@@ -1,5 +1,6 @@
1
  from collections.abc import AsyncIterator
2
  from contextlib import asynccontextmanager
 
3
 
4
  import logging
5
  import time
@@ -778,3 +779,14 @@ app.mount(
778
  StaticFiles(directory=settings.resolved_generated_video_output_dir, check_dir=False),
779
  name="generated-videos",
780
  )
 
 
 
 
 
 
 
 
 
 
 
 
1
  from collections.abc import AsyncIterator
2
  from contextlib import asynccontextmanager
3
+ from pathlib import Path
4
 
5
  import logging
6
  import time
 
779
  StaticFiles(directory=settings.resolved_generated_video_output_dir, check_dir=False),
780
  name="generated-videos",
781
  )
782
+ # Learn Anything lesson cache (manifests + per-beat audio). Must live under a
783
+ # writable path on Hugging Face (/app/generated/...), not monorepo public/.
784
+ _learn_lesson_static = (
785
+ Path(__file__).resolve().parents[1] / "generated" / "learn-anything"
786
+ )
787
+ _learn_lesson_static.mkdir(parents=True, exist_ok=True)
788
+ app.mount(
789
+ "/generated/learn-anything",
790
+ StaticFiles(directory=_learn_lesson_static, check_dir=False),
791
+ name="generated-learn-lessons",
792
+ )
backend/app/routes/learning_engine.py CHANGED
@@ -196,3 +196,20 @@ def generate_learn_lesson(
196
  )
197
  except LessonBuildError as exc:
198
  raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
196
  )
197
  except LessonBuildError as exc:
198
  raise HTTPException(status_code=status.HTTP_502_BAD_GATEWAY, detail=str(exc)) from exc
199
+ except Exception as exc:
200
+ # Surface a clear student-safe message instead of the global 500
201
+ # "internal error" envelope (common when cache paths fail on HF).
202
+ import logging
203
+
204
+ logging.getLogger(__name__).exception(
205
+ "Unhandled learn-lesson failure for topic=%s lesson=%s",
206
+ payload.topic,
207
+ payload.lesson_title,
208
+ )
209
+ raise HTTPException(
210
+ status_code=status.HTTP_502_BAD_GATEWAY,
211
+ detail=(
212
+ f"Could not prepare this class ({type(exc).__name__}). "
213
+ "Your plan is still saved — retry in a moment."
214
+ ),
215
+ ) from exc
backend/app/services/learn_lesson_builder.py CHANGED
@@ -32,12 +32,15 @@ from dataclasses import dataclass, field
32
  from pathlib import Path
33
  from typing import Any
34
 
35
- from app.core.config import PROJECT_ROOT, get_settings
36
 
37
  logger = logging.getLogger(__name__)
38
 
39
- # Served to the browser under /generated/learn-anything/<hash>/...
40
- PUBLIC_ROOT = PROJECT_ROOT / "public" / "generated" / "learn-anything"
 
 
 
41
 
42
  GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
43
  # llama-4-scout was retired from Groq; 3.3-70b is the strongest current chat model.
@@ -757,6 +760,28 @@ def _lesson_lock_for(key: str) -> threading.Lock:
757
  return lock
758
 
759
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
760
  def build_lesson(
761
  *,
762
  topic: str,
@@ -783,7 +808,8 @@ def build_lesson(
783
  else (_env("DOCDOE_TTS_MODEL") or "aura-luna-en")
784
  )
785
  key = lesson_hash(topic, lesson_title, level, medium, voice)
786
- out_dir = PUBLIC_ROOT / key
 
787
  manifest_path = out_dir / "lesson.json"
788
 
789
  if manifest_path.exists() and not force:
@@ -806,7 +832,12 @@ def build_lesson(
806
  context=context,
807
  )
808
  )
809
- out_dir.mkdir(parents=True, exist_ok=True)
 
 
 
 
 
810
  delivery_mode = "reading" if mock_mode else "audio"
811
  delivery_notice = (
812
  "Local preview: this complete starter class is available in reading mode; no generated voice is being presented as real audio."
 
32
  from pathlib import Path
33
  from typing import Any
34
 
35
+ from app.core.config import BACKEND_DIR, PROJECT_ROOT, get_settings
36
 
37
  logger = logging.getLogger(__name__)
38
 
39
+ # Writable cache for lesson manifests + audio. On Hugging Face Docker the
40
+ # backend tree is /app, so PROJECT_ROOT/public is outside the container and
41
+ # not writable (PermissionError → opaque 500). Cache under BACKEND_DIR.
42
+ # Tests may monkeypatch this path.
43
+ PUBLIC_ROOT = BACKEND_DIR / "generated" / "learn-anything"
44
 
45
  GROQ_URL = "https://api.groq.com/openai/v1/chat/completions"
46
  # llama-4-scout was retired from Groq; 3.3-70b is the strongest current chat model.
 
760
  return lock
761
 
762
 
763
+ def ensure_public_root() -> Path:
764
+ """Ensure the lesson cache directory exists and is writable."""
765
+ env = _env("LEARN_LESSON_CACHE_DIR")
766
+ root = Path(env) if env else PUBLIC_ROOT
767
+ try:
768
+ root.mkdir(parents=True, exist_ok=True)
769
+ probe = root / ".write_probe"
770
+ probe.write_text("ok", encoding="utf-8")
771
+ probe.unlink(missing_ok=True)
772
+ return root
773
+ except OSError:
774
+ fallback = Path(os.getenv("TMPDIR") or os.getenv("TEMP") or "/tmp") / "docdoe-learn-lessons"
775
+ try:
776
+ fallback.mkdir(parents=True, exist_ok=True)
777
+ logger.warning("Lesson cache fell back to %s", fallback)
778
+ return fallback
779
+ except OSError as exc:
780
+ raise LessonBuildError(
781
+ f"Lesson cache directory is not writable ({root}): {exc}"
782
+ ) from exc
783
+
784
+
785
  def build_lesson(
786
  *,
787
  topic: str,
 
808
  else (_env("DOCDOE_TTS_MODEL") or "aura-luna-en")
809
  )
810
  key = lesson_hash(topic, lesson_title, level, medium, voice)
811
+ cache_root = ensure_public_root()
812
+ out_dir = cache_root / key
813
  manifest_path = out_dir / "lesson.json"
814
 
815
  if manifest_path.exists() and not force:
 
832
  context=context,
833
  )
834
  )
835
+ try:
836
+ out_dir.mkdir(parents=True, exist_ok=True)
837
+ except OSError as exc:
838
+ raise LessonBuildError(
839
+ f"Could not create lesson cache folder ({out_dir}): {exc}"
840
+ ) from exc
841
  delivery_mode = "reading" if mock_mode else "audio"
842
  delivery_notice = (
843
  "Local preview: this complete starter class is available in reading mode; no generated voice is being presented as real audio."