Spaces:
Running on Zero
Running on Zero
File size: 5,555 Bytes
0921a5d 46e02e0 0921a5d 46e02e0 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 | from __future__ import annotations
from pathlib import Path
import re
from typing import Any
from env.config import SUPPORTED_IMAGE_SUFFIXES
# Match headings that the tutoring model is instructed to emit.
_SECTION_MARKER_PATTERN = re.compile(
r"(?im)^[ \t]*(?:[-*][ \t]*)?(?:#{1,6}[ \t]*)?"
r"(?:\*\*)?(?:={2,}[ \t]*)*"
r"(?P<label>"
r"problem read|knowns?|strategy|worked steps?|check|next hint|parent note"
r")"
r"\b"
r"(?:[ \t]*={2,})*(?:\*\*)?[ \t]*(?::|-)?[ \t]*"
r"(?P<trailing>[^\n]*)$"
)
_SECTION_ORDER = (
"problem",
"knowns",
"strategy",
"steps",
"check",
"hint",
"parent",
)
_SECTION_DEFAULTS = {
"problem": "Upload a homework image or type the question to begin.",
"knowns": "- No givens identified yet.",
"strategy": "No strategy generated yet.",
"steps": "No worked steps generated yet.",
"check": "No answer check generated yet.",
"hint": "Ask for a hint after the first explanation.",
"parent": "Parent support note will appear here.",
}
def resolve_path(file_input: object | None) -> Path | None:
"""Normalizes Gradio file payload variants into a local path."""
# Empty upload components should let text or audio drive the request.
if not file_input:
return None
if isinstance(file_input, (list, tuple)):
for item in file_input:
path = resolve_path(item)
if path:
return path
return None
if isinstance(file_input, dict):
for key in ("path", "name", "orig_name"):
value = file_input.get(key)
if value:
return Path(str(value))
return None
return Path(str(file_input))
def validate_image_path(file_input: object | None) -> tuple[str | None, str]:
"""Returns a usable image path and a short validation message."""
# Accept common classroom photo formats only.
path = resolve_path(file_input)
if not path:
return None, "No image uploaded."
suffix = path.suffix.lower()
if suffix not in SUPPORTED_IMAGE_SUFFIXES:
return None, f"Unsupported image format: {suffix}."
if not path.exists():
return None, "Uploaded image was not found in the local runtime."
return str(path), f"Image accepted: {path.name}"
def stringify_content(content: Any) -> str:
"""Converts Gradio text, audio transcripts, and message payloads into prompt-safe text."""
# Plain textbox messages arrive as strings.
if content is None:
return ""
if isinstance(content, str):
return content.strip()
if isinstance(content, (list, tuple)):
parts = [stringify_content(item) for item in content]
return " ".join(part for part in parts if part).strip()
if isinstance(content, dict):
for key in ("text", "value", "path", "url", "name", "alt_text"):
value = content.get(key)
if value:
return stringify_content(value)
return ""
return str(content).strip()
def _canonical_section(label: str) -> str:
"""Maps model heading variants onto the app's fixed output cards."""
normalized = re.sub(r"[^a-z]+", " ", label.lower()).strip()
if "problem" in normalized:
return "problem"
if "known" in normalized:
return "knowns"
if "strategy" in normalized:
return "strategy"
if "worked" in normalized or "step" in normalized:
return "steps"
if "check" in normalized:
return "check"
if "hint" in normalized:
return "hint"
return "parent"
def parse_sections(response: str) -> tuple[str, str, str, str, str, str, str]:
"""Extracts tutoring sections from a structured model response."""
# Find candidate headings and keep defaults when a section is absent.
matches = list(_SECTION_MARKER_PATTERN.finditer(response))
sections = dict(_SECTION_DEFAULTS)
if not matches:
return (
sections["problem"],
sections["knowns"],
sections["strategy"],
sections["steps"],
sections["check"],
sections["hint"],
sections["parent"],
)
# Prefer the best ordered block of sections in the generation.
best_values: dict[str, str] = {}
best_count = -1
for start_index, _ in enumerate(matches):
values: dict[str, str] = {}
last_order_index = -1
for current_index in range(start_index, len(matches)):
current = matches[current_index]
section = _canonical_section(current.group("label"))
order_index = _SECTION_ORDER.index(section)
if order_index <= last_order_index:
break
next_start = (
matches[current_index + 1].start()
if current_index + 1 < len(matches)
else len(response)
)
value = "\n".join(
[current.group("trailing"), response[current.end() : next_start]]
).strip()
if value:
values[section] = value
last_order_index = order_index
if len(values) >= best_count:
best_values = values
best_count = len(values)
# Merge extracted values over stable UI defaults.
sections.update(best_values)
return (
sections["problem"],
sections["knowns"],
sections["strategy"],
sections["steps"],
sections["check"],
sections["hint"],
sections["parent"],
)
|