Ноутейкер встреч на LiveKit – полный репозиторий

Автор: Николай СапуновОбновлено: август 202624 мин чтения
Содержание статьи +

Кратко

Этот урок предлагает готовый рабочий open-source проект: ноутейкер на базе LiveKit Agents, который подключается к любой встрече как молчаливый дополнительный участник, распознаёт речь каждого спикера с указанием имён и временных меток, а по окончании звонка преобразует транскрипт в структурированные заметки – резюме, решения, задачи – и сохраняет их в форматах Markdown и JSON. Мы подробно разберём все шесть исходных файлов простым языком, чтобы вы не только поняли, что делает каждый из них, но и почему он устроен именно так. Дизайн намеренно ориентирован на прослушивание: агент никогда не говорит, что позволяет исключить самый дорогой компонент и сделать его незаметным. К концу урока вы сможете склонировать репозиторий, запустить проект на своём сервере LiveKit примерно за пять минут, интегрировать его из своего приложения и развернуть в LiveKit Cloud или Docker.

Почему это важно

Большинство команд, которым нужен ИИ-ноутейкер, начинают с интеграции API транскрипции и LLM, но быстро сталкиваются с серьёзными проблемами: как подключить бота к звонку, как разделять речь каждого участника, когда генерировать резюме и сколько всё это будет стоить. Этот урок устраняет неопределённость, предлагая готовую реализацию, которая решает эти задачи так же, как собственные примеры LiveKit, но с дополнительными продакшен-компонентами, которых в них нет: диспетчеризация, запись, структурированные заметки и тесты. Если вы – основатель или продуктовый лидер, вы получаете конкретный артефакт, по которому можно оценивать и масштабировать. Если вы – инженер, вы получаете код, который легко читается от начала до конца и быстро адаптируется под ваш продукт.

Что вы получаете

Загрузка ниже – это весь проект под лицензией Apache-2.0, как и сам LiveKit, проверенный по версии livekit-agents 1.х на июнь 2026 года. Он намеренно небольшой – шесть исходных файлов, один тестовый и обвязка для деплоя – потому что цель здесь не в создании фреймворка, который нужно учить, а в понимании.

Перед кодом – напоминание из опорного урока по LiveKit: ассистент встреч – это не просто функция, «прикрученная» к экрану одного человека. Это полноценный участник, созданный программно. Он подключается к той же комнате, что и люди, слушает общее аудио и выдаёт результат в виде текста, записи или речи. Всё, что есть в этом репозитории, вытекает из этой одной идеи. Если термины LiveKit ниже – комната, участник, диспатч, агент – кажутся незнакомыми, сначала ознакомьтесь с опорным уроком; здесь он считается известным.

Вот весь проект целиком, прежде чем мы откроем хоть один файл.

Рис. 1. Репозиторий как рантайм. Каждый блок – один исходный файл; стрелки – порядок событий, от входа пользователя до заметок на диске.

Пять исходных файлов в src/ чётко распределяют работу, а файл тестов фиксирует ту единственную часть, которую стоит тестировать без подключения к сети. Таблица ниже – это карта, по которой мы пройдём весь оставшийся урок.

ФайлЗа что отвечаетЧто запомнить
src/agent.pyТочка входа – связывает всё вместеСервер агентов передаёт управление вашему entrypoint на каждый звонок
src/transcriber.pyПо одной STT-сессии на участникаРаздельные сессии держат слова каждого спикера атрибутированными
src/notes.pyСобрать строки, резюмировать один раз, записать файлыРезюме – один проход LLM в конце, а не во время звонка
src/recording.pyОпциональная запись через LiveKit EgressЗапись – отдельная задача от транскрипции
src/token_server.pyВыдать токен пользователю и диспатчить агентаЯвный диспатч ставит агента только туда, куда нужно
tests/test_notes.pyЮнит-тесты слоя заметокДетерминированное ядро тестируется без сети

Точка входа: `agent.py`

У каждого агента LiveKit есть одна входная дверь, и этот файл – она. Представьте эту дверь как стойку ресепшена в здании: долгоживущая программа ждёт там, и каждый раз, когда встрече нужен ассистент, ресепшен выдаёт посетителю – вашему коду – ключ от одной конкретной комнаты.

В фреймворке LiveKit стойка ресепшена – AgentServer, а передача ключа – функция, помеченная декоратором. Декоратор – это однострочная пометка, написанная символом @ над функцией, которая говорит фреймворку: «вызови эту функцию, когда придёт задача». Вот её форма, сокращённая до сути:

server = AgentServer()

def prewarm(proc: JobProcess) -> None:
    # Загрузить модель активности голоса один раз на процесс и переиспользовать.
    proc.userdata["vad"] = silero.VAD.load()

server.setup_fnc = prewarm

@server.rtc_session(agent_name="meeting-notetaker")
async def entrypoint(ctx: JobContext) -> None:
    ...

if __name__ == "__main__":
    cli.run_app(server)

Три детали в этом маленьком блоке имеют реальное значение. Первая: prewarm выполняется один раз при запуске процесса-воркера – до назначения ему встречи – и загружает небольшую модель voice activity detection, сокращённо VAD. Это лёгкая модель, которая просто определяет, говорит ли кто-то в данный момент. Загрузив её один раз и используя для всех участников в рамках одного процесса, мы экономим время запуска на каждом звонке. Поскольку фреймворк выполняет каждую задачу в отдельном процессе операционной системы ради изоляции, «прогрев» модели оказывается выгодным именно на уровне процесса.

Вторая: аргумент agent_name="meeting-notetaker" – не косметика. В LiveKit имя агента в декораторе сессии переключает его в режим явного диспатча: агент будет входить только в те комнаты, куда вы его конкретно отправили, и никогда – во все автоматически. Это как раз то, что нужно для продукта встреч, поэтому token-сервер обязан далее запрашивать агента по тому же имени.

Третья: cli.run_app(server) превращает файл в программу командной строки с тремя режимами. Запуск python src/agent.py console позволяет общаться с агентом в терминале без использования фронтенда. Режим dev запускает воркер против вашего сервера LiveKit с горячей перезагрузкой и понятными логами. Режим start – продакшен-режим, использующий Dockerfile и выводящий машиночитаемые JSON-логи.

Теперь тело точки входа – это код, который выполняется при каждой встрече:

@server.rtc_session(agent_name="meeting-notetaker")
async def entrypoint(ctx: JobContext) -> None:
    store = TranscriptStore(room_name=ctx.room.name)
    transcriber = MultiUserTranscriber(ctx, store, stt_model=STT_MODEL)
    egress_id = await start_room_recording(ctx.room.name)

    # Подписаться только на аудио — ноутейкеру никогда не нужны видеотреки.
    await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
    transcriber.start()

    async def on_shutdown() -> None:
        await transcriber.aclose()
        await stop_recording(egress_id)
        if store.is_empty:
            return
        summary = await summarize_transcript(store)
        write_notes(NOTES_DIR, store, summary)

    ctx.add_shutdown_callback(on_shutdown)

Читайте это как короткий рассказ. Агент создаёт пустую тетрадь (store), нанимает транскрайбера, который будет в неё писать, и при необходимости запускает запись. Затем подключается к комнате, запрашивая только аудио – видео не нужно, ведь ноутейкеру изображение не требуется, а отказ от видео экономит трафик, за который вы иначе платите. Запускает транскрипцию. Наконец, регистрирует колбэк завершения – функцию, которую фреймворк обязуется выполнить, когда все покинут комнату. Этот колбэк закрывает транскрайбера, останавливает запись и – только если что-то действительно было сказано – запускает резюме и сохраняет заметки. Реальный файл оборачивает резюме в try/except, чтобы в случае сбоя резюмирования вы не потеряли исходный транскрипт; потерять слова из-за поломки резюме было бы худшим исходом.

Форма, которую стоит усвоить: во время звонка агент почти ничего умного не делает – он просто собирает информацию. Интеллект проявляется один раз, в конце. У этого выбора есть последствия для стоимости и качества, к которым мы вернёмся.

Сердце: `transcriber.py`

Это файл, который делает ноутейкером ноутейкера, и на нём стоит сбавить темп. Задача, которую он решает, звучит просто, но на деле оказывается сложной: как в звонке с пятью участниками получить пять чётко разделённых потоков «кто что сказал», а не один общий, перепутанный транскрипт?

Наивный подход – направить один транскрайбер на смешанное аудио комнаты – даёт сплошной текст без возможности надёжно определить, кто из спикеров что сказал. В этом случае потребуется дополнительная система, называемая диаризацией спикеров, чтобы постфактум определить, кто произнёс каждую реплику; более сложный путь мы подробно разбираем в уроке по диаризации Pyannote. Этот репозиторий полностью обходит эту проблему более чистым решением: каждому участнику даётся собственный транскрайбер.

Это работает благодаря особенностям доставки аудио в LiveKit. Аудиопоток каждого участника поступает как отдельный трек – так называемый медиапоток, который пересылает медиасервер, описанный в сравнении WebRTC и video SDK. Поскольку потоки изначально разделены на источнике, к каждому можно привязать отдельную STT-сессию, и каждая расшифрованная строка автоматически атрибутируется нужному человеку – без необходимости угадывать, кто говорит. Это канонический паттерн LiveKit «multi-user transcriber», и репозиторий следует официальному примеру, добавляя лишь одну деталь, которую он опускает: отправку финализированных строк в общее хранилище.

Рис. 2. Почему одна сессия на участника лучше одной сессии на комнату. Раздельные треки на входе – уже размеченные строки на выходе, без отдельного шага «кто говорил?».

Вклад самого агента в диалог минимальный. Это агент STT-только, который при завершении высказывания спикера записывает строку и затем намеренно молчит:

class ParticipantTranscriber(Agent):
    def __init__(self, *, participant_identity, store, stt_model):
        super().__init__(instructions="not-needed", stt=inference.STT(stt_model))
        self.participant_identity = participant_identity
        self._store = store

    async def on_user_turn_completed(self, chat_ctx, new_message) -> None:
        text = (new_message.text_content or "").strip()
        if text:
            self._store.add(speaker=self.participant_identity, text=text)
        # Подавить ответ по умолчанию: этот агент только слушает.
        raise StopResponse()

Самая важная строка – raise StopResponse(). По умолчанию агент LiveKit, услышав завершённую реплику, попытается на неё ответить – запустить языковую модель и произнести вслух. Для ноутейкера это была бы катастрофа: бот начал бы отвечать всем подряд. raise StopResponse() – это способ сообщить фреймворку: «Я услышал, я закончил, не генерируй ответ». Именно так разговорный агент превращается в чистого слушателя. Вызов inference.STT(stt_model) направляет аудио через LiveKit Inference – брокерский способ подключения к провайдеру вроде Deepgram – с моделью, заданной в одной переменной окружения (deepgram/nova-3 по умолчанию); урок по потоковому ASR сравнивает варианты провайдеров.

Окружающий класс MultiUserTranscriber – это менеджер. Он отслеживает два события в комнате: вход и выход участников, и поддерживает словарь, в котором для каждого человека хранится отдельная сессия транскрипции:

def start(self) -> None:
    self.ctx.room.on("participant_connected", self._on_participant_connected)
    self.ctx.room.on("participant_disconnected", self._on_participant_disconnected)
    # Обработать всех, кто уже в комнате, когда агент входит.
    for participant in self.ctx.room.remote_participants.values():
        self._on_participant_connected(participant)

Последний цикл важнее, чем может показаться. События срабатывают только для будущего: если в комнате уже три человека, когда приходит агент, ни одно событие «connected» для них не произойдёт. Перебор существующих участников при старте – это способ, которым агент «подхватывает» всех, кто уже был. Забыть этот цикл – классическая ошибка: агент идеально обрабатывает опоздавших, но молча игнорирует тех, кто пришёл раньше.

Когда участник подключается, менеджер создаёт для него сессию, в настройках которой устанавливается режим прослушивания – listen-only.

await session.start(
    agent=ParticipantTranscriber(...),
    room=self.ctx.room,
    room_options=room_io.RoomOptions(
        audio_input=True,    # слушать этого участника
        text_output=True,    # публиковать живые субтитры обратно в комнату
        audio_output=False,  # никогда не говорить — это ноутейкер
        text_input=False,
        participant_identity=participant.identity,
    ),
)

Читайте эти четыре флага как контракт агента с комнатой. Он получает аудио на вход, может публиковать текст наружу (чтобы встреча отображала живые субтитры, если интерфейс того требует), никогда не передаёт аудио наружу и привязан строго к одному участнику. Поставьте audio_output=True – и у вас будет говорящий бот; оставьте False – и весь смысл пропадёт. Когда встреча завершается, aclose() отменяет фоновые задачи и аккуратно завершает каждую сессию, чтобы ни одна незавершённая строка не была потеряна.

Интеллект, применённый один раз: `notes.py`

Этот файл выполняет две задачи, которые легко перепутать: сбор транскрипта в ходе звонка и создание краткого резюме в конце. Разделение этих функций делает архитектуру простой и предсказуемой.

Сборщик – небольшое хранилище в оперативной памяти. Каждая запись содержит информацию о том, кто сказал, что сказал и сколько секунд прошло с начала встречи:

@dataclass
class TranscriptLine:
    speaker: str
    text: str
    t: float  # секунды от начала встречи

def add(self, speaker: str, text: str) -> None:
    self.lines.append(
        TranscriptLine(speaker=speaker, text=text, t=time.monotonic() - self.started_at)
    )

Таймстамп использует монотонные часы – те, что идут только вперёд и не реагируют на изменение системного времени во время звонка, – поэтому прошедшее время никогда не идёт назад, даже если серверные часы сдвинуты. Хранилище также возвращает список спикеров без дубликатов и текстовую версию полного транскрипта, используемую как промпт для создания резюме и как читаемый человеком блок в файле заметок.

Резюме – это один вызов языковой модели, сделанный после встречи, а не во время неё. Это решение оказывает наибольшее влияние на стоимость во всём репозитории, поэтому стоит подчеркнуть это прямо. Агент не просит LLM «постоянно резюмировать» в процессе разговора. Он ждёт окончания звонка, затем отправляет весь транскрипт модели ровно один раз и запрашивает структурированный вывод:

resp = await client.chat.completions.create(
    model=model,
    response_format={"type": "json_object"},
    messages=[
        {"role": "system", "content": SUMMARY_SYSTEM_PROMPT},
        {"role": "user", "content": user_prompt},
    ],
)

Настройка response_format={"type": "json_object"} требует от модели возвращать строгий JSON, а не свободную прозу, чтобы результат можно было сразу использовать в остальной программе без хрупкого парсинга текста. Промпт указывает ровно пять ключей, которые должны быть возвращены: краткий обзор, темы, решения, задачи (каждая с владельцем и опциональным сроком) и любые открытые follow-up’ы – и строго запрещает модели выдумывать факты, не подтверждённые транскриптом. Код всё равно оборачивает обработку в защиту: если модель когда-либо вернёт что-то, не являющееся валидным JSON, строка json.loads(raw) падает мягко, и сырой текст сохраняется, а не ломает выполнение.

Рис. 3. Один проход, два вывода. Транскрипт становится структурным резюме за один вызов, затем сохраняется как Markdown для людей и JSON для систем.

Запись вывода – последний шаг. Репозиторий генерирует два файла на каждую встречу, названные по имени комнаты и таймстампу: Markdown-файл с разделами для человека – Резюме, Решения, Задачи, Follow-up’ы – и полный транскрипт, а также JSON-файл с тем же содержанием, но в формате, пригодном для обработки программами. Выдавать оба – намеренно: Markdown предназначен для участников встречи, а JSON – для системы, которая создаст задачи в вашем трекере. Имя файла санитизируется, чтобы комната с названием Q3 / planning не могла «вырваться» за пределы папки с заметками – это небольшая, но полезная мера безопасности, которую стоит перенять.

Опциональное дополнение: `recording.py`

Запись выглядит так, будто она должна быть внутри транскрайбера, но одно из самых чистых решений в этом репозитории – в том, что это не так. Транскрипция и запись реализованы как две отдельные задачи, потому что именно так устроен LiveKit, и именно так LiveKit Cloud взимает плату за эти функции.

Запись осуществляется с помощью компонента LiveKit под названием Egress – части системы, отвечающей за экспорт или повторную трансляцию содержимого комнаты. Агент никогда не пытается самостоятельно захватывать медиа; вместо этого он просит Egress записать композит комнаты (объединение всех участников в один файл) и загрузить его в ваше собственное хранилище:

request = api.RoomCompositeEgressRequest(
    room_name=room_name,
    layout="speaker",
    audio_only=False,
    file_outputs=[file_output],   # внимание: список, а не одиночное 'output'
)
async with api.LiveKitAPI() as lkapi:
    info = await lkapi.egress.start_room_composite_egress(request)
    return info.egress_id

Вся функциональность скрыта за одной переменной окружения, RECORD_MEETINGS, и отключена по умолчанию – ведь запись влечёт за собой обязательства по приватности и согласию, в которые стоит входить осознанно, а не случайно. Если функция включена, но хранилище не настроено, код выводит предупреждение и пропускает запись, не прерывая встречу: ноутейкер, который ничего не записал, всё ещё полезен; тот, что «умер» из-за отсутствия бакета, – уже нет.

«Частая ловушка: пропущенное поле output. Реальный баг, с которым сталкивались многие команды LiveKit, – это отправка запроса записи с одиночным полем output= и последующее получение от LiveKit Cloud ошибки invalid_argument: missing field: output. Текущий, правильный API ожидает список – file_outputs=[...] – и именно такой формат используется в этом репозитории намеренно. Если при адаптации кода запись падает в Cloud с этой ошибкой, значит, вы случайно вернулись к устаревшему формату с одиночным полем. Та же логика разделения ответственности применима и к более тонкой ловушке: не пытайтесь перехватывать аудиофреймы внутри агента, чтобы «сохранить запись» – это смешивает два тарифицируемых ресурса и два режима отказа в один хрупкий и ненадёжный путь. Пусть Egress отвечает за запись, а агент – за расшифровку.»

Приятное свойство, о котором стоит знать: запись композита комнаты привязана к её жизненному циклу и автоматически останавливается, когда покидает последний участник. Репозиторий всё равно явно вызывает stop_recording при завершении – это безвредная подстраховка, делающая намерение очевидным для следующего, кто будет читать код.

Как ввести агента в звонок: `token_server.py`

Пока агент может работать, но его ещё не назначили на конкретную встречу. Это задача последнего исходного файла – он выполняет две операции в одном HTTP-запросе.

Приложению пользователя нужны две вещи, чтобы войти в комнату LiveKit: адрес сервера и подписанный токен доступа – короткоживущее удостоверение, как нумерованный билет, говорящее «этому человеку можно войти в эту конкретную комнату». Token-сервер чеканит этот билет. Но в том же запросе, до выдачи билета, он также диспатчит ноутейкер в эту комнату:

@app.get("/token")
async def token(room: str, identity: str) -> dict:
    # 1. Диспатчить агента-ноутейкер в комнату.
    async with api.LiveKitAPI() as lkapi:
        await lkapi.agent_dispatch.create_dispatch(
            api.CreateAgentDispatchRequest(agent_name=AGENT_NAME, room=room)
        )
    # 2. Выдать токен входа человеку.
    grant = api.VideoGrants(room_join=True, room=room)
    jwt = (
        api.AccessToken(os.environ["LIVEKIT_API_KEY"], os.environ["LIVEKIT_API_SECRET"])
        .with_identity(identity).with_name(identity).with_grants(grant).to_jwt()
    )
    return {"serverUrl": os.environ["LIVEKIT_URL"], "roomName": room, "token": jwt}

Это явный диспатч – и это рекомендуемый паттерн по причине, описанной в опорном уроке по LiveKit: он отправляет агента только в те комнаты, которые вы выбираете, и позволяет прикрепить метаданные – например, какая это встреча, кто хост – чтобы агент понимал свой контекст. Альтернатива – диспатч по токену – срабатывает только при первом создании комнаты и молча ничего не делает, если комната уже существует; у явного диспатча такой ловушки нет. Обратите внимание: ключ и секрет API берутся из переменных окружения и никогда не передаются в браузер – сервер подписывает токен, клиент лишь получает его.

Ваш фронтенд вызывает один эндпоинт, получает адрес сервера и токен, после чего подключается. Ноутейкер уже в пути внутрь – обычно он помещается в комнату быстрее, чем за пятую долю секунды. С точки зрения пользователя ассистент просто появляется, как только начинается звонок.

Доказать, что ядро работает: `tests/test_notes.py`

Замечание о тестировании, потому что оно показывает, как нужно мыслить о надёжности в ИИ-функциях. Те части, что зависят от живой сети и платной модели – транскрипция и резюмирование LLM, – сложно поддаются юнит-тестированию. А вот те, которые никогда не должны ломаться – сбор строк, атрибуция спикеров, рендеринг файла заметок, – основаны на чистой логике, и именно их проверяет тестовый файл:

def test_render_markdown_has_sections():
    summary = {
        "summary": "The team agreed to ship on Friday.",
        "decisions": ["Ship the note-taker on Friday."],
        "action_items": [{"owner": "bob", "task": "Write the deployment guide.", "due": None}],
        "follow_ups": [],
    }
    md = render_notes_markdown(_store(), summary)
    assert "## Summary" in md
    assert "**bob**" in md

Тесты запускаются без API-ключей и без интернета, и проходят заметно быстрее секунды. Такое разделение – жёстко тестировать детерминированное ядро, а модель рассматривать как внешнюю зависимость, которую можно замокать или пропускать – является правильным стандартом для любой ИИ-функции. Именно поэтому слой заметок реализован как простые функции над простой структурой данных, а не вплетён в живого агента.

Запуск и стоимость

Пять шагов помогут вам перейти от клона к разговору с агентом. Установите зависимости через uv sync, скопируйте .env.example в .env.local и введите ключи LiveKit, Deepgram и OpenAI. Один раз скачайте веса модели VAD через python src/agent.py download-files, затем запустите python src/agent.py console, чтобы общаться с агентом в терминале, или python src/agent.py dev, чтобы запустить его как воркер на сервере. Войдите в комнату с любого фронтенда LiveKit, поговорите и выйдите – заметки появятся в notes/.

Рис. 4. От ноутбука до продакшена. Тот же код запускается в трёх локальных режимах и может быть развернут тремя способами; счётчики затрат внизу – это то, за что вы реально платите.

Деплой в LiveKit Cloud – всего две команды, если livekit.toml и Dockerfile настроены: lk agent create – впервые, чтобы зарегистрировать агента и записать его ID в livekit.toml, а затем lk agent deploy – для каждой последующей версии. LiveKit Cloud собирает образ контейнера автоматически и запускает его. Либо соберите Docker-образ самостоятельно и запустите на любой инфраструктуре через docker run --env-file .env.local meeting-notetaker.

Структура затрат – это то, что стоит учитывать при планировании. Поскольку речь идёт о ноутбуке с функцией прослушивания (listen-only), это относительно дешёвое решение. В расчёт входят три счётчика и модель распознавания речи (speech-to-text). Рассмотрим пример: 2 000 встреч в месяц, продолжительностью по 45 минут, с участием пяти человек и одного агента.

Время подключения тарифицируется за каждого участника, включая агента:

2 000 встреч × 45 минут × 6 участников
= 540 000 минут участников WebRTC / месяц

Собственное время работы агента тарифицируется отдельно – только за самого агента:

2 000 встреч × 45 минут × 1 агент
= 90 000 минут сессии агента / месяц

И распознавание речи по представительной тарифной ставке около 0,006 доллара за минуту расшифрованного аудио:

2 000 встреч × 45 минут × $0.006
= $540 / месяц на speech-to-text

Урок в пропорциях, а не в итогах. Поскольку агент не говорит, нет этапа text-to-speech – а он обычно самый затратный слой, часто обходящийся примерно в $0.03 за минуту. Отсутствие этого этапа – причина, по которой listen-only-ассистент может стоить менее половины того, что стоил бы говорящий копилот. Полные тарифы по уровням с лимитами Build, Ship и Scale – в опорном уроке по архитектуре и ценам LiveKit, а юнит-экономику по функциям мы моделируем в реальной стоимости ИИ в видеопродуктах.

Строить на этом или купить готового бота?

Этот репозиторий – пример «build», и он подходит, когда ассистент является частью вашего продукта или должен работать в среде, которую вы уже контролируете: ваше приложение для конференций, телемедицинская платформа, комната для e-learning. Вы контролируете поведение, модели и путь данных.

Если всё, что нужно, – «дать пользователям транскрипт их звонка в Zoom», то купить готовый сервис-бота встреч вроде Recall.ai – бота под ключ, который входит в Zoom, Google Meet или Teams и возвращает транскрипты через API примерно за $0.50 за час записи – быстрее и дешевле для запуска, чем держать любую инфраструктуру. Компромисс обычный: чем более готовый продукт вы покупаете, тем меньше можете менять. Этот репозиторий существует для случая, когда менять – и есть смысл.

ПутьЧто вы делаетеЛучше всего, когда
Этот репо на LiveKit CloudЗапускаете агента; LiveKit хостит медиаНоутейкер живёт внутри вашего приложения
Этот репо на self-hostЗапускаете open-source сервер и агентаСтрогая приватность или резидентность данных
Купить бота встреч (напр. Recall.ai)Зовёте API; бот входит в Zoom/Meet/TeamsТранскрипт – товарное дополнение

Где здесь Фора Софт

Мы разрабатываем продукты для видеосвязи в реальном времени с 2005 года – видеоконференции, e-learning, телемедицину и инструменты совместной работы – и ноутейкер сегодня почти стандартный запрос в проектах по конференциям и телемедицине, которые мы прорабатываем. Мы выпускали агентов, присоединявшихся к звонкам как участники, чтобы расшифровывать и резюмировать содержание, и архитектура в этом репозитории – та же, что мы используем, когда искусственный интеллект должен работать внутри продукта клиента, а не через стороннего бота. В телемедицине этот подход превращается в ИИ-скрайба, который составляет заметки о приёме; в e-learning – в резюме сессии; в конференциях – в ноутейкера, которого пользователи уже ожидают. Важное инженерное решение – согласовать дизайн с реальной потребностью: сделать ноутейкера только слушающим, чтобы его не воспринимали – и не тарифицировали – как говорящего помощника.

Главное

  • Репозиторий – готовый рабочий ноутбук на основе LiveKit Agents под лицензией Apache-2.0, состоящий из шести полностью разобранных файлов.
  • Одна STT-сессия на участника обеспечивает атрибутированные транскрипты без необходимости отдельного этапа диаризации.
  • raise StopResponse() – это то, что превращает агента в режим прослушивания без ответов.
  • Резюме генерируется за один проход LLM в конце встречи и возвращается в виде строгого JSON, сохранённого также в форматах Markdown и JSON.
  • Запись – отдельная задача Egress, отключена по умолчанию; используйте file_outputs=[...], а не одиночный output.
  • Режим listen-only исключает text-to-speech – самый ресурсоёмкий и дорогой компонент – поэтому работает за долю стоимости полноценного копилота.

Что почитать дальше

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.