File size: 5,999 Bytes
0921a5d cb14994 0921a5d cb14994 0921a5d 73da37e 0921a5d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 | from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
try:
import spaces
except ImportError:
# Use a no-op GPU decorator during local development.
class _LocalSpacesFallback:
@staticmethod
def GPU(
duration: int = 30,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
def decorator(function: Callable[..., Any]) -> Callable[..., Any]:
return function
return decorator
spaces = _LocalSpacesFallback()
from core.inference import run_tutor_inference, transcribe_audio
from core.parser import parse_sections, stringify_content, validate_image_path
from env.config import MODEL_ID, PARAMETER_COUNT, QUESTION_LIMIT, TRANSCRIPT_LIMIT
# The tutor prompt keeps the app educational rather than answer-dumping.
TUTOR_SYSTEM_PROMPT = (
"You are Pocket Tutor, a patient multimodal homework coach. "
"Help the learner understand the problem without shaming them or simply dumping final answers. "
"For math and science, show compact steps and check the answer. "
"For writing or reading, explain the reasoning and offer a revision or clue. "
"If the uploaded image is unclear, say what information is missing. "
"Do not solve active graded tests, exams, or requests to cheat; offer study guidance instead."
)
@dataclass(frozen=True)
class TutorReport:
"""Structure containing the prompt context, execution logs, and parsed tutoring result."""
student_context: str
model_path: str
problem: str
knowns: str
strategy: str
steps: str
check: str
hint: str
parent: str
def build_tutor_prompt(
question: str,
transcript: str,
grade_band: str,
help_mode: str,
image_status: str,
) -> str:
"""Builds the multimodal tutoring prompt expected by the fine-tuned adapter."""
combined_question = "\n".join(
part for part in [question.strip(), transcript.strip()] if part
).strip()
return f"""{TUTOR_SYSTEM_PROMPT}
Student grade band: {grade_band}
Help mode: {help_mode}
Image status: {image_status}
Return exactly these sections:
Important:
- Output all seven sections every time, in the exact order below.
- Keep each section concise, but do not omit any section.
- Do not stop after WORKED STEPS. You must also fill CHECK, NEXT HINT, and PARENT NOTE.
- The final section must always be PARENT NOTE and must be a complete single sentence.
- If the answer is partial, say so inside WORKED STEPS and continue to the later sections.
=== PROBLEM READ ===
[Briefly restate what the learner is asking.]
=== KNOWNS ===
- [List useful givens, facts, or constraints.]
=== STRATEGY ===
[Explain the plan in 2-4 sentences.]
=== WORKED STEPS ===
[Give concise step-by-step help. For hint mode, stop before the final answer.]
=== CHECK ===
[Check units, logic, or answer quality.]
=== NEXT HINT ===
[Ask one short follow-up question or give the smallest next hint.]
=== PARENT NOTE ===
[One complete sentence a parent/tutor can use to support the learner.]
Learner question:
{combined_question[:QUESTION_LIMIT] or "The learner uploaded an image and did not type a question."}
"""
def analyze_homework(
image_file: object | None,
question: Any,
audio_path: object | None,
grade_band: str,
help_mode: str,
) -> TutorReport:
"""Orchestrates image validation, speech transcription, inference, and parsing."""
# Convert text and microphone input before building the model prompt.
typed_question = stringify_content(question)[:QUESTION_LIMIT]
transcript, transcript_log = transcribe_audio(audio_path)
transcript = transcript[:TRANSCRIPT_LIMIT]
image_path, image_status = validate_image_path(image_file)
# Build a traceable prompt that matches the Modal training format.
prompt = build_tutor_prompt(
typed_question,
transcript,
grade_band,
help_mode,
image_status,
)
response, inference_log = run_tutor_inference(prompt, image_path)
sections = parse_sections(response)
# Keep diagnostics clear but never hide a model-unavailable path.
model_path = "\n".join(
[
f"Primary model: {MODEL_ID}",
f"Parameters: {PARAMETER_COUNT}",
"Execution flow: local Space runtime; no external answer API",
"---",
image_status,
transcript_log,
inference_log,
]
)
student_context = "\n".join(
part
for part in [
f"Grade band: {grade_band}",
f"Help mode: {help_mode}",
f"Typed question: {typed_question}" if typed_question else "",
f"Voice transcript: {transcript}" if transcript else "",
image_status,
]
if part
)
return TutorReport(student_context, model_path, *sections)
@spaces.GPU(duration=45)
def analyze_homework_ui(
image_file: object | None,
question: Any,
audio_path: object | None,
grade_band: str,
help_mode: str,
) -> tuple[str, str, str, str, str, str, str, str, str]:
"""Gradio-compatible GPU entry point for tutoring analysis."""
# Convert the report into stable component outputs.
report = analyze_homework(image_file, question, audio_path, grade_band, help_mode)
return (
report.student_context,
report.model_path,
report.problem,
report.knowns,
report.strategy,
report.steps,
report.check,
report.hint,
report.parent,
)
def reset_outputs() -> tuple[str, str, str, str, str, str, str, str, str]:
"""Clears visible outputs before a fresh tutoring run."""
# Keep click feedback immediate while the model loads.
return (
"Preparing tutoring context...",
"Starting local model execution...",
"",
"",
"",
"",
"",
"",
"",
)
|