Приветствую! Меня зовут Григорий, и я главный по спецпроектам в команде AllSee. В современном мире искусственный интеллект стал незаменимым помощником в различных сферах нашей жизни. Однако, я верю, что всегда нужно стремиться к большему, автоматизируя все процессы, которые возможно. В этой статье я поделюсь опытом использования Whisper и ChatGPT для создания ИИ‑секретаря, способного оптимизировать хранение и обработку корпоративных созвонов.
Мотивация
Не только очевидно, но и научно доказано, что использование ИИ в рабочем процессе повышает продуктивность: для белых воротничков до 37%!
Давайте и мы попробуем «выбить» наши проценты продуктивности, автоматизировав рутинную задачу просмотра записей созвонов.
Какие вводные?
Есть достаточно большая база созвонов, которые иногда приходится пересматривать, расходуя по 30–40 минут на одну запись, а для некоторых видео, особенно для интервью или сбора требований, обработка может занимать даже больше времени, чем сама встреча.
Я хочу повысить эффективность данного процесса, извлекая транскрипции встреч с выделением участников для быстрого просмотра текста, а также уточняя конкретные вопросы у ИИ‑ассистента. Для взаимодействия с нашей системой я буду использовать Telegram‑бота.
Решение
Техническая структура решения включает в себя:
Телеграмм бота на базе python‑telegram‑bot
Локальный сервер telegram‑bot‑api
Локальный API WhisperX
Базу данных SQLite
Прокси‑сервер
Я опущу многие детали, связанные с UX бота, но постараюсь затронуть важные моменты, с которыми вам точно придётся иметь дело при разработке и интеграции подобного проекта.
Sqiud Proxy
Для работы с ChatGPT API из России нам потребуется настроить proxy‑сервер. Я буду использовать Squid — кэширующий прокси‑сервер для протоколов HTTP, HTTPS, FTP и Gopher.
Инструкция по настройке Squid Proxy
Описывается установка на сервер с OS Ubuntu, но и для других дистрибутивов Linux инструкция будет похожей, отличаясь только используемым менеджером пакетов.
Установка squid
sudo apt update
sudo apt-get -y install squid
Преднастройка сети
sudo ufw allow squid
sudo iptables -P INPUT ACCEPT
Активация сервиса squid
sudo systemctl enable squid
Настройка squid
Создаём бекап конфигурации squid и удаляем из оригинального файла всё 8000 строк комментариев.
sudo cp /etc/squid/squid.conf /etc/squid/squid_back.conf
sudo grep -v '^ *#\|^ *$' /etc/squid/squid.conf > /etc/squid/squid.conf
Открываем файл конфигурации squid.
sudo apt-get -y install nano
sudo nano /etc/squid/squid.conf
Файл squid.conf необходимо изменить следующим образом:
...
http_access allow localhost
acl whitelist src 111.111.111.111 # Добавляем IP адрес нашего основного сервера (нужно указать реальный IP) в список доступа
http_access allow whitelist # Разрешаем основному серверу использовать наш прокси
http_access deny all
...
Перезапуск cервиса squid
Для применения наших настроек достаточно просто перезапустить squid.
sudo systemctl restart squid
WhisperX
Для решения поставленной задачи недостаточно просто транскрибировать текст, требуется также решать задачу диаризации: выделять из аудиопотока отдельных дикторов. Задача осложняется тем, что в переговорах и созвонах могут участвовать несколько собеседников, а многие алгоритмы ограничены 2-мя участниками разговора.
На помощь нам приходит WhisperX — «обёртка» над стандартной моделью Whisper для транскрибации, выделения временных меток отдельных слов, а также диаризации до 100 собеседников.
Для интеграции WhisperX в наше решение я написал небольшой FastAPI‑сервер, обрабатывающий входящие аудиофайлы.
Реализация и запуск сервера
Импорты и настройки
import asyncio
import os
import uuid
from dataclasses import dataclass
from datetime import datetime
from queue import Queue
from threading import Thread
from typing import Any
from fastapi import HTTPException, BackgroundTasks, FastAPI, status, Request
from fastapi.middleware.cors import CORSMiddleware
from pydantic_settings import BaseSettings
from streaming_form_data import StreamingFormDataParser
from streaming_form_data.targets import FileTarget
from streaming_form_data.validators import MaxSizeValidator
import whisperx
@dataclass
class WhisperXModels:
whisper_model: Any
diarize_pipeline: Any
align_model: Any
align_model_metadata: Any
class TranscriptionAPISettings(BaseSettings):
tmp_dir: str = 'tmp'
cors_origins: str = '*'
cors_allow_credentials: bool = True
cors_allow_methods: str = '*'
cors_allow_headers: str = '*'
whisper_model: str = 'large-v2'
device: str = 'cuda'
compute_type: str = 'float16'
batch_size: int = 16
language_code: str = 'auto'
hf_api_key: str = ''
file_loading_chunk_size_mb: int = 1024
task_cleanup_delay_min: int = 60
max_file_size_mb: int = 4096
max_request_body_size_mb: int = 5000
class Config:
env_file = 'env/.env.cuda'
env_file_encoding = 'utf-8'
class MaxBodySizeException(Exception):
def __init__(self, body_len: int):
self.body_len = body_len
class MaxBodySizeValidator:
def __init__(self, max_size: int):
self.body_len = 0
self.max_size = max_size
def __call__(self, chunk: bytes):
self.body_len += len(chunk)
if self.body_len > self.max_size:
raise MaxBodySizeException(self.body_len)
settings = TranscriptionAPISettings()
app = FastAPI()
# noinspection PyTypeChecker
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins.split(','),
allow_credentials=settings.cors_allow_credentials,
allow_methods=settings.cors_allow_methods.split(','),
allow_headers=settings.cors_allow_headers.split(','),
)
Логика транскрибации
trancription_tasks = {}
trancription_tasks_queue = Queue()
whisperx_models = WhisperXModels(
whisper_model=None,
diarize_pipeline=None,
align_model=None,
align_model_metadata=None
)
def load_whisperx_models() -> None:
global whisperx_models
whisperx_models.whisper_model = whisperx.load_model(
whisper_arch=settings.whisper_model,
device=settings.device,
compute_type=settings.compute_type,
language=settings.language_code if settings.language_code != "auto" else None
)
whisperx_models.diarize_pipeline = whisperx.DiarizationPipeline(
use_auth_token=settings.hf_api_key,
device=settings.device
)
if settings.language_code != "auto":
(
whisperx_models.align_model,
whisperx_models.align_model_metadata
) = whisperx.load_align_model(
language_code=settings.language_code,
device=settings.device
)
def transcribe_audio(audio_file_path: str) -> dict:
global whisperx_models
audio = whisperx.load_audio(audio_file_path)
transcription_result = whisperx_models.whisper_model.transcribe(
audio,
batch_size=int(settings.batch_size),
)
if settings.language_code == "auto":
language = transcription_result["language"]
(
whisperx_models.align_model,
whisperx_models.align_model_metadata
) = whisperx.load_align_model(
language_code=language,
device=settings.device
)
aligned_result = whisperx.align(
transcription_result["segments"],
whisperx_models.align_model,
whisperx_models.align_model_metadata,
audio,
settings.device,
return_char_alignments=False
)
diarize_segments = whisperx_models.diarize_pipeline(audio)
final_result = whisperx.assign_word_speakers(
diarize_segments,
aligned_result
)
return final_result
def transcription_worker() -> None:
while True:
task_id, tmp_path = trancription_tasks_queue.get()
try:
result = transcribe_audio(tmp_path)
trancription_tasks[task_id].update({"status": "completed", "result": result})
except Exception as e:
trancription_tasks[task_id].update({"status": "failed", "result": str(e)})
finally:
trancription_tasks_queue.task_done()
os.remove(tmp_path)
Логика FasAPI
@app.on_event("startup")
async def startup_event() -> None:
os.makedirs(settings.tmp_dir, exist_ok=True)
load_whisperx_models()
Thread(target=transcription_worker, daemon=True).start()
async def cleanup_task(task_id: str) -> None:
await asyncio.sleep(settings.task_cleanup_delay_min * 60)
trancription_tasks.pop(task_id, None)
@app.post("/transcribe/")
async def create_upload_file(
request: Request,
background_tasks: BackgroundTasks
) -> dict:
task_id = str(uuid.uuid4())
tmp_path = f"{settings.tmp_dir}/{task_id}.audio"
trancription_tasks[task_id] = {
"status": "loading",
"creation_time": datetime.utcnow(),
"result": None
}
body_validator = MaxBodySizeValidator(settings.max_request_body_size_mb * 1024 * 1024)
try:
file_target = FileTarget(
tmp_path,
validator=MaxSizeValidator(settings.max_file_size_mb * 1024 * 1024)
)
parser = StreamingFormDataParser(headers=request.headers)
parser.register('file', file_target)
async for chunk in request.stream():
body_validator(chunk)
parser.data_received(chunk)
except MaxBodySizeException as e:
raise HTTPException(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
detail=f"Maximum request body size limit exceeded: {e.body_len} bytes"
)
except Exception as e:
if os.path.exists(tmp_path):
os.remove(tmp_path)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Error processing upload: {str(e)}"
)
if not file_target.multipart_filename:
if os.path.exists(tmp_path):
os.remove(tmp_path)
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail='No file was uploaded'
)
trancription_tasks[task_id].update({"status": "processing"})
trancription_tasks_queue.put((task_id, tmp_path))
background_tasks.add_task(cleanup_task, task_id)
return {
"task_id": task_id,
"creation_time": trancription_tasks[task_id]["creation_time"].isoformat(),
"status": trancription_tasks[task_id]["status"]
}
@app.get("/transcribe/status/{task_id}")
async def get_task_status(task_id: str) -> dict:
task = trancription_tasks.get(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
return {
"task_id": task_id,
"creation_time": task["creation_time"],
"status": task["status"]
}
@app.get("/transcribe/result/{task_id}")
async def get_task_result(task_id: str) -> dict:
task = trancription_tasks.get(task_id)
if not task:
raise HTTPException(status_code=404, detail="Task not found")
if task["status"] == "pending":
raise HTTPException(status_code=404, detail="Task not completed")
return {
"task_id": task_id,
"creation_time": task["creation_time"],
"status": task["status"],
"result": task["result"]
}
Dockerfile
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu20.04
RUN apt-get update && apt-get install -y software-properties-common
RUN add-apt-repository ppa:deadsnakes/ppa
RUN apt-get update && apt-get install -y python3.10 python3.10-venv python3-pip ffmpeg
WORKDIR /whisperx-fastapi
COPY . .
RUN python3.10 -m venv venv
RUN /bin/bash -c "source venv/bin/activate && pip install --upgrade pip"
RUN /bin/bash -c "source venv/bin/activate && pip install -e ."
RUN /bin/bash -c "source venv/bin/activate && pip install -r fastapi/requirements-fastapi-cuda.txt"
WORKDIR /whisperx-fastapi/fastapi
EXPOSE 8000
CMD ["../venv/bin/uvicorn", "api.app:app", "--host", "0.0.0.0", "--port", "8000"]
Запуск сервера
Для развёртывания сервера достаточно скопировать репозиторий, указать в ./fastapi/env/.env.cuda
токен доступа HuggingFace, собрать и запустить контейнер Docker.
sudo docker build -f fastapi/dockerization/dockerfile.fastapi.cuda -t whisperx-fastapi-cuda .
sudo docker run -p 8000:8000 --env-file ./fastapi/env/.env.cuda --gpus all --name whisperx-fastapi-cuda-container whisperx-fastapi-cuda
Telegram Bot API
Большой проблемой стало ограничение для Telegram‑ботов на обработку файлов более 50мб, что в контексте обработки голосовых и видеозаписей часовых созвонов — смешные цифры.
Данную проблему можно решить достаточно просто: развернуть свой локальный API для Telegram‑бота с помощью telegram‑bot‑api, что позволит нам обрабатывать файлы уже до 2000мб. При этом вся информация бота будет храниться прямо на нашей машине, что позволит быстрее работать с файлами, не дожидаясь получения multipart c сервера.
Как это можно сделать?
На самом деле, очень просто:
apt-get install -y --no-install-recommends \
build-essential \
libssl-dev \
zlib1g-dev \
git \
cmake \
gperf \
g++
git clone --recursive https://github.com/tdlib/telegram-bot-api.git && \
cd telegram-bot-api && \
mkdir build && \
cd build && \
cmake .. && \
cmake --build . --target install
cd telegram-bot-api/build
./telegram-bot-api --api-id=${TELEGRAM_API_ID} --api-hash=${TELEGRAM_API_HASH} --local
TELEGRAM_API_HASH — Hash приложения Telegram
TELEGRAM_BOT_TOKEN — Токен бота Telegram
Python Telegram Bot
Ну и, наконец, для обработки запросов от пользователей я использовал python‑telegram‑bot.
Ничего особенного в логике работы я не выделю, поэтому остановимся лишь на интеграции уже готового Telegram‑бота в наше решение.
Развёртывание Telegram-бота
Загрузка репозитория
git clone https://github.com/allseeteam/ai-secretary.git
Сборка контейнера
docker build --build-arg HTTP_PROXY=${HTTP_PROXY} --build-arg HTTPS_PROXY=${HTTPS_PROXY} --build-arg NO_PROXY=${NO_PROXY} -t ai-secretary .
HTTP_PROXY — Адрес прокси-сервера для обхода географических ограничений
HTTPS_PROXY — См. HTTP_PROXY
NO_PROXY — Адреса, запросы на которые мы будем отправлять без прокси
Создание docker volume для sqlite базы данных
docker volume create ai_secretary_sqlite_db
Запуск контейнера
docker run -d --network host --volume ai_secretary_sqlite_db:/ai-secretary/database --env-file env/.env --name ai-secretary-container ai-secretary
TELEGRAM_API_ID — ID приложения Telegram
TELEGRAM_API_HASH — Hash приложения Telegram
TELEGRAM_BOT_TOKEN — Токен бота Telegram
TELEGRAM_BOT_API_BASE_URL — Базовый адрес сервера вашего бота (для локального сервера: http://localhost:8081/bot)
OPENAI_API_KEY — Токен OpenAI
SQLITE_DB_PATH — Путь по которому мы хотим хранить нашу SQLite базу данных (стандартный адрес: bot/database/ai-secretary.db)
TRANSCRIPTION_API_BASE_URL — Базовый адрес сервера для транскрибации (развернуть сервер можно по инструкциям из репозитория, для локального сервера адрес (для данного кейса обязательно при запуске образа указываем --network host): http://127.0.0.1:8000)
Результат
Как видно, наш ИИ‑секретарь без проблем обрабатывает входящие видеофайлы, а также обсуждает их содержимое с пользователем, учитывая содержание записи и контекст прошлых вопросов.
Заключение
С помощью ChatGPT, Whisper и капли «прогерской магии» нам удалось повысить продуктивность работы с корпоративной базой данных и избавиться от необходимости просматривать часовые видео созвонов, чтобы освежить память перед работой.
А ещё крутая новость для всех любителей «потыкать»: в течение следующей недели бот будет доступен всем желающим для ознакомления и работы. Если у вас возникнут проблемы или предложения по функционалу бота — свободно пишите по контактам в описании.
Всю кодовую базу вы можете найти в репозитории проекта. Если кто‑то вдохновиться проектом и решит модифицировать наши наработки — буду рад принять ваш pull request.
А какие моменты конкретно вашей работы просят автоматизации? Делитесь идеями и мыслями в комментариях — с нетерпением обсужу их вместе с вами. Будьте хорошими людьми, а всё остальное оставьте машинам. Удачи и будем на связи✌️
Комментарии (8)
RazrabRazrabich
01.04.2024 10:53Какое железо стоит для распознавания ? У меня для large модели скорость 1 к 1(
allseeteam Автор
01.04.2024 10:53Сейчас на nvidia v100 работает large‑модель. Есть в планах поэксперементировать и запустить пайплайн транскрибации и диаризации на «дешёвом» CPU, чтобы мы смогли оставить доступ к боту открытым навсегда
theurus
01.04.2024 10:53+1Whisper medium на i9-11600 5 минут аудио распознает 2.5 минуты напрягая все ядра
xmdy
А оно уже научилось хорошо распознавать людей на русском языке? Какое-то время назад русский слабо поддерживался.
f_s_b_37
Вчера только распознавал корпоративный трындеж в довольно плохом качетсве через whisper (whisper.cpp) large-v3. Работало удивительно хорошо, включая вкропления иностранных слов, нелогизмы и неидеальную дикцию.
И это на записи слушать которую самому откровенно больно -там к части людей приходилось прислушиваться чтобы хотябы примерно распознать о чем речь.
allseeteam Автор
Да, на русском достаточно хорошо работает на модели large. Правда, как я уже писал, используется WhisperX — там свои доработки есть