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...",
        "",
        "",
        "",
        "",
        "",
        "",
        "",
    )