Инженерный хаб

Кофе за 40 секунд или бесплатно: Оптимизация пиковых очередей в БЦ Астаны через Firebase Realtime Database и Gemini Live Audio

24.08.2026
Шарафутдинов Р.

Утро понедельника, 08:45, центральный атриум бизнес-центра класса «А» на левом берегу Астаны. На улице минус двадцать восемь градусов и штормовой столичный ветер, а внутри — непрерывный поток из сотен сотрудников нацкомпаний, финтех-разработчиков и корпоративных юристов. Входная зона заполнена звуками шагов, шелестом зимней одежды, эхом высоченного стеклянного купола (акустический шум 76–82 dB SPL) и гулом кофемолок. Перед единственной островной стойкой спешелти-кофейни формата Grab-and-Go выстраивается «хвост» из 25–30 человек. Скоростные лифты поднимают людей партиями каждые две минуты, утренние стендапы стартуют ровно в 09:00, а живая очередь движется со скоростью черепахи.

Если провести детальный покадровый хронометраж работы бариста, вскрывается классический парадокс пропускной способности (Throughput bottleneck): из 135 секунд общего цикла обслуживания одного клиента физическое заваривание напитка (помол порции, темперовка, экстракция эспрессо в холдере и взбивание холодного молока паровой трубкой) занимает всего 26–30 секунд!

Куда уходят остальные 100+ секунд? Декомпозиция потерь вскрывает фундаментальное интерфейсное трение:

  • Акустический барьер и вербальная верификация: 35–45 сек. Бариста сквозь шум холла пытается разобрать невнятный заказ и вынужден задавать до 4 уточняющих вопросов: «Вам на обычном, овсяном или безлактозном? Сироп добавлять? Сахар? С собой или здесь?».
  • Навигация в кассовом интерфейсе (POS): 25–30 сек. Поиск вложенных модификаторов в сенсорном меню iiko/Poster влажными пальцами в резиновых перчатках.
  • Транзакционный цикл оплаты: 15–20 сек. Переключение экрана POS в режим оплаты, печать пречека, ожидание, пока клиент найдет смартфон, откроет приложение Kaspi и отсканирует статический QR-код на стойке.

Результат: до 38% потенциальных покупателей, оценив длину очереди в 15+ человек, разворачиваются и уходят наверх без напитка. Прямые недополученные убытки точки составляют до 160 000 – 190 000 ₸ за один утренний слот.

Когда руководство сети обратилось в инженерную лабораторию ОЗАТ, мы сразу отвергли классические напольные сенсорные киоски самообслуживания. Наш предыдущий опыт показал, что в спешке люди тратят у сенсорных экранов до 80 секунд, листая многоуровневые каталоги. Решением стала разработка AI Voice Speed Bar: аппаратного комплекса с направленным микрофонным массивом (Beamforming), ультранизколатентным голосовым шлюзом на базе Gemini 2.5 Flash Multimodal Live API (Bidirectional WebSocket) в Google Cloud Run, распределенной шины тикетов на Firebase Realtime Database и аппаратной генерации динамического Kaspi QR.

1. Аппаратно-программный контур и подавление шума (Acoustic Pipeline)

Главным вызовом при развертывании речевого ИИ в публичных пространствах является акустическая деградация сигнала (Cocktail Party Problem). При фоновом шуме 80 dB стандартный ненаправленный микрофон захватывает голоса соседних людей и грохот кофемолки, что приводит к ложным срабатываниям и галлюцинациям в транскрипции.

🎙️ Аппаратный тракт захвата звука:

  • Микрофонный массив ReSpeaker 4-Mic Array: 4 цифровых MEMS-микрофона с аппаратным формированием луча (Beamforming) с апертурой 45° строго в зону лица стоящего перед стойкой клиента.
  • Client-side WebRTC DSP & VAD: Аппаратное эхоподавление (AEC), динамический Noise Gate (-42 dBFS) и Voice Activity Detection (Silero VAD) на AudioWorklet в браузере терминала.
  • Двунаправленный WebSocket-шлюз (Cloud Run Gateway): Терминал подключается по WSS к защищенному бэкенду на Cloud Run. Бэкенд управляет TLS-аутентификацией, хранит ключ сервисного аккаунта GCP, производит авторизацию через gRPC / Bidirectional WebSocket к Vertex AI / Gemini Live API (эндпоинт gemini-2.5-flash) и транслирует 16-битные PCM чанки (16 кГц, моно) с шагом 100 мс.

Если вам интересно, как проектируются отказоустойчивые Cloud-Native микросервисы реального времени, ознакомьтесь с нашими решениями по разработке умных сервисов и интеграции ИИ, а также миграции серверной инфраструктуры в Google Cloud.

2. Обработка смешанной речи и механизм прерываний (Barge-in Handling)

Живая речь жителей Астаны — это динамичный трилингвальный код-свитчинг (русский, казахский, шала-казахский). Человек может сказать: «Салем! Маған бір капучино на овсяном, үлкен болсын, без сахара, и шоколадный макарон. Қанша болады?».

Традиционные каскадные цепочки Whisper STT -> LLM -> TTS дают катастрофическую задержку от 3.5 до 6.2 секунд и спотыкаются на казахских морфемах. Gemini 2.5 Flash Multimodal Live API обрабатывает непрерывный аудиопоток сквозным (end-to-end) образом, генерируя вызовы инструментов (Tool Calling) на лету без промежуточной текстовой сериализации.

Архитектурный паттерн Barge-in (перебивание ассистента): если бот начинает подтверждать заказ, а клиент резко добавляет «Ой, нет, сироп не надо!», клиентский VAD фиксирует входящий звук, прерывает воспроизведение аудиопотока и отправляет на сервер сигнал clientContent.interrupted = true, сбрасывая контекст предыдущей генерации.

⚡ Архитектурные особенности: Cloud Run WebSockets & Payment State Machine

  • WebSocket Timeouts: Cloud Run поддерживает постоянные соединения до 60 минут. Шлюз на Node.js обрабатывает ping/pong каждые 25 секунд и реализует восстановление контекста сессии при кратковременном разрыве 4G/Wi-Fi клиента.
  • Авторитарный расчет цен: LLM строго ограничена классификацией заказа (сироп, температура, объем) и формирует JSON-спецификацию. Итоговая стоимость рассчитывается доверенным сервером на основе прайс-листа из Firestore, исключая prompt injection уязвимости по изменению цены.
  • State Machine транзакций: Статус оплаты строго типизирован: CREATED → PENDING_KASPI → AUTHORIZED → SETTLED → FULFILLED (или EXPIRED/FAILED) с дедупликацией по order_uuid.
  • Эмпирический профиль задержки: $p50 = 380 ext{ ms}$, $p95 = 640 ext{ ms}$, $p99 = 1120 ext{ ms}$ (с учетом TTS синтеза речи). Ежемесячные затраты Cloud Run + Firestore + Gemini Flash: $16.40/мес при 3 200 заказах.

3. Исходный код: AudioWorklet и хук управления сессией Gemini Live с серверной валидацией цен

Важнейший принцип безопасности: клиентский терминал и LLM никогда не рассчитывают итоговую сумму заказа. Модель лишь структурирует намерения клиента в нормализованный SKU-массив, после чего бэкенд шлюза валидирует остатки в ERP, рассчитывает точные цены по прейскуранту и генерирует криптографически стойкий orderId:

import { useState, useRef, useCallback } from 'react';

const GATEWAY_WS_URL = 'wss://gemini-live-gateway.ozat.kz/v1/kiosk-session';

export type PaymentStatus = 'pending' | 'qr_emitted' | 'authorized' | 'captured' | 'failed' | 'refunded';

export interface ValidatedOrderItem {
  sku: string;
  name: string;
  size: 'regular' | 'large';
  milk: 'standard' | 'oat' | 'coconut' | 'lactose_free';
  syrup?: string;
  temperature: 'hot' | 'iced';
  unitPriceKzt: number;
}

export interface KioskOrder {
  orderId: string;
  items: ValidatedOrderItem[];
  totalKzt: number;
  paymentPayloadKaspi: string;
  paymentState: PaymentStatus;
  status: 'draft' | 'validated' | 'paid' | 'pushed_to_kds';
}

export function useGeminiLiveKiosk(onOrderReady: (order: KioskOrder) => Promise<void>) {
  const [sessionActive, setSessionActive] = useState(false);
  const [currentOrder, setCurrentOrder] = useState<KioskOrder | null>(null);

  const wsRef = useRef<WebSocket | null>(null);
  const audioCtxRef = useRef<AudioContext | null>(null);
  const workletNodeRef = useRef<AudioWorkletNode | null>(null);
  const mediaStreamRef = useRef<MediaStream | null>(null);

  const startSession = useCallback(async () => {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        audio: {
          channelCount: 1,
          sampleRate: 16000,
          echoCancellation: true,
          noiseSuppression: true,
          autoGainControl: true
        }
      });
      mediaStreamRef.current = stream;

      const ws = new WebSocket(GATEWAY_WS_URL);
      wsRef.current = ws;

      ws.onopen = () => {
        setSessionActive(true);
        // Конфигурация сессии через надежный Cloud Run Gateway
        ws.send(JSON.stringify({
          type: 'session.setup',
          model: 'models/gemini-2.5-flash',
          generationConfig: {
            temperature: 0.1,
            responseModalities: ['AUDIO', 'TEXT']
          },
          systemInstruction: {
            parts: [{
              text: "Сен ОЗАТ Coffee Bar AI-көмекшісісің. Тез әрі нақты жұмыс істе. Қазақша, орысша немесе аралас сөйлегенді табиғи түсін. Клиент кофе түрі мен сүтін айтқан бойда request_order_validation құралын шақыр. Бағаны ешқашан өзің ойдан шығарма."
            }]
          },
          tools: [{
            functionDeclarations: [{
              name: 'request_order_validation',
              description: 'Отправляет нормализованный состав заказа на доверенный бэкенд для расчета цен и проверки остатков',
              parameters: {
                type: 'OBJECT',
                properties: {
                  requestedItems: {
                    type: 'ARRAY',
                    items: {
                      type: 'OBJECT',
                      properties: {
                        sku: { type: 'STRING' },
                        name: { type: 'STRING' },
                        size: { type: 'STRING', enum: ['regular', 'large'] },
                        milk: { type: 'STRING', enum: ['standard', 'oat', 'coconut', 'lactose_free'] },
                        syrup: { type: 'STRING' },
                        temperature: { type: 'STRING', enum: ['hot', 'iced'] }
                      },
                      required: ['sku', 'name', 'size', 'milk']
                    }
                  }
                },
                required: ['requestedItems']
              }
            }]
          }]
        }));
      };

      ws.onmessage = async (evt) => {
        const msg = JSON.parse(evt.data);

        // Обработка Function Call: валидация заказа и расчет чека выполняется в доверенном микросервисе
        if (msg.toolCall?.functionCalls) {
          for (const call of msg.toolCall.functionCalls) {
            if (call.name === 'request_order_validation') {
              // Запрос к защищенному API калькуляции и генерации Kaspi Pay QR
              const response = await fetch('/api/v1/orders/validate-and-price', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ items: call.args.requestedItems })
              });
              const validatedPayload: KioskOrder = await response.json();

              setCurrentOrder(validatedPayload);
              await onOrderReady(validatedPayload);

              // Возврат статуса в Live-сессию для голосового подтверждения клиенту
              ws.send(JSON.stringify({
                type: 'tool_response',
                toolResponses: [{
                  response: {
                    output: {
                      success: true,
                      orderId: validatedPayload.orderId,
                      totalKzt: validatedPayload.totalKzt,
                      currency: 'KZT'
                    }
                  },
                  id: call.id
                }]
              }));
            }
          }
        }
      };

      const audioCtx = new AudioContext({ sampleRate: 16000 });
      audioCtxRef.current = audioCtx;
      const source = audioCtx.createMediaStreamSource(stream);

      await audioCtx.audioWorklet.addModule('/audio-stream-processor.js');
      const workletNode = new AudioWorkletNode(audioCtx, 'audio-stream-processor');
      workletNodeRef.current = workletNode;

      workletNode.port.onmessage = (e) => {
        if (ws.readyState === WebSocket.OPEN && e.data.pcm16Chunk) {
          ws.send(e.data.pcm16Chunk);
        }
      };

      source.connect(workletNode);
    } catch (err) {
      console.error('[Gemini Live Gateway] Init error:', err);
      setSessionActive(false);
    }
  }, [onOrderReady]);

  const stopSession = useCallback(() => {
    setSessionActive(false);
    mediaStreamRef.current?.getTracks().forEach(t => t.stop());
    workletNodeRef.current?.disconnect();
    audioCtxRef.current?.close();
    wsRef.current?.close();
  }, []);

  return { sessionActive, startSession, stopSession, currentOrder };
}

4. Шина кухонных тикетов: Транзакционный KDS-менеджер на Firebase Realtime Database

Для экрана бариста критична абсолютная предсказуемость синхронизации без блокировок. Клиенты Firebase Realtime Database поддерживают постоянное duplex-соединение (WebSocket с автоматическим fallback на HTTP long-polling при строгих корпоративных прокси), локальный диск-кеш и транзакционные операции. В отличие от асинхронных потоков, разовое чтение состояния при архивации выполняется через детерминированный вызов get():

import { initializeApp, getApps } from 'firebase/app';
import { getDatabase, ref, set, get, onValue, push, runTransaction, update } from 'firebase/database';

const firebaseConfig = {
  apiKey: "AIzaSy_OZAT_COFFEE_PROD_KEY",
  authDomain: "ozat-astana-coffee.firebaseapp.com",
  databaseURL: "https://ozat-astana-coffee-default-rtdb.asia-southeast1.firebasedatabase.app",
  projectId: "ozat-astana-coffee"
};

const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
const rtdb = getDatabase(app);

export type PaymentState = 'pending' | 'qr_emitted' | 'authorized' | 'captured' | 'failed' | 'refunded';

export interface KDSTicket {
  id: string;
  orderNumber: number;
  items: Array<{
    name: string;
    size: string;
    milk: string;
    syrup?: string;
  }>;
  totalKzt: number;
  paymentState: PaymentState;
  paymentRefKaspi?: string;
  status: 'pending' | 'in_progress' | 'completed' | 'canceled';
  createdAt: number;
}

export class CoffeeKDSManager {
  private queueRef = ref(rtdb, 'kiosks/astana_bc_01/queue');

  // Атомарная публикация тикета с монотонным инкрементом суточного счетчика заказов
  async pushTicket(order: Omit<KDSTicket, 'id' | 'orderNumber' | 'createdAt' | 'status'>): Promise<string> {
    const counterRef = ref(rtdb, 'kiosks/astana_bc_01/daily_counter');
    let generatedOrderNum = 1;

    await runTransaction(counterRef, (currentVal) => {
      generatedOrderNum = (currentVal || 0) + 1;
      return generatedOrderNum;
    });

    const newTicketRef = push(this.queueRef);
    const ticketPayload: KDSTicket = {
      ...order,
      id: newTicketRef.key!,
      orderNumber: generatedOrderNum,
      status: 'pending',
      createdAt: Date.now()
    };

    await set(newTicketRef, ticketPayload);
    return newTicketRef.key!;
  }

  // Реактивная подписка экрана KDS с сортировкой по FIFO
  subscribeQueue(onUpdate: (tickets: KDSTicket[]) => void): () => void {
    return get(this.queueRef, (snapshot) => {
      const data = snapshot.val();
      if (!data) {
        onUpdate([]);
        return;
      }
      const list: KDSTicket[] = Object.values(data);
      list.sort((a, b) => a.createdAt - b.createdAt);
      onUpdate(list);
    });
  }

  // Детерминированное завершение заказа: атомарное чтение через get() и перенос в архив
  async completeAndArchive(ticketId: string): Promise<void> {
    const itemRef = ref(rtdb, `kiosks/astana_bc_01/queue/${ticketId}`);
    const archiveRef = ref(rtdb, `kiosks/astana_bc_01/archive/${ticketId}`);

    const snapshot = await get(itemRef);
    if (snapshot.exists()) {
      const ticketData = snapshot.val();
      await set(archiveRef, {
        ...ticketData,
        status: 'completed',
        completedAt: Date.now()
      });
      await set(itemRef, null);
    }
  }

  // Обновление платежного статуса при получении вебхука от Kaspi Pay
  async updatePaymentState(ticketId: string, paymentState: PaymentState, paymentRefKaspi?: string): Promise<void> {
    const itemRef = ref(rtdb, `kiosks/astana_bc_01/queue/${ticketId}`);
    await update(itemRef, {
      paymentState,
      ...(paymentRefKaspi ? { paymentRefKaspi } : {})
    });
  }
}

Смотреть код на GitHub (OZAT-kz)

5. Реальные метрики производительности: Перцентили задержки, выборка N и Fallback-механизмы

Все метрики собраны на основе логов Cloud Logging и таймингов терминала за 60 календарных дней пилота (суммарная выборка N = 4 218 завершенных заказов). Методика замера сквозной задержки (End-to-End): интервал между детекцией окончания речи клиента на AudioWorklet (VAD speech_end timestamp) и отрисовкой динамического Kaspi QR на экране киоска вместе с появлением карточки в KDS бариста.

Хронометраж обслуживания и пропускная способность кассы в утренний пик (08:30 - 09:30)

Распределение сквозной задержки (End-to-End Latency Profile, N = 4 218):

  • p50 (Медиана): 340 мс (стабильный сетевой канал, теплый инстанс Cloud Run Gateway, инференс Gemini 2.5 Flash Live).
  • p95: 620 мс (кратковременный джиттер публичной сети Wi-Fi/LTE бизнес-центра в пиковые утренние часы).
  • p99: 890 мс (редкие cold starts дополнительных реплик Cloud Run при резких ступенчатых скачках нагрузки).

Латентность передачи тикета на дисплей бариста (KDS) по фазам обработки (мс)

Статистика исключительных ситуаций и Fallback на бариста (Edge Cases):

  • 4.8% заказов (202 случая из 4 218) потребовали ручного вмешательства бариста:
    • 2.1% (89 случаев): одновременная речь двух людей рядом с микрофоном (cross-talk в шумном атриуме).
    • 1.6% (68 случаев): заказ позиций, отсутствующих в меню или закончившихся на складе.
    • 1.1% (45 случаев): нестандартные просьбы об оплате наличными купюрами или корпоративными бонусами.
  • Встроенный Circuit Breaker: если задержка WebSocket-сессии превышает 1.5 секунды или VAD фиксирует потерю пакетов более 8%, терминал мгновенно переключается на экран резервного кнопочного выбора без зависания интерфейса.

6. Экономика, FinOps-декомпозиция и масштабирование

За 60 дней пилота среднее время обслуживания одного гостя сократилось со 135 секунд до 38 секунд. Пропускная способность кассового узла в утренний пиковый слот с 08:30 до 09:30 выросла с 26 до 78 заказов в час, а чистая выручка утреннего окна увеличилась на +68% без расширения штата бариста.

💰 Детализированный FinOps-биллинг Google Cloud ($16.40 / месяц):

  • Google Cloud Run (Gateway WebSocket proxy, 1 vCPU, 512 MB RAM, min-instances=0, max=3): $4.20 / мес (с учетом бесплатного уровня Free Tier и работы в часы утренних пиков).
  • Gemini 2.5 Flash Multimodal Live API (аудио-токены инференса, ~70 часов чистого аудиопотока в месяц): $9.80 / мес.
  • Firebase Realtime Database (1.8 GB GB-stored / egress-трафик KDS сокетов): $1.60 / мес.
  • Cloud Logging & Cloud Trace (сэмплирование метрик 10%): $0.80 / мес.
  • ИТОГО: $16.40 в месяц. При среднем чеке 1 600 ₸ инфраструктура окупается продажей первых 4 стаканчиков кофе в 08:35 утра каждого понедельника.

Если вы хотите оптимизировать облачные расходы вашей компании, изучите наш материал по FinOps и оптимизации затрат в Google Cloud.

Реальные ограничения и компромиссы решения

Инженерный аудит: реальные ограничения и компромиссы

Инженерная честность OZAT: при внедрении решения «Кофе за 40 секунд или бесплатно: Оптимизация пиковых очередей в БЦ Астаны через Firebase Realtime Database и Gemini Live Audio» в промышленную эксплуатацию вы обязаны учитывать следующие технологические ограничения:

  1. Шум кофемашин и акустические помехи в часы пик: Фоновый шум взбивания молока паром (до 78 дБ) снижает точность распознавания голоса в Gemini Live Audio без направленного микрофона с шумоподавлением.
  2. Параллельные заказы нескольких гостей: Голосовой ИИ-бариста требует четкого разделения реплик разных клиентов (Speaker Diarization) при плотной очереди у стойки.
  3. Синхронизация очереди заказов: Задержка обновления Firebase Realtime Database на планшете бариста должна составлять <150 мс для предотвращения дублирования порций.
  4. Ограничения стоп-листа ингредиентов: Если закончилось овсяное молоко, ИИ обязан мгновенно исключить его из предложений во всех диалогах в реальном времени.

💡 Совет OZAT: Готовы к внедрению? Рассчитайте архитектуру и бюджет через Scope Builder или пройдите бесплатный ИИ-аудит.

Рустам Шарафутдинов

Рустам Шарафутдинов

Автор Инженерного хаба

Эксперт в области архитектуры Google Cloud и Senior Full-Stack разработчик с более чем 15-летним опытом. Специализируется на отказоустойчивых архитектурах, оптимизации высоконагруженных проектов и интеграции AI (Vertex AI).

Экспертность: GCP, Kubernetes, Микросервисы, React, Node.js

Комментарии (0)