Développement de bots
Bots vocaux
Quand quelqu'un appelle un bot depuis l'application, c'est un modèle vocal d'IA qui répond. Activation, instructions et voix, notifications d'appel, outils, serveurs MCP, clé propre, transcription, mode flux, Apple Watch et « Garder la conversation » : le guide des propriétaires de bot.
Un bot vocal est un bot que l'on peut appeler depuis l'application. Ce n'est pas le code du propriétaire du bot qui décroche, mais le modèle vocal d'IA de Pabal ; le propriétaire du bot fixe, par la Bot API, les instructions, le message d'accueil, la voix, la langue et les limites que ce modèle doit suivre. À chaque début et fin d'appel, il reçoit une mise à jour voice_call, et pendant l'appel il peut laisser le modèle appeler le code du bot (des outils) pour retrouver une commande ou vérifier une réservation. Il existe aussi un mode flux où votre propre serveur reçoit directement l'audio à la place du modèle d'IA. Si vous créez votre premier bot, commencez par Créer un bot.
État actuel
On ne peut appeler un bot vocal que sur un serveur qui remplit toutes les conditions ci-dessous. Les méthodes existent aussi sur un serveur où la voix est désactivée, si bien que l'on peut lire les réglages. Mais tant que la voix n'est pas activée sur ce serveur et que l'opérateur n'a pas autorisé le bot, aucun réglage n'est enregistré, sauf pour désactiver quelque chose (couper la voix, couper la transcription, quitter le mode flux, effacer votre propre clé, supprimer un serveur MCP) : 403 VOICE_NOT_AVAILABLE. Pour savoir si votre bot peut prendre des appels en ce moment, regardez available et unavailable_reason dans getVoiceCallSettings.
| Fonction | Pour l'activer (toutes les conditions) | pabal.me aujourd'hui |
|---|---|---|
| Appels aux bots vocaux | Le serveur dispose d'une passerelle vocale et d'un relais d'appel (TURN) · la politique de confidentialité publie la section sur les appels aux bots (bot-calls) · l'opérateur a autorisé le bot · avant l'ouverture au public, seuls les testeurs choisis par l'opérateur peuvent appeler · l'application de l'appelant est une version qui contient l'écran de confirmation (bureau pabal-bc2 — tant que l'écran de confirmation n'existe qu'en coréen et en anglais, pabal-bc1 est aussi accepté —, Android 62151, iPhone build 5 ou plus, app pour Apple Watch build 1 ou plus) — ces versions ne sont pas encore publiées | Désactivé |
| Appeler depuis l'Apple Watch | Appels aux bots vocaux + l'API montre du serveur + le point d'entrée audio de la montre (pabal-voice-edge) + une passerelle qui accepte les appels directs (direct) · une version de l'app pour Apple Watch au moins égale à TELEGRAM_VOICE_MIN_BUILD_WATCHOS · pas encore pour les bots qui utilisent des outils, des serveurs MCP, une clé propre, le mode flux ou la transcription du propriétaire du bot | Désactivé — en test sur le serveur de développement |
| Garder la conversation (copie de l'appelant) | Appels aux bots vocaux + la section bot-calls-keep-text de la politique + une passerelle qui annonce keep_text (0.9.0) + une version de l'application avec la case à cocher · examen juridique du texte de l'écran de confirmation | Désactivé — pas encore examiné juridiquement |
| Appels des bots | La section bot-calls-outbound de la politique de confidentialité et la section voice-bot-owners-outbound des conditions d'utilisation sont publiées (ainsi que la section voice-bot-owners, pour que les propriétaires acceptent la version 4 des conditions) · l'interrupteur général de l'opérateur, après l'examen juridique · le propriétaire du bot l'active dans @BotFather (conditions, version 4, buts) · l'examen du bot par l'opérateur · l'autorisation de la personne appelée (une version de l'application avec l'écran d'autorisation) · une passerelle avec les enregistrements approuvés des annonces d'appel sortant · une langue du bot coréenne ou anglaise | Désactivé — en attente de l'examen juridique |
| Langues du bot autres que le coréen et l'anglais (7 langues) | Appels aux bots vocaux + une passerelle qui prend en charge la langue du bot (depuis la version 0.7.0, elle prend en charge les 9) · l'opérateur a lu et autorisé les instructions et le message d'accueil du bot (même sur un serveur qui accepte tous les bots) | Désactivé — pas encore déployé |
| Écran de confirmation et annonces vocales dans d'autres langues que le coréen et l'anglais (8 langues) | Intégrés aux versions de l'application et à l'image de la passerelle langue par langue, une fois l'examen juridique et la vérification par des locuteurs natifs terminés. D'ici là, même un appelant dont l'application est dans l'une de ces langues voit l'écran de confirmation en anglais et entend les annonces en anglais | Brouillons — pas encore validés |
| Conditions du propriétaire de bot (version 4) | Les conditions d'utilisation publient la section des propriétaires de bot (voice-bot-owners). La transcription, votre propre clé, le mode flux, les serveurs MCP et les appels des bots exigent tous d'abord ces conditions | Impossible de les accepter |
| Outils | Appels aux bots vocaux + une passerelle qui prend en charge les outils | Désactivé |
| Serveurs MCP | Composants MCP · adresse de sortie dédiée (condition d'exploitation — le code ne la vérifie pas) · la section bot-calls-mcp de la politique et la section des propriétaires de bot des conditions · acceptation des conditions du propriétaire de bot · une version de l'application qui affiche dans l'écran de confirmation le destinataire MCP du bot (avant cela, on peut enregistrer un serveur, mais aucun appel n'emporte d'outil MCP) | Désactivé |
| Votre propre clé de modèle | Clé maîtresse du serveur · activation par l'opérateur une fois que les applications affichent l'écran de confirmation (version 2) qui montre le destinataire d'un bot à clé propre · conditions du propriétaire de bot · une passerelle qui prend en charge les clés des propriétaires (par défaut, seulement la clé Google) | Désactivé — les clés ne sont même pas acceptées |
| Transcription | Autorisation de l'opérateur dans la page d'administration après un examen juridique (désactivée par défaut) · conditions du propriétaire de bot · l'interrupteur du propriétaire · une passerelle qui dispose de l'annonce vocale de transcription · appel géré | Désactivé |
| Mode flux | Clé maîtresse du serveur · la section bot-calls-stream de la politique et la section des propriétaires de bot des conditions · une passerelle qui prend en charge les appels en mode flux · une version de l'application avec l'écran de confirmation du mode flux (qui n'existe pas encore) | Désactivé |
- L'appelant (la personne qui appelle) doit avoir 19 ans ou plus ; avant d'appeler, il voit l'écran de confirmation « An AI answers this call » (« Une IA répond à cet appel ») et, une fois connecté, entend d'abord une annonce vocale sur l'IA (Ce que voit l'appelant). Les applications sans écran de confirmation (y compris l'application Telegram d'origine et les clients comme Telethon) n'affichent pas de bouton d'appel pour le bot.
- L'audio des appels gérés part chez Google — l'API Gemini Live de Vertex AI, dans l'Iowa aux États-Unis (Google Cloud
us-central1). Pabal n'enregistre pas l'audio. Google ne conserve ni l'audio ni le texte après avoir produit la réponse, mais un contenu que ses contrôles automatiques soupçonnent d'abus peut être consigné jusqu'à 90 jours (c'est ce qu'indique aussi l'écran de confirmation). C'est le seul moteur en production, et Pabal ne bascule jamais en cachette vers un autre fournisseur (pas de bascule automatique). - Contrairement aux appels entre personnes, les appels aux bots ne sont pas chiffrés de bout en bout : la passerelle vocale de Pabal déchiffre l'audio pour le transmettre au modèle d'IA. L'écran de confirmation de l'appelant le lui dit.
Le parcours d'un appel
Explication du diagramme
- Trois zones : l'application de l'appelant → le serveur de l'opérateur Pabal (serveur Pabal, passerelle vocale, serveur relais) → hors de Pabal (le modèle d'IA de Google, le programme du propriétaire du bot). Le code du propriétaire du bot ne voit pas un seul octet d'audio.
- ① Appel : l'application signale au serveur le consentement donné dans l'écran de confirmation, puis appelle. Le serveur vérifie à la fois l'appelant (consentement — il déclare lui-même avoir 19 ans ou plus · version de l'application · testeur · limites) et le bot (autorisation · réglages · limites), puis décroche ou termine l'appel par une tonalité d'occupation.
- ② Liaison : le serveur confie l'appel à la passerelle vocale en lui envoyant les instructions, le message d'accueil, la voix, la langue et les outils du propriétaire du bot. La passerelle place toujours d'abord les règles de base de Pabal (dire que l'on est une IA, etc.), puis les instructions.
- ③ Bot API : le programme du propriétaire fixe les réglages, reçoit
voice_callau début et à la fin d'un appel, etvoice_call_tool_queryquand le modèle appelle un outil, auquel il répond. La flèche bleue de ③ correspond à ce que le serveur envoie au bot, la flèche grise en dessous à ce que le bot envoie au serveur. - ④ Audio (ligne du bas) : l'application et la passerelle ne connaissent pas l'adresse l'une de l'autre et n'échangent l'audio que par le serveur relais (coturn) ; la passerelle transmet l'audio au modèle de Google et en reçoit la réponse. La case rouge marque l'endroit où l'audio sort de Pabal.
- En mode flux, le serveur du propriétaire du bot prend la place de Google en ④ (Mode flux).
Les appels depuis l'Apple Watch (en préparation)
L'app pour Apple Watch n'utilise pas MTProto. La montre se connecte par l'API montre du serveur (JSON sur HTTPS), reçoit la liste des bots et l'écran de confirmation, et passe l'appel. Le serveur applique la même décision d'admission qu'aux appels des applications, puis donne à la montre un ticket à usage unique (15 secondes) valable seulement pour cet appel et confie l'appel à la passerelle. La montre se connecte avec ce ticket au point d'entrée audio (WSS, pabal-voice-edge), qui relie l'audio au processus de la passerelle chargé de cet appel. Il n'y a ni échange de clés (DH) ni relais d'appel (coturn). Mais la passerelle déchiffre toujours l'audio pour le transmettre au modèle : l'appel n'est donc pas chiffré de bout en bout, et l'audio va au même endroit. Le propriétaire du bot reçoit exactement le même voice_call que pour un appel depuis une application (seul le moment de started diffère, voir ci-dessous).
Activer les appels vocaux
C'est une personne, dans @BotFather, qui active les appels vocaux : accepter les règles des bots vocaux est un engagement du propriétaire du bot, pas du jeton. Tous les autres réglages se font aussi bien dans @BotFather que par la Bot API.
- Demandez à l'opérateur d'autoriser votre bot. Sur un serveur qui n'accepte que les bots autorisés (le réglage par défaut), tant que l'opérateur n'a pas autorisé le bot avec son identifiant (les chiffres avant
:dans le jeton) dans l'onglet Voix de la page d'administration, @BotFather n'affiche pas de bouton d'appels vocaux, et toute modification des réglages renvoie403 VOICE_NOT_AVAILABLE. Avant l'ouverture au public, demandez aussi à être ajouté comme testeur avec le compte qui servira aux essais (votre identifiant d'utilisateur estmessage.from.iddansgetUpdatesaprès avoir envoyé un message au bot). - Rédigez les instructions. C'est le texte qui décrit ce que fait le bot au téléphone. Définissez-les avec
setVoiceCallSettingsci-dessous, ou envoyez-les avec le bouton Instructions (지시문) de l'écran des appels vocaux de @BotFather. Sans instructions, on ne peut pas activer les appels (sauf en mode flux). - Dans @BotFather :
/mybots→ votre bot → Appels vocaux (음성 통화) → Activer (켜기). La première fois, les règles des bots vocaux s'affichent ; appuyez sur Accepter les règles et activer (규칙에 동의하고 켜기) pour activer. Les fois suivantes, BotFather demande seulement « Activer ? » (켤까요?). - Attendez une nouvelle autorisation. Sur un serveur qui n'accepte que les bots autorisés, l'autorisation porte sur les instructions, le message d'accueil et les outils du moment : si vous modifiez les instructions après l'autorisation, vous obtenez
unavailable_reason: "awaiting_review". Le bot ne reprend les appels qu'une fois que l'opérateur a vérifié et autorisé à nouveau. Il en va de même chaque fois que vous modifiez l'un des trois (ou l'adresse de flux en mode flux). Si la langue du bot n'est ni le coréen ni l'anglais, même sur un serveur qui accepte tous les bots, le bot ne prend des appels qu'une fois que l'opérateur a lu et autorisé les instructions et le message d'accueil (Voix et langues). - Vérifiez que
availablevauttruedansgetVoiceCallSettings, puis appelez avec le bouton d'appel du profil du bot, depuis une version de l'application qui contient l'écran de confirmation.
Pour l'instant, BotFather répond en coréen. Les règles disent : 1. le bot dit toujours qu'il est une IA et ne peut pas se faire passer pour un humain ; 2. il ne peut pas usurper l'identité d'un organisme, d'une entreprise ou d'une personne réels ; 3. il ne peut pas demander de numéro de carte, de numéro d'identification national, de mot de passe ni de code de vérification ; 4. Pabal n'enregistre pas l'audio des appels, qui est envoyé à Google (Vertex AI) pour produire les réponses ; 5. seules les personnes de 19 ans ou plus peuvent appeler un bot ; 6. en cas d'infraction, l'opérateur peut suspendre les appels vocaux ou le bot. Après l'acceptation, BotFather confirme que les appels vocaux sont activés, affiche l'état (voix Kore, langue coréenne…) et rappelle qu'à chaque appel l'annonce « You're now talking to an AI voice bot. » passe d'abord, puis le bot salue.
Sur un serveur qui n'accepte que les bots autorisés, l'état peut s'afficher ainsi : "켜짐 — 바뀐 지시문·인사말을 운영자가 확인할 때까지 전화를 받지 않아요" (« Activé — le bot ne prend pas d'appels tant que l'opérateur n'a pas vérifié les instructions et le message d'accueil modifiés ») (étape 4 ci-dessus). Si la langue du bot n'est ni le coréen ni l'anglais, l'état s'affiche ainsi sur n'importe quel serveur tant que l'opérateur ne l'a pas autorisé : "켜짐 — 한국어·영어가 아닌 봇은 운영자가 지시문·인사말을 읽고 허용할 때까지 전화를 받지 않아요 (자동 규칙 검사가 한국어와 영어만 읽어요)" (« Activé — un bot dans une autre langue que le coréen et l'anglais ne prend pas d'appels tant que l'opérateur n'a pas lu et autorisé ses instructions et son message d'accueil (le contrôle automatique des règles ne lit que le coréen et l'anglais) »).
L'écran des appels vocaux propose les boutons activer/désactiver, instructions, message d'accueil, voix, langue, limites, utilisation, transcription, clé propre, mode flux et (sur un serveur où MCP est activé) serveurs MCP. Même une fois la voix désactivée ou l'autorisation retirée, désactiver et effacer restent toujours possibles : couper la voix, effacer votre propre clé, quitter le mode flux, supprimer un serveur MCP.
Le nom du bot est lui aussi contrôlé contre l'usurpation d'identité, à l'activation et à chaque appel. Un bot qui porte le nom d'un organisme ou d'une entreprise réels ne s'active pas : VOICE_CONTENT_REFUSED: name: impersonation. Si le bot appartient vraiment à cet organisme, prévenez le service client du serveur.
Réglages — setVoiceCallSettings
Définit les réglages vocaux du bot. C'est une modification partielle : seul ce que vous envoyez change ; le serveur vérifie l'ensemble puis écrit tout d'un coup, ou refuse tout. Si rien ne change, rien n'est écrit.
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
"instructions": "Vous êtes le standard téléphonique de la boutique Aide-Commande. Répondez brièvement et poliment. La boutique est ouverte de 10 h à 20 h.",
"greeting": "Aide-Commande, bonjour. Que puis-je faire pour vous ?",
"voice": "kore",
"language": "ko-KR",
"max_call_duration": 180
}'
# {"ok":true,"result":true}
| Paramètre | Type | Description |
|---|---|---|
enabled | Boolean | Activer ou désactiver. Pour activer, il faut d'abord avoir accepté les règles dans @BotFather (sinon 403 VOICE_RULES_NOT_ACCEPTED) et, en mode géré, avoir des instructions. Désactiver est toujours possible : les appels en cours vont jusqu'au bout, seuls les nouveaux appels ne sont plus pris |
instructions | String | Instructions que le modèle suit. Jusqu'à 8 192 octets UTF-8. "" les efface — un bot géré activé ne peut pas les vider. Plus elles sont longues, plus l'appel coûte cher ; et comme l'appelant peut parvenir à les découvrir, n'y mettez ni mot de passe ni clé |
greeting | String | Première phrase du bot après l'annonce sur l'IA. Jusqu'à 512 octets. Avec "", le modèle salue brièvement de lui-même |
voice | String | Identifiant de voix (liste), kore par défaut |
language | String | La langue que le bot parle et comprend : l'un des 9 identifiants (ko-KR par défaut, liste). zh-CN est enregistré sous la forme cmn-CN, et un identifiant qui ne diffère que par la casse (ja-jp), sous la forme de la liste. Si ce n'est ni le coréen ni l'anglais, le bot ne prend des appels qu'une fois que l'opérateur a lu et autorisé les instructions et le message d'accueil. L'annonce vocale sur l'IA, au tout début, n'utilise pas cette langue mais celle de l'écran de confirmation vu par l'appelant |
max_call_duration | Integer | Durée maximale d'un appel (secondes). Au moins 60, au plus limits.max_call_duration (le plus petit des plafonds de l'opérateur et du serveur, 600 par défaut). Valeur par défaut du bot : 300 |
max_concurrent_calls | Integer | Nombre d'appels pris en même temps. Au moins 1, au plus limits.max_concurrent_calls (2 par défaut). Valeur par défaut du bot : 2 |
tools | Array | Outils du bot que le modèle peut appeler pendant un appel (Outils). [] les supprime tous |
transcript | Boolean | Interrupteur de la transcription (Transcription) |
mode, stream_url, stream_secret | String | Mode flux (Réglage du mode flux) |
model, api_key | Refusés — VOICE_FIELD_NOT_AVAILABLE. Le serveur choisit le modèle, et une clé ne passe que par setVoiceCallApiKey |
- Règles de contenu : les instructions, le message d'accueil et les descriptions d'outils sont contrôlés à l'enregistrement et à chaque appel. Faire dire au bot qu'il est humain (
claims_human), lui faire cacher qu'il est une IA (hides_ai), lui demander d'ignorer les règles de Pabal (overrides_rules), usurper l'identité d'un organisme ou d'une entreprise réels (impersonation), se présenter comme un organisme officiel (official_claim), demander un numéro de carte, un numéro d'identification national, un mot de passe ou un code de vérification (asks_secrets), un texte qui ressemble à une clé secrète (looks_like_secret) ou des caractères invisibles (hidden_text) sont refusés avecVOICE_CONTENT_REFUSED: <champ>: <code>. Une phrase négative comme « Ne demandez jamais de mot de passe » passe. - Les règles de base de Pabal passent toujours en premier. À chaque appel, le modèle lit, dans l'ordre, les règles de Pabal → les instructions du propriétaire du bot → la conclusion des règles de Pabal, et les instructions sont placées dans un bloc marqué « texte du propriétaire du bot ». Les instructions ne peuvent ni remplacer ni précéder les règles de Pabal.
- Jusqu'à 30 modifications par heure (en comptant @BotFather, et seulement les modifications effectives). Au-delà :
429 Too Many Requests: retry after Navecparameters.retry_after. - Avec un formulaire (
-d name=value), passeztoolssous forme de chaîne JSON. - Un bot connecté uniquement en MTProto envoie le même JSON avec
bots.sendCustomRequest(Bots MTProto).
Voir les réglages actuels — getVoiceCallSettings
curl -s "https://pabal.me/bot$BOT_TOKEN/getVoiceCallSettings"
{
"ok": true,
"result": {
"enabled": true,
"available": true,
"suspended": false,
"rules_accepted": true,
"instructions": "Vous êtes le standard téléphonique de la boutique Aide-Commande. …",
"greeting": "Aide-Commande, bonjour. Que puis-je faire pour vous ?",
"voice": "kore",
"language": "ko-KR",
"max_call_duration": 180,
"max_concurrent_calls": 2,
"limits": {"max_call_duration": 600, "max_concurrent_calls": 2, "bot_minutes_per_day": 60, "owner_minutes_per_day": 120},
"voices": [{"id": "zephyr", "name": "Zephyr", "description": "Bright"}, …],
"languages": ["ko-KR", "en-US", "ja-JP", "cmn-CN", "fr-FR", "es-US", "id-ID", "vi-VN", "ar-EG"],
"testers_only": true,
"mode": "managed",
"has_stream_secret": false,
"owner_terms_accepted": false,
"stream_available": false,
"updated_date": 1790380712,
"tools": [],
"tool_limits": {"max_tools": 16, "max_definition_bytes": 8192, "definition_bytes": 0, "mcp_tools": 0},
"tool_delivery": "none",
"transcript": false,
"transcript_available": false,
"transcript_delivery": "get_updates",
"has_api_key": false,
"own_keys_available": false,
"mcp_available": false
}
}
| Champ | Signification |
|---|---|
enabled | Si le propriétaire du bot a activé la voix |
available, unavailable_reason | Si le bot peut prendre un appel en ce moment, quel que soit l'appelant, et sinon pourquoi (tableau ci-dessous) |
rules_accepted, owner_terms_accepted | Acceptation des règles des bots vocaux et des conditions actuelles du propriétaire de bot (version 4) |
limits | Valeurs maximales réglables sur ce serveur et limites quotidiennes (minutes) |
voices, languages | Voix et langues disponibles |
testers_only | true si, avant l'ouverture au public, seuls les testeurs choisis par l'opérateur peuvent appeler |
tools, tool_limits, tool_delivery, tool_delivery_problem | Outils et leurs limites, chemin qu'emprunteraient les requêtes d'outil en ce moment (webhook·get_updates·mtproto·none) et raison pour laquelle elles ne passent pas (Recevoir les requêtes d'outil) |
mode, stream_url, stream_secret_fingerprint, has_stream_secret, stream_available, stream_ignores | Mode flux — le secret n'apparaît jamais, seulement une empreinte de 8 caractères |
transcript, transcript_available, transcript_unavailable_reason, transcript_delivery, transcript_delivery_problem | Interrupteur de la transcription, et si elle sera réellement transmise |
has_api_key, api_key, own_keys_available | Votre propre clé de modèle — la clé n'apparaît jamais, seulement le fournisseur, l'empreinte, la date de réglage et si elle est utilisable |
mcp_available, mcp_servers, mcp_unavailable_reason | Serveurs MCP |
outbound | L'état des appels du bot (Appels des bots — réglages et utilisation) |
updated_date | Dernière modification (secondes Unix) |
unavailable_reason | Signification |
|---|---|
voice_off | La voix est désactivée sur ce serveur, ou la politique de confidentialité n'a pas encore de section sur les appels aux bots |
kill_switch | L'opérateur a arrêté tous les appels vocaux |
bot_voice_off | Le propriétaire du bot n'a pas activé la voix |
not_allowed | L'opérateur n'a pas autorisé ce bot |
awaiting_review | L'opérateur n'a pas encore vérifié les instructions, le message d'accueil ou les outils modifiés (en mode flux, l'adresse de flux). Si la langue du bot n'est ni le coréen ni l'anglais, l'opérateur doit lire et autoriser les instructions et le message d'accueil, même sur un serveur qui accepte tous les bots |
suspended | L'opérateur a suspendu la voix de ce bot (ou du compte de son propriétaire) |
name_refused, text_refused | Le nom du bot, ou les instructions, le message d'accueil ou les descriptions d'outils enregistrés, ne respectent pas les règles de contenu actuelles. text_refused apparaît aussi quand la langue choisie pour le bot a été retirée de la liste et n'est plus utilisable — choisissez de nouveau une langue |
no_relay, no_gateway | Le serveur n'a pas de relais d'appel ou pas de passerelle vocale |
no_gateway_with_tools, no_own_key_gateway | Aucune passerelle ne peut utiliser les outils ou la clé propre de ce bot |
no_gateway_with_language | Aucune passerelle connectée ne prend en charge la langue de ce bot (Voix et langues) |
own_key_unavailable, stream_unavailable | La clé propre ou le mode flux configurés sont inutilisables en ce moment, donc le bot ne prend pas d'appels (il ne les prend pas non plus avec la clé de la plateforme à la place) |
Voix et langues
Vous choisissez parmi les 30 voix prédéfinies de Gemini Live (kore par défaut). Il n'est pas possible de téléverser ou de cloner une voix. Le bouton Voix (목소리) de @BotFather permet de choisir par nom et par caractère.
| id | Caractère | id | Caractère | id | Caractère |
|---|---|---|---|---|---|
zephyr | lumineuse | puck | enjouée | charon | nette |
kore | ferme | fenrir | enthousiaste | leda | juvénile |
orus | ferme | aoede | fraîche | callirrhoe | décontractée |
autonoe | lumineuse | enceladus | soufflée | iapetus | claire |
umbriel | décontractée | algieba | douce | despina | douce |
erinome | claire | algenib | rocailleuse | rasalgethi | nette |
laomedeia | enjouée | achernar | tendre | alnilam | ferme |
schedar | posée | gacrux | mûre | pulcherrima | affirmée |
achird | amicale | zubenelgenubi | détendue | vindemiatrix | bienveillante |
sadachbia | vive | sadaltager | érudite | sulafat | chaleureuse |
La langue (language) est l'une des 9 ci-dessous. C'est la langue que le bot parle et comprend, et c'est dans cette langue qu'il répond. Dans @BotFather, on la choisit avec le bouton Langue (언어) de l'écran des appels vocaux.
| id | Langue | À savoir |
|---|---|---|
ko-KR | Coréen | Par défaut |
en-US | Anglais | — |
ja-JP | Japonais | — |
cmn-CN | Chinois (mandarin) | Ne figure pas dans la liste officielle des langues prises en charge par Google, qui n'en garantit donc pas la qualité (lors des tests, la compréhension comme les réponses étaient exactes). zh-CN est également accepté et enregistré sous la forme cmn-CN |
fr-FR | Français | — |
es-US | Espagnol | — |
id-ID | Indonésien | — |
vi-VN | Vietnamien | — |
ar-EG | Arabe (standard) | Le bot parle l'arabe standard moderne. Le code est ar-EG, le seul code arabe du modèle en temps réel de Google, mais il n'impose aucun dialecte : le bot ne parle un dialecte que si les instructions le demandent |
- Un bot dans une autre langue que le coréen et l'anglais ne prend des appels qu'une fois que l'opérateur a lu et autorisé ses instructions et son message d'accueil — même sur un serveur qui accepte tous les bots. En effet, le contrôle automatique des règles effectué à l'enregistrement (se faire passer pour un humain, usurper une identité, etc.) ne lit que le coréen et l'anglais. Avant l'autorisation, on obtient
unavailable_reason: "awaiting_review", et si vous modifiez les instructions, le message d'accueil ou les outils après l'autorisation, le bot attend de nouveau une autorisation. @BotFather le signale sur l'écran de choix de la langue et après le choix : "한국어·영어가 아닌 언어는 자동 규칙 검사가 읽지 못해서, 운영자가 지시문·인사말을 읽고 허용한 뒤에 전화를 받아요." (« Le contrôle automatique des règles ne sait pas lire les langues autres que le coréen et l'anglais : le bot prend des appels une fois que l'opérateur a lu et autorisé ses instructions et son message d'accueil. »). Dans l'onglet Voix de la page d'administration, l'opérateur voit ce bot avec la mention "한국어·영어가 아닌 봇 (자동 규칙 검사가 읽지 못함): 지시문·인사말을 읽어 주세요" (« Bot dans une autre langue que le coréen et l'anglais (illisible pour le contrôle automatique des règles) : lisez les instructions et le message d'accueil »). - Il faut aussi une passerelle qui prend en charge la langue du bot. En se connectant au serveur, la passerelle annonce les langues de bot qu'elle prend en charge (depuis la version 0.7.0, elle prend en charge les 9) ; une passerelle qui n'annonce aucune langue est réputée ne prendre en charge que le coréen et l'anglais. Si aucune passerelle ne prend en charge la langue du bot, on obtient
no_gateway_with_languageet le bouton d'appel est masqué. L'appel n'est jamais pris dans une autre langue à la place. - L'identifiant est accepté sans tenir compte de la casse (
ja-jp) et enregistré sous la forme de la liste. Une valeur absente de la liste est refusée avecVOICE_LANGUAGE_INVALID: language is one of ko-KR, en-US, ja-JP, cmn-CN, fr-FR, es-US, id-ID, vi-VN, ar-EG, qui rappelle les identifiants acceptés. - Si la langue choisie est plus tard retirée de la liste, le bot garde ses réglages mais ne prend plus d'appels (
text_refused). Il ne les prend pas non plus dans une autre langue à la place : choisissez de nouveau une langue avec le bouton Langue (언어) de @BotFather. - Le khmer ne peut pas encore être choisi comme langue de bot. Le modèle le comprend, mais ses réponses libres en khmer n'ont pas encore été vérifiées par un locuteur natif.
Si l'appelant continue de parler une autre langue, le modèle répond dans cette langue. L'annonce vocale sur l'IA n'utilise pas le language du bot, mais la langue de l'écran de confirmation vu par l'appelant (Ce que voit l'appelant).
Recevoir les notifications d'appel — voice_call
Quand quelqu'un appelle le bot, une mise à jour voice_call arrive. Les trois mises à jour vocales (voice_call, voice_call_tool_query, voice_call_transcript) n'arrivent que si vous les demandez par leur nom : ajoutez-les à allowed_updates de getUpdates ou de setWebhook. Elles n'arrivent pas non plus si la liste est vide (pour les autres types, une liste vide les fait tous arriver).
curl -s "https://pabal.me/bot$BOT_TOKEN/getUpdates" -H 'Content-Type: application/json' \
-d '{"allowed_updates": ["message", "voice_call", "voice_call_tool_query"], "timeout": 30}'
{
"update_id": 7,
"voice_call": {
"id": "4611686018427387905",
"direction": "incoming",
"event": "started",
"from": {"id": 100002, "is_bot": false, "first_name": "Léa", "language_code": "ko"},
"date": 1790380803,
"max_duration": 180
}
}{
"update_id": 9,
"voice_call": {
"id": "4611686018427387905",
"direction": "incoming",
"event": "ended",
"from": {"id": 100002, "is_bot": false, "first_name": "Léa", "language_code": "ko"},
"date": 1790380952,
"start_date": 1790380803,
"duration": 149,
"max_duration": 180,
"end_reason": "caller"
}
}direction:"incoming"pour un appel au bot,"outgoing"pour un appel passé par le bot (mises à jour des appels des bots). Lire l'absence du champ comme un appel au bot, comme avant, reste juste.allowed_updatesest un seul réglage par bot : c'est la dernière valeur donnée pargetUpdatesousetWebhookqui reste (si vous n'en donnez pas, la valeur précédente est conservée). Ainsi, si vous donnez une liste comme dans l'exemple ci-dessus, les types par défaut que vous n'y avez pas mis (edited_message,callback_query) n'arrivent plus — indiquez tous les types que votre bot utilise. Les notifications des appels survenus avant votre demande ne sont pas mises de côté : elles n'arrivent pas même si vous les demandez ensuite.- De l'appelant (
from), vous ne recevez que l'identifiant, le nom et la langue de l'application. Ni le nom d'utilisateur ni le numéro de téléphone (c'est ce que promet l'écran de confirmation). Un compte supprimé donnefirst_name: "Deleted Account". - Avec un webhook, elles arrivent en POST dans l'ordre, comme les autres mises à jour. La file de getUpdates garde en mémoire les 1 000 dernières mises à jour par bot et disparaît au redémarrage du serveur.
- Un bot suspendu ou supprimé ne reçoit rien.
event | Quand | Champs en plus |
|---|---|---|
started | Quand l'application de l'appelant a confirmé l'appel (échange de clés terminé) et que le serveur a signalé à la passerelle que l'appel est établi. Le serveur ne voit pas l'audio : c'est l'« établi » tel que le serveur le connaît. Un appel depuis l'Apple Watch n'a pas d'échange de clés : c'est quand la passerelle signale que l'audio de la montre est connecté | max_duration |
ended | Quand un appel établi se termine. Peut arriver jusqu'à 10 secondes plus tard s'il attend la transcription. Sur un serveur où « Garder la conversation » est activé, tous les appels hors mode flux arrivent jusqu'à 10 secondes plus tard, quel que soit le choix de l'appelant — pour que le propriétaire du bot ne puisse pas connaître ce choix | start_date, duration (secondes d'appel établi, selon l'horloge du serveur), max_duration, end_reason, (si la transcription était activée) transcript |
missed | Un appel qui s'est terminé avant d'être établi (raccroché pendant la sonnerie, etc.) | end_reason |
refused | Seulement quand l'appelant a passé toutes les vérifications mais que l'appel n'a pas pu être pris à cause du bot (appels simultanés, minutes du jour, passerelle) — les autres refus ne sont pas signalés au propriétaire du bot | refused_reason : busy (appels simultanés au complet) · budget (minutes du jour du bot, du propriétaire ou du serveur épuisées) · unavailable (passerelles au complet) |
end_reason : caller (l'appelant a raccroché) · bot (le bot a raccroché, endVoiceCall) · max_length (durée maximale) · failed (échec de la connexion) · operator (opérateur ou arrêt d'urgence) · suspended (bot suspendu ou supprimé).
Pour chaque appel confié à la passerelle, un message d'appel (durée de l'appel, ou « appel annulé ») reste dans la conversation entre l'appelant et le bot (pas pour les appels refusés avant d'être pris). La copie côté bot n'arrive pas en mise à jour message — la trace côté bot, c'est voice_call. Si l'appelant choisit « Garder la conversation », le texte de la conversation reste uniquement dans la conversation de l'appelant, juste sous le message d'appel, et rien ne reste côté bot. Sur un serveur où « Garder la conversation » est activé, 30 numéros de message sont réservés après le message d'appel : le message qui suit le message d'appel a donc un numéro supérieur d'au moins 31 à celui du message d'appel (même pour un appel dont rien n'a été gardé).
Pendant l'appel
Raccrocher — endVoiceCall
curl -s "https://pabal.me/bot$BOT_TOKEN/endVoiceCall" -d voice_call_id=4611686018427387905
farewell(Boolean, true par défaut) : si l'appel est établi, le modèle dit un court au revoir puis raccroche. Si ce n'est pas terminé au bout de 10 secondes, le serveur y met fin. Avecfalse, l'appel est coupé tout de suite. Un appel en mode flux est toujours coupé tout de suite.- Le
voice_call.end_reasonde l'appel terminé vautbot. Si ce n'est pas un appel en cours de ce bot :400 VOICE_CALL_NOT_FOUND.
Donner une note au modèle — sendVoiceCallContext
Indique au modèle, pendant l'appel, ce qu'il doit savoir. Le modèle le lit comme un texte du propriétaire du bot, pas comme des paroles de l'appelant, et les règles de Pabal passent toujours en premier.
curl -s "https://pabal.me/bot$BOT_TOKEN/sendVoiceCallContext" -d voice_call_id=4611686018427387905 \
--data-urlencode "text=Cet appelant vient d'annuler la commande A-1042. S'il pose la question, dites-le-lui." -d respond=false
text: jusqu'à 2 000 caractères (6 144 octets), soumis aux règles de contenu. Avecrespond=true, le modèle réagit aussitôt ; avec false (par défaut), il s'en sert à son prochain tour.- La note part elle aussi au modèle (chez Google pour un appel géré), et le modèle peut la répéter à l'appelant — n'y mettez que ce que l'appelant peut savoir.
- Avant la connexion :
400 VOICE_CALL_NOT_ACTIVE; appel en mode flux ou passerelle qui n'accepte pas les notes :400 VOICE_CONTEXT_NOT_SUPPORTED; plus de 10 par minute et par appel :429(retry_after60).
Utilisation — getVoiceCallUsage
curl -s "https://pabal.me/bot$BOT_TOKEN/getVoiceCallUsage?period=today"
{"ok": true, "result": {"period": "today", "from_date": 1790348400, "to_date": 1790434799,
"calls": 3, "seconds": 412, "missed": 1,
"refused": {"busy": 0, "budget": 0, "unavailable": 0},
"platform_seconds": 412, "own_key_seconds": 0,
"remaining_seconds_today": {"bot": 3188, "owner": 6788}}}
period:today(par défaut) oumonth. Avecmonth, vous pouvez choisir le mois, par exemplemonth=2026-09(un mois futur donneVOICE_USAGE_PERIOD_INVALID).- Les limites du jour et du mois suivent le fuseau horaire du serveur (
Asia/Seoulpar défaut).remaining_seconds_todayest la limite quotidienne moins les appels terminés et le temps réservé par les appels en cours. - Le relevé d'utilisation de chaque appel (sans l'appelant) est conservé 180 jours après la fin du mois, et les totaux mensuels restent tant que le bot existe.
Outils — appeler le code du bot pendant un appel
Si l'appelant demande « Où en est ma commande A-1042 ? », le modèle appelle l'outil check_order du bot, le serveur transmet cette requête au bot, et le modèle rapporte de vive voix la réponse du bot. Le propriétaire du bot définit les outils, et c'est le code du bot qui y répond.
Explication du diagramme
- Trois colonnes : à gauche le modèle et la passerelle vocale, au centre le serveur Pabal, à droite le code du propriétaire du bot. Les pointillés verticaux représentent le temps. Le modèle ne parle jamais directement au bot : chaque requête et chaque réponse passent par le serveur.
- La case grise de ① — le serveur vérifie d'abord les arguments : conformité aux
parametersde l'outil, taille et profondeur, clés non déclarées (retirées avant la transmission), valeurs qui ressemblent à un numéro de carte ou à un numéro d'identification national (elles ne vont pas au bot, et le modèle entend un refus). - Ce que le bot reçoit en ② : l'identifiant de la requête, l'identifiant de l'appel, l'appelant, le nom de l'outil, les arguments (une chaîne JSON) et
budget_ms— les millisecondes qui restent avant que la réponse doive atteindre le serveur. Pendant l'attente, la passerelle comble le silence (case de gauche). - La réponse de ③ est vérifiée elle aussi : ce qui ressemble à un secret, les phrases qui poussent à enfreindre les règles, les numéros de carte ou d'identification nationaux sont refusés, et le modèle entend une erreur. La première réponse est la dernière : une réponse refusée ferme elle aussi la requête.
- ④ Le trait bleu plein est le cas normal, le pointillé rouge le dépassement du délai. Le modèle reçoit le résultat de l'outil uniquement comme des données, jamais comme des consignes ; en cas d'échec, il entend une courte phrase de Pabal et explique la situation à l'appelant.
Définir des outils
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -H 'Content-Type: application/json' -d '{
"instructions": "Vous êtes le standard téléphonique de la boutique Aide-Commande. Répondez brièvement et poliment. Si l'appelant donne un numéro de commande, cherchez l'état de la livraison avec l'outil check_order et communiquez-le.",
"tools": [{
"name": "check_order",
"description": "Cherche l'état de livraison d'après un numéro de commande. À appeler quand l'appelant donne un numéro de commande.",
"parameters": {"type": "object",
"properties": {"order_id": {"type": "string", "description": "Numéro de commande, par ex. A-1042"}},
"required": ["order_id"]},
"timeout_ms": 5000
}]
}'
| Élément | Règle |
|---|---|
| Clés | Seulement ces quatre : name, description, parameters (facultatif), timeout_ms (facultatif) |
| Nombre et taille | 16 au total en comptant les outils du bot et les outils MCP épinglés, et 8 192 octets UTF-8 pour la somme des définitions (name·description·parameters). Pareil pour un bot à clé propre |
name | [A-Za-z_][A-Za-z0-9_-]{0,63}. Ne peut contenir __ (réservé aux noms d'outils MCP), ne peut commencer par pabal, et deux noms ne peuvent se confondre en ignorant la casse |
description | Obligatoire, jusqu'à 1 024 octets, soumise aux règles de contenu. C'est d'après elle que le modèle décide quand appeler l'outil : soyez précis |
parameters | Au premier niveau, {"type": "object"}. Absent = « aucun argument » |
timeout_ms | De 1 000 à 15 000, 5 000 par défaut. Temps maximal pendant lequel le modèle attend la réponse |
parameters n'utilise qu'un sous-ensemble de JSON Schema que les deux moteurs acceptent.
- Seulement dix mots-clés :
type,description,enum,properties,required,items,minimum,maximum,minItems,maxItems. Un seultype(string·number·integer·boolean·array·object). properties·requiredpour un object,items·minItems·maxItemspour un array (itemsobligatoire),minimum·maximumpour un nombre,enumseulement pour une string (1 à 32 valeurs, 64 octets chacune).- Noms de propriétés
[A-Za-z_][A-Za-z0-9_]{0,63}, 24 propriétés par outil, jusqu'à 3 niveaux d'object et 5 niveaux d'object et d'array cumulés. Description de paramètre : 512 octets. - Les paramètres qui demandent un secret sont refusés : si le nom contient un mot comme
pin·cvv·password·otp·ssn·rrn·cardnumber, ou si la description parle de numéro de carte, de mot de passe, de code de vérification ou de numéro d'identification national :asks_secrets. - Une erreur signale le premier problème, avec son chemin :
VOICE_TOOL_INVALID: tools[0].parameters.properties.tags: items_missing,VOICE_TOOL_INVALID: tools[0].name: reserved,VOICE_CONTENT_REFUSED: tools[0].description: impersonation. - Les outils se définissent uniquement par la Bot API. L'écran des appels vocaux de @BotFather se contente d'afficher le nom des outils.
- Sur un serveur qui n'accepte que les bots autorisés, ajouter ou modifier un outil empêche le bot de prendre des appels jusqu'à ce que l'opérateur l'autorise à nouveau (
awaiting_review, étape 4 de Activer).
Recevoir les requêtes d'outil
{
"update_id": 813,
"voice_call_tool_query": {
"id": "8190448532776044545",
"voice_call_id": "4611686018427387905",
"from": {"id": 100002, "is_bot": false, "first_name": "Léa", "language_code": "ko"},
"name": "check_order",
"arguments": "{\"order_id\":\"A-1042\"}",
"budget_ms": 4700
}
}
argumentsest le texte d'un objet JSON (4 096 octets et 8 niveaux de profondeur au plus, déjà vérifié par le serveur). Mais ce sont des valeurs que le modèle a tirées des paroles de l'appelant : vérifiez-les à nouveau dans le bot. Connaître un numéro de commande ne doit pas suffire pour entendre la commande de quelqu'un d'autre — ne communiquez que ce qui appartient àfrom.id(l'appelant vérifié par Pabal).budget_msest le nombre de millisecondes entre la réception par le bot et le moment où la réponse doit atteindre le serveur. C'est letimeout_msde l'outil moins le trajet de retour vers le modèle (200 ms) et le trajet jusqu'au bot (100 ms) : il est donc en général inférieur d'environ 300 ms àtimeout_ms. Les horloges des deux côtés ne sont pas utilisées.
Le chemin par lequel la requête atteint le bot est choisi dans cet ordre (getVoiceCallSettings.tool_delivery).
- Par le webhook, s'il y en a un.
voice_call_tool_querydoit figurer dans leallowed_updatesdu webhook ; sinon, la requête ne prend pas un autre chemin et n'arrive pas (tool_delivery_problem: "not_in_allowed_updates"). La requête est un POST envoyé à part : elle n'est pas bloquée par la file ordonnée et n'est pas renvoyée. - Sans webhook, si le bot a appelé getUpdates dans les 15 dernières secondes, en tête du prochain getUpdates. Là aussi, elle doit figurer dans
allowed_updates. Jusqu'à 8 requêtes en attente par bot. - Sinon, si le bot est connecté en MTProto, par
updateBotWebhookJSONQuery(indépendamment deallowed_updates). - Dans tous les autres cas, elle n'arrive pas (
tool_delivery_problem: "not_listening") et le modèle entend « The tool could not be reached. » (« L'outil n'a pas pu être joint »).
Répondre — answerVoiceCallToolQuery
| Paramètre | Description |
|---|---|
tool_query_id | voice_call_tool_query.id de la mise à jour |
result | Le résultat. Du texte, ou un objet ou un tableau dans un corps JSON (un texte qui se lit comme un objet ou un tableau JSON est aussi traité comme du JSON). Jusqu'à 16 384 octets UTF-8 (l'opérateur peut abaisser cette limite) |
error | Message d'échec propre au bot, jusqu'à 512 octets. Le modèle transmet ce message à l'appelant |
- Exactement un des deux,
resultouerror(aucun :TOOL_RESULT_EMPTY; les deux :TOOL_RESULT_INVALID). Renvoie True. - La réponse de l'outil part au modèle (chez Google pour un appel géré), qui en dit le contenu à l'appelant — n'y mettez que ce que l'appelant peut savoir.
- La première réponse est la dernière. Pour une requête en retard ou terminée :
400 TOOL_QUERY_EXPIRED: <raison>—answered(déjà répondu),timeout,cancelled(l'appelant a coupé la parole et le modèle a retiré la requête),call_ended,unreachable. Un identifiant inconnu ou vieux de plus de 10 minutes :TOOL_QUERY_ID_INVALID. - La réponse est elle aussi vérifiée : les caractères invisibles sont retirés, et ce qui ressemble à un secret (
looks_like_secret), les phrases qui poussent à enfreindre les règles (overrides_rules·hides_ai·claims_human·asks_secrets) et les numéros de carte ou d'identification nationaux (sensitive_data) donnentTOOL_RESULT_REFUSED: <code>. Les données du propriétaire, comme l'adresse, le téléphone ou l'URL d'une boutique, passent. Au-delà de la taille :TOOL_RESULT_TOO_LONG·TOOL_ERROR_TOO_LONG. - Avec un webhook, vous pouvez répondre dans le corps de la réponse — il suffit de mettre ceci dans la réponse 200 au POST de la requête d'outil (aucune autre méthode n'est exécutée). Vérifiez toujours l'en-tête
X-Telegram-Bot-Api-Secret-Tokenavant de répondre. Si vous mettez des données de clients dans la réponse sans vérifier, n'importe qui envoyant de fausses requêtes à cette adresse obtient la réponse.
{"method": "answerVoiceCallToolQuery", "tool_query_id": "8190448532776044545",
"result": {"order_id": "A-1042", "status": "En cours de livraison", "eta": "Livraison demain après-midi"}}
Exemple — python-telegram-bot
Voici un bot qui répond à l'outil check_order ci-dessus. Il reçoit par getUpdates et traite les requêtes en parallèle des autres mises à jour (concurrent_updates) : si le traitement d'une mise à jour précédente le retarde, budget_ms risque d'expirer. La commande est cherchée d'après l'identifiant d'utilisateur Pabal de la personne qui l'a passée, et n'est communiquée que si elle correspond au from.id de la requête.
# order_voice_bot.py — pip install "python-telegram-bot==22.*"
# Lancement : read -rs BOT_TOKEN && export BOT_TOKEN
# CUSTOMER_ID=<identifiant Pabal de la personne qui a commandé> python3 order_voice_bot.py
# Pour les essais, mettez votre propre ID dans CUSTOMER_ID — c'est message.from.id dans getUpdates après avoir écrit au bot
import json
import os
import re
from telegram import Update
from telegram.error import TelegramError
from telegram.ext import Application, ContextTypes, TypeHandler
SERVER = "https://pabal.me"
CUSTOMER = int(os.environ.get("CUSTOMER_ID", "0"))
ORDERS = { # petite table de l'exemple ; en vrai, le système de commandes de la boutique
(CUSTOMER, "A-1042"): {"status": "En cours de livraison", "eta": "Livraison demain après-midi"},
}
def check_order(arguments, caller_id):
"""{"result": …} ou {"error": …} à passer tel quel à answerVoiceCallToolQuery"""
order_id = str(json.loads(arguments).get("order_id", "")).strip().upper()
if not re.fullmatch(r"[A-Z]-\d{4}", order_id): # les arguments viennent du modèle — on revérifie
return {"error": "Je n'ai pas compris le numéro de commande. Exemple : A-1042"}
found = ORDERS.get((caller_id, order_id)) # seulement les commandes de la personne qui appelle
if found is None: # la commande de quelqu'un d'autre reçoit la même réponse qu'une commande inexistante
return {"error": f"Je ne trouve pas la commande {order_id}."}
return {"result": json.dumps({"order_id": order_id, **found}, ensure_ascii=False)}
async def on_update(update: Update, context: ContextTypes.DEFAULT_TYPE):
q = update.api_kwargs.get("voice_call_tool_query") # PTB range les types qu'il ne connaît pas dans api_kwargs
if q is None:
return
if q["name"] == "check_order":
answer = check_order(q["arguments"], q["from"]["id"])
else:
answer = {"error": "Outil inconnu."}
try:
await context.bot.do_api_request("answerVoiceCallToolQuery",
api_kwargs={"tool_query_id": q["id"], **answer})
except TelegramError as e: # TOOL_QUERY_EXPIRED : trop tard, ou l'appel est terminé
print("Réponse non acceptée :", e, flush=True)
app = (Application.builder().token(os.environ["BOT_TOKEN"])
.base_url(f"{SERVER}/bot").base_file_url(f"{SERVER}/file/bot")
.concurrent_updates(True).build())
app.add_handler(TypeHandler(Update, on_update))
app.run_polling(allowed_updates=["message", "voice_call", "voice_call_tool_query"], poll_interval=0, timeout=10)
Comme aiogram ignore les types de mises à jour qu'il ne connaît pas, il faut intercepter voice_call_tool_query dans update.model_extra avec un middleware externe du dispatcher (dp.update.outer_middleware). Le dépôt de Pabal contient un exemple (scripts/bots/voice_tools_aiogram.py) : demandez-le à l'opérateur du serveur si besoin.
Ce qu'entendent le modèle et l'appelant
- Succès : le résultat devient la réponse de fonction du modèle (une valeur JSON s'il s'agit de JSON). Le texte du résultat est lu uniquement comme des données, jamais comme des consignes.
- Échec : le modèle entend une courte phrase de Pabal dans la langue de l'appel et explique la situation à l'appelant — délai dépassé : « The tool did not answer in time. » (« L'outil n'a pas répondu à temps ») ; injoignable : « The tool could not be reached. » (« L'outil n'a pas pu être joint ») ; argument de type numéro de carte : « Pabal does not pass on card numbers or national ID numbers. » (« Pabal ne transmet pas les numéros de carte ni les numéros d'identification nationaux ») ; et l'
errordu bot tel quel. Pour un argument erroné, le modèle reçoit une indication de ce qui cloche et peut rappeler l'outil. - Pendant l'attente, la passerelle comble le silence : après 1,5 seconde de silence, « One moment, let me check. » (« Un instant, je vérifie ») une fois (tout de suite si l'outil a été lent récemment), puis après 5 secondes « Just a moment longer, please. » (« Encore un petit instant, s'il vous plaît ») une fois. Ces deux phrases ne sont pas dites dans la langue de l'appelant mais dans la langue du bot ; s'il n'existe pas d'enregistrement validé dans cette langue, elles sont dites en anglais (pour l'instant, seuls le coréen et l'anglais sont validés).
- Si l'appelant coupe la parole (interruption), la passerelle peut retirer les requêtes ouvertes ; une réponse arrivée ensuite donne
TOOL_QUERY_EXPIRED: cancelled.
Limites des outils
Requêtes ouvertes en même temps : 2 par appel, 8 par bot. Nombre d'appels d'outil : 30 par minute et par appel, 120 par minute et par bot. Au-delà, la requête ne va pas au bot et le modèle entend aussitôt too_many_open·rate_limited.
Conditions du propriétaire de bot
La transcription, votre propre clé de modèle, le mode flux, les serveurs MCP et les appels des bots ne s'activent qu'après acceptation des conditions du propriétaire de bot (version 4). C'est une personne qui les accepte, dans l'écran des appels vocaux de @BotFather (depuis n'importe lequel des écrans transcription, clé propre, mode flux, serveurs MCP ou appels des bots) — impossible avec le jeton. Même si vous avez accepté une version précédente, la question est posée à nouveau. Tant que la section des propriétaires de bot n'est pas publiée dans les conditions d'utilisation du serveur, le bouton d'acceptation n'apparaît pas (un appel avant cela donne 403 VOICE_OWNER_TERMS_NOT_ACCEPTED).
BotFather affiche ces conditions en coréen ; le bouton en bas est Accepter les conditions (약관에 동의하기). En substance, le propriétaire du bot s'engage à ceci :
- Avec la transcription activée, vous recevez à la fin de l'appel le texte de la conversation écrit par l'IA ; c'est vous qui le traitez et le protégez.
- Quand on appelle un bot dont la transcription est activée, une annonce vocale prévient d'abord, à la connexion, que le texte de la conversation part au propriétaire du bot.
- Vous ne gardez le texte reçu que le temps nécessaire, vous l'effacez si l'appelant le demande, et vous ne vous en servez pas pour entraîner des modèles.
- Avec votre propre clé de modèle, l'audio et le texte des appels partent chez le fournisseur de votre clé, sous votre contrat ; ce traitement relève de votre responsabilité, et votre propre information sur la vie privée doit le dire.
- En mode flux, l'audio de l'appel part vers votre serveur au lieu du modèle d'IA de Pabal, et c'est vous qui le recevez et le traitez ; vous n'utilisez la voix et les paroles de l'appelant que dans les limites permises par la loi et les effacez quand vous n'en avez plus besoin.
- Si l'appelant le demande, vous lui dites où se trouve votre serveur et ce qu'il fait (idéalement, écrivez-le dans la présentation du bot).
- Le bot dit toujours qu'il est une IA et ses réponses sont produites par une IA : aucun humain ne répond directement, et aucun enregistrement d'une voix humaine n'est diffusé comme s'il s'agissait d'une réponse d'IA. En mode flux, Pabal diffuse au tout début de l'appel l'annonce « This is an AI voice bot. The call's audio goes to the bot developer's server. ».
- Vous ne demandez ni numéro de carte, ni numéro d'identification national, ni mot de passe, ni code de vérification, et vous continuez à respecter les règles des bots vocaux.
- Pabal n'enregistre pas le texte des conversations : une fois transmis, il est effacé du serveur au plus tard 24 heures après ; les clés et les secrets du mode flux et des serveurs MCP sont conservés chiffrés et ne sont journalisés nulle part.
- Si vous enregistrez un serveur MCP, les valeurs dites par l'appelant partent vers ce serveur quand le modèle appelle ses outils pendant l'appel ; vous êtes responsable du traitement par ce serveur et par ceux à qui il transmet, et vous déclarez ces destinataires fidèlement.
- En cas d'infraction à ces conditions, l'opérateur peut suspendre les appels vocaux ou le bot.
Les points 12 à 24 sont ce que vous respectez quand vous activez les appels des bots : ne les utiliser que pour les buts déclarés et autorisés par l'opérateur, sans publicité ; donner le nom du bot et dire qu'il est une IA dès ses premiers mots ; ne mettre aucune information sensible dans context ; ne jamais chercher à appeler une personne de moins de 19 ans ; ne jamais contourner les heures calmes, les pauses ou les limites ; n'utiliser ce que vous apprenez des personnes appelées que pour ces buts, et l'effacer quand l'autorisation est retirée ; protéger le jeton. Les appels passés par un bot n'apportent aucune transcription (point 24). Le texte des conditions peut changer après l'examen juridique.
Serveurs MCP
Ils ne s'activent que si l'opérateur installe les composants MCP (pabal-mcp, le signataire, le proxy de sortie) et publie la section bot-calls-mcp de la politique de confidentialité et la section des propriétaires de bot des conditions d'utilisation (prévoir une adresse de sortie dédiée aux requêtes MCP est une condition d'exploitation, que le code ne vérifie pas). Même une fois activés, tant que l'application n'affiche pas dans l'écran de confirmation le destinataire MCP du bot, on peut enregistrer un serveur mais aucun appel n'emporte d'outil MCP (mcp_unavailable_reason: "sheet"). Sur un serveur sans MCP, setVoiceCallMcpServer renvoie 403 VOICE_NOT_AVAILABLE: MCP tools are not available on this server, et @BotFather n'affiche pas de bouton Serveurs MCP (MCP 서버).
Si vous enregistrez un serveur MCP que vous exploitez (ou que vous utilisez sous contrat), le modèle peut appeler les outils de ce serveur pendant un appel. Ce n'est pas le code du bot qui les appelle, mais Pabal : le client MCP de Pabal envoie les requêtes à ce serveur via le proxy de sortie de Pabal, en faisant respecter délais, tailles et vérifications. Les valeurs dites par l'appelant partent donc vers ce serveur (point 10 des conditions du propriétaire de bot).
Ce que le serveur MCP doit offrir
- Streamable HTTP, POST (version du protocole 2024-11-05 ou ultérieure).
https://avec un certificat public, pas de redirection, et seulement les ports autorisés par le serveur (443 sur pabal.me). - Un serveur qui utilise des identifiants doit refuser honnêtement par
401une requête sans identifiants et ne pas accepter d'identifiants erronés. Un même identifiant ne peut servir qu'à un seul bot. - Document de l'opérateur : l'opérateur du serveur publie un document JSON (32 Ko au plus, sans redirection) à
https://<hôte>/.well-known/pabal-voice-mcp— entité exploitante, pays, contact, finalité, types et emplacement des données, destinataires et sous-traitance, durée de conservation. Ce sont les informations de destinataire affichées dans l'écran de confirmation de l'appelant ; à défaut, le propriétaire du bot les déclare à l'enregistrement (ci-dessous). Un serveur public que tout le monde peut appeler sans identifiants (auth.type: "none") doit indiquer"accepts": "any"dans l'entrée de point de terminaison de ce document pour dire qu'il accepte les appels de Pabal. - Ce que Pabal ajoute : le
_metadetools/callporte le délai restantme.pabal/budget_ms(millisecondes) et la taille maximale du résultatme.pabal/result_budget_bytes, et peut porter une assertion signée par Pabal (me.pabal/assertion, un JWT).getVoiceCallMcpBindingdonne l'émetteur de l'assertion, lesub(bot:<id du bot>) et les clés de vérification (JWKS). tools/listtient en 2 Mo et 20 pages au plus. Aucune étape ne doit exiger une intervention humaine (connexion dans un navigateur, etc.).- Chaque outil donne sa classification dans
_meta["me.pabal/voice"]—data(none·caller·third_party),content(system·third_party_text),effect(none·write·notify·financial),max_ms(15 000 au plus),max_result_bytes(65 536 au plus) et, en option,p95_ms. Si le serveur ne la donne pas, le propriétaire du bot la fournit à l'enregistrement avectools. - La version 1.0 du profil n'accorde que le niveau L1 : seuls les outils avec
data: "none"eteffect: "none"(des outils qui ne font que lire des informations publiques) sont épinglés aux appels. Les outils qui manipulent des données de l'appelant (L2) ou qui exécutent quelque chose (L3) sont seulement enregistrés, jamais utilisés. - À l'enregistrement, Pabal lance des tests de conformité (adresse, TLS, version, 401, liste d'outils, structure des outils, portée des identifiants, étapes humaines). En cas d'échec, l'enregistrement est refusé avec
MCP_CONFORMANCE_FAILED: CT-xx.
Enregistrer un serveur
Les secrets ne s'envoient que dans le corps de la requête (dans la chaîne de requête : 400). Pour qu'aucun secret ne reste dans l'historique de la ligne de commande, mettez-les dans un fichier et envoyez celui-ci.
umask 077
cat > mcp-shop.json <<'EOF'
{"label": "shop",
"url": "https://mcp.example.com/mcp",
"auth": {"type": "bearer", "token": "(jeton fourni par le serveur MCP)"}}
EOF
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallMcpServer" -H 'Content-Type: application/json' -d @mcp-shop.json
label:[a-z][a-z0-9]{0,11}, ne peut commencer parpabal. 2 serveurs par bot. Le nom d'outil que voit le modèle estlabel__nomdeloutil.auth.type:none,bearer(token),headers(3 en-têtes au plus),client_credentials(token_url,client_id,client_secret, ouprivate_key_jwt: true— d'abordcreateVoiceCallMcpClientKey).authorization_coden'existe pas encore.none(serveur public) n'est enregistré que si le document public de l'opérateur de ce serveur indique qu'il accepte les appels de Pabal (sinonMCP_OPT_IN_REQUIRED).- Une modification qui change le destinataire (nouveau serveur, autre adresse ou autre hôte de jeton, nouvelle déclaration, niveau plus élevé) donne en réponse
400 MCP_CHANGE_NEEDS_CONFIRMATION, et ne s'applique que si le propriétaire du bot appuie dans les 5 minutes sur le bouton Confirmer (확인 — 이 변경을 적용) qui s'affiche dans @BotFather. Ainsi, même si le jeton fuit, les paroles de l'appelant ne partent pas en cachette ailleurs. Une simple rotation du secret s'applique immédiatement. - Si l'opérateur du serveur ne publie pas de document, le propriétaire du bot déclare fidèlement le destinataire avec
declaration(entité exploitante, pays, contact, finalité, types de données, emplacement, sous-traitance, durée de conservation). L'appelant le voit dans l'écran de confirmation, et la question lui est reposée en cas de changement. - Enregistrements et vérifications : 10 par heure et par bot, 20 par propriétaire. Autres méthodes :
refreshVoiceCallMcpServer(nouvelle vérification),deleteVoiceCallMcpServer(toujours possible),getVoiceCallMcpBinding(émetteur,subet JWKS qui permettent à votre serveur de reconnaître les appels de Pabal),createVoiceCallMcpClientKey. - L'état des serveurs et outils enregistrés apparaît dans
getVoiceCallSettings.mcp_servers(sans chemin, chaîne de requête ni secret). Le texte complet du profil (Pabal Voice MCP Profile 1.0) se trouve dans le dépôt de Pabal, àdocs/app/voice-bot-mcp-profile.html: demandez-le à l'opérateur du serveur si besoin.
Votre propre clé de modèle
Pour un appel pris avec une clé propre, l'appelant doit voir un écran de confirmation (version 2) qui lui dit que l'audio part chez le fournisseur de cette clé, et aucune application n'affiche encore cet écran. Un serveur autre qu'un serveur de test n'accepte donc même pas les clés tant que l'opérateur ne l'a pas activé en indiquant que les applications sont prêtes (403 VOICE_OWN_KEY_NOT_AVAILABLE). Le serveur doit aussi disposer d'une clé maîtresse.
Faites passer les appels du bot par votre propre compte de modèle. Les frais sont facturés sur votre compte chez le fournisseur, et les appels avec clé propre ne consomment pas les limites quotidiennes de la plateforme pour le bot, le propriétaire et le serveur (celles de l'appelant s'appliquent toujours). Les conditions du propriétaire de bot passent d'abord.
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallApiKey" -d provider=google \
--data-urlencode "api_key@vertex-sa.json"
provider:googlepour une clé JSON de compte de service Vertex AI (seuls les champs nécessaires sont conservés),openaipour une clé d'API qui commence parsk-. Toute autre valeur :VOICE_PROVIDER_INVALID. La clé ne s'envoie que dans le corps de la requête (dans la chaîne de requête : 400) — en la lisant depuis un fichier comme ci-dessus, elle ne reste pas dans la ligne de commande. Ne la collez pas dans une conversation avec @BotFather.- Avant d'accepter la clé, le serveur vérifie qu'elle fonctionne vraiment avec une session de test de la passerelle (12 secondes au plus). Si c'est bon, elle est conservée et la méthode renvoie True. Si le fournisseur refuse :
API_KEY_INVALID; si la connexion passe mais sans droit sur l'API temps réel :API_KEY_NO_REALTIME_ACCESS; s'il n'y a pas de passerelle pour tester ou qu'elle est occupée :503 API_KEY_CHECK_UNAVAILABLE: …: the key was not kept. - Tests : 5 par heure et par bot, 10 par propriétaire (tous ses bots confondus). Seuls les tests qui ont atteint une passerelle comptent.
- La clé n'apparaît nulle part — ni dans les réponses, ni dans les erreurs, ni dans les journaux, ni à l'écran.
getVoiceCallSettings.api_keyne contient que le fournisseur, la date de réglage, l'empreinte (8 caractères) et si elle est utilisable. - Si la clé configurée est inutilisable (conditions modifiées, le serveur ne peut pas ouvrir la clé, etc.), le bot ne prend pas d'appels — il ne bascule pas en cachette sur la clé de la plateforme. Si vous effacez la clé avec
deleteVoiceCallApiKey(toujours possible), les appels sont pris avec la clé de la plateforme, et les appels en cours avec votre clé se terminent. - Les appels avec clé propre n'ont pas de limite quotidienne Pabal — seules s'appliquent la durée d'un appel, les appels simultanés et les limites de l'appelant. Les frais vont sur votre compte : configurez des alertes de budget dans la console du fournisseur.
- Une clé OpenAI n'est utilisée que si l'opérateur de la passerelle l'active à part (par défaut, la passerelle n'accepte que les clés Google). S'il n'y a aucune passerelle qui l'a activée, le test d'une clé OpenAI donne
503 … no_gateway. Le chemin des clés OpenAI n'a encore jamais été testé avec le vrai OpenAI (seulement avec de faux serveurs), et un appel pris avec une clé OpenAI utilise la voix OpenAI choisie par la passerelle (marinpar défaut), quel que soit le réglage de voix du bot. Une clé Google est utilisée dans la région Vertex de la passerelle (us-central1). - En mode flux, la clé est seulement conservée, pas utilisée.
Transcription
L'opérateur doit l'autoriser dans la page d'administration après avoir terminé un examen juridique (désactivée par défaut). Si vous activez l'interrupteur sur un serveur où elle n'est pas autorisée, vous obtenez transcript_available: false et transcript_unavailable_reason: "not_allowed", et aucun texte n'arrive.
Une fois activée, le propriétaire du bot reçoit le texte de la conversation écrit par l'IA. Acceptez les conditions du propriétaire de bot, puis activez l'interrupteur.
La transcription (cette section) est activée par le propriétaire du bot et c'est lui qui la reçoit ; l'appelant entend une annonce vocale à la connexion. « Garder la conversation » est choisi par la personne qui appelle, avec la case à cocher de l'écran de confirmation, et le texte reste après l'appel uniquement dans sa propre conversation — il n'est envoyé ni au bot ni à son propriétaire, et il n'y a pas d'annonce vocale. Un même appel peut avoir les deux. Le propriétaire du bot n'a aucun interrupteur pour « Garder la conversation » : la fonction s'active quand la politique de confidentialité du serveur contient la section bot-calls-keep-text (en préparation — Utiliser l'application — bots vocaux).
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -d transcript=true
- Les appels passés par un bot n'ont pas de transcription (appels des bots) — même avec l'interrupteur activé.
- Quand on appelle un bot dont la transcription est activée, l'appelant entend juste après l'annonce sur l'IA : « What is said on this call is written down and sent to the bot's owner. » (« Ce qui est dit pendant cet appel est mis par écrit et envoyé au propriétaire du bot »). Seules les paroles prononcées après la fin de l'annonce sont transcrites. Les appels en mode flux n'ont pas de transcription.
- Le champ
transcriptarrive dans levoice_callde l'appel terminé (event: "ended"). C'est la seule version complète : mettez doncvoice_calldansallowed_updates. Si vous voulez recevoir les tours de parole un par un pendant l'appel, ajoutez aussivoice_call_transcript(uniquement par la Bot API HTTP, pas en MTProto). - Dans un tour de parole qui contient un numéro de carte ou un numéro d'identification national, tous les chiffres sont remplacés par
*. Un numéro dit sur deux tours ou lu en toutes lettres n'est pas repéré : manipulez aussi le texte reçu avec prudence. - 4 096 octets par tour, 256 Kio et 4 000 tours par appel. Au-delà, le reste est abandonné et
truncated: true. - Pabal n'enregistre pas le texte des conversations : il reste en mémoire pendant l'appel, est remis une fois au bot, et ce qui n'a pas été récupéré est effacé au bout de 24 heures. Il disparaît au redémarrage du serveur. La conservation et le traitement après réception relèvent de la responsabilité du propriétaire du bot.
"transcript": {
"turns": [
{"role": "bot", "text": "Aide-Commande, bonjour. Que puis-je faire pour vous ?", "offset_ms": 5200},
{"role": "caller", "text": "Où en est ma commande A-1042 ?", "offset_ms": 9800}
],
"truncated": false
}{"update_id": 21, "voice_call_transcript": {"voice_call_id": "4611686018427387905", "seq": 2,
"role": "caller", "text": "Où en est ma commande A-1042 ?", "is_final": true, "offset_ms": 9800, "date": 1790380813}}role vaut caller (l'appelant) ou bot (le modèle), et offset_ms compte les millisecondes depuis le début de l'audio de l'appel.
Mode flux
Pour qu'on puisse appeler un bot en mode flux, l'application de l'appelant doit afficher l'écran de confirmation du mode flux, qui dit que « l'audio de l'appel part vers le serveur du développeur du bot », et aucune version de l'application ne le contient encore. Le serveur doit aussi disposer d'une clé maîtresse, de la section bot-calls-stream de la politique de confidentialité, de la section des propriétaires de bot et d'une passerelle qui prend en charge le mode flux. Sinon, mode=stream renvoie 403 VOICE_STREAM_NOT_AVAILABLE.
En mode flux, l'audio de l'appel ne va pas au modèle d'IA de Pabal mais au serveur du propriétaire du bot. Votre serveur écoute l'audio brut et renvoie ses réponses sous forme d'audio — vous choisissez l'IA que vous voulez (le modèle temps réel d'un autre fournisseur, votre propre combinaison reconnaissance vocale + synthèse). Pabal diffuse toujours l'annonce vocale sur l'IA au tout début et continue de faire respecter la confirmation de l'appelant, les limites, la durée d'appel et les appels simultanés. Les instructions sont inutiles, et les outils, la clé propre et la transcription sont seulement conservés, pas utilisés (stream_ignores).
Explication du diagramme
- Trois colonnes : à gauche l'appelant, au centre Pabal (le serveur et la passerelle vocale), à droite le serveur du propriétaire du bot. Le serveur du bot ne parle ni à l'application ni au serveur Pabal : il ne dialogue qu'avec la passerelle, en WebSocket.
- ② à ④ se déroulent pendant la sonnerie : la passerelle se connecte en TLS à l'adresse publique qu'elle a vérifiée, fait l'upgrade avec l'en-tête de signature et envoie
start. L'appel n'est pris que si le serveur du bot envoiereadydans les 3 secondes ; sinon, l'appelant entend une tonalité d'occupation. - La case grise est un sas : tant que l'annonce vocale sur l'IA (« This is an AI voice bot. The call's audio goes to the bot developer's server. ») n'est pas terminée, la voix de l'appelant ne va pas au serveur du bot, et l'audio envoyé trop tôt par le bot n'est pas diffusé. Dès qu'elle se termine, ⑤
liveest envoyé. - ⑥ L'audio est dans les deux sens en PCM16, 24 kHz, mono ; côté appelant, toujours par blocs de 20 ms (960 octets). Les flèches bleues sont ce que Pabal envoie, les grises ce que le serveur du bot envoie.
- ⑦ et ⑧ : le bot peut envoyer une interruption (
clear), un repère de lecture (mark) et un raccrochage (hangup) ; à la fin de l'appel, Pabal envoiestopavec la raison et ferme avec le code 1000.
Réglage et sonde
Commencez par lancer le serveur d'exemple ci-dessous sur une adresse publique (un nom qui correspond au certificat). La sonde se connecte à ce serveur au moment même du réglage, et même un échec de connexion compte dans la limite de 5 par heure.
Après avoir accepté les conditions du propriétaire de bot (@BotFather → Appels vocaux (음성 통화) → Mode flux (원음 모드) → Accepter les conditions du propriétaire de bot (봇 주인 약관에 동의)), indiquez l'adresse du serveur et le secret. Le secret fait de 32 à 128 caractères ASCII imprimables (sans espace), et une nouvelle adresse s'envoie toujours avec un nouveau secret.
umask 077
openssl rand -hex 32 | tr -d '\n' > stream-secret.txt # il ne doit pas y avoir de saut de ligne à la fin
curl -s "https://pabal.me/bot$BOT_TOKEN/setVoiceCallSettings" -d mode=stream \
-d stream_url=wss://bot.example.com:8443/voice --data-urlencode "stream_secret@stream-secret.txt"
- Format de l'adresse : uniquement
wss://, sans nom d'utilisateur,#ni espace, en ASCII uniquement (les autres caractères encodés en pourcentage), 2 048 caractères au plus. L'hôte est un nom qui contient un point ou une IP publique (paslocalhost), le port 443 ou de 1024 à 65535. Sinon400 STREAM_URL_INVALID: …. - Les limites de sonde comptent dès la requête : 5 par heure et par bot, 10 par propriétaire, et en même temps 1 par bot et 2 par propriétaire. Au-delà :
429. Une sonde qui ne s'est connectée nulle part parce que la passerelle était occupée ne compte pas. - Vérification de l'adresse : si le nom pointe vers ne serait-ce qu'une adresse privée ou réservée, ou vers une adresse de Pabal lui-même :
400 STREAM_URL_INVALID: the host must resolve only to public addresses. - Sonde : la passerelle essaie une connexion (
"probe": truedansstart). Sireadyarrive, le mode s'active et le secret est conservé chiffré sous la clé maîtresse du serveur. En cas d'échec :400 STREAM_PROBE_FAILED: <code>—unreachable(nom, connexion, délai ou TLS),handshake_failed(une réponse autre que 101 — le plus souvent la signature a été refusée),redirect,no_subprotocol,no_ready,bad_ready,address_refused,busy,url, etc. — les autres codes sont transmis tels que la passerelle les donne. Le serveur attend jusqu'à 12 secondes. - Quand le mode flux est activé ou que l'adresse change par la Bot API (le jeton), @BotFather envoie au propriétaire du bot un message indiquant que l'adresse de flux est devenue … (원음 주소가 … 로 바뀌었어요 (비밀 …)) avec un bouton Revenir au mode géré (관리형으로 돌아가기) ; quand le bot revient au mode géré, il le signale avec un bouton Appels vocaux (음성 통화). C'est pour éviter que, si le jeton fuit, l'audio parte en cachette vers un autre serveur.
- Mettre
stream_secretdans la chaîne de requête donne 400 avant toute autre chose. On ne peut pas régler le mode flux dans une réponse de webhook. - Pour revenir au mode géré :
mode=managed(l'adresse et le secret sont effacés). Si le bot est activé sans instructions, vous obtenezVOICE_INSTRUCTIONS_REQUIRED: envoyez les instructions en même temps. - Sur un serveur qui n'accepte que les bots autorisés, le bot ne prend des appels qu'après que l'opérateur a vérifié l'adresse de flux et l'a de nouveau autorisé.
getVoiceCallSettingsne montre questream_urlet l'empreinte du secret.sendVoiceCallContextne fonctionne pas pour un appel en mode flux (VOICE_CONTEXT_NOT_SUPPORTED), etendVoiceCallcoupe tout de suite, sans au revoir.
Spécification
- TLS 1.2 ou plus, avec un certificat public qui correspond au nom de l'URL. La passerelle se connecte à l'IP qu'elle a vérifiée, mais fait le SNI et la vérification du certificat avec le nom. Elle ne suit pas les redirections (3xx).
- Le sous-protocole
pabal.voice.v1doit être choisi (Sec-WebSocket-Protocol). Pas de compression. Un message fait au plus 65 536 octets, un message texte au plus 4 096 octets (UTF-8). - La passerelle envoie un ping toutes les 15 secondes. Sans pong dans les 15 secondes, elle coupe (la plupart des bibliothèques répondent d'elles-mêmes).
- Les messages texte sont des événements JSON, les messages binaires sont de l'audio. Un seul format audio : PCM16 little-endian, mono, 24 000 Hz.
call_idetbot_idsont des chaînes décimales.
| Sens | Message | Quand · signification |
|---|---|---|
| Pabal → bot | {"event":"start","protocol":1,"call_id":"…","bot_id":"…","probe":false,"caller":{"lang":"ko"},"format":{"encoding":"pcm_s16le","sample_rate":24000,"channels":1,"frame_ms":20},"max_seconds":300} | Une fois, juste après l'upgrade. Ni nom, ni numéro, ni identifiant de l'appelant (seulement caller.lang — les informations sur la personne viennent par la mise à jour voice_call) |
| Pabal → bot | {"event":"live"} | L'annonce sur l'IA est terminée. À partir de là, l'audio circule |
| Pabal → bot | Binaire | Voix de l'appelant, toujours 960 octets (20 ms). Seulement après live |
| Pabal → bot | {"event":"mark","name":"…"} | Tout l'audio qui précédait ce mark envoyé par le bot a été diffusé |
| Pabal → bot | {"event":"cleared","played_ms":N,"dropped_ms":N,"marks":["…"]} | Réponse à clear |
| Pabal → bot | {"event":"buffer_full","dropped_ms":N} | L'audio du bot dépassait 30 secondes en attente et le surplus a été jeté |
| Pabal → bot | {"event":"stop","reason":"…","played_ms":N,"duration_ms":N} | L'appel est terminé. Fermeture en 1000 juste après |
| Bot → Pabal | {"event":"ready"} | Dans les 3 secondes après start, et en tout premier |
| Bot → Pabal | Binaire | Audio du bot : un nombre pair d'octets, de 2 à 65 536. Ce qui arrive avant live est jeté |
| Bot → Pabal | {"event":"clear"} | Interruption : arrêter tout de suite l'audio du bot en attente |
| Bot → Pabal | {"event":"mark","name":"[A-Za-z0-9_.:-]{1,64}"} | « Préviens-moi quand la lecture arrive ici ». Jusqu'à 256 en attente par appel |
| Bot → Pabal | {"event":"hangup"} | Termine l'appel. L'audio en attente est d'abord diffusé pendant 5 secondes au plus, puis l'appel est coupé |
- Un
eventinconnu est ignoré. Un texte qui n'est pas du JSON, un texte de plus de 4 Kio, un audio d'un nombre impair d'octets ou de plus de 64 Kio constituent une violation de la spécification et mettent fin à l'appel (l'appelant entend une annonce d'excuse). - Plafonds d'envoi : événements texte, 50 par seconde en moyenne (100 en pointe) ; audio, 4 fois le temps réel en moyenne (60 secondes en pointe). Si le socket du bot ne lit pas et que la voix de l'appelant s'accumule plus de 30 secondes (1 500 blocs), l'appel se termine aussi.
Signature
La passerelle ajoute cet en-tête à la requête d'upgrade.
X-Pabal-Voice-Signature: t=<secondes Unix>,nonce=<16 octets en hexa>,call=<call_id>,v1=<hexa>
v1 = HMAC-SHA256(stream_secret, "pabal-voice-stream/1\n<t>\n<nonce>\n<call>\n<host>\n<path?query>")
hostest l'hôte de l'URL que vous avez donnée à Pabal (en minuscules, sans port). Ne le calculez pas à partir de l'en-têteHostde la requête : c'est l'expéditeur qui l'écrit, ce qui ne lie la signature à aucun hôte.path?queryest le chemin et la chaîne de requête de l'URL tels quels (/s'il n'y en a pas). Pour une sonde,callest le numéro de la sonde.- Le serveur du bot vérifie obligatoirement :
|maintenant − t| ≤ 300 secondes, chaquenonceune seule fois, et le HMAC comparé octet par octet avec une fonction à temps constant. En cas d'erreur, répondez autre chose que 101, par exemple 401. - Il vérifie ensuite que
start.call_idest égal aucallde la signature. Sinon, fermez en 1008 — pour qu'un upgrade signé ne puisse pas être réutilisé pour un autre appel. Un serveur qui ne sert qu'un bot vérifie aussi questart.bot_idest bien son bot. - Valeurs de test : secret
example-stream-secret-of-the-bot-developer-0001,t=1790000000, nonce00112233445566778899aabbccddeeff, call4611686018427387905, hostbot.example.com, chemin/voice?bot=100042→v1=807517ff96ac75223bde35b48c775f49a2eda2fb4832a02c0ea832b914d70d0c.
Audio · interruption · mark · fin
- Tampon bot → appelant : la passerelle diffuse au rythme de la lecture, vous pouvez donc tout envoyer d'un coup ; mais au-delà de 30 secondes en attente, le surplus est jeté et
buffer_fullest envoyé. Une pause de 300 ms dans l'audio du bot marque la fin d'un « segment » (l'unité demarket declear). clear: jette tout l'audio du bot pas encore diffusé ; le silence commence dès les 10 ms suivantes (moins de 50 ms mesurées). Dans la réponsecleared,played_msest la somme de l'audio du bot réellement diffusé depuis le début de l'appel,dropped_msce qui vient d'être jeté, etmarksles noms des mark attachés aux segments jetés — utile pour dire au modèle « où il a été coupé ».mark: revient sous le même nom quand l'audio qui le précède a été diffusé. Il vaut mieux que l'audio avant un mark se termine sur un multiple de 480 octets (10 ms) (le reste est complété par du silence).- Fin : le bot envoie
hangupou ferme proprement en 1000 ou 1001. Toute autre fermeture ou coupure TCP se termine comme une violation de la spécification (annonce d'excuse à l'appelant). Quand Pabal met fin à l'appel,stop.reasonest un code court —hangup(l'appelant a raccroché),max_length,bot_ended,disconnect,drain,kill_switch,operator,link_lost,media_failed… Traitez une valeur inconnue comme « terminé ». - Le
startd'une sonde contient"probe": true, uncallervide etmax_seconds: 0. Après votreready,{"event":"stop","reason":"probe"}arrive et la connexion se ferme. N'ouvrez rien de coûteux, comme une session de modèle, pour une sonde.
Exemple — le plus petit serveur de flux
Un serveur qui vérifie toute la spécification sans IA : à live, il joue une sonnerie, puis renvoie telle quelle la voix de l'appelant (écho). Il vérifie la signature, l'heure, le nonce, l'appel et le bot. Voyez d'abord avec lui qu'un appel va bien jusqu'au bout, puis mettez votre IA à la place de l'écho. Le dépôt de Pabal contient aussi scripts/bots/stream_echo_bot.py (avec une démonstration de l'interruption) et scripts/bots/stream_openai_relay.mjs (relaie vers Realtime avec votre clé OpenAI — un exemple qui n'a pas été exécuté) : demandez-les à l'opérateur du serveur si besoin.
# stream_bot.py — pip install "websockets>=13"
# Lancement : STREAM_SECRET_FILE=stream-secret.txt TLS_CERT=fullchain.pem TLS_KEY=privkey.pem \
# PUBLIC_HOST=bot.example.com BOT_ID=<ID du bot — les chiffres avant : dans le jeton> python3 stream_bot.py
import asyncio
import hashlib
import hmac
import json
import math
import os
import ssl
import struct
import time
from websockets.asyncio.server import serve
SECRET = open(os.environ["STREAM_SECRET_FILE"], "rb").read().strip()
PUBLIC_HOST = os.environ["PUBLIC_HOST"].lower() # hôte du stream_url donné à Pabal (pas l'en-tête Host)
BOT_ID = os.environ.get("BOT_ID", "") # vide : accepte les appels de n'importe quel bot
PATH = os.environ.get("STREAM_PATH", "/voice")
PORT = int(os.environ.get("PORT", "8443"))
CHIME = b"".join(struct.pack("<h", int(8000 * math.sin(2 * math.pi * 880 * i / 24000)))
for i in range(24000 * 6 // 10)) # 880 Hz pendant 0,6 s, PCM16 24 kHz
seen = {} # nonces déjà utilisés
def signed_call(header, target):
"""Vérifie X-Pabal-Voice-Signature et renvoie l'id d'appel signé. None en cas d'erreur."""
try:
parts = dict(p.split("=", 1) for p in header.split(","))
t, nonce, call, v1 = int(parts["t"]), parts["nonce"], parts["call"], parts["v1"]
except (KeyError, ValueError):
return None
now = time.time()
if abs(now - t) > 300 or nonce in seen:
return None
text = f"pabal-voice-stream/1\n{t}\n{nonce}\n{call}\n{PUBLIC_HOST}\n{target}"
expected = hmac.new(SECRET, text.encode("utf-8", "surrogateescape"), hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected.encode(), v1.encode("utf-8", "surrogateescape")): # octet par octet
return None
for old in [n for n, at in seen.items() if at < now - 600]:
del seen[old]
seen[nonce] = now
return call
def check(connection, request):
if request.path.split("?", 1)[0] != PATH:
return connection.respond(404, "not here\n")
call = signed_call(request.headers.get("X-Pabal-Voice-Signature", ""), request.path)
if call is None:
return connection.respond(401, "bad signature\n")
connection.signed_call = call # start doit parler de cet appel
return None
async def handle(ws):
if ws.subprotocol != "pabal.voice.v1":
await ws.close(1002, "pabal.voice.v1 only")
return
start = json.loads(await asyncio.wait_for(ws.recv(), 5))
if (start.get("event") != "start" or start.get("call_id") != ws.signed_call
or (BOT_ID and start.get("bot_id") != BOT_ID)):
await ws.close(1008, "not this call")
return
await ws.send(json.dumps({"event": "ready"})) # dans les 3 secondes, en tout premier
live = False
async for message in ws:
if isinstance(message, bytes):
if live:
await ws.send(message) # renvoie tels quels les 20 ms de l'appelant (écho)
continue
event = json.loads(message)
if event.get("event") == "live" and not start.get("probe"): # l'annonce sur l'IA est finie
live = True
await ws.send(CHIME)
await ws.send(json.dumps({"event": "mark", "name": "chime"}))
elif event.get("event") == "stop":
print("Fin :", start["call_id"], event.get("reason"), flush=True)
break
async def main():
tls = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
tls.minimum_version = ssl.TLSVersion.TLSv1_2
tls.load_cert_chain(os.environ["TLS_CERT"], os.environ["TLS_KEY"])
async with serve(handle, "0.0.0.0", PORT, ssl=tls, subprotocols=["pabal.voice.v1"],
process_request=check, max_size=65536, compression=None):
print(f"À l'écoute : wss://{PUBLIC_HOST}:{PORT}{PATH}", flush=True)
await asyncio.get_running_loop().create_future()
asyncio.run(main())
Ce que voit l'appelant
- Bouton d'appel : un bouton d'appel apparaît dans l'en-tête de la conversation et dans le profil du bot vocal. Il n'est visible que si le serveur estime pouvoir prendre un appel de cette personne, depuis cet appareil (avant l'ouverture au public, seulement pour les testeurs). Les appels vidéo ne sont pas disponibles.
- Écran de confirmation (avant même l'autorisation du micro) : titre « An AI answers this call » ; le bot n'est pas humain et sa voix est produite par une IA ; l'appel n'est pas chiffré de bout en bout ; où va l'audio (Google, API Gemini Live de Vertex AI) et dans quel pays ou région (« United States (Iowa, Google Cloud us-central1) ») ; ce qui est envoyé (la voix pendant l'appel, le texte de la conversation et les réponses de l'IA, les notes ajoutées par le bot pendant l'appel et les résultats des fonctions du bot) ; la conservation (Pabal n'enregistre pas l'audio et garde seulement le journal des appels pendant 180 jours ; Google ne conserve rien après avoir produit la réponse, mais un contenu que ses contrôles automatiques soupçonnent d'abus peut être consigné jusqu'à 90 jours) ; ce que reçoit le propriétaire du bot (nom, identifiant de compte, langue, heure et durée de l'appel, valeurs utilisées par les outils — ni le numéro de téléphone ni l'audio) ; 19 ans ou plus. Les boutons sont « I'm 19 or older · Call » et Annuler. L'âge n'est pas une vérification d'identité : c'est l'appelant qui le confirme lui-même. Le bouton d'appel est le bouton principal (plein, couleur d'accent) et Annuler le bouton secondaire. Sur un serveur où « Garder la conversation » est activé, la case « Keep this call's conversation as text in my chat » (décochée par défaut) s'ajoute après les explications.
- À quelle fréquence : sur ordinateur et Android, au premier appel à ce bot (et de nouveau si le destinataire ou la version de l'écran change) ; sur iPhone et Apple Watch, à chaque fois. La montre affiche l'écran en entier à chaque fois, et la position de départ de l'interrupteur « Garder la conversation » est le choix fait lors du dernier appel à ce bot. L'application envoie le consentement au serveur à chaque appel, et le serveur utilise une seule fois, pour cet appel, un consentement venant du même appareil, pour le même bot et datant de moins de 2 minutes.
- À la connexion : l'annonce vocale sur l'IA « You're now talking to an AI voice bot. » (pour un bot en mode flux : « This is an AI voice bot. The call's audio goes to the bot developer's server. ») → (pour un bot avec transcription) l'annonce de transcription → le message d'accueil du bot. Le propriétaire du bot ne peut pas couper l'annonce, et ce que dit l'appelant avant la fin de l'annonce ne va pas au modèle. « Garder la conversation » n'a pas d'annonce vocale — l'appelant l'a choisi lui-même à l'écran.
- Langue des annonces : les annonces vocales ne sont pas dans le
languagedu bot mais dans la langue de l'écran de confirmation qu'a lu l'appelant. Toutes les annonces d'un même appel sont dans une seule langue : l'annonce sur l'IA, l'annonce de transcription et les excuses en cas de coupure (pour un bot en mode flux, l'annonce du mode flux et les excuses). Si une seule d'entre elles n'a pas d'enregistrement validé dans cette langue, toutes passent en anglais. Pour l'instant, les seules langues validées sont le coréen et l'anglais. L'écran de confirmation et les annonces dans les 8 autres langues (japonais, chinois, français, espagnol, indonésien, vietnamien, khmer, arabe) sont des brouillons en attente d'un examen juridique et d'une vérification par des locuteurs natifs : pour l'instant, même un appelant dont l'application est dans l'une de ces langues voit l'écran de confirmation en anglais et entend les annonces en anglais. Les enregistrements en khmer et la version japonaise de « Just a moment longer, please. » attendent encore qu'une personne les écoute et choisisse la bonne prise. La première annonce de l'appel et l'annonce de transcription sont toujours précédées d'au moins 300 ms de silence — pour que la ligne téléphonique n'avale pas le premier mot. - Écran d'appel : sous le nom, « @nom_du_bot · AI · Bot » et « AI-generated voice · Call audio is processed by Google ». Ni clé emoji ni mention du chiffrement.
- En cas de problème de connexion, l'appelant entend « Sorry, we've lost the connection. Ending the call now. » et l'appel est coupé. Tous les refus se terminent par une tonalité d'occupation.
Les explications destinées aux utilisateurs de l'application se trouvent dans Utiliser l'application — Appeler un bot vocal.
Appels des bots
Le serveur sait le faire, mais aucun bot ne peut appeler qui que ce soit tant que la section bot-calls-outbound de la politique de confidentialité et la section voice-bot-owners-outbound des conditions d'utilisation n'ont pas passé l'examen juridique et ne sont pas publiées. Aucune version de l'application avec l'écran d'autorisation n'est encore sortie non plus (État actuel). Ce qui suit décrit le fonctionnement sur un serveur où c'est activé.
Un bot vocal peut aussi appeler une personne de lui-même : pour confirmer un rendez-vous, réveiller quelqu'un à l'heure choisie, annoncer une livraison. Le code du propriétaire du bot fait la demande avec startVoiceCall, le serveur l'examine et fait sonner les appareils de la personne ; si elle décroche, l'annonce de Pabal passe d'abord, puis le modèle vocal d'IA parle selon les instructions du bot. La façon dont l'appel s'est terminé arrive dans une mise à jour voice_call.
- Seulement les personnes qui l'ont autorisé. La personne doit autoriser ce bot à l'appeler dans l'application Pabal (L'autorisation de la personne appelée) et peut la retirer à tout moment.
- Pas de publicité. Il y a quatre buts — rappels, réveils, livraisons, rendez-vous — et la publicité, la sollicitation commerciale et la promotion sont interdites (conditions du propriétaire de bot, points 12 à 24).
- Les appels des bots ne sont pas chiffrés de bout en bout — le son passe par le serveur de Pabal et par le fournisseur d'IA. Une fois que la personne a décroché, le son part chez Google pendant l'appel — API Gemini Live de Vertex AI, Iowa, États-Unis (Google Cloud
us-central1). Pabal ne conserve pas le son. - Aucun texte de la conversation ne va au propriétaire du bot. Les appels passés par un bot n'ont pas de transcription. Ce que reçoit le propriétaire du bot, ce sont les cinq éléments indiqués dans Ce que la personne appelée voit et entend.
Déroulement
Explication du schéma
- Quatre couloirs : de gauche à droite, le code du propriétaire du bot, le serveur Pabal, la passerelle vocale avec le modèle d'IA, et l'application de la personne appelée. Les pointillés verticaux représentent le temps ; les traits bleus pleins, l'appel qui avance ; le trait gris, la réponse au bot.
- Le cadre gris en ① — trois examens : avant de répondre, le côté du bot (activation, examen par l'opérateur, conditions, limites) et l'existence d'une autorisation ; après le 200, le côté de la personne (heures calmes, pause, limite quotidienne et intervalle de la personne, appareil à faire sonner) ; et une dernière fois juste avant la sonnerie.
- Le 200 en ② veut seulement dire « pris en compte ». Si cela a sonné et si la personne a répondu, seules les mises à jour en ⑧ le disent.
- ④ à ⑦, côté personne : la sonnerie affiche « (AI) » après le nom du bot, et à la réponse, l'annonce de Pabal (⑥) passe avant le bot. Dans ses premiers mots (⑦), le bot dit son nom, qu'il est une IA et le but de l'appel. Pendant le cadre vert, le son passe par la passerelle et le modèle d'IA — ce n'est pas chiffré de bout en bout.
- Le trait rouge en pointillés est l'essentiel : une demande qui n'a jamais sonné à cause du côté de la personne et une demande qui a sonné sans réponse se terminent de la même façon, au même moment, par
not_answered. Le bot ne peut pas savoir si la personne dormait ou était en ligne.
Activer — @BotFather
Les appels d'un bot s'activent uniquement par son propriétaire dans @BotFather, jamais avec le jeton (envoyer les champs d'appel à setVoiceCallSettings donne 400 VOICE_FIELD_NOT_AVAILABLE). Les appels vocaux doivent d'abord être activés. /mybots → le bot → Appels vocaux (음성 통화) → Appels du bot (봇이 거는 전화) → Activer (켜기).
BotFather répond en coréen. Le premier message demande si le bot peut appeler des personnes : uniquement celles qui l'ont autorisé, jamais pendant leurs heures calmes ni pendant une pause, avec un plafond quotidien (50 pour l'instant) et les limites du serveur, et seulement après que l'opérateur a examiné et autorisé le nom, la photo, les instructions, la voix et les buts du bot (et à nouveau dès que l'un d'eux change) ; les appels des bots ne sont pas chiffrés de bout en bout. Le second montre l'écran du bot une fois activé : activé, en attente de l'opérateur, but livraison, plafond 50, demandes du jour.
- Conditions, version 4 : si vous ne les avez pas encore acceptées, les conditions du propriétaire de bot s'affichent avant l'activation. Les points 12 à 24 sont les engagements propres aux appels des bots.
- Buts : l'activation exige au moins un but parmi
reminder(rappels) ·wake_up(réveils) ·delivery(livraisons) ·appointment(rendez-vous). Chaque demande indique l'un d'eux ; un but que le bot n'a pas choisi donne400. Changer de buts redemande l'autorisation aux personnes concernées, et l'opérateur examine de nouveau. - Plafond quotidien : le nombre de demandes que le bot peut envoyer par jour — 10, 20 ou 50 (50 par défaut).
- L'examen par l'opérateur : l'opérateur consulte sur la page d'administration le nom, le @nom d'utilisateur, la photo, les instructions, le message d'accueil, les outils, la voix, la langue, le mode, les buts et le nom du propriétaire, puis autorise le bot. Si l'un d'eux change, le bot ne peut plus appeler tant qu'il n'est pas de nouveau autorisé (
403 VOICE_OUTBOUND_OFF: review). - Le jeton : modifier les réglages vocaux avec le jeton (sauf pour couper la voix) gèle les appels du bot pendant 24 heures (
frozen). Si c'était vous, levez le gel avec C'est moi qui l'ai changé (내가 바꿨어요,vdk·vdK) dans @BotFather. Régénérer le jeton gèle aussi pendant 24 heures, met fin aux appels en cours et supprime le webhook d'un bot dont les appels sont activés. - Seuls les bots en coréen ou en anglais (
ko-KR·en-US) peuvent appeler. Un bot en mode flux, avec sa propre clé, des outils ou des serveurs MCP ne le peut pas (mode).
L'autorisation de la personne appelée
Un bot ne peut pas obtenir l'autorisation lui-même. La personne ouvre l'écran d'autorisation de ce bot dans l'application Pabal et touche « I am 19 or older · Allow » (J'ai 19 ans ou plus · Autoriser). L'écran indique qu'une IA passe ces appels, les buts, le nom du propriétaire du bot, la destination du son et les heures calmes (Utiliser l'application — Autoriser un bot à vous appeler). Il s'ouvre de trois façons.
- Le menu du bot : « Allow calls… » (Autoriser les appels…) dans la discussion ou le profil du bot.
- Un lien dans la conversation 1:1 : le bot envoie
https://pabal.me/<nom d'utilisateur du bot>?callme=1dans sa conversation 1:1 avec la personne, en lien dans le texte ou en bouton URL intégré. L'écran ne s'ouvre que pour un message envoyé par ce bot dans cette conversation — un lien transféré ailleurs, un aperçu ou le web n'ouvrent que la discussion. Une personne peut ouvrir l'écran depuis un lien une fois par jour (tous bots confondus), et jamais si elle n'a jamais écrit au bot. - La liste dans les réglages « Bots allowed to call me » (Bots autorisés à m'appeler) : voir les bots autorisés, les désactiver, les autoriser de nouveau.
curl -s "https://pabal.me/bot$BOT_TOKEN/sendMessage" -d chat_id=$USER_ID \
--data-urlencode "text=Voulez-vous un appel avant l'arrivée de votre commande ?" \
--data-urlencode 'reply_markup={"inline_keyboard": [[{"text": "Autoriser les appels", "url": "https://pabal.me/order_helper_bot?callme=1"}]]}'
- Serveurs auto-hébergés : les applications ne connaissent que les liens
pabal.me. Pour un autre serveur, son hôte doit être ajouté aux règles de liens de la version de l'application avant qu'un lien ouvre l'écran (le menu et la liste des réglages fonctionnent tels quels). - Pour l'instant : seuls les comptes avec un numéro +82, sur un appareil réglé sur le fuseau horaire de la Corée (Séoul) et connecté depuis au moins 24 heures, peuvent activer l'autorisation (l'opérateur choisit les pays). Une personne qui répond avoir moins de 19 ans ne peut plus avoir d'appels avec des bots pendant un an.
- Quand le nom du bot, son propriétaire ou ses buts changent, ou un an après l'autorisation, la question est reposée ; d'ici là, le bot ne peut pas appeler.
Appeler — startVoiceCall
curl -s "https://pabal.me/bot$BOT_TOKEN/startVoiceCall" -d user_id=$USER_ID -d purpose=delivery \
--data-urlencode "context=La commande A-1042 arrive aujourd'hui vers 15 h. Demandez si la personne pourra la recevoir." \
-d max_duration=180 -d ring_timeout=30
{"ok": true, "result": {"voice_call_id": "8812345678901234"}}
| Paramètre | Signification |
|---|---|
user_id | L'identifiant de la personne à appeler, qui doit avoir autorisé ce bot |
purpose | L'un des buts choisis par ce bot : reminder · wake_up · delivery · appointment |
context | Ce que le modèle, et lui seul, sait de cet appel : 1 à 1 000 caractères, 3 072 octets. Contrôlé selon les règles de contenu (usurpation, IA cachée, demande de secrets, publicité). Le modèle s'en sert après ses premiers mots (nom, IA, but), une fois que la personne a répondu |
max_duration | Durée maximale une fois l'appel décroché (secondes) : de 30 jusqu'à 300 ou au maximum du bot, le plus petit des deux. 180 par défaut |
ring_timeout | Durée de sonnerie (secondes), de 30 à 45. 40 par défaut |
| Réponse | Signification | Que faire |
|---|---|---|
200 {"voice_call_id": "…"} | Prise en compte. Si cela a sonné et si la personne a répondu arrive dans les mises à jour | — |
400 Bad Request: <quoi> | Un paramètre erroné, un but que le bot n'a pas choisi (purpose is not declared), une langue du bot autre que le coréen ou l'anglais | Corriger |
400 VOICE_CONTEXT_REFUSED: <code> | context enfreint les règles de contenu | Corriger |
403 VOICE_OUTBOUND_OFF: <raison> | Côté bot : owner_off (non activé) · terms (conditions, version 4) · review (en attente de l'examen par l'opérateur) · suspended (arrêt automatique) · frozen (gel lié au jeton) · owner_name (le nom du propriétaire échoue au contrôle d'usurpation) · mode, ou une raison côté bot des réglages vocaux (unavailable_reason) : bot_voice_off · not_allowed · suspended · name_refused · text_refused · awaiting_review · stream_unavailable · own_key_unavailable · no_gateway · no_gateway_with_tools · no_gateway_with_language · no_own_key_gateway. Les raisons côté serveur voice_off · kill_switch · no_relay · outbound_off arrivent en 503, pas en 403 | @BotFather · l'opérateur |
403 USER_UNAVAILABLE | Impossible d'appeler cette personne : pas d'autorisation, désactivée ou à redemander, bot bloqué, pas une personne, hors périmètre, moins de 19 ans, pas testeur. Le corps est toujours le même, quelle que soit la raison | Demander l'autorisation |
429 retry after N | Une limite côté bot ou propriétaire : rythme des demandes, demandes quotidiennes du bot ou du propriétaire, appels simultanés du bot, minutes d'appels sortants du propriétaire, minutes d'appel par jour du bot ou du propriétaire (partagées avec les appels vers le bot) | Après N secondes |
503 VOICE_UNAVAILABLE | Côté serveur : interrupteur des appels sortants coupé, voix coupée sur le serveur (y compris quand la politique de confidentialité n'a pas de section sur les appels de bots), arrêt d'urgence, aucun relais d'appel, limites et part d'appels sortants du serveur, aucune passerelle capable d'appeler (ou toutes pleines), demande impossible à enregistrer | Plus tard |
{"ok": false, "error_code": 403, "description": "Forbidden: USER_UNAVAILABLE"}
Le côté de la personne est examiné après le 200. Même avec une autorisation, pendant les heures calmes, pendant une pause, au-delà de la limite quotidienne avec cette personne ou de sa propre limite et de son intervalle, sans appareil à faire sonner ou si elle est déjà en ligne, rien ne sonne et not_answered arrive environ ring_timeout + 12 secondes après la demande — exactement comme une sonnerie restée sans réponse. Une autorisation ne change rien au côté du bot (403, 429, 503).
Réglages et utilisation
"outbound": {
"enabled": true,
"purposes": ["delivery"],
"daily_cap": 50,
"reviewed": true,
"frozen_until": 0,
"suspended": false,
"available": true
}"outbound": {
"today": {"requests": 4, "answered": 2, "not_answered": 2, "minutes": 3},
"limits": {"daily": 50, "owner_daily": 100, "owner_minutes": 30}
}outbounddansgetVoiceCallSettingsest toujours présent : ce qui a été réglé dans @BotFather (enabled·purposes·daily_cap), l'examen par l'opérateur (reviewed), la fin d'un gel lié au jeton (frozen_until, 0 sans gel), l'arrêt automatique (suspended) et la possibilité d'appeler maintenant (available).unavailable_reasonn'apparaît que siavailablevaut false (côté bot, les mêmes valeurs que les raisons de403 VOICE_OUTBOUND_OFF; côté serveur,voice_off·kill_switch·no_relay·outbound_off— ces quatre-là correspondent à503 VOICE_UNAVAILABLE).outbounddansgetVoiceCallUsagen'apparaît que pour un bot qui a déjà activé les appels : demandes, réponses, absences de réponse et minutes du jour, et les limites quotidiennes qui s'appliquent à ce bot. Le nombre de sonneries n'est pas donné.calls·seconds·missed·refusedne comptent jamais les appels passés par le bot. Mais les appels passés par le bot consomment les mêmes minutes d'appel par jour du bot et de son propriétaire que les appels vers le bot : les secondes d'un appel décroché sont décomptées, doncbotetownerderemaining_seconds_todaybaissent — et pendant la sonnerie, ils baissent déjà de ce que l'appel réserve, qui revient si personne ne répond.setVoiceCallSettingsne peut pas les modifier (400 VOICE_FIELD_NOT_AVAILABLE).
Mises à jour
Elles arrivent sous forme des mêmes mises à jour voice_call que les appels au bot (indiquez voice_call dans allowed_updates). direction vaut "outgoing", et la personne n'apparaît que par l'identifiant dans to (ni nom, ni nom d'utilisateur, ni numéro de téléphone).
{
"update_id": 83,
"voice_call": {
"id": "8812345678901234",
"direction": "outgoing",
"event": "not_answered",
"to": {"id": 100005},
"purpose": "delivery",
"date": 1790000123
}
}{
"update_id": 81,
"voice_call": {
"id": "8812345678901234",
"direction": "outgoing",
"event": "started",
"to": {"id": 100005},
"purpose": "delivery",
"date": 1790000130,
"max_duration": 180
}
}{
"update_id": 82,
"voice_call": {
"id": "8812345678901234",
"direction": "outgoing",
"event": "ended",
"to": {"id": 100005},
"purpose": "delivery",
"date": 1790000250,
"start_date": 1790000130,
"duration": 120,
"max_duration": 180,
"end_reason": "callee"
}
}{
"update_id": 84,
"voice_call": {
"id": "8812345678901234",
"direction": "outgoing",
"event": "failed",
"to": {"id": 100005},
"purpose": "delivery",
"date": 1790000110
}
}event | Quand | Champs en plus |
|---|---|---|
started | La personne a décroché et l'appel est établi. Sur une Apple Watch, quand le son de la montre s'est joint (sinon, un ended de 0 seconde avec failed) | max_duration |
ended | Un appel décroché s'est terminé. Il arrive jusqu'à 10 secondes plus tard quels que soient les réglages du serveur, le temps que le serveur reçoive l'usage de la passerelle | start_date, duration, max_duration, end_reason |
not_answered | Une demande restée sans réponse — sonnerie ignorée, refus, ou aucune sonnerie à cause du côté de la personne, sans distinction. Environ ring_timeout + 12 secondes après la demande | Aucun |
failed | La liaison avec la passerelle chargée de la demande s'est perdue avant la réponse | Aucun |
end_reason : callee (la personne a raccroché) · bot (le bot a raccroché, ou son jeton a été régénéré) · max_length (durée maximale) · failed (connexion perdue après la réponse) · operator (l'opérateur ou l'arrêt d'urgence) · suspended (bot ou propriétaire suspendu). Les appels passés par un bot n'ont ni missed, ni refused, ni ringing — la rapidité de la réponse révélerait si la personne était près de son téléphone — et pas de transcript (désactivé par conception).
Pendant l'appel
endVoiceCall: tant que le bot n'a pas été informé de l'issue, retire la demande, qu'elle sonne ou pas encore (true, puisnot_answered). Termine un appel décroché (end_reasonbot). Retirer une demande dans les 15 secondes met la paire en pause pendant une heure.sendVoiceCallContext: seulement une fois que la personne a décroché (avant,VOICE_CALL_NOT_FOUND). Trois par appel, 1 000 caractères et 3 072 octets, mêmes règles de contenu quecontext. Si la passerelle n'accepte pas les notes,VOICE_CONTEXT_NOT_SUPPORTED.
Ce que la personne appelée voit et entend
- La sonnerie : « (AI) » après le nom du bot, et « AI voice bot · Pabal never asks for codes » (bot vocal IA · Pabal ne demande jamais de code).
- En décrochant, une annonce passe avant le bot : « This is a Pabal AI voice call. We never ask for passwords, codes or money transfers. To stop these calls, turn off call permission in the bot's chat. » (C'est un appel vocal IA de Pabal. Nous ne demandons jamais de mot de passe, de code ni de virement. Pour arrêter ces appels, retirez l'autorisation d'appel dans la discussion du bot.) — dans la langue de l'écran d'autorisation que la personne a lu (coréen ou anglais) ; depuis une application dans une autre langue, en anglais. Le propriétaire du bot ne peut pas la couper.
- Les premiers mots du bot : son nom, qu'il est une IA, et le but de l'appel.
contextvient ensuite. - Pour y mettre fin : « Stop calls from this bot » (Arrêter les appels de ce bot) dans le menu, bloquer le bot ou le signaler y mettent toujours fin. Dire clairement pendant un appel quelque chose comme « ne m'appelez plus jamais » le coupe aussitôt (un simple « stop » dépend du jugement du modèle).
- En pause : après un refus, une sonnerie restée sans réponse ou un raccrochage dans les 10 secondes suivant la réponse, le bot ne peut plus appeler cette personne pendant 24 heures.
- La trace d'appel manqué : quand un appareil a vraiment sonné, que la personne n'a pas bloqué le bot et que la discussion du bot n'a aucun message non lu, il reste « Missed AI bot call » (appel de bot IA manqué — lu, sans notification). S'il y a un message non lu, ou si rien n'a sonné à cause des heures calmes ou d'une pause, il n'y a pas de trace. Chaque demande d'un bot est conservée sans exception dans les preuves de signalement (30 jours).
- Ce que reçoit le propriétaire du bot (dans l'ordre de l'écran d'autorisation) :
- l'identifiant Pabal de la personne
- si l'autorisation est active (les réponses à ses demandes d'appel le montrent)
- si la personne a répondu
- l'heure de début de l'appel et sa durée
- qui a raccroché
- Appareils partagés : tout appareil connecté peut répondre, et le bot dit le but et ce qu'on lui a indiqué à quiconque répond. Sur un ordinateur partagé, rappelez de se déconnecter.
- L'écran verrouillé : sur Android 14 et versions ultérieures, si la personne n'autorise pas les notifications en plein écran, l'appel arrive comme une notification et non comme un écran d'appel par-dessus l'écran verrouillé. Les iPhone et les Apple Watch sonnent comme pour un appel téléphonique.
- Garder la conversation la garde uniquement dans la discussion de la personne, si elle le choisit sur l'écran d'autorisation. Avec Répondre sur la montre activé dans l'app Pabal pour Apple Watch, l'Apple Watch sonne aussi.
- Les appels des bots ne sont pas chiffrés de bout en bout — le son passe par le serveur de Pabal et par le fournisseur d'IA.
Limites · heures calmes · pause · arrêt automatique
| Limite | Par défaut | Sur un serveur de production |
|---|---|---|
| Un bot vers une personne (par jour) | 3 appels | L'opérateur peut seulement l'abaisser |
| Vers une personne (par jour, tous bots) | 6 appels | Peut seulement être abaissée |
| Intervalle entre deux appels vers une personne | 10 minutes | Peut seulement être augmenté |
| Demandes d'un bot (par jour) | 50 appels | Le propriétaire choisit 10, 20 ou 50 dans @BotFather |
| Un propriétaire de bot (par jour, tous bots) | 100 appels · 30 minutes | Le nombre d'appels peut seulement être abaissé |
| Avant que le bot puisse envoyer sa demande suivante | 10 secondes | Peut seulement être augmenté |
| Minutes d'appels sortants du serveur | 30 % des minutes quotidiennes du serveur | Peut seulement être abaissée |
| Heures calmes | 21:00–08:00 | L'opérateur et la personne peuvent seulement les élargir. Dans le fuseau horaire de l'appareil |
Plage de réveil (wake_up uniquement) | 120 minutes au plus | La personne la choisit dans ses heures calmes |
Sonnerie (ring_timeout) | 30–45 secondes | 40 secondes si la demande ne le précise pas |
| Pause — refus, sonnerie sans réponse, raccrochage dans les 10 secondes après la réponse | 24 heures | Peut seulement être augmentée |
| Pause — le bot a retiré sa demande dans les 15 secondes | 1 heure | Peut seulement être augmentée |
- « Par jour » s'entend dans le fuseau horaire du serveur (
TELEGRAM_VOICE_TIME_ZONE). Un appel ne dure jamais plus de 300 secondes. - Les appels des bots consomment aussi les mêmes minutes d'appel par jour que les appels vers le bot (60 minutes pour le bot · 120 minutes pour le propriétaire). Une fois épuisées,
startVoiceCallreçoit429, et les appels vers le bot sont refusés aussi (refused,budget). - Une pause se termine quand le délai est écoulé, quand la personne appelle le bot, touche « I can take calls now » (Je peux prendre les appels maintenant) ou l'autorise de nouveau. Un message ou un bouton du bot n'y met pas fin.
- L'arrêt automatique ne coupe que les appels du bot vers des personnes (
403 VOICE_OUTBOUND_OFF: suspended) et dure jusqu'à ce que l'opérateur vérifie et le lève : un taux de réponse inférieur à 20 % sur les 20 derniers appels, 3 signalements en 7 jours de personnes qu'il a fait sonner, 30 % ou plus de ses personnes qui retirent l'autorisation (à partir de 10), 2 appels bloqués par les règles pendant l'appel, 5 retraits dans les 15 secondes, ou 2 personnes au plus qui répondent pour 60 minutes ou plus de réponse.
Limites
| Limite | Par défaut | Qui la fixe |
|---|---|---|
| Durée maximale d'un appel | 300 s pour le bot (plage réglable : de 60 au plafond de l'opérateur et du serveur, 600 par défaut) | Le propriétaire du bot, dans le plafond de l'opérateur et du serveur. Les boutons de @BotFather proposent 1, 3, 5 et 10 minutes |
| Appels simultanés d'un bot | 2 (de 1 au plafond de l'opérateur et du serveur, 2 par défaut) | Le propriétaire du bot, dans le plafond de l'opérateur et du serveur |
| Minutes d'appel par jour pour un bot | 60 minutes | L'opérateur |
| Par jour pour un propriétaire de bot (tous ses bots) | 120 minutes | L'opérateur |
| Par jour pour tout le serveur | 300 minutes | L'opérateur |
| Par appelant | 60 minutes par jour, 20 appels par heure | L'opérateur |
| Appel le plus court accepté | 60 secondes — s'il reste moins de temps, l'appel n'est pas pris | L'opérateur |
| Nouveaux comptes | Une personne inscrite depuis moins de 7 jours ne peut pas appeler — sauf les testeurs, et les personnes à qui le bot a envoyé un message il y a plus d'un jour | L'opérateur |
| Modifier les réglages | 30 par heure et par bot | — |
| Notes pendant l'appel | 10 par minute et par appel | — |
| Outils | 16 et 8 Ko ; requêtes ouvertes : 2 par appel, 8 par bot ; par minute : 30 par appel, 120 par bot | — |
| Tests de clé propre · sondes de flux | 5 par heure et par bot, 10 par propriétaire | — |
| Enregistrements et vérifications MCP | 10 par heure et par bot, 20 par propriétaire, 2 serveurs par bot | — |
Depuis l'Apple Watch, on ne peut pas appeler un bot qui utilise des outils, des serveurs MCP, une clé propre, le mode flux ou la transcription du propriétaire du bot. Ce refus n'est signalé qu'à l'appelant (403 WATCH_NOT_YET dans l'app pour Apple Watch), et le bot ne reçoit pas de mise à jour refused.
La durée d'un appel est la plus petite des valeurs suivantes : le temps restant aujourd'hui pour l'appelant, le bot, le propriétaire et le serveur, et le plafond de l'appel. À la prise de l'appel, cette durée est réservée sur les quatre limites, puis, à la fin, seules les secondes réelles selon l'horloge du serveur sont décomptées. Les appels avec clé propre ne consomment pas les limites du bot, du propriétaire et du serveur (celles de l'appelant s'appliquent toujours).
Erreurs
Les erreurs ont la même forme que pour les autres méthodes : {"ok": false, "error_code": 400, "description": "Bad Request: VOICE_…"}. Un 403 prend la forme Forbidden: <code>: <description> (sauf USER_UNAVAILABLE, toujours exactement Forbidden: USER_UNAVAILABLE, sans description), et un 429 porte parameters.retry_after.
| Code | Signification |
|---|---|
403 VOICE_RULES_NOT_ACCEPTED | Il faut d'abord accepter les règles des bots vocaux dans @BotFather |
403 VOICE_NOT_AVAILABLE | Ce bot ne peut pas utiliser la voix sur ce serveur (voix désactivée, bot non autorisé par l'opérateur) — les réglages ne peuvent pas être modifiés, seuls désactiver et effacer sont possibles |
403 VOICE_SUSPENDED | L'opérateur a suspendu la voix de ce bot. Seule la désactivation est possible |
403 VOICE_OWNER_TERMS_NOT_ACCEPTED | Il faut d'abord accepter les conditions du propriétaire de bot (version 4) |
403 VOICE_OWN_KEY_NOT_AVAILABLE | Ce serveur n'accepte pas de clé propre |
403 VOICE_STREAM_NOT_AVAILABLE | Le mode flux n'est pas utilisable sur ce serveur en ce moment |
403 VOICE_OUTBOUND_OFF: <raison> | Ce bot ne peut pas appeler de personnes pour l'instant : owner_off · terms · review · suspended · frozen · owner_name · mode, ou une raison côté bot des réglages vocaux (Appeler). voice_off · kill_switch · no_relay · outbound_off arrivent en 503 |
403 USER_UNAVAILABLE | Impossible d'appeler cette personne — toujours le même corps, quelle que soit la raison, absence d'autorisation comprise |
503 VOICE_UNAVAILABLE | Le serveur ne peut pas appeler de personnes pour l'instant (interrupteur des appels sortants, voix coupée sur le serveur, arrêt d'urgence, aucun relais d'appel, limites du serveur, passerelles, demande non enregistrée) — réessayez plus tard |
Bad Request: purpose is not declared, Bad Request: the bot's voice language must be ko or en for outbound calls | startVoiceCall : un but que le bot n'a pas choisi, une langue du bot autre que le coréen ou l'anglais |
VOICE_INSTRUCTIONS_REQUIRED, VOICE_INSTRUCTIONS_TOO_LONG, VOICE_GREETING_TOO_LONG | Des instructions sont nécessaires · les instructions dépassent 8 192 octets · le message d'accueil dépasse 512 octets |
VOICE_INVALID, VOICE_LANGUAGE_INVALID, VOICE_LIMIT_INVALID, VOICE_MODE_INVALID | Voix inexistante · langue · limite hors plage · mode autre que managed/stream |
VOICE_CONTENT_REFUSED: <champ>: <code>, VOICE_CONTEXT_REFUSED: <code> | Règles de contenu (Réglages, le context de startVoiceCall) |
VOICE_TOOL_INVALID: <chemin>: <raison> | Définition d'outil (Définir des outils) |
VOICE_FIELD_NOT_AVAILABLE: … | Champ refusé (model, api_key), champs de flux pour un bot géré, ou champs des appels des bots (outbound·purposes·daily_cap — uniquement dans @BotFather) |
VOICE_CALL_NOT_FOUND, VOICE_CALL_NOT_ACTIVE, VOICE_CONTEXT_NOT_SUPPORTED, VOICE_CONTEXT_TOO_LONG | Méthodes en cours d'appel (Pendant l'appel) |
VOICE_USAGE_PERIOD_INVALID | Période de getVoiceCallUsage |
TOOL_QUERY_ID_INVALID, TOOL_QUERY_EXPIRED: …, TOOL_RESULT_…, TOOL_ERROR_TOO_LONG | Réponse d'outil (Répondre) |
API_KEY_INVALID, API_KEY_NO_REALTIME_ACCESS, 503 API_KEY_CHECK_UNAVAILABLE, VOICE_PROVIDER_INVALID | Clé propre (Votre propre clé de modèle) |
STREAM_URL_INVALID, STREAM_SECRET_REQUIRED, STREAM_SECRET_INVALID, STREAM_PROBE_FAILED: … | Mode flux (Réglage et sonde) |
MCP_…, MCP_OPT_IN_REQUIRED | Serveurs MCP (Enregistrer un serveur). 503 VOICE_NOT_AVAILABLE: mcp: … : réessayez un peu plus tard ; 409 MCP_CHECK_IN_PROGRESS : réessayez une fois la vérification terminée |
429 Too Many Requests: retry after N | Limite horaire (réglages, tests de clé, sondes, MCP), limite des notes ou limite des appels des bots (limites) |
Bots connectés en MTProto
- Un bot connecté uniquement en MTProto appelle les méthodes vocales avec
bots.sendCustomRequest(même JSON, avec le nom de la méthode). Méthodes disponibles :setVoiceCallSettings,getVoiceCallSettings,answerVoiceCallToolQuery,endVoiceCall,sendVoiceCallContext,getVoiceCallUsage,startVoiceCallet les cinq méthodes MCP. Tout le reste :METHOD_INVALID. La réponse destartVoiceCallest{"voice_call_id": "…"}, et les refus arrivent en erreurs RPC (USER_UNAVAILABLE,VOICE_OUTBOUND_OFF: review,FLOOD_WAIT_n,VOICE_UNAVAILABLE). - Méthodes impossibles à appeler depuis une réponse de webhook :
setVoiceCallApiKey,startVoiceCallet les méthodes d'enregistrement et de clé MCP. Placées dans le corps de la réponse à un POST de webhook, elles ne sont pas exécutées — pour qu'aucun message envoyé par une personne ne devienne directement un appel téléphonique par une réponse de webhook. Appelez-les par une requête à part. - La clé propre passe uniquement par HTTPS :
setVoiceCallApiKeyetdeleteVoiceCallApiKeyne peuvent pas être appelées parsendCustomRequest. Pour effacer la clé, utilisez la Bot API HTTP ou @BotFather. - Pour un bot sans webhook qui n'a pas appelé
getUpdatesdans les 15 dernières secondes,voice_call(y compris celui d'un appel passé par le bot) est poussé une fois parupdateBotWebhookJSON(indépendamment deallowed_updates, sans renvoi). Les requêtes d'outil arrivent parupdateBotWebhookJSONQuery, et l'on répond{"result": …}ou{"error": "…"}avecbots.answerWebhookJSONQuery.voice_call_transcriptn'arrive pas en MTProto (le champtranscriptde la mise à jour de fin, lui, arrive). - Côté erreurs RPC,
429devientFLOOD_WAIT_n, et les autres restent le code en majuscules tel quel.