П/ВИН

Генератор коммерческих предложений и аналитика звонков на Next.js

·8 мин чтения

Когда клиент говорит «нам нужен сайт», за этим обычно скрывается что-то гораздо большее. В случае с проектом 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 в две глобальные вкладки.

Вкладка «Генерация КП» включает весь существующий пайплайн:

  1. Ввод данных клиента
  2. Транскрипция или ручной ввод данных
  3. Итоги диагностики
  4. Генерация КП через LLM
  5. Генерация слайдов
  6. Финальная презентация
  7. История сессий
  8. Админка

Вкладка «Транскрибация» — отдельный сервис с 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. Было два «тихих» места, где результат мог потеряться:

  1. Callback приходил, но транскрипция не сохранялась при обновлении (только при вставке)
  2. Если вендор возвращал пустой результат, ошибка не всплывала

Оба места закрыли явными проверками и логированием ошибок вендора.

Интеграция с 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