"""Tab 5: Generate Results.
Process all inputs and generate assessment outputs.
"""
import gradio as gr
from typing import Any, Optional
from datetime import datetime
import tempfile
from ui.state import SessionState
from ui.components import create_stats_dict, create_progress_html, image_store
from pipeline import FDAMPipeline, PipelineResult, PDFGenerator
def create_tab() -> dict[str, Any]:
"""Create Tab 5 UI components.
Returns:
Dictionary of component references for event wiring.
"""
gr.Markdown("### Generate Assessment")
# Pre-flight check summary
with gr.Row():
preflight_status = gr.HTML(
value="",
elem_id="preflight_status",
)
gr.Markdown(
"""
Click below to process all inputs and generate:
1. **Cleaning Specification / Scope of Work** (primary output)
2. **Sampling Plan Recommendations**
3. **Confidence Report**
"""
)
with gr.Row():
generate_btn = gr.Button(
"Generate Assessment",
variant="primary",
scale=2,
elem_id="generate_btn",
)
processing_status = gr.Textbox(
label="Status",
value="Ready",
interactive=False,
elem_id="processing_status",
)
# Progress display
with gr.Row():
progress_html = gr.HTML(
value="",
elem_id="progress_html",
)
gr.Markdown("---")
gr.Markdown("### Results")
with gr.Row():
with gr.Column():
gr.Markdown("#### Annotated Images")
annotated_gallery = gr.Gallery(
label="AI-Analyzed Images",
columns=2,
height="auto",
elem_id="annotated_gallery",
)
with gr.Column():
gr.Markdown("#### Assessment Summary")
stats_output = gr.JSON(
label="Statistics",
elem_id="stats_output",
)
gr.Markdown("### Cleaning Specification / Scope of Work")
sow_output = gr.Markdown(
value="*Results will appear here after generation.*",
elem_id="sow_output",
)
gr.Markdown("### Downloads")
with gr.Row():
download_md = gr.File(
label="Download Markdown (.md)",
elem_id="download_md",
)
download_pdf = gr.File(
label="Download PDF (.pdf)",
elem_id="download_pdf",
)
# Navigation
with gr.Row():
back_btn = gr.Button("← Back to Observations")
regenerate_btn = gr.Button(
"Regenerate Assessment",
variant="secondary",
)
return {
"preflight_status": preflight_status,
"generate_btn": generate_btn,
"processing_status": processing_status,
"progress_html": progress_html,
"annotated_gallery": annotated_gallery,
"stats_output": stats_output,
"sow_output": sow_output,
"download_md": download_md,
"download_pdf": download_pdf,
"back_btn": back_btn,
"regenerate_btn": regenerate_btn,
}
def check_preflight(session: SessionState) -> str:
"""Check if assessment can be generated.
Returns:
HTML string with preflight status.
"""
can_generate, errors = session.can_generate()
# Also check if images are in memory
expected_ids = [img.id for img in session.images]
missing_ids = image_store.get_missing_ids(expected_ids)
if missing_ids:
errors.append(f"{len(missing_ids)} image(s) need to be re-uploaded")
can_generate = False
if can_generate:
# Show summary of what will be processed
stats = create_stats_dict(session)
return f"""
✓ Ready to Generate
Room: {stats['room_name']}
Images: {stats['images']}
Total Area: {stats['total_floor_area_sf']} SF
Facility: {stats['facility_classification']}
Era: {stats['construction_era']}
"""
else:
error_items = "".join(f"{e}" for e in errors)
return f"""
Cannot Generate - Please Fix:
"""
def generate_assessment(
session: SessionState,
progress: Optional[gr.Progress] = None,
) -> tuple[SessionState, str, str, list[tuple], dict, str, Optional[str], Optional[str]]:
"""Generate the assessment using the FDAM pipeline.
Returns:
Tuple of (session, status, progress_html, annotated_images,
stats, sow_markdown, md_file_path, pdf_file_path).
"""
# Create pipeline instance
pipeline = FDAMPipeline()
# Define progress callback for Gradio
def progress_callback(prog):
if progress:
progress(prog.percent, desc=prog.message)
# Execute pipeline
result: PipelineResult = pipeline.execute(
session=session,
progress_callback=progress_callback,
)
# Handle errors
if not result.success:
error_msg = "**Error:** Please fix the following before generating:\n\n"
error_msg += "\n".join(f"- {e}" for e in result.errors)
return (
result.session,
"Error: Cannot generate",
"",
[],
{},
error_msg,
None,
None,
)
# Generate stats dictionary for UI
stats = pipeline.generate_stats_dict(result)
# Get markdown content
sow_markdown = result.document.markdown if result.document else ""
# Save markdown file
md_path = None
pdf_path = None
try:
if sow_markdown:
# Save Markdown file
room_name_safe = session.room.name.replace(' ', '_') if session.room.name else "Room"
with tempfile.NamedTemporaryFile(
mode='w',
suffix='.md',
delete=False,
prefix=f"SOW_{room_name_safe}_",
) as f:
f.write(sow_markdown)
md_path = f.name
# Generate PDF
pdf_generator = PDFGenerator()
pdf_result = pdf_generator.generate_pdf(sow_markdown)
if pdf_result.success:
pdf_path = pdf_result.pdf_path
else:
result.warnings.append(f"PDF generation failed: {pdf_result.error_message}")
except Exception as e:
print(f"Error saving files: {e}")
# Add warnings to status if any
status = "Complete"
if result.warnings:
status = f"Complete ({len(result.warnings)} warnings)"
return (
result.session,
status,
create_progress_html(6, 6, f"Complete! ({result.execution_time_seconds:.1f}s)"),
result.annotated_images,
stats,
sow_markdown,
md_path,
pdf_path,
)
def _generate_sow_markdown(
session: SessionState,
stats: dict,
vision_results: dict,
) -> str:
"""Generate Scope of Work markdown document.
This is a placeholder - real implementation uses DocumentGenerator.
Kept for backwards compatibility but should not be called directly.
"""
r = session.room
area = r.length_ft * r.width_ft
volume = area * r.ceiling_height_ft
# Build vision summary
vision_lines = []
for img_meta in session.images:
result = vision_results.get(img_meta.id, {})
zone = result.get("zone", {}).get("classification", "N/A")
condition = result.get("condition", {}).get("level", "N/A")
vision_lines.append(f"- **{img_meta.filename}**: Zone={zone}, Condition={condition}")
vision_summary = "\n".join(vision_lines) if vision_lines else "No images analyzed."
# Build observations summary
obs = session.observations
obs_items = []
if obs.smoke_fire_odor:
obs_items.append(f"- Smoke/fire odor: {obs.odor_intensity}")
if obs.visible_soot_deposits:
obs_items.append(f"- Visible soot deposits: {obs.soot_pattern_description or 'Yes'}")
if obs.large_char_particles:
obs_items.append(f"- Large char particles: {obs.char_density_estimate or 'Yes'}")
if obs.ash_like_residue:
obs_items.append(f"- Ash residue: {obs.ash_color_texture or 'Yes'}")
if obs.surface_discoloration:
obs_items.append(f"- Surface discoloration: {obs.discoloration_description or 'Yes'}")
if obs.wildfire_indicators:
obs_items.append(f"- Wildfire indicators: {obs.wildfire_notes or 'Yes'}")
obs_summary = "\n".join(obs_items) if obs_items else "No significant observations noted."
# Regulatory flags
reg_flags = "\n".join(f"- {f}" for f in stats.get("regulatory_flags", [])) or "None identified."
markdown = f"""# Cleaning Specification / Scope of Work
## Room Information
| Field | Value |
|-------|-------|
| **Room Name** | {r.name} |
| **Facility Classification** | {r.facility_classification or 'Not specified'} |
| **Construction Era** | {r.construction_era or 'Not specified'} |
---
## Scope Summary
| Metric | Value |
|--------|-------|
| Room | {r.name} |
| Total Floor Area | {stats['total_floor_area_sf']} SF |
| Total Volume | {stats['total_volume_cf']} CF |
| Images Analyzed | {stats['total_images']} |
---
## Room Details
| Property | Value |
|----------|-------|
| **Room Name** | {r.name} |
| **Dimensions** | {r.length_ft:.0f}' x {r.width_ft:.0f}' x {r.ceiling_height_ft:.0f}' |
| **Floor Area** | {area:,.0f} SF |
| **Volume** | {volume:,.0f} CF |
---
## AI Vision Analysis Summary
{vision_summary}
---
## Field Observations
{obs_summary}
---
## Air Filtration Requirements
Per NADCA ACR 2021, Section 3.6:
- **Required ACH**: 4 air changes per hour
- **Total Volume**: {stats['total_volume_cf']} CF
- **Air Scrubbers Required**: {stats['air_scrubbers_required']} units (2000 CFM each)
- **Calculation**: ({stats['total_volume_cf']} CF × 4 ACH) / (2000 CFM × 60) = {stats['air_scrubbers_required']} units
---
## Regulatory Flags
{reg_flags}
---
## Sampling Recommendations
*Detailed sampling plan to be generated based on surface inventory and zone classifications.*
---
## Disclaimer
This document was generated using AI-assisted analysis and should be reviewed by a qualified
industrial hygienist before implementation. Visual assessments require laboratory confirmation
for definitive particle identification.
---
*Generated by FDAM AI Pipeline v4.0.1*
*{datetime.now().strftime('%Y-%m-%d %H:%M')}*
"""
return markdown