|
| 1 | +# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: MIT |
| 4 | +import argparse |
| 5 | +import json |
| 6 | +from pathlib import Path |
| 7 | +import os |
| 8 | +import shutil |
| 9 | +import wave |
| 10 | +import requests |
| 11 | +from ollama import chat |
| 12 | +from ollama import ChatResponse |
| 13 | +from piper import PiperVoice |
| 14 | + |
| 15 | +# pylint: disable=line-too-long |
| 16 | +parser = argparse.ArgumentParser( |
| 17 | + prog="python generate_translated_weather_audio.py", |
| 18 | + description="Multi-Lingual Weather & Wardrobe Assistant - " |
| 19 | + "Fetches weather conditions from weather.gov for a given set of location points. " |
| 20 | + "Generates a wardrobe suggestion based on the weather conditions. " |
| 21 | + "Translates the weather and wardrobe suggestion into one of 5 other languages. " |
| 22 | + "Synthesizes a wave audio file narrating the weather and wardrobe info in " |
| 23 | + "the specified language.", |
| 24 | + epilog="Made with: SmolLM3 & Piper1-gpl", |
| 25 | +) |
| 26 | +parser.add_argument( |
| 27 | + "-l", |
| 28 | + "--language", |
| 29 | + default="es", |
| 30 | + help="The language to translate into. One of (de, es, fr, it, pt). Default is es.", |
| 31 | +) |
| 32 | +parser.add_argument( |
| 33 | + "-p", |
| 34 | + "--location-points", |
| 35 | + default="36,33", |
| 36 | + help="The weather.gov API location points to get weather for. Default is 36,33. " |
| 37 | + "Visit https://api.weather.gov/points/{lat},{lon} to find location points " |
| 38 | + "for GPS coordinates", |
| 39 | +) |
| 40 | +parser.add_argument( |
| 41 | + "-e", |
| 42 | + "--period", |
| 43 | + default="current", |
| 44 | + help="The weather period to consider, current or next. Default is current.", |
| 45 | +) |
| 46 | +parser.add_argument( |
| 47 | + "-c", |
| 48 | + "--cached", |
| 49 | + action="store_true", |
| 50 | + help="Use the cached weather data from forecast.json instead of fetching from the server.", |
| 51 | +) |
| 52 | +args = parser.parse_args() |
| 53 | +language_name_map = { |
| 54 | + "es": "spanish", |
| 55 | + "de": "german", |
| 56 | + "fr": "french", |
| 57 | + "it": "italian", |
| 58 | + "pt": "portuguese", |
| 59 | +} |
| 60 | + |
| 61 | +language_voice_map = { |
| 62 | + "es": "es_MX-claude-high.onnx", |
| 63 | + "de": "de_DE-kerstin-low.onnx", |
| 64 | + "fr": "fr_FR-upmc-medium.onnx", |
| 65 | + "it": "it_IT-paola-medium.onnx", |
| 66 | + "pt": "pt_BR-jeff-medium.onnx", |
| 67 | +} |
| 68 | +if args.language not in language_name_map.keys(): # pylint: disable=consider-iterating-dictionary |
| 69 | + raise ValueError( |
| 70 | + f"Invalid language {args.language}. Valid languages are {language_name_map.keys()}" |
| 71 | + ) |
| 72 | + |
| 73 | +if args.period.lower() not in {"current", "cur", "next"}: |
| 74 | + raise ValueError( |
| 75 | + f"Invalid period {args.period}. Valid periods are 'current', 'next'" |
| 76 | + ) |
| 77 | + |
| 78 | +replacements = {"mph": "miles per hour"} |
| 79 | + |
| 80 | +# latlng_lookup_url = "https://api.weather.gov/points/{lat},{lon}" |
| 81 | +location_points = args.location_points |
| 82 | + |
| 83 | +if not args.cached: |
| 84 | + weather_data = requests.get( |
| 85 | + f"https://api.weather.gov/gridpoints/TOP/{location_points}/forecast", timeout=20 |
| 86 | + ).json() |
| 87 | + print("Fetched weather...") |
| 88 | + |
| 89 | + with open("forecast.json", "w") as f: |
| 90 | + json.dump(weather_data, f) |
| 91 | +else: |
| 92 | + weather_data = json.loads(Path("forecast.json").read_text()) |
| 93 | + print("Read cached weather...") |
| 94 | +period_index = 0 |
| 95 | +if args.period == "next": |
| 96 | + period_index = 1 |
| 97 | +elif args.period in {"cur", "current"}: |
| 98 | + period_index = 0 |
| 99 | + |
| 100 | +period = weather_data["properties"]["periods"][period_index] |
| 101 | + |
| 102 | +english_weather = ( |
| 103 | + f'Current Temperature is {period["temperature"]}{period["temperatureUnit"]}. ' |
| 104 | +) |
| 105 | +english_weather += f'{period["name"]} {period["detailedForecast"]}' |
| 106 | + |
| 107 | +for key, replacement in replacements.items(): |
| 108 | + english_weather = english_weather.replace(key, replacement) |
| 109 | + |
| 110 | +print(f"english_weather: {english_weather}") |
| 111 | + |
| 112 | +print("Generating wardrobe suggestion...") |
| 113 | +response: ChatResponse = chat( |
| 114 | + model="translator-smollm3", |
| 115 | + messages=[ |
| 116 | + { |
| 117 | + "role": "system", |
| 118 | + "content": "You are a wardrobe assistant. Your job is to suggest some appropriate " |
| 119 | + "clothes attire for a person to wear based on the weather. You can include clothing items " |
| 120 | + "and accessories that are appropriate for the specified weather conditions. " |
| 121 | + "Use positive and re-affirming language. Do not output any explanations, " |
| 122 | + "only output the wardrobe suggestion. Do not summarize the weather." |
| 123 | + "The wardrobe suggestion output should be no more than 2 sentences.", |
| 124 | + }, |
| 125 | + { |
| 126 | + "role": "user", |
| 127 | + "content": f"{english_weather}", |
| 128 | + }, |
| 129 | + ], |
| 130 | +) |
| 131 | + |
| 132 | +print(response["message"]["content"]) |
| 133 | +# combine weather and wardrobe suggestion |
| 134 | +english_weather += " " + response["message"]["content"] |
| 135 | + |
| 136 | +print("Translating weather & wardrobe...") |
| 137 | + |
| 138 | +language = language_name_map[args.language] |
| 139 | +response: ChatResponse = chat( |
| 140 | + model="translator-smollm3", |
| 141 | + messages=[ |
| 142 | + { |
| 143 | + "role": "system", |
| 144 | + "content": "You are a translation assistant. The user is going to give you a short passage in english, " |
| 145 | + f"please translate it to {language}. Output only the {language} translation of the input. " |
| 146 | + "Do not output explanations, notes, or anything else. If there is not an exact literal translation, " |
| 147 | + "just output the best fitting alternate word or phrase that you can. Do not explain anything, " |
| 148 | + f"only output the translation. All output should be in {language}", |
| 149 | + }, |
| 150 | + { |
| 151 | + "role": "user", |
| 152 | + "content": f"{english_weather}", |
| 153 | + }, |
| 154 | + ], |
| 155 | +) |
| 156 | +translated_weather = response["message"]["content"] |
| 157 | +print(translated_weather) |
| 158 | + |
| 159 | +print("Generating audio...") |
| 160 | + |
| 161 | +shutil.rmtree("sound_files", ignore_errors=True) |
| 162 | +os.mkdir("sound_files") |
| 163 | + |
| 164 | +voice = PiperVoice.load(language_voice_map[args.language]) |
| 165 | +with wave.open("sound_files/weather_and_wardrobe.wav", "wb") as wav_file: |
| 166 | + voice.synthesize_wav(translated_weather, wav_file) |
| 167 | + |
| 168 | +print("Audio generation complete...") |
0 commit comments