ESP32-S3 Bench Guide
ESP32‑S3 N16R8 벤치 가이드: 실시간 핀아웃, 강의, 시뮬레이터 및 벤치 로그
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Скрипт панельной дискуссии → один MP3 с озвучкой через Google Cloud Text-to-Speech. Next.js 16 (App Router) + TypeScript + Tailwind. Скрипт пока готовится снаружи (Gemini по промту из PDF/Word) и вставляется в поле. Генерация скрипта внутри приложения и библиотека упражнений — релиз 2. 1. Самые естественные голоса не принимают SSML Chirp 3 HD и Journey звучат заметно живее Neural2, но работают только с plain text: запрос input: { ssml } к ним падает с INVALID_ARGUMENT. Именно поэтому пример из документации Google (en-US-Journey-F + SSML) не заработал бы как есть.README 전문 보기 ↗
Скрипт панельной дискуссии → один MP3 с озвучкой через Google Cloud Text-to-Speech. Next.js 16 (App Router) + TypeScript + Tailwind.
Скрипт пока готовится снаружи (Gemini по промту из PDF/Word) и вставляется в поле. Генерация скрипта внутри приложения и библиотека упражнений — релиз 2.
Chirp 3 HD и Journey звучат заметно живее Neural2, но работают только с plain text:
запрос input: { ssml } к ним падает с INVALID_ARGUMENT. Именно поэтому пример из
документации Google (en-US-Journey-F + SSML) не заработал бы как есть.
Приложение решает это автоматически, без ручной работы: формат выбирается не пользователем, а выбранными голосами.
| Выбраны голоса | Режим | Что происходит с паузами |
|---|---|---|
| только Neural2 / Wavenet / Standard / Studio | ssml | <break> уходит в запрос разметкой |
| хоть один Chirp 3 HD / Journey | text | разметка снимается, <break> становится реальной тишиной при склейке |
Тишина генерируется локально (src/lib/silence.ts, lamejs) в том же формате,
что отдаёт Google — 24 кГц моно, MPEG-2 Layer III, 576 сэмплов на фрейм, —
поэтому куски стыкуются без пересборки файла. Точность проверена: 13 пауз
по 1.5 с дают 19.344 с против расчётных 19.5 с.
Смешивать форматы в одном файле нельзя (разметка применялась бы выборочно), поэтому достаточно одного text-only голоса, чтобы весь скрипт пошёл текстом.
synthesizeSpeech принимает максимум 5000 байт, причём в SSML-режиме теги
считаются — и в лимит, и в счёт. Пример панели это 15 463 байта, в один запрос
он физически не влезает. Поэтому:
скрипт → блоки (<p>, <break>) → план (речь + паузы) → N запросов → склейка MP3
Режут только по границам абзацев, сверхдлинный абзац — по предложениям, так что ни одна фраза не рвётся посередине.
| Семейство | SSML | Цена / 1 млн симв.* |
|---|---|---|
| Standard | полный | ~$4 |
| Neural2 / WaveNet / Polyglot | полный | ~$16 |
| Chirp 3 HD (по умолчанию) | нет, text-режим | ~$30 |
| Journey | нет, text-режим | ~$30 |
| Studio | подмножество тегов | ~$160 |
* ориентировочно, сверяйтесь с прайсом Google.
Пример панели на 17 минут — около $0.45 на Chirp 3 HD и $0.24 на Neural2.
Studio в 5 раз дороже Chirp — легко сжечь $300 за пару сотен прогонов.
UI показывает оценку до нажатия кнопки, TTS_MAX_CHARS ограничивает одну генерацию.
Нужны включённый Cloud Text-to-Speech API и проект с биллингом. Если проект уже настроен, для локальной разработки достаточно войти своим аккаунтом:
gcloud auth application-default login --project=ВАШ_ПРОЕКТ
Ключ ляжет в ~/.config/gcloud/application_default_credentials.json, приложение
подхватит его само — в .env.local при этом ничего дописывать не нужно.
Для деплоя на Vercel файловой системы под ключ нет, поэтому там нужен сервис-аккаунт: выдать ему роль, создать ключ JSON и положить ключ целиком в переменную.
Роль — roles/serviceusage.serviceUsageConsumer. У Cloud Text-to-Speech нет
собственной IAM-роли: разрешений texttospeech.* не существует, а roles/speech.*
относятся к Speech-to-Text и к синтезу отношения не имеют. Нужно ровно право
вызывать включённые в проекте API, то есть serviceusage.services.use.
cp .env.example .env.local
base64 -i key.json | tr -d '\n' | pbcopy # вставить в GOOGLE_SERVICE_ACCOUNT_JSON
tr обязателен на Linux/CI: там base64 переносит строки на 76 символах,
а многострочное значение ломает парсинг .env.
Код принимает и сырой JSON, и base64 — см. src/lib/google-tts.ts.
npm install
npm run dev
Открыть http://localhost:3000 → «Загрузить пример» → «Сгенерировать MP3».
Приложение публичное, а каждая генерация стоит денег, поэтому генерация закрыта
общим паролем: переменная APP_PASSWORD. Без неё генерация просто отключена —
это осознанно, безопаснее отдать 503, чем открытый кошелёк.
Сессия — подписанная HttpOnly-кука на 7 дней. Ключ подписи берётся из
AUTH_SECRET, а если его нет — выводится из пароля, так что в проде достаточно
одной переменной, а смена пароля разлогинивает всех.
Middleware сознательно не используется: в Next 16 он переименован в proxy.js,
а проверки в серверном компоненте страницы и в каждом роуте хватает, и она не
зависит от этого переименования.
Расходы пишутся после каждой успешной генерации, показываются в карточке «Бюджет» и ограничивают генерацию:
| Переменная | Что делает |
|---|---|
TTS_BUDGET_USD | потолок за всё время; превышение — отказ с 402 |
TTS_MONTHLY_LIMIT_USD | потолок на календарный месяц, сбрасывается первого числа |
TTS_MAX_CHARS | потолок на одну генерацию |
Проверка идёт до синтеза и учитывает стоимость самой заявки, иначе лимит можно было бы перешагнуть последним запросом. Если лимит задан, а прочитать накопленную сумму не удалось, генерация останавливается с 503: молча отключившийся предохранитель хуже временной недоступности.
Один и тот же budgetBlock() из lib/budget.ts вызывают и сервер в /api/tts,
и клиент для блокировки кнопки — чтобы две проверки не разъехались.
Зачем это вообще нужно: Google не умеет останавливать траты на Text-to-Speech. В его бюджетах есть Spend cap enforcement, но он поддерживает только Cloud Run, Cloud Run Functions, Gemini API и Vertex AI. Для озвучки остаются письма, причём фактические расходы появляются с задержкой в несколько часов. Так что жёсткий предел есть только здесь.
Важно про точность:
Где хранится счётчик, зависит от окружения:
| Окружение | Хранилище | Переживает перезапуск |
|---|---|---|
| локально | .data/usage.json | да |
| Vercel + Blob-стор | Vercel Blob, приватный | да |
| Vercel без Blob-стора | память процесса | нет, UI об этом предупреждает |
На Blob запись идёт с ifMatch: если между чтением и записью кто-то успел
сохранить свою генерацию, цикл повторяется, иначе параллельные запросы затирали
бы счётчик друг друга. Агрегаты хранятся отдельно от журнала — журнал обрезан
до 200 записей, а итог считается вечно.
npm run smoke
Проверяет разбор скрипта, оба режима нарезки в обеих раскладках голосов, лимит 5000 байт, очистку разметки в text-режиме и валидность склеенной тишины.
src/
lib/
ssml.ts разбор скрипта, план синтеза для обоих режимов, статистика.
Изоморфный: один и тот же код считает превью в браузере
и режет на сервере
silence.ts генератор MP3-тишины для text-режима (lamejs)
auth.ts пароль, подпись сессионной куки, guard для роутов
usage.ts учёт расходов: Vercel Blob / файл / память
voices.ts каталог голосов, тарифы, автораздача, выбор формата
google-tts.ts клиент Google, ретраи, параллельный синтез, склейка, ошибки
api-types.ts контракт запроса
app/
api/tts/route.ts POST → audio/mpeg
api/voices/route.ts GET → голоса с пометкой, кто принимает SSML
api/login|logout вход по паролю, сессионная кука
api/usage/route.ts GET → сводка расходов
page.tsx серверный компонент: пароль или студия
components/studio.tsx весь UI
components/login-form.tsx
scripts/smoke.mjs дымовой тест пайплайна
public/examples/panel.ssml
Moderator: …), каждому
выдаётся свой голос, и нарезка никогда не смешивает двух спикеров в одном запросе.
Имя спикера по умолчанию не произносится — смена голоса и так слышна.
Голоса раздаются с разными акцентами (US / GB / AU / IN): для тренировки синхрониста
это ближе к реальной панели, чем один диктор.unhandledRejection мимо try/catch
(в dev — шум в логах, в проде может уронить функцию).speakingRate / pitch (что вероятно для Chirp 3 HD),
запрос автоматически повторяется без них, вместо того чтобы упасть.UNAVAILABLE, RESOURCE_EXHAUSTED и прочих
временных кодах.npm run smoke — все проверки зелёные:
| Режим | Запросов | Пауз | Макс. кусок | Символов |
|---|---|---|---|---|
| ssml / один голос | 4 | 0 | 4462 B | 15 530 |
| ssml / разные голоса | 14 | 0 | 1442 B | 15 762 |
| text / один голос | 14 | 13 | 1391 B | 14 988 |
| text / разные голоса | 14 | 13 | 1369 B | 14 760 |
Разбор примера: 7 спикеров, 14 абзацев, 19.5 с пауз, ~17:19 звучания. UI, автовыбор формата, оценка стоимости и сообщения об ошибках проверены в браузере.
Реальный вызов Google проверен: генерация проходит, Chirp 3 HD принимает
запросы, склейка тишины звучит как задумано. Живой список голосов из
/api/voices подтвердил имена Chirp 3 HD (Achernar, Aoede, Charon и
далее), так что фолбэк-список в voices.ts совпал с фактическим.
Пароль и учёт расходов проверены отдельно:
/api/tts, /api/usage, /api/voices без куки отдают 401;.data/usage.json вместе с агрегатами.Репозиторий уже инициализирован, коммиты на месте. Остаётся создать пустой репозиторий на GitHub (без README, .gitignore и лицензии — они уже есть) и привязать его:
git remote add origin [email protected]:ВАШ_ЛОГИН/sync-trainer.git
git push -u origin main
Если работа лежит в ветке, сначала перенесите её на main:
git checkout main && git merge --ff-only ВЕТКА && git branch -d ВЕТКА
Секретов в репозитории нет: .env.local и .data/ закрыты .gitignore,
в .env.example только пустые плейсхолдеры.
На Vercel gcloud auth application-default login не работает — там нет ни
gcloud, ни постоянной файловой системы. Нужен ключ сервис-аккаунта, см. раздел
про учётные данные выше: роль roles/serviceusage.serviceUsageConsumer, затем
ключ JSON, затем в одну строку:
base64 -i key.json | tr -d '\n' | pbcopy
New Project → Import Git Repository, выбрать sync-trainer.
Framework Next.js определится сам, build-команды менять не нужно.
До первого деплоя раскрыть Environment Variables и добавить:
| Переменная | Значение | Обязательна |
|---|---|---|
GOOGLE_SERVICE_ACCOUNT_JSON | base64 ключа из шага 2 | да |
APP_PASSWORD | длинный случайный пароль | да |
TTS_BUDGET_USD | 300 | нет |
TTS_MAX_CHARS | 120000 | нет |
Пароль сгенерировать можно так:
openssl rand -base64 24
Deploy.
После деплоя: Storage → Create Database → Blob, привязать к проекту.
Переменная BLOB_READ_WRITE_TOKEN подставится сама. Без неё приложение
работает, но счётчик расходов живёт в памяти и обнуляется при перезапуске —
UI об этом честно предупреждает.
Redeploy, чтобы подхватить токен Blob.
Проверка после деплоя: открыть адрес — должна встретить форма пароля; войти; сгенерировать короткий текст; убедиться, что в карточке «Бюджет» вырос счётчик.
Функции по умолчанию имеют лимит 300 секунд, /api/tts объявляет
maxDuration = 300 — скрипта на 20 минут хватает с запасом.