Добавлен полный пайплайн выгрузки записей разговоров из 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). Все звонки без + в номерах телефонов.
102 lines
3.2 KiB
Python
102 lines
3.2 KiB
Python
import os
|
|
import shutil
|
|
import subprocess
|
|
from typing import Optional
|
|
|
|
|
|
def _find_ffmpeg() -> str:
|
|
path = shutil.which("ffmpeg")
|
|
if path:
|
|
return path
|
|
winget_path = os.path.join(
|
|
os.environ.get("LOCALAPPDATA", ""),
|
|
"Microsoft", "WinGet", "Links", "ffmpeg.exe"
|
|
)
|
|
if os.path.exists(winget_path):
|
|
return winget_path
|
|
alt_paths = [
|
|
r"C:\ffmpeg\bin\ffmpeg.exe",
|
|
r"C:\Program Files\ffmpeg\bin\ffmpeg.exe",
|
|
]
|
|
for p in alt_paths:
|
|
if os.path.exists(p):
|
|
return p
|
|
raise RuntimeError("FFmpeg not found. Install it via: winget install ffmpeg")
|
|
|
|
|
|
def get_audio_channels(input_path: str) -> int:
|
|
ffmpeg_path = _find_ffmpeg()
|
|
cmd = [
|
|
ffmpeg_path, "-i", input_path,
|
|
"-f", "null", "-"
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
stderr = result.stderr
|
|
for line in stderr.split("\n"):
|
|
if "Audio:" in line:
|
|
if "stereo" in line or "2 channels" in line:
|
|
return 2
|
|
elif "mono" in line or "1 channel" in line:
|
|
return 1
|
|
import re
|
|
m = re.search(r"(\d+)\s+channels?", line)
|
|
if m:
|
|
return int(m.group(1))
|
|
return 2
|
|
|
|
|
|
def get_audio_duration(input_path: str) -> Optional[float]:
|
|
ffprobe = _find_ffmpeg().replace("ffmpeg", "ffprobe")
|
|
if not os.path.exists(ffprobe):
|
|
return None
|
|
cmd = [ffprobe, "-v", "error", "-show_entries", "format=duration",
|
|
"-of", "csv=p=0", input_path]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode == 0 and result.stdout.strip():
|
|
try:
|
|
return int(float(result.stdout.strip()))
|
|
except ValueError:
|
|
return None
|
|
return None
|
|
|
|
|
|
def convert_to_wav(input_path: str, output_dir: str, sample_rate: int = 16000) -> tuple[Optional[str], int]:
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
base = os.path.splitext(os.path.basename(input_path))[0]
|
|
output_path = os.path.join(output_dir, f"{base}_16khz.wav")
|
|
|
|
channels = get_audio_channels(input_path)
|
|
ac = "2" if channels == 2 else "1"
|
|
|
|
ffmpeg_path = _find_ffmpeg()
|
|
cmd = [
|
|
ffmpeg_path, "-y",
|
|
"-i", input_path,
|
|
"-acodec", "pcm_s16le",
|
|
"-ac", ac,
|
|
"-ar", str(sample_rate),
|
|
output_path
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"FFmpeg error: {result.stderr}")
|
|
return output_path, channels
|
|
|
|
|
|
def split_stereo_channels(wav_path: str, output_dir: str) -> tuple[Optional[str], Optional[str]]:
|
|
base = os.path.splitext(os.path.basename(wav_path))[0]
|
|
left_path = os.path.join(output_dir, f"{base}_left.wav")
|
|
right_path = os.path.join(output_dir, f"{base}_right.wav")
|
|
ffmpeg_path = _find_ffmpeg()
|
|
|
|
cmd = [
|
|
ffmpeg_path, "-y",
|
|
"-i", wav_path,
|
|
"-filter_complex", "[0:a]pan=mono|c0=c0[left];[0:a]pan=mono|c0=c1[right]",
|
|
"-map", "[left]", left_path,
|
|
"-map", "[right]", right_path,
|
|
]
|
|
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
if result.returncode != 0:
|
|
raise RuntimeError(f"FFmpeg split error: {result.stderr}")
|
|
return left_path, right_path
|