Eishaan's picture
Polish submission workflow and board preview
cb4d56e
Raw
History Blame Contribute Delete
41.8 kB
from __future__ import annotations
import base64
import json
import os
import inspect
from pathlib import Path
from typing import Any
import gradio as gr
from src.board_export import create_board_artifacts
from src.cad_parser import candidate_to_selection, parse_dxf
from src.climate import fetch_climate_bundle
from src.diagrams import create_diagrams
from src.earth_reference import build_earth_reference
from src.evidence import evidence_to_html_table, make_evidence, reset_evidence_counter
from src.export import write_markdown_export
from src.geocoding import reverse_geocode_site
from src.geojson_parser import parse_geojson_file
from src.geometry import normalize_map_state, selection_from_lat_lon
from src.kml_parser import parse_kml_file
from src.location import extract_coordinates_from_text
from src.models import BoundaryCandidate, ReportBundle, SiteSelection
from src.osm_context import fetch_osm_context
from src.report import build_board_bundle, build_markdown_report
from src.sample_data import apply_chorwad_sample_fallbacks
from src.small_model_assistant import build_assistant_brief
from src.soil import fetch_soil
from src.sun import summarize_sun_wind
from src.topography import fetch_topography
from src.upload_limits import (
MAX_BOARD_PREVIEW_MB,
MAX_DXF_MB,
MAX_GEOJSON_MB,
MAX_KML_MB,
MAX_PDF_REFERENCE_MB,
validate_upload,
)
ROOT = Path(__file__).parent
ASSETS = ROOT / "assets"
def read_asset(name: str) -> str:
path = ASSETS / name
return path.read_text(encoding="utf-8") if path.exists() else ""
def image_preview_html(path: str | None) -> str:
if not path:
return """
<div class="board-preview-empty board-preview-waiting">
<div class="empty-board-grid">
<div class="empty-board-title">
<span>Waiting for selected land</span>
<b>Presentation board will appear here</b>
<small>Draw a site boundary, drop a pin radius, or upload CAD/KML/GeoJSON. Optional project fields can stay blank.</small>
</div>
<div class="empty-board-card"><b>01</b><span>Boundary metrics</span></div>
<div class="empty-board-card"><b>02</b><span>Climate + sun/wind</span></div>
<div class="empty-board-card"><b>03</b><span>OSM context</span></div>
<div class="empty-board-card"><b>04</b><span>Terrain + soil caution</span></div>
<div class="empty-board-card"><b>05</b><span>Site-visit checklist</span></div>
<div class="empty-board-card"><b>06</b><span>Evidence table</span></div>
</div>
</div>
"""
try:
source = validate_upload(
path,
allowed_suffixes={".png"},
max_mb=MAX_BOARD_PREVIEW_MB,
label="Presentation board preview",
)
data = base64.b64encode(source.read_bytes()).decode("ascii")
except Exception as exc: # noqa: BLE001
return f"<div class='board-preview-empty'>Board preview unavailable: {type(exc).__name__}: {exc}</div>"
return (
"<div class='board-preview-frame'>"
"<img alt='Generated site-analysis presentation board' "
f"src='data:image/png;base64,{data}' />"
"</div>"
)
def gradio_js_callback(script: str) -> str:
return "() => {\n" + script + "\n}"
MAP_HTML = """
<div id="sis-map-shell">
<div class="map-topbar">
<div>
<strong>Boundary Desk</strong>
<span>Trace a site, drop a radius pin, or anchor uploaded CAD/KML/GeoJSON before generating the board pack.</span>
</div>
<div class="map-confidence">Approximate trace</div>
</div>
<div class="map-controls">
<button type="button" id="mode-pin">Pin radius</button>
<button type="button" id="mode-polygon">Polygon</button>
<button type="button" id="mode-rectangle">Rect</button>
<button type="button" id="finish-shape">Finish</button>
<button type="button" id="undo-point">Undo</button>
<button type="button" id="clear-map">Clear</button>
<label>Radius
<select id="radius-select">
<option value="50">50 m</option>
<option value="100">100 m</option>
<option value="250" selected>250 m</option>
<option value="500">500 m</option>
<option value="1000">1 km</option>
</select>
</label>
</div>
<div id="site-map"></div>
<div id="map-status">Map loading. Drawn boundaries are approximate; upload CAD/KML/GeoJSON for better accuracy.</div>
</div>
"""
DELIVERABLES_HTML = """
<div class="deliverables-strip">
<div><b>Board PNG/PDF</b><span>sheet-style artifact</span></div>
<div><b>6 diagrams</b><span>climate, context, edge, matrix</span></div>
<div><b>Evidence</b><span>source / limit / verification</span></div>
<div><b>Report</b><span>editable workbook text</span></div>
</div>
"""
SAMPLE_MAP_STATE = json.dumps(
{
"mode": "polygon",
"geometry": {
"type": "Polygon",
"coordinates": [
[
[70.24495, 21.00195],
[70.24605, 21.00210],
[70.24578, 21.00302],
[70.24470, 21.00286],
[70.24495, 21.00195],
]
],
},
}
)
def load_sample_site():
return (
"Chorwad Coastal Thesis Sample",
"Chorwad site - sample boundary",
"Architecture thesis / resort concept",
"Sample drawn boundary; replace with faculty CAD, KML/KMZ, GeoJSON, or real site survey for actual work.",
"21.00245, 70.24550",
(
"Use as validation notes only: check fishing/tourism activity, seasonal visitor movement, local material language, "
"coastal humidity, service access, and public/private edge conditions during site visit."
),
(
"Before treating the sample as a real submission, compare with Google Earth/KML or faculty CAD. Manually mark visible trees, "
"water edge, road approach, surrounding structures, open ground, drainage signs, and any construction activity."
),
SAMPLE_MAP_STATE,
)
def build_head_html() -> str:
shell_css = """
<style>
html, body {
margin: 0 !important;
width: 100% !important;
max-width: 100vw !important;
overflow-x: hidden !important;
background: #e9eef0 !important;
}
gradio-app {
display: block !important;
width: 100% !important;
max-width: 100vw !important;
background: #e9eef0 !important;
}
@media (max-width: 920px) {
html, body, gradio-app {
min-width: 0 !important;
width: 100vw !important;
max-width: 100vw !important;
}
}
</style>
"""
leaflet_css = read_asset("leaflet.css")
leaflet_js = read_asset("leaflet.js")
if leaflet_css and leaflet_js:
return f"{shell_css}\n<style>{leaflet_css}</style>\n<script>{leaflet_js}</script>"
return shell_css + """
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
"""
HEAD_HTML = build_head_html()
def parse_dxf_candidates(dxf_file: Any):
if not dxf_file:
return gr.update(choices=[], value=None), "Upload a DXF to inspect boundary candidates.", None
try:
path = validate_upload(
_file_path(dxf_file),
allowed_suffixes={".dxf"},
max_mb=MAX_DXF_MB,
label="DXF",
)
parsed = parse_dxf(path)
candidates: list[BoundaryCandidate] = parsed["boundary_candidates"] # type: ignore[assignment]
choices = [(f"{c.id} | {c.layer_name} | {c.area_sqm:,.1f} sqm | {c.vertex_count} vertices", c.id) for c in candidates]
context = parsed.get("context_layers", {})
layers = parsed.get("layers", [])
summary = [
f"**DXF:** `{path.name}`",
f"**Detected unit:** {parsed.get('unit')}",
f"**Layers found:** {len(layers)}",
f"**Boundary candidates:** {len(candidates)}",
]
if context:
summary.append("**Context layers:** " + ", ".join(f"{k}: {v}" for k, v in context.items()))
if not candidates:
summary.append("No boundary candidate was found. Try drawing the boundary or exporting a simpler DXF with a site boundary layer.")
return gr.update(choices=choices, value=choices[0][1] if choices else None), "\n\n".join(summary), parsed
except Exception as exc: # noqa: BLE001
return gr.update(choices=[], value=None), f"DXF parse failed: {type(exc).__name__}: {exc}", None
def generate_site_analysis(
project_name: str,
site_name: str,
project_type: str,
boundary_source: str,
culture_notes: str,
earth_observation_notes: str,
map_state: str,
manual_location: str,
dxf_file: Any,
selected_candidate_id: str,
dxf_state: dict[str, Any] | None,
geojson_file: Any,
kml_file: Any,
pdf_file: Any,
):
reset_evidence_counter()
warnings: list[str] = []
try:
selection, setup_evidence, setup_warnings = _build_selection(
map_state=map_state,
manual_location=manual_location,
dxf_file=dxf_file,
selected_candidate_id=selected_candidate_id,
dxf_state=dxf_state,
geojson_file=geojson_file,
kml_file=kml_file,
)
warnings.extend(setup_warnings)
except Exception as exc: # noqa: BLE001
message = f"### Input Needed\n\n{exc}"
return None, None, None, message, None, None, None, None, None, None, None, "", message, ""
if pdf_file:
try:
validate_upload(
_file_path(pdf_file),
allowed_suffixes={".pdf"},
max_mb=MAX_PDF_REFERENCE_MB,
label="PDF reference",
)
except Exception as exc: # noqa: BLE001
warnings.append(f"PDF reference ignored: {type(exc).__name__}: {exc}")
warnings.append("PDF upload is reference-only in this MVP; geometry is not extracted from PDFs.")
evidence = list(setup_evidence)
evidence.append(
make_evidence(
category="Soil / ground",
finding="Soil and foundation are treated as site-visit/professional verification items in this MVP.",
source_name="Safety rule",
source_url="",
source_type="application policy",
resolution_or_scope="project-wide",
confidence="high",
limitation="No geotechnical survey or final foundation recommendation is generated.",
design_implication="Use only as a prompt to request soil reports or professional input.",
verification_needed="Obtain geotechnical/professional verification before foundation decisions.",
output_label="professional_verification_required",
)
)
if culture_notes:
evidence.append(
make_evidence(
category="Region / culture",
finding="User supplied local/culture/site-observation notes.",
source_name="User note",
source_url="",
source_type="user-provided",
resolution_or_scope="editable observation",
confidence="medium",
limitation="Not independently verified by the app.",
design_implication="Use as a prompt for site visit and studio discussion.",
verification_needed="Verify with site visit, local interviews, or faculty-approved sources.",
output_label="user_input",
)
)
earth_reference, earth_evidence = build_earth_reference(selection, earth_observation_notes or "")
evidence.extend(earth_evidence)
lat, lon = selection.anchor_lat, selection.anchor_lon
climate: dict[str, Any] = {"forecast": None, "recent_historical": None, "climate_normal": None}
osm_context: dict[str, Any] = {"counts": {}, "features": []}
topography: dict[str, Any] = {}
soil: dict[str, Any] = {}
site_identity: dict[str, Any] | None = None
sun_summary: dict[str, str] = {
"orientation_note": "Sun/orientation summary unavailable because no anchor coordinate is available.",
"east_west_note": "",
"wind_note": "",
"limitation": "Provide a real-world anchor coordinate for public-data analysis.",
}
if lat is not None and lon is not None:
site_identity, identity_evidence = reverse_geocode_site(lat, lon)
evidence.extend(identity_evidence)
climate, climate_evidence = fetch_climate_bundle(lat, lon)
evidence.extend(climate_evidence)
wind_dir = _current_wind_direction(climate)
sun_summary, sun_evidence = summarize_sun_wind(lat, lon, wind_dir)
evidence.extend(sun_evidence)
osm_context, osm_evidence = fetch_osm_context(selection)
evidence.extend(osm_evidence)
topography, topography_evidence = fetch_topography(selection)
evidence.extend(topography_evidence)
soil, soil_evidence = fetch_soil(selection)
evidence.extend(soil_evidence)
else:
warnings.append("No anchor coordinate was available, so climate, sun, OSM, terrain, and soil public-data layers were skipped.")
(
site_identity,
climate,
osm_context,
topography,
soil,
sample_evidence,
sample_warnings,
) = apply_chorwad_sample_fallbacks(
project_name=project_name,
site_name=site_name,
boundary_source=boundary_source,
site_identity=site_identity,
climate=climate,
osm_context=osm_context,
topography=topography,
soil=soil,
anchor_lat=selection.anchor_lat,
anchor_lon=selection.anchor_lon,
)
evidence.extend(sample_evidence)
warnings.extend(sample_warnings)
(
project_name,
site_name,
project_type,
boundary_source,
auto_label_warnings,
) = _resolve_project_fields(
project_name=project_name,
site_name=site_name,
project_type=project_type,
boundary_source=boundary_source,
selection=selection,
site_identity=site_identity,
)
warnings.extend(auto_label_warnings)
diagram_paths = create_diagrams(
selection,
climate,
osm_context,
sun_summary,
topography=topography,
soil=soil,
)
if len(diagram_paths) < 3:
warnings.append(
"One or more diagrams could not be generated; use the Markdown report and evidence table, then retry if needed."
)
assistant_md = build_assistant_brief(
selection=selection,
evidence_rows=evidence,
warnings=warnings,
project_type=project_type,
)
bundle = build_board_bundle(
project_name=project_name,
site_name=site_name,
selection=selection,
climate=climate,
osm_context=osm_context,
sun_summary=sun_summary,
site_identity=site_identity,
evidence_rows=evidence,
diagram_paths=diagram_paths,
topography=topography,
soil=soil,
warnings=warnings,
)
if boundary_source:
bundle.board_markdown += f"\n\n### Boundary Source\n\n{boundary_source}\n"
if culture_notes:
bundle.board_markdown += f"\n\n### Editable Local / Culture Notes\n\n{culture_notes}\n"
bundle.board_markdown += "\n\n" + earth_reference["markdown"]
bundle.board_markdown += "\n\n### Small Model Assistant / Template Brief\n\n" + assistant_md
report = build_markdown_report(project_name, site_name, selection, bundle)
export_path = write_markdown_export(report, project_name or site_name or "site-intelligence")
board_artifacts = create_board_artifacts(
project_name=project_name,
site_name=site_name,
project_type=project_type,
boundary_source=boundary_source,
culture_notes=culture_notes,
selection=selection,
climate=climate,
osm_context=osm_context,
sun_summary=sun_summary,
site_identity=site_identity,
evidence_rows=evidence,
diagram_paths=diagram_paths,
warnings=warnings,
topography=topography,
soil=soil,
)
evidence_html = evidence_to_html_table(evidence)
padded_paths = diagram_paths + [None] * (6 - len(diagram_paths))
warning_md = "### Warnings / Limits\n\n" + ("\n".join(f"- {w}" for w in warnings) if warnings else "- No additional warnings.")
return (
image_preview_html(board_artifacts["png"]),
board_artifacts["png"],
board_artifacts["pdf"],
report,
export_path,
padded_paths[0],
padded_paths[1],
padded_paths[2],
padded_paths[3],
padded_paths[4],
padded_paths[5],
evidence_html,
warning_md,
assistant_md,
)
def _build_selection(
*,
map_state: str,
manual_location: str,
dxf_file: Any,
selected_candidate_id: str,
dxf_state: dict[str, Any] | None,
geojson_file: Any,
kml_file: Any,
) -> tuple[SiteSelection, list[Any], list[str]]:
evidence = []
warnings = []
anchor = extract_coordinates_from_text(manual_location)
if dxf_file and selected_candidate_id and dxf_state:
validate_upload(
_file_path(dxf_file),
allowed_suffixes={".dxf"},
max_mb=MAX_DXF_MB,
label="DXF",
)
candidates: list[BoundaryCandidate] = dxf_state.get("boundary_candidates", [])
selected = next((c for c in candidates if c.id == selected_candidate_id), None)
if not selected:
raise ValueError("Select a DXF boundary candidate before generating.")
if not anchor:
anchor = _anchor_from_map_state(map_state)
if not anchor:
raise ValueError("CAD/DXF boundaries need an anchor lat/lon or map pin before public data can run.")
selection = candidate_to_selection(selected, anchor[0], anchor[1])
evidence.append(
make_evidence(
category="Boundary",
finding=f"DXF boundary candidate `{selected.id}` was selected from layer `{selected.layer_name}`.",
source_name=selected.source_file,
source_url="",
source_type="uploaded DXF",
resolution_or_scope=f"{selected.unit}; local CAD geometry",
confidence="medium",
limitation="CAD may use local coordinates and is not legal/cadastral verification.",
design_implication="Use for preliminary board area/perimeter and site-form understanding.",
verification_needed="Confirm with faculty drawing, survey, or official project documents.",
output_label="cad_derived",
)
)
context = dxf_state.get("context_layers") or {}
if context:
evidence.append(
make_evidence(
category="Existing conditions",
finding="DXF contains context layers: " + ", ".join(f"{k}: {v}" for k, v in context.items()),
source_name=selected.source_file,
source_url="",
source_type="uploaded DXF",
resolution_or_scope="CAD layer names and entity counts",
confidence="medium",
limitation="Layer names are user/CAD-authored and may be inconsistent.",
design_implication="Use layers as prompts for roads, water, vegetation, built-up, and contour checks.",
verification_needed="Visually inspect CAD and verify conditions on site.",
output_label="cad_derived",
)
)
return selection, evidence, warnings
if geojson_file:
geojson_path = validate_upload(
_file_path(geojson_file),
allowed_suffixes={".geojson", ".json"},
max_mb=MAX_GEOJSON_MB,
label="GeoJSON",
)
selection = parse_geojson_file(geojson_path)
evidence.append(
make_evidence(
category="Boundary",
finding="GeoJSON boundary was uploaded and treated as WGS84 geometry.",
source_name=geojson_path.name,
source_url="",
source_type="uploaded GeoJSON",
resolution_or_scope="uploaded polygon",
confidence="high",
limitation="Boundary is only as reliable as the uploaded GeoJSON source.",
design_implication="Use for boundary-based first-pass site analysis.",
verification_needed="Confirm with original source or site survey.",
output_label="user_input",
)
)
return selection, evidence, warnings
if kml_file:
kml_path = validate_upload(
_file_path(kml_file),
allowed_suffixes={".kml", ".kmz"},
max_mb=MAX_KML_MB,
label="KML/KMZ",
)
selection = parse_kml_file(kml_path)
evidence.append(
make_evidence(
category="Boundary",
finding="KML/KMZ boundary was uploaded and treated as Google Earth / WGS84 geometry.",
source_name=kml_path.name,
source_url="",
source_type="uploaded KML/KMZ",
resolution_or_scope="uploaded polygon; Google Earth/GIS-style coordinates",
confidence="medium",
limitation="Boundary is only as reliable as the exported KML/KMZ source and is not legal/cadastral verification.",
design_implication="Use for boundary-based first-pass site analysis and board generation.",
verification_needed="Confirm with faculty CAD, survey, or official project documents.",
output_label="user_input",
)
)
return selection, evidence, warnings
try:
if map_state and map_state.strip() and map_state.strip() != "{}":
selection = normalize_map_state(map_state)
evidence.append(
make_evidence(
category="Boundary",
finding=f"Map selection was created using `{selection.selection_type}` mode.",
source_name="User map input",
source_url="",
source_type="drawn map / pin",
resolution_or_scope=selection.accuracy_label,
confidence="low" if selection.selection_type == "pin_radius" else "medium",
limitation="Drawn and pin-radius boundaries are approximate.",
design_implication="Use for early studio context and verification planning.",
verification_needed="Verify with CAD, KML, GeoJSON, faculty drawing, or site survey.",
output_label="user_input",
)
)
return selection, evidence, warnings
except Exception as exc: # noqa: BLE001
warnings.append(f"Map selection could not be used: {exc}")
if anchor:
selection = selection_from_lat_lon(anchor[0], anchor[1], radius_m=250)
evidence.append(
make_evidence(
category="Boundary",
finding="Manual coordinate input was converted to pin-radius context analysis.",
source_name="User coordinate input",
source_url="",
source_type="manual lat/lon or parsed URL",
resolution_or_scope="250 m radius",
confidence="low",
limitation="Point-radius mode is not exact boundary analysis.",
design_implication="Use for neighborhood/context analysis only.",
verification_needed="Add a drawn or uploaded boundary for plot-level analysis.",
output_label="user_input",
)
)
return selection, evidence, warnings
raise ValueError("Draw, pin, upload GeoJSON/DXF, or enter coordinates first.")
def _anchor_from_map_state(map_state: str) -> tuple[float, float] | None:
try:
selection = normalize_map_state(map_state)
if selection.anchor_lat is not None and selection.anchor_lon is not None:
return selection.anchor_lat, selection.anchor_lon
except Exception:
return None
return None
def _current_wind_direction(climate: dict[str, Any]) -> float | None:
forecast = climate.get("forecast") or {}
value = forecast.get("current_wind_direction_deg")
try:
return float(value) if value is not None else None
except (TypeError, ValueError):
return None
def _resolve_project_fields(
*,
project_name: str,
site_name: str,
project_type: str,
boundary_source: str,
selection: SiteSelection,
site_identity: dict[str, Any] | None,
) -> tuple[str, str, str, str, list[str]]:
warnings: list[str] = []
project = (project_name or "").strip()
site = (site_name or "").strip()
ptype = (project_type or "").strip()
source = (boundary_source or "").strip()
if not project:
project = "Preliminary Site Analysis"
warnings.append("Project name was not provided; the app used a generic title.")
if not site or site.lower() in {"untitled site", "untitled", "site"}:
site = _auto_site_name(selection, site_identity)
warnings.append("Site name was not provided; the app generated a label from the selected area.")
if not ptype:
ptype = "Early-stage architecture site analysis"
if not source:
source = _auto_boundary_source(selection)
warnings.append("Boundary source was not provided; the report labels the selection from the input mode.")
return project, site, ptype, source, warnings
def _auto_site_name(selection: SiteSelection, site_identity: dict[str, Any] | None) -> str:
if site_identity:
for key in ("city", "town", "village", "district", "state"):
value = site_identity.get(key)
if value:
return f"{value} selected site"
display = site_identity.get("display_name")
if display:
return f"{str(display).split(',')[0]} selected site"
if selection.anchor_lat is not None and selection.anchor_lon is not None:
return f"Selected site {selection.anchor_lat:.5f}, {selection.anchor_lon:.5f}"
return "Selected site area"
def _auto_boundary_source(selection: SiteSelection) -> str:
if selection.selection_type in {"drawn_polygon", "rectangle"}:
return "User-drawn boundary on OpenStreetMap base map; approximate and not legal/cadastral."
if selection.selection_type == "pin_radius":
return "User-selected pin and radius; approximate context analysis, not exact boundary."
if selection.selection_type == "dxf_boundary":
return "Uploaded DXF boundary candidate selected by user."
if selection.selection_type == "geojson_boundary":
return "Uploaded GeoJSON boundary selected by user."
if selection.selection_type == "kml_boundary":
return "Uploaded KML/KMZ boundary selected by user."
return "User-provided site selection."
def _file_path(file_obj: Any) -> str:
return str(getattr(file_obj, "name", file_obj))
def build_app() -> gr.Blocks:
map_js = read_asset("map.js")
map_js_callback = gradio_js_callback(map_js)
block_kwargs: dict[str, Any] = {"title": "Site Intelligence Studio"}
block_parameters = inspect.signature(gr.Blocks).parameters
if "css" in block_parameters:
block_kwargs["css"] = read_asset("style.css")
if "head" in block_parameters:
block_kwargs["head"] = HEAD_HTML
if "js" in block_parameters:
block_kwargs["js"] = map_js_callback
with gr.Blocks(**block_kwargs) as demo:
gr.HTML(
"""
<div class="studio-shell">
<div class="studio-topbar">
<div>
<span class="studio-kicker">Build Small Hackathon / Backyard AI</span>
<h1>Site Intelligence Studio</h1>
</div>
<div class="studio-meta">
<span>Boundary first</span>
<span>Open data</span>
<span>Field verify</span>
</div>
</div>
<div class="studio-briefing">
<div>
<b>Start with land, not a prompt.</b>
<span>Draw a boundary, drop a radius pin, or upload CAD/KML/GeoJSON. The app turns that selection into a sourced site-analysis board pack.</span>
</div>
<div>
<b>Only the site is required.</b>
<span>Project fields are optional. Blank names are auto-labelled from coordinates or approximate address.</span>
</div>
<div>
<b>Output stays cautious.</b>
<span>Climate, terrain, soil, OSM, and AI text are labelled by source, confidence, and verification need.</span>
</div>
</div>
<div class="studio-proof-row">
<div><b>For students</b><span>faster studio sheets</span></div>
<div><b>For reviewers</b><span>sources and limits visible</span></div>
<div><b>For safety</b><span>no final foundation or legal claims</span></div>
<div><b>For demo</b><span>works from polygon only</span></div>
</div>
""" + DELIVERABLES_HTML + """
</div>
"""
)
with gr.Row(elem_classes=["studio-workbench"]):
with gr.Column(scale=3, elem_classes=["brief-pane"]):
gr.HTML(
"""
<div class="pane-heading">
<span>01</span>
<div><b>Site brief</b><small>Optional context for cleaner board copy</small></div>
</div>
<div class="minimum-card">
<b>Minimum input</b>
<span>Draw a polygon, draw a rectangle, drop a pin radius, or paste coordinates. Everything else can be blank.</span>
</div>
"""
)
with gr.Accordion("Optional labels and manual anchor", open=False, elem_classes=["brief-accordion"]):
project_name = gr.Textbox(
label="Project name (optional)",
placeholder="Auto: Preliminary Site Analysis",
info="Leave blank if you only want to draw/select a site and generate analysis.",
)
site_name = gr.Textbox(
label="Site name (optional)",
placeholder="Auto-filled from approximate address or coordinates",
info="If blank, the app labels the selected polygon/pin from reverse geocoding or coordinates.",
)
project_type = gr.Textbox(
label="Project type (optional)",
placeholder="Thesis, resort, housing, institute...",
)
boundary_source = gr.Textbox(
label="Boundary source (optional)",
placeholder="Auto-labelled from map draw, pin radius, CAD, KML/KMZ, or GeoJSON",
)
manual_location = gr.Textbox(
label="Lat/lon or Google Maps URL",
placeholder="21.002, 70.245 or a Google Maps URL with coordinates",
info="Used as fallback input and as CAD anchor. Drawn polygons use their centroid.",
)
culture_notes = gr.Textbox(
label="Editable region / culture / activity notes",
lines=4,
placeholder="Local activity, user groups, culture, materials, typology, observed movement patterns...",
info="This remains user-provided evidence, not AI-invented demographic analysis.",
)
with gr.Column(scale=6, elem_classes=["canvas-pane"]):
gr.HTML(
"""
<div class="pane-heading canvas-heading">
<span>02</span>
<div><b>Boundary canvas</b><small>Trace land first. Files and forms are secondary.</small></div>
</div>
"""
)
map_generate_btn = gr.Button("Generate from selected site", variant="primary", elem_classes=["map-generate-button"])
gr.HTML(MAP_HTML)
map_state = gr.Textbox(label="Map state", elem_id="map_state", lines=4)
gr.HTML(
"<p class='warning-note'><strong>Boundary accuracy:</strong> map tracing is useful for early context only. Upload CAD/DXF, KML, or GeoJSON when available. Public climate and map layers use the selected anchor or centroid, not legal plot data.</p>"
)
with gr.Column(scale=3, elem_classes=["source-pane"]):
gr.HTML(
"""
<div class="pane-heading">
<span>03</span>
<div><b>Source intake</b><small>Optional files, sample data, and final run</small></div>
</div>
<div class="source-card">
<b>No upload required</b>
<span>Uploads improve accuracy, but map selection alone still creates a full preliminary board pack.</span>
</div>
"""
)
with gr.Accordion("Optional Google Earth / field notes", open=False, elem_classes=["brief-accordion"]):
earth_observation_notes = gr.Textbox(
label="Satellite / Google Earth / site-observation notes",
lines=4,
placeholder="Visible trees, water edge, existing structures, access roads, open ground, slope/drainage signs, construction nearby...",
info="Optional. These are user observations and visual-reference notes, not app-verified satellite facts.",
)
sample_btn = gr.Button("Use Chorwad sample site", variant="secondary")
with gr.Accordion("Boundary and reference uploads", open=False, elem_classes=["upload-panel"]):
dxf_file = gr.File(label="DXF upload", file_types=[".dxf"], type="filepath")
geojson_file = gr.File(label="GeoJSON upload", file_types=[".geojson", ".json"], type="filepath")
kml_file = gr.File(label="Google Earth KML/KMZ upload", file_types=[".kml", ".kmz"], type="filepath")
pdf_file = gr.File(label="PDF reference only", file_types=[".pdf"], type="filepath")
dxf_candidate = gr.Dropdown(label="DXF boundary candidate", choices=[], interactive=True)
dxf_summary = gr.Markdown("Upload a DXF to inspect boundary candidates.")
gr.HTML(
"""
<div class="run-card">
<b>Generated board pack</b>
<span>Identity, area/perimeter, address context, climate views, sun/wind, OSM context, terrain/soil caution, diagrams, checklist, evidence, PNG/PDF/Markdown.</span>
</div>
"""
)
generate_btn = gr.Button("Generate board pack", variant="primary", elem_classes=["generate-button"])
dxf_state = gr.State(None)
gr.HTML(
"""
<div class="review-header">
<span>04</span>
<div>
<b>Review desk</b>
<small>Generated sheet material appears here after the boundary run.</small>
</div>
</div>
"""
)
with gr.Tabs(elem_classes=["review-tabs"]):
with gr.Tab("Presentation Board"):
gr.HTML(
"<div class='output-intro'><strong>Sheet-ready first pass.</strong><span>This is the main artifact: a 16:9 architecture board. Preview is scaled in the browser; download PNG/PDF for full resolution.</span></div>"
)
board_preview = gr.HTML(value=image_preview_html(None))
with gr.Row():
board_png_file = gr.File(label="Download board PNG")
board_pdf_file = gr.File(label="Download board PDF")
warnings_output = gr.Markdown()
with gr.Tab("Report"):
gr.HTML(
"<div class='output-intro'><strong>Editable report text.</strong><span>Use this for copy, review, and source checking. It is intentionally more verbose than the board.</span></div>"
)
board_output = gr.Markdown()
export_file = gr.File(label="Download Markdown report")
with gr.Tab("Diagrams"):
gr.HTML(
"<div class='output-intro'><strong>Studio diagram pack.</strong><span>The first row is the core prototype output. The second row is the submission upgrade: edge/access mapping, climate strategy, and a constraints-verification matrix.</span></div>"
)
with gr.Row():
climate_image = gr.Image(label="Climate diagram", type="filepath")
sun_image = gr.Image(label="Sun / wind diagram", type="filepath")
context_image = gr.Image(label="Geographic context diagram", type="filepath")
with gr.Row():
access_image = gr.Image(label="Access / edge context", type="filepath")
strategy_image = gr.Image(label="Climate strategy sheet", type="filepath")
matrix_image = gr.Image(label="Constraints / verification matrix", type="filepath")
with gr.Tab("Evidence Table"):
evidence_table = gr.HTML()
with gr.Tab("Small Model Brief"):
gr.HTML(
"<div class='output-intro'><strong>Bounded AI layer.</strong><span>Uses a <=4B Hugging Face model only when configured; otherwise the deterministic fallback keeps the app usable.</span></div>"
)
assistant_output = gr.Markdown()
dxf_file.change(
fn=parse_dxf_candidates,
inputs=dxf_file,
outputs=[dxf_candidate, dxf_summary, dxf_state],
)
sample_btn.click(
fn=load_sample_site,
inputs=[],
outputs=[
project_name,
site_name,
project_type,
boundary_source,
manual_location,
culture_notes,
earth_observation_notes,
map_state,
],
)
analysis_inputs = [
project_name,
site_name,
project_type,
boundary_source,
culture_notes,
earth_observation_notes,
map_state,
manual_location,
dxf_file,
dxf_candidate,
dxf_state,
geojson_file,
kml_file,
pdf_file,
]
analysis_outputs = [
board_preview,
board_png_file,
board_pdf_file,
board_output,
export_file,
climate_image,
sun_image,
context_image,
access_image,
strategy_image,
matrix_image,
evidence_table,
warnings_output,
assistant_output,
]
generate_btn.click(
fn=generate_site_analysis,
inputs=analysis_inputs,
outputs=analysis_outputs,
)
map_generate_btn.click(
fn=generate_site_analysis,
inputs=analysis_inputs,
outputs=analysis_outputs,
)
if "js" not in block_parameters:
demo.load(fn=None, js=map_js_callback)
return demo
demo = build_app()
if __name__ == "__main__":
launch_kwargs: dict[str, Any] = {
"server_name": "0.0.0.0",
"server_port": int(os.environ.get("PORT", "7860")),
}
launch_parameters = inspect.signature(demo.launch).parameters
if "css" in launch_parameters:
launch_kwargs["css"] = read_asset("style.css")
if "head" in launch_parameters:
launch_kwargs["head"] = HEAD_HTML
if "js" in launch_parameters:
launch_kwargs["js"] = gradio_js_callback(read_asset("map.js"))
if "ssr_mode" in launch_parameters:
launch_kwargs["ssr_mode"] = False
demo.launch(**launch_kwargs)