Spaces:
Build error
Build error
Update app.py
Browse files
app.py
CHANGED
|
@@ -152,32 +152,86 @@ def _normalize_depr_columns(df_in: pd.DataFrame) -> pd.DataFrame:
|
|
| 152 |
out = out[~out[["Begin BV","Depreciation","Accum Dep","End BV"]].isna().all(axis=1)].reset_index(drop=True)
|
| 153 |
return out
|
| 154 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 155 |
# ---------- Gradio callbacks ----------
|
| 156 |
def handle_docx(file):
|
| 157 |
if file is None:
|
| 158 |
-
return "(no file)", None, None
|
| 159 |
df_raw, header = _docx_to_table_and_text(file.name if hasattr(file, "name") else file)
|
| 160 |
params = _extract_params(header or "")
|
| 161 |
df_norm = _normalize_depr_columns(df_raw) if df_raw is not None else None
|
| 162 |
-
return header or "(no text found)", params, df_norm
|
| 163 |
|
| 164 |
def handle_image(img):
|
| 165 |
if img is None:
|
| 166 |
-
return "(no image)", None, None, None
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
from PIL import Image as PILImage
|
| 170 |
-
pil = PILImage.fromarray(img)
|
| 171 |
-
else:
|
| 172 |
-
pil = img
|
| 173 |
ocr_text = _image_to_text(pil)
|
| 174 |
params = _extract_params(ocr_text or "")
|
| 175 |
df_raw = _table_from_ocr_text(ocr_text or "")
|
| 176 |
df_norm = _normalize_depr_columns(df_raw) if df_raw is not None else None
|
| 177 |
-
return ocr_text or "(empty OCR)", params, df_raw, df_norm
|
|
|
|
| 178 |
|
| 179 |
# ---------- UI ----------
|
| 180 |
with gr.Blocks(title="Jerry • HW Intake (Echo)") as demo:
|
|
|
|
|
|
|
| 181 |
gr.Markdown("## Jerry (TA) – Homework Intake\nThis Space **only reads and echoes** your files.\nNext step will add solving & coaching.")
|
| 182 |
with gr.Tab("Upload .docx"):
|
| 183 |
docx_in = gr.File(file_types=[".docx"], label="Homework .docx")
|
|
@@ -196,6 +250,60 @@ with gr.Blocks(title="Jerry • HW Intake (Echo)") as demo:
|
|
| 196 |
norm_df = gr.Dataframe(label="Detected table (normalized)", interactive=False)
|
| 197 |
btn2.click(handle_image, inputs=img_in, outputs=[ocr_txt, params_json2, raw_df, norm_df])
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
gr.Markdown("— Echo mode finished. When this looks good, we’ll plug in the SL solver + coaching.")
|
| 200 |
|
| 201 |
if __name__ == "__main__":
|
|
|
|
| 152 |
out = out[~out[["Begin BV","Depreciation","Accum Dep","End BV"]].isna().all(axis=1)].reset_index(drop=True)
|
| 153 |
return out
|
| 154 |
|
| 155 |
+
# Monday Aug 11 New helpers
|
| 156 |
+
def build_sl_schedule(cost: float, salvage: float, life: int, start_year: int):
|
| 157 |
+
"""Return the straight-line depreciation schedule as a DataFrame."""
|
| 158 |
+
dep = (cost - salvage) / life
|
| 159 |
+
years = [start_year + i for i in range(life)]
|
| 160 |
+
begin_bv, dep_col, accum, end_bv = [], [], [], []
|
| 161 |
+
b = cost
|
| 162 |
+
acc = 0.0
|
| 163 |
+
for _ in years:
|
| 164 |
+
begin_bv.append(b)
|
| 165 |
+
dep_col.append(dep)
|
| 166 |
+
acc += dep
|
| 167 |
+
accum.append(acc)
|
| 168 |
+
b = b - dep
|
| 169 |
+
end_bv.append(b)
|
| 170 |
+
out = pd.DataFrame(
|
| 171 |
+
{
|
| 172 |
+
"Year": years,
|
| 173 |
+
"Begin BV": begin_bv,
|
| 174 |
+
"Depreciation": dep_col,
|
| 175 |
+
"Accum Dep": accum,
|
| 176 |
+
"End BV": end_bv,
|
| 177 |
+
}
|
| 178 |
+
)
|
| 179 |
+
return out
|
| 180 |
+
|
| 181 |
+
def audit_against_expected(expected: pd.DataFrame, actual: pd.DataFrame):
|
| 182 |
+
"""Row-by-row deltas (actual - expected). Assumes normalized col names."""
|
| 183 |
+
if actual is None or actual.empty:
|
| 184 |
+
return pd.DataFrame(), "No student table found to check."
|
| 185 |
+
# align only overlapping years
|
| 186 |
+
merged = expected.merge(
|
| 187 |
+
actual[["Year","Begin BV","Depreciation","Accum Dep","End BV"]],
|
| 188 |
+
on="Year", how="inner", suffixes=("_exp","_act")
|
| 189 |
+
)
|
| 190 |
+
if merged.empty:
|
| 191 |
+
return pd.DataFrame(), "No matching years between expected and uploaded table."
|
| 192 |
+
deltas = pd.DataFrame({"Year": merged["Year"]})
|
| 193 |
+
for c in ["Begin BV","Depreciation","Accum Dep","End BV"]:
|
| 194 |
+
deltas[c + " Δ"] = merged[f"{c}_act"] - merged[f"{c}_exp"]
|
| 195 |
+
# first mismatch helper
|
| 196 |
+
first_bad = None
|
| 197 |
+
for _, r in deltas.iterrows():
|
| 198 |
+
if any(abs(r[col]) > 1e-6 for col in deltas.columns if col.endswith("Δ")):
|
| 199 |
+
first_bad = int(r["Year"])
|
| 200 |
+
break
|
| 201 |
+
msg = (
|
| 202 |
+
"All good 🎉 Straight‑line matches your table."
|
| 203 |
+
if first_bad is None
|
| 204 |
+
else f"First mismatch at year {first_bad}. Remember: Dep=(Cost−Salvage)/Life and Accum_t=Accum_(t−1)+Dep."
|
| 205 |
+
)
|
| 206 |
+
return deltas, msg
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
|
| 210 |
# ---------- Gradio callbacks ----------
|
| 211 |
def handle_docx(file):
|
| 212 |
if file is None:
|
| 213 |
+
return "(no file)", None, None, {}, pd.DataFrame()
|
| 214 |
df_raw, header = _docx_to_table_and_text(file.name if hasattr(file, "name") else file)
|
| 215 |
params = _extract_params(header or "")
|
| 216 |
df_norm = _normalize_depr_columns(df_raw) if df_raw is not None else None
|
| 217 |
+
return header or "(no text found)", params, df_norm, params, (df_norm if df_norm is not None else pd.DataFrame())
|
| 218 |
|
| 219 |
def handle_image(img):
|
| 220 |
if img is None:
|
| 221 |
+
return "(no image)", None, None, None, {}, pd.DataFrame()
|
| 222 |
+
from PIL import Image as PILImage
|
| 223 |
+
pil = img if isinstance(img, PILImage.Image) else PILImage.fromarray(img)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 224 |
ocr_text = _image_to_text(pil)
|
| 225 |
params = _extract_params(ocr_text or "")
|
| 226 |
df_raw = _table_from_ocr_text(ocr_text or "")
|
| 227 |
df_norm = _normalize_depr_columns(df_raw) if df_raw is not None else None
|
| 228 |
+
return ocr_text or "(empty OCR)", params, df_raw, df_norm, params, (df_norm if df_norm is not None else pd.DataFrame())
|
| 229 |
+
|
| 230 |
|
| 231 |
# ---------- UI ----------
|
| 232 |
with gr.Blocks(title="Jerry • HW Intake (Echo)") as demo:
|
| 233 |
+
last_params = gr.State({})
|
| 234 |
+
last_table = gr.State(pd.DataFrame())
|
| 235 |
gr.Markdown("## Jerry (TA) – Homework Intake\nThis Space **only reads and echoes** your files.\nNext step will add solving & coaching.")
|
| 236 |
with gr.Tab("Upload .docx"):
|
| 237 |
docx_in = gr.File(file_types=[".docx"], label="Homework .docx")
|
|
|
|
| 250 |
norm_df = gr.Dataframe(label="Detected table (normalized)", interactive=False)
|
| 251 |
btn2.click(handle_image, inputs=img_in, outputs=[ocr_txt, params_json2, raw_df, norm_df])
|
| 252 |
|
| 253 |
+
with gr.Tab("Straight‑Line • Solve & Check"):
|
| 254 |
+
gr.Markdown("Enter params (auto-filled if detected) → build the correct SL schedule → compare to your uploaded table.")
|
| 255 |
+
with gr.Row():
|
| 256 |
+
in_cost = gr.Number(label="Cost", value=0.0)
|
| 257 |
+
in_salv = gr.Number(label="Salvage", value=0.0)
|
| 258 |
+
in_life = gr.Number(label="Life (years)", value=10, precision=0)
|
| 259 |
+
in_year = gr.Number(label="Start year", value=2025, precision=0)
|
| 260 |
+
btn_build = gr.Button("Build expected schedule")
|
| 261 |
+
expected_df = gr.Dataframe(label="Expected (SL) schedule", interactive=False)
|
| 262 |
+
btn_check = gr.Button("Check against uploaded table")
|
| 263 |
+
deltas_df = gr.Dataframe(label="Differences (student − expected)", interactive=False)
|
| 264 |
+
coach_txt = gr.Markdown()
|
| 265 |
+
|
| 266 |
+
def fill_from_state(p):
|
| 267 |
+
# prefill controls when we have parsed params
|
| 268 |
+
p = p or {}
|
| 269 |
+
return (
|
| 270 |
+
p.get("cost", 0.0),
|
| 271 |
+
p.get("salvage", 0.0),
|
| 272 |
+
p.get("life", 10),
|
| 273 |
+
p.get("start_year", pd.Timestamp.now().year),
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
def build_cb(cost, salv, life, year):
|
| 277 |
+
try:
|
| 278 |
+
df = build_sl_schedule(float(cost), float(salv), int(life), int(year))
|
| 279 |
+
except Exception as e:
|
| 280 |
+
return pd.DataFrame([{"error": str(e)}])
|
| 281 |
+
return df
|
| 282 |
+
|
| 283 |
+
def check_cb(cost, salv, life, year, table):
|
| 284 |
+
exp = build_sl_schedule(float(cost), float(salv), int(life), int(year))
|
| 285 |
+
deltas, msg = audit_against_expected(exp, table if isinstance(table, pd.DataFrame) else pd.DataFrame())
|
| 286 |
+
return deltas, msg
|
| 287 |
+
|
| 288 |
+
# Wire up
|
| 289 |
+
btn_build.click(build_cb, [in_cost, in_salv, in_life, in_year], [expected_df])
|
| 290 |
+
btn_check.click(check_cb, [in_cost, in_salv, in_life, in_year, last_table], [deltas_df, coach_txt])
|
| 291 |
+
|
| 292 |
+
btn1.click(handle_docx, inputs=docx_in,
|
| 293 |
+
outputs=[header_txt, params_json, table_df, last_params, last_table])
|
| 294 |
+
btn2.click(handle_image, inputs=img_in,
|
| 295 |
+
outputs=[ocr_txt, params_json2, raw_df, norm_df, last_params, last_table])
|
| 296 |
+
|
| 297 |
+
# When params state changes, prefill inputs
|
| 298 |
+
last_params.change(lambda p: fill_from_state(p), inputs=last_params,
|
| 299 |
+
outputs=[in_cost, in_salv, in_life, in_year])
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
# Auto-fill inputs whenever we parse new params
|
| 303 |
+
def prefill_inputs(p): return fill_from_state(p)
|
| 304 |
+
params_from_docx = gr.EventListener(last_params, triggers=["change"])
|
| 305 |
+
params_from_img = gr.EventListener(last_params, triggers=["change"])
|
| 306 |
+
|
| 307 |
gr.Markdown("— Echo mode finished. When this looks good, we’ll plug in the SL solver + coaching.")
|
| 308 |
|
| 309 |
if __name__ == "__main__":
|