Come filtrare chiamate indesiderate e telemarketing con Asterisk e Whisper

Guida pratica alla realizzazione di un risponditore telefonico AI su VPS Linux: Asterisk gestisce le chiamate, Whisper trascrive e classifica, Piper genera la voce e Telegram riceve numero, categoria e trascrizione.

Le chiamate indesiderate non sono più soltanto un fastidio: tra telemarketing aggressivo, offerte su luce e gas, proposte commerciali poco trasparenti e numeri sconosciuti che insistono più volte al giorno, il telefono rischia di trasformarsi in una fonte continua di interruzioni.

La risposta più ovvia sarebbe bloccare tutto, ma significherebbe correre il rischio di perdere anche una chiamata importante: un corriere, un medico, una scuola, un tecnico, un ufficio o qualcuno che deve semplicemente comunicare qualcosa di utile. Da qui nasce l’idea di costruire un assistente telefonico automatico capace di rispondere al posto nostro, capire chi sta chiamando, riconoscere il motivo della telefonata e distinguere in pochi secondi un contatto utile da una probabile chiamata commerciale.

Il progetto descritto in questa guida utilizza componenti open source e servizi facilmente accessibili: Asterisk gestisce la telefonata, Whisper trascrive il parlato in italiano, Piper genera una voce naturale per le risposte automatiche e Telegram riceve numero del chiamante, trascrizione e classificazione della chiamata. Tutto gira su un modesto VPS Linux, senza dipendere da servizi cloud costosi per il riconoscimento vocale.

Come funziona il risponditore intelligente per gestire le chiamate in arrivo

Il sistema al quale abbiamo pensato non si limita a registrare le chiamate, interroga il chiamante con due domande semplici, analizza rapidamente ciò che viene detto e sceglie il comportamento più appropriato. Se riconosce una chiamata commerciale relativa a luce e gas, riproduce un avviso specifico; se identifica altro telemarketing, utilizza un messaggio dedicato al Registro pubblico delle opposizioni; se invece la chiamata appare utile o semplicemente incerta, informa il chiamante che il messaggio sarà riferito.

Nel frattempo una seconda fase di riconoscimento vocale, più accurata, prepara il riepilogo che arriva su Telegram con il numero richiamabile con un tocco, la trascrizione e una classificazione immediatamente comprensibile.

L’obiettivo non è costruire un centralino complesso, ma qualcosa di molto più pratico: un filtro telefonico personale che risponda quando noi non possiamo o non vogliamo farlo, lasciando passare l’informazione utile e opponendo una barriera automatica alle chiamate commerciali indesiderate.

La parte interessante è che l’intero sistema può essere installato, modificato e rimosso senza alterare gli altri servizi presenti sul VPS.

Da parte nostra usiamo già Spam Call Blocker per bloccare le chiamate certamente spam sui terminali mobili ma sentivamo l’esigenza di un filtro aggiuntivo più flessibile e versatile.

I componenti necessari

Per il nostro esperimento abbiamo voluto far funzionare il risponditore automatico AI su una macchina dotata di una configurazione hardware volutamente modesta. Potendo contare su hardware di profilo più elevato, si possono migliorare nettamente le prestazioni del filtro e la qualità delle trascrizioni.

Per iniziare servono i seguenti componenti:

  • VPS Ubuntu;
  • Docker e Docker Compose;
  • numero SIP con credenziali;
  • un bot Telegram;
  • eventualmente una SIM/cellulare da cui deviare le chiamate non risposte.

Nell’implementazione descritta abbiamo utilizzato VivaVox come provider SIP accedendo all’offerta Ehiweb che permette di ottenere una numerazione VoIP gratis per 30 giorni. Dopo aver attivato il servizio, è necessario conoscere numerazione, username, password e indirizzo del server SIP.

Preparazione del server VPS

Accedendo con un account root oppure usando il comando sudo, è necessario procedere con l’aggiornamento dei pacchetti, l’installazione degli strumenti necessari e l’avvio di Docker:

apt update
apt upgrade -y

apt install -y \
docker.io \
docker-compose-v2 \
curl \
wget \
ffmpeg \
ufw

systemctl enable --now docker

docker --version
docker compose version

Su un VPS dotato di poca RAM, è opportuno configurare un file di swap. Il comando consente di verificare RAM e swap: free -h

Se si volessero aggiungere circa 1,5 GB di swap:

fallocate -l 1536M /swap-ai-phone
chmod 600 /swap-ai-phone
mkswap /swap-ai-phone
swapon /swap-ai-phone

echo '/swap-ai-phone none swap sw 0 0' >> /etc/fstab

Configurazione del firewall

Lato firewall locale, è necessario aprire la porta usata per le comunicazioni SIP e RTP:

ufw allow 5060/udp
ufw allow 10000:10019/udp

Non serve invece aprire la porta 8080. Con il comando ufw status, è possibile verificare la configurazione in uso.

Struttura del progetto

Il passo seguente consiste nel predisporre tutte le cartelle che ospiteranno le risorse essenziali per il funzionamento del progetto.

mkdir -p /opt/ai-phone/asterisk/etc/asterisk
mkdir -p /opt/ai-phone/bot/models
mkdir -p /opt/ai-phone/data/audio
mkdir -p /opt/ai-phone/data/prompts
mkdir -p /opt/ai-phone/data/reports
mkdir -p /opt/ai-phone/logs/asterisk
mkdir -p /opt/ai-phone/piper

/opt/ai-phone/
├── .env
├── compose.yml
├── asterisk/
│   ├── Dockerfile
│   └── etc/asterisk/
│       ├── pjsip.conf
│       ├── extensions.conf
│       └── rtp.conf
├── bot/
│   ├── watch.sh
│   └── models/
│       ├── ggml-tiny-q5_1.bin
│       └── ggml-small-q5_1.bin
├── data/
│   ├── audio/
│   ├── prompts/
│   └── reports/
└── logs/
    └── asterisk/

Come si vede, la cartella /opt/ai-phone conterrà tutto il necessario, senza bisogno di sporcare inutilmente il filesystem.

Come predisporre il container Asterisk

A questo punto si deve creare il Dockerfile di Asterisk, cioè il file che definisce come costruire il container telefonico installando Asterisk e le dipendenze necessarie, senza modificare direttamente il sistema operativo della VPS:

nano /opt/ai-phone/asterisk/Dockerfile

All’interno del file, basta incollare (tasto destro del mouse) quanto segue:

FROM debian:forky-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        asterisk \
        asterisk-config \
        asterisk-modules \
        asterisk-core-sounds-en \
        ca-certificates \
        tini \
        ffmpeg \
        curl && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

ENTRYPOINT ["/usr/bin/tini", "--"]

CMD ["/usr/sbin/asterisk", "-f", "-vvv"]

Configurazione RTP e SIP

Il seguente file permette di impostare l’intervallo di porte RTP usato da Asterisk per trasportare l’audio delle chiamate, limitandolo a 10000-10019/UDP così da semplificare firewall e gestione del traffico voce.

nano /opt/ai-phone/asterisk/etc/asterisk/rtp.conf

Il contenuto da incollare al suo interno:

[general]
rtpstart=10000
rtpend=10019

Come secondo passo, configuriamo PJSIP cioè il modulo con cui Asterisk si registra al provider VoIP e gestisce le chiamate SIP in ingresso, definendo server, credenziali, codec e parametri necessari per ricevere correttamente le telefonate:

nano /opt/ai-phone/asterisk/etc/asterisk/pjsip.conf

Di seguito, il contenuto da incollare. È fondamentale sostituire nei vari punti NUMERO_SIP e PASSWORD_SIP con le credenziali reali ottenute dal gestore della numerazione VoIP:

[global]
type=global
user_agent=AI-Phone

[transport-udp]
type=transport
protocol=udp
bind=0.0.0.0:5060

[vivavox-auth]
type=auth
auth_type=userpass
username=NUMERO_SIP
password=PASSWORD_SIP

[vivavox-aor]
type=aor
contact=sip:sip.vivavox.it:5060
qualify_frequency=60

[vivavox]
type=endpoint
transport=transport-udp
context=from-vivavox

disallow=all
allow=alaw
allow=ulaw

outbound_auth=vivavox-auth
aors=vivavox-aor

from_user=NUMERO_SIP
from_domain=sip.vivavox.it

direct_media=no
rtp_symmetric=yes
force_rport=yes
rewrite_contact=yes

[vivavox-registration]
type=registration
transport=transport-udp

outbound_auth=vivavox-auth

server_uri=sip:sip.vivavox.it:5060
client_uri=sip:NUMERO_SIP@sip.vivavox.it

contact_user=NUMERO_SIP

retry_interval=60
forbidden_retry_interval=300
fatal_retry_interval=300

expiration=300

[vivavox-identify]
type=identify
endpoint=vivavox
match=sip.vivavox.it

Come già indicato in precedenza, noi abbiamo utilizzato VivaVox; nel caso in cui si scegliesse un altro provider, è necessario applicare le opportune modifiche.

Creazione di un bot Telegram

Per ricevere sul telefono le notifiche con numero del chiamante, classificazione e trascrizione della conversazione, utilizziamo Telegram come canale di notifica. Basta creare un bot tramite BotFather, recuperare il token del bot e l’ID della chat su cui ricevere i messaggi.

BotFather Telegram

La creazione di un nuovo bot Telegram si concretizza aprendo BotFather quindi digitando /newbot nella conversazione e infine digitando un nome colloquiale per il bot e un username univoco. A questo punto è fondamentale salvare la lunga stringa riportata dopo Use this token to access the HTTP API.

Token API Bot Telegram

Sul VPS si può quindi digitare nano /opt/ai-phone/.env e incollare nel file quanto segue:

TELEGRAM_BOT_TOKEN=TOKEN_DEL_BOT
TELEGRAM_CHAT_ID=ID_DELLA_CHAT

Proteggere il file con il comando seguente:

chmod 600 /opt/ai-phone/.env

Per verificare il funzionamento del bot Telegram:

cd /opt/ai-phone

set -a
source .env
set +a

curl -sS \
  -X POST \
  "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
  -d chat_id="${TELEGRAM_CHAT_ID}" \
  --data-urlencode "text=AI Phone: test Telegram OK."

Scaricare i modelli Whisper

Prima di avviare il riconoscimento vocale, dobbiamo scaricare i modelli Whisper che saranno usati nelle due fasi del sistema. Ne utilizziamo uno molto leggero e veloce per la classificazione immediata della chiamata e uno più accurato per produrre la trascrizione finale da inviare su Telegram.

Entrambi sono modelli multilingua quantizzati, scelti per ridurre consumo di RAM e tempi di elaborazione sulla VPS senza rinunciare troppo alla qualità del riconoscimento.

Portandosi nella directory seguente cd /opt/ai-phone/bot/models è quindi necessario scaricare due modelli, uno rapido e uno più accurato:

wget -O ggml-tiny-q5_1.bin \
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny-q5_1.bin"

wget -O ggml-small-q5_1.bin \
"https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-small-q5_1.bin"

Questi modelli funzionano “benino” e sono multilingua. Tuttavia, i possessori di configurazioni hardware multicore e di una dotazione di memoria RAM più estesa possono sfruttare modelli di più grandi dimensioni.

Generare la voce italiana Piper e tutti i messaggi del risponditore AI

Per rendere più naturale la voce dell’assistente utilizziamo Piper, un motore TTS (text-to-speech) locale che genera audio senza dipendere da servizi cloud. In questo passaggio scarichiamo la voce italiana it_IT-paola-medium che sarà poi usata per creare tutti i messaggi vocali riprodotti da Asterisk durante le chiamate.

Il comando salva nella directory /opt/ai-phone/piper sia il modello ONNX della voce sia il relativo file di configurazione JSON, necessari per generare successivamente i WAV del progetto.

docker run --rm \
-v /opt/ai-phone/piper:/piper \
python:3.11-slim \
sh -c '
pip install --no-cache-dir piper-tts >/dev/null &&
cd /piper &&
python -m piper.download_voices it_IT-paola-medium
'

Come creare i file vocali

A questo punto generiamo 6 file vocali in formato WAV. Piper sintetizza le frasi con la voce italiana scelta in precedenza, mentre FFmpeg converte poi ogni file nel formato più adatto ad Asterisk: WAV mono, PCM 16 bit, 8 kHz.

In questo modo prepariamo sia i messaggi generici della conversazione – saluto, richiesta del motivo e frase di chiusura – sia i due avvisi dedicati alle chiamate commerciali, uno specifico per luce e gas (settore energia) e uno per lo spam non energetico. I testi che leggete di seguito, sono ovviamente del tutto personalizzabili:

docker run --rm \
  -v /opt/ai-phone/piper:/piper \
  -v /opt/ai-phone/data/prompts:/out \
  python:3.11-slim \
  sh -c '
    apt-get update >/dev/null &&
    apt-get install -y --no-install-recommends ffmpeg >/dev/null &&
    pip install --no-cache-dir piper-tts >/dev/null &&

    echo "Pronto, con chi parlo?" |
      python -m piper \
      -m /piper/it_IT-paola-medium.onnx \
      -f /tmp/pronto.wav &&

    echo "Per quale motivo chiama?" |
      python -m piper \
      -m /piper/it_IT-paola-medium.onnx \
      -f /tmp/motivo.wav &&

    echo "Un momento." |
      python -m piper \
      -m /piper/it_IT-paola-medium.onnx \
      -f /tmp/verifica.wav &&

    echo "Io sono un assistente digitale, riferirò la chiamata." |
      python -m piper \
      -m /piper/it_IT-paola-medium.onnx \
      -f /tmp/riferiro.wav &&

    echo "Dal 19 giugno 2026 le sollecitazioni telefoniche per proporre contratti luce e gas sono vietate, \
       salvo le eccezioni previste. Questa numerazione è iscritta al Registro pubblico delle opposizioni \
       e nessun consenso è stato prestato dal titolare. Vi invitiamo a cancellare ogni dato personale \
       e a non effettuare nuove chiamate. La telefonata è stata registrata e archiviata." |
      python -m piper \
      -m /piper/it_IT-paola-medium.onnx \
      -f /tmp/energia-legale.wav &&

    echo "Questa numerazione è iscritta al Registro pubblico delle opposizioni \
      e nessun consenso è stato prestato dal titolare. Vi invitiamo a cancellare \
      ogni dato personale e a non effettuare nuove chiamate. La telefonata è stata \
      registrata e archiviata." |
      python -m piper \
      -m /piper/it_IT-paola-medium.onnx \
      -f /tmp/spam-legale.wav &&

    for f in pronto motivo verifica riferiro energia-legale spam-legale
    do
      ffmpeg -y -loglevel error \
        -i "/tmp/${f}.wav" \
        -ar 8000 \
        -ac 1 \
        -c:a pcm_s16le \
        "/out/${f}.wav"
    done
  '

Qualche nota legale

Il testo specifico per luce e gas riflette l’art. 51, comma 8-bis, del Codice del consumo introdotto dalla legge n. 49/2026: il divieto opera dopo 60 giorni dall’entrata in vigore della norma, quindi dal 19 giugno 2026, con le eccezioni espressamente previste dalla legge.

Per il Registro pubblico delle opposizioni (RPO), va ricordato che esistono eccezioni, ad esempio consensi prestati successivamente e determinati rapporti contrattuali continuativi.

È quindi bene opportuno usare la frase “nessun consenso è stato prestato” soltanto se corrisponde effettivamente alla situazione reale. Il RPO, comunque, permette di revocare in qualunque momento tutti i consensi eventualmente prestati in date antecedenti al rinnovo della registrazione.

Allestire il dialplan completo per Asterisk

Ora definiamo il dialplan di Asterisk, cioè la logica che stabilisce cosa deve succedere quando arriva una chiamata. In questo file configuriamo la sequenza completa: risposta, riproduzione dei messaggi, registrazione delle risposte del chiamante, avvio dell’analisi con Whisper e scelta automatica del messaggio finale in base alla classificazione ottenuta.

Creiamo quindi il file principale delle regole di instradamento:

nano /opt/ai-phone/asterisk/etc/asterisk/extensions.conf

Contenuto del file:

[general]
static=yes
writeprotect=no

[from-vivavox]

exten => _X.,1,NoOp(*** AI PHONE - CHIAMATA ENTRANTE ***)

 same => n,Answer()
 same => n,Wait(0.5)

 ; ID chiamata
 same => n,Set(CALLID=${UNIQUEID})

 ; Caller ID
 same => n,Set(CALLER=${CALLERID(num)})
 same => n,System(echo "${CALLER}" > /data/audio/${CALLID}.caller)

 ; Prima domanda
 same => n,Playback(/data/prompts/pronto)
 same => n,Record(/data/audio/${CALLID}.who.wav,2,10,kq)
 same => n,Wait(0.3)

 ; Seconda domanda
 same => n,Playback(/data/prompts/motivo)
 same => n,Record(/data/audio/${CALLID}.reason.wav,2,15,kq)

 ; Avvia classificazione
 same => n,System(touch /data/audio/${CALLID}.ready)
 same => n,Playback(/data/prompts/verifica)

 ; Attesa classificazione rapida
 same => n,Set(WAITCOUNT=0)
 same => n(waitclass),Set(ISENERGY=${STAT(e,/data/audio/${CALLID}.energy)})
 same => n,GotoIf($["${ISENERGY}"="1"]?energy)
 same => n,Set(ISSPAM=${STAT(e,/data/audio/${CALLID}.spam)})
 same => n,GotoIf($["${ISSPAM}"="1"]?spam)
 same => n,Set(ISNORMAL=${STAT(e,/data/audio/${CALLID}.normal)})
 same => n,GotoIf($["${ISNORMAL}"="1"]?normal)
 same => n,Set(WAITCOUNT=$[${WAITCOUNT}+1])
 same => n,GotoIf($[${WAITCOUNT}>=30]?normal)
 same => n,Wait(1)
 same => n,Goto(waitclass)

 ; ============================================
 ; SPAM ENERGIA
 ; ============================================

 same => n(energy),NoOp(*** TELEMARKETING ENERGIA ***)
 same => n,Playback(/data/prompts/energia-legale)
 same => n,Wait(0.5)
 same => n,Hangup()

 ; ============================================
 ; SPAM NON ENERGIA
 ; ============================================

 same => n(spam),NoOp(*** TELEMARKETING / SPAM ***)
 same => n,Playback(/data/prompts/spam-legale)
 same => n,Wait(0.5)
 same => n,Hangup()

 ; ============================================
 ; NORMALE / UTILE / INCERTA
 ; ============================================

 same => n(normal),Playback(/data/prompts/riferiro)
 same => n,Wait(0.5)
 same => n,Hangup()

exten => s,1,Goto(from-vivavox,${EXTEN},1)

Record() consente di impostare sia il numero di secondi di silenzio che provoca la conclusione della registrazione, sia la durata massima; q evita il beep e k conserva il file in caso di riaggancio.

Script completo watch.sh

Creiamo quindi lo script che coordina tutta la parte di analisi della chiamata: watch.sh sorveglia le registrazioni prodotte da Asterisk, invia l’audio a Whisper, applica le regole di classificazione, crea i marker che determinano la risposta vocale da riprodurre e avvia la trascrizione più accurata destinata alla notifica Telegram.

nano /opt/ai-phone/bot/watch.sh

Di seguito il contenuto da incollare:

#!/bin/sh

set -u

mkdir -p /data/reports

HISTORY="/data/reports/history.tsv"

echo "AI Phone classifier scoring + transcriber avviato."

while true
do
    found=0

    for ready in /data/audio/*.ready
    do
        [ -e "$ready" ] || continue

        found=1

        base="${ready%.ready}"
        id="$(basename "$base")"

        caller_file="${base}.caller"
        who_wav="${base}.who.wav"
        reason_wav="${base}.reason.wav"

        energy_marker="${base}.energy"
        spam_marker="${base}.spam"
        normal_marker="${base}.normal"

        quick_input="/tmp/${id}.quick16.wav"

        combined_input="/tmp/${id}.combined16.wav"
        transcript_out="/data/reports/${id}.transcript"

        echo "Elaborazione chiamata $id"


        # -----------------------------
        # CALLER ID
        # -----------------------------

        if [ -s "$caller_file" ]; then
            caller="$(tr -d '\r\n' < "$caller_file")" else caller="Numero non disponibile" fi call_number="$(printf '%s' "$caller" | tr -d ' ()-')" case "$call_number" in +*) ;; 0039*) call_number="+${call_number#00}" ;; 39*) call_number="+$call_number" ;; 0*|3*) call_number="+39$call_number" ;; *) call_number="$caller" ;; esac # ----------------------------- # CLASSIFICAZIONE RAPIDA # ----------------------------- rm -f "$energy_marker" rm -f "$spam_marker" rm -f "$normal_marker" rm -f "$quick_input" if [ -s "$who_wav" ] && [ -s "$reason_wav" ]; then ffmpeg -y -loglevel error \ -i "$who_wav" \ -i "$reason_wav" \ -filter_complex \ "[0:a]aresample=16000,aformat=sample_fmts=s16:channel_layouts=mono[a0]; \ anullsrc=r=16000:cl=mono:d=0.30[s]; \ [1:a]aresample=16000,aformat=sample_fmts=s16:channel_layouts=mono[a1]; \ [a0][s][a1]concat=n=3:v=0:a=1[out]" \ -map "[out]" \ -ar 16000 \ -ac 1 \ -c:a pcm_s16le \ "$quick_input" elif [ -s "$reason_wav" ]; then ffmpeg -y -loglevel error \ -i "$reason_wav" \ -ar 16000 \ -ac 1 \ -c:a pcm_s16le \ "$quick_input" elif [ -s "$who_wav" ]; then ffmpeg -y -loglevel error \ -i "$who_wav" \ -ar 16000 \ -ac 1 \ -c:a pcm_s16le \ "$quick_input" fi # ----------------------------- # TINY Q5_1 RESIDENTE # ----------------------------- quick_text="" if [ -s "$quick_input" ]; then quick_text="$(curl -sS \ --max-time 15 \ "http://whisper-fast:8080/inference" \ -F "file=@${quick_input}" \ -F "language=it" \ -F "response_format=text" \ -F "temperature=0" \ -F "temperature_inc=0" \ -F "no_timestamps=true" \ -F "prompt=Telefonata italiana. Prestare particolare attenzione a offerte commerciali, contratti, questioni contrattuali, rimborsi, tariffe, energia elettrica, luce, gas, bollette, forniture, utenze, mercato libero, cambi gestore, telefonia, fibra, investimenti, trading e assicurazioni." \ 2>/dev/null || true)"

        fi

        quick_normalized="$(printf '%s' "$quick_text" | tr '[:upper:]' '[:lower:]')"

        echo "Quick transcript: $quick_text"


        # -----------------------------
        # SCORING RAPIDO
        # -----------------------------

        spam_score_quick=0
        energy_score_quick=0


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'offert|promozion|tariff|contrat|contract|rimbor|attivazion'
        then
            spam_score_quick=$((spam_score_quick + 3))
        fi


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'cambio gestore|cambio fornitore|nuovo gestore|nuovo fornitore|mercato libero|passaggio'
        then
            spam_score_quick=$((spam_score_quick + 3))
        fi


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'proposta|risparm|convenien|sconto|riduzion|prezzo|costo|spesa'
        then
            spam_score_quick=$((spam_score_quick + 2))
        fi


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'agenzia|call center|operatore|consulente|ufficio commerciale|servizio commerciale'
        then
            spam_score_quick=$((spam_score_quick + 1))
        fi


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'telefonia|fibra|internet|operatore telefonico'
        then
            spam_score_quick=$((spam_score_quick + 2))
        fi


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'trading|investiment|criptovalut|crypto|bitcoin'
        then
            spam_score_quick=$((spam_score_quick + 3))
        fi


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'assicurazion|polizza|preventivo assicurativo'
        then
            spam_score_quick=$((spam_score_quick + 2))
        fi


        # -----------------------------
        # SCORE ENERGIA
        # -----------------------------

        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'energia|energia elettrica|luce|gas'
        then
            energy_score_quick=$((energy_score_quick + 3))
        fi


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'bollett|fornitur|utenza'
        then
            energy_score_quick=$((energy_score_quick + 2))
        fi


        if printf '%s' "$quick_normalized" |
            grep -Eiq \
            'enel|edison|sorgenia|a2a|hera|plenitude|iren'
        then
            energy_score_quick=$((energy_score_quick + 3))
        fi


        # -----------------------------
        # DECISIONE RAPIDA
        # -----------------------------

        quick_energy=0

        if [ "$energy_score_quick" -ge 3 ] &&
           [ "$spam_score_quick" -ge 2 ]
        then
            quick_energy=1
        fi

        if [ "$energy_score_quick" -ge 5 ]; then
            quick_energy=1
        fi


        if [ "$quick_energy" -eq 1 ]; then

            touch "$energy_marker"

            echo "Chiamata $id: TELEMARKETING ENERGIA"
            echo "quick_spam_score=$spam_score_quick energy_score=$energy_score_quick"

        elif [ "$spam_score_quick" -ge 2 ]; then

            touch "$spam_marker"

            echo "Chiamata $id: SPAM / COMMERCIALE NON ENERGIA"
            echo "quick_spam_score=$spam_score_quick energy_score=$energy_score_quick"

        else

            touch "$normal_marker"

            echo "Chiamata $id: NORMALE / INCERTA"
            echo "quick_spam_score=$spam_score_quick energy_score=$energy_score_quick"

        fi


        rm -f "$quick_input"


        # -----------------------------
        # TRASCRIZIONE COMPLETA
        # -----------------------------

        rm -f "$combined_input"
        rm -f "${transcript_out}.txt"


        if [ -s "$who_wav" ] && [ -s "$reason_wav" ]; then

            ffmpeg -y -loglevel error \
                -i "$who_wav" \
                -i "$reason_wav" \
                -filter_complex \
                "[0:a]aresample=16000,aformat=sample_fmts=s16:channel_layouts=mono[a0]; \
                 anullsrc=r=16000:cl=mono:d=0.7[s]; \
                 [1:a]aresample=16000,aformat=sample_fmts=s16:channel_layouts=mono[a1]; \
                 [a0][s][a1]concat=n=3:v=0:a=1[out]" \
                -map "[out]" \
                -ar 16000 \
                -ac 1 \
                -c:a pcm_s16le \
                "$combined_input"

        elif [ -s "$who_wav" ]; then

            ffmpeg -y -loglevel error \
                -i "$who_wav" \
                -ar 16000 \
                -ac 1 \
                -c:a pcm_s16le \
                "$combined_input"

        elif [ -s "$reason_wav" ]; then

            ffmpeg -y -loglevel error \
                -i "$reason_wav" \
                -ar 16000 \
                -ac 1 \
                -c:a pcm_s16le \
                "$combined_input"

        fi


        if [ -s "$combined_input" ]; then

            whisper-cli \
                -m /models/ggml-small-q5_1.bin \
                -f "$combined_input" \
                -l it \
                -t 1 \
                -bs 5 \
                -bo 5 \
                -nt \
                --prompt "Conversazione telefonica in italiano. Trascrivere fedelmente nome, azienda e motivo della chiamata. Prestare attenzione a offerte, contratti, questioni contrattuali, rimborsi, energia elettrica, luce, gas, bollette, forniture, utenze, telefonia, fibra, assicurazioni, banche, investimenti, appuntamenti, consegne e assistenza." \
                --output-txt \
                -of "$transcript_out" \
                -np \
                2>"${transcript_out}.log" || true

        fi


        if [ -s "${transcript_out}.txt" ]; then
            transcript="$(cat "${transcript_out}.txt")"
        else
            transcript="Trascrizione non disponibile"
        fi

        combined="$(printf '%s' "$transcript" | tr '[:upper:]' '[:lower:]')"


        # -----------------------------
        # SCORING FINALE
        # -----------------------------

        final_spam_score=0
        final_energy_score=0


        if printf '%s' "$combined" |
            grep -Eiq \
            'offert|promozion|tariff|contrat|contract|rimbor|attivazion'
        then
            final_spam_score=$((final_spam_score + 3))
        fi


        if printf '%s' "$combined" |
            grep -Eiq \
            'cambio gestore|cambio fornitore|nuovo gestore|nuovo fornitore|mercato libero|passaggio'
        then
            final_spam_score=$((final_spam_score + 3))
        fi


        if printf '%s' "$combined" |
            grep -Eiq \
            'proposta|risparm|convenien|sconto|riduzion|prezzo|costo|spesa'
        then
            final_spam_score=$((final_spam_score + 2))
        fi


        if printf '%s' "$combined" |
            grep -Eiq \
            'telefonia|fibra|operatore telefonico'
        then
            final_spam_score=$((final_spam_score + 2))
        fi


        if printf '%s' "$combined" |
            grep -Eiq \
            'trading|investiment|criptovalut|crypto|bitcoin'
        then
            final_spam_score=$((final_spam_score + 3))
        fi


        if printf '%s' "$combined" |
            grep -Eiq \
            'assicurazion|polizza|preventivo assicurativo'
        then
            final_spam_score=$((final_spam_score + 2))
        fi


        if printf '%s' "$combined" |
            grep -Eiq \
            'energia|energia elettrica|luce|gas'
        then
            final_energy_score=$((final_energy_score + 3))
        fi


        if printf '%s' "$combined" |
            grep -Eiq \
            'bollett|fornitur|utenza'
        then
            final_energy_score=$((final_energy_score + 2))
        fi


        useful_score="$(printf '%s' "$combined" |
            grep -Eio \
            'corriere|consegna|pacco|appuntamento|medico|studio medico|scuola|insegnante|prenotazione|tecnico|assistenza richiesta|ordine|intervento|amministratore|farmacia|spedizione|ritiro|consegnare' |
            wc -l)"


        suspicious_score="$(printf '%s' "$combined" |
            grep -Eio \
            'codice otp|otp|password|iban|bonifico|carta di credito|codice della carta|codice ricevuto|credenziali|codice di sicurezza' |
            wc -l)"


        # -----------------------------
        # CATEGORIA TELEGRAM
        # -----------------------------

        if [ "$suspicious_score" -ge 1 ]; then

            category="🟠 POTENZIALMENTE SOSPETTA"

        elif [ "$final_spam_score" -ge 2 ]; then

            category="🔴 COMMERCIALE / SPAM PROBABILE"

        elif [ "$final_energy_score" -ge 5 ]; then

            category="🔴 COMMERCIALE / SPAM PROBABILE"

        elif [ "$useful_score" -ge 1 ] &&
             [ "$final_spam_score" -eq 0 ]; then

            category="🟢 CHIAMATA UTILE"

        else

            category="🟡 DA VERIFICARE"

        fi


        date_now="$(date '+%d/%m/%Y %H:%M:%S')"


        # -----------------------------
        # REPORT LOCALE
        # -----------------------------

        report="/data/reports/${id}.report.txt"

        cat >"$report" <> "$HISTORY"


        # -----------------------------
        # TELEGRAM
        # -----------------------------

        message="📞 AI PHONE

${category}

📱 ${call_number}

📝 ${transcript}

🕐 ${date_now}"

        curl -sS \
            -X POST \
            "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
            -d chat_id="${TELEGRAM_CHAT_ID}" \
            --data-urlencode "text=${message}" \
            >/dev/null


        rm -f "$combined_input"

        mv "$ready" "${base}.done"

        echo "Chiamata $id classificata definitivamente: $category"

    done


    [ "$found" -eq 0 ] && sleep 1

done

Come ultimo passo, è semplicemente necessario rendere eseguibile lo script:

chmod +x /opt/ai-phone/bot/watch.sh

Configurazione di Docker Compose e dei container da avviare

Ora riuniamo i diversi componenti del progetto in un unico file Docker Compose: definiamo i container da avviare, le risorse assegnate, i volumi condivisi e le dipendenze tra Asterisk, il servizio Whisper rapido e il processo che esegue la trascrizione finale.

Creiamo quindi il file che orchestra l’intera applicazione:

nano /opt/ai-phone/compose.yml

Il contenuto da inserire nel file è il seguente:

services:

  asterisk:
    build:
      context: ./asterisk

    container_name: ai-phone-asterisk
    restart: unless-stopped

    network_mode: host

    volumes:
      - ./asterisk/etc/asterisk/pjsip.conf:/etc/asterisk/pjsip.conf:ro
      - ./asterisk/etc/asterisk/extensions.conf:/etc/asterisk/extensions.conf:ro
      - ./asterisk/etc/asterisk/rtp.conf:/etc/asterisk/rtp.conf:ro
      - ./data:/data
      - ./logs/asterisk:/var/log/asterisk

    mem_limit: 180m
    cpus: 0.35

    security_opt:
      - no-new-privileges:true


  whisper-fast:
    image: ghcr.io/ggml-org/whisper.cpp:main

    container_name: ai-phone-whisper-fast
    restart: unless-stopped

    entrypoint:
      - whisper-server

    command:
      - --host
      - 0.0.0.0
      - --port
      - "8080"
      - -m
      - /models/ggml-tiny-q5_1.bin
      - -l
      - it
      - -t
      - "1"
      - -bs
      - "1"
      - -bo
      - "1"

    volumes:
      - ./bot/models:/models:ro

    mem_limit: 220m
    cpus: 0.95

    security_opt:
      - no-new-privileges:true


  transcriber:
    image: ghcr.io/ggml-org/whisper.cpp:main

    container_name: ai-phone-transcriber
    restart: unless-stopped

    entrypoint:
      - /bin/sh
      - /app/watch.sh

    env_file:
      - .env

    depends_on:
      - whisper-fast

    volumes:
      - ./bot/watch.sh:/app/watch.sh:ro
      - ./bot/models:/models:ro
      - ./data:/data

    mem_limit: 950m
    cpus: 0.95

    security_opt:
      - no-new-privileges:true

whisper-server mantiene il modello caricato e accetta file WAV in modo dinamico, eliminando così il costo derivante dal ricaricare il modello tiny-q5_1 a ogni telefonata.

Avvio del risponditore e prime verifiche

I passaggi seguenti consentono l’avvio del sistema e permettono di svolgere una serie di controlli per accertarsi con tutto sia a posto:

cd /opt/ai-phone
docker compose up -d --build
docker compose ps

Devono risultare attivi i seguenti:

ai-phone-asterisk
ai-phone-whisper-fast
ai-phone-transcriber

In docker compose logs --tail=50 whisper-fast, invece, deve comparire ggml-tiny-q5_1.bin.

Dalla CLI (command-line interface) di Asterisk si può verificare la corretta registrazione SIP:

docker exec -it ai-phone-asterisk asterisk -rvvv
pjsip show registrations
pjsip show endpoints
exit

Verificare il funzionamento prima di deviare le chiamate

A questo punto è tutto pronto per provare il risponditore AI. Prima di configurare, ad esempio, una deviazione su mancata risposta dal cellulare conviene eseguire alcuni test sul numero SIP, chiamandolo come se fossimo un normale interlocutore esterno.

In questo modo possiamo verificare separatamente tutta la catena: risposta di Asterisk, riproduzione dei prompt, registrazione della voce, trascrizione rapida con Whisper, classificazione della chiamata, scelta del messaggio finale e successivo invio della notifica Telegram.

È un passaggio importante perché consente di individuare eventuali problemi senza coinvolgere ancora il numero mobile principale. I test seguenti simulano quattro situazioni diverse: telemarketing energetico, spam commerciale generico, chiamata utile e chiamata non classificabile con sufficiente sicurezza.

Notifica push Telegram chiamate indesiderate

Test: telemarketing luce e gas

Alla prima domanda dell’assistente rispondi (attenzione: non ci riferiamo ad alcuna azienda esistente, i nomi sono di pura fantasia):

Sono Marco di Energia Facile

Alla seconda:

La contatto per una questione contrattuale sulla fornitura di energia elettrica e per un rimborso.

Whisper dovrebbe riconoscere termini sufficientemente indicativi sia del settore energetico sia della natura commerciale della chiamata. Il comportamento atteso è:

Un momento. (…) Dal 19 giugno 2026 le sollecitazioni telefoniche per proporre contratti luce e gas sono vietate, salvo le eccezioni previste. Questa numerazione è iscritta al Registro pubblico delle opposizioni e nessun consenso è stato prestato dal titolare. Vi invitiamo a cancellare ogni dato personale e a non effettuare nuove chiamate. La telefonata è stata registrata e archiviata.

Test: spam commerciale non energetico

Alla prima domanda rispondi:

Sono Luca di Fast Internet.

E poi:

La contatto per una nuova offerta fibra con uno sconto sulla tariffa.

In questo caso sono presenti segnali commerciali come offerta, fibra, sconto e tariffa, ma non elementi riconducibili al settore energetico. Il comportamento atteso è quindi il seguente:

Un momento. (…) Questa numerazione è iscritta al Registro pubblico delle opposizioni e nessun consenso è stato prestato dal titolare. Vi invitiamo a cancellare ogni dato personale e a non effettuare nuove chiamate. La telefonata è stata registrata e archiviata.

Test: chiamata utile

Rispondi con questa formula:

Sono Luca del corriere Spedizioni Facili per Te

E poi:

Ho un pacco da consegnare.

Termini come corriere, pacco e consegnare rientrano tra gli indicatori di una possibile chiamata utile. Non deve quindi essere riprodotto alcun messaggio antispam.

Il comportamento atteso è:

Un momento. Io sono un assistente digitale, riferirò la chiamata.

La successiva notifica Telegram riporta la trascrizione e classifica la chiamata come utile.

Test: chiamata incerta

Infine simuliamo una telefonata che non contiene né segnali evidenti di spam né elementi sufficienti per identificarla come utile:

Sono Mario Rossi.

E poi:

Avrei bisogno di parlare con Michele.

In assenza di indicatori affidabili, il sistema adotta volutamente un comportamento prudente e non blocca la chiamata come spam. Il messaggio atteso è il seguente:

Io sono un assistente digitale, riferirò la chiamata.

Se tutti e quattro i test producono il comportamento previsto, significa che Asterisk, Whisper, gli script di classificazione, i file audio e Telegram stanno lavorando correttamente insieme. Solo a questo punto ha senso attivare la deviazione condizionata delle chiamate dal cellulare verso il numero SIP.

Controllare le decisioni e lo storico locale

Log del classificatore:

cd /opt/ai-phone
docker compose logs --tail=80 transcriber

Esempio energia:

Quick transcript: ...
Chiamata ...: TELEMARKETING ENERGIA
quick_spam_score=3 energy_score=5

Esempio spam:

Chiamata ...: SPAM / COMMERCIALE NON ENERGIA
quick_spam_score=2 energy_score=0

Le chiamate vengono registrate nella cartella che segue:

/opt/ai-phone/data/reports/history.tsv

Ultime chiamate:

tail -20 /opt/ai-phone/data/reports/history.tsv

Report più recente:

ls -1t /opt/ai-phone/data/reports/*.report.txt | head -1 | xargs cat

Registrazioni:

/opt/ai-phone/data/audio/

Report:

/opt/ai-phone/data/reports/

Come deviare le chiamate verso il numero SIP

Dopo aver verificato che il numero SIP e l’assistente automatico funzionino correttamente, si può configurare la deviazione delle chiamate dal cellulare verso il numero VoIP. La soluzione più prudente consiste nell’attivare un trasferimento condizionato, per esempio soltanto quando non si risponde entro un certo intervallo, quando il telefono è occupato oppure quando risulta non raggiungibile.

In questo modo il numero SIP non sostituisce il cellulare: interviene soltanto nelle situazioni previste e permette all’assistente di rispondere al posto nostro. Le modalità di attivazione dipendono dall’operatore mobile e possono essere configurate tramite codici di rete, impostazioni dello smartphone oppure servizi messi a disposizione dall’operatore.

Alcuni operatori mobili prevedono la deviazione su mancata risposta con il comando:

*61*[NUMERO]#

Al posto di NUMERO va ovviamente indicato il numero SIP sul quale è in ascolto il risponditore automatico.

Per verificare lo stato della deviazione: *#61#

Per disattivarla: #61#

Il numero SIP deve essere inserito nel formato accettato dalla rete mobile, eventualmente comprensivo di prefisso internazionale. Prima di lasciare la deviazione attiva in modo permanente conviene effettuare una chiamata reale di prova e verificare sia il corretto inoltro verso Asterisk sia gli eventuali costi applicati dall’operatore per la tratta trasferita.

Pulizia periodica delle registrazioni

Se non si volessero conservare indefinitamente gli audio, è ad esempio possibile eliminare i file WAV più vecchi di 30 giorni:

find /opt/ai-phone/data/audio \
-type f \
-name "*.wav" \
-mtime +30 \
-delete

Stessa cosa per i report:

find /opt/ai-phone/data/reports \
-type f \
-mtime +90 \
-delete

Valuta attentamente tempi e modalità di conservazione delle registrazioni in funzione dell’uso concreto del sistema e degli eventuali obblighi applicabili.

Per fare un backup completo:

cd /opt
tar czf ai-phone-backup.tar.gz ai-phone/

Il file di backup sarà il sguente:

/opt/ai-phone-backup.tar.gz

È opportuno ricordare che il backup così creato contiene credenziali Telegram, credenziali SIP, registrazioni telefoniche e trascrizioni. Va quindi protetto adeguatamente.

Disinstallazione completa lato server

Se un giorno o l’altro si volesse rimuovere completamente il risponditore automatico dal server VPS, è innanzi tutto necessario disattivare la deviazione delle chiamate dal cellulare. Successivamente si possono fermare e rimuovere i container:

cd /opt/ai-phone
docker compose down

Per procedere con la rimozione delle immagini specificamente scaricate dal progetto:

docker image rm ghcr.io/ggml-org/whisper.cpp:main 2>/dev/null || true

Per l’immagine Asterisk costruita localmente, prima va individuato il nome con docker images e poi si può procedere alla rimozione. Non usare il comando docker system prune -a su un VPS che ospita altri container, perché si cancellerebbero immagini e risorse appartenenti ad altri servizi.

Eliminare tutti i dati del progetto

Il comando seguente elimina definitivamente configurazioni, modelli, credenziali, audio e trascrizioni mentre quello successivo disabilita le porte utilizzate per il progetto:

rm -rf /opt/ai-phone

ufw delete allow 5060/udp
ufw delete allow 10000:10019/udp

Se si fosse impostato uno swap dedicato, come spiegato in apertura, si può usare la seguente sintassi:

swapoff /swap-ai-phone

Da /etc/fstab va rimossa la riga /swap-ai-phone none swap sw 0 0. Alla fine si deve digitare:

rm -f /swap-ai-phone

Se sul VPS Docker è utilizzato da altri servizi, è fondamentale lasciarlo installato. Solo su una macchina dove si è sicuri che Docker non serva più a nulla è possibile rimuoverlo.

Note finali

Il progetto descritto in questa guida deve essere considerato una traccia tecnica e sperimentale, da adattare al proprio scenario, al provider utilizzato e alle regole applicabili nel contesto in cui è impiegato.

La configurazione, l’uso delle registrazioni, la conservazione dei dati, i messaggi riprodotti ai chiamanti e l’eventuale deviazione delle telefonate restano quindi sotto la responsabilità dell’utente, che deve verificarne correttezza, opportunità e conformità prima di utilizzare il sistema in modo continuativo.

L’architettura proposta non pretende inoltre di essere definitiva. Al contrario, rappresenta una base facilmente espandibile: si potrebbe, per esempio, fare in modo che il numero SIP richiami automaticamente il titolare quando la chiamata è classificata come utile, oppure inoltrare la telefonata in tempo reale verso un altro numero, creare regole diverse in base all’orario, integrare rubriche e whitelist, aggiungere un’interfaccia Web o memorizzare eventi e trascrizioni in un database strutturato.

Un ulteriore sviluppo riguarda l’intelligenza artificiale vera e propria. Nel progetto attuale Whisper è utilizzato per la trascrizione, mentre la classificazione si basa soprattutto su regole e punteggi deterministici. Nulla vieta di sostituire o affiancare questo meccanismo con un modello AI locale o remoto capace di interpretare il significato complessivo della conversazione, distinguere meglio chiamate commerciali, tentativi di frode e contatti legittimi, produrre riassunti automatici o decidere dinamicamente quale risposta fornire. Con hardware più potente si potrebbero utilizzare anche modelli linguistici eseguiti direttamente sul server, mantenendo l’intero flusso sotto il proprio controllo.

L’aspetto più interessante del progetto è proprio questo: non si tratta di un prodotto chiuso, ma di una base su cui costruire un assistente telefonico personale sempre più sofisticato, modificando liberamente logica, modelli, messaggi e comportamento in funzione delle proprie specifiche esigenze.

Ti consigliamo anche

Link copiato negli appunti