Мы с дочкой затеяли интерактивную книгу на базе отдельного устройства CrowPanel ESP32-P4. Она придумывает героев и события, я помогаю с реализацией и использую нейросети для текста, картинок и программирования. Можно выбирать, как поступят герои, и проходить историю разными путями. Зачем это вообще нужно я рассказал в первой статье.
Во второй части разбирались с панелью: ESP-IDF, LVGL, дисплей, первые анимации. Теперь добрался до самого содержимого книги.
Покажу наш сценарий, устройство сюжетного движка, подготовку картинок и анимаций. А ещё расскажу, как всё это связано с интерфейсом и где лежат исходники.
От идеи к сценарию
Начиналось всё с вопроса дочки, почему Белоснежка не позвала Эльзу. Правда, Эльзу мы по дороге где-то потеряли - готовая история получилась целиком про Белоснежку. Зато она может пойти разными дорогами по лесу, встретить гномов, найти волшебные вещи и прийти к совершенно разным финалам.
Как выяснилось, придумывать с ребёнком одну историю гораздо проще, чем сразу шестнадцать. Поэтому развилки и связь между ними пришлось придумывать мне.
Использовал ChatGPT, но подозреваю, что везде всё плюс-минус сработает одинаково. Сначала в свободной форме рассказал, чего хотим от книги. Потом задал конкретную структуру: четыре выбора, каждый раз по два варианта. Если считать и промежуточные сцены, получается 31, из них 16 - концовки.

Работал в одном чате, поэтому контекст неплохо сохранялся – в принципе, LLM сама всё неплохо описала - для каждой сцены появилось описание картинки, текст диктора, вопрос (для выбора развития дальнейшего сюжета) и два варианта действий. У финальных сцен вопроса уже нет, понятное дело.
Сгенерить текстовый сценарий оказалось просто – с первой попытки получилось собрать общий документ, дальше уже были только точечные правки. Меняли некоторые ветки сюжета, т.к. LLM начинала придумывать скукоту или такие ванильные сопли, что даже для дочки это было бы слишком.
Вот кусочек сгенерированного сценария.
🌿 A. Экспозиция — первый выбор
🖼 Картинка:
Утро во дворце. На балконе — Белоснежка с голубками. В окне башни — королева с зеркалом, свет отражается на её лице. Вдали — зелёный лес, будто зовущий к себе.
🎙 Текст:
Жила-была юная принцесса по имени Белоснежка.
Она жила со своей мачехой — злой королевой, которая каждый день спрашивала у зеркала:
«Зеркальце, зеркальце, кто на свете всех милее?»
Когда зеркало ответило, что милее Белоснежка, королева пришла в ярость и приказала увести принцессу в лес.
Слуга пожалел её и сказал: «Беги, принцесса, прячься, где тебя никто не найдёт!»
❓ Вопрос:
Куда ей идти дальше?
1️⃣ К домику с дымком из трубы.
2️⃣ К сияющему водопаду, где можно напиться и отдохнуть.
Второй уровень
🏡 B1. Путь к домику
🖼 Картинка:
Тропинка в густом лесу, солнечные лучи пробиваются сквозь листву.
Белоснежка с корзиной идёт по тропе, вдалеке виден уютный домик с дымком.
🎙 Текст:
Белоснежка шла по мягкой тропинке, пока не увидела маленький домик среди цветов.
Из трубы поднимался дым, и дверь была чуть приоткрыта.
Всё выглядело уютно и спокойно, как будто домик ждал её.
❓ Вопрос:
Что сделать?
1️⃣ Зайти внутрь и посмотреть, кто там живёт.
2️⃣ Обойти вокруг, чтобы убедиться, что всё безопасно.
💧 B2. Путь к водопаду
🖼 Картинка:
Широкий водопад, сияющий под солнцем. Белоснежка стоит у воды, рядом олень пьёт из ручья. Вокруг — бабочки и блестящие капли.
🎙 Текст:
Белоснежка подошла к водопаду. Воздух был свежим, а шум воды успокаивал.
Рядом стоял олень — красивый и добрый, он не боялся её.
Белоснежка почувствовала, что в этом месте живёт волшебство.
❓Вопрос:
Что сделать?
1️⃣Попросить оленя показать дорогу к людям.
2️⃣Остаться у водопада и отдохнуть.
В результате получился десятистраничный документ: описания картинок, реплики диктора и варианты продолжения для всех сцен. Сначала всё было на русском, позже тексты подготовили на английском для самой книги.
Картинки: единый стиль и подготовка для панели
В первой статье уже писал, что с иллюстрациями оказалось не всё просто. ChatGPT иногда отказывался рисовать известных сказочных персонажей, а когда картинка всё-таки получалась, следующий кадр мог оказаться совсем другим по стилю или внешности героини.
Особенно раздражало, когда Белоснежка вдруг меняла платье, причёску или вообще переставала быть похожей сама на себя. Поэтому внешний вид персонажей и общую рисовку приходилось описывать довольно подробно.
Часть этой рутины тоже можно переложить на ChatGPT: на основе описания сцены он готовит промпт с нужным персонажем, одеждой и окружением. Потом уже смотришь, что получилось, и поправляешь.
Скрытый текст
Начинаем генерацию картинок для иллюстрации истории о белоснежке. Стиль - диснеевский (но не в точности повторяем, чтобы не было вопросов по правам). будет нужно сгенерить по очереди 32 картинки. Картинки все яркие, с сочными диснеевскими цветами и героями, множеством деталей и хорошей прорисовкой - стиль картинки - анимация в стиле диснеевских рисованных мультиков. Зелень, деревья, сказочные замки, небо и так далее. Все картинки связаны между собой общим стилем, героями. Картинка №1: Утро во дворце. На балконе — Белоснежка с голубками. В окне башни — королева с зеркалом, свет отражается на её лице. Текст (закадровый) для картинки (на самой картинке не показываем): Once upon a time, there lived a young princess named Snow White. She lived with her stepmother, the evil Queen, who every day asked her mirror: "Mirror, mirror on the wall, who is the fairest one of all?" When the mirror replied that Snow White was the fairest, the Queen flew into a rage and ordered a servant to take the princess into the forest. The servant took pity on her and said, "Run, princess, hide where no one will find you!" Сделай промпт для первой картинки
Результат:
Disney-style animated illustration, bright and richly detailed. Morning at a fairy-tale castle surrounded by lush greenery and soft golden sunlight. On a high balcony stands Snow White, a young princess with pale skin, dark hair, red lips, and a blue-yellow-red dress, smiling gently as two white doves sit on her hands. In the background, inside a tall tower window, the Queen is seen holding a magic mirror, with the morning light reflecting off it onto her cold, beautiful face. The scene feels full of life — trees, flowers, birds in the sky, and the soft glow of dawn.
Style: hand-painted animation inspired by classic Disney films, rich colors, cinematic composition, soft shading, detailed backgrounds, expressive faces, fairy-tale atmosphere, harmony between characters and environment.
И результат генерации картинки по этому промпту:
Извини, но я не смог сгенерировать изображение, так как этот запрос нарушает наши правила контента.
Давай попробуем немного изменить описание — можешь, например, попросить сцену «утро в сказочном замке» с «девушкой в платье и голубками», без упоминания известных персонажей. Хочешь, я помогу переформулировать промпт в безопасном виде, чтобы сохранить атмосферу истории?
А вот пример генерации с переодеванием Белоснежки. Сначала по запросу сгенерировалась вероятно золушка, а когда дополнил промпт «девушка должна быть в платье принцессы», то нейронка вернула образ исходного персонажа (правда дорисовала голубей из первой картинки).
Далее – персонаж сохранялся автоматически, модель запомнила, что это важно и больше напоминаний не требовалось.
Промпт
A richly detailed, hand-painted animated illustration in a classic fairy-tale style, widescreen landscape composition. Late afternoon light filters through a lush green forest filled with flowers, moss, and glowing dust in the air. In the center, a young woman in a simple dress stands on a forest path, looking uncertain but hopeful. Two paths open before her: — On the left, a cozy cottage with smoke rising from the chimney, surrounded by flowers and trees. — On the right, a sparkling waterfall glows in the distance, with a soft rainbow forming in the mist. The atmosphere is warm and magical, full of sunlight, gentle shadows, and a sense of adventure and choice. Style: traditional animation aesthetic, rich colors, soft natural light, cinematic depth, expressive faces, painterly textures, vivid storytelling composition.
Результат

Статические иллюстрации и небольшие ресурсы у нас лежат в SPIFFS. Образ файловой системы собирается вместе с прошивкой. Крупные анимации храним отдельно, на microSD.
GIF для птицы, CFA для анимации книги
У книжки есть птица-расказчик (панель называется CrowPanel, Crow – это ворон, так что сразу определились, что это будет ворон-рассказчик). Он открывает клюв во время читки текста, иногда машет крылом и т.п.

В самом приложении анимирована GIF-файлами. Для неё есть разные состояния: обычное, начало разговора и речь.
Анимации открытия, перелистывания и закрытия книги подготовлены отдельно. Исходники в MOV конвертируются скриптом python/compile_mov_to_cfa.py в формат CFA, который используется в нашем приложении.
CFA хранит данные с учётом изменяющихся областей кадров и использует сжатие. Не нужно каждый раз читать с карты набор полных несжатых изображений.
В зависимости от размера CFA-файл загружается в PSRAM целиком или читается с microSD частями. Как именно это устроено, можно посмотреть в исходниках.
Для книги у нас три основных файла:
book_open.cfa - открытие;
book_swipe.cfa - перелистывание;
book_close.cfa - закрытие.
Рядом с ними на карте лежат GIF-файлы птицы.
Есть и кэш изображений сцен - на четыре записи. Упреждающую загрузку следующих сцен пока отключили, поэтому сразу несколько уровней сюжетного дерева в память не тащим.
Как устроен сюжетный движок
В прошивке история не зашита в обработчики кнопок. У каждой сцены есть идентификатор, данные и ссылки на продолжение. Поэтому сам экран не обязан знать, что именно происходит у Белоснежки.
Переходы описаны в story_fairy_tale.c. Вот первая развилка из исходников:
[A] = { .question = "Where should she go next ?", .choice1 = "To a cottage with smoke from the chimney", .choice2 = "To a shining waterfall where she can drink and rest", .is_final = false, .next1 = B1, .next2 = B2, },
A - текущая сцена, B1 и B2 - следующие. is_final показывает, что это концовка или обычная сцена.
Причём всё не хранится в одном файле. Переходы и подписи выбора находятся в story_fairy_tale.c, данные об изображениях - в story_assets_fairy_tale.c, а тексты повествования вынесены отдельно.
Кто за что отвечает
В приложении для этого есть три модуля:
story_engine - хранит текущую сцену и переходы между сценами.
story_coordinator - обрабатывает выбор, переключает сцены и запускает нужные действия при переходе.
story_presenter - готовит текст и изображения для показа на экране.
С такой схемой можно менять историю, не переписывая кнопки и расположение элементов на экране. Нажали одну из двух кнопок - интерфейс передал выбор координатору, а дальше уже работает сюжетная логика.
Есть нюанс, который легко проверить на ребёнке: пока загружается следующая сцена, он успевает нажать кнопку ещё пару раз. Если обработать все эти нажатия, можно случайно перескочить через несколько страниц.
Поэтому после первого выбора повторные нажатия блокируются. Обработчик проверяет, что сейчас вообще можно делать выбор, и только затем передаёт его координатору.
С картинками похожая история. Они загружаются в фоне и могут приходить не в том порядке, в каком пользователь успел переключить страницы.
В коде есть номера поколений запросов. Если старая загрузка закончилась уже после перехода к следующей сцене, её результат отбрасывается. Иначе однажды можно увидеть на новой странице иллюстрацию от предыдущей.
Результаты фоновых задач передаются в LVGL через очередь ui_dispatch. Остальные обращения к LVGL синхронизируются блокировкой.
Интерфейс: SquareLine Studio и LVGL
Использовал SquareLine Studio - визуальный редактор интерфейсов на LVGL.
В нём расставлял изображения, кнопки и надписи, подбирал шрифты и размеры. Потом экспортировал интерфейс в C-код и подключали к прошивке.
Рабочее разрешение у панели - 1024x600, глубина цвета LVGL - 16 бит, без перестановки байтов. Версия LVGL в проекте - 8.4.0. Тут главное, чтобы параметры редактора совпадали с настройками прошивки.
Файлы интерфейса подключены как компонент components/ui через CMakeLists.txt. После запуска дисплея программа вызывает ui_init().
Всё поведение, конечно, пришлось делать кодом: подгрузку сцен, смену картинок, запуск анимаций, работу настроек и звука. SquareLine здесь отвечает за внешний вид, а не за сюжет.
И небольшой практический момент: лучше не писать собственную логику прямо в файлах, которые экспортирует SquareLine. После очередного экспорта эти правки можно потерять. Я бы советовал держать своё отдельно и смотреть изменения в Git.
Что со звуком
Кроме картинок в книге есть английская озвучка, музыка и различные звуковые эффекты. Тут особо не усложнял. Шум леса, пение птиц, различные звуковые вставки брал на Pixabay. Там большая библиотека бесплатных звуков, для нашего проекта вполне хватило.
А вот для реплик диктора и персонажей использовал Fish Audio. Это TTS-сервис с большим выбором голосов. Можно подобрать разные голоса для героев и озвучить подготовленные тексты на английском. Сервис активно предлагает платные тарифы, но на практике бесплатных лимитов оказалось вполне достаточно для наших экспериментов.
В итоге для каждой сцены подготовили нужные аудиофайлы и связали их с текстами и иллюстрациями. При переходе к следующей сцене меняется и звуковое сопровождение. Управление воспроизведением и паузой при открытии настроек я уже немного описывал во второй статье.
Исходники и как запустить у себя
Готовая прошивка лежит в основной части репозитория. Исходники приложения, материалы и скрипты для сборки - в ветке source-code репозитория https://github.com/AI-devices/FairyTales.
Проект собирается под ESP-IDF 5.5.2. Настройки - в sdkconfig.defaults, версии зависимостей - в dependencies.lock. Большие файлы, в том числе исходные MOV, шрифты и CFA, хранятся через Git LFS. Поэтому обычного git clone для всех ресурсов недостаточно - нужно ещё скачать содержимое LFS.
После клонирования AI-devices/FairyTales и перехода в его каталог команды такие:
git lfs install git switch source-code git lfs pull idf.py build idf.py -p <PORT> flash monitor
Ресурсы с assets_sdcard/ нужно положить в каталог assets/ на microSD, отформатированной в FAT32.
В общем, можно скачать прошивку и запустить книгу или открыть исходники и посмотреть, как она собрана.
Сделать собственную историю пока не получится заменой одного текстового файла. Придётся подготовить сцены, картинки, озвучку, анимации и связать всё с нужными идентификаторами. Но рабочий пример теперь есть, так что надеюсь, что кого-то подтолкну к созданию своего варианта.
Будут вопросы - пишите, по мере сил подскажу. А если повторите или переделаете под свою историю, поделитесь результатом, очень интересно увидеть подобные проекты.
