nakas Claude commited on
Commit
7d4fcc3
·
1 Parent(s): 92dbe12

Implement natural NOAA-style weather text forecasting

Browse files

- Replace mechanical bullet points with flowing narrative paragraphs
- Add comprehensive overview paragraph with weather pattern analysis
- Implement period-based forecasts (Today, Tonight, Tomorrow, etc.)
- Create natural timing narratives instead of repetitive alerts
- Use authentic NOAA phraseology and sentence structure
- Generate contextual weather descriptions combining conditions

Features natural language like:
"A persistent weather system will bring frequent periods of rain,
with some heavy downpours possible. Temperatures will be mild
with highs near 21°C and overnight lows dropping to 9°C."

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +222 -55
app.py CHANGED
@@ -663,72 +663,239 @@ def analyze_weather_events(forecast_data):
663
  def generate_forecast_text(forecast_data, location_name="Selected Location"):
664
  """
665
  Generate NOAA-style forecast text from gridded data
666
- Similar to NWS Zone Forecast Product (ZFP) formatting
667
  """
668
  events = analyze_weather_events(forecast_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
 
670
- # Group events by time periods
671
- periods = []
 
 
 
 
 
 
 
 
672
  current_time = datetime.now()
 
673
 
674
- # Define forecast periods
675
- period_definitions = [
676
- ('This Afternoon', 12, 18),
677
- ('Tonight', 18, 6),
678
- ('Tomorrow', 6, 18),
679
- ('Tomorrow Night', 18, 6),
680
- ('Day 3', 6, 18),
681
- ('Day 3 Night', 18, 6),
682
- ('Day 4', 6, 18)
683
- ]
684
-
685
- forecast_text = f"**Weather Forecast for {location_name}**\n\n"
686
- forecast_text += f"Issued: {current_time.strftime('%A, %B %d, %Y at %I:%M %p %Z')}\n\n"
687
-
688
- # Process each time period
689
- for period_name, start_hour, end_hour in period_definitions:
690
- period_events = []
691
- period_temps = []
692
-
693
- # Find events in this period
694
- for event in events:
695
- event_hour = event['time'].hour
696
-
697
- if start_hour <= end_hour: # Same day period
698
- if start_hour <= event_hour < end_hour:
699
- period_events.append(event)
700
- else: # Overnight period
701
- if event_hour >= start_hour or event_hour < end_hour:
702
- period_events.append(event)
703
-
704
- if not period_events:
705
- continue
706
-
707
- # Get temperature range for period
708
- period_temps = [forecast_data['temperature'][i] for i, ts in enumerate(forecast_data['timestamps'])
709
- if ts in [e['time'] for e in period_events]]
710
 
711
- if period_temps:
712
- min_temp = min(period_temps)
713
- max_temp = max(period_temps)
714
- else:
 
 
 
 
 
 
 
 
715
  continue
716
 
717
- # Generate period text
718
- period_text = generate_period_text(period_name, period_events, min_temp, max_temp)
719
- forecast_text += period_text + "\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
 
721
- # Add specific event timing predictions
722
- timing_alerts = generate_timing_alerts(events)
723
- if timing_alerts:
724
- forecast_text += "**Timing Details:**\n" + timing_alerts + "\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
725
 
726
- # Add weather advisories
727
- advisories = generate_advisories(events)
728
- if advisories:
729
- forecast_text += "**Weather Advisories:**\n" + advisories + "\n\n"
 
 
 
 
 
 
 
 
 
 
 
 
 
730
 
731
- return forecast_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
732
 
733
  def generate_period_text(period_name, events, min_temp, max_temp):
734
  """
 
663
  def generate_forecast_text(forecast_data, location_name="Selected Location"):
664
  """
665
  Generate NOAA-style forecast text from gridded data
666
+ Natural flowing language similar to NWS Zone Forecast Products
667
  """
668
  events = analyze_weather_events(forecast_data)
669
+ current_time = datetime.now()
670
+
671
+ # Analyze overall conditions and trends
672
+ temperatures = forecast_data['temperature']
673
+ precipitation = forecast_data.get('precipitation', [0] * len(temperatures))
674
+ wind_speeds = forecast_data['wind_speed']
675
+ humidity = forecast_data['humidity']
676
+
677
+ # Calculate key statistics
678
+ max_temp = max(temperatures)
679
+ min_temp = min(temperatures)
680
+ avg_precip = sum(precipitation) / len(precipitation) if precipitation else 0
681
+ max_wind = max(wind_speeds)
682
+
683
+ # Determine dominant weather pattern
684
+ rain_hours = sum(1 for p in precipitation if p > 0.1)
685
+ heavy_rain_hours = sum(1 for p in precipitation if p > 2.0)
686
+ windy_hours = sum(1 for w in wind_speeds if w > 10)
687
+
688
+ forecast_text = f"**{location_name} Extended Forecast**\n\n"
689
+ forecast_text += f"Issued {current_time.strftime('%A %B %d, %Y at %I:%M %p')}\n\n"
690
+
691
+ # Generate overview paragraph
692
+ overview = generate_overview_paragraph(rain_hours, heavy_rain_hours, windy_hours, max_temp, min_temp, max_wind, len(temperatures))
693
+ forecast_text += overview + "\n\n"
694
+
695
+ # Generate detailed daily forecasts with natural language
696
+ daily_forecasts = generate_daily_detailed_forecasts(forecast_data, events)
697
+ forecast_text += daily_forecasts
698
+
699
+ # Add specific timing information in narrative form
700
+ timing_narrative = generate_timing_narrative(events, precipitation, forecast_data['timestamps'])
701
+ if timing_narrative:
702
+ forecast_text += "\n" + timing_narrative + "\n"
703
+
704
+ # Add any significant weather advisories
705
+ advisories = generate_advisories(events)
706
+ if advisories:
707
+ forecast_text += "\n**Weather Advisories:**\n" + advisories
708
+
709
+ return forecast_text
710
+
711
+ def generate_overview_paragraph(rain_hours, heavy_rain_hours, windy_hours, max_temp, min_temp, max_wind, total_hours):
712
+ """Generate a natural overview paragraph like NOAA"""
713
+ overview_parts = []
714
+
715
+ # Temperature narrative
716
+ if max_temp > 25:
717
+ temp_desc = f"warm with highs reaching {max_temp:.0f}°C"
718
+ elif max_temp < 10:
719
+ temp_desc = f"cool with highs only reaching {max_temp:.0f}°C"
720
+ else:
721
+ temp_desc = f"mild with highs near {max_temp:.0f}°C"
722
+
723
+ if min_temp < 0:
724
+ temp_desc += f" and overnight lows dropping to {min_temp:.0f}°C"
725
+ elif abs(max_temp - min_temp) > 15:
726
+ temp_desc += f" with significant cooling overnight to {min_temp:.0f}°C"
727
+
728
+ # Weather pattern narrative
729
+ if rain_hours > total_hours * 0.6:
730
+ if heavy_rain_hours > 3:
731
+ weather_pattern = "A persistent weather system will bring frequent periods of rain, with some heavy downpours possible"
732
+ else:
733
+ weather_pattern = "Unsettled weather with rain likely through much of the forecast period"
734
+ elif rain_hours > total_hours * 0.3:
735
+ weather_pattern = "Scattered showers and periods of rain expected"
736
+ elif rain_hours > 0:
737
+ weather_pattern = "A few light showers possible"
738
+ else:
739
+ weather_pattern = "Generally dry conditions expected"
740
 
741
+ # Wind narrative
742
+ if max_wind > 15:
743
+ weather_pattern += f", accompanied by gusty winds up to {max_wind:.0f} m/s"
744
+ elif windy_hours > total_hours * 0.4:
745
+ weather_pattern += " with breezy conditions at times"
746
+
747
+ return f"{weather_pattern}. Temperatures will be {temp_desc}."
748
+
749
+ def generate_daily_detailed_forecasts(forecast_data, events):
750
+ """Generate detailed daily forecasts in natural NOAA style"""
751
  current_time = datetime.now()
752
+ forecasts = []
753
 
754
+ # Group data by days
755
+ timestamps = forecast_data['timestamps']
756
+ temperatures = forecast_data['temperature']
757
+ precipitation = forecast_data.get('precipitation', [0] * len(temperatures))
758
+ wind_speeds = forecast_data['wind_speed']
759
+ humidity = forecast_data['humidity']
760
+
761
+ # Process 4 days of forecasts
762
+ for day_offset in range(4):
763
+ target_date = current_time + timedelta(days=day_offset)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
 
765
+ # Get day and night periods
766
+ day_start = target_date.replace(hour=6, minute=0, second=0, microsecond=0)
767
+ day_end = target_date.replace(hour=18, minute=0, second=0, microsecond=0)
768
+ night_end = (target_date + timedelta(days=1)).replace(hour=6, minute=0, second=0, microsecond=0)
769
+
770
+ # Find data for this day
771
+ day_indices = [i for i, ts in enumerate(timestamps)
772
+ if day_start <= ts < day_end]
773
+ night_indices = [i for i, ts in enumerate(timestamps)
774
+ if day_end <= ts < night_end]
775
+
776
+ if not day_indices and not night_indices:
777
  continue
778
 
779
+ # Day period
780
+ if day_indices:
781
+ day_temps = [temperatures[i] for i in day_indices]
782
+ day_precip = [precipitation[i] for i in day_indices if i < len(precipitation)]
783
+ day_winds = [wind_speeds[i] for i in day_indices]
784
+
785
+ day_high = max(day_temps) if day_temps else 20
786
+ day_rain = any(p > 0.1 for p in day_precip)
787
+ day_heavy_rain = any(p > 2.0 for p in day_precip)
788
+ day_windy = any(w > 12 for w in day_winds)
789
+
790
+ # Generate day forecast
791
+ if day_offset == 0:
792
+ period_name = "Today"
793
+ elif day_offset == 1:
794
+ period_name = "Tomorrow"
795
+ else:
796
+ period_name = target_date.strftime("%A")
797
+
798
+ day_forecast = generate_period_narrative(period_name, day_high, None, day_rain, day_heavy_rain, day_windy, True)
799
+ forecasts.append(day_forecast)
800
+
801
+ # Night period
802
+ if night_indices:
803
+ night_temps = [temperatures[i] for i in night_indices]
804
+ night_precip = [precipitation[i] for i in night_indices if i < len(precipitation)]
805
+ night_winds = [wind_speeds[i] for i in night_indices]
806
+
807
+ night_low = min(night_temps) if night_temps else 10
808
+ night_rain = any(p > 0.1 for p in night_precip)
809
+ night_heavy_rain = any(p > 2.0 for p in night_precip)
810
+ night_windy = any(w > 12 for w in night_winds)
811
+
812
+ # Generate night forecast
813
+ if day_offset == 0:
814
+ night_name = "Tonight"
815
+ elif day_offset == 1:
816
+ night_name = "Tomorrow Night"
817
+ else:
818
+ night_name = f"{target_date.strftime('%A')} Night"
819
+
820
+ night_forecast = generate_period_narrative(night_name, None, night_low, night_rain, night_heavy_rain, night_windy, False)
821
+ forecasts.append(night_forecast)
822
 
823
+ return '\n\n'.join(forecasts)
824
+
825
+ def generate_period_narrative(period_name, high_temp, low_temp, has_rain, heavy_rain, windy, is_day):
826
+ """Generate natural narrative for a specific period"""
827
+ conditions = []
828
+
829
+ # Weather conditions
830
+ if heavy_rain:
831
+ conditions.append("heavy rain at times")
832
+ elif has_rain:
833
+ conditions.append("periods of rain")
834
+
835
+ if windy:
836
+ if conditions:
837
+ conditions.append("gusty winds")
838
+ else:
839
+ conditions.append("breezy conditions")
840
 
841
+ # Build the narrative
842
+ if is_day:
843
+ if conditions:
844
+ weather_text = f"**{period_name}:** {', '.join(conditions).capitalize()}"
845
+ else:
846
+ weather_text = f"**{period_name}:** Partly cloudy"
847
+
848
+ if high_temp:
849
+ weather_text += f". High {high_temp:.0f}°C"
850
+ else:
851
+ if conditions:
852
+ weather_text = f"**{period_name}:** {', '.join(conditions).capitalize()}"
853
+ else:
854
+ weather_text = f"**{period_name}:** Mostly clear"
855
+
856
+ if low_temp:
857
+ weather_text += f". Low {low_temp:.0f}°C"
858
 
859
+ weather_text += "."
860
+ return weather_text
861
+
862
+ def generate_timing_narrative(events, precipitation, timestamps):
863
+ """Generate narrative timing information rather than bullet points"""
864
+ if not events or not any(p > 0.1 for p in precipitation):
865
+ return ""
866
+
867
+ # Find rain periods
868
+ rain_periods = []
869
+ in_rain = False
870
+ rain_start = None
871
+
872
+ for i, (ts, precip) in enumerate(zip(timestamps, precipitation)):
873
+ if precip > 0.1 and not in_rain:
874
+ rain_start = ts
875
+ in_rain = True
876
+ elif precip <= 0.1 and in_rain:
877
+ rain_periods.append((rain_start, timestamps[i-1]))
878
+ in_rain = False
879
+
880
+ if in_rain and rain_start:
881
+ rain_periods.append((rain_start, timestamps[-1]))
882
+
883
+ if not rain_periods:
884
+ return ""
885
+
886
+ # Create narrative
887
+ if len(rain_periods) == 1:
888
+ start, end = rain_periods[0]
889
+ return f"Rain expected from approximately {start.strftime('%I %p')} through {end.strftime('%I %p')}."
890
+ elif len(rain_periods) <= 3:
891
+ timing_text = "Periods of rain expected "
892
+ times = []
893
+ for start, end in rain_periods:
894
+ times.append(f"{start.strftime('%I %p')}-{end.strftime('%I %p')}")
895
+ timing_text += " and ".join(times[:-1]) + f" and {times[-1]}." if len(times) > 1 else times[0] + "."
896
+ return timing_text
897
+ else:
898
+ return "Multiple rounds of rain expected throughout the forecast period with the heaviest amounts during afternoon and evening hours."
899
 
900
  def generate_period_text(period_name, events, min_temp, max_temp):
901
  """