| import os |
| import re |
| import json |
| import gradio as gr |
| from huggingface_hub import InferenceClient |
|
|
| |
| |
| |
|
|
| MODEL = "mistralai/Mistral-7B-Instruct-v0.3" |
| HF_TOKEN = os.environ.get("HF_TOKEN", None) |
|
|
| SYSTEM_PROMPT = """You are an aviation training scenario analyst. Your role is to help flight instructors, |
| students, and aviation enthusiasts structure hypothetical scenarios for training |
| discussion and expert review. |
| |
| You do NOT make real flight safety decisions. You do NOT have access to live weather, |
| NOTAMs, or real aviation data. You are NOT a certified dispatcher or aviation authority. |
| |
| Given a hypothetical aviation scenario, you extract structured training information |
| and generate discussion questions for expert review. |
| |
| Return ONLY a valid JSON object with this exact schema. No markdown, no backticks, |
| no extra text. |
| |
| { |
| "scenario_summary": "2 sentences max. Plain English. Always starts with 'This hypothetical scenario...'", |
| "extracted_parameters": { |
| "departure": "string or 'Not specified'", |
| "destination": "string or 'Not specified'", |
| "aircraft_type": "string or 'Not specified'", |
| "crew_experience": "string or 'Not specified'", |
| "planned_altitude": "string or 'Not specified'", |
| "weather_state": "string or 'Not specified'", |
| "fuel_endurance": "string or 'Not specified'", |
| "special_conditions": "string or 'None mentioned'" |
| }, |
| "weather_terrain_airspace": [ |
| "string — each item is a consideration for training discussion, not a verdict. Use language like 'The described weather suggests...', 'Terrain profile implies...', 'Airspace in this context typically involves...'" |
| ], |
| "risk_flags": [ |
| { |
| "severity": "HIGH | MEDIUM | LOW", |
| "factor": "short label", |
| "discussion_point": "one sentence — what a training instructor would flag for discussion" |
| } |
| ], |
| "assumptions_missing": [ |
| "string — each item is something the model had to assume or that was not specified" |
| ], |
| "expert_questions": [ |
| "string — each item is a question a flight instructor or safety examiner would raise in a debrief or training session. 5-7 questions." |
| ], |
| "training_verdict": "Low Concern | Caution | High Risk | Expert Review Recommended", |
| "training_verdict_context": "One sentence. Avoid operational safety claims. Use: 'This hypothetical scenario presents factors that would typically warrant [level] of discussion in a training context.'", |
| "expert_review_message": "Pre-filled message for user to send to a domain expert. Starts with: 'Hi — I used an AI training brief simulator to structure this hypothetical aviation scenario. I would appreciate your expert review of the risk flags and discussion questions below. Specifically: are these the right factors to flag for this scenario, and what important consideration did the model miss?'" |
| }""" |
|
|
| |
| |
| |
|
|
| EX1 = """Hypothetical scenario: a student pilot with 75 hours total time plans a VFR day flight from Grenoble to Turin, crossing the French Alps at 6500ft in October. Aircraft is a Cessna 172. Weather: broken clouds at 5000ft, visibility 10km, light turbulence forecast above 5000ft. Fuel: 3 hours endurance. No IFR rating. Pilot has never flown above 4000ft before.""" |
|
|
| EX2 = """Hypothetical scenario: a single-pilot helicopter ferry from Marseille to a fictional offshore platform 60 nautical miles south. Night VFR. Aircraft: fictional medium twin-engine helicopter. Crew: 1800 hours total, 200 hours night. Weather: wind 220/20G30KT, visibility 6km in haze, overcast 1200ft. Fuel: 2.2 hours endurance. No instrument approach at destination.""" |
|
|
| EX3 = """Hypothetical scenario: a fictional regional turboprop completes its first post-maintenance test flight after a major avionics upgrade. Two experienced test pilots, 4000 hours each on type. Departure from a busy international airport. Weather: CAVOK, light wind. Duration: 2 hours. Test card includes full avionics checkout, one engine-out simulation at altitude, and return ILS approach.""" |
|
|
| |
| |
| |
|
|
| FALLBACK_EX1 = { |
| "scenario_summary": "This hypothetical scenario involves a student pilot with limited high-altitude experience planning a VFR crossing of the French Alps in October, with cloud bases below the planned cruising altitude. The scenario presents multiple compounding risk factors that would be central to a structured training debrief.", |
| "extracted_parameters": { |
| "departure": "Grenoble", |
| "destination": "Turin", |
| "aircraft_type": "Cessna 172", |
| "crew_experience": "Student pilot, 75 hours total time, no IFR rating, no prior flight above 4000ft", |
| "planned_altitude": "6500ft", |
| "weather_state": "Broken clouds at 5000ft, visibility 10km, light turbulence forecast above 5000ft", |
| "fuel_endurance": "3 hours", |
| "special_conditions": "Mountain crossing, October weather patterns, VFR-only pilot" |
| }, |
| "weather_terrain_airspace": [ |
| "The described weather suggests that broken cloud coverage at 5000ft would place the planned cruise altitude of 6500ft in or above cloud layers, which raises IMC exposure concerns for a VFR-only flight.", |
| "Terrain profile implies that the French-Italian Alpine corridor includes passes and peaks well above 6500ft, requiring precise route selection to maintain terrain clearance.", |
| "Turbulence forecast above 5000ft is a consideration for the airframe and pilot workload during the most demanding phase of the crossing.", |
| "Airspace in this context typically involves restricted zones, mandatory crossing altitudes, and limited diversion options once committed to the pass." |
| ], |
| "risk_flags": [ |
| { |
| "severity": "HIGH", |
| "factor": "VFR into IMC exposure", |
| "discussion_point": "Broken clouds at 5000ft combined with a planned altitude of 6500ft creates a high likelihood of cloud encounter for a pilot with no IFR rating — a serious training concern in general aviation, especially in mountainous terrain." |
| }, |
| { |
| "severity": "HIGH", |
| "factor": "Student experience vs. terrain complexity", |
| "discussion_point": "75 hours total time with no prior high-altitude experience is a training-relevant mismatch for an Alpine crossing, where mountain wave, orographic lift, and rapid weather change are common." |
| }, |
| { |
| "severity": "MEDIUM", |
| "factor": "Turbulence forecast above MEA", |
| "discussion_point": "Light turbulence forecast at and above the planned altitude introduces handling and fatigue factors that compound the other risk elements in this training discussion." |
| }, |
| { |
| "severity": "MEDIUM", |
| "factor": "Limited diversion options", |
| "discussion_point": "Once committed to a high Alpine pass at low altitude with marginal weather, viable diversion airports are sparse — a scenario worth discussing in terms of decision-making under pressure." |
| }, |
| { |
| "severity": "LOW", |
| "factor": "Fuel endurance", |
| "discussion_point": "Three hours endurance appears adequate for the planned route, but a diversion or weather hold could significantly reduce margins — worth noting in the training context." |
| } |
| ], |
| "assumptions_missing": [ |
| "No information provided on whether the pilot has completed mountain flying training or received a mountain checkout", |
| "Specific route through the Alps not stated — critical for terrain clearance analysis", |
| "No mention of alternate airports or diversion planning", |
| "MEF (Maximum Elevation Figure) for the crossing corridor not included in the scenario", |
| "No information on forecast evolution — whether weather is improving or deteriorating" |
| ], |
| "expert_questions": [ |
| "At what point in flight preparation should a VFR student pilot recognize that this scenario warrants deferral, and what weather minimums would you set for this route in training?", |
| "How would you structure a mountain weather briefing for a student unfamiliar with orographic effects and Alpine meteorology?", |
| "What personal minimums framework would you recommend this pilot develop before attempting high-terrain VFR, and at what total time?", |
| "If the pilot encountered clouds at 5500ft mid-crossing, what options should be discussed in a training debrief and how does limited terrain diversion factor into the analysis?", |
| "How does October weather in the western Alps differ from summer conditions, and what training materials cover seasonal mountain weather patterns?", |
| "What does a standard VFR flight following or AFIS service offer on this route, and what are the limitations in the Alpine corridor?" |
| ], |
| "training_verdict": "High Risk", |
| "training_verdict_context": "This hypothetical scenario presents factors that would typically warrant High Risk discussion in a training context, given the combination of student experience level, planned altitude relative to cloud bases, and the complexity of Alpine terrain.", |
| "expert_review_message": "Hi — I used an AI training brief simulator to structure this hypothetical aviation scenario. I would appreciate your expert review of the risk flags and discussion questions below. Specifically: are these the right factors to flag for this scenario, and what important consideration did the model miss?\n\nScenario: Student pilot, 75 hours TT, VFR Alps crossing Grenoble to Turin at 6500ft, October. Broken clouds at 5000ft, turbulence above 5000ft. Cessna 172, 3h fuel.\n\nTop risk flags identified: VFR into IMC exposure (HIGH), student experience vs. terrain complexity (HIGH), limited diversion options (MEDIUM).\n\nKey training questions: personal minimums for mountain VFR, in-flight decision triggers for weather deterioration, October Alpine meteorology." |
| } |
|
|
| FALLBACK_EX2 = { |
| "scenario_summary": "This hypothetical scenario describes a single-pilot night VFR helicopter ferry over open water to a fictional offshore platform, with gusty crosswind conditions, reduced visibility, a low overcast, and no instrument approach available at the destination. The combination of environmental and operational factors makes this a high-complexity training discussion case.", |
| "extracted_parameters": { |
| "departure": "Marseille", |
| "destination": "Fictional offshore platform, 60nm south", |
| "aircraft_type": "Fictional medium twin-engine helicopter", |
| "crew_experience": "Single pilot, 1800 hours total, 200 hours night", |
| "planned_altitude": "Not specified", |
| "weather_state": "Wind 220/20G30KT, visibility 6km in haze, overcast 1200ft", |
| "fuel_endurance": "2.2 hours", |
| "special_conditions": "Night VFR, offshore over-water, no instrument approach at destination, gusting crosswind" |
| }, |
| "weather_terrain_airspace": [ |
| "The described weather suggests that an overcast at 1200ft with 6km visibility in haze creates marginal VFR conditions that could deteriorate rapidly over open water where visual horizon references are reduced at night.", |
| "Terrain profile implies minimal obstruction concerns en route, but the offshore platform environment presents limited approach path options and no standard instrument procedure as a backup.", |
| "Wind 20 gusting 30 knots introduces significant hover and landing considerations at the platform, particularly for a night arrival with limited visual cues.", |
| "Airspace in this context typically involves offshore transition airspace and possibly restricted military or platform exclusion zones that require specific coordination." |
| ], |
| "risk_flags": [ |
| { |
| "severity": "HIGH", |
| "factor": "Night over-water with no instrument approach", |
| "discussion_point": "Night VFR over open water with an overcast at 1200ft and no instrument approach at the destination creates a scenario where spatial disorientation and weather deterioration have no procedural escape route — a critical training discussion point." |
| }, |
| { |
| "severity": "HIGH", |
| "factor": "Marginal VFR at night", |
| "discussion_point": "Visibility 6km in haze at night is significantly more operationally limiting than the same condition in daylight, as horizon reference is lost and lighting on the water surface is absent." |
| }, |
| { |
| "severity": "MEDIUM", |
| "factor": "Fuel endurance vs. diversion options", |
| "discussion_point": "2.2 hours endurance for a 60nm single-leg offshore flight leaves limited margin for weather hold, platform unavailability, or diversion back to Marseille with headwind component." |
| }, |
| { |
| "severity": "MEDIUM", |
| "factor": "Single-pilot workload at night", |
| "discussion_point": "Night offshore single-pilot operations compress situational awareness, communication, and navigation tasks without a second crew member to cross-check — relevant for crew resource management training." |
| }, |
| { |
| "severity": "LOW", |
| "factor": "Night experience ratio", |
| "discussion_point": "200 night hours from a total of 1800 is approximately 11% night exposure — worth flagging as context for the pilot's currency and recency on night offshore operations." |
| } |
| ], |
| "assumptions_missing": [ |
| "Planned cruise altitude not stated — relevant for obstacle clearance and VFR cloud clearance minimums", |
| "Platform lighting and visual approach aids not described", |
| "No mention of life raft, survival equipment, or offshore survival training currency", |
| "Forecast trend not provided — whether conditions are improving or deteriorating during the flight", |
| "No information on whether the platform has a dedicated landing officer or AFIS service" |
| ], |
| "expert_questions": [ |
| "At what point does this scenario transition from night VFR to a flight that operationally requires IFR certification and an instrument approach at destination — and where is that regulatory boundary in French/EASA airspace?", |
| "What spatial disorientation risk mitigation techniques would you brief for a night over-water segment with overcast conditions and no visible horizon?", |
| "How would you calculate a minimum weather diversion fuel reserve for this scenario, and what airport would serve as the designated alternate?", |
| "What offshore survival equipment requirements apply to this scenario, and how would you verify currency and training for the pilot?", |
| "How does gusting crosswind at 20G30KT affect helicopter platform approach and hover technique, and what wind limits are typical for medium twin platforms?", |
| "What are the crew resource management considerations for single-pilot night offshore operations, and what workload management techniques reduce risk in this profile?" |
| ], |
| "training_verdict": "Expert Review Recommended", |
| "training_verdict_context": "This hypothetical scenario presents factors that would typically warrant Expert Review Recommended discussion in a training context, as the combination of night VFR, offshore over-water, marginal weather, and no instrument approach at destination exceeds the complexity of standard training scenario analysis.", |
| "expert_review_message": "Hi — I used an AI training brief simulator to structure this hypothetical aviation scenario. I would appreciate your expert review of the risk flags and discussion questions below. Specifically: are these the right factors to flag for this scenario, and what important consideration did the model miss?\n\nScenario: Single-pilot night VFR helicopter ferry, Marseille to fictional offshore platform 60nm south. Medium twin helicopter. Wind 20G30KT, 6km in haze, overcast 1200ft. 2.2h fuel. No instrument approach at destination.\n\nTop risk flags: night over-water with no instrument approach (HIGH), marginal VFR at night (HIGH), fuel margin (MEDIUM).\n\nKey training questions: regulatory IFR threshold for this profile, spatial disorientation mitigation, offshore survival requirements." |
| } |
|
|
| FALLBACK_EX3 = { |
| "scenario_summary": "This hypothetical scenario involves a post-maintenance test flight on a fictional regional turboprop following a major avionics upgrade, conducted by two experienced type-rated pilots with a structured test card including an engine-out simulation. The scenario is representative of a low meteorological risk but operationally high-complexity flight profile.", |
| "extracted_parameters": { |
| "departure": "Busy international airport (fictional)", |
| "destination": "Return to departure", |
| "aircraft_type": "Fictional regional turboprop", |
| "crew_experience": "Two test pilots, 4000 hours each on type", |
| "planned_altitude": "Not specified", |
| "weather_state": "CAVOK, light wind", |
| "fuel_endurance": "2 hours planned duration", |
| "special_conditions": "Post-maintenance test flight, major avionics upgrade, engine-out simulation, ILS return approach" |
| }, |
| "weather_terrain_airspace": [ |
| "The described weather (CAVOK, light wind) represents optimal conditions for a test flight, reducing environmental risk to its lowest practical level for this scenario.", |
| "Airspace in this context typically involves coordination with ATC for the test area, engine-out simulation altitude, and return ILS — a busy international airport departure requires careful integration.", |
| "Terrain profile is not a primary concern given CAVOK conditions and no mention of mountainous terrain — departure airspace complexity is the relevant factor." |
| ], |
| "risk_flags": [ |
| { |
| "severity": "HIGH", |
| "factor": "Post-maintenance avionics uncertainty", |
| "discussion_point": "The first flight following a major avionics upgrade introduces system uncertainty — displays, autopilot coupling, navigation databases, and failure modes may behave differently until the test card items are validated." |
| }, |
| { |
| "severity": "HIGH", |
| "factor": "Engine-out simulation in busy airspace", |
| "discussion_point": "Conducting an engine-out simulation at altitude near a busy international airport requires precise ATC coordination and a defined recovery area — the risk is procedural and airspace-conflict related, not meteorological." |
| }, |
| { |
| "severity": "MEDIUM", |
| "factor": "ILS approach with unvalidated avionics", |
| "discussion_point": "The return ILS approach serves as a functional check on the upgraded avionics, but an ILS coupled to a system under test introduces the possibility of unexpected behavior during the approach phase — a training-relevant discussion point." |
| }, |
| { |
| "severity": "MEDIUM", |
| "factor": "Test card sequence and abort criteria", |
| "discussion_point": "A structured test card means the crew must decide in real time when a test item anomaly is significant enough to abandon the card — this decision framework is worth discussing in the context of test flight discipline." |
| }, |
| { |
| "severity": "LOW", |
| "factor": "Crew fatigue and briefing load", |
| "discussion_point": "A comprehensive test card covering avionics checkout, engine-out, and ILS in a 2-hour flight is a high cognitive load for the crew — preflight briefing depth and crew coordination are worth examining." |
| } |
| ], |
| "assumptions_missing": [ |
| "Specific avionics system upgraded not stated — relevant to understanding which flight systems are under test", |
| "Test altitude for engine-out simulation not provided", |
| "No mention of whether a flight test engineer or observer is on board", |
| "Abort criteria for the test card not described", |
| "No information on maintenance release documentation or airworthiness sign-off process" |
| ], |
| "expert_questions": [ |
| "What are the standard go/no-go criteria for a first post-maintenance test flight, and how would you structure the pre-test briefing for a major avionics upgrade?", |
| "How do you define and communicate the test area and engine-out simulation parameters with ATC at a busy international airport — what coordination is required?", |
| "What anomaly reporting and abort criteria would you include on the test card for an avionics upgrade validation flight?", |
| "How would you validate autopilot and FMS coupling on the ILS approach, and what are the crew abort triggers if the approach behaves unexpectedly?", |
| "What role does the airworthiness release process play before this test flight, and who has authority to approve the flight following the maintenance event?", |
| "How would you structure the debrief after this test flight, and what documentation is required for the avionics system to be returned to line service?" |
| ], |
| "training_verdict": "Caution", |
| "training_verdict_context": "This hypothetical scenario presents factors that would typically warrant Caution discussion in a training context, as weather and crew experience are favorable but the post-maintenance system uncertainty and test card complexity introduce operational risks worth structured review.", |
| "expert_review_message": "Hi — I used an AI training brief simulator to structure this hypothetical aviation scenario. I would appreciate your expert review of the risk flags and discussion questions below. Specifically: are these the right factors to flag for this scenario, and what important consideration did the model miss?\n\nScenario: Fictional regional turboprop, first post-maintenance test flight after major avionics upgrade. Two test pilots, 4000h each on type. CAVOK conditions. Test card: full avionics checkout, engine-out sim at altitude, return ILS.\n\nTop risk flags: post-maintenance avionics uncertainty (HIGH), engine-out in busy airspace (HIGH), ILS with unvalidated systems (MEDIUM).\n\nKey training questions: go/no-go criteria for test flights, ATC coordination for engine-out, test card abort criteria." |
| } |
|
|
| FALLBACKS = [FALLBACK_EX1, FALLBACK_EX2, FALLBACK_EX3] |
|
|
| |
| |
| |
|
|
| VERDICT_STYLES = { |
| "Low Concern": ("background:#EAF3DE;border:1px solid #BBDFA0;border-left:4px solid #3B6D11;", "#2D5A0D", "🟢"), |
| "Caution": ("background:#FAEEDA;border:1px solid #F0C68A;border-left:4px solid #854F0B;", "#6B3D08", "🟡"), |
| "High Risk": ("background:#FCEBEB;border:1px solid #F5AAAA;border-left:4px solid #A32D2D;", "#8B1A1A", "🔴"), |
| "Expert Review Recommended": ("background:#F0EAFF;border:1px solid #C4B5FD;border-left:4px solid #5B21B6;", "#4C1D95", "🔵"), |
| } |
|
|
| SEVERITY_STYLES = { |
| "HIGH": ("background:#FCEBEB;border-left:4px solid #A32D2D;", "#A32D2D", "🔴"), |
| "MEDIUM": ("background:#FAEEDA;border-left:4px solid #854F0B;", "#854F0B", "🟡"), |
| "LOW": ("background:#EAF3DE;border-left:4px solid #3B6D11;", "#3B6D11", "🟢"), |
| } |
|
|
|
|
| def _card(content: str, style: str = "") -> str: |
| return f'<div style="border-radius:10px;padding:18px 22px;margin-bottom:14px;box-shadow:0 1px 4px rgba(0,0,0,0.06);{style}">{content}</div>' |
|
|
|
|
| def _section_label(text: str, color: str = "#374151") -> str: |
| return f'<div style="font-size:10px;font-weight:700;letter-spacing:.09em;text-transform:uppercase;color:{color};margin-bottom:10px;">{text}</div>' |
|
|
|
|
| def render_brief(data: dict) -> str: |
| parts = [] |
|
|
| verdict = data.get("training_verdict", "Caution") |
| verdict_ctx = data.get("training_verdict_context", "") |
| v_card, v_color, v_icon = VERDICT_STYLES.get(verdict, VERDICT_STYLES["Caution"]) |
|
|
| |
| parts.append( |
| f'<div style="{v_card}border-radius:10px;padding:22px 26px;margin-bottom:14px;' |
| f'box-shadow:0 2px 8px rgba(0,0,0,0.09);">' |
| f'{_section_label("Training Verdict — not an operational decision", color=v_color)}' |
| f'<div style="font-size:22px;font-weight:700;color:{v_color};margin-bottom:8px;letter-spacing:-0.3px;">' |
| f'{v_icon} {verdict}</div>' |
| f'<div style="font-size:12px;color:#4B5563;line-height:1.65;border-top:1px solid rgba(0,0,0,0.08);' |
| f'padding-top:8px;">{verdict_ctx}</div>' |
| f'</div>' |
| ) |
|
|
| |
| summary = data.get("scenario_summary", "") |
| parts.append(_card( |
| _section_label("Scenario Summary") + |
| f'<p style="font-size:13px;line-height:1.75;margin:0;color:#1F2937;">{summary}</p>', |
| style="background:#F8FAFC;border:1px solid #E2E8F0;" |
| )) |
|
|
| |
| params = data.get("extracted_parameters", {}) |
| param_labels = { |
| "departure": "Departure", |
| "destination": "Destination", |
| "aircraft_type": "Aircraft Type", |
| "crew_experience": "Crew Experience", |
| "planned_altitude": "Planned Altitude", |
| "weather_state": "Weather State", |
| "fuel_endurance": "Fuel Endurance", |
| "special_conditions": "Special Conditions", |
| } |
| rows = "" |
| for i, (key, label) in enumerate(param_labels.items()): |
| val = params.get(key, "Not specified") |
| muted = val in ("Not specified", "None mentioned") |
| val_color = "#9CA3AF" if muted else "#1F2937" |
| prefix = '<span style="color:#D97706;margin-right:4px;" title="Not specified in scenario">⚠</span>' if muted else "" |
| row_bg = "background:#F8FAFC;" if i % 2 == 0 else "background:#FFFFFF;" |
| rows += ( |
| f'<tr style="{row_bg}">' |
| f'<td style="padding:8px 14px 8px 10px;font-size:10px;font-weight:700;color:#64748B;' |
| f'white-space:nowrap;vertical-align:top;text-transform:uppercase;letter-spacing:.06em;' |
| f'width:130px;border-right:1px solid #E2E8F0;">{label}</td>' |
| f'<td style="padding:8px 12px;font-size:13px;color:{val_color};line-height:1.5;">{prefix}{val}</td>' |
| f'</tr>' |
| ) |
| parts.append(_card( |
| _section_label("Extracted Parameters") + |
| f'<div style="border:1px solid #E2E8F0;border-radius:7px;overflow:hidden;">' |
| f'<table style="border-collapse:collapse;width:100%;">{rows}</table></div>', |
| style="background:#FFFFFF;border:1px solid #E2E8F0;" |
| )) |
|
|
| |
| wta = data.get("weather_terrain_airspace", []) |
| bullets = "".join( |
| f'<li style="margin-bottom:8px;font-size:13px;line-height:1.65;color:#1E3A5F;">{item}</li>' |
| for item in wta |
| ) |
| parts.append(_card( |
| _section_label("Weather · Terrain · Airspace — Considerations for Training Discussion", color="#1D4ED8") + |
| f'<ul style="margin:0;padding-left:20px;">{bullets}</ul>', |
| style="background:#EFF6FF;border:1px solid #BFDBFE;" |
| )) |
|
|
| |
| flags = data.get("risk_flags", []) |
| flag_inner = "" |
| for flag in flags: |
| sev = flag.get("severity", "LOW").upper() |
| sty, col, sev_icon = SEVERITY_STYLES.get(sev, SEVERITY_STYLES["LOW"]) |
| badge = ( |
| f'<span style="font-size:10px;font-weight:700;color:{col};background:rgba(255,255,255,0.9);' |
| f'border:1px solid {col};border-radius:4px;padding:1px 7px;margin-right:6px;' |
| f'letter-spacing:.06em;">{sev}</span>' |
| ) |
| factor = flag.get("factor", "") |
| point = flag.get("discussion_point", "") |
| flag_inner += ( |
| f'<div style="{sty}border-radius:7px;padding:12px 16px;margin-bottom:9px;">' |
| f'<div style="font-size:13px;font-weight:600;color:{col};margin-bottom:5px;">' |
| f'{sev_icon} {badge} — {factor}</div>' |
| f'<div style="font-size:12px;color:#374151;line-height:1.6;padding-left:22px;">{point}</div>' |
| f'</div>' |
| ) |
| parts.append(_card( |
| _section_label("Risk Flags — Training discussion factors") + flag_inner, |
| style="background:#FAFAFA;border:1px solid #E5E7EB;" |
| )) |
|
|
| |
| missing = data.get("assumptions_missing", []) |
| m_bullets = "".join( |
| f'<li style="margin-bottom:6px;font-size:13px;color:#374151;line-height:1.55;">{item}</li>' |
| for item in missing |
| ) |
| parts.append(_card( |
| _section_label("Assumptions & Missing Information") + |
| f'<p style="font-size:11px;color:#6B7280;margin:0 0 8px 0;">What the model had to infer or could not determine</p>' |
| f'<ul style="margin:0;padding-left:20px;">{m_bullets}</ul>', |
| style="background:#F8FAFC;border:1px solid #D1D5DB;" |
| )) |
|
|
| |
| questions = data.get("expert_questions", []) |
| q_items = "".join( |
| f'<li style="margin-bottom:10px;font-size:13px;line-height:1.65;color:#3B1F6E;">{q}</li>' |
| for q in questions |
| ) |
| parts.append(_card( |
| _section_label("Debrief Discussion Questions", color="#5B21B6") + |
| f'<p style="font-size:11px;color:#6B7280;margin:0 0 10px 0;">For a flight instructor or safety examiner</p>' |
| f'<ol style="margin:0;padding-left:20px;">{q_items}</ol>', |
| style="background:#EEEDFE;border:1px solid #C4B5FD;" |
| )) |
|
|
| |
| msg = data.get("expert_review_message", "") |
| safe_msg = msg.replace("&", "&").replace("<", "<").replace(">", ">") |
| parts.append(_card( |
| _section_label("Expert Review Message") + |
| f'<p style="font-size:11px;color:#6B7280;margin:0 0 10px 0;">Send this to an aviation professional for review</p>' |
| f'<textarea id="expert-msg" readonly ' |
| f'style="width:100%;box-sizing:border-box;height:150px;font-size:12px;' |
| f'line-height:1.7;color:#374151;background:#F0F4FF;border:1px solid #C7D2FE;border-radius:7px;' |
| f'padding:12px 14px;resize:vertical;font-family:inherit;">{safe_msg}</textarea>' |
| f'<button onclick="navigator.clipboard.writeText(document.getElementById(\'expert-msg\').value)' |
| f'.then(()=>{{this.textContent=\'✓ Copied!\';setTimeout(()=>this.textContent=\'Copy message\',2000)}})" ' |
| f'style="margin-top:8px;padding:7px 16px;font-size:12px;font-weight:500;background:#1D4ED8;' |
| f'color:white;border:none;border-radius:6px;cursor:pointer;">Copy message</button>', |
| style="background:#FFFFFF;border:1px solid #E2E8F0;" |
| )) |
|
|
| |
| parts.append( |
| '<div style="border-left:3px solid #D97706;background:#FFFBEB;border-radius:0 8px 8px 0;' |
| 'padding:14px 18px;margin-bottom:4px;">' |
| '<span style="font-size:13px;color:#92400E;line-height:1.65;">' |
| '⚠ <strong>Educational and demonstration use only.</strong> This brief is generated by an AI language model ' |
| 'and is not a substitute for certified flight planning, official dispatch, NOTAMs, METARs, or advice from ' |
| 'a qualified aviation professional. Never use this output for real operational decisions.' |
| '</span></div>' |
| ) |
|
|
| return "\n".join(parts) |
|
|
|
|
| |
| |
| |
|
|
| def make_generic_fallback(scenario_text: str) -> dict: |
| text = scenario_text |
| tl = text.lower() |
|
|
| |
| aircraft_type = "Not specified" |
| for pat, label in [ |
| (r"cessna\s*152", "Cessna 152"), |
| (r"cessna\s*172", "Cessna 172"), |
| (r"cessna\s*182", "Cessna 182"), |
| (r"cessna\s*\d+", "Cessna (variant unspecified)"), |
| (r"piper\b", "Piper aircraft"), |
| (r"cirrus\b", "Cirrus aircraft"), |
| (r"diamond\s*da", "Diamond DA"), |
| (r"glider|sailplane", "Glider / sailplane"), |
| (r"\buav\b|\bdrone\b|quadcopter|multirotor|\buas\b", "UAV / drone"), |
| (r"turboprop", "Turboprop"), |
| (r"helicopter", "Helicopter"), |
| (r"light sport aircraft|\blsa\b", "Light sport aircraft"), |
| (r"twin.engine", "Twin-engine aircraft"), |
| (r"single.engine piston", "Single-engine piston"), |
| (r"autogyro|gyrocopter", "Autogyro"), |
| (r"balloon", "Hot air balloon"), |
| (r"light aircraft|light plane", "Light aircraft"), |
| ]: |
| if re.search(pat, tl): |
| aircraft_type = label |
| break |
|
|
| |
| crew_experience = "Not specified" |
| |
| _sentences = [s.strip() for s in re.split(r'(?<=[.!?])\s+|\n', text) if s.strip()] |
| _crew_kw = re.compile( |
| r'\b(pilot|crew|student|instructor|observer|engineer|remote operator|' |
| r'\d+\s*hours?\b|first solo|solo flight|licensed|rated|ppl|cpl|atpl)\b', re.I |
| ) |
| for _s in _sentences: |
| if _crew_kw.search(_s) and len(_s) >= 10: |
| _s_clean = _s.lstrip(',-: ') |
| if re.match(r'^[A-Za-z0-9"]', _s_clean): |
| crew_experience = _s_clean[:150] |
| break |
|
|
| |
| weather_state = "Not specified" |
| |
| _wm = re.search(r'weather(?:\s+is|\s+state\s*:|:)\s*([^.!?\n]{5,})', text, re.I) |
| if _wm: |
| weather_state = _wm.group(1).strip()[:180] |
| else: |
| |
| _wx_kw = re.compile( |
| r'cavok|visibility|wind|cloud|overcast|broken|scattered|haze|fog|' |
| r'icing|turbulence|thermal|gusting|imc|vmc|mvfr|precipitation', re.I |
| ) |
| _best_sent, _best_n = None, 0 |
| for _s in _sentences: |
| _n = len(_wx_kw.findall(_s)) |
| if _n > _best_n: |
| _best_n, _best_sent = _n, _s |
| if _best_sent and _best_n >= 1: |
| weather_state = _best_sent.strip()[:180] |
|
|
| |
| fuel_endurance = "Not specified" |
| _fm = re.search( |
| r'(battery\s+endurance\s*:\s*[\d\.]+\s*(?:h(?:ours?)?|min(?:utes?)?)|' |
| r'fuel\s+endurance\s*:\s*[\d\.]+\s*(?:h(?:ours?)?|min(?:utes?)?)|' |
| r'[\d\.]+\s*h(?:ours?)?\s*fuel\s+endurance|' |
| r'[\d\.]+\s*h(?:ours?)?\s*(?:of\s+)?(?:fuel|endurance)|' |
| r'endurance\s*:\s*[\d\.]+\s*(?:h(?:ours?)?|min(?:utes?)?)|' |
| r'fuel\s*:\s*[\d\.]+\s*h(?:ours?)?|' |
| r'battery\s*:\s*[\d\.]+\s*(?:h(?:ours?)?|min(?:utes?)?))', |
| text, re.I |
| ) |
| if _fm: |
| fuel_endurance = _fm.group(0).strip()[:80] |
|
|
| |
| planned_altitude = "Not specified" |
| m = re.search(r"([\d,]+\s*ft(?:\.?\s|$)|flight level\s*\d+|\bfl\s*\d+|[\d,]+\s*feet)", tl) |
| if m: |
| planned_altitude = text[m.start() : m.end()].strip()[:60] |
|
|
| |
| departure = "Not specified" |
| destination = "Not specified" |
| m = re.search(r"from\s+([A-Z][a-zA-Z\s\-]{2,30})\s+to\s+([A-Z][a-zA-Z\s\-]{2,30})", text) |
| if m: |
| departure = m.group(1).strip() |
| destination = m.group(2).strip() |
|
|
| |
| special_map = [ |
| (r"mountain|alpine|cordillera", "Mountain terrain"), |
| (r"\bvalley\b|confined area", "Valley / confined area"), |
| (r"\bridge\b", "Ridge soaring / terrain proximity"), |
| (r"offshore|over.water", "Offshore / over-water"), |
| (r"\bnight\b", "Night operation"), |
| (r"\buav\b|\bdrone\b|\buas\b", "UAV / drone operation"), |
| (r"prototype|\bfirst flight\b|\bmaiden\b", "Prototype / first flight"), |
| (r"low visibility|reduced visibility", "Low visibility"), |
| (r"crosswind", "Crosswind conditions"), |
| (r"no alternate|without alternate", "No designated alternate"), |
| (r"\bturbulence\b", "Turbulence"), |
| (r"\bsink\b|sink.rate", "Sink / sink-rate consideration"), |
| (r"\binstructor\b|\bdual\b", "Instructor on board"), |
| (r"return.to.home|\brth\b", "Return-to-home (RTH) dependency"), |
| (r"manual takeover|manual override", "Manual takeover"), |
| (r"emergency landing zone|\belz\b", "Emergency landing zone"), |
| (r"no ifr|no instrument|without instrument", "No instrument capability"), |
| (r"\bsolo\b", "Solo flight"), |
| (r"local circuit|circuit training|pattern work|touch.and.go", "Local circuit training"), |
| (r"uncontrolled airfield|uncontrolled aerodrome|non.towered|\bctaf\b|\bunicom\b", "Uncontrolled airfield"), |
| (r"test card|avionics checkout|system test", "Test card operations"), |
| ] |
| specials = [label for pat, label in special_map if re.search(pat, tl)] |
| special_conditions = ", ".join(specials) if specials else "None mentioned" |
|
|
| |
| flags = [] |
|
|
| if re.search(r"mountain|ridge|valley|high terrain|alpine|cordillera", tl): |
| flags.append({ |
| "severity": "MEDIUM", |
| "factor": "Terrain proximity", |
| "discussion_point": "Mountain, ridge, or valley operations introduce terrain clearance concerns, orographic turbulence, and limited diversion options — key training discussion factors for this scenario type.", |
| }) |
|
|
| if re.search(r"\bnight\b", tl): |
| flags.append({ |
| "severity": "HIGH" if re.search(r"offshore|over.water|no alternate|haze|overcast", tl) else "MEDIUM", |
| "factor": "Night operation", |
| "discussion_point": "Night operations reduce visual reference and situational awareness — the training discussion should cover personal minimums, recency, and contingency planning specific to this profile.", |
| }) |
|
|
| if re.search(r"low visibility|haze|fog|overcast|broken|turbulence|gusting|icing|imc|mvfr", tl): |
| flags.append({ |
| "severity": "MEDIUM", |
| "factor": "Marginal weather conditions", |
| "discussion_point": "The described weather involves reduced visibility, cloud cover, or turbulence — discussion should address weather decision-making and personal minimums for this aircraft and pilot profile.", |
| }) |
|
|
| if re.search(r"no alternate|without alternate|no diversion|no instrument approach|no ils", tl): |
| flags.append({ |
| "severity": "HIGH", |
| "factor": "Limited diversion options", |
| "discussion_point": "The scenario describes limited or no alternate options — training discussion should address contingency planning and decision thresholds for this hypothetical profile.", |
| }) |
|
|
| if re.search(r"student pilot|first solo|limited experience|low hours|ppl student", tl): |
| flags.append({ |
| "severity": "MEDIUM", |
| "factor": "Pilot experience level", |
| "discussion_point": "The scenario involves a pilot with limited experience — discussion should address experience-appropriate task loading, supervision, and scenario complexity.", |
| }) |
|
|
| if re.search(r"\buav\b|\bdrone\b|quadcopter|multirotor|\buas\b", tl): |
| flags.append({ |
| "severity": "MEDIUM", |
| "factor": "UAV / UAS operation", |
| "discussion_point": "UAV / UAS operations involve return-to-home dependencies, manual takeover thresholds, lost-link procedures, and emergency landing zone identification — all relevant training discussion points.", |
| }) |
| if re.search(r"test|prototype|first flight|maiden", tl): |
| _uav_test_sev = "HIGH" if re.search( |
| r"no.*elz|elz.*not|elz.*miss|not.*defined.*elz|emergency landing zone.*not|" |
| r"missing.*emergency|without.*emergency landing|no.*emergency landing zone", tl |
| ) else "MEDIUM" |
| flags.append({ |
| "severity": _uav_test_sev, |
| "factor": "UAV test / prototype flight", |
| "discussion_point": "A test or prototype UAV flight introduces system uncertainty and fault modes not covered by standard checklists — test card, abort criteria, and manual recovery procedures warrant discussion.", |
| }) |
|
|
| if re.search(r"glider|sailplane", tl): |
| flags.append({ |
| "severity": "MEDIUM", |
| "factor": "Glider / ridge / thermal operations", |
| "discussion_point": "Ridge and thermal soaring involves terrain proximity, sink-rate contingencies, and low-altitude decision points — all central training discussion factors for glider operations.", |
| }) |
|
|
| has_mitigation = bool(re.search(r"\binstructor\b|\bdual\b|supervised|cavok|clear skies", tl)) |
| if has_mitigation: |
| flags.append({ |
| "severity": "LOW", |
| "factor": "Instructor presence / favorable conditions", |
| "discussion_point": "Instructor presence and/or favorable conditions are mitigating factors — the training discussion should weigh these against any identified risk elements.", |
| }) |
|
|
| if fuel_endurance != "Not specified" or re.search(r"low fuel|minimum fuel|limited fuel", tl): |
| flags.append({ |
| "severity": "LOW", |
| "factor": "Fuel / energy endurance", |
| "discussion_point": "Fuel or battery endurance margins are worth examining — the training discussion should address reserve requirements and the implications of diversion or delay.", |
| }) |
|
|
| if not flags: |
| flags.append({ |
| "severity": "MEDIUM", |
| "factor": "Factors not fully extracted", |
| "discussion_point": "The model could not identify specific risk conditions — an instructor should review the scenario text directly to identify the relevant training discussion points.", |
| }) |
|
|
| |
| high_n = sum(1 for f in flags if f["severity"] == "HIGH") |
| med_n = sum(1 for f in flags if f["severity"] == "MEDIUM") |
|
|
| if high_n >= 2: |
| verdict = "Expert Review Recommended" |
| elif high_n == 1 and med_n >= 1: |
| verdict = "High Risk" |
| elif high_n == 1: |
| verdict = "Caution" |
| elif med_n >= 3: |
| verdict = "High Risk" |
| elif med_n >= 1 and not has_mitigation: |
| verdict = "Caution" |
| elif med_n >= 1: |
| verdict = "Caution" |
| elif all(f["severity"] == "LOW" for f in flags): |
| verdict = "Low Concern" |
| else: |
| verdict = "Low Concern" |
|
|
| |
| _benign_local = ( |
| has_mitigation |
| and high_n == 0 |
| and not re.search( |
| r"mountain|ridge|night|offshore|over.water|low visibility|haze|fog|" |
| r"turbulence|gusting|icing|no alternate|imc|mvfr|overcast|broken", tl |
| ) |
| and re.search(r"cavok|clear skies?|light wind|\bcalm\b|local|circuit|training flight", tl) |
| ) |
| if _benign_local and med_n <= 1: |
| verdict = "Low Concern" |
|
|
| verdict_ctx = ( |
| f"This hypothetical scenario presents factors that would typically warrant {verdict} " |
| f"discussion in a training context, based on the detected operation type and conditions." |
| ) |
|
|
| |
| wta = [] |
| if re.search(r"cavok|clear skies|vmc|light wind|calm", tl): |
| wta.append("The described weather appears favorable — meteorological risk is a low factor in this training discussion, allowing focus on other scenario elements.") |
| elif weather_state != "Not specified": |
| _wx_display = re.sub(r'^(?:weather(?:\s+(?:is|shows|state|conditions?))?[:.\s]+)', '', weather_state, flags=re.I).strip().rstrip('.') |
| wta.append(f"The described weather conditions ({_wx_display}) are a training-relevant factor — instructors should discuss how these conditions affect the planned operation.") |
| else: |
| wta.append("Weather conditions are not clearly specified — the training discussion should establish what weather state was assumed and whether it was appropriate for the operation.") |
|
|
| if re.search(r"mountain|ridge|valley|terrain|hill|slope|alpine", tl): |
| wta.append("Terrain profile implies elevated obstacle and turbulence risk — orographic effects, wind shear, and limited diversion fields are relevant training discussion points.") |
| elif re.search(r"offshore|over.water|ocean|\bsea\b", tl): |
| wta.append("Over-water operations introduce reduced visual reference, limited diversion options, and survival equipment considerations.") |
| else: |
| wta.append("Airspace and terrain considerations should be reviewed by an instructor familiar with the described operating area.") |
|
|
| if re.search(r"controlled airspace|class [abcde]\b|tma|ctr|restricted|prohibited|danger area", tl): |
| wta.append("Airspace in this context appears to involve controlled or restricted areas — ATC coordination and authorization are training-relevant factors.") |
|
|
| |
| acft = aircraft_type if aircraft_type != "Not specified" else "the aircraft described" |
| questions = [] |
|
|
| if re.search(r"\buav\b|\bdrone\b|\buas\b", tl): |
| questions += [ |
| f"What return-to-home and lost-link procedures apply to this {acft} scenario, and how would you brief them before flight?", |
| "How do you identify and communicate an emergency landing zone for a UAV / UAS operation in this environment?", |
| "At what point would you initiate a manual takeover in this scenario, and what training is required for that transition?", |
| ] |
| elif re.search(r"glider|sailplane", tl): |
| questions += [ |
| f"How would you assess ridge lift and thermal conditions for a {acft} in this terrain environment, and communicate them to a student?", |
| f"What are the sink-rate contingency plans for a {acft} in this scenario, and how do you set a low-altitude decision commitment point?", |
| "At what altitude above terrain would you call a hard turn-back or landing commitment in this scenario?", |
| ] |
| elif re.search(r"helicopter", tl): |
| questions += [ |
| f"What autorotation considerations are relevant for the {acft} in this scenario, and how would you brief the forced-landing profile?", |
| "How does crew resource management apply to single-pilot helicopter operations of this type?", |
| ] |
| else: |
| questions += [ |
| f"What preflight preparation and decision criteria would you set for a {acft} in this type of scenario?", |
| f"How would you structure the pre-flight assessment discussion for a training scenario with these characteristics?", |
| ] |
|
|
| if re.search(r"mountain|ridge|terrain", tl): |
| questions.append("What personal altitude and weather minimums would you establish for terrain operations of this type, and how would you communicate them to a student?") |
| if re.search(r"\bnight\b", tl): |
| questions.append("What recency and currency requirements would you verify before a night flight of this profile, and how would they affect the training discussion?") |
| if re.search(r"student|solo|limited experience", tl): |
| questions.append("How does pilot experience level affect the risk profile of this scenario, and what supervision or dual instruction would be appropriate?") |
| if re.search(r"fuel|battery|endurance", tl): |
| questions.append("What fuel or energy reserve margins are appropriate for this scenario, and how would a diversion or delay affect the planning?") |
| if re.search(r"test|prototype|maiden|avionics|upgrade", tl): |
| questions.append("What abort criteria would you include in the test card for this scenario, and who has authority to call off the test?") |
|
|
| questions.append("What is the most important training discussion point this scenario raises, and how would you structure a debrief around it?") |
|
|
| seen: set = set() |
| questions = [q for q in questions if not (q in seen or seen.add(q))][:6] |
|
|
| |
| missing = [] |
| if departure == "Not specified": |
| missing.append("Departure location not specified — relevant for airspace and terrain context.") |
| if destination == "Not specified": |
| missing.append("Destination not specified — relevant for route planning and diversion options.") |
| if planned_altitude == "Not specified": |
| missing.append("Planned altitude not specified — required for terrain clearance and airspace assessment.") |
| if weather_state == "Not specified": |
| missing.append("Weather state not specified — the training discussion should establish assumed conditions.") |
| if fuel_endurance == "Not specified": |
| missing.append("Fuel or battery endurance not specified — reserve margins cannot be assessed without this.") |
| if not missing: |
| missing.append("Key parameters appear to have been specified in the scenario text.") |
| missing.append("Regulatory framework and airspace classification assumed from context — not confirmed.") |
|
|
| |
| conds = [] |
| if re.search(r"\bnight\b", tl): conds.append("night conditions") |
| if re.search(r"mountain|ridge|alpine", tl): conds.append("mountainous terrain") |
| if re.search(r"offshore|over.water", tl): conds.append("over-water environment") |
| if re.search(r"\buav\b|\bdrone\b", tl): conds.append("UAV / UAS operations") |
| acft_clause = f" in a {aircraft_type}" if aircraft_type != "Not specified" else "" |
| cond_clause = f", involving {' and '.join(conds[:2])}," if conds else "" |
| summary = ( |
| f"This hypothetical scenario{acft_clause}{cond_clause} has been submitted for training discussion and structured review. " |
| f"The brief below extracts available parameters from the scenario text and generates training-relevant risk flags and discussion questions for expert review." |
| ) |
|
|
| |
| top_flags = ", ".join(f"{f['factor']} ({f['severity']})" for f in flags[:3]) |
| expert_msg = ( |
| "Hi — I used an AI training brief simulator to structure this hypothetical aviation scenario. " |
| "I would appreciate your expert review of the risk flags and discussion questions below. " |
| "Specifically: are these the right factors to flag for this scenario, and what important consideration did the model miss?\n\n" |
| f"Scenario (as entered): {scenario_text.strip()}\n\n" |
| f"Aircraft identified: {aircraft_type}\n" |
| f"Top risk flags: {top_flags or 'none extracted'}\n\n" |
| "Please review the debrief questions and let me know if the risk framing is appropriate for this training context." |
| ) |
|
|
| return { |
| "scenario_summary": summary, |
| "extracted_parameters": { |
| "departure": departure, |
| "destination": destination, |
| "aircraft_type": aircraft_type, |
| "crew_experience": crew_experience, |
| "planned_altitude": planned_altitude, |
| "weather_state": weather_state, |
| "fuel_endurance": fuel_endurance, |
| "special_conditions": special_conditions, |
| }, |
| "weather_terrain_airspace": wta, |
| "risk_flags": flags, |
| "assumptions_missing": missing, |
| "expert_questions": questions, |
| "training_verdict": verdict, |
| "training_verdict_context": verdict_ctx, |
| "expert_review_message": expert_msg, |
| } |
|
|
|
|
| def select_fallback(scenario_text: str) -> dict: |
| tl = scenario_text.lower() |
|
|
| |
| if ( |
| ("grenoble" in tl and "turin" in tl) |
| or ("alps" in tl and "6500ft" in tl) |
| or ("cessna 172" in tl and "grenoble" in tl) |
| ): |
| return FALLBACK_EX1 |
|
|
| |
| if ( |
| ("marseille" in tl and "offshore" in tl) |
| or "220/20g30kt" in tl |
| or ("helicopter" in tl and "offshore platform" in tl) |
| ): |
| return FALLBACK_EX2 |
|
|
| |
| if ( |
| ("turboprop" in tl and "avionics upgrade" in tl) |
| or ("engine-out simulation" in tl and "ils approach" in tl) |
| or ("post-maintenance" in tl and "avionics" in tl) |
| ): |
| return FALLBACK_EX3 |
|
|
| return make_generic_fallback(scenario_text) |
|
|
|
|
| |
| |
| |
|
|
| def call_model(scenario: str) -> dict: |
| if not scenario.strip(): |
| return {} |
|
|
| client = InferenceClient(token=HF_TOKEN) |
| try: |
| response = client.chat_completion( |
| model=MODEL, |
| messages=[ |
| {"role": "system", "content": SYSTEM_PROMPT}, |
| {"role": "user", "content": f"Analyze this hypothetical aviation scenario for training purposes:\n\n{scenario}"}, |
| ], |
| max_tokens=1200, |
| temperature=0.3, |
| ) |
| raw = response.choices[0].message.content.strip() |
| |
| if raw.startswith("```"): |
| raw = raw.split("```")[1] |
| if raw.startswith("json"): |
| raw = raw[4:] |
| return json.loads(raw) |
| except Exception: |
| return select_fallback(scenario) |
|
|
|
|
| |
| |
| |
|
|
| def generate(scenario: str) -> str: |
| if not scenario.strip(): |
| return '<p style="color:#6B7280;padding:20px;text-align:center;">Enter a hypothetical scenario above and click Generate.</p>' |
| data = call_model(scenario) |
| if not data: |
| return '<p style="color:#6B7280;padding:20px;text-align:center;">No output — please try again.</p>' |
| return render_brief(data) |
|
|
|
|
| |
| |
| |
|
|
| css = """ |
| footer { display: none !important; } |
| #flightbrief-hero { |
| background: linear-gradient(135deg, #0F2142 0%, #1A3561 100%); |
| border-radius: 12px; |
| padding: 28px 32px; |
| margin-bottom: 20px; |
| } |
| #fb-title { |
| font-size: 24px; |
| font-weight: 700; |
| color: #FFFFFF; |
| letter-spacing: -0.3px; |
| margin-bottom: 6px; |
| } |
| #fb-subtitle { |
| font-size: 14px; |
| color: #94A3B8; |
| margin-bottom: 14px; |
| line-height: 1.5; |
| } |
| .fb-badge { |
| display: inline-block; |
| font-size: 11px; |
| font-weight: 600; |
| padding: 3px 11px; |
| border-radius: 20px; |
| margin-right: 6px; |
| margin-bottom: 4px; |
| } |
| #fb-safety-note { |
| border-left: 3px solid #D97706; |
| background: #FFFBEB; |
| padding: 9px 13px; |
| border-radius: 0 6px 6px 0; |
| margin-bottom: 14px; |
| font-size: 12px; |
| color: #92400E; |
| line-height: 1.5; |
| } |
| """ |
|
|
| _GR_MAJOR = int(gr.__version__.split(".")[0]) |
| |
| _theme = gr.themes.Soft(primary_hue="sky", font=["Inter", "sans-serif"]) |
| _blocks_kw = {} if _GR_MAJOR >= 6 else {"theme": _theme, "css": css} |
| _launch_kw = {"theme": _theme, "css": css} if _GR_MAJOR >= 6 else {} |
|
|
| with gr.Blocks(**_blocks_kw) as demo: |
| gr.HTML(""" |
| <div id="flightbrief-hero"> |
| <div id="fb-title">✈ FlightBrief — Aviation Scenario Briefing Simulator</div> |
| <div id="fb-subtitle">Turn any hypothetical flight scenario into a structured training brief — for learning, discussion, and expert review.</div> |
| <div> |
| <span class="fb-badge" style="background:#1D4ED8;color:white;">🏠 Backyard AI</span> |
| <span class="fb-badge" style="background:#374151;color:#D1D5DB;">🤖 Small model ≤32B</span> |
| <span class="fb-badge" style="background:#7C2D12;color:#FEF3C7;">📋 Educational demo only</span> |
| </div> |
| </div> |
| """) |
|
|
| with gr.Row(): |
| with gr.Column(scale=2): |
| gr.HTML('<div id="fb-safety-note">📋 <strong>Educational and demo use only</strong> — not for real flight planning or operational decisions.</div>') |
| scenario_box = gr.Textbox( |
| label="Hypothetical flight scenario", |
| lines=7, |
| placeholder=( |
| "Describe a hypothetical aviation scenario in plain English.\n" |
| "Include any relevant details: aircraft type, route, altitude,\n" |
| "weather, crew experience, fuel, special conditions.\n\n" |
| "Example: 'Hypothetical scenario: a student pilot plans\n" |
| "a VFR crossing of the Alps at 6500ft in October…'" |
| ), |
| ) |
|
|
| with gr.Row(): |
| btn_ex1 = gr.Button("Demo 1: Student Alps", size="sm") |
| btn_ex2 = gr.Button("Demo 2: Night Ferry", size="sm") |
| btn_ex3 = gr.Button("Demo 3: Test Flight", size="sm") |
|
|
| generate_btn = gr.Button("Generate Training Brief ▶", variant="primary") |
|
|
| with gr.Column(scale=3): |
| output_html = gr.HTML( |
| '<div style="color:#9CA3AF;padding:48px 32px;text-align:center;">' |
| '<div style="font-size:32px;margin-bottom:12px;">✈</div>' |
| '<div style="font-size:14px;font-weight:500;margin-bottom:6px;color:#6B7280;">Your training brief will appear here.</div>' |
| '<div style="font-size:12px;color:#9CA3AF;">Select a demo or enter a hypothetical scenario, then click Generate.</div>' |
| '</div>' |
| ) |
|
|
| |
| btn_ex1.click(fn=lambda: EX1, outputs=scenario_box) |
| btn_ex2.click(fn=lambda: EX2, outputs=scenario_box) |
| btn_ex3.click(fn=lambda: EX3, outputs=scenario_box) |
|
|
| |
| generate_btn.click(fn=generate, inputs=scenario_box, outputs=output_html) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch(**_launch_kw) |
|
|