Kesheratmex commited on
Commit
f45be64
·
1 Parent(s): 011a229

Add PDF/MD/JSON reporting helpers and GPT‑OSS wrapper for strong analysis

Browse files
Files changed (1) hide show
  1. app.py +232 -2
app.py CHANGED
@@ -145,7 +145,216 @@ def _extract_path(d):
145
  return (d.get("path") if isinstance(d, dict) else d)
146
 
147
  # ────────────────────────────
148
- # Interfaz Gradio
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  # ────────────────────────────
150
  with gr.Blocks(title="Kesherat · Inspección de palas eólicas") as demo:
151
  gr.Markdown("## Inspección de palas eólicas con YOLOv8\n"
@@ -180,8 +389,29 @@ with gr.Blocks(title="Kesherat · Inspección de palas eólicas") as demo:
180
  txt_classes = gr.Textbox(label="Clases cargadas", interactive=False)
181
  btn_classes.click(fn=show_classes, outputs=txt_classes)
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  # Habilitar cola para ZeroGPU
184
- # Nota: en Gradio 4.x el parámetro es "max_size" / "default_concurrency_limit"; sin kwargs específicos, queue() basta.
185
  demo.queue()
186
 
187
  if __name__ == "__main__":
 
145
  return (d.get("path") if isinstance(d, dict) else d)
146
 
147
  # ────────────────────────────
148
+ # Helpers for multimodal reporting (PDF/MD/JSON)
149
+ # ────────────────────────────
150
+
151
+ def _write_pdf(path: str, title: str, narrative: str, frames):
152
+ if REPORTLAB_AVAILABLE:
153
+ c = canvas.Canvas(path, pagesize=A4)
154
+ width, height = A4
155
+ margin = 40
156
+ y = height - margin
157
+ c.setFont("Helvetica-Bold", 16)
158
+ c.drawString(margin, y, title)
159
+ y -= 30
160
+ c.setFont("Helvetica", 11)
161
+ for line in (narrative or "").splitlines():
162
+ if y < margin + 50:
163
+ c.showPage()
164
+ y = height - margin
165
+ c.setFont("Helvetica", 11)
166
+ c.drawString(margin, y, line)
167
+ y -= 16
168
+ y -= 10
169
+ c.setFont("Helvetica-Bold", 12)
170
+ c.drawString(margin, y, "Per-frame detections:")
171
+ y -= 18
172
+ c.setFont("Helvetica", 10)
173
+ for f in frames:
174
+ if y < margin + 80:
175
+ c.showPage()
176
+ y = height - margin
177
+ c.setFont("Helvetica", 10)
178
+ c.drawString(margin, y, f"Frame {f.get('frame_index')}:")
179
+ y -= 14
180
+ dets = f.get("detections", [])
181
+ if not dets:
182
+ c.drawString(margin + 12, y, "No detections")
183
+ y -= 12
184
+ else:
185
+ for d in dets:
186
+ line = f"- {d.get('label')} | conf={d.get('confidence')} | bbox={d.get('bbox')}"
187
+ c.drawString(margin + 12, y, line)
188
+ y -= 12
189
+ c.save()
190
+ else:
191
+ # Fallback: plain text file (saved with .pdf extension)
192
+ with open(path, "w", encoding="utf-8") as f:
193
+ f.write(title + "\n\n")
194
+ f.write((narrative or "") + "\n\n")
195
+ f.write("Per-frame detections:\n")
196
+ for fr in frames:
197
+ f.write(f"Frame {fr.get('frame_index')}:\n")
198
+ dets = fr.get("detections", [])
199
+ if not dets:
200
+ f.write(" No detections\n")
201
+ else:
202
+ for d in dets:
203
+ f.write(f" - {d}\n")
204
+
205
+ def _load_gptoss_wrapper():
206
+ """
207
+ Load the blade-inspection-demo/gptoss_wrapper.py module by filepath so we don't rely on package imports.
208
+ """
209
+ try:
210
+ base = os.path.dirname(__file__)
211
+ wrapper_path = os.path.join(base, "blade-inspection-demo", "gptoss_wrapper.py")
212
+ if not os.path.exists(wrapper_path):
213
+ # fallback: maybe file already at project root
214
+ wrapper_path = os.path.join(base, "gptoss_wrapper.py")
215
+ spec = importlib.util.spec_from_file_location("gptoss_wrapper", wrapper_path)
216
+ module = importlib.util.module_from_spec(spec)
217
+ spec.loader.exec_module(module)
218
+ return getattr(module, "GPTOSSWrapper", None)
219
+ except Exception:
220
+ return None
221
+
222
+ def _build_prompt(frames):
223
+ lines = []
224
+ lines.append("You are an expert inspection assistant for wind turbine blade images/videos.")
225
+ lines.append("Given per-frame detections (label, confidence, bbox), write a concise inspection report with:")
226
+ lines.append("- Summary of main findings")
227
+ lines.append("- Suggested severity (low/medium/high) when appropriate")
228
+ lines.append("- Recommended next steps for inspection/repair")
229
+ lines.append("")
230
+ lines.append("Frame detections follow:")
231
+ for f in frames:
232
+ fid = f.get("frame_index")
233
+ dets = f.get("detections", [])
234
+ if not dets:
235
+ lines.append(f"Frame {fid}: No detections")
236
+ else:
237
+ det_texts = []
238
+ for d in dets:
239
+ conf = d.get("confidence")
240
+ conf_s = f"{conf:.2f}" if isinstance(conf, float) else str(conf)
241
+ det_texts.append(f"{d.get('label')}({conf_s})")
242
+ lines.append(f"Frame {fid}: " + ", ".join(det_texts))
243
+ lines.append("")
244
+ lines.append("Produce the report in plain text, 6-10 short paragraphs.")
245
+ return "\n".join(lines)
246
+
247
+ def generar_analisis_fuerte(media_path):
248
+ """Generate strong analysis (PDF/MD/JSON) from a given media file path."""
249
+ if not media_path:
250
+ return {"status": "no_input", "report_pdf": None, "report_md": None, "report_json": None}
251
+
252
+ tmpdir = tempfile.mkdtemp()
253
+ frames = []
254
+
255
+ try:
256
+ ext = os.path.splitext(media_path)[1].lower()
257
+ # attempt to extract up to 3 frames/detections using the loaded YOLO model
258
+ if ext in [".mp4", ".mov", ".avi", ".mkv"]:
259
+ cap = cv2.VideoCapture(media_path)
260
+ idx = 0
261
+ grabbed = 0
262
+ while grabbed < 3:
263
+ ret, frame = cap.read()
264
+ if not ret:
265
+ break
266
+ tmpf = os.path.join(tmpdir, f"frame_{idx}.jpg")
267
+ cv2.imwrite(tmpf, frame)
268
+ results = model.predict(source=tmpf, conf=0.25, iou=0.45)
269
+ dets = []
270
+ if results and len(results) > 0:
271
+ for box in results[0].boxes:
272
+ try:
273
+ cls_id = int(box.cls[0])
274
+ label = model.names[cls_id]
275
+ except Exception:
276
+ label = "object"
277
+ try:
278
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
279
+ except Exception:
280
+ x1 = y1 = x2 = y2 = 0
281
+ try:
282
+ confv = float(box.conf[0])
283
+ except Exception:
284
+ confv = None
285
+ dets.append({"label": label, "confidence": confv, "bbox": [x1, y1, x2, y2]})
286
+ frames.append({"frame_index": idx, "detections": dets})
287
+ idx += 1
288
+ grabbed += 1
289
+ cap.release()
290
+ else:
291
+ # single image
292
+ results = model.predict(source=media_path, conf=0.25, iou=0.45)
293
+ dets = []
294
+ if results and len(results) > 0:
295
+ for box in results[0].boxes:
296
+ try:
297
+ cls_id = int(box.cls[0])
298
+ label = model.names[cls_id]
299
+ except Exception:
300
+ label = "object"
301
+ try:
302
+ x1, y1, x2, y2 = map(int, box.xyxy[0])
303
+ except Exception:
304
+ x1 = y1 = x2 = y2 = 0
305
+ try:
306
+ confv = float(box.conf[0])
307
+ except Exception:
308
+ confv = None
309
+ dets.append({"label": label, "confidence": confv, "bbox": [x1, y1, x2, y2]})
310
+ frames.append({"frame_index": 0, "detections": dets})
311
+
312
+ prompt = _build_prompt(frames)
313
+ GPTClass = _load_gptoss_wrapper()
314
+ narrative = None
315
+ if GPTClass:
316
+ try:
317
+ wrapper = GPTClass(model="gpt-oss-120")
318
+ narrative = wrapper.generate(prompt)
319
+ except Exception as e:
320
+ narrative = f"(GPT call failed) {e}"
321
+ else:
322
+ narrative = "(GPT wrapper unavailable) Fallback summary:\n"
323
+ counts = {}
324
+ for f in frames:
325
+ for d in f.get("detections", []):
326
+ counts[d["label"]] = counts.get(d["label"], 0) + 1
327
+ narrative += "Detected classes: " + ", ".join([f"{k}({v})" for k, v in counts.items()]) if counts else "No detections"
328
+
329
+ # Write Markdown
330
+ report_md = os.path.join(tmpdir, "report.md")
331
+ with open(report_md, "w", encoding="utf-8") as md:
332
+ md.write("# Informe de inspección (Generar analisis fuerte)\n\n")
333
+ md.write(narrative or "Sin narrativa disponible.\n\n")
334
+ md.write("\n## Per-frame detections\n\n")
335
+ for f in frames:
336
+ md.write(f"- Frame {f.get('frame_index')}: ")
337
+ dets = f.get("detections", [])
338
+ if not dets:
339
+ md.write("No detections\n")
340
+ else:
341
+ md.write("; ".join([f\"{d['label']}({d['confidence']}) bbox={d['bbox']}\" for d in dets]) + "\n")
342
+
343
+ # Write JSON
344
+ report_json = os.path.join(tmpdir, "report.json")
345
+ with open(report_json, "w", encoding="utf-8") as jf:
346
+ json.dump({"narrative": narrative, "frames": frames}, jf, indent=2)
347
+
348
+ # Write PDF
349
+ report_pdf = os.path.join(tmpdir, "report.pdf")
350
+ _write_pdf(report_pdf, "Informe de inspección - Generar analisis fuerte", narrative, frames)
351
+
352
+ return {"status": "done", "report_pdf": report_pdf, "report_md": report_md, "report_json": report_json}
353
+ except Exception as e:
354
+ return {"status": f"error: {e}", "report_pdf": None, "report_md": None, "report_json": None}
355
+
356
+ # ────────────────────────────
357
+ # Interfaz Gradio (extendida)
358
  # ────────────────────────────
359
  with gr.Blocks(title="Kesherat · Inspección de palas eólicas") as demo:
360
  gr.Markdown("## Inspección de palas eólicas con YOLOv8\n"
 
389
  txt_classes = gr.Textbox(label="Clases cargadas", interactive=False)
390
  btn_classes.click(fn=show_classes, outputs=txt_classes)
391
 
392
+ # New reporting outputs and button
393
+ btn_report = gr.Button("Generar analisis fuerte")
394
+ status = gr.Textbox(label="Estado", interactive=False)
395
+ pdf_out = gr.File(label="Reporte PDF")
396
+ md_out = gr.File(label="Reporte Markdown")
397
+ json_out = gr.File(label="Reporte JSON")
398
+
399
+ def _on_report(vid, img):
400
+ path = None
401
+ # gr.Video returns path-like; gr.Image type="filepath" returns path
402
+ if vid:
403
+ path = vid
404
+ elif img:
405
+ # if gr.Image returns an object with .name (local runs)
406
+ path = img if isinstance(img, str) else getattr(img, "name", None)
407
+ if not path:
408
+ return "No media provided", None, None, None
409
+ res = generar_analisis_fuerte(path)
410
+ return res.get("status", "error"), (res.get("report_pdf") if res.get("report_pdf") else None), (res.get("report_md") if res.get("report_md") else None), (res.get("report_json") if res.get("report_json") else None)
411
+
412
+ btn_report.click(fn=_on_report, inputs=[video_input, image_input], outputs=[status, pdf_out, md_out, json_out])
413
+
414
  # Habilitar cola para ZeroGPU
 
415
  demo.queue()
416
 
417
  if __name__ == "__main__":