Галоўная / Артыкулы

Агент для жывага перакладу тэксту і стварэння падсумку для тых, хто прыўязваецца пазней, з агентамі LiveKit

Створыце агента на Python, які зберагае транскрыпцію календара, трансляюце прыемныя напісы у браузер і прыватна падсумавляе зустрэч для тых, хто прыўязваецца пазней.

Выдавец: REACTAPP.TOP8 хв чытання1790 слоў

Крыніцы: Building an AI agent for transcribing and summarizing audio calls

Уж існуе багато агентоў і сервісаў на базе ШЫ, таму пропонуема ўсьмохнены і практычны раўязанне: агента, які дапамагае людзям пад час аудыякалу. Ён выканае тры заведамыя задачы. Перша — зберагае полны тэкст пераказу зустрэчы у файле з тэкстам. Другая — надае прыемнікі розмовы всім учаснікам у рэальны час. А калі хтась прыўязуецца пазней, ёму надаецца прыватны підсумак таго, што ён прапусціў.

Для стварэння агента вікорыстоўваецца локальны медыя-сервер, сумесны з LiveKit, і фрэймворк LiveKit Agents на Python. Ўжоўсёе яны абавязваюцца за складныя аспекты транспортування аудыя у рэальны час, таму застаецца толькі напісаць функціі самога агента.

Компаненты і прабег дадзенняў

Існуе тры основныя часткі:

  • Аудыя-камера, дзе зустрэчаюцца людзі і бот, якая розмешчаная на локальным серверы, таму не трэба рэўістрацыі.
  • Агент-транскрыбар — маленькая програма на Python, якая входзіць у кожную камеру як невыразны учаснік, перакладае словы кожнаго чалавека у тэкст і зберагае яго.
  • Маленькі веб-дапрынг — простая сторанка для падключэння да камеры, размовы і адзірвання, як формуецца транскрыпцыя.
  • Даныя пераходзяць между ямі так:

    Заўядзенне дэмаверы

    Трэбуецца Python 3.10 або новейшая версія, а для стварэння падсумкаў — адказнасць у OpenAI або AWS. Дэмавера можа працаваць без адказнасці, калі пэраметр LLM_PROVIDER застаецца порожнім: субтыты і зберажаная транскрыпцыя будуць працаваць, але знікнуць толькі падсумкі для наздаганяння.

    Пачніце з клонавання рэпазітарыя:

    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
    

    Далей зменіце файл .env, каб выбраць прадаўца транскрыпцыі (STT_PROVIDER) і прадаўца стасумарызаціі (LLM_PROVIDER):

    # 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
    

    Ачыніце http://localhost:8080, увядзіце назву і прыўяжыцеся, пасля чаго паведзіце каліматы на канікуле, і тады пачне заполняцца транскрыпцыя. Завантажыце сторанку ў іншай вікніце пад іншай назвай, і чыраўы панель з’явіцца за час, падтварыўшы тое, што было сказана ў першай вікніце. Полны лог з часовымі пазначкамі зберагаецца пад падметам transcripts/.

    У AWS крантыцыям трэба разрэшэння на вікорыстоўванне Amazon Transcribe і Amazon Bedrock, напрыклад, праз гэтую політыку:

    {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "TranscriberSummarizer",
                "Effect": "Allow",
                "Action": [
                    "transcribe:StartStreamTranscription",
                    "bedrock:InvokeModel",
                    "bedrock:InvokeModelWithResponseStream"
                ],
                "Resource": "*"
            }
        ]
    }
    

    Входныя точкі для агента

    Агент LiveKit — гэта функцыя, якая запускаецца адна раз пры кожнай кімнаце. Яна рэгіструецца на AgentServer, а рэшальнікы командной лініі займаюцца рэштой:

    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)
    

    Заўважыце, што @server.rtc_session() не мае атрыбута agent_name. Як толькі агенту даецца назва, LiveKit пачынае чакаць на явны запрос на адправку дадзеных. Без назвы агент автаматычна прыўязываецца да кожнай новай кімнаты, што падходзіць для перапісвача.

    Пераклад мовы на тэкст за схованкаю

    Ёнколі трэба змяніць прадавца, выбор ховаецца за адной маленькай функцыяй. Кожны плагін LiveKit для перакладу мовы на тэкст, чы то Vosk, OpenAI чы AWS, аддае тую ж самую інтарфейсную прамову .stream(), таму самэ гэта месца ў агенце відпавядае за назначэнне прадавца:

    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}")
    

    Імпорты знаходзяцца ў кожнай гілцы, таму той, хто выбірае AWS, інсталюе толькі livekit-plugins-aws, адколі іншыя гілкі ніколі не запускаюцца. Для Vosk параметр partial_results=False абырае проміжныя працэзналежніцы слоў і заставляе толькі готавыя рэчэнні.

    Одны распазнаўач выкорыстоўваецца всіма спікерамі, а працоўнік запускаецца, калі падпішося на аудыя-тракт:

    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))
    

    Кожны выкладчык атрыбутуецца сае власным аудыя- і STT-стрэмамі, пры чым два задання выконваюцца адночасова за дапамою asyncio.gather: падача фрэмав і чытанне транскрыпцый.

    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())
    

    feed_audio дадзеўае фрэмы ў распазнавач, тады як emit_transcripts выкарыстоўвае ўжо завершаныя рэчэнні, а блок async with займаецца чысткай. Код рэагуе толькі на запуск зменных FINAL_TRANSCRIPT, якія ўключаюць цэлыя рэчэнні, а не кожную прызмойныя дагадкі. Усё, што ён прыме, прабрацоўваецца функцыяй record_line, якая зберагае тэкст на дыску, адправляе яго ў браузер і запамячае для пазнейшых статыстык.

    Запіс кожнай лініі

    record_line ператварае завершаны выказваў у стойкі элемент. Яна дадае яго да файла на дыску, зберагае копію ў памяці для статыстык і адправляе тэкст у браузер:

    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
    

    Для крока публікацыяй не трэбуе спецыяльнага канала. Вбудованая функцыя транскрыпціі LiveKit, адзначаная тэмай lk.transcription, выконвае гэту задачу. Калі sender_identity настаўленая на спаклікача, кожны кліент LiveKit прымець тэкст як звычны, правільна атрыбутаваны фрагмент транскрыпціі.

    Апুсткі для тых, хто запаздзеў

    Гэта самая цікавая функцыя. Калі хтось прыўязваецца да ўжо трываючага калу, агент стварыць апুстку пра тое, што ён праспеў, і надасіць яе толькі ёму. LiveKit выклікае сігнал participant_connected праз кожны прыўязок:

    @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],
        )
    

    LLM таксама знаходзіцца ўнутрь завода. Кожны LiveKit LLM адкрывае той самы метод .chat(), таму метод summarize() ніколі не робіць развітання:

    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()
    

    SUMMARY_PROMPT — гэта адаптаваная лінія, якая прасіць запісаць трохі з чатырма абзацамі паведамленняў. Функцыя .to_str_iterable() ператварае потокавыя адпаведзенні на часткі тэксту, якія пазьменююцца разам; гэта дзеянне адбываецца так сама для кожнага прадавця і без спецыяльных SDK-ў кожнага з ягох.

    Две дэталі спакойваюць процэс адправкі. Перша — чаканне двух секундаў запобегае конфліктам: функцыя participant_connected можа быць вызваная яшчэ до таго, калі браузер новага учасніка настаўіць своіх слухачоў. Другая — адправка паведамлення з выкарыстоўваннем destination_identities гарантуе, што паведамленне будзе прынята толькі адной особе, таму всі іншыя учаснікі застаюцца без втручання.

    Калі оба плагіні беруцца з LiveKit, адміністратор паведамляе даныя для розпізнавання мовы і LLM у ўсіх пакетах за допамогою однаго набору аутэнтыфікацыйных даных. За дапамогою openai адна OPENAI_API_KEY відпаведае як за транскрыпцыю, так і за стварэнне підсумкав. За дапамогою aws одна пара ключоў IAM падтрымлівае як функцыю Transcribe, так і Bedrock.

    Таксама можліва настройка без падключэння да інтэрнэту і безкоштовна: для перакладу на мову вікорыстоўваецца vosk, а поле LLM_PROVIDER застаецца порожнім. У такым разе транскрыпцыя выконваецца локальна, а функцыя стварэння підсумкав выключаецца, і ёй заменяецца цэлы тэкст транскрыпцыі.

    Бок браузера

    Фронтэнд спецыяльна просты: адна файл HTML, браузерны SDK LiveKit з CDN і некалькі файлоў JavaScript, без вікорыстання якоўых-небудзь фрэймворкаў чы бандлероў.

    Браузеры не можаць ствараць своія сабе токены доступу, адколькі гэта прызведзе да таго, што секрэт API стане відкрытым для кожнага візітара. Невялікі сервер токена, створаны на базе стандартнай бібліятэкі Python і livekit-api, заполняе гэты прыём:

    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);
    

    Адказ працэйкі значыць падпіску на зарэзерваваны LiveKit тэкстовы стрым lk.transcription. Адпрацоўвальнік атрымле чытача плюс інфармацыю пра адправальніка, якая паводзіць імя спікера, а атрыбут пад назвай lk.transcription_final дозволяе яму ігнараваць тэкст, які ўсё шчэ не завершаны:

    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());
    });
    

    Паколькі гэты стандартны канал, той самы працоўнік функцыонуе ў будзь-ям кліенце LiveKit. Кароткі паветарак прыходзіць па спецыяльным топіку summary, і калі ён прыбывае, сторанка выкладзе яго ў падсвечаныя рамкі. Гэта і ёсць весь кліент.

    Што вы у канцыку отрымаеце

    Готавы агент можа обрабатваць ўсё — ад чата з двума людзьмі да цэлага сабеседавання. Ён запісвае чысты транскрыпт на дыск, паказвае прыемныя тэксты ў рэальнам часе і дае тым, хто запазніўся, кароткі паветарак, усё гэта прыблізна ў дваста роўнакоў. Ёго можна запрацаваць з Vosk, OpenAI або AWS, а адна настройка .env дазволяе перыходзіць между ўсімі імі, таму што обе часткі абгорнутыя ў фабрыкі make_stt() і make_llm(). Вочныя наступныя крокі — гэта зберагчыць транскрыпты, адрабатваць пераклад у рэальнам часе або ператвараць паветракі на конкрэтныя заведамы.

    Ён застаецца коракі, таму што складныя часткі ужо створэны: транспорт аудыя ў рэальны час, падпіскі на тракі і шары кантакту з прадаўцамі AI. LiveKit Agents і яго плагіны забезпечваюць неабходныя елементы, таму код фокусуецца на логіке агента, а не на тэхнічных дэталях.

    Для прыменення ў практыцы самастойна адрэсаваная платформа, створаная на тым жа ядрэ, сумесным з LiveKit, дазволяе агенту працаваць без змян, тады калі платформа адпаведзяе за автаскалінг, высокую доступнасць, рэлейную перадачу даных праз TURN для сетак з обмежэннямі, кераванне запісямі і стварэнне можлівасцей аналізу, усё гэта внутрь вашай сопрацоўной інфраструктуры, дзе даны застаюцца прыватнымі, а віткі не залежнаць ад цэн на SaaS за хвіліну.