Записывать звонки в Asterisk просто. А вот получать из этих записей реальную пользу — задача, на которой спотыкается большинство команд.
Если вы уже настроили MixMonitor или Monitor в диалплане, у вас под рукой золотая жила данных. Проблема в том, что в большинстве инсталляций Asterisk записи разговоров воспринимают как галочку в чек-листе комплаенса: файлы лежат в /var/spool/asterisk/monitor/, и к ним никто не притрагивается, пока не возникнет спорная ситуация.
В этом руководстве разбираем, как превратить сырые записи разговоров в структурированную аналитическую систему. Вы узнаете, как организовать хранение для быстрого поиска, связать записи с метаданными CDR, построить дашборды, которые сами показывают проблемы с качеством, и даже добавить речевую аналитику на базе ИИ с помощью open-source инструментов.
Зачем нужна аналитика записей разговоров
Запись разговоров без аналитики — это как установить камеры видеонаблюдения и никогда не смотреть записи.
Что даёт аналитика записей:
| Сценарий | Без аналитики | С аналитикой |
|---|---|---|
| Контроль качества | Выборочные проверки (охват 2–5%) | Системная оценка 100% звонков |
| Обучение операторов | Субъективная обратная связь по памяти | Обучение на данных с конкретными примерами |
| Проверка комплаенса | Ручной разбор, когда пришла жалоба | Автоматическая пометка нарушений |
| Понимание клиентов | Разрозненные наблюдения | Анализ трендов по тысячам обращений |
| Разбор спорных ситуаций | Ручной поиск по файловой системе | Мгновенный доступ по метаданным звонка |
Компании, использующие аналитику записей разговоров, сообщают об улучшении показателя решения вопроса с первого звонка на 23% и сокращении среднего времени обработки на 18% уже в первом квартале после внедрения (ICMI Contact Center Research, 2024).
Шаг 1. Фундамент: настройка записи
Прежде чем строить аналитику, нужна надёжная основа для записи. В Asterisk есть два основных приложения:
MixMonitor против Monitor
| Характеристика | MixMonitor | Monitor |
|---|---|---|
| Сведение аудио | Пишет обе стороны в один файл | Отдельные файлы по направлениям (или сведённый) |
| Влияние на производительность | Лёгкий (рекомендуется) | Тяжелее, возможны проблемы со звуком |
| Управление во время разговора | Динамический старт/стоп/пауза | Ограниченное управление |
| Форматы записи | WAV, WAV49, GSM, SLN, SLIN | WAV, WAV49, GSM |
| Требования к каналу | Работает на отвеченных каналах | Работает на отвеченных каналах |
| Рекомендуется для | Промышленных колл-центров | Legacy-инсталляций |
Используйте MixMonitor. Это современный вариант, готовый к продакшену.
Базовый диалплан с MixMonitor
; extensions.conf — Recording with structured filenames
[macro-record-call]
exten => s,1,NoOp(Starting call recording)
same => n,Set(RECORD_DIR=/var/spool/asterisk/monitor/${STRFTIME(${EPOCH},,%Y/%m/%d)})
same => n,System(mkdir -p ${RECORD_DIR})
same => n,Set(RECORD_FILE=${RECORD_DIR}/${STRFTIME(${EPOCH},,%Y%m%d-%H%M%S)}-${UNIQUEID}-${CALLERID(num)}-${EXTEN})
same => n,Set(CDR(recordingfile)=${RECORD_FILE}.wav)
same => n,MixMonitor(${RECORD_FILE}.wav,b)
same => n,MacroExit()Ключевые моменты этого диалплана:
- —Каталоги по датам (
YYYY/MM/DD) не дают файловой системе тормозить из-за тысяч файлов в одной папке - —Структурированные имена файлов содержат метку времени, уникальный ID, номер звонящего и внутренний номер — это критично для аналитики
- —Связь с CDR через
CDR(recordingfile)привязывает запись к детализации звонков
Настройка записи в FreePBX
Если вы работаете в FreePBX, запись настраивается отдельно для внутреннего номера, группы обзвона или очереди:
- —Перейдите в Admin → Extensions → [Extension] → Recording
- —Задайте политику записи: Force, Don't Care, Yes или No
- —Выберите Inbound External, Outbound External, Inbound Internal, Outbound Internal
FreePBX хранит записи в /var/spool/asterisk/monitor/ в формате:
{year}/{month}/{day}/{type}-{date}-{time}-{source}-{destination}-{uniqueid}.wav
Шаг 2. Архитектура хранения для аналитики
Промышленной системе записи в колл-центре нужен продуманный план хранения. При битрейте 64 кбит/с (G.711 WAV) один час записанных разговоров занимает примерно 28,8 МБ. Колл-центр на 50 операторов со средним разговорным временем 6 часов на оператора в день генерирует 8,6 ГБ в сутки, или 260 ГБ в месяц.
Калькулятор объёма хранилища
| Операторов | Ср. часов разговора/день | Хранилище в сутки | В месяц | В год |
|---|---|---|---|---|
| 10 | 5 | 1,4 ГБ | 43 ГБ | 516 ГБ |
| 25 | 5 | 3,6 ГБ | 108 ГБ | 1,3 ТБ |
| 50 | 6 | 8,6 ГБ | 260 ГБ | 3,1 ТБ |
| 100 | 6 | 17,3 ГБ | 518 ГБ | 6,2 ТБ |
| 200 | 6 | 34,6 ГБ | 1 ТБ | 12,4 ТБ |
Рекомендуемая структура хранения
/var/spool/asterisk/
└── monitor/
├── 2026/
│ ├── 01/
│ │ ├── 15/
│ │ │ ├── 20260115-093042-1705312242.1-2125551234-100.wav
│ │ │ └── ...
│ │ └── 16/
│ └── 02/
├── compressed/ # Archived recordings (MP3/Opus)
│ ├── 2025/
│ └── ...
└── transcripts/ # AI-generated transcripts
├── 2026/
└── ...Стратегия сжатия
Для архива WAV-файлы неоправданно велики. Настройте автоматическое сжатие:
#!/bin/bash
# compress-recordings.sh — Run daily via cron
# Compresses WAV recordings older than 7 days to Opus format
# Opus achieves 10:1 compression vs WAV with excellent quality
MONITOR_DIR="/var/spool/asterisk/monitor"
COMPRESS_DIR="$MONITOR_DIR/compressed"
DAYS_OLD=7
find "$MONITOR_DIR" -name "*.wav" -mtime +$DAYS_OLD \
-not -path "*/compressed/*" | while read wavfile; do
# Preserve directory structure
relative_path="${wavfile#$MONITOR_DIR/}"
opus_path="$COMPRESS_DIR/${relative_path%.wav}.opus"
mkdir -p "$(dirname "$opus_path")"
# Convert to Opus (high quality, ~1/10 the size)
ffmpeg -i "$wavfile" -c:a libopus -b:a 24k \
-application voip "$opus_path" 2>/dev/null
if [ $? -eq 0 ] && [ -f "$opus_path" ]; then
rm "$wavfile"
echo "Compressed: $relative_path"
fi
doneДобавьте в crontab:
# Run compression daily at 2 AM
0 2 * * * /usr/local/bin/compress-recordings.sh >> /var/log/recording-compression.log 2>&1Шаг 3. Связываем записи с метаданными CDR
Сырые записи становятся по-настоящему полезными, когда связаны с данными CDR (детализацией звонков). Эта связь позволяет делать запросы вроде «покажи все записи, где время ожидания превысило 60 секунд» или «найди звонки с этого номера клиента за последние 30 дней».
Схема БД для аналитики записей
-- Create a recordings metadata table that extends CDR
CREATE TABLE recording_metadata (
id SERIAL PRIMARY KEY,
uniqueid VARCHAR(64) NOT NULL,
linkedid VARCHAR(64),
recording_path TEXT NOT NULL,
recording_format VARCHAR(10) DEFAULT 'wav',
file_size_bytes BIGINT,
duration_seconds INTEGER,
silence_percentage DECIMAL(5,2),
caller_id VARCHAR(80),
destination VARCHAR(80),
queue_name VARCHAR(64),
agent VARCHAR(64),
direction VARCHAR(10), -- 'inbound', 'outbound', 'internal'
disposition VARCHAR(20),
recorded_at TIMESTAMP NOT NULL,
compressed BOOLEAN DEFAULT FALSE,
transcribed BOOLEAN DEFAULT FALSE,
transcript_path TEXT,
quality_score DECIMAL(3,1),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Indexes for common analytics queries
CONSTRAINT unique_recording UNIQUE (uniqueid, recording_path)
);
CREATE INDEX idx_recorded_at ON recording_metadata(recorded_at);
CREATE INDEX idx_agent ON recording_metadata(agent);
CREATE INDEX idx_queue ON recording_metadata(queue_name);
CREATE INDEX idx_caller ON recording_metadata(caller_id);
CREATE INDEX idx_disposition ON recording_metadata(disposition);
CREATE INDEX idx_direction ON recording_metadata(direction);Скрипт автоматического извлечения метаданных
#!/usr/bin/env python3
"""
recording_indexer.py — Scans recordings, extracts metadata, populates database.
Run every 15 minutes via cron.
"""
import os
import re
import wave
import subprocess
import psycopg2
from datetime import datetime
from pathlib import Path
DB_CONFIG = {
'host': 'localhost',
'database': 'asterisk',
'user': 'asterisk',
'password': 'your_password'
}
MONITOR_DIR = '/var/spool/asterisk/monitor'
FILENAME_PATTERN = re.compile(
r'(\d{8}-\d{6})-(\d+\.\d+)-(\d+)-(\d+)\.wav$'
)
def get_wav_duration(filepath):
"""Get duration in seconds from WAV file."""
try:
with wave.open(filepath, 'r') as wf:
frames = wf.getnframes()
rate = wf.getframerate()
return frames / float(rate)
except Exception:
return None
def detect_silence_percentage(filepath):
"""Detect percentage of silence in recording using sox."""
try:
result = subprocess.run(
['sox', filepath, '-n', 'stats'],
capture_output=True, text=True, timeout=30
)
# Parse sox stats for RMS level
for line in result.stderr.split('\n'):
if 'RMS lev dB' in line:
rms = float(line.split()[-1])
# Very rough: if RMS < -40dB, significant silence
if rms < -40:
return 80.0
elif rms < -30:
return 50.0
elif rms < -20:
return 20.0
return 5.0
except Exception:
return None
def index_recordings():
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
indexed = 0
for root, dirs, files in os.walk(MONITOR_DIR):
for filename in files:
if not filename.endswith('.wav'):
continue
filepath = os.path.join(root, filename)
# Skip already indexed
cur.execute(
"SELECT 1 FROM recording_metadata WHERE recording_path = %s",
(filepath,)
)
if cur.fetchone():
continue
# Extract metadata from filename
match = FILENAME_PATTERN.search(filename)
if match:
timestamp_str, uniqueid, caller, dest = match.groups()
recorded_at = datetime.strptime(timestamp_str, '%Y%m%d-%H%M%S')
else:
# Fallback: use file modification time
uniqueid = filename.replace('.wav', '')
recorded_at = datetime.fromtimestamp(os.path.getmtime(filepath))
caller = dest = None
duration = get_wav_duration(filepath)
file_size = os.path.getsize(filepath)
silence_pct = detect_silence_percentage(filepath)
cur.execute("""
INSERT INTO recording_metadata
(uniqueid, recording_path, file_size_bytes, duration_seconds,
silence_percentage, caller_id, destination, recorded_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON CONFLICT (uniqueid, recording_path) DO NOTHING
""", (uniqueid, filepath, file_size, duration,
silence_pct, caller, dest, recorded_at))
indexed += 1
conn.commit()
cur.close()
conn.close()
print(f"Indexed {indexed} new recordings")
if __name__ == '__main__':
index_recordings()Связь с данными CDR
Ключевое поле для join между записями и CDR — uniqueid:
-- Join recordings with CDR for complete call context
SELECT
r.recording_path,
r.duration_seconds,
r.silence_percentage,
c.src AS caller,
c.dst AS destination,
c.dcontext AS context,
c.billsec AS billable_seconds,
c.disposition,
c.accountcode,
c.userfield AS queue_name
FROM recording_metadata r
JOIN cdr c ON r.uniqueid = c.uniqueid
WHERE r.recorded_at >= CURRENT_DATE - INTERVAL '7 days'
ORDER BY r.recorded_at DESC;Шаг 4. Базовые SQL-запросы для аналитики записей
Когда записи связаны с данными CDR, можно строить полноценные аналитические дашборды.
Запрос 1. Суточный объём записей и хранилища
-- Daily recording stats: count, total duration, storage used
SELECT
DATE(recorded_at) AS day,
COUNT(*) AS total_recordings,
ROUND(SUM(duration_seconds) / 3600.0, 1) AS total_hours,
ROUND(SUM(file_size_bytes) / 1073741824.0, 2) AS storage_gb,
ROUND(AVG(duration_seconds), 0) AS avg_duration_sec,
ROUND(AVG(silence_percentage), 1) AS avg_silence_pct
FROM recording_metadata
WHERE recorded_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY DATE(recorded_at)
ORDER BY day DESC;Запрос 2. Анализ записей по операторам
-- Per-agent recording metrics: identify coaching opportunities
SELECT
c.dstchannel AS agent_channel,
SUBSTRING(c.dstchannel FROM 'SIP/(.+)-') AS agent,
COUNT(*) AS calls_recorded,
ROUND(AVG(r.duration_seconds), 0) AS avg_duration,
ROUND(AVG(r.silence_percentage), 1) AS avg_silence_pct,
SUM(CASE WHEN r.duration_seconds < 30 THEN 1 ELSE 0 END) AS short_calls,
SUM(CASE WHEN r.silence_percentage > 50 THEN 1 ELSE 0 END) AS high_silence_calls,
ROUND(AVG(c.billsec), 0) AS avg_billsec
FROM recording_metadata r
JOIN cdr c ON r.uniqueid = c.uniqueid
WHERE r.recorded_at >= CURRENT_DATE - INTERVAL '7 days'
AND c.disposition = 'ANSWERED'
GROUP BY c.dstchannel, SUBSTRING(c.dstchannel FROM 'SIP/(.+)-')
ORDER BY calls_recorded DESC;Запрос 3. Анализ покрытия записью
-- What percentage of calls are being recorded?
-- Identifies gaps in recording coverage
SELECT
DATE(calldate) AS day,
COUNT(*) AS total_calls,
COUNT(r.id) AS recorded_calls,
ROUND(COUNT(r.id)::DECIMAL / COUNT(*) * 100, 1) AS coverage_pct,
COUNT(*) - COUNT(r.id) AS missing_recordings
FROM cdr c
LEFT JOIN recording_metadata r ON c.uniqueid = r.uniqueid
WHERE c.calldate >= CURRENT_DATE - INTERVAL '7 days'
AND c.disposition = 'ANSWERED'
AND c.billsec > 5
GROUP BY DATE(calldate)
ORDER BY day DESC;Запрос 4. Детекция тишины — ищем проблемные звонки
-- Calls with excessive silence (potential quality issues)
-- High silence = hold without music, dead air, or connection problems
SELECT
r.recorded_at,
r.caller_id,
r.destination,
r.duration_seconds,
r.silence_percentage,
r.recording_path,
c.disposition,
c.userfield AS queue
FROM recording_metadata r
JOIN cdr c ON r.uniqueid = c.uniqueid
WHERE r.silence_percentage > 40
AND r.duration_seconds > 60
AND r.recorded_at >= CURRENT_DATE - INTERVAL '7 days'
ORDER BY r.silence_percentage DESC
LIMIT 20;Устали угадывать, что происходит в очередях?
Astervis даёт 30+ реалтайм-графиков, KPI операторов и CRM-интеграцию для вашего Asterisk PBX. On-premise. Установка за 5 минут. От $119/мес flat — без ограничения на операторов.
Запрос 5. Почасовая тепловая карта записей
-- Recording volume by hour and day of week
-- Identifies peak periods and potential capacity issues
SELECT
EXTRACT(DOW FROM recorded_at) AS day_of_week,
EXTRACT(HOUR FROM recorded_at) AS hour,
COUNT(*) AS recordings,
ROUND(AVG(duration_seconds), 0) AS avg_duration,
ROUND(SUM(file_size_bytes) / 1048576.0, 0) AS storage_mb
FROM recording_metadata
WHERE recorded_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY EXTRACT(DOW FROM recorded_at), EXTRACT(HOUR FROM recorded_at)
ORDER BY day_of_week, hour;Шаг 5. Речевая аналитика на базе ИИ
Современная аналитика записей не ограничивается метаданными. ИИ-речевая аналитика умеет автоматически расшифровывать записи и вытаскивать из них инсайты: тональность клиента, тематику обращения, соблюдение скриптов и требований комплаенса.
Обзор архитектуры
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Asterisk │────>│ Storage │────>│ Transcribe │────>│ Analyze │
│ MixMonitor │ │ (WAV/Opus) │ │ (Whisper) │ │ (NLP/LLM) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│
┌──────┴──────┐
│ Dashboard │
│ (Metrics) │
└─────────────┘
Вариант 1. Whisper на своём сервере
Модель Whisper от OpenAI бесплатна, имеет открытый исходный код и работает на вашем железе:
#!/usr/bin/env python3
"""
transcribe_recordings.py — Batch transcribe recordings using Whisper.
Requires: pip install openai-whisper torch
"""
import whisper
import json
import os
import psycopg2
from pathlib import Path
# Load Whisper model (options: tiny, base, small, medium, large-v3)
# 'small' is the best balance of speed and accuracy for telephony
model = whisper.load_model("small")
DB_CONFIG = {
'host': 'localhost',
'database': 'asterisk',
'user': 'asterisk',
'password': 'your_password'
}
TRANSCRIPT_DIR = '/var/spool/asterisk/monitor/transcripts'
def transcribe_pending():
conn = psycopg2.connect(**DB_CONFIG)
cur = conn.cursor()
# Get un-transcribed recordings
cur.execute("""
SELECT id, recording_path, recorded_at
FROM recording_metadata
WHERE transcribed = FALSE
AND duration_seconds > 10
AND duration_seconds < 1800
ORDER BY recorded_at DESC
LIMIT 50
""")
for rec_id, rec_path, recorded_at in cur.fetchall():
if not os.path.exists(rec_path):
continue
try:
# Transcribe with Whisper
result = model.transcribe(
rec_path,
language=None, # Auto-detect language
task="transcribe",
fp16=False # Use fp32 for CPU
)
# Save transcript
transcript_path = os.path.join(
TRANSCRIPT_DIR,
recorded_at.strftime('%Y/%m/%d'),
f"{os.path.basename(rec_path).replace('.wav', '.json')}"
)
os.makedirs(os.path.dirname(transcript_path), exist_ok=True)
transcript_data = {
'text': result['text'],
'language': result.get('language', 'unknown'),
'segments': [
{
'start': seg['start'],
'end': seg['end'],
'text': seg['text']
}
for seg in result.get('segments', [])
]
}
with open(transcript_path, 'w') as f:
json.dump(transcript_data, f, indent=2)
# Update database
cur.execute("""
UPDATE recording_metadata
SET transcribed = TRUE, transcript_path = %s
WHERE id = %s
""", (transcript_path, rec_id))
conn.commit()
print(f"Transcribed: {os.path.basename(rec_path)} "
f"({result.get('language', '?')})")
except Exception as e:
print(f"Error transcribing {rec_path}: {e}")
continue
cur.close()
conn.close()
if __name__ == '__main__':
transcribe_pending()Требования к железу для Whisper
| Модель | VRAM | CPU: время/мин аудио | GPU: время/мин аудио | Точность |
|---|---|---|---|---|
| tiny | ~1 ГБ | ~10 сек | ~1 сек | Годится для поиска ключевых слов |
| base | ~1 ГБ | ~15 сек | ~2 сек | Приемлемо для аналитики |
| small | ~2 ГБ | ~30 сек | ~3 сек | Рекомендуется для телефонии |
| medium | ~5 ГБ | ~60 сек | ~5 сек | Высокая точность |
| large-v3 | ~10 ГБ | ~120 сек | ~8 сек | Максимальная точность |
Для колл-центра на 50 операторов, который обрабатывает 300 записей в день средней длительностью 5 минут, модель small на GPU справится со всеми записями за день примерно за 75 минут.
Вариант 2. Облачный API транскрибации
Для команд, которым удобнее управляемый сервис:
# Using OpenAI Whisper API (cloud)
import openai
client = openai.OpenAI(api_key="your-api-key")
def transcribe_cloud(audio_path):
with open(audio_path, "rb") as audio_file:
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file,
response_format="verbose_json",
timestamp_granularities=["segment"]
)
return transcriptОблачный API стоит примерно $0.006 за минуту аудио. При 300 звонках × 5 минут в день это $9 в день, то есть ~$270 в месяц.
Поиск ключевых слов и определение тем
Когда расшифровки готовы, ключевые слова и темы можно находить даже без ИИ:
#!/usr/bin/env python3
"""
keyword_analyzer.py — Detect keywords and topics in call transcripts.
"""
import json
import re
from collections import Counter
# Define keyword categories relevant to your business
KEYWORD_CATEGORIES = {
'complaint': [
'complaint', 'unhappy', 'dissatisfied', 'terrible', 'worst',
'cancel', 'refund', 'manager', 'supervisor', 'unacceptable'
],
'upsell_opportunity': [
'upgrade', 'additional', 'more features', 'enterprise',
'premium', 'advanced', 'expand', 'grow', 'scale'
],
'technical_issue': [
'not working', 'broken', 'error', 'bug', 'crash',
'down', 'outage', 'slow', 'timeout', 'failed'
],
'positive': [
'thank you', 'excellent', 'great', 'wonderful', 'perfect',
'appreciate', 'helpful', 'resolved', 'solved', 'happy'
],
'compliance_risk': [
'guarantee', 'promise', 'definitely', 'always',
'never', 'lawsuit', 'legal', 'attorney', 'sue'
]
}
def analyze_transcript(transcript_path):
with open(transcript_path) as f:
data = json.load(f)
text = data['text'].lower()
results = {}
for category, keywords in KEYWORD_CATEGORIES.items():
found = []
for keyword in keywords:
count = len(re.findall(r'\b' + re.escape(keyword) + r'\b', text))
if count > 0:
found.append({'keyword': keyword, 'count': count})
results[category] = {
'matches': found,
'total_hits': sum(m['count'] for m in found),
'flagged': len(found) > 0
}
return resultsДинамика тональности по сегментам
# Simple sentiment scoring per transcript segment
# For production: use a fine-tuned model or LLM API
from textblob import TextBlob
def segment_sentiment(transcript_path):
"""Analyze sentiment across call segments to detect escalation patterns."""
with open(transcript_path) as f:
data = json.load(f)
timeline = []
for segment in data.get('segments', []):
blob = TextBlob(segment['text'])
timeline.append({
'start': segment['start'],
'end': segment['end'],
'text': segment['text'],
'polarity': round(blob.sentiment.polarity, 2), # -1 to 1
'subjectivity': round(blob.sentiment.subjectivity, 2) # 0 to 1
})
# Detect escalation: sentiment dropping over time
if len(timeline) > 4:
first_quarter = sum(s['polarity'] for s in timeline[:len(timeline)//4])
last_quarter = sum(s['polarity'] for s in timeline[-len(timeline)//4:])
escalation = first_quarter - last_quarter > 0.5
else:
escalation = False
return {
'segments': timeline,
'avg_polarity': round(sum(s['polarity'] for s in timeline) / max(len(timeline), 1), 2),
'escalation_detected': escalation
}Шаг 6. Комплаенс и сроки хранения
Требования к записи разговоров сильно различаются в разных юрисдикциях. Нарушение этих требований грозит штрафами и юридической ответственностью.
Модели согласия на запись
| Модель | Требование | Юрисдикции |
|---|---|---|
| Согласие одной стороны | О записи знает один участник разговора | США (федеральный уровень), Великобритания, большая часть ЕС |
| Согласие обеих/всех сторон | О записи знают все участники разговора | Калифорния, Флорида, Германия, ряд стран ЕС |
| Согласие не требуется | Деловые звонки выведены из-под требования | Отдельные B2B-сценарии |
Как реализовать уведомление о записи
; extensions.conf — Play recording notification before connecting
[inbound-queue]
exten => s,1,Answer()
same => n,Playback(this-call-may-be-recorded)
same => n,Wait(0.5)
same => n,Macro(record-call)
same => n,Queue(support,t,,,180)
same => n,Hangup()Автоматическая политика хранения
#!/bin/bash
# recording-retention.sh — Enforce data retention policies
# Run weekly via cron
MONITOR_DIR="/var/spool/asterisk/monitor"
TRANSCRIPT_DIR="$MONITOR_DIR/transcripts"
# Retention periods (adjust per your compliance requirements)
RECORDING_RETENTION_DAYS=365 # Keep recordings for 1 year
TRANSCRIPT_RETENTION_DAYS=730 # Keep transcripts for 2 years
COMPRESSED_RETENTION_DAYS=730 # Keep compressed archives for 2 years
echo "$(date): Starting retention cleanup"
# Delete original WAV files older than retention period
find "$MONITOR_DIR" -name "*.wav" -mtime +$RECORDING_RETENTION_DAYS \
-not -path "*/compressed/*" -delete -print | wc -l | \
xargs -I {} echo "Deleted {} expired WAV files"
# Delete compressed files older than retention period
find "$MONITOR_DIR/compressed" -name "*.opus" \
-mtime +$COMPRESSED_RETENTION_DAYS -delete -print | wc -l | \
xargs -I {} echo "Deleted {} expired compressed files"
# Delete transcripts older than retention period
find "$TRANSCRIPT_DIR" -name "*.json" \
-mtime +$TRANSCRIPT_RETENTION_DAYS -delete -print | wc -l | \
xargs -I {} echo "Deleted {} expired transcripts"
# Clean empty directories
find "$MONITOR_DIR" -type d -empty -delete
echo "$(date): Retention cleanup complete"GDPR и запросы субъектов данных
Для соответствия GDPR вам нужна возможность найти и удалить все записи по конкретному звонящему:
-- Find all recordings for a specific caller (data subject request)
SELECT
recording_path,
transcript_path,
recorded_at,
duration_seconds,
destination
FROM recording_metadata
WHERE caller_id = '2125551234'
ORDER BY recorded_at DESC;
-- Delete all data for a specific caller (right to erasure)
-- Step 1: Get file paths for physical deletion
SELECT recording_path, transcript_path
FROM recording_metadata
WHERE caller_id = '2125551234';
-- Step 2: Delete database records
DELETE FROM recording_metadata WHERE caller_id = '2125551234';Шаг 7. Собираем дашборд
Дашборд аналитики записей должен показывать выводы сам, не заставляя супервизора вручную переслушивать отдельные разговоры.
Каркас дашборда: три уровня
Уровень 1 — воллборд в реальном времени (взгляд на секунду):
- —Записи, которые идут прямо сейчас
- —Количество записей за сегодня против плана
- —Процент заполнения хранилища
- —Оповещения о сбоях записи
Уровень 2 — ежедневная операционка (для супервизоров):
- —Процент покрытия записью (все ли звонки записываются?)
- —Динамика средней длительности разговора
- —Распределение доли тишины
- —Пометки звонков с высокой долей тишины для разбора
- —Звонки, отмеченные детектором ключевых слов
Уровень 3 — стратегическая аналитика (неделя/месяц):
- —Тренды объёма записей
- —Прогноз роста хранилища
- —Инсайты из расшифровок: топ тем, динамика тональности
- —Точки для обучения операторов (много тишины, короткие звонки)
- —Аудит комплаенса: пробелы в покрытии, проверка уведомлений о записи
Ключевые метрики
| Метрика | Формула | Целевое значение | Почему важно |
|---|---|---|---|
| Покрытие записью | Записанные звонки / Все отвеченные звонки × 100 | >98% | Если звонки не пишутся — это риск по комплаенсу |
| Средняя длительность записи | Всего секунд записи / Число записей | Зависит от бизнеса | Изменение тренда сигнализирует о сбоях в процессах |
| Доля тишины | Секунды тишины / Общая длительность × 100 | <20% | Много тишины = время ожидания, «мёртвый эфир» |
| Скорость роста хранилища | ГБ в сутки | Планируйте с запасом ×2 | Защищает от аварийного заполнения диска |
| Очередь на транскрибацию | Нерасшифрованные / Все записи | <5% | Гарантирует актуальность аналитики |
| Доля негативной тональности | Негативные звонки / Все расшифрованные × 100 | <15% | Индикатор удовлетворённости клиентов |
| Доля комплаенс-флагов | Помеченные звонки / Все расшифрованные × 100 | <2% | Управление рисками |
Сравнение: подходы к аналитике записей
| Возможность | Свои скрипты | QueueMetrics | CallCabinet | Astervis |
|---|---|---|---|---|
| Трудозатраты на внедрение | Высокие (недели) | Средние (дни) | Низкие (часы) | Низкие (минуты) |
| Прослушивание записей | Ручной доступ к файлам | Встроенный плеер | Облачный плеер | Встроенный плеер |
| Связь с CDR | Свой SQL | Автоматически | Автоматически | Автоматически |
| Поиск по номеру/дате | SQL-запросы | Поиск в GUI | Поиск в GUI | Мгновенный поиск |
| Речевая аналитика | Пишите сами | Нет | На базе ИИ ($$$) | Скоро |
| Детекция тишины | Свои скрипты | Нет | Есть | Есть |
| Инструменты комплаенса | Вручную | Базовые | Полный набор | Встроенные политики хранения |
| Оценка операторов | Нет | Ручные QA-формы | ИИ-оценка | Автоматические KPI |
| Стоимость | Бесплатно + ваше время | CHF 8/агент/мес | Цена по запросу | От $119/мес |
| Self-hosted | Да | Да | Только облако | Да |
| Дашборд в реальном времени | Пишите сами | Базовый | Да | 30+ графиков |
Чек-лист быстрого старта
Эти шаги проведут вас от нуля до работающей аналитики записей разговоров:
- — Шаг 1: включите запись — добавьте MixMonitor в диалплан со структурированными именами файлов
- — Шаг 2: организуйте хранение — заведите каталоги по датам, спланируйте объём
- — Шаг 3: свяжите с CDR — убедитесь, что
CDR(recordingfile)заполняется, создайте таблицу метаданных - — Шаг 4: индексируйте записи — запускайте скрипт-индексатор по cron каждые 15 минут
- — Шаг 5: сжимайте старые файлы — настройте ежедневную cron-задачу на сжатие
- — Шаг 6: соберите базовый дашборд — начните с запросов по покрытию и длительности
- — Шаг 7: добавьте транскрибацию — установите Whisper, запустите пакетную расшифровку
- — Шаг 8: настройте детекцию ключевых слов — задайте категории, сканируйте расшифровки автоматически
- — Шаг 9: задайте политику хранения — настройте автоочистку под требования вашего комплаенса
- — Шаг 10: пересматривайте и улучшайте — еженедельно смотрите метрики дашборда и подкручивайте пороги
Что дальше
Собрать аналитику записей разговоров с нуля можно, и получится мощно, но это долго. Придётся поддерживать скрипты, следить за хранилищем, удерживать в рабочем состоянии пайплайны транскрибации и строить дашборды — параллельно с работой самого колл-центра.
Astervis берёт эту рутину на себя. Установка одной командой — и вы сразу получаете 30+ аналитических графиков, прослушивание записей со связью с CDR, отслеживание эффективности операторов и мониторинг очередей в реальном времени. Без единого SQL-запроса.
Начните с 14-дневного бесплатного триала: astervis.io
Нужны ещё руководства по аналитике Asterisk? Читайте наши статьи о мониторинге очередей Asterisk в реальном времени, лучших практиках CDR-отчётности и отслеживании эффективности операторов.
Хватит гадать. Начни видеть.
Реальная картина вашего Asterisk колл-центра: время ожидания, активность операторов, нагрузка на транки и 30+ графиков. On-premise на вашем сервере. Установка за 5 минут. Без карты.
От $119/мес flat. Без ограничения на операторов. Триал 14 дней.
