Spaces:
Sleeping
Sleeping
Max Saavedra commited on
Commit ·
6697007
1
Parent(s): 771e8b6
Flash cards artifact added.
Browse files- backend/api/artifacts.py +28 -4
- backend/models/schemas.py +1 -0
- backend/modules/artifacts.py +64 -15
- frontend/app.py +68 -20
- tests/test_api_artifacts.py +7 -0
- tests/test_artifacts.py +4 -0
backend/api/artifacts.py
CHANGED
|
@@ -3,8 +3,9 @@ from fastapi.responses import FileResponse
|
|
| 3 |
|
| 4 |
from backend.models.schemas import ArtifactGenerateOut, ArtifactGenerateRequest, ArtifactListOut
|
| 5 |
from backend.modules.artifacts import (
|
| 6 |
-
|
| 7 |
-
|
|
|
|
| 8 |
generate_report,
|
| 9 |
list_artifacts,
|
| 10 |
resolve_artifact_path,
|
|
@@ -72,8 +73,31 @@ def create_quiz_artifact(
|
|
| 72 |
raise HTTPException(status_code=404, detail="Notebook not found")
|
| 73 |
except ValueError as exc:
|
| 74 |
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 75 |
-
except Exception as exc:
|
| 76 |
-
raise HTTPException(status_code=500, detail=f"Quiz generation failed: {exc}") from exc
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
@router.post("/{notebook_id}/artifacts/podcast", response_model=ArtifactGenerateOut)
|
|
|
|
| 3 |
|
| 4 |
from backend.models.schemas import ArtifactGenerateOut, ArtifactGenerateRequest, ArtifactListOut
|
| 5 |
from backend.modules.artifacts import (
|
| 6 |
+
generate_flashcards,
|
| 7 |
+
generate_podcast,
|
| 8 |
+
generate_quiz,
|
| 9 |
generate_report,
|
| 10 |
list_artifacts,
|
| 11 |
resolve_artifact_path,
|
|
|
|
| 73 |
raise HTTPException(status_code=404, detail="Notebook not found")
|
| 74 |
except ValueError as exc:
|
| 75 |
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 76 |
+
except Exception as exc:
|
| 77 |
+
raise HTTPException(status_code=500, detail=f"Quiz generation failed: {exc}") from exc
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@router.post("/{notebook_id}/artifacts/flashcards", response_model=ArtifactGenerateOut)
|
| 81 |
+
def create_flashcards_artifact(
|
| 82 |
+
notebook_id: str,
|
| 83 |
+
payload: ArtifactGenerateRequest,
|
| 84 |
+
current_user: User = Depends(get_current_user),
|
| 85 |
+
) -> ArtifactGenerateOut:
|
| 86 |
+
try:
|
| 87 |
+
enforce_user_match(current_user, payload.user_id)
|
| 88 |
+
return generate_flashcards(
|
| 89 |
+
store,
|
| 90 |
+
user_id=current_user.user_id,
|
| 91 |
+
notebook_id=notebook_id,
|
| 92 |
+
prompt=payload.prompt,
|
| 93 |
+
num_questions=payload.num_questions,
|
| 94 |
+
)
|
| 95 |
+
except FileNotFoundError:
|
| 96 |
+
raise HTTPException(status_code=404, detail="Notebook not found")
|
| 97 |
+
except ValueError as exc:
|
| 98 |
+
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
| 99 |
+
except Exception as exc:
|
| 100 |
+
raise HTTPException(status_code=500, detail=f"Flashcards generation failed: {exc}") from exc
|
| 101 |
|
| 102 |
|
| 103 |
@router.post("/{notebook_id}/artifacts/podcast", response_model=ArtifactGenerateOut)
|
backend/models/schemas.py
CHANGED
|
@@ -99,6 +99,7 @@ class PodcastArtifactOut(BaseModel):
|
|
| 99 |
class ArtifactListOut(BaseModel):
|
| 100 |
reports: List[ArtifactFileOut]
|
| 101 |
quizzes: List[ArtifactFileOut]
|
|
|
|
| 102 |
podcasts: List[PodcastArtifactOut]
|
| 103 |
|
| 104 |
|
|
|
|
| 99 |
class ArtifactListOut(BaseModel):
|
| 100 |
reports: List[ArtifactFileOut]
|
| 101 |
quizzes: List[ArtifactFileOut]
|
| 102 |
+
flashcards: List[ArtifactFileOut]
|
| 103 |
podcasts: List[PodcastArtifactOut]
|
| 104 |
|
| 105 |
|
backend/modules/artifacts.py
CHANGED
|
@@ -59,11 +59,12 @@ def _now() -> str:
|
|
| 59 |
def _artifact_dirs(store: NotebookStore, user_id: str, notebook_id: str) -> Dict[str, Path]:
|
| 60 |
notebook_dir = store.require_notebook_dir(user_id, notebook_id)
|
| 61 |
root = notebook_dir / "artifacts"
|
| 62 |
-
dirs = {
|
| 63 |
-
"report": root / "reports",
|
| 64 |
-
"quiz": root / "quizzes",
|
| 65 |
-
"
|
| 66 |
-
|
|
|
|
| 67 |
for d in dirs.values():
|
| 68 |
d.mkdir(parents=True, exist_ok=True)
|
| 69 |
return dirs
|
|
@@ -167,7 +168,7 @@ def _quiz_fallback(sources: List[Dict[str, str]], num_questions: int) -> str:
|
|
| 167 |
)
|
| 168 |
|
| 169 |
|
| 170 |
-
def _podcast_transcript_fallback(sources: List[Dict[str, str]], extra_prompt: Optional[str]) -> str:
|
| 171 |
focus = extra_prompt.strip() if extra_prompt else "core concepts from the notebook"
|
| 172 |
lines = [
|
| 173 |
"# Podcast Transcript",
|
|
@@ -182,7 +183,20 @@ def _podcast_transcript_fallback(sources: List[Dict[str, str]], extra_prompt: Op
|
|
| 182 |
lines.append(f"**Host:** Great, and why does that matter in practice?")
|
| 183 |
lines.append("**Co-Host:** That wraps the study summary. Review the report and quiz next.")
|
| 184 |
lines.append("")
|
| 185 |
-
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
|
| 188 |
def _encode_pcm_to_mp3(pcm_16le: bytes, sample_rate: int, channels: int) -> bytes:
|
|
@@ -286,7 +300,7 @@ def generate_report(
|
|
| 286 |
)
|
| 287 |
|
| 288 |
|
| 289 |
-
def generate_quiz(
|
| 290 |
store: NotebookStore,
|
| 291 |
*,
|
| 292 |
user_id: str,
|
|
@@ -310,13 +324,47 @@ def generate_quiz(
|
|
| 310 |
)
|
| 311 |
content = _llm_or_fallback(llm_prompt, _quiz_fallback(sources, questions))
|
| 312 |
out_path = _write_markdown_artifact(dirs["quiz"], "quiz", content)
|
| 313 |
-
return ArtifactGenerateOut(
|
| 314 |
-
artifact_type="quiz",
|
| 315 |
message=f"Generated {out_path.name}",
|
| 316 |
markdown_path=str(out_path.as_posix()),
|
| 317 |
audio_path=None,
|
| 318 |
-
created_at=_now(),
|
| 319 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 320 |
|
| 321 |
|
| 322 |
def generate_podcast(
|
|
@@ -361,8 +409,9 @@ def generate_podcast(
|
|
| 361 |
def list_artifacts(store: NotebookStore, *, user_id: str, notebook_id: str) -> ArtifactListOut:
|
| 362 |
dirs = _artifact_dirs(store, user_id, notebook_id)
|
| 363 |
|
| 364 |
-
reports = [_artifact_file_out(p) for p in sorted(dirs["report"].glob("report_*.md"))]
|
| 365 |
-
quizzes = [_artifact_file_out(p) for p in sorted(dirs["quiz"].glob("quiz_*.md"))]
|
|
|
|
| 366 |
|
| 367 |
podcast_indices: set[int] = set()
|
| 368 |
for path in dirs["podcast"].glob("podcast_*.*"):
|
|
@@ -381,7 +430,7 @@ def list_artifacts(store: NotebookStore, *, user_id: str, notebook_id: str) -> A
|
|
| 381 |
)
|
| 382 |
)
|
| 383 |
|
| 384 |
-
return ArtifactListOut(reports=reports, quizzes=quizzes, podcasts=podcasts)
|
| 385 |
|
| 386 |
|
| 387 |
def resolve_artifact_path(
|
|
|
|
| 59 |
def _artifact_dirs(store: NotebookStore, user_id: str, notebook_id: str) -> Dict[str, Path]:
|
| 60 |
notebook_dir = store.require_notebook_dir(user_id, notebook_id)
|
| 61 |
root = notebook_dir / "artifacts"
|
| 62 |
+
dirs = {
|
| 63 |
+
"report": root / "reports",
|
| 64 |
+
"quiz": root / "quizzes",
|
| 65 |
+
"flashcards": root / "flashcards",
|
| 66 |
+
"podcast": root / "podcasts",
|
| 67 |
+
}
|
| 68 |
for d in dirs.values():
|
| 69 |
d.mkdir(parents=True, exist_ok=True)
|
| 70 |
return dirs
|
|
|
|
| 168 |
)
|
| 169 |
|
| 170 |
|
| 171 |
+
def _podcast_transcript_fallback(sources: List[Dict[str, str]], extra_prompt: Optional[str]) -> str:
|
| 172 |
focus = extra_prompt.strip() if extra_prompt else "core concepts from the notebook"
|
| 173 |
lines = [
|
| 174 |
"# Podcast Transcript",
|
|
|
|
| 183 |
lines.append(f"**Host:** Great, and why does that matter in practice?")
|
| 184 |
lines.append("**Co-Host:** That wraps the study summary. Review the report and quiz next.")
|
| 185 |
lines.append("")
|
| 186 |
+
return "\n".join(lines)
|
| 187 |
+
|
| 188 |
+
|
| 189 |
+
def _flashcards_fallback(sources: List[Dict[str, str]], num_cards: int) -> str:
|
| 190 |
+
cards = max(3, min(20, int(num_cards)))
|
| 191 |
+
lines = ["# Flashcards", ""]
|
| 192 |
+
for i in range(1, cards + 1):
|
| 193 |
+
src = sources[(i - 1) % len(sources)]
|
| 194 |
+
snippet = src["text"].replace("\n", " ")[:200].strip()
|
| 195 |
+
lines.append(f"## Card {i}")
|
| 196 |
+
lines.append(f"Q: What key point appears in {src['source_name']}?")
|
| 197 |
+
lines.append(f"A: {snippet}")
|
| 198 |
+
lines.append("")
|
| 199 |
+
return "\n".join(lines).strip() + "\n"
|
| 200 |
|
| 201 |
|
| 202 |
def _encode_pcm_to_mp3(pcm_16le: bytes, sample_rate: int, channels: int) -> bytes:
|
|
|
|
| 300 |
)
|
| 301 |
|
| 302 |
|
| 303 |
+
def generate_quiz(
|
| 304 |
store: NotebookStore,
|
| 305 |
*,
|
| 306 |
user_id: str,
|
|
|
|
| 324 |
)
|
| 325 |
content = _llm_or_fallback(llm_prompt, _quiz_fallback(sources, questions))
|
| 326 |
out_path = _write_markdown_artifact(dirs["quiz"], "quiz", content)
|
| 327 |
+
return ArtifactGenerateOut(
|
| 328 |
+
artifact_type="quiz",
|
| 329 |
message=f"Generated {out_path.name}",
|
| 330 |
markdown_path=str(out_path.as_posix()),
|
| 331 |
audio_path=None,
|
| 332 |
+
created_at=_now(),
|
| 333 |
+
)
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
def generate_flashcards(
|
| 337 |
+
store: NotebookStore,
|
| 338 |
+
*,
|
| 339 |
+
user_id: str,
|
| 340 |
+
notebook_id: str,
|
| 341 |
+
prompt: Optional[str] = None,
|
| 342 |
+
num_questions: int = 8,
|
| 343 |
+
) -> ArtifactGenerateOut:
|
| 344 |
+
dirs = _artifact_dirs(store, user_id, notebook_id)
|
| 345 |
+
sources = _collect_source_texts(store, user_id, notebook_id)
|
| 346 |
+
if not sources:
|
| 347 |
+
raise ValueError("No ingested sources available. Upload or ingest sources first.")
|
| 348 |
+
|
| 349 |
+
cards = max(3, min(20, int(num_questions)))
|
| 350 |
+
focus = prompt.strip() if prompt else "Create concise study flashcards."
|
| 351 |
+
llm_prompt = (
|
| 352 |
+
"Create markdown flashcards from SOURCES.\n"
|
| 353 |
+
f"Include exactly {cards} cards in format:\n"
|
| 354 |
+
"## Card N\nQ: ...\nA: ...\n"
|
| 355 |
+
"Keep answers concise and grounded in source content.\n\n"
|
| 356 |
+
f"FOCUS:\n{focus}\n\n"
|
| 357 |
+
f"SOURCES:\n{_sources_block(sources)}\n"
|
| 358 |
+
)
|
| 359 |
+
content = _llm_or_fallback(llm_prompt, _flashcards_fallback(sources, cards))
|
| 360 |
+
out_path = _write_markdown_artifact(dirs["flashcards"], "flashcards", content)
|
| 361 |
+
return ArtifactGenerateOut(
|
| 362 |
+
artifact_type="flashcards",
|
| 363 |
+
message=f"Generated {out_path.name}",
|
| 364 |
+
markdown_path=str(out_path.as_posix()),
|
| 365 |
+
audio_path=None,
|
| 366 |
+
created_at=_now(),
|
| 367 |
+
)
|
| 368 |
|
| 369 |
|
| 370 |
def generate_podcast(
|
|
|
|
| 409 |
def list_artifacts(store: NotebookStore, *, user_id: str, notebook_id: str) -> ArtifactListOut:
|
| 410 |
dirs = _artifact_dirs(store, user_id, notebook_id)
|
| 411 |
|
| 412 |
+
reports = [_artifact_file_out(p) for p in sorted(dirs["report"].glob("report_*.md"))]
|
| 413 |
+
quizzes = [_artifact_file_out(p) for p in sorted(dirs["quiz"].glob("quiz_*.md"))]
|
| 414 |
+
flashcards = [_artifact_file_out(p) for p in sorted(dirs["flashcards"].glob("flashcards_*.md"))]
|
| 415 |
|
| 416 |
podcast_indices: set[int] = set()
|
| 417 |
for path in dirs["podcast"].glob("podcast_*.*"):
|
|
|
|
| 430 |
)
|
| 431 |
)
|
| 432 |
|
| 433 |
+
return ArtifactListOut(reports=reports, quizzes=quizzes, flashcards=flashcards, podcasts=podcasts)
|
| 434 |
|
| 435 |
|
| 436 |
def resolve_artifact_path(
|
frontend/app.py
CHANGED
|
@@ -417,16 +417,18 @@ def send_message(message: str, history, user_id: str, notebook_id: str):
|
|
| 417 |
|
| 418 |
|
| 419 |
def _empty_artifacts_payload() -> dict[str, Any]:
|
| 420 |
-
return {"reports": [], "quizzes": [], "podcasts": []}
|
| 421 |
|
| 422 |
|
| 423 |
def _artifact_outputs_from_payload(payload: dict[str, Any]):
|
| 424 |
reports = payload.get("reports") or []
|
| 425 |
quizzes = payload.get("quizzes") or []
|
|
|
|
| 426 |
podcasts = payload.get("podcasts") or []
|
| 427 |
|
| 428 |
latest_report = reports[-1].get("path") if reports else None
|
| 429 |
latest_quiz = quizzes[-1].get("path") if quizzes else None
|
|
|
|
| 430 |
|
| 431 |
latest_transcript = None
|
| 432 |
latest_audio = None
|
|
@@ -436,14 +438,14 @@ def _artifact_outputs_from_payload(payload: dict[str, Any]):
|
|
| 436 |
audio = last.get("audio") or {}
|
| 437 |
latest_transcript = transcript.get("path")
|
| 438 |
latest_audio = audio.get("path")
|
| 439 |
-
return latest_report, latest_quiz, latest_transcript, latest_audio
|
| 440 |
|
| 441 |
|
| 442 |
def refresh_artifacts(user_id: str, notebook_id: str):
|
| 443 |
user_id = (user_id or "").strip()
|
| 444 |
if not user_id or not notebook_id:
|
| 445 |
payload = _empty_artifacts_payload()
|
| 446 |
-
return gr.JSON(value=payload), None, None, None, None, "Select a notebook first."
|
| 447 |
try:
|
| 448 |
payload = _api_request(
|
| 449 |
"GET",
|
|
@@ -451,23 +453,23 @@ def refresh_artifacts(user_id: str, notebook_id: str):
|
|
| 451 |
params={"user_id": user_id},
|
| 452 |
headers=_auth_headers(user_id),
|
| 453 |
)
|
| 454 |
-
report, quiz, transcript, audio = _artifact_outputs_from_payload(payload)
|
| 455 |
-
return gr.JSON(value=payload), report, quiz, transcript, audio, ""
|
| 456 |
except Exception as exc:
|
| 457 |
payload = _empty_artifacts_payload()
|
| 458 |
-
return gr.JSON(value=payload), None, None, None, None, str(exc)
|
| 459 |
|
| 460 |
|
| 461 |
def sync_artifacts_on_notebook_change(user_id: str, notebook_id: str):
|
| 462 |
-
payload_json, report, quiz, transcript, audio, _status = refresh_artifacts(user_id, notebook_id)
|
| 463 |
-
return payload_json, report, quiz, transcript, audio
|
| 464 |
|
| 465 |
|
| 466 |
def generate_report_artifact(user_id: str, notebook_id: str, artifact_prompt: str):
|
| 467 |
user_id = (user_id or "").strip()
|
| 468 |
if not user_id or not notebook_id:
|
| 469 |
payload = _empty_artifacts_payload()
|
| 470 |
-
return gr.JSON(value=payload), None, None, None, None, "Select a notebook first."
|
| 471 |
try:
|
| 472 |
resp = _api_request(
|
| 473 |
"POST",
|
|
@@ -476,18 +478,18 @@ def generate_report_artifact(user_id: str, notebook_id: str, artifact_prompt: st
|
|
| 476 |
headers=_auth_headers(user_id),
|
| 477 |
timeout=180,
|
| 478 |
)
|
| 479 |
-
payload_json, report, quiz, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
|
| 480 |
-
return payload_json, report, quiz, transcript, audio, str(resp.get("message", "Generated report."))
|
| 481 |
except Exception as exc:
|
| 482 |
payload = _empty_artifacts_payload()
|
| 483 |
-
return gr.JSON(value=payload), None, None, None, None, str(exc)
|
| 484 |
|
| 485 |
|
| 486 |
def generate_quiz_artifact(user_id: str, notebook_id: str, artifact_prompt: str, num_questions: float):
|
| 487 |
user_id = (user_id or "").strip()
|
| 488 |
if not user_id or not notebook_id:
|
| 489 |
payload = _empty_artifacts_payload()
|
| 490 |
-
return gr.JSON(value=payload), None, None, None, None, "Select a notebook first."
|
| 491 |
try:
|
| 492 |
resp = _api_request(
|
| 493 |
"POST",
|
|
@@ -500,18 +502,42 @@ def generate_quiz_artifact(user_id: str, notebook_id: str, artifact_prompt: str,
|
|
| 500 |
headers=_auth_headers(user_id),
|
| 501 |
timeout=180,
|
| 502 |
)
|
| 503 |
-
payload_json, report, quiz, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
|
| 504 |
-
return payload_json, report, quiz, transcript, audio, str(resp.get("message", "Generated quiz."))
|
| 505 |
except Exception as exc:
|
| 506 |
payload = _empty_artifacts_payload()
|
| 507 |
-
return gr.JSON(value=payload), None, None, None, None, str(exc)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
|
| 510 |
def generate_podcast_artifact(user_id: str, notebook_id: str, artifact_prompt: str):
|
| 511 |
user_id = (user_id or "").strip()
|
| 512 |
if not user_id or not notebook_id:
|
| 513 |
payload = _empty_artifacts_payload()
|
| 514 |
-
return gr.JSON(value=payload), None, None, None, None, "Select a notebook first."
|
| 515 |
try:
|
| 516 |
resp = _api_request(
|
| 517 |
"POST",
|
|
@@ -520,11 +546,11 @@ def generate_podcast_artifact(user_id: str, notebook_id: str, artifact_prompt: s
|
|
| 520 |
headers=_auth_headers(user_id),
|
| 521 |
timeout=240,
|
| 522 |
)
|
| 523 |
-
payload_json, report, quiz, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
|
| 524 |
-
return payload_json, report, quiz, transcript, audio, str(resp.get("message", "Generated podcast."))
|
| 525 |
except Exception as exc:
|
| 526 |
payload = _empty_artifacts_payload()
|
| 527 |
-
return gr.JSON(value=payload), None, None, None, None, str(exc)
|
| 528 |
|
| 529 |
def greet_user(profile: gr.OAuthProfile | None) -> tuple[str | None, str]:
|
| 530 |
if profile is None:
|
|
@@ -593,10 +619,12 @@ with gr.Blocks(title="MemoriaLM") as demo:
|
|
| 593 |
report_btn = gr.Button("Generate Report")
|
| 594 |
with gr.Row():
|
| 595 |
quiz_btn = gr.Button("Generate Quiz")
|
|
|
|
| 596 |
podcast_btn = gr.Button("Generate Podcast")
|
| 597 |
artifacts_json = gr.JSON(label="Generated Artifacts", value=_empty_artifacts_payload())
|
| 598 |
latest_report_file = gr.File(label="Latest Report (.md)", interactive=False)
|
| 599 |
latest_quiz_file = gr.File(label="Latest Quiz (.md)", interactive=False)
|
|
|
|
| 600 |
latest_podcast_transcript_file = gr.File(label="Latest Podcast Transcript (.md)", interactive=False)
|
| 601 |
latest_podcast_audio = gr.Audio(label="Latest Podcast Audio (.mp3)", type="filepath", interactive=False)
|
| 602 |
|
|
@@ -666,6 +694,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
|
|
| 666 |
artifacts_json,
|
| 667 |
latest_report_file,
|
| 668 |
latest_quiz_file,
|
|
|
|
| 669 |
latest_podcast_transcript_file,
|
| 670 |
latest_podcast_audio,
|
| 671 |
],
|
|
@@ -678,6 +707,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
|
|
| 678 |
artifacts_json,
|
| 679 |
latest_report_file,
|
| 680 |
latest_quiz_file,
|
|
|
|
| 681 |
latest_podcast_transcript_file,
|
| 682 |
latest_podcast_audio,
|
| 683 |
],
|
|
@@ -689,6 +719,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
|
|
| 689 |
artifacts_json,
|
| 690 |
latest_report_file,
|
| 691 |
latest_quiz_file,
|
|
|
|
| 692 |
latest_podcast_transcript_file,
|
| 693 |
latest_podcast_audio,
|
| 694 |
],
|
|
@@ -736,6 +767,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
|
|
| 736 |
artifacts_json,
|
| 737 |
latest_report_file,
|
| 738 |
latest_quiz_file,
|
|
|
|
| 739 |
latest_podcast_transcript_file,
|
| 740 |
latest_podcast_audio,
|
| 741 |
status_box,
|
|
@@ -748,6 +780,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
|
|
| 748 |
artifacts_json,
|
| 749 |
latest_report_file,
|
| 750 |
latest_quiz_file,
|
|
|
|
| 751 |
latest_podcast_transcript_file,
|
| 752 |
latest_podcast_audio,
|
| 753 |
status_box,
|
|
@@ -760,6 +793,20 @@ with gr.Blocks(title="MemoriaLM") as demo:
|
|
| 760 |
artifacts_json,
|
| 761 |
latest_report_file,
|
| 762 |
latest_quiz_file,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 763 |
latest_podcast_transcript_file,
|
| 764 |
latest_podcast_audio,
|
| 765 |
status_box,
|
|
@@ -772,6 +819,7 @@ with gr.Blocks(title="MemoriaLM") as demo:
|
|
| 772 |
artifacts_json,
|
| 773 |
latest_report_file,
|
| 774 |
latest_quiz_file,
|
|
|
|
| 775 |
latest_podcast_transcript_file,
|
| 776 |
latest_podcast_audio,
|
| 777 |
status_box,
|
|
|
|
| 417 |
|
| 418 |
|
| 419 |
def _empty_artifacts_payload() -> dict[str, Any]:
|
| 420 |
+
return {"reports": [], "quizzes": [], "flashcards": [], "podcasts": []}
|
| 421 |
|
| 422 |
|
| 423 |
def _artifact_outputs_from_payload(payload: dict[str, Any]):
|
| 424 |
reports = payload.get("reports") or []
|
| 425 |
quizzes = payload.get("quizzes") or []
|
| 426 |
+
flashcards = payload.get("flashcards") or []
|
| 427 |
podcasts = payload.get("podcasts") or []
|
| 428 |
|
| 429 |
latest_report = reports[-1].get("path") if reports else None
|
| 430 |
latest_quiz = quizzes[-1].get("path") if quizzes else None
|
| 431 |
+
latest_flashcards = flashcards[-1].get("path") if flashcards else None
|
| 432 |
|
| 433 |
latest_transcript = None
|
| 434 |
latest_audio = None
|
|
|
|
| 438 |
audio = last.get("audio") or {}
|
| 439 |
latest_transcript = transcript.get("path")
|
| 440 |
latest_audio = audio.get("path")
|
| 441 |
+
return latest_report, latest_quiz, latest_flashcards, latest_transcript, latest_audio
|
| 442 |
|
| 443 |
|
| 444 |
def refresh_artifacts(user_id: str, notebook_id: str):
|
| 445 |
user_id = (user_id or "").strip()
|
| 446 |
if not user_id or not notebook_id:
|
| 447 |
payload = _empty_artifacts_payload()
|
| 448 |
+
return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
|
| 449 |
try:
|
| 450 |
payload = _api_request(
|
| 451 |
"GET",
|
|
|
|
| 453 |
params={"user_id": user_id},
|
| 454 |
headers=_auth_headers(user_id),
|
| 455 |
)
|
| 456 |
+
report, quiz, flashcards, transcript, audio = _artifact_outputs_from_payload(payload)
|
| 457 |
+
return gr.JSON(value=payload), report, quiz, flashcards, transcript, audio, ""
|
| 458 |
except Exception as exc:
|
| 459 |
payload = _empty_artifacts_payload()
|
| 460 |
+
return gr.JSON(value=payload), None, None, None, None, None, str(exc)
|
| 461 |
|
| 462 |
|
| 463 |
def sync_artifacts_on_notebook_change(user_id: str, notebook_id: str):
|
| 464 |
+
payload_json, report, quiz, flashcards, transcript, audio, _status = refresh_artifacts(user_id, notebook_id)
|
| 465 |
+
return payload_json, report, quiz, flashcards, transcript, audio
|
| 466 |
|
| 467 |
|
| 468 |
def generate_report_artifact(user_id: str, notebook_id: str, artifact_prompt: str):
|
| 469 |
user_id = (user_id or "").strip()
|
| 470 |
if not user_id or not notebook_id:
|
| 471 |
payload = _empty_artifacts_payload()
|
| 472 |
+
return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
|
| 473 |
try:
|
| 474 |
resp = _api_request(
|
| 475 |
"POST",
|
|
|
|
| 478 |
headers=_auth_headers(user_id),
|
| 479 |
timeout=180,
|
| 480 |
)
|
| 481 |
+
payload_json, report, quiz, flashcards, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
|
| 482 |
+
return payload_json, report, quiz, flashcards, transcript, audio, str(resp.get("message", "Generated report."))
|
| 483 |
except Exception as exc:
|
| 484 |
payload = _empty_artifacts_payload()
|
| 485 |
+
return gr.JSON(value=payload), None, None, None, None, None, str(exc)
|
| 486 |
|
| 487 |
|
| 488 |
def generate_quiz_artifact(user_id: str, notebook_id: str, artifact_prompt: str, num_questions: float):
|
| 489 |
user_id = (user_id or "").strip()
|
| 490 |
if not user_id or not notebook_id:
|
| 491 |
payload = _empty_artifacts_payload()
|
| 492 |
+
return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
|
| 493 |
try:
|
| 494 |
resp = _api_request(
|
| 495 |
"POST",
|
|
|
|
| 502 |
headers=_auth_headers(user_id),
|
| 503 |
timeout=180,
|
| 504 |
)
|
| 505 |
+
payload_json, report, quiz, flashcards, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
|
| 506 |
+
return payload_json, report, quiz, flashcards, transcript, audio, str(resp.get("message", "Generated quiz."))
|
| 507 |
except Exception as exc:
|
| 508 |
payload = _empty_artifacts_payload()
|
| 509 |
+
return gr.JSON(value=payload), None, None, None, None, None, str(exc)
|
| 510 |
+
|
| 511 |
+
|
| 512 |
+
def generate_flashcards_artifact(user_id: str, notebook_id: str, artifact_prompt: str, num_questions: float):
|
| 513 |
+
user_id = (user_id or "").strip()
|
| 514 |
+
if not user_id or not notebook_id:
|
| 515 |
+
payload = _empty_artifacts_payload()
|
| 516 |
+
return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
|
| 517 |
+
try:
|
| 518 |
+
resp = _api_request(
|
| 519 |
+
"POST",
|
| 520 |
+
f"/api/notebooks/{notebook_id}/artifacts/flashcards",
|
| 521 |
+
json_body={
|
| 522 |
+
"user_id": user_id,
|
| 523 |
+
"prompt": (artifact_prompt or "").strip() or None,
|
| 524 |
+
"num_questions": int(num_questions),
|
| 525 |
+
},
|
| 526 |
+
headers=_auth_headers(user_id),
|
| 527 |
+
timeout=180,
|
| 528 |
+
)
|
| 529 |
+
payload_json, report, quiz, flashcards, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
|
| 530 |
+
return payload_json, report, quiz, flashcards, transcript, audio, str(resp.get("message", "Generated flashcards."))
|
| 531 |
+
except Exception as exc:
|
| 532 |
+
payload = _empty_artifacts_payload()
|
| 533 |
+
return gr.JSON(value=payload), None, None, None, None, None, str(exc)
|
| 534 |
|
| 535 |
|
| 536 |
def generate_podcast_artifact(user_id: str, notebook_id: str, artifact_prompt: str):
|
| 537 |
user_id = (user_id or "").strip()
|
| 538 |
if not user_id or not notebook_id:
|
| 539 |
payload = _empty_artifacts_payload()
|
| 540 |
+
return gr.JSON(value=payload), None, None, None, None, None, "Select a notebook first."
|
| 541 |
try:
|
| 542 |
resp = _api_request(
|
| 543 |
"POST",
|
|
|
|
| 546 |
headers=_auth_headers(user_id),
|
| 547 |
timeout=240,
|
| 548 |
)
|
| 549 |
+
payload_json, report, quiz, flashcards, transcript, audio, _ = refresh_artifacts(user_id, notebook_id)
|
| 550 |
+
return payload_json, report, quiz, flashcards, transcript, audio, str(resp.get("message", "Generated podcast."))
|
| 551 |
except Exception as exc:
|
| 552 |
payload = _empty_artifacts_payload()
|
| 553 |
+
return gr.JSON(value=payload), None, None, None, None, None, str(exc)
|
| 554 |
|
| 555 |
def greet_user(profile: gr.OAuthProfile | None) -> tuple[str | None, str]:
|
| 556 |
if profile is None:
|
|
|
|
| 619 |
report_btn = gr.Button("Generate Report")
|
| 620 |
with gr.Row():
|
| 621 |
quiz_btn = gr.Button("Generate Quiz")
|
| 622 |
+
flashcards_btn = gr.Button("Generate Flashcards")
|
| 623 |
podcast_btn = gr.Button("Generate Podcast")
|
| 624 |
artifacts_json = gr.JSON(label="Generated Artifacts", value=_empty_artifacts_payload())
|
| 625 |
latest_report_file = gr.File(label="Latest Report (.md)", interactive=False)
|
| 626 |
latest_quiz_file = gr.File(label="Latest Quiz (.md)", interactive=False)
|
| 627 |
+
latest_flashcards_file = gr.File(label="Latest Flashcards (.md)", interactive=False)
|
| 628 |
latest_podcast_transcript_file = gr.File(label="Latest Podcast Transcript (.md)", interactive=False)
|
| 629 |
latest_podcast_audio = gr.Audio(label="Latest Podcast Audio (.mp3)", type="filepath", interactive=False)
|
| 630 |
|
|
|
|
| 694 |
artifacts_json,
|
| 695 |
latest_report_file,
|
| 696 |
latest_quiz_file,
|
| 697 |
+
latest_flashcards_file,
|
| 698 |
latest_podcast_transcript_file,
|
| 699 |
latest_podcast_audio,
|
| 700 |
],
|
|
|
|
| 707 |
artifacts_json,
|
| 708 |
latest_report_file,
|
| 709 |
latest_quiz_file,
|
| 710 |
+
latest_flashcards_file,
|
| 711 |
latest_podcast_transcript_file,
|
| 712 |
latest_podcast_audio,
|
| 713 |
],
|
|
|
|
| 719 |
artifacts_json,
|
| 720 |
latest_report_file,
|
| 721 |
latest_quiz_file,
|
| 722 |
+
latest_flashcards_file,
|
| 723 |
latest_podcast_transcript_file,
|
| 724 |
latest_podcast_audio,
|
| 725 |
],
|
|
|
|
| 767 |
artifacts_json,
|
| 768 |
latest_report_file,
|
| 769 |
latest_quiz_file,
|
| 770 |
+
latest_flashcards_file,
|
| 771 |
latest_podcast_transcript_file,
|
| 772 |
latest_podcast_audio,
|
| 773 |
status_box,
|
|
|
|
| 780 |
artifacts_json,
|
| 781 |
latest_report_file,
|
| 782 |
latest_quiz_file,
|
| 783 |
+
latest_flashcards_file,
|
| 784 |
latest_podcast_transcript_file,
|
| 785 |
latest_podcast_audio,
|
| 786 |
status_box,
|
|
|
|
| 793 |
artifacts_json,
|
| 794 |
latest_report_file,
|
| 795 |
latest_quiz_file,
|
| 796 |
+
latest_flashcards_file,
|
| 797 |
+
latest_podcast_transcript_file,
|
| 798 |
+
latest_podcast_audio,
|
| 799 |
+
status_box,
|
| 800 |
+
],
|
| 801 |
+
)
|
| 802 |
+
flashcards_btn.click(
|
| 803 |
+
generate_flashcards_artifact,
|
| 804 |
+
inputs=[user_id, notebook_selector, artifact_prompt, quiz_questions],
|
| 805 |
+
outputs=[
|
| 806 |
+
artifacts_json,
|
| 807 |
+
latest_report_file,
|
| 808 |
+
latest_quiz_file,
|
| 809 |
+
latest_flashcards_file,
|
| 810 |
latest_podcast_transcript_file,
|
| 811 |
latest_podcast_audio,
|
| 812 |
status_box,
|
|
|
|
| 819 |
artifacts_json,
|
| 820 |
latest_report_file,
|
| 821 |
latest_quiz_file,
|
| 822 |
+
latest_flashcards_file,
|
| 823 |
latest_podcast_transcript_file,
|
| 824 |
latest_podcast_audio,
|
| 825 |
status_box,
|
tests/test_api_artifacts.py
CHANGED
|
@@ -45,6 +45,12 @@ def test_artifact_endpoints_generate_list_and_download(monkeypatch, tmp_path: Pa
|
|
| 45 |
headers=AUTH_U1,
|
| 46 |
)
|
| 47 |
assert report.status_code == 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
podcast = client.post(
|
| 50 |
f"/api/notebooks/{nb.notebook_id}/artifacts/podcast",
|
|
@@ -62,6 +68,7 @@ def test_artifact_endpoints_generate_list_and_download(monkeypatch, tmp_path: Pa
|
|
| 62 |
assert listed.status_code == 200
|
| 63 |
payload = listed.json()
|
| 64 |
assert len(payload["reports"]) == 1
|
|
|
|
| 65 |
assert len(payload["podcasts"]) == 1
|
| 66 |
|
| 67 |
dl = client.get(
|
|
|
|
| 45 |
headers=AUTH_U1,
|
| 46 |
)
|
| 47 |
assert report.status_code == 200
|
| 48 |
+
flashcards = client.post(
|
| 49 |
+
f"/api/notebooks/{nb.notebook_id}/artifacts/flashcards",
|
| 50 |
+
json={"user_id": "u1", "num_questions": 6},
|
| 51 |
+
headers=AUTH_U1,
|
| 52 |
+
)
|
| 53 |
+
assert flashcards.status_code == 200
|
| 54 |
|
| 55 |
podcast = client.post(
|
| 56 |
f"/api/notebooks/{nb.notebook_id}/artifacts/podcast",
|
|
|
|
| 68 |
assert listed.status_code == 200
|
| 69 |
payload = listed.json()
|
| 70 |
assert len(payload["reports"]) == 1
|
| 71 |
+
assert len(payload["flashcards"]) == 1
|
| 72 |
assert len(payload["podcasts"]) == 1
|
| 73 |
|
| 74 |
dl = client.get(
|
tests/test_artifacts.py
CHANGED
|
@@ -34,15 +34,19 @@ def test_generate_report_quiz_and_list(monkeypatch, tmp_path: Path):
|
|
| 34 |
|
| 35 |
report = artifacts.generate_report(store, user_id="u1", notebook_id=nb.notebook_id, prompt="Focus")
|
| 36 |
quiz = artifacts.generate_quiz(store, user_id="u1", notebook_id=nb.notebook_id, num_questions=5)
|
|
|
|
| 37 |
|
| 38 |
assert report.artifact_type == "report"
|
| 39 |
assert quiz.artifact_type == "quiz"
|
|
|
|
| 40 |
assert Path(report.markdown_path).exists()
|
| 41 |
assert Path(quiz.markdown_path).exists()
|
|
|
|
| 42 |
|
| 43 |
listed = artifacts.list_artifacts(store, user_id="u1", notebook_id=nb.notebook_id)
|
| 44 |
assert len(listed.reports) == 1
|
| 45 |
assert len(listed.quizzes) == 1
|
|
|
|
| 46 |
assert listed.podcasts == []
|
| 47 |
|
| 48 |
|
|
|
|
| 34 |
|
| 35 |
report = artifacts.generate_report(store, user_id="u1", notebook_id=nb.notebook_id, prompt="Focus")
|
| 36 |
quiz = artifacts.generate_quiz(store, user_id="u1", notebook_id=nb.notebook_id, num_questions=5)
|
| 37 |
+
flashcards = artifacts.generate_flashcards(store, user_id="u1", notebook_id=nb.notebook_id, num_questions=6)
|
| 38 |
|
| 39 |
assert report.artifact_type == "report"
|
| 40 |
assert quiz.artifact_type == "quiz"
|
| 41 |
+
assert flashcards.artifact_type == "flashcards"
|
| 42 |
assert Path(report.markdown_path).exists()
|
| 43 |
assert Path(quiz.markdown_path).exists()
|
| 44 |
+
assert Path(flashcards.markdown_path).exists()
|
| 45 |
|
| 46 |
listed = artifacts.list_artifacts(store, user_id="u1", notebook_id=nb.notebook_id)
|
| 47 |
assert len(listed.reports) == 1
|
| 48 |
assert len(listed.quizzes) == 1
|
| 49 |
+
assert len(listed.flashcards) == 1
|
| 50 |
assert listed.podcasts == []
|
| 51 |
|
| 52 |
|