v0-german: генератор документов для диагностики бизнеса

Когда у тебя есть продукт, который продаётся через живые диагностические сессии с менеджером, и таких сессий становится десятки в месяц — ручная подготовка документов превращается в настоящую боль. После каждой встречи нужно отправить клиенту три вещи: отчёт о его текущей ситуации, коммерческое предложение с тарифами и дорожную карту работы. Раньше всё это делалось вручную, занимало часы, и качество зависело от настроения и загрузки менеджера. Так появился проект v0-german — AI-инструмент, который берёт транскрипцию звонка и за несколько минут генерирует все три документа в брендированном PDF.
По итогу это вырос в полноценный production-инструмент с визардом на 4 шага, интеграцией с Whisper для транскрибации, GPT-4o для генерации текста и несколькими итерациями PDF-рендеринга — от @react-pdf/renderer до собственного HTML-шаблонизатора. Плюс бонусом — CJM-модуль с воронкой, где мы словили неочевидный баг с каскадным удалением данных из PostgreSQL.
Контекст: что такое школа Германа Юна и зачем нужен генератор
Герман Юн — бизнес-консультант и ментор. Его программа начинается с диагностики: потенциальный клиент проходит созвон с менеджером, который выясняет текущее состояние бизнеса — отдел продаж, найм, дебиторку, выручку за последние годы, узкие места. После этой диагностики клиент получает пакет документов и принимает решение о входе в программу.
Входной порог — 50 тысяч рублей за мягкий вход, дальше три тарифа: 250К, 750К и 1 миллион рублей. При таком чеке документы должны выглядеть соответственно: профессионально, персонализированно, в едином бренде.
До автоматизации менеджер тратил 2–3 часа после каждой диагностики на подготовку документов. С v0-german этот процесс занимает 5–10 минут, включая время на транскрибацию аудио.
Архитектура: что внутри
Проект построен на Next.js App Router с TypeScript, Supabase как база данных и хранилище, OpenAI API для генерации текста и транскрибации. Деплой через Dokploy.
Основной флоу — это страница /generator с пошаговым визардом:
Шаг 1 — Данные клиента: имя, описание бизнеса, локация, дата встречи и следующей встречи с Германом.
Шаг 2 — Источник транскрипции: либо загрузка аудиофайла (обрабатывается Whisper через OpenAI API), либо вставка готового текста напрямую. Оба варианта равноценны.
Шаг 3 — Выбор тарифа: менеджер выбирает один из трёх пакетов. Это влияет на содержание КП — цены, состав услуг, условия.
Шаг 4 — Генерация и скачивание: три кнопки, три PDF.
Структура API-роутов:
app/api/generator/
transcribe/route.ts — Whisper транскрибация
diagnostic/route.ts — GPT-4o генерация отчёта
kp/route.ts — генерация КП с тарифом
roadmap/route.ts — генерация дорожной карты
pdf/route.ts — рендеринг HTML → PDF
Три документа: промпты и структура
Отчёт диагностики (Точка А + Ключевые выводы)
Это самый важный документ. GPT-4o получает транскрипцию звонка и промпт, который описывает роль «эксперта по диагностике бизнесов, обученного в школе Германа Юна». Структура жёстко зафиксирована: блок «Точка А» с подразделами (Отдел продаж, Найм, Дебиторка, Финансы и т.д.) и блок «Ключевые выводы» с корневыми причинами и ограничениями роста.
Позже мы добавили инъекцию стиля коммуникации Германа в промпты — чтобы документы звучали в его голосе, а не как стандартный ChatGPT. Это один из коммитов в git-истории: feat: inject German's communication style into KP generation prompts.
Коммерческое предложение
КП генерируется с учётом выбранного тарифа. Документ включает описание программы, состав услуг, цену и условия. Под каждый тариф — свой раздел с акцентами.
Дорожная карта
Персонализированный план работы: по месяцам, с конкретными этапами, привязанными к ситуации клиента из транскрипции.
Итерации PDF-рендеринга: от react-pdf к HTML-шаблонам
Это та история, где мы несколько раз меняли подход — и каждый раз по делу.
Попытка 1: @react-pdf/renderer
Начали с @react-pdf/renderer — популярной библиотеки для генерации PDF прямо из React-компонентов. Плюсы очевидны: настоящие векторные PDF, кастомные шрифты, работает в API routes без браузера. Но есть серьёзный минус — у неё собственный синтаксис и layout-модель, которая не совместима с Tailwind и обычным CSS. Пришлось бы писать весь layout заново в их примитивах.
Первое время это работало, но когда дизайн документов усложнился (появились изображения, сложная типографика, японская эстетика бренда Германа), ограничения стали критичными.
Попытка 2: Puppeteer
Puppeteer даёт pixel-perfect результат из HTML — Tailwind работает, любые CSS-эффекты. Но в production это боль: ~400MB Chromium, медленный холодный старт, проблемы в Docker Alpine окружении. Для нашего случая — избыточно.
Итог: kp-renderer — собственный HTML-шаблонизатор
В итоге мы написали собственный рендерер HTML-шаблонов (kp-renderer), который генерирует HTML с инлайн-стилями и конвертирует в PDF через более лёгкую библиотеку. Это дало нам полный контроль над вёрсткой без оверхеда Puppeteer.
Git-коммит зафиксировал этот переход:
50b7dc7 refactor: switch PDF generation from @react-pdf to kp-renderer HTML templates
Позже добавили поддержку .docx файлов для загрузки (транскрипции иногда приходят в Word), обработку изображений через Sharp с WebP и memory-кэшем, и систему Image Styles — когда для каждого слота документа можно выбрать один из трёх визуальных вариантов.
Брендинг: японская эстетика Германа Юна
Сайт germanyun.online выполнен в необычном стиле: благородный бордовый #9b2c2c, золотой акцент #c9a959, тёплый белый фон, шрифты Inter + Noto Serif JP. Японская минималистичная эстетика.
Все генерируемые документы используют эту же дизайн-систему. Когда мы переписывали PDF-шаблоны, синхронизировали их с V2 дизайн-системой:
fd93a62 feat: redesign PDF documents to match KP slide design system
4279769 fix: update PDF styles to V2 design system
CJM-модуль: воронка с планом и фактом
Параллельно с генератором документов в проекте существует CJM-модуль (Customer Journey Map) — визуальная воронка, где менеджеры отслеживают метрики по каждому этапу: лиды, клики, конверсии, затраты.
Здесь мы столкнулись с одним из самых неприятных багов проекта.
Баг: данные в таблице не сохранялись
Симптом: пользователь вводит число в ячейку таблицы, нажимает Enter, видит галочку успешного сохранения. Обновляет страницу — данных нет. Таблица пустая.
Первая проверка — дёрнуть API напрямую:
curl -X PUT https://germanyun.online/api/cjm/metrics \
-H "Content-Type: application/json" \
-d '{"stage_id": "test", "value": 42}'API отвечает 200, данные в базе появляются. Значит, проблема не в API — проблема в том, что запросы из UI до API не доходят. Или данные удаляются после сохранения.
Root cause: ON DELETE CASCADE
Каждые ~2 секунды при любом изменении в конструкторе воронки срабатывал auto-save. Эндпоинт PUT /api/cjm/funnels/[id]/stages делал буквально следующее:
// stages/route.ts — БЫЛО (проблемный код)
const { error: delError } = await supabaseCjm
.from("stages")
.delete()
.eq("funnel_id", funnelId);
// ... потом INSERT новых stagesА таблица stage_metrics (куда сохранялись числа из таблицы) имела внешний ключ:
FOREIGN KEY (stage_id) REFERENCES stages(id) ON DELETE CASCADEИтоговая цепочка событий:
- Пользователь вводит число → PUT /api/cjm/metrics → ✓ сохранено
- Через 2 секунды auto-save → DELETE FROM stages WHERE funnel_id = X
- CASCADE → DELETE FROM stage_metrics (все метрики)
- INSERT stages обратно — но метрик уже нет
- Пользователь обновляет страницу → пусто
Исправление: UPSERT вместо DELETE+INSERT
// stages/route.ts — СТАЛО
// Получаем текущие stage IDs
const existingIds = existing.map(s => s.id);
const newIds = stages.map(s => s.id);
// Удаляем только реально убранные stages
const toDelete = existingIds.filter(id => !newIds.includes(id));
if (toDelete.length > 0) {
await supabaseCjm.from("stages").delete().in("id", toDelete);
}
// UPSERT остальных
await supabaseCjm.from("stages").upsert(stages);Теперь каскадное удаление срабатывает только когда этап реально удалён из воронки — а не при каждом auto-save.
Plan/Fact: новый слой аналитики
После фикса с CASCADE мы расширили модель данных CJM: добавили разделение на план и факт для каждой метрики. Теперь менеджер может зафиксировать плановые показатели воронки и потом вводить фактические — а таблица автоматически считает дельту.
Миграция базы данных:
-- Добавляем колонку type
ALTER TABLE stage_metrics
ADD COLUMN type TEXT NOT NULL DEFAULT 'fact'
CHECK (type IN ('plan', 'fact'));
-- Обновляем unique constraint
ALTER TABLE stage_metrics
DROP CONSTRAINT stage_metrics_stage_id_date_key;
ALTER TABLE stage_metrics
ADD CONSTRAINT stage_metrics_stage_id_date_type_key
UNIQUE (stage_id, date, type);
-- Все существующие записи = fact
UPDATE stage_metrics SET type = 'fact';Таблица теперь показывает три колонки для каждого этапа: план, факт, дельта (с цветовой индикацией). Это даёт менеджерам нормальный инструмент управления воронкой, а не просто справочник.
Image Styles и умная выборка примеров
Одна из последних фич — визуальные стили для изображений в документах. Вместо одного фиксированного изображения на слот теперь генерируется три варианта, менеджер выбирает понравившийся.
Для генерации используется Wavespeed API с параметром aspect_ratio. Превью загружаются через proxy-эндпоинт с Sharp-ресайзингом и WebP-конвертацией — это существенно ускорило загрузку интерфейса.
Для инъекции стиля Германа в промпты реализована умная выборка примеров с бюджетом 12K токенов и алгоритмом diversity selection — чтобы примеры были разнообразными, а не повторяли один и тот же паттерн:
f94e8ec feat: smart style example sampling — 12K budget with diversity selection
Результаты
- Время подготовки пакета документов: с 2–3 часов до 5–10 минут
- Три PDF-документа генерируются параллельно по одной транскрипции
- Единый брендинг: все документы соответствуют дизайн-системе germanyun.online
- CJM-модуль с корректным сохранением метрик и Plan/Fact аналитикой
- Поддержка аудио и .docx как источников транскрипции
- Три визуальных варианта изображений на выбор
Выводы
Главный технический урок этого проекта — каскадные зависимости в базе данных могут убивать данные совершенно незаметно. ON DELETE CASCADE выглядит как удобная фича, пока ты не сталкиваешься с ситуацией, когда auto-save внешне работающего эндпоинта молча уничтожает данные пользователя. Паттерн DELETE+INSERT для обновления коллекций — это антипаттерн, который нужно заменять на UPSERT с точечным удалением только реально убранных элементов.
По части PDF-рендеринга: нет универсального решения. @react-pdf/renderer хорош для простых документов с контролируемой структурой. Puppeteer — когда нужен pixel-perfect из HTML любой сложности и есть ресурсы. Собственный HTML-шаблонизатор — когда нужен баланс между гибкостью верстки и производительностью. Мы прошли все три итерации, прежде чем нашли подходящий для нашего случая вариант.
Brainstorming перед реализацией реально работает. Мы потратили время на уточнение деталей (сколько тарифов, как выглядят примеры документов, какой флоу у менеджера) и это позволило спроектировать архитектуру за один подход, без больших переделок потом. Визард с 4 шагами и три отдельных PDF — это решение, которое пришло именно из этого разговора, а не из технических ограничений.
По части AI-интеграции: инъекция стиля коммуникации в промпты — недооценённая техника. Когда документ звучит в голосе конкретного эксперта (а не как стандартный GPT-вывод), это кардинально меняет восприятие клиентом. 12K-бюджет с diversity selection для примеров — хороший баланс между качеством воспроизведения стиля и стоимостью API-вызова.
И последнее: Plan/Fact разделение в аналитике — это то, что нужно закладывать в схему данных с самого начала, а не добавлять миграцией. Обновление unique constraint в production без даунтайма потребовало аккуратной последовательности операций, которой можно было избежать при правильном проектировании исходной схемы.
Ссылки на технологии

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