Dans ce tutoriel, vous apprendrez à créer votre propre tuteur de langue IA sur un UNIHIKER M10 en utilisant les modèles d’IA d’OpenAI. Vous aurez besoin d’un compte OpenAI et d’une clé API pour cela.
Le UNIHIKER M10 est une petite carte avec 512 Mo de RAM et 16 Go de stockage eMMC, ainsi qu’un écran tactile de 2,8 pouces. En plus du Wi-Fi et du Bluetooth, la carte intègre des capteurs tels qu’un capteur de lumière, un accéléromètre, un gyroscope et, surtout, un microphone.
Il fonctionne sous Debian Linux, supporte la programmation en Python et est livré avec de nombreuses bibliothèques Python préinstallées. Cela facilite la mise en œuvre de solutions IA comme un tuteur de langue IA. Le court clip vidéo suivant montre le tuteur de langue IA que nous allons construire.
Vous devrez peut-être augmenter le volume de votre ordinateur pour entendre ma voix et la réponse de l’IA.
Pièces requises
Pour ce tutoriel, vous avez besoin d’une carte UNIHIKER M10. Vous pouvez l’acheter chez DFRobot ou sur Amazon. Si vous avez un haut-parleur USB, vous pouvez l’utiliser en le connectant au UNIHIKER M10. Mais si vous souhaitez ajouter vos propres petits haut-parleurs, vous aurez besoin d’un amplificateur PAM8403 et d’un ou deux des haut-parleurs listés ci-dessous.

UNIHIKER M10

Amplificateur PAM8403

2 x haut-parleurs 3 Watts 4 Ohms
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.
Matériel du UNIHIKER M10
Le UNIHIKER M10 est un ordinateur monocarte compact destiné à l’éducation, au prototypage et aux applications AIoT. Il intègre un système basé sur Linux, des capteurs et des interfaces matérielles sur une seule carte.
L’appareil est basé sur un processeur quad-core Arm Cortex-A35 cadencé jusqu’à 1,2 GHz. Il comprend 512 Mo de RAM et 16 Go de stockage eMMC et fonctionne sous Debian Linux.

La carte intègre un écran tactile de 2,8 pouces avec une résolution de 240 × 320 pixels, une connectivité Wi-Fi et Bluetooth. Les capteurs intégrés comprennent un capteur de lumière, un accéléromètre, un gyroscope et un microphone. L’extension matérielle est possible via des ports USB, des connecteurs de capteurs Gravity et un connecteur de bord compatible micro:bit qui expose des interfaces GPIO, I2C, SPI et UART.
Pour plus de détails techniques, consultez notre Voice Assistant on UNIHIKER M10 with OpenAI tutoriel. Ce tutoriel vous expliquera également comment programmer le UNIHIKER, comment installer des bibliothèques Python et comment configurer le Wi-Fi. Tout cela est nécessaire pour faire fonctionner le tuteur IA que nous allons construire dans ce tutoriel.
Connexion des haut-parleurs au UNIHIKER M10
Notre tuteur IA pourra générer de la parole. Pour cela, nous aurons besoin de haut-parleurs. Vous pouvez connecter des haut-parleurs au UNIHIKER M10 via USB ou Bluetooth. C’est la solution la plus simple et si vous optez pour celle-ci, vous pouvez passer cette section.
Je voulais utiliser des haut-parleurs plus petits pour rendre mon tuteur IA portable. Le UNIHIKER M10 dispose d’une sortie lineout mais malheureusement sans connecteur. Vous devez souder des fils sur des pastilles spécifiques à l’arrière de la carte et vous avez aussi besoin d’un petit amplificateur pour piloter les haut-parleurs. J’ai récupéré le câblage depuis le AI Assistant with OpenAI GPT, Azure Speech API and UNIHIKER post.
Amplificateur PAM8403
J’ai utilisé un module PAM8403 comme amplificateur. Le module PAM8403 est un petit amplificateur audio stéréo de classe D basé sur la puce PAM8403. Il accepte une tension d’alimentation de 2,5 V à 5,5 V. À 5 V, et en pilotant des haut-parleurs de 4 Ω, chaque canal peut fournir jusqu’à environ 3 W de puissance de sortie. L’image suivante montre le brochage du module amplificateur PAM8403 :

Pour plus d’informations sur le module amplificateur PAM8403, consultez notre Audio with PAM8403, PCM5102 and ESP32 tutoriel.
Sortie lineout du UNIHIKER M10
Comme mentionné, il n’y a pas de connecteur pour la sortie lineout sur le UNIHIKER M10, mais vous pouvez y accéder via des pastilles (test) à l’arrière de la carte (schematics). La photo ci-dessous montre l’emplacement des pastilles lineout et d’alimentation dont nous avons besoin pour connecter l’amplificateur PAM8403 :

J’ai mesuré 4,7 V sur la pastille VCC, ce qui est étrange car je m’attendais à 3,3 V ou 5 V, mais le PAM8403 fonctionne bien avec 4,7 V. De plus, ma carte n’avait qu’une seule pastille pour VCC, alors que la photo ci-dessus en montre deux. Mais tout a bien fonctionné malgré tout.
Connexion de l’amplificateur et des haut-parleurs au UNIHIKER M10
Dans cette section, je vais vous montrer comment connecter l’amplificateur PAM8403 et les haut-parleurs au UNIHIKER M10. Les petits haut-parleurs de 3 W sont souvent fournis avec des fiches qu’il faudra couper, car nous soudons les fils des haut-parleurs directement sur le module PAM8403 :

L’image suivante montre comment câbler le UNIHIKER M10 à l’amplificateur PAM8403 et aux haut-parleurs :

Souder les fils sur les pastilles du UNIHIKER M10 est facile, mais assurez-vous de connecter aux bonnes pastilles et de ne rien endommager pendant l’opération. La photo ci-dessous montre mon câblage terminé :

Notez que vous n’avez pas besoin de deux haut-parleurs, car le son stéréo n’est pas vraiment nécessaire. Mais deux haut-parleurs seront plus puissants qu’un seul. Si vous voulez un son vraiment fort, utilisez un haut-parleur USB avec une alimentation externe.
Manuel utilisateur pour le tuteur de langue IA
Dans cette section, je vais rapidement expliquer comment fonctionne l’interface utilisateur du tuteur de langue IA. Cela vous facilitera la compréhension du code dans la section suivante et l’utilisation du logiciel du tuteur. L’image ci-dessous montre l’interface graphique avec les éléments fonctionnels annotés :

Vous appuyez sur le bouton A et le maintenez en parlant. Lorsque vous relâchez le bouton A, l’audio enregistré est envoyé à OpenAI pour transcription et traduction. La transcription et la traduction sont ensuite affichées dans le champ Réponse et la traduction est lue à voix haute. Vous pouvez rejouer l’audio de la traduction en appuyant sur le bouton B.
Vous pouvez sélectionner la langue cible pour la traduction en appuyant sur le bouton « Select Language » en haut. Il fait actuellement défiler « Japanese », « Italian » et « German » comme langues. Mais vous pouvez facilement étendre cela à d’autres langues ou plus de langues dans le code.
Notez que, bien que l’interface utilisateur soit en anglais, vous pouvez en fait parler dans n’importe quelle langue prise en charge par le modèle TTS d’OpenAI (link). Le système comprendra la langue (si elle est prise en charge) et traduira dans la langue cible sélectionnée. Vous pouvez même parler dans la langue cible pour vérifier votre prononciation. Voir les deux clips vidéo suivants pour une démo, où je parle allemand et japonais :
Lorsque vous appuyez sur le bouton « Explain » en bas, une explication de la grammaire de la traduction s’affiche. De même, si vous appuyez sur le bouton « Example », des phrases exemples avec une grammaire similaire sont ajoutées. Les deux clips vidéo suivants démontrent cette fonctionnalité :
Enfin, il y a des boutons pour augmenter et diminuer le volume et la taille de la police en haut de l’écran, ainsi que des boutons de défilement en bas de l’écran.
Bien que vous puissiez utiliser vos doigts pour contrôler l’interface, utiliser le petit stylet fourni avec le UNIHIKER M10 fonctionne mieux. Notez que vous pouvez même toucher le champ réponse et le faire glisser/défiler au lieu d’utiliser les boutons de défilement ou la barre de défilement.
Obtention de la clé API OpenAI
Notre tuteur de langue IA va utiliser les modèles d’IA fournis par OpenAI. Vous aurez donc besoin d’un compte OpenAI et d’une clé API. Rendez-vous sur https://platform.openai.com et inscrivez-vous avec une adresse e-mail ou un compte Google ou Microsoft existant.
Après avoir vérifié votre e-mail et complété la configuration initiale, connectez-vous au tableau de bord OpenAI, platform.openai.com/api-keys et trouvez ou créez votre clé API (=SECRET KEY) comme montré ci-dessous :

La clé API est une chaîne unique et longue, commençant par « sk-proj-« , nécessaire pour authentifier vos requêtes API. Vous devrez plus tard copier cette chaîne entière dans le code du tuteur de langue IA.
Code pour le tuteur de langue IA
Le code suivant implémente notre application de tuteur de langue alimentée par IA. Il permet aux utilisateurs de prononcer des phrases dans n’importe quelle langue, qui sont ensuite transcrites, traduites dans une langue cible sélectionnée, et lues à voix haute via la synthèse vocale (TTS). L’application offre aussi des explications grammaticales et des phrases exemples pour faciliter l’apprentissage.
# 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()
Importations
Le programme commence par importer les modules et bibliothèques Python nécessaires. Ceux-ci incluent des modules standards comme sys, time, os, et threading pour l’interaction système, la gestion du temps, des fichiers et la concurrence. Il utilise numpy pour les opérations numériques sur les données audio, pyaudio pour l’enregistrement audio, et pygame pour la lecture audio.
Le code importe aussi la bibliothèque cliente OpenAI pour accéder aux services IA, ainsi que les boutons matériels button_a et button_b de l’extension UNIHIKER. Pour l’interface graphique (GUI), il utilise qtpy pour créer des widgets et gérer les signaux et slots.
Notez que vous devrez installer la bibliothèque OpenAI, car elle n’est pas préinstallée sur le UNIHIKER M10. Si vous avez besoin d’aide, consultez le Voice Assistant on UNIHIKER M10 with OpenAI tutoriel.
Constantes et configuration
Plusieurs constantes définissent le comportement et l’apparence de l’application. Cela inclut la clé API OpenAI, les paramètres du périphérique d’entrée audio comme la fréquence d’échantillonnage et les canaux, ainsi que des constantes liées à l’interface utilisateur comme les langues supportées et les codes couleur pour le style des boutons et des arrière-plans.
Les paramètres de contrôle du volume sont aussi définis, incluant le volume initial et le pas d’ajustement du volume.
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
N’oubliez pas de remplacer la valeur de la constante API_KEY par votre propre clé API !
Style des boutons
La fonction _btn_style() retourne une chaîne de style qui définit l’apparence des boutons dans différents états (normal, pressé, désactivé). Cette fonction centralise le style pour maintenir une apparence cohérente dans toute l’interface.
Classe AssistantWorker
Cette classe encapsule la logique principale qui s’exécute dans un thread séparé pour garder l’interface réactive. Elle gère l’enregistrement audio, l’interaction avec les API OpenAI, et la gestion des états.
La classe définit des signaux pour communiquer les mises à jour de statut, les réponses, l’état des boutons et les changements de langue vers l’interface.
Dans le constructeur, elle initialise des variables pour stocker la dernière question, la traduction, l’explication grammaticale et les phrases exemples. Elle configure aussi le flux d’entrée audio avec pyaudio en utilisant le périphérique et les paramètres spécifiés.
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 fournit des méthodes slot pour gérer les clics sur les boutons demandant des explications grammaticales, des phrases exemples et des changements de langue. Ces méthodes définissent des drapeaux thread-safe que la boucle principale surveille.
Boucle d’exécution d’AssistantWorker
La méthode run() contient la boucle principale qui vérifie continuellement l’état des boutons matériels et traite les entrées utilisateur en conséquence.
Lorsque le bouton A est maintenu, elle enregistre l’audio du microphone. Lorsqu’on relâche le bouton A, elle normalise l’audio enregistré, le convertit en format WAV, et l’envoie au modèle Whisper d’OpenAI pour transcription. Le texte anglais transcrit est ensuite traduit dans la langue sélectionnée via le modèle GPT d’OpenAI.
Ensuite, le texte traduit est affiché, un fichier audio TTS est généré et lu. Le bouton B permet de rejouer le dernier audio généré. Le bouton de langue fait défiler les langues supportées.
Si l’utilisateur demande des explications grammaticales ou des phrases exemples, le worker appelle les points d’API OpenAI appropriés pour générer le contenu et met à jour l’affichage.
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)
Méthodes de gestion d’état
La classe worker inclut des méthodes auxiliaires pour réinitialiser l’état interne, mettre à jour le texte affiché, et définir le message de statut par défaut. Ces méthodes garantissent que l’interface reflète correctement l’état actuel de l’application.
Classe AssistantUI
Cette classe définit l’interface graphique utilisateur avec des widgets Qt. Elle crée une fenêtre de taille fixe avec un fond orange et un texte noir, correspondant au schéma de couleurs défini précédemment.
L’interface comprend une rangée supérieure avec des boutons pour ajuster la taille de la police, le volume et la sélection de la langue. En dessous, un label de statut affiche des messages à l’utilisateur. La zone de texte principale montre le texte transcrit, traduit et explicatif.
En bas, des boutons permettent de faire défiler le texte et de demander des explications grammaticales ou des phrases exemples. Les boutons sont stylisés de manière cohérente avec les styles définis précédemment.
La classe fournit des méthodes pour mettre à jour le texte de statut, la réponse affichée, activer ou désactiver les boutons, et changer le label de langue. Elle gère aussi les interactions utilisateur pour le défilement et l’ajustement de la taille de police et du 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)
Fonctions auxiliaires audio
Plusieurs fonctions auxiliaires gèrent le traitement audio. La fonction normalize() ajuste l’audio PCM enregistré pour maximiser le volume sans saturation. record_while_held() enregistre l’audio du microphone pendant que le bouton A est pressé. Et la fonction pcm_to_wav() convertit les octets PCM bruts en format WAV, nécessaire pour la transcription.
Ensuite, la fonction generate_tts() envoie du texte à l’API TTS d’OpenAI et sauvegarde le fichier audio résultant. Enfin, play_audio() joue l’audio généré avec pygame au volume actuel.
Fonctions auxiliaires OpenAI
Ces fonctions interagissent avec l’API OpenAI pour effectuer la transcription, la traduction, l’explication grammaticale et la génération de phrases exemples.
La fonction transcribe() envoie l’audio WAV enregistré au modèle Whisper pour obtenir le texte en anglais. translate_only() traduit la question anglaise dans la langue sélectionnée sans explications supplémentaires.
Les fonctions explain_grammar() et add_examples() demandent respectivement des explications grammaticales et des phrases exemples, en utilisant GPT avec des invites adaptées à l’enseignement des langues.
Fonction principale
La fonction main() initialise l’application Qt, crée des instances des classes UI et worker, et configure un thread séparé pour exécuter le worker en parallèle.
Elle connecte les signaux et slots entre le worker et l’UI pour mettre à jour l’interface selon la progression du worker et les interactions utilisateur. Enfin, elle lance la boucle d’événements de l’application.
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())
En résumé, ce code intègre l’entrée des boutons matériels, le traitement audio, les services linguistiques IA et une interface graphique réactive pour créer un tuteur de langue interactif basé sur la voix. Il exploite les modèles d’OpenAI pour la transcription, la traduction et l’enseignement des langues.
Conclusions
Dans ce tutoriel, vous avez appris à implémenter un tuteur de langue IA simple sur le UNIHIKER M10 en utilisant les services OpenAI. Je vous recommande également de lire le Voice Assistant on UNIHIKER M10 with OpenAI tutoriel, qui couvre des bases non abordées ici.
Bien que le tuteur de langue IA de ce tutoriel soit déjà utile pour l’apprentissage des langues, de nombreuses extensions sont possibles pour le rendre encore plus performant. Par exemple, le programme pourrait stocker les traductions et explications pour les revoir plus tard. Il pourrait générer de courtes histoires autour des phrases et les lire à voix haute. Il pourrait extraire les verbes et noms pour un entraînement séparé. Et bien plus encore…
Amusez-vous à étendre le tuteur selon vos besoins, et si vous avez des questions, n’hésitez pas à les laisser dans la section commentaires.
Bon bricolage 😉

