Добавлен полный пайплайн выгрузки записей разговоров из 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). Все звонки без + в номерах телефонов.
81 lines
2.7 KiB
Python
81 lines
2.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
import sys, httpx
|
|
from dotenv import load_dotenv
|
|
import os
|
|
|
|
load_dotenv()
|
|
|
|
DIRECTUS_URL = os.getenv("DIRECTUS_URL", "http://localhost:8055").rstrip("/")
|
|
DIRECTUS_TOKEN = os.getenv("DIRECTUS_TOKEN", "")
|
|
|
|
HEADERS = {
|
|
"Authorization": f"Bearer {DIRECTUS_TOKEN}",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
client = httpx.Client(base_url=DIRECTUS_URL, headers=HEADERS, timeout=30)
|
|
|
|
COLLECTIONS = ["calls", "calls_74951284933"]
|
|
|
|
|
|
def get_all(collection: str, fields: str) -> list:
|
|
items = []
|
|
offset = 0
|
|
limit = 200
|
|
while True:
|
|
resp = client.get(
|
|
f"/items/{collection}",
|
|
params={"fields": fields, "limit": limit, "offset": offset, "sort": "id"},
|
|
)
|
|
if resp.status_code != 200:
|
|
print(f" ERROR fetching {collection}: {resp.status_code}", file=sys.stderr)
|
|
break
|
|
data = resp.json().get("data", [])
|
|
if not data:
|
|
break
|
|
items.extend(data)
|
|
offset += limit
|
|
return items
|
|
|
|
|
|
def main():
|
|
contacts_map = {}
|
|
contacts = get_all("contacts", "id,phone_number,name,company")
|
|
for c in contacts:
|
|
phone = (c.get("phone_number") or "").strip()
|
|
if phone:
|
|
contacts_map[phone] = c
|
|
print(f"Loaded {len(contacts_map)} contacts.")
|
|
|
|
total_updated = 0
|
|
for col in COLLECTIONS:
|
|
calls = get_all(col, "id,call_id,caller_number,callee_number,caller_name,callee_name")
|
|
print(f" {col}: {len(calls)} calls")
|
|
for call in calls:
|
|
updates = {}
|
|
for side in ("caller", "callee"):
|
|
num = call.get(f"{side}_number") or ""
|
|
contact = contacts_map.get(num)
|
|
if contact:
|
|
name = contact.get("name") or ""
|
|
company = contact.get("company") or ""
|
|
label = f"{name} ({company})" if name and company else (name or company)
|
|
if label and call.get(f"{side}_name") != label:
|
|
updates[f"{side}_name"] = label
|
|
if updates:
|
|
try:
|
|
resp = client.patch(f"/items/{col}/{call['id']}", json=updates)
|
|
if resp.status_code in (200, 201):
|
|
total_updated += 1
|
|
print(f" Updated {col} id={call['id']}: {updates}")
|
|
else:
|
|
print(f" FAILED {col} id={call['id']}: {resp.status_code} {resp.text[:200]}", file=sys.stderr)
|
|
except Exception as e:
|
|
print(f" ERROR {col} id={call['id']}: {e}", file=sys.stderr)
|
|
|
|
print(f"\nUpdated {total_updated} calls.")
|
|
client.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|