Практические советы: создание ИИ-агента для транскрипции и краткого изложения аудио
Пошаговое руководство по практическим рекомендациям: создание ИИ-агента для транскрипции и краткого изложения аудио — контракты, проверки и готовые блоки кода для команд, внедряющих эту схему.
В следующих заметках описывается практический подход к созданию ИИ-агента для транскрибации и резюмирования аудиозвонков. Основное внимание уделяется контрактам, проверкам и местам для вставки кода, а не мотивирующим аспектам. На этапе обзора сначала запишите контракт: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Рядом с функциональными результатами записывайте время выполнения и стоимость токенов или запросов. Отображение затрат с самого начала предотвращает неожиданные счета при переходе от демо-версии к общедоступным средам.
Что мы создаем
Этап «То, что мы создаём», работает наилучшим образом, если рассматривать его как измеримую структуру. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь граф. Сохраняйте состояние графа простым и типизированным. Вложенные структуры данных скрывают информацию о том, какой узел заполнил тот или иной поле, и мешают возобновлению работы после прерываний.
Запуск демо-версии
Этап тестирования демо-версии работает наилучшим образом, если рассматривать его как измеримую среду. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма тестирования. Документируйте одновременно успешный и восстановительный сценарии работы. Повторные попытки, проверки человеком и обработка неработающих сообщений являются частью продукта, а не этапом последующей доработки. Сохраняйте структуру графа простой и типизированной; вложенные структуры данных скрывают информацию о том, какой узел заполнил тот или иной поле, и мешают возобновлению работы после прерываний.
git clone https://github.com/openvidu-labs/transcriber-summarizer-agent.git
cd transcriber-summarizer-agent
python -m venv venv && source venv/bin/activate
pip install -r requirements.txt
# STT_PROVIDER: openai | aws | vosk (offline)
# LLM_PROVIDER: openai | aws | (empty for no summarization)
STT_PROVIDER=
LLM_PROVIDER=
# Required when "openai" is selected for STT_PROVIDER or LLM_PROVIDER
OPENAI_API_KEY=
# Required when "aws" is selected for STT_PROVIDER or LLM_PROVIDER
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
python main.py dev
python app/server.py
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TranscriberSummarizer",
"Effect": "Allow",
"Action": [
"transcribe:StartStreamTranscription",
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream"
],
"Resource": "*"
}
]
}
Понимание кода нашего агента
Понимание этапов работы агента наиболее эффективно при рассмотрении их как измеримых элементов. Соберите один образец успешной работы, один пример сбоя и записку о возврате к предыдущему состоянию перед расширением объёма задачи. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. При сбое какого-либо шага причина должна быть связана с конкретной ответственностью, а не с запутанной цепочкой операций. Сохраняйте структуру графа простой и типизированной. Вложенные структуры данных маскируют информацию о том, какой узел заполнил тот или иной поле, и могут нарушить возобновление работы после прерываний.
Шаг 1: Агент, который прослушивает всех
На первом этапе работа с агентами наиболее эффективна, если рассматривать их как измеримые объекты. Соберите один образец успешной работы, один пример сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работ. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия всем создаваемым элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задачи. Сохраняйте структуру графа простой и типизированной. Вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, что приводит к нарушению возобновления работы после перерывов.
from livekit.agents import AgentServer, JobContext, cli
server = AgentServer()
@server.rtc_session()
async def entrypoint(ctx: JobContext):
await ctx.connect()
room = ctx.room
if __name__ == "__main__":
cli.run_app(server)
def make_stt():
if STT_PROVIDER == "vosk":
from livekit.plugins import vosk
return vosk.STT(model_path=VOSK_MODEL_PATH, language="en-US", partial_results=False)
if STT_PROVIDER == "openai":
from livekit.plugins import openai
return openai.STT(model="gpt-4o-mini-transcribe")
if STT_PROVIDER == "aws":
from livekit.plugins import aws
return aws.STT()
raise ValueError(f"Unknown STT_PROVIDER {STT_PROVIDER!r}")
speech_to_text = make_stt() # one engine, shared by every speaker
@room.on("track_subscribed")
def _on_track_subscribed(track, publication, participant):
if track.kind == rtc.TrackKind.KIND_AUDIO:
asyncio.create_task(transcribe_track(participant, track))
async def transcribe_track(participant, track):
audio = rtc.AudioStream(track, sample_rate=16000, num_channels=1)
async with speech_to_text.stream() as stt_stream:
async def feed_audio():
async for event in audio:
stt_stream.push_frame(event.frame)
stt_stream.end_input() # no more audio: let the recognizer finish
async def emit_transcripts():
async for event in stt_stream:
if event.type == stt_api.SpeechEventType.FINAL_TRANSCRIPT and event.alternatives:
text = event.alternatives[0].text.strip()
if text:
await record_line(participant, track, text)
await asyncio.gather(feed_audio(), emit_transcripts())
Шаг 2: Запись записи в файл
Этап 2, связанный с написанием кода, работает наилучшим образом, если рассматривать его как измеримую поверхность. Соберите один идеальный пример результата, один пример сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости на ранних этапах предотвращает неожиданные счёты при переходе от демо-среды к общедоступным средам. Сохраняйте структуру графа простой и типизированной; вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, и могут нарушить возобновление работы после прерываний.
conversation = [] # in-memory history, used for the summary
async def record_line(participant, track, text):
speaker = participant.name or participant.identity
timestamp = datetime.datetime.now().strftime("%H:%M:%S")
conversation.append(f"{speaker}: {text}")
with open(transcript_path, "a", encoding="utf-8") as f:
f.write(f"[{timestamp}] {speaker}: {text}\n")
# Publish on LiveKit's built-in transcription channel, attributed to the speaker.
writer = await room.local_participant.stream_text(
topic=TOPIC_TRANSCRIPTION, # "lk.transcription"
sender_identity=participant.identity,
attributes={
ATTRIBUTE_TRANSCRIPTION_FINAL: "true",
ATTRIBUTE_TRANSCRIPTION_TRACK_ID: track.sid,
ATTRIBUTE_TRANSCRIPTION_SEGMENT_ID: utils.shortuuid("SG_"),
},
)
await writer.write(text)
await writer.aclose()
[14:02:11] Alice: should we ship the release today
[14:02:15] Bob: yes but let us wait for the tests to pass
[14:02:20] Alice: agreed lets do it after lunch
Шаг 3: Догоняющая поддержка с использованием LLM
Этап №3, предназначенный для обнаружения опоздавших участников, работает наилучшим образом, если рассматривать его как измеримую площадку. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объема работ. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Установите лимиты на количество токенов за раунд и сессию. Инструменты-агенты активно расширяют объем данных; жесткие ограничения предотвращают появление неожиданных счетов при демонстрациях.
@room.on("participant_connected")
def _on_participant_connected(participant):
asyncio.create_task(summarize_for(participant))
async def summarize_for(participant):
await asyncio.sleep(2) # let the newcomer's browser get ready
if not conversation:
return # nothing said yet, nothing to summarize
summary = await summarize(conversation)
await room.local_participant.send_text(
summary,
topic="summary",
destination_identities=[participant.identity],
)
def make_llm():
model = os.getenv("SUMMARY_MODEL") # optional override; default per provider
if LLM_PROVIDER == "openai":
from livekit.plugins import openai
return openai.LLM(model=model or "gpt-4.1")
if LLM_PROVIDER == "aws":
from livekit.plugins import aws
return aws.LLM(model=model or "us.amazon.nova-2-lite-v1:0")
raise ValueError(f"Unknown LLM_PROVIDER {LLM_PROVIDER!r}")
async def summarize(conversation):
ctx = llm.ChatContext.empty()
ctx.add_message(role="system", content=SUMMARY_PROMPT)
ctx.add_message(role="user", content="Transcript so far:\n" + "\n".join(conversation))
chunks = [c async for c in make_llm().chat(chat_ctx=ctx).to_str_iterable()]
return "".join(chunks).strip()
Один ключ для обеих частей
Основной принцип для обеих фаз работает наилучшим образом, если рассматривать его как измеримую поверхность. Зафиксируйте один идеальный пример выполнения, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работ. Документируйте одновременно успешный и восстановительный пути выполнения. Повторные попытки, проверки человеком и обработка неработающих сообщений являются частью продукта, а не этапом последующей доработки. Сохраняйте структуру графа простой и типизированной; вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, и мешают возобновлению работы после прерываний.
Шаг 4: Очень простой фронтенд
Этап 4 A, представляющий собой крайне простую стадию, работает наилучшим образом, когда его рассматривают как измеримую среду. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Когда происходит сбой, он должен указывать на конкретную ответственность, а не на запутанную цепочку операций. Сохраняйте структуру графа простой и типизированной. Вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, что приводит к нарушению работоспособности после перерывов. Этап 4 A, представляющий собой крайне простую стадию, работает наилучшим образом, когда его рассматривают как измеримую среду. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости на раннем этапе предотвращает неожиданные расходы при переходе от демо-среды к общедоступным средам.
from livekit.api import AccessToken, VideoGrants
token = (
AccessToken(API_KEY, API_SECRET)
.with_identity(identity)
.with_name(name)
.with_grants(VideoGrants(room_join=True, room=room))
.to_jwt()
)
const { token, url } = await (await fetch(`/token?room=${room}&identity=${id}&name=${name}`)).json();
const room = new LivekitClient.Room();
await room.connect(url, token);
await room.localParticipant.setMicrophoneEnabled(true);
room.registerTextStreamHandler("lk.transcription", async (reader, participantInfo) => {
if (reader.info.attributes?.["lk.transcription_final"] !== "true") return;
const text = await reader.readAll();
addLine(nameFor(participantInfo?.identity), text, new Date().toLocaleTimeString());
});
Куда двигаться дальше
На этапе «Куда перейти» необходимо определить входные данные, ответственного за шаг и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь кодовый граф. Вводите утверждение человека для операций, связанных с тратой денег или изменением производственных данных. Подключение на этапе компиляции не гарантирует полноты бизнес-логики.
Чек-лист операций
На этапе чек-листа операций необходимо определить входные данные, ответственного за шаг и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии.
Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успешности и не допускайте молчаливого частичного выполнения задач.
Обеспечьте человеческое утверждение для операций, связанных с тратой денег или изменением производственных данных. Подключение на этапе компиляции не гарантирует полноты решения бизнес-задач.
Напишите краткий руководство: как обновлять ключи, как опустошать очередь, как откатывать последнюю загрузку данных.
Записывайте временные показатели, стоимость токенов или запросов рядом с функциональными результатами. Ясность стоимости заранее предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды.
Обеспечьте человеческое утверждение для операций, связанных с тратой денег или изменением производственных данных. Подключение на этапе компиляции не гарантирует полноты решения бизнес-задач.
Перед внедрением стека заморозьте версии, сделайте копию «золотого» протокола для критической цепочки операций и уточните шаги возврата к предыдущему состоянию. В совместных средах необходимы ограничения по частоте запросов, проверки принадлежности пользователя и четко определенный ответственный за обновление секретов. Лучше выбирать надежность, чем креативные одноразовые демонстрации.
Примечание для d9d69769604b: не храните ключи поставщика в репозитории, установите лимит токенов на сессию и сохраняйте протоколы рядом с фикстурами для оценки, чтобы последующие замены моделей оставались сопоставимыми.