Spaces:
Sleeping
Sleeping
| import fitz # PyMuPDF | |
| import json | |
| import os | |
| import io | |
| import re | |
| import tempfile | |
| from docx import Document | |
| from docx.shared import Inches, Pt, RGBColor | |
| from docx.enum.text import WD_ALIGN_PARAGRAPH | |
| from docx.enum.table import WD_TABLE_ALIGNMENT | |
| from docx.oxml.ns import qn | |
| from docx.oxml import OxmlElement | |
| import gradio as gr | |
| from datetime import datetime | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # 1. PDF EXTRACTION | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def extract_pdf_content(pdf_path): | |
| """Extract text and images from a PDF, grouped by page.""" | |
| doc = fitz.open(pdf_path) | |
| pages_text = [] | |
| images = [] # list of (page_num, image_bytes, img_index) | |
| for page_num, page in enumerate(doc): | |
| text = page.get_text("text") | |
| pages_text.append(text) | |
| # Extract embedded images | |
| img_list = page.get_images(full=True) | |
| for img_idx, img_info in enumerate(img_list): | |
| xref = img_info[0] | |
| try: | |
| base_image = doc.extract_image(xref) | |
| img_bytes = base_image["image"] | |
| img_ext = base_image["ext"] | |
| if len(img_bytes) > 5000: # Skip tiny icons | |
| images.append({ | |
| "page": page_num, | |
| "bytes": img_bytes, | |
| "ext": img_ext, | |
| "xref": xref | |
| }) | |
| except Exception: | |
| continue | |
| full_text = "\n".join(pages_text) | |
| doc.close() | |
| return full_text, images | |
| def parse_thermal_data(thermal_text): | |
| """ | |
| Parse Bosch GTC 400C thermal report into structured readings. | |
| Returns a formatted summary string for groq. | |
| """ | |
| readings = [] | |
| pattern = re.compile( | |
| r'Thermal image\s*:\s*(\S+\.JPG).*?' | |
| r'Hotspot\s*:\s*([\d.]+)\s*Β°C.*?' | |
| r'Coldspot\s*:\s*([\d.]+)\s*Β°C', | |
| re.DOTALL | re.IGNORECASE | |
| ) | |
| for i, match in enumerate(pattern.finditer(thermal_text), 1): | |
| filename, hotspot, coldspot = match.groups() | |
| delta = round(float(hotspot) - float(coldspot), 1) | |
| if delta >= 5: | |
| level = "HIGH moisture" | |
| elif delta >= 3: | |
| level = "MODERATE moisture" | |
| else: | |
| level = "LOW/trace moisture" | |
| readings.append( | |
| f"Reading {i} ({filename}): Hotspot={hotspot}Β°C, Coldspot={coldspot}Β°C, Delta={delta}Β°C β {level}" | |
| ) | |
| if not readings: | |
| return thermal_text[:8000] | |
| summary = f"THERMAL REPORT β {len(readings)} readings, Bosch GTC 400C, 27/09/2022\n" | |
| summary += "Ambient ~23Β°C. Coldspots below ambient = moisture confirmed.\n\n" | |
| summary += "\n".join(readings) | |
| return summary | |
| def parse_impacted_areas_from_text(text): | |
| """ | |
| Parse the structured impacted areas from the UrbanRoof report. | |
| Returns a list of dicts with area name, negative description, positive description. | |
| """ | |
| areas = [] | |
| # Extract Summary Table section | |
| summary_match = re.search( | |
| r'SUMMARY TABLE(.*?)(?:Appendix|$)', text, re.DOTALL | re.IGNORECASE | |
| ) | |
| summary_text = summary_match.group(1) if summary_match else "" | |
| # Parse numbered rows from summary table | |
| # Pattern: number followed by description text | |
| rows = re.findall( | |
| r'(\d+)\s+(Observed[^\n]+(?:\n(?!\d).*)*)', | |
| summary_text | |
| ) | |
| for row_num, row_text in rows: | |
| row_text = row_text.strip().replace('\n', ' ') | |
| areas.append({ | |
| "point": row_num, | |
| "description": row_text | |
| }) | |
| # Also extract impacted area sections for richer data | |
| area_sections = re.findall( | |
| r'Impacted Area (\d+)\s*Negative side Description\s*([^\n]+(?:\n(?!Positive|Impacted).*)*)' | |
| r'.*?Positive side Description\s*([^\n]+(?:\n(?!Negative|Impacted).*)*)', | |
| text, re.DOTALL | |
| ) | |
| area_details = [] | |
| for match in area_sections: | |
| area_num, neg_desc, pos_desc = match | |
| area_details.append({ | |
| "area_num": area_num.strip(), | |
| "negative": neg_desc.strip().replace('\n', ' '), | |
| "positive": pos_desc.strip().replace('\n', ' ') | |
| }) | |
| return areas, area_details | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # 2. groq AI ANALYSIS | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| DDR_PROMPT = """You are an expert civil engineer analyzing a property inspection report and thermal imaging report from UrbanRoof. | |
| INSPECTION REPORT TEXT: | |
| {inspection_text} | |
| {thermal_section} | |
| THERMAL DATA INTERPRETATION GUIDE: | |
| - The thermal report uses a Bosch GTC 400C infrared camera (emissivity 0.94) | |
| - Cold spots (blue/cyan in thermal images) = moisture/dampness presence | |
| - A coldspot significantly below ambient (23Β°C) confirms active moisture | |
| - Temperature delta (hotspot - coldspot) > 4Β°C indicates significant moisture | |
| - Readings taken same day as inspection: 27/09/2022 | |
| Based ONLY on the information in the reports above, generate a structured Detailed Diagnostic Report (DDR). | |
| STRICT RULES: | |
| - Do NOT invent any facts not present in the documents | |
| - If information is missing β write "Not Available" | |
| - If thermal data conflicts with visual inspection β note the conflict explicitly | |
| - Use thermal readings to CONFIRM or QUALIFY the severity of each impacted area | |
| - Return ONLY valid JSON with no markdown fences, no preamble, no explanation | |
| Return this exact JSON structure: | |
| {{ | |
| "property_summary": {{ | |
| "property_type": "...", | |
| "floors": "...", | |
| "inspection_date": "...", | |
| "inspected_by": "...", | |
| "overall_score": "...", | |
| "flagged_items": "...", | |
| "previous_structural_audit": "...", | |
| "previous_repair_work": "..." | |
| }}, | |
| "executive_summary": "2-3 sentence summary combining inspection findings with thermal confirmation", | |
| "area_wise_observations": [ | |
| {{ | |
| "point_no": "1", | |
| "area": "area name (e.g. Hall, Bedroom)", | |
| "impacted_side_observation": "what was observed on the negative/impacted side", | |
| "source_side_observation": "what was found on the positive/exposed side (root cause location)", | |
| "thermal_finding": "thermal camera reading for this area: hotspot temp, coldspot temp, delta, and what it indicates. Write 'Not Available' if no thermal reading correlates to this area.", | |
| "severity": "High/Medium/Low β use thermal delta to inform severity: >5Β°C delta = High, 3-5Β°C = Medium, <3Β°C = Low" | |
| }} | |
| ], | |
| "probable_root_cause": "Detailed root cause analysis combining visual inspection and thermal evidence", | |
| "severity_assessment": {{ | |
| "overall": "High/Medium/Low", | |
| "reasoning": "Why this overall severity β cite both visual and thermal evidence", | |
| "breakdown": [ | |
| {{"issue": "issue name", "severity": "High/Medium/Low", "basis": "combined visual + thermal basis"}} | |
| ] | |
| }}, | |
| "recommended_actions": [ | |
| {{ | |
| "priority": "Immediate/Short-term/Long-term", | |
| "action": "specific action to take", | |
| "area": "which area this applies to" | |
| }} | |
| ], | |
| "checklist_findings": {{ | |
| "wc_checklist": "summary of WC checklist findings", | |
| "external_wall_checklist": "summary of external wall findings", | |
| "structural_condition": "summary of structural condition" | |
| }}, | |
| "thermal_summary": "Overall interpretation of the 30 thermal readings: moisture extent, worst affected areas by thermal delta, whether thermal confirms or contradicts visual findings", | |
| "additional_notes": "Any other important observations", | |
| "missing_or_unclear": ["list any information that was missing or unclear in the reports"] | |
| }}""" | |
| def analyze_with_groq(api_key, inspection_text, thermal_text=""): | |
| from groq import Groq | |
| client = Groq(api_key=api_key) | |
| thermal_section = "" | |
| if thermal_text and thermal_text.strip(): | |
| parsed_thermal = parse_thermal_data(thermal_text) | |
| thermal_section = f"THERMAL REPORT TEXT:\n{parsed_thermal[:10000]}" | |
| prompt = DDR_PROMPT.format( | |
| inspection_text=inspection_text[:15000], | |
| thermal_section=thermal_section | |
| ) | |
| response = client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=[{"role": "user", "content": prompt}], | |
| response_format={"type": "json_object"} | |
| ) | |
| raw = response.choices[0].message.content.strip() | |
| try: | |
| return json.loads(raw) | |
| except json.JSONDecodeError as e: | |
| json_match = re.search(r'\{.*\}', raw, re.DOTALL) | |
| if json_match: | |
| return json.loads(json_match.group()) | |
| raise ValueError(f"Could not parse response as JSON: {e}\n\nRaw: {raw[:500]}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # 3. DOCX REPORT BUILDER | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def set_cell_bg(cell, hex_color): | |
| """Set table cell background color.""" | |
| tc = cell._tc | |
| tcPr = tc.get_or_add_tcPr() | |
| shd = OxmlElement('w:shd') | |
| shd.set(qn('w:val'), 'clear') | |
| shd.set(qn('w:color'), 'auto') | |
| shd.set(qn('w:fill'), hex_color) | |
| tcPr.append(shd) | |
| def add_heading(doc, text, level=1, color=None): | |
| """Add a styled heading.""" | |
| heading = doc.add_heading(text, level=level) | |
| if color: | |
| for run in heading.runs: | |
| run.font.color.rgb = RGBColor(*color) | |
| return heading | |
| def add_colored_paragraph(doc, text, bold=False, color=None, size=None): | |
| """Add a paragraph with optional styling.""" | |
| para = doc.add_paragraph() | |
| run = para.add_run(text) | |
| run.bold = bold | |
| if color: | |
| run.font.color.rgb = RGBColor(*color) | |
| if size: | |
| run.font.size = Pt(size) | |
| return para | |
| def build_ddr_document(ddr_data, images, output_path): | |
| """Build the full DDR Word document from parsed data and images.""" | |
| doc = Document() | |
| # Page margins | |
| for section in doc.sections: | |
| section.top_margin = Inches(1) | |
| section.bottom_margin = Inches(1) | |
| section.left_margin = Inches(1.2) | |
| section.right_margin = Inches(1.2) | |
| # ββ COVER / HEADER ββ | |
| title = doc.add_heading('DETAILED DIAGNOSTIC REPORT', 0) | |
| title.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| title.runs[0].font.color.rgb = RGBColor(0x8B, 0x1A, 0x1A) | |
| subtitle = doc.add_paragraph('Property Inspection Analysis β UrbanRoof') | |
| subtitle.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| subtitle.runs[0].bold = True | |
| subtitle.runs[0].font.size = Pt(13) | |
| doc.add_paragraph(f'Report Generated: {datetime.now().strftime("%d %B %Y")}').alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| doc.add_paragraph('') | |
| # ββ SECTION 1: PROPERTY SUMMARY ββ | |
| prop = ddr_data.get('property_summary', {}) | |
| add_heading(doc, '1. Property Summary', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| summary_table = doc.add_table(rows=0, cols=2) | |
| summary_table.style = 'Table Grid' | |
| fields = [ | |
| ('Property Type', prop.get('property_type', 'Not Available')), | |
| ('Number of Floors', prop.get('floors', 'Not Available')), | |
| ('Inspection Date', prop.get('inspection_date', 'Not Available')), | |
| ('Inspected By', prop.get('inspected_by', 'Not Available')), | |
| ('Overall Score', prop.get('overall_score', 'Not Available')), | |
| ('Flagged Items', prop.get('flagged_items', 'Not Available')), | |
| ('Previous Structural Audit', prop.get('previous_structural_audit', 'Not Available')), | |
| ('Previous Repair Work', prop.get('previous_repair_work', 'Not Available')), | |
| ] | |
| for label, value in fields: | |
| row = summary_table.add_row() | |
| row.cells[0].text = label | |
| row.cells[0].paragraphs[0].runs[0].bold = True | |
| set_cell_bg(row.cells[0], 'F2F2F2') | |
| row.cells[1].text = str(value) | |
| doc.add_paragraph('') | |
| # ββ SECTION 2: EXECUTIVE SUMMARY ββ | |
| add_heading(doc, '2. Executive Summary', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| doc.add_paragraph(ddr_data.get('executive_summary', 'Not Available')) | |
| doc.add_paragraph('') | |
| # ββ SECTION 3: AREA-WISE OBSERVATIONS ββ | |
| add_heading(doc, '3. Area-Wise Observations', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| observations = ddr_data.get('area_wise_observations', []) | |
| if observations: | |
| obs_table = doc.add_table(rows=1, cols=6) | |
| obs_table.style = 'Table Grid' | |
| # Header row | |
| headers = ['Point', 'Area', 'Observed Damage (Impacted Side)', 'Root Source (Exposed Side)', 'Thermal Finding', 'Severity'] | |
| hdr_row = obs_table.rows[0] | |
| for i, hdr in enumerate(headers): | |
| cell = hdr_row.cells[i] | |
| cell.text = hdr | |
| cell.paragraphs[0].runs[0].bold = True | |
| set_cell_bg(cell, '8B1A1A') | |
| cell.paragraphs[0].runs[0].font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) | |
| # Data rows | |
| severity_colors = {'High': 'FFE0E0', 'Medium': 'FFF3CD', 'Low': 'E8F5E9'} | |
| for obs in observations: | |
| row = obs_table.add_row() | |
| row.cells[0].text = str(obs.get('point_no', '')) | |
| row.cells[1].text = obs.get('area', '') | |
| row.cells[2].text = obs.get('impacted_side_observation', '') | |
| row.cells[3].text = obs.get('source_side_observation', '') | |
| row.cells[4].text = obs.get('thermal_finding', 'Not Available') | |
| severity = obs.get('severity', 'Medium') | |
| row.cells[5].text = severity | |
| bg = severity_colors.get(severity, 'FFFFFF') | |
| set_cell_bg(row.cells[5], bg) | |
| # Add first few inspection photos after observations | |
| doc.add_paragraph('') | |
| if images: | |
| add_heading(doc, 'Inspection Photographs β Impacted Areas', level=2) | |
| # Add photos in groups of 2 per row | |
| photo_count = 0 | |
| for img_data in images[:12]: # First 12 photos (impacted area shots) | |
| try: | |
| img_stream = io.BytesIO(img_data['bytes']) | |
| doc.add_picture(img_stream, width=Inches(2.8)) | |
| photo_count += 1 | |
| except Exception: | |
| continue | |
| doc.add_paragraph('') | |
| # ββ SECTION 4: ROOT CAUSE ANALYSIS ββ | |
| add_heading(doc, '4. Probable Root Cause', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| doc.add_paragraph(ddr_data.get('probable_root_cause', 'Not Available')) | |
| doc.add_paragraph('') | |
| # ββ SECTION 5: SEVERITY ASSESSMENT ββ | |
| add_heading(doc, '5. Severity Assessment', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| sev = ddr_data.get('severity_assessment', {}) | |
| overall_sev = sev.get('overall', 'Not Available') | |
| # Overall severity box | |
| sev_para = doc.add_paragraph() | |
| sev_run = sev_para.add_run(f'Overall Severity: {overall_sev}') | |
| sev_run.bold = True | |
| sev_run.font.size = Pt(13) | |
| color_map = {'High': RGBColor(0xCC, 0x00, 0x00), 'Medium': RGBColor(0xFF, 0x8C, 0x00), 'Low': RGBColor(0x00, 0x80, 0x00)} | |
| sev_run.font.color.rgb = color_map.get(overall_sev, RGBColor(0, 0, 0)) | |
| doc.add_paragraph(sev.get('reasoning', '')) | |
| breakdown = sev.get('breakdown', []) | |
| if breakdown: | |
| sev_table = doc.add_table(rows=1, cols=3) | |
| sev_table.style = 'Table Grid' | |
| hdr_cells = sev_table.rows[0].cells | |
| for i, h in enumerate(['Issue', 'Severity', 'Basis']): | |
| hdr_cells[i].text = h | |
| hdr_cells[i].paragraphs[0].runs[0].bold = True | |
| set_cell_bg(hdr_cells[i], 'F2F2F2') | |
| for item in breakdown: | |
| row = sev_table.add_row() | |
| row.cells[0].text = item.get('issue', '') | |
| row.cells[1].text = item.get('severity', '') | |
| row.cells[2].text = item.get('basis', '') | |
| doc.add_paragraph('') | |
| # ββ SECTION 6: RECOMMENDED ACTIONS ββ | |
| add_heading(doc, '6. Recommended Actions', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| actions = ddr_data.get('recommended_actions', []) | |
| if actions: | |
| act_table = doc.add_table(rows=1, cols=3) | |
| act_table.style = 'Table Grid' | |
| priority_cells = act_table.rows[0].cells | |
| for i, h in enumerate(['Priority', 'Recommended Action', 'Area']): | |
| priority_cells[i].text = h | |
| priority_cells[i].paragraphs[0].runs[0].bold = True | |
| set_cell_bg(priority_cells[i], '8B1A1A') | |
| priority_cells[i].paragraphs[0].runs[0].font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF) | |
| priority_colors = {'Immediate': 'FFE0E0', 'Short-term': 'FFF3CD', 'Long-term': 'E8F5E9'} | |
| for action in actions: | |
| row = act_table.add_row() | |
| priority = action.get('priority', '') | |
| row.cells[0].text = priority | |
| row.cells[1].text = action.get('action', '') | |
| row.cells[2].text = action.get('area', '') | |
| set_cell_bg(row.cells[0], priority_colors.get(priority, 'FFFFFF')) | |
| doc.add_paragraph('') | |
| # ββ SECTION 7: CHECKLIST FINDINGS ββ | |
| add_heading(doc, '7. Checklist Findings', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| checklist = ddr_data.get('checklist_findings', {}) | |
| for key, label in [('wc_checklist', 'WC Checklist'), ('external_wall_checklist', 'External Wall'), ('structural_condition', 'Structural Condition')]: | |
| if checklist.get(key): | |
| add_heading(doc, label, level=2) | |
| doc.add_paragraph(checklist[key]) | |
| # Additional source-side photos | |
| if len(images) > 12: | |
| doc.add_paragraph('') | |
| add_heading(doc, 'Inspection Photographs β Source Areas', level=2) | |
| for img_data in images[12:24]: | |
| try: | |
| img_stream = io.BytesIO(img_data['bytes']) | |
| doc.add_picture(img_stream, width=Inches(2.8)) | |
| except Exception: | |
| continue | |
| # ββ THERMAL SUMMARY ββ | |
| thermal_summary = ddr_data.get('thermal_summary', '') | |
| if thermal_summary and thermal_summary != 'Not Available': | |
| doc.add_paragraph('') | |
| add_heading(doc, '8. Thermal Imaging Summary', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| doc.add_paragraph(thermal_summary) | |
| # ββ SECTION 8/9: ADDITIONAL NOTES & MISSING INFO ββ | |
| doc.add_paragraph('') | |
| add_heading(doc, '9. Additional Notes', level=1, color=(0x8B, 0x1A, 0x1A)) | |
| doc.add_paragraph(ddr_data.get('additional_notes', 'None')) | |
| missing = ddr_data.get('missing_or_unclear', []) | |
| if missing: | |
| add_heading(doc, 'Missing or Unclear Information', level=2) | |
| for item in missing: | |
| para = doc.add_paragraph(style='List Bullet') | |
| para.add_run(item) | |
| doc.save(output_path) | |
| return output_path | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| # 4. GRADIO UI | |
| # βββββββββββββββββββββββββββββββββββββββββββββ | |
| def generate_ddr(api_key, inspection_pdf, thermal_pdf): | |
| """Main pipeline: PDF β AI analysis β DDR Word document.""" | |
| if not api_key or not api_key.strip(): | |
| return None, "β Please enter your groq API key. Get one free at: https://aistudio.google.com/app/apikey" | |
| if inspection_pdf is None: | |
| return None, "β Please upload an Inspection Report PDF." | |
| try: | |
| # Step 1: Extract inspection PDF | |
| yield None, "π Step 1/4: Extracting inspection report..." | |
| inspection_text, images = extract_pdf_content(inspection_pdf) | |
| if len(inspection_text.strip()) < 100: | |
| return None, "β Could not extract text from the inspection PDF. It may be a scanned image without OCR." | |
| # Step 2: Extract thermal PDF (optional) | |
| thermal_text = "" | |
| if thermal_pdf is not None: | |
| yield None, "π Step 2/4: Extracting thermal report..." | |
| thermal_text, thermal_images = extract_pdf_content(thermal_pdf) | |
| images = images + thermal_images | |
| else: | |
| yield None, "β οΈ No thermal report provided β proceeding with inspection report only." | |
| # Step 3: AI analysis | |
| yield None, f"π€ Step 3/4: Analyzing with groq AI... ({len(images)} photos found)" | |
| ddr_data = analyze_with_groq(api_key.strip(), inspection_text, thermal_text) | |
| # Step 4: Build document | |
| yield None, "π Step 4/4: Building DDR Word document..." | |
| output_path = tempfile.mktemp(suffix=".docx") | |
| build_ddr_document(ddr_data, images, output_path) | |
| areas_count = len(ddr_data.get('area_wise_observations', [])) | |
| actions_count = len(ddr_data.get('recommended_actions', [])) | |
| overall_sev = ddr_data.get('severity_assessment', {}).get('overall', 'N/A') | |
| summary = f"""β DDR Generated Successfully! | |
| π Summary: | |
| β’ {areas_count} impacted areas documented | |
| β’ {actions_count} recommended actions | |
| β’ Overall severity: {overall_sev} | |
| β’ {len(images)} photos embedded | |
| Download your report below β""" | |
| yield output_path, summary | |
| except Exception as e: | |
| import traceback | |
| yield None, f"β Error: {str(e)}\n\nDetails:\n{traceback.format_exc()}" | |
| with gr.Blocks(title="DDR Generator β UrbanRoof", theme=gr.themes.Soft()) as app: | |
| gr.Markdown(""" | |
| # π Detailed Diagnostic Report Generator | |
| **UrbanRoof Property Inspection β Professional DDR Word Document** | |
| Upload your inspection report (and optionally a thermal report), and the AI will generate a structured DDR with observations, root cause analysis, severity assessment, and recommended actions. | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| api_key_input = gr.Textbox( | |
| label="π groq API Key", | |
| placeholder="Paste your groq API key here (free at aistudio.google.com)", | |
| type="password" | |
| ) | |
| inspection_input = gr.File( | |
| label="π Inspection Report PDF (Required)", | |
| file_types=[".pdf"] | |
| ) | |
| thermal_input = gr.File( | |
| label="π‘οΈ Thermal Report PDF (Optional)", | |
| file_types=[".pdf"] | |
| ) | |
| generate_btn = gr.Button("π Generate DDR Report", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| status_output = gr.Textbox( | |
| label="Status", | |
| lines=10, | |
| interactive=False, | |
| value="Upload your PDFs and click Generate to begin." | |
| ) | |
| file_output = gr.File(label="π₯ Download DDR Report (.docx)") | |
| generate_btn.click( | |
| fn=generate_ddr, | |
| inputs=[api_key_input, inspection_input, thermal_input], | |
| outputs=[file_output, status_output] | |
| ) | |
| gr.Markdown(""" | |
| --- | |
| **How it works:** PyMuPDF extracts text + photos β groq analyzes and structures the data β python-docx builds the Word report | |
| **Note:** The thermal report is optional. If not provided, the DDR will be generated from the inspection report alone. | |
| """) | |
| if __name__ == "__main__": | |
| app.launch(share=True) | |