Генератор коммерческих предложений и аналитика звонков на Next.js
Когда клиент говорит «нам нужен сайт», за этим обычно скрывается что-то гораздо большее. В случае с проектом v0-german за этой фразой оказалась целая операционная экосистема: публичный сайт, генератор коммерческих предложений, аналитика звонков из телефонии, транскрибация встреч, дашборды продаж и работа с возражениями клиентов. Расскажу, что мы построили, какие грабли собрали и что в итоге работает в продакшне.
Контекст проекта
Проект — это Next.js 16-приложение на React 19 и TypeScript для экосистемы Германа Юна, эксперта по обучению продажам. На старте задача звучала как «публичный сайт с лендингами», но быстро выросла до полноценного внутреннего инструмента для отдела продаж.
Стек выглядит так:
- Frontend: Next.js 16, React 19, TypeScript, Tailwind CSS
- Backend: API Routes внутри Next.js, Supabase как основная БД
- Интеграции: AmoCRM, Google Sheets, Brevo, Yandex Metrika, UIS телефония, Fireflies, Deepgram
- AI: LLM Router для генерации текстов и анализа
- Деплой: Dokploy + Caddy на Hetzner
Архитектурно это монорепо, где публичный сайт и внутренние инструменты живут в одном приложении, разделённые по роутам и правами доступа.
Генератор КП: от простой формы к полноценному пайплайну
Первая крупная фича — генератор коммерческих предложений. Изначально это был набор разрозненных экранов, но в какой-то момент стало понятно, что нужна единая навигация.
Решение: реорганизовать GeneratorTabs и оболочку app/generator/page.tsx в две глобальные вкладки.
Вкладка «Генерация КП» включает весь существующий пайплайн:
- Ввод данных клиента
- Транскрипция или ручной ввод данных
- Итоги диагностики
- Генерация КП через LLM
- Генерация слайдов
- Финальная презентация
- История сессий
- Админка
Вкладка «Транскрибация» — отдельный сервис с wizard-интерфейсом:
- Шаг 1: выбор источника (Яндекс Диск, Google Drive, YouTube, RuTube, VK Видео, загрузка файла, готовая транскрибация или текст вручную)
- Шаг 2: AI анализирует транскрибацию и предлагает варианты отчётов
- Шаг 3: запуск одного или нескольких анализов параллельно
Ключевое архитектурное решение по транскрибации: одну транскрибацию можно прогнать через несколько разных отчётов без повторной загрузки. В истории это одна исходная запись с набором связанных отчётов. Каждый отчёт имеет свой статус: ожидает → анализируется → готов / ошибка.
Визуальная часть отчётов строится на компонентах из components/dashboard/charts — те же карточки KPI, графики факт/план, сравнение периодов, что и в основном дашборде продаж.
Речевая аналитика звонков из UIS
Это, пожалуй, самая технически насыщенная часть проекта. Задача: тянуть звонки из телефонии UIS, расшифровывать их и строить аналитику по менеджерам прямо в дашборде продаж.
Архитектура пайплайна:
UIS Data API → syncCalls → dashboard.uis_calls (Supabase)
↓
analyzePending
↓
media.uiscom.ru → ffmpeg → Deepgram
↓
транскрипция + speaker diarization
↓
аналитика по менеджерам
Работа велась в изолированном воркtree .worktrees/uis-speech-analytics на ветке feat/uis-speech-analytics — это позволило не трогать основную кодовую базу во время разработки.
Выбор STT-провайдера
Первоначально использовали Whisper, но провели полноценный bake-off между несколькими провайдерами. Победил Deepgram с моделью nova-3 — лучшее соотношение скорости, качества и поддержки русского языка.
Одна из нетривиальных проблем: при транскрибации стереозаписей звонков Whisper давал соотношение речи собеседников всегда 50/50, потому что каналы зеркалировались. Фикс — явно разводить каналы перед отправкой:
// До фикса: каналы зеркалировались, talk ratio всегда 50/50
// После: явное разделение каналов
const managerChannel = audioBuffer.getChannelData(0);
const clientChannel = audioBuffer.getChannelData(1);Вторая проблема: Deepgram работает асинхронно через callback. Было два «тихих» места, где результат мог потеряться:
- Callback приходил, но транскрипция не сохранялась при обновлении (только при вставке)
- Если вендор возвращал пустой результат, ошибка не всплывала
Оба места закрыли явными проверками и логированием ошибок вендора.
Интеграция с Fireflies
Параллельно подключили Fireflies для автоматического захвата встреч через бота. Здесь столкнулись с ограничениями free tier: бесплатный план не позволяет polling API, только webhook-нотификации. Задокументировали это явно, чтобы не наступать на те же грабли.
Фильтрация по менеджерам
По бизнес-требованию: анализировать всех сотрудников, кроме менеджеров подразделения ВИН. Реализовали через фильтр на уровне синхронизации — при получении списка сотрудников из UIS исключаем нужных по тегу/группе.
Вкладка «Сомнения ЦА»: баг с кнопкой и устаревший корпус
Одна из самых показательных историй проекта — починка вкладки с анализом сомнений целевой аудитории.
Баг с кнопкой «Перегенерировать»
Симптом: нажимаешь кнопку — ничего не происходит. Диагностика показала интересную картину:
Браузер: POST /api/dashboard/insights/regenerate → TypeError: Failed to fetch (46 147 мс)
Сервер: POST /api/dashboard/insights/regenerate 200 in 46s
БД: новая строка в kp.audience_insights ЕСТЬ
UI: «обновлено 03.08.2026» — как было
Сервер отработал и сохранил инсайт. Клиент этого не увидел. Причина структурная: роут делал синтез LLM внутри одного HTTP-запроса — 46–80 секунд. Браузер рвал соединение по таймауту раньше, чем сервер успевал ответить.
Решение: перевести на асинхронную очередь. Клиент отправляет запрос, получает 202 Accepted, периодически поллит статус. Когда задача готова — обновляет UI.
Устаревший корпус цитат
После починки кнопки выяснилось, что сама перегенерация работает, но синтезирует по замороженному корпусу:
kp.doubts: 131 цитата / 10 клиентов
Последнее извлечение: 2026-05-12
Последняя встреча: 2026-04-29
Во всех 60+ строках истории одинаковые 10 диагностик · 131 цитат — это не совпадение, а симптом того, что экстрактор цитат не запускался с мая.
Запустили экстракцию по всем 23 сессиям. Результат:
Цитат: 131 → 341
Клиентов: 10 → 18
Очередь: 20 done / 0 pending
Инсайт: «База 18 диагностик · 341 цитат · обновлено 05.08.2026, 10:43:46»
Частоты: было 9/10 → стало 13/18, 15/18, 10/18
Разбор восьми диагностик занял от 50 до 110 секунд на каждую (581k символов суммарно). Три сессии не попали в очередь — пустые транскрипции (тестовые записи). Одна сессия дала 0 цитат — это тоже нормально, не каждый разговор содержит возражения.
Деплой и DNS: как не сломать GetCourse
Отдельная история — перевод домена germanyun.ru на новый сервер. Сложность: на домене уже работает GetCourse на поддомене lk.germanyun.ru, и его нельзя трогать.
Схема DNS после миграции:
@ A [IP нового сервера] → новый сайт germanyun.ru
www A [IP нового сервера] → новый сайт
site A [IP нового сервера] → алиас нового сайта
lk A [старый IP] → GetCourse (не трогаем)
Важный момент: при выборе IP для нового сервера нужно учитывать доступность из РФ. Hetzner-серверы с определёнными диапазонами адресов могут быть заблокированы российскими провайдерами — это реальный риск, который нужно проверять перед переключением DNS.
Стaging-версия сайта работала на germanyun.pashavin.ru ещё до переключения production-домена — это правильный подход, позволяющий проверить всё до того, как реальные пользователи увидят изменения.
Технические решения, которые стоит запомнить
Изолированные воркtree для крупных фич
Для разработки речевой аналитики использовали git worktree — отдельная рабочая директория на отдельной ветке, без переключения основного репозитория. Это особенно удобно, когда фича затрагивает много файлов и разрабатывается параллельно с другими задачами.
git worktree add .worktrees/uis-speech-analytics feat/uis-speech-analyticsАсинхронные LLM-операции
Любая операция с LLM, которая может занять больше 10-15 секунд, должна быть асинхронной. Синхронный подход работает на демо, но ломается в продакшне из-за таймаутов браузера, прокси и CDN. Паттерн: POST → 202 Accepted → polling → результат.
Документирование ограничений вендоров
Мы явно документировали ограничения каждого STT-провайдера в репозитории (см. коммиты с префиксом docs:). Это экономит время при онбординге и при возврате к задаче через несколько месяцев. Например: «Fireflies free tier не поддерживает polling, только webhook» — звучит очевидно, но без документации это открытие приходится делать заново каждый раз.
Мониторинг дрейфа env-переменных
Одна из задокументированных проблем — дрейф переменных окружения между Dokploy и swarm-сервисом. Когда одно и то же приложение деплоится в нескольких местах, env-переменные могут расходиться. Решение: явная проверка при деплое и документирование того, где какие переменные живут.
Результаты и метрики
Что работает в продакшне на момент написания статьи:
- Публичный сайт germanyun.ru — лендинги, программы, кейсы, диагностические формы
- Генератор КП — полный пайплайн от диагностики до презентации
- Речевая аналитика — звонки из UIS транскрибируются и анализируются по менеджерам
- Транскрибация встреч — через Fireflies бот + Deepgram nova-3
- Анализ сомнений ЦА — корпус 341 цитата от 18 клиентов, автоматическое обновление
- Дашборды продаж — графики KPI, факт/план, сравнение периодов
По транскрибации: переход с Whisper на Deepgram nova-3 дал заметное улучшение качества на русском языке и корректное разделение собеседников по каналам.
По корпусу сомнений: рост с 131 до 341 цитаты (в 2.6 раза) и с 10 до 18 клиентов дал более репрезентативную выборку — частоты сомнений теперь считаются из 18, а не из 10, что меняет приоритеты в работе с возражениями.
Выводы
Главный урок этого проекта: внутренние инструменты для продаж растут быстрее, чем публичный сайт. Мы начали с лендинга, а пришли к операционной системе отдела продаж. Это не плохо — просто нужно закладывать архитектуру с запасом с самого начала. Монорепо с чёткими границами между публичной и внутренней частью оказалось правильным решением.
Второй урок: любая операция с AI/LLM должна проектироваться как асинхронная. Синхронный подход — это технический долг, который обязательно выстрелит в продакшне. Паттерн с очередью и polling сложнее в реализации, но это единственный надёжный способ работать с операциями, которые занимают десятки секунд.
Третий урок: документируйте ограничения вендоров прямо в репозитории. Мы потратили время на изучение того, что Fireflies free tier не поддерживает polling. Это знание живёт в коммитах с префиксом docs: и больше не нужно открывать заново. То же касается дрейфа env-переменных, особенностей STT-провайдеров и поведения телефонии UIS.
Четвёртый урок: изолируйте крупные фичи через git worktree. Речевая аналитика разрабатывалась параллельно с другими задачами без конфликтов именно потому, что жила в отдельном воркtree. Это особенно важно в монорепо, где одно неосторожное изменение может сломать несвязанную функциональность.
Проект продолжает развиваться: впереди полноценная вкладка транскрибации с поддержкой нескольких источников, расширение аналитики по менеджерам и, возможно, интеграция с дополнительными CRM-системами.
Полезные ссылки

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.
Связанный проект
CJM Designer →