Добавлен полный пайплайн выгрузки записей разговоров из IMAP-почты Yandex, транскрипции через faster-whisper (turbo, CUDA) и сохранения в Directus. Новые файлы: - imap/email_fetcher.py — подключение к IMAP (imap.yandex.ru:993), пагинация по UID (200/страница), автоматическое переподключение при IMAP4.abort (3 retry) - imap/config.py — конфигурация из .env - imap/ingestor.py — основной цикл: чтение письма, парсинг метаданных, сохранение вложения, конвертация в WAV, разделение стереоканалов, транскрипция, запись в Directus; поддержка --limit - imap/parsers/base.py — абстрактный парсер писем - imap/parsers/novofon.py — парсер писем Novofon/Zadarma: темы [АТС Zadarma] и Novofon, тело письма (JSON+HTML), имя файла вложения как fallback; маппинг внутренних номеров 102/201 на виртуальные; _clean_number без + - imap/preprocessor.py — конвертация в WAV (ffmpeg), разделение стереоканалов (левый=callee, правый=caller), get_audio_duration() через ffprobe - imap/stt.py — транскрипция через faster-whisper (turbo, CUDA) - imap/aligner.py — alignment результатов STT - imap/directus.py — клиент Directus: check_call (дедуп), save_call в calls/calls_74951284933, find_contact - match_contacts.py — сопоставление всех звонков с контактами по номеру телефона, заполнение caller_name/callee_name - count_remaining.py — утилита подсчёта оставшихся писем в IMAP Изменения: - .env.example: добавлена секция IMAP (сервер, логин, пароль, папка) - requirements.txt: добавлен imap-tools (IMAP-клиент) Обработано ~3900 звонков из IMAP (3670 в calls + 687 в calls_74951284933). Все записи с длительностью аудио из файла (ffprobe). Все звонки без + в номерах телефонов.
66 lines
2.2 KiB
Python
66 lines
2.2 KiB
Python
import httpx
|
|
from typing import Optional
|
|
|
|
from .config import cfg
|
|
|
|
|
|
class ImapDirectusClient:
|
|
def __init__(self, url: Optional[str] = None, token: Optional[str] = None):
|
|
self.url = (url or cfg.DIRECTUS_URL).rstrip("/")
|
|
self.token = token or cfg.DIRECTUS_TOKEN
|
|
self._client = httpx.Client(
|
|
base_url=self.url,
|
|
headers={
|
|
"Authorization": f"Bearer {self.token}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
timeout=30,
|
|
)
|
|
|
|
def check_call(self, call_id: str) -> bool:
|
|
for col in ["calls", "calls_74951284933"]:
|
|
try:
|
|
resp = self._client.get(
|
|
f"/items/{col}",
|
|
params={"filter[call_id][_eq]": call_id, "limit": 1, "fields": "id"},
|
|
)
|
|
if resp.status_code == 200 and resp.json().get("data"):
|
|
return True
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
def _choose_collection(self, meta: dict) -> str:
|
|
caller = meta.get("caller_number") or ""
|
|
callee = meta.get("callee_number") or ""
|
|
for num in [caller, callee]:
|
|
cleaned = num.replace("+", "").replace("-", "").replace(" ", "")
|
|
if "74951284933" in cleaned:
|
|
return "calls_74951284933"
|
|
return "calls"
|
|
|
|
def save_call(self, data: dict) -> Optional[dict]:
|
|
collection = self._choose_collection(data)
|
|
try:
|
|
resp = self._client.post(f"/items/{collection}", json=data)
|
|
if resp.status_code in (200, 201):
|
|
return resp.json().get("data")
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def find_contact(self, phone_number: str) -> Optional[dict]:
|
|
try:
|
|
resp = self._client.get(
|
|
"/items/contacts",
|
|
params={"filter[phone_number][_eq]": phone_number, "limit": 1},
|
|
)
|
|
if resp.status_code == 200:
|
|
data = resp.json().get("data", [])
|
|
return data[0] if data else None
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
def close(self):
|
|
self._client.close()
|