Собираю робота на Raspberry Pi 5 для дочери. Но говорящей колонкой тут не отделаться! Вызов принят, погнали

Я хотел сделать дочери робота помощника с локальным ИИ, чтоб она не отвлекалась от уроков, а наоборот увлеченно их делала.
Но ничего для этого у меня небыло. Была только идея: сделать для дочери робота, с которым можно разговаривать даже без интернета. И хотелось, чтобы он не выглядел просто очередной колонкой с часами или черным лицом с синими глазами. Поэтому первым делом я сделал на ПК анимированное лицо — обычное роботическое, без волос, банта и прочих украшений, хотелось что-то привлекательное.
Показал дочери. Она посмотрела и спросила: «А можно, чтобы это была аниме-кошечка?»
Так первоначальный робот начал превращаться в персонажа. Я стал расспрашивать, что именно ей нравится: глаза, причёска, одежда, выражение лица. Первого варианта оказалось мало. Мысли у неё были довольно конкретные, и выяснилось, что недостаточно просто приделать ушки к экрану. В итоге появились два подробных эскиза образа. Один — с тёмными волосами и розовым кимоно, другой — в более светлых цветах. С этого момента я уже делал не абстрактного робота, а маленького собеседника для своего ребёнка.
первый вариант ей понравился как модель кимоно:

и второй, который в итоге по цветовой гамме понравился ей больше:

Из картинки — в живое лицо
Я начал с того, что можно было проверять без железа. Написал приложение на Python с экранным лицом: глаза, зрачки, брови, рот, моргание. Добавил улыбку и несколько выражений. Когда робот произносил ответ, рот должен был двигаться вместе со звуком. Если программа надолго замирает между вопросом и ответом, никакие красивые глаза не спасают: ребёнок решит, что она сломалась или что хуже просто игнорит тебя. Так постепенно у лица появились состояния «слушаю», «думаю» и обычная спокойная мимика. Но я оставил и другие, первые сделанные лица, их можно менять в настройках.

Для интерфейса я использовал Python и Tkinter. Отрисовку лица отделил от обработки голоса и ответа модели: ожидание сети или генерации текста не должно останавливать моргание. Позже это разделение пригодилось и для движения головы.
Параллельно я понял, что одной анимации мало. Роботу нужен мозг, причём локальный. Но подо что его собирать? Arduino для такого диалога не подходила. Я поискал одноплатный компьютер, сравнил возможности, посмотрел что люди уже запрускали и на чем, и нашёл на Авито Raspberry Pi 5 с 8 ГБ памяти по адекватной цене и заказал его. Хотелось запустить на нём модель примерно на 4 миллиарда параметров. Пока посылка ехала, я продолжал писать программу на ПК и выбирать модель.
Двадцать(!!!) моделей и один маленький компьютер
Я скачал около двадцати LLM-кандидатов. Часть отсеялась быстро: одни плохо держали заданный характер, другие невнятно отвечали на простые вопросы или путались в длинной реплике. Для оставшихся я собрал набор проверок — арифметика, объяснения, связная речь, следование инструкции. Самый «умный» ответ в отдельном тесте не был победой: ребёнку ещё надо дождаться его вслух.
В финале у меня остались четыре GGUF-модели. На Pi 5 основным вариантом стала Qwen3-4B-Instruct-2507: в моём наборе она дала лучший баланс качества и ожидания. Gemma_4_E2B отвечала быстрее и пригодилась как вариант для короткого разговора. Gemma_4_E4B выглядела сильнее в некоторых объяснениях, но работала медленнее. Qwen3.5_9B_DeepSeek_Distill я оставил запасным экспериментом: она интересная, но для маленького робота её длинный ответ кажется очень долгим по времени (там и я уже думал что завис).
В одной из проверок на самом Pi скорость генерации получилась примерно 4,3 токена/с у Qwen3 4B и 7,1 токена/с у Gemma E2B. Это измерения моего набора задач и настроек, не универсальный рейтинг моделей. Для ребёнка разница ощущается просто: задала вопрос — ждёт. Поэтому быстрый ответ иногда полезнее чуть более подробного.
Речь я тоже оставил локальной. Микрофонный звук распознавал Vosk, а ответ озвучивал Piper. Базовый голос «Ирина» был хорош, но персонажу дочери хотелось более мультяшного звучания или даже скорее анимешного. Я не обучал отдельную голосовую модель, да и времени не было: замедлил исходный синтез и изменил высоту уже готового звука. В пресете это выглядит так:
'anime_kawaii': ('irina', 1.38, 1.30)
Здесь 1,38 - коэффициент изменения высоты, 1,30 - замедление исходного синтеза; после этого звук приводится к частоте аудиоустройства. Подбирал на слух: слишком высокий голос превращался в писк, слишком медленный утомлял. Когда получилось приятнее, стало ясно, что просто хлопающих глаз на экране уже мало.
А если голова повернётся?
Я задумался: ну допустим персонаж смотрит на человека, но как он поймёт, где тот стоит, да и по сути ребенок будет ждать внимание наверно? Сначала придумал небольшой поворот головы. Потом добавил к этой идее камеру и поиск лица. Началась охота за деталями.
Для сравнения заказал MG90S и MG996R. Первый маленький, второй заметно крупнее - как раз хотелось проверить, справится ли лёгкий привод с будущей головой.

Управлять ими решил через PCA9685. Для камеры выбрал модуль на IMX708 с автофокусом. Экран — сенсорный Waveshare 3.5-inch DSI LCD (H), 480×800.
Для звука я постепенно собрал отдельную цепочку: цифровой микрофон ICS-43434 подключается к Pi через I²S, воспроизведение идёт через USB-звуковую карту и усилитель к динамикам. Аналоговый MAX9814 тоже рассматривал, но основным микрофоном он не стал. Ещё заказал два подшипника 6901 — они понадобятся для опоры шеи, чтоб не было люфта ни при повороте ни при касании экрана пальцем или стилусом.
Пока нужная камера ехала, я поставил на пробный кронштейн камеру от 3D-принтера, которой до этого пользовался как вебкой, а для разговора взял микрофон компьютера. Это был некрасивый, но полезный стенд: уже можно было наблюдать, находит ли программа лицо и куда она посылает сервопривод.

С питанием PCA9685 вышла отдельная история. На зелёном клеммнике я измерил 5 В, а на силовом контакте ряда сервоприводов — ноль. Через боковой вход V+ те же 5 В до ряда дошли, и привод заработал. Кто виноват и как исправить, я нашел в отзывах маркетплейса, но не стал запаивать, оставил вход V+. Зато на практике усвоил различие: VCC питает логику платы от 3,3 В Pi, а V+ - отдельную силовую шину серво на 5 В. Просто видеть контроллер по I²C ещё не значит, что мотор получил питание.
Настоящая камера и первая голова
Когда пришла IMX708, я сначала проверил не паспортную, а реальную скорость. На Pi 5 при сохранении полного угла обзора захват дал 50,03 кадра/с. С включённым поиском лица я получил 29,90 результата/с в 12-секундной пробе после прогрева. Это два разных числа: камера успевает снять кадры чаще, чем программа обработать лицо. Для плавного слежения мне хватило примерно 30 обновлений координат за секунду.

Экран тоже пришлось запускать и поворачивать уже на Pi. Потом приехал цифровой микрофон. Впервые удалось убрать временные устройства ПК и посмотреть, как ведут себя вместе камера, лицо, голос и привод.
В этот же период я добавил возможность обратиться к DeepSeek по API. Для пробы пополнил счёт на 200 рублей. Ответы на некоторые вопросы стали богаче, но локальные Vosk, Piper и модель на Pi я оставил: исходная идея робота без обязательного интернета никуда не делась. В программе я подбирал длину ответа и подсказку для модели, чтобы получалась реплика собеседника, а не монолог на полминуты.
С механикой всё оказалось муторнее, чем с красивым эскизом. Я подгонял 3D-модель, вырезал место под экран, камеру и крепёж, примерял шлейфы, печатал детали, снова мерил. На картинке всё сходится; в пластике внезапно мешает толщина платы, винт оказывается недоступен отвёртке, а отверстие корпуса зажимает подвижный объектив. Последнее я проверил руками: освободил объектив — автофокус заработал нормально. Поэтому готовность 3D модели ещё не означает готовности собранной головы. Заодно потратил остатки того пластика, который не жалко и валялся уже пару лет.

И вот я запустил первый стенд с экраном, камерой и движением. Голова находила человека и поворачивалась. Казалось бы, цель достигнута. Но я смотрел на неё и понимал: чего-то не хватает, помимо подбора пропорций лица (их я подобрал позже с большим вниманием и вообще сделал настраиваемой в меню).

Конечно. Если собеседник сместился чуть в сторону, сначала должны посмотреть глаза, потом при необходимости повернуться голова. И зрачки должны идти вместе с глазами, а не застывать на месте. Я начал связывать координаты лица с положением глаз и командой привода: сначала взгляд, потом плавный доворот, без рывка за каждым пикселем камеры. Очень долго получалось криво и в неверную сторону. После тщательной настройки движение вдруг стало убедительным: на меня смотрел уже не экран, а персонаж.

MG90S на лёгком кронштейне справлялся, но были рывки, а MG996R в сравнительной пробе оказался плавнее. Окончательный угол поворота и люфт я оставил до полной сборки.
«Как назовём?»
Появилась ещё одна неожиданная проблема. Робот хорошо слышал речь и охотно отвечал — на любую речь вокруг. Стоило поговорить между собой, как он уже считал, что обращаются к нему. Позвал дочь и спросил, как назвать персонажа.
«Лягушка», — ответила она.
WHAT!?!?!?! Нет, не ослышался. Ну ладно, Лягушка так Лягушка. По-моему, даже прикольно.
Вот так и пошло зарождение “Лягушки”.
Я добавил обращение по имени и разговор после взгляда в камеру. Имя помогает позвать её, когда человека нет перед объективом. Если сказать «Лягушка, стоп», она прекращает разговор, возвращает голову в центр и ждёт нового обращения. Это были первые правила поведения, после которых программа начала напоминать домашнего робота, а не постоянно открытый микрофон. А взгляд в камеру активировал запись звука и даже не надо было называть по имени, очень удобно оказалось.
Имя я тоже вынес в настройки: если дочери когда-нибудь надоест «Лягушка», можно вписать другое. Сохранил — и робот уже откликается на новое имя, без правок в коде.
Сейчас это ещё стенд и напечатанная пробная голова, а не окончательно собранная фигурка. Но я уже увидел главное: дочь разговаривает с персонажем, тот отвечает своим голосом, переводит взгляд и поворачивается к ней. В следующей части покажу, как я учил Лягушку не просто смотреть и говорить, а помогать с предметом перед камерой, выводить ответ на экран и отправлять результат на печать.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.