Spaces:
Sleeping
Add precipitation chances, 6-hour timing, and sky conditions
Browse filesEnhanced NOAA-style forecasting with:
🌧️ **Precipitation Chances**: Calculate and display percentage chances (20%, 50%, 70%, 90%)
⏰ **6-Hour Timing Analysis**: Identify when rain is most likely (morning, afternoon, evening, overnight)
☀️ **Sky Conditions**: Detailed cloud cover descriptions (sunny, partly cloudy, overcast, etc.)
🌪️ **Wind Integration**: Natural wind descriptions integrated into narrative
Example output:
"Today: Mostly cloudy, then chance of rain, mainly afternoon.
Chance of precipitation 50%. High 21°C."
"Tonight: Rain likely, mainly evening. Chance of precipitation 70%.
Low 9°C."
Features authentic NOAA timing phrases like "rain mainly afternoon"
and proper precipitation probability formatting.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
|
@@ -747,7 +747,7 @@ def generate_overview_paragraph(rain_hours, heavy_rain_hours, windy_hours, max_t
|
|
| 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
|
| 751 |
current_time = datetime.now()
|
| 752 |
forecasts = []
|
| 753 |
|
|
@@ -757,6 +757,7 @@ def generate_daily_detailed_forecasts(forecast_data, events):
|
|
| 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):
|
|
@@ -776,16 +777,12 @@ def generate_daily_detailed_forecasts(forecast_data, events):
|
|
| 776 |
if not day_indices and not night_indices:
|
| 777 |
continue
|
| 778 |
|
| 779 |
-
# Day period
|
| 780 |
if day_indices:
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 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:
|
|
@@ -795,19 +792,15 @@ def generate_daily_detailed_forecasts(forecast_data, events):
|
|
| 795 |
else:
|
| 796 |
period_name = target_date.strftime("%A")
|
| 797 |
|
| 798 |
-
day_forecast =
|
| 799 |
forecasts.append(day_forecast)
|
| 800 |
|
| 801 |
-
# Night period
|
| 802 |
if night_indices:
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
| 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:
|
|
@@ -817,11 +810,187 @@ def generate_daily_detailed_forecasts(forecast_data, events):
|
|
| 817 |
else:
|
| 818 |
night_name = f"{target_date.strftime('%A')} Night"
|
| 819 |
|
| 820 |
-
night_forecast =
|
| 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 = []
|
|
|
|
| 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 with precipitation chances, timing, and cloud cover"""
|
| 751 |
current_time = datetime.now()
|
| 752 |
forecasts = []
|
| 753 |
|
|
|
|
| 757 |
precipitation = forecast_data.get('precipitation', [0] * len(temperatures))
|
| 758 |
wind_speeds = forecast_data['wind_speed']
|
| 759 |
humidity = forecast_data['humidity']
|
| 760 |
+
cloud_cover = forecast_data.get('cloud_cover', [50] * len(temperatures))
|
| 761 |
|
| 762 |
# Process 4 days of forecasts
|
| 763 |
for day_offset in range(4):
|
|
|
|
| 777 |
if not day_indices and not night_indices:
|
| 778 |
continue
|
| 779 |
|
| 780 |
+
# Day period analysis
|
| 781 |
if day_indices:
|
| 782 |
+
day_data = analyze_period_conditions(
|
| 783 |
+
day_indices, timestamps, temperatures, precipitation,
|
| 784 |
+
wind_speeds, cloud_cover, day_start, day_end
|
| 785 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
|
| 787 |
# Generate day forecast
|
| 788 |
if day_offset == 0:
|
|
|
|
| 792 |
else:
|
| 793 |
period_name = target_date.strftime("%A")
|
| 794 |
|
| 795 |
+
day_forecast = generate_enhanced_period_narrative(period_name, day_data, True)
|
| 796 |
forecasts.append(day_forecast)
|
| 797 |
|
| 798 |
+
# Night period analysis
|
| 799 |
if night_indices:
|
| 800 |
+
night_data = analyze_period_conditions(
|
| 801 |
+
night_indices, timestamps, temperatures, precipitation,
|
| 802 |
+
wind_speeds, cloud_cover, day_end, night_end
|
| 803 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 804 |
|
| 805 |
# Generate night forecast
|
| 806 |
if day_offset == 0:
|
|
|
|
| 810 |
else:
|
| 811 |
night_name = f"{target_date.strftime('%A')} Night"
|
| 812 |
|
| 813 |
+
night_forecast = generate_enhanced_period_narrative(night_name, night_data, False)
|
| 814 |
forecasts.append(night_forecast)
|
| 815 |
|
| 816 |
return '\n\n'.join(forecasts)
|
| 817 |
|
| 818 |
+
def analyze_period_conditions(indices, timestamps, temperatures, precipitation, wind_speeds, cloud_cover, period_start, period_end):
|
| 819 |
+
"""Analyze weather conditions for a specific time period with 6-hour sub-periods"""
|
| 820 |
+
data = {
|
| 821 |
+
'temps': [temperatures[i] for i in indices],
|
| 822 |
+
'precip': [precipitation[i] for i in indices if i < len(precipitation)],
|
| 823 |
+
'winds': [wind_speeds[i] for i in indices],
|
| 824 |
+
'clouds': [cloud_cover[i] for i in indices if i < len(cloud_cover)],
|
| 825 |
+
'timestamps': [timestamps[i] for i in indices]
|
| 826 |
+
}
|
| 827 |
+
|
| 828 |
+
# Calculate statistics
|
| 829 |
+
data['high_temp'] = max(data['temps']) if data['temps'] else 20
|
| 830 |
+
data['low_temp'] = min(data['temps']) if data['temps'] else 10
|
| 831 |
+
data['avg_clouds'] = sum(data['clouds']) / len(data['clouds']) if data['clouds'] else 50
|
| 832 |
+
data['max_wind'] = max(data['winds']) if data['winds'] else 5
|
| 833 |
+
|
| 834 |
+
# Precipitation analysis
|
| 835 |
+
data['precip_chance'] = calculate_precipitation_chance(data['precip'])
|
| 836 |
+
data['rain_timing'] = analyze_6hour_precipitation_timing(data['timestamps'], data['precip'], period_start, period_end)
|
| 837 |
+
|
| 838 |
+
# Cloud cover description
|
| 839 |
+
data['sky_condition'] = get_sky_condition(data['avg_clouds'])
|
| 840 |
+
|
| 841 |
+
# Wind conditions
|
| 842 |
+
data['wind_desc'] = get_wind_description(data['max_wind'])
|
| 843 |
+
|
| 844 |
+
return data
|
| 845 |
+
|
| 846 |
+
def calculate_precipitation_chance(precip_data):
|
| 847 |
+
"""Calculate precipitation chance percentage"""
|
| 848 |
+
if not precip_data:
|
| 849 |
+
return 0
|
| 850 |
+
|
| 851 |
+
rainy_hours = sum(1 for p in precip_data if p > 0.1)
|
| 852 |
+
total_hours = len(precip_data)
|
| 853 |
+
|
| 854 |
+
if rainy_hours == 0:
|
| 855 |
+
return 0
|
| 856 |
+
elif rainy_hours >= total_hours * 0.8:
|
| 857 |
+
return 90
|
| 858 |
+
elif rainy_hours >= total_hours * 0.6:
|
| 859 |
+
return 70
|
| 860 |
+
elif rainy_hours >= total_hours * 0.4:
|
| 861 |
+
return 50
|
| 862 |
+
elif rainy_hours >= total_hours * 0.2:
|
| 863 |
+
return 30
|
| 864 |
+
else:
|
| 865 |
+
return 20
|
| 866 |
+
|
| 867 |
+
def analyze_6hour_precipitation_timing(timestamps, precip_data, period_start, period_end):
|
| 868 |
+
"""Analyze when rain is most likely within the period using 6-hour blocks"""
|
| 869 |
+
if not timestamps or not precip_data:
|
| 870 |
+
return None
|
| 871 |
+
|
| 872 |
+
# Define 6-hour periods
|
| 873 |
+
period_duration = (period_end - period_start).total_seconds() / 3600 # hours
|
| 874 |
+
|
| 875 |
+
if period_duration <= 6:
|
| 876 |
+
# Single period
|
| 877 |
+
avg_precip = sum(precip_data) / len(precip_data) if precip_data else 0
|
| 878 |
+
if avg_precip > 0.1:
|
| 879 |
+
start_hour = period_start.hour
|
| 880 |
+
if 6 <= start_hour < 12:
|
| 881 |
+
return "morning"
|
| 882 |
+
elif 12 <= start_hour < 18:
|
| 883 |
+
return "afternoon"
|
| 884 |
+
elif 18 <= start_hour < 24:
|
| 885 |
+
return "evening"
|
| 886 |
+
else:
|
| 887 |
+
return "overnight"
|
| 888 |
+
return None
|
| 889 |
+
|
| 890 |
+
# Analyze multiple 6-hour blocks
|
| 891 |
+
blocks = []
|
| 892 |
+
block_size = max(1, len(timestamps) // int(period_duration / 6))
|
| 893 |
+
|
| 894 |
+
for i in range(0, len(timestamps), block_size):
|
| 895 |
+
block_precip = precip_data[i:i+block_size] if i+block_size <= len(precip_data) else precip_data[i:]
|
| 896 |
+
block_avg = sum(block_precip) / len(block_precip) if block_precip else 0
|
| 897 |
+
block_time = timestamps[i]
|
| 898 |
+
blocks.append((block_time, block_avg))
|
| 899 |
+
|
| 900 |
+
# Find the block with highest precipitation
|
| 901 |
+
if blocks:
|
| 902 |
+
max_precip_block = max(blocks, key=lambda x: x[1])
|
| 903 |
+
if max_precip_block[1] > 0.1:
|
| 904 |
+
hour = max_precip_block[0].hour
|
| 905 |
+
if 6 <= hour < 12:
|
| 906 |
+
return "morning"
|
| 907 |
+
elif 12 <= hour < 18:
|
| 908 |
+
return "afternoon"
|
| 909 |
+
elif 18 <= hour < 24:
|
| 910 |
+
return "evening"
|
| 911 |
+
else:
|
| 912 |
+
return "overnight"
|
| 913 |
+
|
| 914 |
+
return None
|
| 915 |
+
|
| 916 |
+
def get_sky_condition(cloud_percentage):
|
| 917 |
+
"""Convert cloud percentage to descriptive terms"""
|
| 918 |
+
if cloud_percentage < 10:
|
| 919 |
+
return "sunny"
|
| 920 |
+
elif cloud_percentage < 25:
|
| 921 |
+
return "mostly sunny"
|
| 922 |
+
elif cloud_percentage < 50:
|
| 923 |
+
return "partly cloudy"
|
| 924 |
+
elif cloud_percentage < 75:
|
| 925 |
+
return "mostly cloudy"
|
| 926 |
+
elif cloud_percentage < 90:
|
| 927 |
+
return "cloudy"
|
| 928 |
+
else:
|
| 929 |
+
return "overcast"
|
| 930 |
+
|
| 931 |
+
def get_wind_description(wind_speed):
|
| 932 |
+
"""Convert wind speed to descriptive terms"""
|
| 933 |
+
if wind_speed < 3:
|
| 934 |
+
return "light winds"
|
| 935 |
+
elif wind_speed < 8:
|
| 936 |
+
return "light winds"
|
| 937 |
+
elif wind_speed < 12:
|
| 938 |
+
return "breezy"
|
| 939 |
+
elif wind_speed < 18:
|
| 940 |
+
return "windy"
|
| 941 |
+
else:
|
| 942 |
+
return "very windy"
|
| 943 |
+
|
| 944 |
+
def generate_enhanced_period_narrative(period_name, data, is_day):
|
| 945 |
+
"""Generate enhanced narrative with precipitation chances, timing, and sky conditions"""
|
| 946 |
+
conditions = []
|
| 947 |
+
|
| 948 |
+
# Sky condition and precipitation
|
| 949 |
+
if data['precip_chance'] > 60:
|
| 950 |
+
if data['rain_timing']:
|
| 951 |
+
conditions.append(f"rain likely, mainly {data['rain_timing']}")
|
| 952 |
+
else:
|
| 953 |
+
conditions.append("rain likely")
|
| 954 |
+
conditions.append(f"Chance of precipitation {data['precip_chance']}%")
|
| 955 |
+
elif data['precip_chance'] > 30:
|
| 956 |
+
if data['rain_timing']:
|
| 957 |
+
conditions.append(f"chance of rain, mainly {data['rain_timing']}")
|
| 958 |
+
else:
|
| 959 |
+
conditions.append("chance of rain")
|
| 960 |
+
conditions.append(f"Chance of precipitation {data['precip_chance']}%")
|
| 961 |
+
elif data['precip_chance'] > 0:
|
| 962 |
+
conditions.append(f"slight chance of rain. Chance of precipitation {data['precip_chance']}%")
|
| 963 |
+
|
| 964 |
+
# If no significant precipitation, describe sky condition
|
| 965 |
+
if data['precip_chance'] <= 30:
|
| 966 |
+
if not conditions: # No rain mentioned yet
|
| 967 |
+
conditions.insert(0, data['sky_condition'])
|
| 968 |
+
else:
|
| 969 |
+
conditions.insert(0, f"{data['sky_condition']}, then")
|
| 970 |
+
|
| 971 |
+
# Wind conditions
|
| 972 |
+
if data['max_wind'] > 12:
|
| 973 |
+
conditions.append(data['wind_desc'])
|
| 974 |
+
|
| 975 |
+
# Build the narrative
|
| 976 |
+
if is_day:
|
| 977 |
+
if conditions:
|
| 978 |
+
weather_text = f"**{period_name}:** {' '.join(conditions).capitalize()}"
|
| 979 |
+
else:
|
| 980 |
+
weather_text = f"**{period_name}:** {data['sky_condition'].capitalize()}"
|
| 981 |
+
|
| 982 |
+
weather_text += f". High {data['high_temp']:.0f}°C"
|
| 983 |
+
else:
|
| 984 |
+
if conditions:
|
| 985 |
+
weather_text = f"**{period_name}:** {' '.join(conditions).capitalize()}"
|
| 986 |
+
else:
|
| 987 |
+
weather_text = f"**{period_name}:** {data['sky_condition'].capitalize()}"
|
| 988 |
+
|
| 989 |
+
weather_text += f". Low {data['low_temp']:.0f}°C"
|
| 990 |
+
|
| 991 |
+
weather_text += "."
|
| 992 |
+
return weather_text
|
| 993 |
+
|
| 994 |
def generate_period_narrative(period_name, high_temp, low_temp, has_rain, heavy_rain, windy, is_day):
|
| 995 |
"""Generate natural narrative for a specific period"""
|
| 996 |
conditions = []
|