Skip to content

Commit 927e4eb

Browse files
authored
Merge pull request #3156 from FoamyGuy/rpi_weather_translator
pi5 edge models guide files
2 parents fa04f5b + 5ba65b5 commit 927e4eb

File tree

5 files changed

+387
-0
lines changed

5 files changed

+387
-0
lines changed
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
FROM hf.co/unsloth/SmolLM3-3B-128K-GGUF
2+
TEMPLATE "
3+
{{- $lastUserIdx := -1 }}
4+
{{- range $i, $_ := .Messages }}
5+
{{- if eq .Role "user" }}{{- $lastUserIdx = $i }}{{ end }}
6+
{{- end -}}
7+
<|im_start|>system
8+
## Metadata
9+
10+
Knowledge Cutoff Date: June 2025
11+
Today Date: {{ currentDate }}
12+
Reasoning Mode: {{ if $.IsThinkSet }}{{ if $.Think }}/think{{ else }}/no_think{{ end }}{{ else }}/think{{ end }}
13+
14+
{{ if .System }}
15+
## Custom Instructions
16+
17+
{{ .System }}
18+
19+
20+
{{ end }}
21+
{{- range $i, $_ := .Messages }}
22+
{{- $last := eq (len (slice $.Messages $i)) 1 }}
23+
{{- if eq .Role "user" }}<|im_start|>user
24+
{{ .Content }}<|im_end|>
25+
{{- else if eq .Role "assistant" }}<|im_start|>assistant
26+
{{- if (and $.IsThinkSet (and .Thinking (or $last (gt $i $lastUserIdx)))) -}}
27+
<think>{{ .Thinking }}</think>
28+
{{- end }}
29+
{{ .Content }}
30+
{{- end }}
31+
{{ if and (ne .Role "assistant") $last }}<|im_start|>assistant
32+
{{- if and $.IsThinkSet (not $.Think) -}}
33+
<think>
34+
35+
</think>
36+
37+
{{ end }}
38+
{{ end }}
39+
{{- end -}}
40+
"
41+
PARAMETER temperature 0.3
42+
PARAMETER top_p 0.9
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
python -m piper.download_voices es_MX-claude-high
5+
python -m piper.download_voices de_DE-kerstin-low
6+
python -m piper.download_voices fr_FR-upmc-medium
7+
python -m piper.download_voices it_IT-paola-medium
8+
python -m piper.download_voices pt_BR-jeff-medium
9+
python -m piper.download_voices en_US-amy-medium
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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...")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
ollama
2+
piper-tts
3+
requests
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
# pylint: disable=line-too-long
5+
import argparse
6+
import json
7+
import os
8+
import sys
9+
from pathlib import Path
10+
import wave
11+
from ollama import chat
12+
from ollama import ChatResponse
13+
from piper import PiperVoice
14+
15+
translation_wavs_dir = Path("translation_wavs")
16+
17+
if not translation_wavs_dir.exists():
18+
translation_wavs_dir.mkdir()
19+
20+
history_file = Path("history.json")
21+
if not history_file.exists():
22+
history_obj = {"history": []}
23+
with open(history_file, "w") as f:
24+
f.write(json.dumps(history_obj))
25+
26+
with open(history_file, "r") as f:
27+
history_obj = json.loads(f.read())
28+
29+
30+
def save_history():
31+
with open(history_file, "w") as open_history_file:
32+
open_history_file.write(json.dumps(history_obj))
33+
34+
35+
def get_translation_filepath(text):
36+
filename = text.replace(" ", "_")
37+
return str(translation_wavs_dir / Path(filename + ".wav"))
38+
39+
40+
def create_history_entry(text, translated_text, language_choice):
41+
new_entry = {
42+
"input_text": text,
43+
"translation_file": get_translation_filepath(text),
44+
"translated_text": translated_text,
45+
"language": language_choice,
46+
}
47+
return new_entry
48+
49+
50+
def add_to_history(entry_obj):
51+
history_obj["history"].append(entry_obj)
52+
save_history()
53+
54+
55+
def play_translation_wav(entry_obj):
56+
print(f"{entry_obj['language']}: {entry_obj['translated_text']}")
57+
os.system(f"aplay --disable-softvol {entry_obj['translation_file']}")
58+
59+
60+
parser = argparse.ArgumentParser(
61+
prog="translate.py",
62+
description="Translates a word or phrase from english to another language and then speak the translation.",
63+
epilog="Made with: SmolLM3 & Piper TTS.",
64+
)
65+
66+
language_name_map = {
67+
"es": "spanish",
68+
"de": "german",
69+
"fr": "french",
70+
"it": "italian",
71+
"pt": "portuguese",
72+
}
73+
74+
language_voice_map = {
75+
"es": "es_MX-claude-high.onnx",
76+
"de": "de_DE-kerstin-low.onnx",
77+
"fr": "fr_FR-upmc-medium.onnx",
78+
"it": "it_IT-paola-medium.onnx",
79+
"pt": "pt_BR-jeff-medium.onnx",
80+
}
81+
82+
parser.add_argument("input", nargs="?")
83+
parser.add_argument("-l", "--language", default="es")
84+
parser.add_argument("-r", "--replay", action="store_true")
85+
parser.add_argument("-t", "--history", action="store_true")
86+
args = parser.parse_args()
87+
input_str = args.input
88+
89+
if args.replay:
90+
replay_num = None
91+
try:
92+
replay_num = int(args.input)
93+
except (ValueError, TypeError):
94+
if args.input is not None:
95+
print("Replay number must be an integer.")
96+
sys.exit()
97+
98+
if replay_num is None:
99+
chosen_entry = history_obj["history"][-1]
100+
else:
101+
index = len(history_obj["history"]) - replay_num
102+
chosen_entry = history_obj["history"][index]
103+
104+
play_translation_wav(chosen_entry)
105+
sys.exit()
106+
107+
if args.history:
108+
for i, entry in enumerate(reversed(history_obj["history"])):
109+
print(
110+
f"{i+1}: {entry['language']} - {entry['input_text']} - {entry['translated_text']}"
111+
)
112+
sys.exit()
113+
114+
115+
if args.language not in language_name_map.keys(): # pylint: disable=consider-iterating-dictionary
116+
raise ValueError(
117+
f"Invalid language {args.language}. Valid languages are {language_name_map.keys()}"
118+
)
119+
120+
language = language_name_map[args.language]
121+
122+
for history_entry in history_obj["history"]:
123+
if (
124+
history_entry["input_text"].lower() == input_str.lower()
125+
and history_entry["language"] == args.language
126+
):
127+
play_translation_wav(history_entry)
128+
sys.exit()
129+
130+
response: ChatResponse = chat(
131+
model="translator-smollm3",
132+
messages=[
133+
{
134+
"role": "system",
135+
"content": "You are a translation assistant. The user is going to give you a word or short phrase in english, "
136+
f"please translate it to {language}. Output only the {language} translation of the input. Do not output "
137+
"explanations, notes, or anything else. If there is not an exact literal translation, just output "
138+
"the best fitting alternate word or phrase that you can. Do not explain anything, only output "
139+
"the translation.",
140+
},
141+
{
142+
"role": "user",
143+
"content": f"{input_str}",
144+
},
145+
],
146+
)
147+
148+
translation = response["message"]["content"]
149+
# print(translation)
150+
if "\n" in translation:
151+
translation = translation.split("\n")[0]
152+
if len(translation) == 0:
153+
parts = translation.split("\n")
154+
for part in parts:
155+
if len(part) > 0:
156+
translation = part
157+
158+
history_entry = create_history_entry(input_str, translation, args.language)
159+
160+
voice = PiperVoice.load(language_voice_map[args.language])
161+
with wave.open(history_entry["translation_file"], "wb") as wav_file:
162+
voice.synthesize_wav(translation, wav_file)
163+
164+
add_to_history(history_entry)
165+
play_translation_wav(history_entry)

0 commit comments

Comments
 (0)