Содержание статьи +
- Кратко
- Почему это важно
- Что вы получаете
- Точка входа: `agent.py`
- Сердце: `transcriber.py`
- Интеллект, применённый один раз: `notes.py`
- Опциональное дополнение: `recording.py`
- Как ввести агента в звонок: `token_server.py`
- Доказать, что ядро работает: `tests/test_notes.py`
- Запуск и стоимость
- Строить на этом или купить готового бота?
- Где здесь Фора Софт
- Главное
- Что почитать дальше
Кратко
Этот урок даёт вам готовый рабочий open-source проект: ноутейкер на LiveKit Agents, который входит в любую встречу как молчаливый дополнительный участник, расшифровывает речь каждого спикера с именами и таймстампами, а по завершении звонка превращает транскрипт в структурные заметки (резюме, решения, задачи), сохранённые как Markdown и JSON. Мы разберём все шесть исходных файлов простыми словами – чтобы вы понимали не только что делает каждый, но и почему он устроен именно так. Дизайн намеренно listen-only: агент никогда не говорит, что убирает самый дорогой слой затрат и делает его незаметным. К концу вы сможете склонировать репозиторий, запустить его против своего сервера LiveKit примерно за пять минут, диспатчить из своего приложения и задеплоить в LiveKit Cloud или Docker.
Почему это важно
Большинство команд, которым нужен ИИ-ноутейкер, начинают со склейки API транскрипции и LLM и быстро упираются в сложные места: как бот вообще попадает в звонок, как держать слова каждого спикера раздельно, когда запускать резюме и сколько всё это стоит. Этот урок убирает гадание, давая референсную реализацию, которая решает эти проблемы так же, как собственные примеры LiveKit, плюс продакшен-части, которых в примерах нет: диспатч, запись, структурные заметки и тесты. Если вы основатель или продакт-лид, вы получаете конкретный артефакт, под который можно оценивать и скоупить. Если вы инженер, вы получаете код, который читается сверху вниз за один присест и адаптируется под ваш продукт.
Что вы получаете
Загрузка ниже – это весь проект под лицензией Apache-2.0, как и сам LiveKit, проверенный против livekit-agents 1.x по состоянию на июнь 2026. Он намеренно небольшой – шесть исходных файлов, один файл тестов и обвязка для деплоя, – потому что цель в понимании, а не в фреймворке, который надо учить.
Перед кодом одно напоминание из опорного урока по LiveKit: ассистент встреч – это не функция, прикрученная к экрану одного человека. Это дополнительный участник, сделанный из софта. Он подключается к той же комнате, что и люди, слушает общее аудио и выдаёт результат как текст, запись или речь. Всё в этом репозитории вытекает из этой одной идеи. Если термины LiveKit ниже кажутся незнакомыми – комната, участник, диспатч, агент – сначала прочитайте тот опорный урок; здесь он предполагается известным.
Вот весь проект целиком, прежде чем мы откроем хоть один файл.
Пять исходных файлов в 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», и репозиторий близко следует официальному примеру, добавляя часть, которую пример опускает: пересылку финализированных строк в общее хранилище.
Сам агент на участника крошечный. Это STT-only агент, который при каждом завершении мысли спикера записывает строку, а затем намеренно молчит:
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,
),
)Читайте эти четыре флага как контракт агента с комнатой. Он берёт аудио на вход, может публиковать текст наружу (чтобы встреча показывала живые субтитры, если UI хочет), никогда не выдаёт аудио наружу и привязан ровно к одному участнику. Поставьте audio_output=True – и у вас говорящий бот; оставить False – весь смысл. Когда встреча заканчивается, aclose() отменяет фоновые задачи и аккуратно дренирует каждую сессию, чтобы ни одна недописанная строка не потерялась.
Интеллект, применённый один раз: `notes.py`
Этот файл держит две обязанности, которые легко спутать: сбор транскрипта по ходу звонка и резюмирование один раз в конце. Их разделение – это то, что держит дизайн дешёвым и предсказуемым.
Сборщик – небольшое in-memory хранилище. Каждая строка записывает, кто это сказал, что сказал и сколько секунд от начала встречи прошло:
@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) падает мягко, и сырой текст сохраняется, а не рушит прогон.
Запись вывода – последний шаг. Репозиторий выдаёт два файла на встречу, названные по комнате и таймстампу: 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/.
Деплой в 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 минут сессии агента / месяцИ speech-to-text по представительной потоковой ставке около $0.006 за минуту расшифрованного аудио:
2 000 встреч × 45 минут × $0.006
= $540 / месяц на speech-to-textУрок в пропорциях, а не в итоге. Поскольку агент никогда не говорит, нет стадии text-to-speech – а 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 – резюме сессии; в конференциях – ноутейкером, которого пользователи уже ждут. Инженерное суждение, которое нам важно, – это сопоставить дизайн с потребностью: держать ноутейкер listen-only, чтобы его не построили – и не тарифицировали – как говорящий копилот.
Главное
- Репозиторий – готовый рабочий ноутейкер на LiveKit Agents под Apache-2.0 – шесть файлов, разобранных полностью.
- По одной STT-сессии на участника даёт атрибутированные транскрипты без отдельного шага диаризации.
- raise StopResponse() – это то, что делает агента listen-only вместо ответов всем.
- Резюме – один проход LLM в конце встречи, возвращающий строгий JSON, записанный как Markdown и JSON.
- Запись – отдельная задача Egress, выключена по умолчанию; используйте file_outputs=[...], а не одиночное output.
- Listen-only означает отсутствие text-to-speech – самого тяжёлого слоя затрат – поэтому работает за долю стоимости копилота.