Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions Raspberry_Pi/Raspberry_Pi5_Edge_Model_Demos/SmolLM3.modelfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
FROM hf.co/unsloth/SmolLM3-3B-128K-GGUF
TEMPLATE "
{{- $lastUserIdx := -1 }}
{{- range $i, $_ := .Messages }}
{{- if eq .Role "user" }}{{- $lastUserIdx = $i }}{{ end }}
{{- end -}}
<|im_start|>system
## Metadata

Knowledge Cutoff Date: June 2025
Today Date: {{ currentDate }}
Reasoning Mode: {{ if $.IsThinkSet }}{{ if $.Think }}/think{{ else }}/no_think{{ end }}{{ else }}/think{{ end }}

{{ if .System }}
## Custom Instructions

{{ .System }}


{{ end }}
{{- range $i, $_ := .Messages }}
{{- $last := eq (len (slice $.Messages $i)) 1 }}
{{- if eq .Role "user" }}<|im_start|>user
{{ .Content }}<|im_end|>
{{- else if eq .Role "assistant" }}<|im_start|>assistant
{{- if (and $.IsThinkSet (and .Thinking (or $last (gt $i $lastUserIdx)))) -}}
<think>{{ .Thinking }}</think>
{{- end }}
{{ .Content }}
{{- end }}
{{ if and (ne .Role "assistant") $last }}<|im_start|>assistant
{{- if and $.IsThinkSet (not $.Think) -}}
<think>

</think>

{{ end }}
{{ end }}
{{- end -}}
"
PARAMETER temperature 0.3
PARAMETER top_p 0.9
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
python -m piper.download_voices es_MX-claude-high
python -m piper.download_voices de_DE-kerstin-low
python -m piper.download_voices fr_FR-upmc-medium
python -m piper.download_voices it_IT-paola-medium
python -m piper.download_voices pt_BR-jeff-medium
python -m piper.download_voices en_US-amy-medium
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import argparse
import json
from pathlib import Path
import os
import shutil
import wave
import requests
from ollama import chat
from ollama import ChatResponse
from piper import PiperVoice

# pylint: disable=line-too-long
parser = argparse.ArgumentParser(
prog="python generate_translated_weather_audio.py",
description="Multi-Lingual Weather & Wardrobe Assistant - "
"Fetches weather conditions from weather.gov for a given set of location points. "
"Generates a wardrobe suggestion based on the weather conditions. "
"Translates the weather and wardrobe suggestion into one of 5 other languages. "
"Synthesizes a wave audio file narrating the weather and wardrobe info in "
"the specified language.",
epilog="Made with: SmolLM3 & Piper1-gpl",
)
parser.add_argument(
"-l",
"--language",
default="es",
help="The language to translate into. One of (de, es, fr, it, pt). Default is es.",
)
parser.add_argument(
"-p",
"--location-points",
default="36,33",
help="The weather.gov API location points to get weather for. Default is 36,33. "
"Visit https://api.weather.gov/points/{lat},{lon} to find location points "
"for GPS coordinates",
)
parser.add_argument(
"-e",
"--period",
default="current",
help="The weather period to consider, current or next. Default is current.",
)
parser.add_argument(
"-c",
"--cached",
action="store_true",
help="Use the cached weather data from forecast.json instead of fetching from the server.",
)
args = parser.parse_args()
language_name_map = {
"es": "spanish",
"de": "german",
"fr": "french",
"it": "italian",
"pt": "portuguese",
}

language_voice_map = {
"es": "es_MX-claude-high.onnx",
"de": "de_DE-kerstin-low.onnx",
"fr": "fr_FR-upmc-medium.onnx",
"it": "it_IT-paola-medium.onnx",
"pt": "pt_BR-jeff-medium.onnx",
}
if args.language not in language_name_map.keys(): # pylint: disable=consider-iterating-dictionary
raise ValueError(
f"Invalid language {args.language}. Valid languages are {language_name_map.keys()}"
)

if args.period.lower() not in {"current", "cur", "next"}:
raise ValueError(
f"Invalid period {args.period}. Valid periods are 'current', 'next'"
)

replacements = {"mph": "miles per hour"}

# latlng_lookup_url = "https://api.weather.gov/points/{lat},{lon}"
location_points = args.location_points

if not args.cached:
weather_data = requests.get(
f"https://api.weather.gov/gridpoints/TOP/{location_points}/forecast", timeout=20
).json()
print("Fetched weather...")

with open("forecast.json", "w") as f:
json.dump(weather_data, f)
else:
weather_data = json.loads(Path("forecast.json").read_text())
print("Read cached weather...")
period_index = 0
if args.period == "next":
period_index = 1
elif args.period in {"cur", "current"}:
period_index = 0

period = weather_data["properties"]["periods"][period_index]

english_weather = (
f'Current Temperature is {period["temperature"]}{period["temperatureUnit"]}. '
)
english_weather += f'{period["name"]} {period["detailedForecast"]}'

for key, replacement in replacements.items():
english_weather = english_weather.replace(key, replacement)

print(f"english_weather: {english_weather}")

print("Generating wardrobe suggestion...")
response: ChatResponse = chat(
model="translator-smollm3",
messages=[
{
"role": "system",
"content": "You are a wardrobe assistant. Your job is to suggest some appropriate "
"clothes attire for a person to wear based on the weather. You can include clothing items "
"and accessories that are appropriate for the specified weather conditions. "
"Use positive and re-affirming language. Do not output any explanations, "
"only output the wardrobe suggestion. Do not summarize the weather."
"The wardrobe suggestion output should be no more than 2 sentences.",
},
{
"role": "user",
"content": f"{english_weather}",
},
],
)

print(response["message"]["content"])
# combine weather and wardrobe suggestion
english_weather += " " + response["message"]["content"]

print("Translating weather & wardrobe...")

language = language_name_map[args.language]
response: ChatResponse = chat(
model="translator-smollm3",
messages=[
{
"role": "system",
"content": "You are a translation assistant. The user is going to give you a short passage in english, "
f"please translate it to {language}. Output only the {language} translation of the input. "
"Do not output explanations, notes, or anything else. If there is not an exact literal translation, "
"just output the best fitting alternate word or phrase that you can. Do not explain anything, "
f"only output the translation. All output should be in {language}",
},
{
"role": "user",
"content": f"{english_weather}",
},
],
)
translated_weather = response["message"]["content"]
print(translated_weather)

print("Generating audio...")

shutil.rmtree("sound_files", ignore_errors=True)
os.mkdir("sound_files")

voice = PiperVoice.load(language_voice_map[args.language])
with wave.open("sound_files/weather_and_wardrobe.wav", "wb") as wav_file:
voice.synthesize_wav(translated_weather, wav_file)

print("Audio generation complete...")
3 changes: 3 additions & 0 deletions Raspberry_Pi/Raspberry_Pi5_Edge_Model_Demos/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ollama
piper-tts
requests
165 changes: 165 additions & 0 deletions Raspberry_Pi/Raspberry_Pi5_Edge_Model_Demos/translate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# SPDX-FileCopyrightText: 2025 Tim Cocks for Adafruit Industries
#
# SPDX-License-Identifier: MIT
# pylint: disable=line-too-long
import argparse
import json
import os
import sys
from pathlib import Path
import wave
from ollama import chat
from ollama import ChatResponse
from piper import PiperVoice

translation_wavs_dir = Path("translation_wavs")

if not translation_wavs_dir.exists():
translation_wavs_dir.mkdir()

history_file = Path("history.json")
if not history_file.exists():
history_obj = {"history": []}
with open(history_file, "w") as f:
f.write(json.dumps(history_obj))

with open(history_file, "r") as f:
history_obj = json.loads(f.read())


def save_history():
with open(history_file, "w") as open_history_file:
open_history_file.write(json.dumps(history_obj))


def get_translation_filepath(text):
filename = text.replace(" ", "_")
return str(translation_wavs_dir / Path(filename + ".wav"))


def create_history_entry(text, translated_text, language_choice):
new_entry = {
"input_text": text,
"translation_file": get_translation_filepath(text),
"translated_text": translated_text,
"language": language_choice,
}
return new_entry


def add_to_history(entry_obj):
history_obj["history"].append(entry_obj)
save_history()


def play_translation_wav(entry_obj):
print(f"{entry_obj['language']}: {entry_obj['translated_text']}")
os.system(f"aplay --disable-softvol {entry_obj['translation_file']}")


parser = argparse.ArgumentParser(
prog="translate.py",
description="Translates a word or phrase from english to another language and then speak the translation.",
epilog="Made with: SmolLM3 & Piper TTS.",
)

language_name_map = {
"es": "spanish",
"de": "german",
"fr": "french",
"it": "italian",
"pt": "portuguese",
}

language_voice_map = {
"es": "es_MX-claude-high.onnx",
"de": "de_DE-kerstin-low.onnx",
"fr": "fr_FR-upmc-medium.onnx",
"it": "it_IT-paola-medium.onnx",
"pt": "pt_BR-jeff-medium.onnx",
}

parser.add_argument("input", nargs="?")
parser.add_argument("-l", "--language", default="es")
parser.add_argument("-r", "--replay", action="store_true")
parser.add_argument("-t", "--history", action="store_true")
args = parser.parse_args()
input_str = args.input

if args.replay:
replay_num = None
try:
replay_num = int(args.input)
except (ValueError, TypeError):
if args.input is not None:
print("Replay number must be an integer.")
sys.exit()

if replay_num is None:
chosen_entry = history_obj["history"][-1]
else:
index = len(history_obj["history"]) - replay_num
chosen_entry = history_obj["history"][index]

play_translation_wav(chosen_entry)
sys.exit()

if args.history:
for i, entry in enumerate(reversed(history_obj["history"])):
print(
f"{i+1}: {entry['language']} - {entry['input_text']} - {entry['translated_text']}"
)
sys.exit()


if args.language not in language_name_map.keys(): # pylint: disable=consider-iterating-dictionary
raise ValueError(
f"Invalid language {args.language}. Valid languages are {language_name_map.keys()}"
)

language = language_name_map[args.language]

for history_entry in history_obj["history"]:
if (
history_entry["input_text"].lower() == input_str.lower()
and history_entry["language"] == args.language
):
play_translation_wav(history_entry)
sys.exit()

response: ChatResponse = chat(
model="translator-smollm3",
messages=[
{
"role": "system",
"content": "You are a translation assistant. The user is going to give you a word or short phrase in english, "
f"please translate it to {language}. Output only the {language} translation of the input. Do not output "
"explanations, notes, or anything else. If there is not an exact literal translation, just output "
"the best fitting alternate word or phrase that you can. Do not explain anything, only output "
"the translation.",
},
{
"role": "user",
"content": f"{input_str}",
},
],
)

translation = response["message"]["content"]
# print(translation)
if "\n" in translation:
translation = translation.split("\n")[0]
if len(translation) == 0:
parts = translation.split("\n")
for part in parts:
if len(part) > 0:
translation = part

history_entry = create_history_entry(input_str, translation, args.language)

voice = PiperVoice.load(language_voice_map[args.language])
with wave.open(history_entry["translation_file"], "wb") as wav_file:
voice.synthesize_wav(translation, wav_file)

add_to_history(history_entry)
play_translation_wav(history_entry)