In questo tutorial imparerai come costruire il tuo AI Language Tutor su un UNIHIKER M10 utilizzando i modelli AI di OpenAI. Avrai bisogno di un account OpenAI e di una chiave API per farlo.
L’UNIHIKER M10 è una piccola scheda con 512 MB di RAM e 16 GB di memoria eMMC, e un touchscreen da 2,8 pollici. Oltre alla connettività Wi-Fi e Bluetooth, la scheda include sensori integrati come sensore di luce, accelerometro, giroscopio e, cosa più importante, un microfono.
Esegue Debian Linux, supporta la programmazione in Python e viene fornito con molte librerie Python preinstallate. Questo rende facile implementare soluzioni AI come un AI Language Tutor. Il breve video seguente dimostra l’AI Language Tutor che andremo a costruire.
Potresti dover aumentare il volume del tuo computer per sentire la mia voce e la risposta dell’AI.
Componenti necessari
Per questo tutorial ti serve una scheda UNIHIKER M10. Puoi acquistarla su DFRobot o Amazon. Se hai un altoparlante USB puoi usarlo collegandolo all’UNIHIKER M10. Ma se vuoi aggiungere i tuoi piccoli altoparlanti, ti servirà un amplificatore PAM8403 e uno o due degli altoparlanti elencati di seguito.

UNIHIKER M10

Amplificatore PAM8403

2 x Altoparlanti 3 Watt 4 Ohm
Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.
Hardware dell’UNIHIKER M10
L’UNIHIKER M10 è un computer a scheda singola compatto per educazione, prototipazione e applicazioni AIoT. Include un sistema basato su Linux, sensori e interfacce hardware su un’unica scheda.
Il dispositivo si basa su un processore quad-core Arm Cortex-A35 che arriva fino a 1,2 GHz. Include 512 MB di RAM e 16 GB di memoria eMMC e utilizza il sistema operativo Debian Linux.

La scheda integra un touchscreen da 2,8 pollici con risoluzione 240 × 320 pixel, connettività Wi-Fi e Bluetooth. I sensori integrati includono sensore di luce, accelerometro, giroscopio e microfono. L’espansione hardware è possibile tramite porte USB, connettori Gravity per sensori e un connettore edge compatibile con micro:bit che espone interfacce GPIO, I2C, SPI e UART.
Per maggiori dettagli tecnici consulta il nostro Voice Assistant on UNIHIKER M10 with OpenAI tutorial. Questo tutorial ti spiegherà anche come programmare l’UNIHIKER, come installare librerie Python e come configurare il Wi-Fi. Tutto ciò ti servirà per far funzionare l’AI tutor che costruiremo.
Collegare gli altoparlanti all’UNIHIKER M10
Il nostro AI Tutor sarà in grado di generare voce. Per questo ci servono gli altoparlanti. Puoi collegare altoparlanti all’UNIHIKER M10 via USB o Bluetooth. Questa è la soluzione più semplice e, se la scegli, puoi saltare questa sezione.
Io volevo usare altoparlanti più piccoli per rendere il mio AI Tutor portatile. L’UNIHIKER M10 ha un’uscita lineout ma purtroppo senza connettore. Devi saldare fili su specifici pad sul retro della scheda e ti serve anche un piccolo amplificatore per pilotare gli altoparlanti. Ho preso lo schema dal post AI Assistant with OpenAI GPT, Azure Speech API and UNIHIKER.
Amplificatore PAM8403
Ho usato un modulo PAM8403 come amplificatore. Il modulo PAM8403 è un piccolo amplificatore audio stereo di classe D basato sul chip PAM8403. Accetta una tensione di alimentazione da 2,5 V fino a 5,5 V. A 5V, pilotando altoparlanti da 4 Ω, ogni canale può erogare fino a circa 3 W di potenza in uscita. L’immagine seguente mostra il pinout del modulo amplificatore PAM8403:

Per maggiori informazioni sul modulo amplificatore PAM8403 consulta il nostro Audio with PAM8403, PCM5102 and ESP32 tutorial.
Uscita Lineout dell’UNIHIKER M10
Come detto, non c’è un connettore per l’uscita lineout sull’UNIHIKER M10 ma puoi accedervi tramite pad (di test) sul retro della scheda (schematics). L’immagine sotto mostra la posizione dei pad Lineout e di alimentazione necessari per collegare l’amplificatore PAM8403:

Ho misurato 4,7 V sul pad VCC, il che è strano, dato che mi aspettavo 3,3V o 5V, ma il PAM8403 funziona bene con 4,7 V. Inoltre la mia scheda aveva un solo pad per VCC, mentre la foto sopra ne mostra due. Comunque tutto ha funzionato correttamente.
Collegare amplificatore e altoparlanti all’UNIHIKER M10
In questa sezione ti mostro come collegare l’amplificatore PAM8403 e gli altoparlanti all’UNIHIKER M10. I piccoli altoparlanti da 3W spesso hanno connettori che dovrai tagliare, perché salderemo i fili direttamente al modulo PAM8403:

L’immagine seguente mostra come cablare l’UNIHIKER M10 all’amplificatore PAM8403 e agli altoparlanti:

Saldare i fili ai pad dell’UNIHIKER M10 è semplice, ma assicurati di collegarti ai pad corretti e di non danneggiare nulla. La foto sotto mostra il mio cablaggio completato:

Nota che non servono due altoparlanti, dato che il suono stereo non è realmente necessario. Ma due altoparlanti saranno più potenti di uno solo. Se vuoi un suono davvero forte usa un altoparlante USB con alimentazione esterna.
Manuale utente per AI Language Tutor
In questa sezione spiegherò rapidamente come funziona l’interfaccia utente dell’AI Language Tutor. Questo ti aiuterà a capire il codice nella sezione successiva e a usare il software Tutor. L’immagine sotto mostra la GUI con gli elementi funzionali annotati:

Premi il pulsante A e tienilo premuto mentre parli. Quando rilasci il pulsante A, l’audio registrato viene inviato a OpenAI per trascrizione e traduzione. La trascrizione e la traduzione vengono poi mostrate nel campo Risposta e la traduzione viene riprodotta vocalmente. Puoi riascoltare l’audio della traduzione premendo il pulsante B.
Puoi selezionare la lingua di destinazione per la traduzione premendo il pulsante “Select Language” in alto. Attualmente cicla tra “Japanese”, “Italian” e “German” come lingue. Ma puoi facilmente estendere il codice per altre o più lingue.
Nota che, anche se l’interfaccia utente è in inglese, puoi parlare in qualsiasi lingua supportata dal modello TTS di OpenAI (link). Il sistema riconoscerà la lingua (se supportata) e tradurrà nella lingua di destinazione selezionata. Puoi anche parlare nella lingua di destinazione per controllare la pronuncia. Guarda i due video seguenti per una demo, dove parlo in tedesco e giapponese:
Quando premi il pulsante “Explain” in basso, viene stampata una spiegazione della grammatica della traduzione. Allo stesso modo, se premi il pulsante “Example”, vengono aggiunte frasi di esempio con grammatica simile. I due video seguenti mostrano questa funzionalità:
Infine, ci sono pulsanti per aumentare e diminuire il volume e la dimensione del font in alto e pulsanti di scorrimento in basso.
Puoi usare le dita per controllare l’interfaccia, ma usare la piccola penna fornita con l’UNIHIKER M10 funziona meglio. Nota che puoi anche toccare il campo risposta e trascinarlo/scorrere invece di usare i pulsanti o la barra di scorrimento.
Ottenere la chiave API OpenAI
Il nostro AI Language Tutor utilizzerà modelli AI forniti da OpenAI. Perciò ti serve un account OpenAI e una chiave API. Vai su https://platform.openai.com e registrati con un indirizzo email o un account Google o Microsoft esistente.
Dopo aver verificato la tua email e completato la configurazione iniziale, accedi alla dashboard OpenAI, platform.openai.com/api-keys e trova o crea la tua chiave API (=SECRET KEY) come mostrato sotto:

La chiave API è una stringa unica e lunga, che inizia con “sk-proj-“, necessaria per autenticare le richieste API. Successivamente dovrai copiare questa stringa intera nel codice dell’AI Language Tutor.
Codice per AI Language Tutor
Il codice seguente implementa la nostra applicazione AI-powered per l’insegnamento delle lingue. Permette agli utenti di parlare frasi in qualsiasi lingua, che vengono trascritte, tradotte nella lingua di destinazione selezionata e riprodotte tramite text-to-speech (TTS). L’app offre anche spiegazioni grammaticali e frasi di esempio per facilitare l’apprendimento.
# www.makerguides.com
# Python 3.7
# openai 1.39.0
# PyAudio 0.2.11
# pinpong 0.6.1
# numpy 1.21.6
import sys
import time
import os
import threading
import tempfile
import numpy as np
import pyaudio
import io
import wave
import pygame
from openai import OpenAI
from pinpong.extension.unihiker import button_a, button_b
from qtpy.QtWidgets import (
QApplication,
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QTextEdit,
QScroller,
QPushButton,
)
from qtpy.QtCore import Qt, QObject, Signal, QThread, Slot
API_KEY = "sk-proj-my-api_key" # SET YOUR API KEY HERE!
DEVICE_INDEX = 2
SAMPLE_RATE = 16000
CHANNELS = 1
CHUNK = 1024
LANGUAGES = ["Japanese", "Italian", "German"]
DEFAULT_STATUS = "Hold A: new | B: replay"
VOLUME_STEP = 0.1 # increment per button press
INITIAL_VOLUME = 0.8 # volume level when not muted
_volume = INITIAL_VOLUME # float, 0.0 – 1.0
TTS_FILE = os.path.join(tempfile.gettempdir(), "tts_translation.mp3")
client = OpenAI(api_key=API_KEY)
pygame.mixer.init()
# STYLE ----------------------------------------------------------
ORA_BG = "#fcdb03" # bright yellow
ORA_DARK = "#d4a800" # darker yellow (pressed / language btn)
ORA_DIM = "#fef08a" # disabled button bg
ORA_TEXT = "#000000" # black text everywhere
ORA_DT = "#888800" # disabled button text
def _btn_style(bg=ORA_BG, fg=ORA_TEXT, en=ORA_BG,
pr=ORA_DARK, ds=ORA_DIM, dt=ORA_DT):
return (
f"QPushButton {{ background: {bg}; color: {fg};"
f" border: none; font-size: 13px; }}"
f"QPushButton:enabled {{ background: {en}; }}"
f"QPushButton:pressed {{ background: {pr}; }}"
f"QPushButton:disabled {{ background: {ds}; color: {dt}; }}"
)
# WORKER THREAD ----------------------------------------------------
class AssistantWorker(QObject):
status = Signal(str)
answer = Signal(str)
btn_explain_on = Signal(bool)
btn_examples_on = Signal(bool)
language_changed = Signal(str)
def __init__(self):
super().__init__()
self._last_question = ""
self._last_translation = ""
self._last_grammar = ""
self._last_examples = ""
self._tts_ready = False
self._lang_index = 0
self._language = LANGUAGES[0]
# Thread-safe flags
self._explain_requested = False
self._examples_requested = False
self._lang_requested = False
self._lock = threading.Lock()
self._pa = pyaudio.PyAudio()
self._stream = self._pa.open(
format=pyaudio.paInt16,
channels=CHANNELS,
rate=SAMPLE_RATE,
input=True,
input_device_index=DEVICE_INDEX,
frames_per_buffer=CHUNK,
)
@Slot()
def on_explain_clicked(self):
with self._lock:
self._explain_requested = True
@Slot()
def on_examples_clicked(self):
with self._lock:
self._examples_requested = True
@Slot()
def on_language_clicked(self):
with self._lock:
self._lang_requested = True
def run(self):
self.status.emit("Hold Button A to speak")
while True:
a_pressed = button_a.is_pressed()
b_pressed = button_b.is_pressed()
with self._lock:
explain_req = self._explain_requested
examples_req = self._examples_requested
lang_req = self._lang_requested
self._explain_requested = False
self._examples_requested = False
self._lang_requested = False
if lang_req and not a_pressed:
self._lang_index = (self._lang_index + 1) % len(LANGUAGES)
self._language = LANGUAGES[self._lang_index]
self.language_changed.emit(self._language)
elif a_pressed:
self._clear_state()
raw_pcm = record_while_held(self, self._stream)
if not raw_pcm:
self.status.emit("Hold Button A to speak")
continue
self.status.emit("Transcribing...")
normalized = normalize(raw_pcm)
wav_bytes = pcm_to_wav(normalized)
question = transcribe(wav_bytes)
self.status.emit(f"Translating to {self._language}...")
translation = translate_only(question, self._language)
self._last_question = question
self._last_translation = translation
self._refresh_display()
self.btn_explain_on.emit(True)
self.btn_examples_on.emit(True)
self.status.emit("Generating audio...")
tts_text = extract_first_line(translation)
generate_tts(tts_text, TTS_FILE)
self._tts_ready = True
self._set_default_status()
play_audio(TTS_FILE)
elif b_pressed:
while button_b.is_pressed():
time.sleep(0.01)
if self._tts_ready:
self.status.emit("Replaying...")
play_audio(TTS_FILE)
self._set_default_status()
elif explain_req and self._last_translation:
self.status.emit("Explaining grammar...")
self._last_grammar = explain_grammar(
self._last_question, self._last_translation, self._language
)
self._refresh_display()
self._set_default_status()
elif examples_req and self._last_translation:
self.status.emit("Generating examples...")
self._last_examples = add_examples(
self._last_question, self._last_translation, self._language
)
self._refresh_display()
self._set_default_status()
else:
time.sleep(0.02)
def _set_default_status(self):
self.status.emit(DEFAULT_STATUS)
def _clear_state(self):
self._last_question = ""
self._last_translation = ""
self._last_grammar = ""
self._last_examples = ""
self._tts_ready = False
if pygame.mixer.music.get_busy():
pygame.mixer.music.stop()
if os.path.exists(TTS_FILE):
try:
os.remove(TTS_FILE)
except OSError:
pass
self.answer.emit("")
self.btn_explain_on.emit(False)
self.btn_examples_on.emit(False)
def _refresh_display(self):
parts = []
if self._last_question:
parts.append(self._last_question)
if self._last_translation:
parts.append(self._last_translation)
if self._last_grammar:
parts.append(f"── Grammar ──\n{self._last_grammar}")
if self._last_examples:
parts.append(f"── Examples ──\n{self._last_examples}")
self.answer.emit("\n\n".join(parts))
# GUI ----------------------------------------------------
class AssistantUI(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Voice Chatbot")
self.setFixedSize(240, 320)
# Orange window background, black text; white text area
self.setStyleSheet(
f"QWidget {{ background-color: {ORA_BG}; color: {ORA_TEXT}; }}"
f"QTextEdit {{ background-color: #ffffff; color: {ORA_TEXT};"
f" border: none; }}"
)
layout = QVBoxLayout(self)
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(3)
lang_row = QHBoxLayout()
lang_row.setSpacing(4)
_dark_style = _btn_style(bg=ORA_DARK, en=ORA_DARK, pr="#8a3d00")
self.btn_font_minus = QPushButton("-")
self.btn_font_minus.setFixedSize(28, 26)
self.btn_font_minus.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_font_minus)
self.btn_vol_down = QPushButton("<")
self.btn_vol_down.setFixedSize(28, 26)
self.btn_vol_down.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_vol_down)
self.btn_language = QPushButton(LANGUAGES[0])
self.btn_language.setFixedHeight(26)
self.btn_language.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_language, stretch=1)
self.btn_vol_up = QPushButton(">")
self.btn_vol_up.setFixedSize(28, 26)
self.btn_vol_up.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_vol_up)
self.btn_font_plus = QPushButton("+")
self.btn_font_plus.setFixedSize(28, 26)
self.btn_font_plus.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_font_plus)
layout.addLayout(lang_row)
self._font_size = 10
self.btn_font_plus.clicked.connect(self._font_increase)
self.btn_font_minus.clicked.connect(self._font_decrease)
self.btn_vol_down.clicked.connect(self._vol_decrease)
self.btn_vol_up.clicked.connect(self._vol_increase)
self.status = QLabel("Starting...")
self.status.setAlignment(Qt.AlignCenter)
self.status.setStyleSheet(
"QLabel { background-color: #ffffff; color: #000000; padding: 1px; }"
)
layout.addWidget(self.status)
self.text = QTextEdit()
self.text.setReadOnly(True)
self.text.setTextInteractionFlags(Qt.NoTextInteraction)
layout.addWidget(self.text, stretch=1)
QScroller.grabGesture(
self.text.viewport(),
QScroller.LeftMouseButtonGesture
)
btn_row = QHBoxLayout()
btn_row.setSpacing(4)
_scroll_style = _btn_style(bg=ORA_DARK, en=ORA_DARK, pr="#8a3d00")
self.btn_scroll_up = QPushButton("▲")
self.btn_scroll_up.setFixedSize(40, 26)
self.btn_scroll_up.setStyleSheet(_scroll_style)
btn_row.addWidget(self.btn_scroll_up)
self.btn_explain = QPushButton("Explain")
self.btn_examples = QPushButton("Examples")
for btn in (self.btn_explain, self.btn_examples):
btn.setEnabled(False)
btn.setFixedHeight(26)
btn.setStyleSheet(_btn_style())
btn_row.addWidget(btn, stretch=1)
self.btn_scroll_down = QPushButton("▼")
self.btn_scroll_down.setFixedSize(40, 26)
self.btn_scroll_down.setStyleSheet(_scroll_style)
btn_row.addWidget(self.btn_scroll_down)
layout.addLayout(btn_row)
self.btn_scroll_down.clicked.connect(self._scroll_down)
self.btn_scroll_up.clicked.connect(self._scroll_up)
def update_status(self, txt):
self.status.setText(txt)
def update_answer(self, txt):
self.text.clear()
self.text.setPlainText(txt)
self.text.verticalScrollBar().setValue(0)
def set_explain_enabled(self, enabled: bool):
self.btn_explain.setEnabled(enabled)
def set_examples_enabled(self, enabled: bool):
self.btn_examples.setEnabled(enabled)
def update_language_label(self, lang: str):
self.btn_language.setText(lang)
def _scroll_down(self):
sb = self.text.verticalScrollBar()
sb.setValue(sb.value() + self.text.viewport().height())
def _scroll_up(self):
sb = self.text.verticalScrollBar()
sb.setValue(sb.value() - self.text.viewport().height())
def _font_increase(self):
self._font_size = min(self._font_size + 1, 28)
self._apply_font_size()
def _font_decrease(self):
self._font_size = max(self._font_size - 1, 6)
self._apply_font_size()
def _apply_font_size(self):
font = self.text.font()
font.setPointSize(self._font_size)
self.text.setFont(font)
def _vol_increase(self):
global _volume
_volume = min(round(_volume + VOLUME_STEP, 1), 1.0)
def _vol_decrease(self):
global _volume
_volume = max(round(_volume - VOLUME_STEP, 1), 0.0)
# AUDIO HELPERS ----------------------------------------------------
def normalize(pcm_bytes: bytes) -> bytes:
samples = np.frombuffer(pcm_bytes, dtype=np.int16).astype(np.float32)
peak = np.max(np.abs(samples))
if peak == 0:
return pcm_bytes
gain = (0.9 * 32767) / peak
return np.clip(samples * gain, -32768, 32767).astype(np.int16).tobytes()
def record_while_held(worker: AssistantWorker, stream) -> bytes:
try:
while stream.get_read_available() > 0:
stream.read(stream.get_read_available(), exception_on_overflow=False)
except Exception:
pass
worker.status.emit("Recording...")
frames = []
while button_a.is_pressed():
frames.append(stream.read(CHUNK, exception_on_overflow=False))
return b"".join(frames)
def pcm_to_wav(pcm_bytes: bytes) -> bytes:
buf = io.BytesIO()
with wave.open(buf, "wb") as wf:
wf.setnchannels(CHANNELS)
wf.setsampwidth(2)
wf.setframerate(SAMPLE_RATE)
wf.writeframes(pcm_bytes)
return buf.getvalue()
def generate_tts(text: str, filepath: str) -> None:
"""Call OpenAI TTS and save to filepath (mp3)."""
response = client.audio.speech.create(
model="tts-1",
voice="alloy",
input=text,
)
with open(filepath, "wb") as f:
f.write(response.content)
def play_audio(filepath: str) -> None:
"""Play audio at the current volume, then mute the output again."""
pygame.mixer.music.set_volume(_volume)
pygame.mixer.music.load(filepath)
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
time.sleep(0.05)
pygame.mixer.music.set_volume(0.0)
# OPENAI HELPERS ------------------------------------------------------
def transcribe(wav_bytes: bytes) -> str:
audio_file = io.BytesIO(wav_bytes)
audio_file.name = "recording.wav"
response = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
language="en",
temperature=0,
)
return response.text
def _script_hint(language: str) -> str:
"""Return a script hint for the system prompt based on language."""
hints = {
"Japanese": "in Kanji/Kana on one line, then the romaji reading on the next line",
"Italian": "in Italian script",
"German": "in German script",
}
return hints.get(language, "in the target language's script")
def translate_only(question: str, language: str) -> str:
hint = _script_hint(language)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
f"Translate the given English phrase to {language}. "
f"Provide the translation {hint}. "
"Do NOT include grammar explanations or example sentences."
),
},
{"role": "user", "content": question},
],
)
return response.choices[0].message.content
def extract_first_line(translation: str) -> str:
"""Return the first line of the translation for TTS (skips romaji line)."""
return translation.splitlines()[0].strip() if translation else translation
def explain_grammar(question: str, translation: str, language: str) -> str:
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
f"You are a {language} language teacher. "
f"Explain the grammar of the {language} translation clearly and concisely. "
"Do NOT provide additional example sentences."
),
},
{
"role": "user",
"content": (
f"Original English: {question}\n"
f"{language} translation: {translation}\n\n"
"Please explain the grammar."
),
},
],
)
return response.choices[0].message.content
def add_examples(question: str, translation: str, language: str) -> str:
hint = _script_hint(language)
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": (
f"You are a {language} language teacher. "
f"Provide 2-3 natural example sentences in {language} ({hint}) "
"that illustrate the same grammar pattern or vocabulary. "
"Include a short English translation for each."
),
},
{
"role": "user",
"content": (
f"Original English: {question}\n"
f"{language} translation: {translation}\n\n"
"Please provide example sentences."
),
},
],
)
return response.choices[0].message.content
# MAIN ------------------------------------------------------
def main():
app = QApplication(sys.argv)
ui = AssistantUI()
worker = AssistantWorker()
thread = QThread()
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.status.connect(ui.update_status)
worker.answer.connect(ui.update_answer)
worker.btn_explain_on.connect(ui.set_explain_enabled)
worker.btn_examples_on.connect(ui.set_examples_enabled)
worker.language_changed.connect(ui.update_language_label)
ui.btn_explain.clicked.connect(worker.on_explain_clicked, Qt.DirectConnection)
ui.btn_examples.clicked.connect(worker.on_examples_clicked, Qt.DirectConnection)
ui.btn_language.clicked.connect(worker.on_language_clicked, Qt.DirectConnection)
ui.show()
thread.start()
sys.exit(app.exec())
if __name__ == "__main__":
main()
Importazioni
Il programma inizia importando i moduli e le librerie Python necessari. Questi includono moduli standard come sys, time, os e threading per interazione con il sistema, temporizzazione, gestione file e concorrenza. Usa numpy per operazioni numeriche su dati audio, pyaudio per la registrazione audio e pygame per la riproduzione audio.
Il codice importa anche la libreria client OpenAI per accedere ai servizi AI, e i pulsanti hardware button_a e button_b dall’estensione UNIHIKER. Per l’interfaccia grafica (GUI) usa qtpy per creare widget e gestire segnali e slot.
Nota che dovrai installare la libreria OpenAI, poiché non è preinstallata sull’UNIHIKER M10. Se ti serve aiuto vedi il Voice Assistant on UNIHIKER M10 with OpenAI tutorial.
Costanti e configurazione
Diverse costanti definiscono il comportamento e l’aspetto dell’applicazione. Queste includono la chiave API OpenAI, parametri del dispositivo di input audio come frequenza di campionamento e canali, e costanti UI come lingue supportate e codici colore per lo stile di pulsanti e sfondi.
Sono impostati anche i parametri di controllo del volume, inclusi volume iniziale e passo per le regolazioni.
API_KEY = "sk-proj-my-api_key" DEVICE_INDEX = 2 SAMPLE_RATE = 16000 CHANNELS = 1 CHUNK = 1024 LANGUAGES = ["Japanese", "Italian", "German"] DEFAULT_STATUS = "Hold A: new | B: replay" VOLUME_STEP = 0.1 INITIAL_VOLUME = 0.8 _volume = INITIAL_VOLUME
Ricorda di sostituire il valore della costante API_KEY con la tua chiave API personale!
Stile dei pulsanti
La funzione _btn_style() restituisce una stringa di stylesheet che definisce l’aspetto dei pulsanti in diversi stati (normale, premuto, disabilitato). Questa funzione centralizza lo stile per mantenere un aspetto coerente nell’interfaccia.
Classe AssistantWorker
Questa classe incapsula la logica principale eseguita in un thread separato per mantenere l’interfaccia reattiva. Gestisce la registrazione audio, l’interazione con le API OpenAI e la gestione dello stato.
La classe definisce segnali per comunicare aggiornamenti di stato, risposte, stato dei pulsanti e cambi di lingua all’interfaccia.
Nel costruttore inizializza variabili per memorizzare l’ultima domanda, traduzione, spiegazione grammaticale e frasi di esempio. Configura anche lo stream di input audio usando pyaudio con il dispositivo e parametri specificati.
class AssistantWorker(QObject):
status = Signal(str)
answer = Signal(str)
btn_explain_on = Signal(bool)
btn_examples_on = Signal(bool)
language_changed = Signal(str)
def __init__(self):
super().__init__()
self._last_question = ""
self._last_translation = ""
self._last_grammar = ""
self._last_examples = ""
self._tts_ready = False
self._lang_index = 0
self._language = LANGUAGES[0]
self._explain_requested = False
self._examples_requested = False
self._lang_requested = False
self._lock = threading.Lock()
self._pa = pyaudio.PyAudio()
self._stream = self._pa.open(
format=pyaudio.paInt16,
channels=CHANNELS,
rate=SAMPLE_RATE,
input=True,
input_device_index=DEVICE_INDEX,
frames_per_buffer=CHUNK,
)
La classe fornisce metodi slot per gestire i click sui pulsanti per richiedere spiegazioni grammaticali, frasi di esempio e cambi di lingua. Questi metodi impostano flag thread-safe che il ciclo principale monitora.
Ciclo di esecuzione di AssistantWorker
Il metodo run() contiene il ciclo principale che controlla continuamente lo stato dei pulsanti hardware e processa l’input utente di conseguenza.
Quando il pulsante A è premuto, registra audio dal microfono. Quando il pulsante A viene rilasciato, normalizza l’audio registrato, lo converte in formato WAV e lo invia al modello Whisper di OpenAI per la trascrizione. Il testo inglese trascritto viene poi tradotto nella lingua selezionata usando il modello GPT di OpenAI.
Successivamente il testo tradotto viene mostrato e viene generato e riprodotto un file audio TTS. Il pulsante B permette di riascoltare l’ultimo audio generato. Premendo il pulsante lingua si cicla tra le lingue supportate.
Se l’utente richiede spiegazioni grammaticali o frasi di esempio, il worker chiama gli endpoint API OpenAI appropriati per generare i contenuti e aggiorna la visualizzazione.
def run(self):
self.status.emit("Hold Button A to speak")
while True:
a_pressed = button_a.is_pressed()
b_pressed = button_b.is_pressed()
with self._lock:
explain_req = self._explain_requested
examples_req = self._examples_requested
lang_req = self._lang_requested
self._explain_requested = False
self._examples_requested = False
self._lang_requested = False
if lang_req and not a_pressed:
self._lang_index = (self._lang_index + 1) % len(LANGUAGES)
self._language = LANGUAGES[self._lang_index]
self.language_changed.emit(self._language)
elif a_pressed:
self._clear_state()
raw_pcm = record_while_held(self, self._stream)
if not raw_pcm:
self.status.emit("Hold Button A to speak")
continue
self.status.emit("Transcribing...")
normalized = normalize(raw_pcm)
wav_bytes = pcm_to_wav(normalized)
question = transcribe(wav_bytes)
self.status.emit(f"Translating to {self._language}...")
translation = translate_only(question, self._language)
self._last_question = question
self._last_translation = translation
self._refresh_display()
self.btn_explain_on.emit(True)
self.btn_examples_on.emit(True)
self.status.emit("Generating audio...")
tts_text = extract_first_line(translation)
generate_tts(tts_text, TTS_FILE)
self._tts_ready = True
self._set_default_status()
play_audio(TTS_FILE)
elif b_pressed:
while button_b.is_pressed():
time.sleep(0.01)
if self._tts_ready:
self.status.emit("Replaying...")
play_audio(TTS_FILE)
self._set_default_status()
elif explain_req and self._last_translation:
self.status.emit("Explaining grammar...")
self._last_grammar = explain_grammar(
self._last_question, self._last_translation, self._language
)
self._refresh_display()
self._set_default_status()
elif examples_req and self._last_translation:
self.status.emit("Generating examples...")
self._last_examples = add_examples(
self._last_question, self._last_translation, self._language
)
self._refresh_display()
self._set_default_status()
else:
time.sleep(0.02)
Metodi di gestione dello stato
La classe worker include metodi di supporto per resettare lo stato interno, aggiornare il testo mostrato e impostare il messaggio di stato predefinito. Questi metodi assicurano che l’interfaccia rifletta accuratamente lo stato corrente dell’applicazione.
Classe AssistantUI
Questa classe definisce l’interfaccia grafica usando widget Qt. Crea una finestra a dimensione fissa con sfondo arancione e testo nero, in linea con lo schema colori definito.
L’interfaccia consiste in una riga superiore con pulsanti per la regolazione della dimensione del font, controllo del volume e selezione della lingua. Sotto c’è un’etichetta di stato che mostra messaggi all’utente. L’area principale mostra il testo trascritto, tradotto e le spiegazioni.
In basso, pulsanti permettono di scorrere il testo e richiedere spiegazioni grammaticali o frasi di esempio. I pulsanti sono stilizzati in modo coerente usando gli stili definiti.
La classe fornisce metodi per aggiornare il testo di stato, la risposta mostrata, abilitare o disabilitare pulsanti e cambiare l’etichetta della lingua. Gestisce anche le interazioni utente per lo scorrimento e la regolazione di font e volume.
class AssistantUI(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Voice Chatbot")
self.setFixedSize(240, 320)
self.setStyleSheet(
f"QWidget {{ background-color: {ORA_BG}; color: {ORA_TEXT}; }}"
f"QTextEdit {{ background-color: #ffffff; color: {ORA_TEXT};"
f" border: none; }}"
)
layout = QVBoxLayout(self)
layout.setContentsMargins(4, 4, 4, 4)
layout.setSpacing(3)
lang_row = QHBoxLayout()
lang_row.setSpacing(4)
_dark_style = _btn_style(bg=ORA_DARK, en=ORA_DARK, pr="#8a3d00")
self.btn_font_minus = QPushButton("-")
self.btn_font_minus.setFixedSize(28, 26)
self.btn_font_minus.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_font_minus)
self.btn_vol_down = QPushButton("<")
self.btn_vol_down.setFixedSize(28, 26)
self.btn_vol_down.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_vol_down)
self.btn_language = QPushButton(LANGUAGES[0])
self.btn_language.setFixedHeight(26)
self.btn_language.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_language, stretch=1)
self.btn_vol_up = QPushButton(">")
self.btn_vol_up.setFixedSize(28, 26)
self.btn_vol_up.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_vol_up)
self.btn_font_plus = QPushButton("+")
self.btn_font_plus.setFixedSize(28, 26)
self.btn_font_plus.setStyleSheet(_dark_style)
lang_row.addWidget(self.btn_font_plus)
layout.addLayout(lang_row)
self._font_size = 10
self.btn_font_plus.clicked.connect(self._font_increase)
self.btn_font_minus.clicked.connect(self._font_decrease)
self.btn_vol_down.clicked.connect(self._vol_decrease)
self.btn_vol_up.clicked.connect(self._vol_increase)
self.status = QLabel("Starting...")
self.status.setAlignment(Qt.AlignCenter)
self.status.setStyleSheet(
"QLabel { background-color: #ffffff; color: #000000; padding: 1px; }"
)
layout.addWidget(self.status)
self.text = QTextEdit()
self.text.setReadOnly(True)
self.text.setTextInteractionFlags(Qt.NoTextInteraction)
layout.addWidget(self.text, stretch=1)
QScroller.grabGesture(
self.text.viewport(),
QScroller.LeftMouseButtonGesture
)
btn_row = QHBoxLayout()
btn_row.setSpacing(4)
_scroll_style = _btn_style(bg=ORA_DARK, en=ORA_DARK, pr="#8a3d00")
self.btn_scroll_up = QPushButton("▲")
self.btn_scroll_up.setFixedSize(40, 26)
self.btn_scroll_up.setStyleSheet(_scroll_style)
btn_row.addWidget(self.btn_scroll_up)
self.btn_explain = QPushButton("Explain")
self.btn_examples = QPushButton("Examples")
for btn in (self.btn_explain, self.btn_examples):
btn.setEnabled(False)
btn.setFixedHeight(26)
btn.setStyleSheet(_btn_style())
btn_row.addWidget(btn, stretch=1)
self.btn_scroll_down = QPushButton("▼")
self.btn_scroll_down.setFixedSize(40, 26)
self.btn_scroll_down.setStyleSheet(_scroll_style)
btn_row.addWidget(self.btn_scroll_down)
layout.addLayout(btn_row)
self.btn_scroll_down.clicked.connect(self._scroll_down)
self.btn_scroll_up.clicked.connect(self._scroll_up)
Funzioni di supporto audio
Diverse funzioni di supporto gestiscono compiti di elaborazione audio. La funzione normalize() regola l’audio PCM registrato per massimizzare il volume senza distorsioni. record_while_held() registra l’audio dal microfono mentre il pulsante A è premuto. La funzione pcm_to_wav() converte i byte PCM grezzi in formato WAV, necessario per la trascrizione.
Segue la funzione generate_tts() che invia testo all’API TTS di OpenAI e salva il file audio risultante. Infine, play_audio() riproduce l’audio generato usando pygame al volume corrente.
Funzioni di supporto OpenAI
Queste funzioni interagiscono con l’API OpenAI per trascrizione, traduzione, spiegazioni grammaticali e generazione di frasi di esempio.
La funzione transcribe() invia l’audio WAV registrato al modello Whisper per ottenere il testo in inglese. translate_only() traduce la domanda in inglese nella lingua selezionata senza spiegazioni aggiuntive.
Le funzioni explain_grammar() e add_examples() richiedono rispettivamente spiegazioni grammaticali e frasi di esempio, usando GPT con prompt specifici per l’insegnamento delle lingue.
Funzione principale
La funzione main() inizializza l’applicazione Qt, crea istanze delle classi UI e worker, e configura un thread separato per eseguire il worker in parallelo.
Collega segnali e slot tra worker e UI per aggiornare l’interfaccia in base ai progressi del worker e alle interazioni utente. Infine avvia il ciclo eventi dell’applicazione.
def main():
app = QApplication(sys.argv)
ui = AssistantUI()
worker = AssistantWorker()
thread = QThread()
worker.moveToThread(thread)
thread.started.connect(worker.run)
worker.status.connect(ui.update_status)
worker.answer.connect(ui.update_answer)
worker.btn_explain_on.connect(ui.set_explain_enabled)
worker.btn_examples_on.connect(ui.set_examples_enabled)
worker.language_changed.connect(ui.update_language_label)
ui.btn_explain.clicked.connect(worker.on_explain_clicked, Qt.DirectConnection)
ui.btn_examples.clicked.connect(worker.on_examples_clicked, Qt.DirectConnection)
ui.btn_language.clicked.connect(worker.on_language_clicked, Qt.DirectConnection)
ui.show()
thread.start()
sys.exit(app.exec())
In sintesi, questo codice integra input da pulsanti hardware, elaborazione audio, servizi AI per le lingue e una GUI reattiva per creare un tutor linguistico interattivo basato sulla voce. Sfrutta i modelli OpenAI per trascrizione, traduzione e insegnamento.
Conclusioni
In questo tutorial hai imparato come implementare un semplice AI Language Tutor sull’UNIHIKER M10 usando i servizi OpenAI. Ti consiglio anche di leggere il Voice Assistant on UNIHIKER M10 with OpenAI tutorial, che copre alcune basi non incluse in questo.
Anche se l’AI Language Tutor di questo tutorial è già utile per l’apprendimento linguistico, ci sono molte possibili estensioni per renderlo ancora più efficace. Per esempio, il programma potrebbe memorizzare traduzioni e spiegazioni per rivederle in seguito. Potrebbe generare brevi storie attorno alle frasi e leggerle ad alta voce. Potrebbe estrarre verbi e sostantivi per allenarli separatamente. E molto altro…
Divertiti a estendere il tutor per i tuoi scopi, e se hai domande sentiti libero di lasciarle nei commenti.
Buon divertimento con il tinkering 😉

