ESP32-S3 Bench Guide
ESP32-S3 N16R8 bench guide: live pinout, lessons, simulators and the bench log.
SSML-скрипт → озвучка через Google Cloud Text-to-Speech
Translation pending
EducationTypeScript · JavaScriptGitHub @alexbaumgertnerOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
No updates detected yet. GitHub releases will appear here.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
Loading the full README. The saved excerpt is shown below.
Скрипт панельной дискуссии → один 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) не заработал бы как есть.Read the full 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 минут хватает с запасом.