Hipcall, çağrılarınızda konuşulanları çağrı sürerken yazıya döker. Bu rehber, o metni kendi sisteminize canlı olarak nasıl alacağınızı anlatır: bir WebSocket bağlantısı açarsınız, hesabınızdaki çağrıların transkripti konuşuldukça size akar.
Kimler için: Kendi yazılımında çalışan bir geliştirici. Hipcall ekranlarında bir şey yapmak için bu rehbere ihtiyacınız yok; transkript zaten çağrı kaydının yanında görünür. Bu akış, metni kendi sisteminize taşımak için.
Nasıl çalışır
Çağrı bağlandığı anda konuşma tanıma oturumu açılır. Tanınan metin, çağrı hâlâ sürerken parça parça yayınlanır. Siz bu parçaları bir WebSocket üzerinden alır ve kendi tarafınızda birleştirirsiniz.
Çağrı (Hipcall telefon platformu)
└─ konuşma tanıma → transkript olayları
└─ wss://stream.hipcall.com.tr/v1/stream
└─ sizin sunucunuz (WebSocket istemcisi)
Bağlantıyı siz açarsınız; Hipcall sizin sunucunuza bağlanmaz. Webhook'lardan farkı burada: dışarıya açık bir uç nokta yayınlamanız, IP açmanız ya da imza doğrulamanız gerekmez.
Bilmeniz gerekenler bu kadar:
- Her olay bir zarf (envelope) içinde gelir:
v,type,seq,call_uuid,ts,data. - Metni yeniden kurmanın tek bir kuralı var:
segment_id'ye göre üzerine yaz,is_final: trueolan bir parça artık donmuştur. - Çağrı bitince gelen
transcript.completedolayı nihai metindir; öncesinde gönderilen her şeyin yerine geçer.
Önkoşullar
| Gereken | Nerede |
|---|---|
| Hesabınızda transkript özelliğinin açık olması | Ayarlar → Yapay zekâ → Transkript |
| Planınızda API erişimi | Planınızda yoksa "Canlı transkript akışı" anahtarı kilitli görünür |
| Geçerli bir API token | Ayarlar → Geliştirici → API Token'ları |
| Jeton bakiyesi | Transkript dakika başına jetonla ücretlendirilir; bakiye bitince yeni metin üretilmez |
| WebSocket istemcisi çalıştırabilen bir sunucu | Tarayıcıdan değil, sunucudan bağlanın (kimlik bilgisi header'da gider) |
1. Adım: Transkripti ve canlı akışı açın
https://use.hipcall.com.tr/portal/settings/ai/transcription/
- Çağrıların transkriptini çıkar anahtarını açın.
- Hangi çağrılar altından en az birini seçin: Gelen çağrılar, Giden çağrılar, Dahili çağrılar. Hiçbiri seçili değilken kayıt yapılamaz.
- Metin ne zaman hazır olsun altında Çağrı sürerken seçeneğini işaretleyin. Canlı akış yalnızca bu seçenekle çalışır. Çağrı bittikten sonra seçilirse metin sadece çağrı kaydının yanında oluşur, akıtılacak bir şey olmaz.
- Canlı transkript akışı anahtarını açın.
- Kaydet'e basın.
[Screenshot: Transkript ayarları sayfası; "Çağrı sürerken" seçili ve "Canlı transkript akışı" açık]
Not: Çağrı bittikten sonra seçeneğine geri dönerseniz canlı akış da otomatik olarak kapanır. Sayfa bunu kaydetmeden önce uyarı olarak gösterir.
Beta: Bu özellik beta aşamasındadır. Sayfada "Bu ayarlar çağrılarınıza henüz uygulanmıyor" uyarısını görüyorsanız, hangi çağrıların yazıya döküldüğü hâlâ hesabınızın mevcut kurulumuna göre belirleniyor demektir; buradaki anahtarlar akışın kendisini açıp kapatmaya devam eder. Uyarı, telefon platformu bu seçimleri okumaya başladığında kaybolur.
2. Adım: API token oluşturun
https://use.hipcall.com.tr/portal/settings/developer/api-tokens/
Yeni API Token → bir ad ve son kullanma tarihi verin (varsayılan 1 yıl, en fazla 3 yıl) → Kaydet. Token yalnızca bir kez gösterilir; bir yere kaydedin.
Bu, REST API'de kullandığınız token'ın aynısıdır. Ayrı bir akış kimliği yoktur: token saklanmış transkriptleri okuma yetkisini zaten veriyor, canlı akış bunu daha erken teslim etmekten ibaret.
Token'ı iptal ederseniz açık bağlantı da kapanır. Sunucu her 5 dakikada bir token'ı ve hesabın yetkisini yeniden kontrol eder; iptal edilmiş ya da süresi dolmuş bir token'la bağlantı
1008koduyla düşürülür.
3. Adım: Bağlanın
wss://stream.hipcall.com.tr/v1/stream
Kimlik bilgisi header ile gider:
Authorization: Bearer <api_token>
Query string (?token=...) desteklenmez. Bu sunucudan sunucuya bir entegrasyon olduğu için buna gerek yok, ve ?token= uzun ömürlü bir kimlik bilgisini aradaki her erişim kaydına yazardı.
Bağlantı kurulduktan sonra bir abonelik mesajı gönderirsiniz:
→ {"action": "subscribe", "v": 1}
← {"action": "subscribed", "v": 1, "call_uuid": null}
call_uuid verirseniz sadece o çağrıyı, vermezseniz hesaptaki bütün çağrıları dinlersiniz:
→ {"action": "subscribe", "v": 1, "call_uuid": "b6e2-..."}
← {"action": "subscribed", "v": 1, "call_uuid": "b6e2-..."}
Bir bağlantı = bir abonelik. İkinci bir
subscribegönderirsenizalready_subscribedhatası alırsınız. İki belirli çağrıyı ayrı ayrı izlemek istiyorsanız iki bağlantı açın ya da hepsine abone olup kendi tarafınızda süzün.
Bağlantıyı canlı tutmak için uygulama seviyesinde ping gönderebilirsiniz:
→ {"action": "ping"}
← {"action": "pong"}
WebSocket protokolünün kendi ping/pong'u da çalışır; istemcinizde 20 saniyelik bir ping aralığı yeterlidir. Bir çağrı dakikalarca tamamen sessiz kalabilir, bu yüzden boşta kalan bağlantıyı kendiniz canlı tutun.
Protokol
İstemciden sunucuya
| Mesaj | Alanlar |
|---|---|
{"action": "subscribe"} |
v (tam sayı, opsiyonel; verilmezse sunucunun en yeni sürümü), call_uuid (opsiyonel) |
{"action": "ping"} |
— |
Sunucudan istemciye
| Mesaj | Anlamı |
|---|---|
{"action": "subscribed", "v": 1, "call_uuid": null} |
Abonelik kuruldu |
{"action": "pong"} |
Ping yanıtı |
{"action": "error", "code": "...", "message": "..."} |
Hata (aşağıdaki tablo) |
| zarf | Transkript olayı; action yerine type taşır |
Gelen mesajı ayırt etmenin yolu:
actionvarsa kontrol mesajıdır,typevarsa transkript olayıdır.
Zarf
Her transkript olayı, tipi ne olursa olsun, bu kabukla gelir:
{
"v": 1,
"type": "transcript.segment",
"seq": 42,
"call_uuid": "b6e2-...",
"account_id": 1234,
"ts": "2026-09-19T10:31:04.812Z",
"data": { }
}
| Alan | Açıklama |
|---|---|
v |
Bu kablo biçiminin sürümü. Bugün 1. |
type |
Olay tipi (aşağıda). |
seq |
Çağrı başına 1'den başlayan sayaç. Gönderilmeyen olayları da sayar; bkz. Kayıp ve tekrar. |
call_uuid |
Çağrının kimliği. Aynı bağlantıda birden çok çağrı akabilir; olayları buna göre ayırın. |
account_id |
Hesap kimliği. |
ts |
Olayın UTC zaman damgası (ISO 8601). |
data |
Tipe özel gövde. |
v'yi subscribe sırasında bildirirsiniz. Sunucu daha yeni olayları anladığınız sürüme indirger, indirgeyemiyorsa aboneliği reddeder. Beklemediğiniz bir biçimi sessizce almazsınız.
Olay tipleri
transcript.started
Bir çağrının ilk olayıdır, her zaman. Önce transcript.segment görüyorsanız çağrıya sonradan katıldınız demektir.
{
"language": "tr",
"direction": "inbound",
"from": "+90...",
"to": "+90..."
}
direction: inbound, outbound veya internal.
transcript.segment
Konuşuldukça gelen parça.
{
"segment_id": 7,
"speaker": "1",
"start_ms": 1500,
"end_ms": 3120,
"text": "Cumartesi. 8 Ağustos",
"is_final": false
}
| Alan | Açıklama |
|---|---|
segment_id |
Çağrı içinde artan parça numarası. Birleştirmede bunu anahtar olarak kullanın. |
speaker |
Sağlayıcı konuşmacı ayrımı yaptıysa "1", "2" gibi bir etiket; yoksa null. Kimlik değildir ve yalnızca o çağrı içinde anlamlıdır, çağrılar arasında taşımayın. |
start_ms / end_ms |
Çağrı başlangıcına göre milisaniye. |
text |
Parçanın o anki metni. |
is_final |
true ise bu parça donmuştur, bir daha değişmez. |
transcript.failed
{"reason": "provider_error"}
Sonlandırıcıdır: bu çağrıda artık yeni metin gelmez. Öncesinde gelen her şey geçerlidir ve transcript.completed yine de gönderilir.
reason bizim tanımladığımız kapalı bir kümedir. Konuşma tanıma sağlayıcısının kendi hata kodu geçirilmez. O kod sağlayıcıya özeldir ve anlamı sağlayıcıyla birlikte değişir.
transcript.completed
Çağrı başına bir kez, çağrı kaydı (CDR) yazıldığında gelir. Nihaidir: o çağrı için gönderilmiş tüm transcript.segment olaylarının yerine geçer.
{
"cdr_uuid": "...",
"segments": [
{"segment_id": 1, "speaker": "1", "start_ms": 0, "end_ms": 3120,
"text": "...", "is_final": true}
]
}
cdr_uuid, metni kendi tarafınızdaki çağrı kaydına bağlayan alandır.
Canlı önizlemeye ihtiyacınız yoksa sadece bu olayı dinleyin, diğer her şeyi yok sayın. Kurulabilecek en basit entegrasyon bu.
Transkripti yeniden kurma
Kuralın tamamı şu:
segment_id'ye göre upsert edin.is_final: truedonmuş demektir. Metin,segment_idsırasına göre birleştirilir.
segments = {} # segment_id -> segment
def apply(segment):
segments[segment["segment_id"]] = segment
def text():
return "".join(s["text"] for _, s in sorted(segments.items()))
Bu kadar basit olmasının sebebi, konuşma tanıma motorunun ham geçici/kesin metin semantiğinin bizim tarafımızda çözülmüş olması. Ham akışta geçici sonuçlar kararsız kuyruğu baştan yazar, kesin sonuçlar yalnızca yeni donan parçayı taşır ve o parça geçici akıştan kaybolur. Birleştirirseniz "CCumCumarCumartesi." elde edersiniz, üzerine yazarsanız donmuş metni çöpe atarsınız. Bunların hiçbiri size ulaşmıyor.
Hata gibi görünen ama hata olmayan iki davranış var:
- Bir parçanın metni kısalabilir. Geçici kuyruğun bir sonraki konuşmacıya ait olduğu anlaşılırsa, parça yalnızca kesinleşmiş metniyle donar ve kuyruk bir sonraki
segment_idaltında yeniden görünür. Upsert bunu kendiliğinden halleder. speakerboş gelebilir. Konuşmacı ayrımı her zaman mümkün olmaz.
Kayıp ve tekrar: seq
seq, çağrının ürettiği her olayı sayar, gönderilmeyenleri de. Bir boşluk gördüğünüzde sunucu yük altında canlı önizlemeyi düşürmüş demektir.
- Düşen tek şey, canlı (henüz donmamış) parça güncellemeleridir. Geç gelen bir önizleme hiç gelmeyenden kötüdür.
transcript.started, donmuş (is_final: true) parçalar,transcript.failedvetranscript.completedasla düşmez. Yük altında akış, harf harf önizleme yerine her konuşma sırasını kapandığında teslim etmeye iner; söylenen hiçbir kelime kaybolmaz.seqgeriye giderse aynı olayı ikinci kez almışsınızdır. Taşıma katmanı aynı mesajı yeniden teslim edebilir. Sırası karışmış değil, tekrarlanmış: yok sayın.
Sıralama yalnızca çağrı bazında ve yalnızca seq ile garantilidir. Varış sırasına güvenmeyin.
def check_gap(call_uuid, seq):
previous = last_seq.get(call_uuid)
last_seq[call_uuid] = seq
if previous is None:
return
if seq <= previous:
pass # tekrar teslim — yok say
elif seq > previous + 1:
pass # önizleme düştü — metin yine de gelecek
Bir de şu tuzak var: tekrar teslim edilen bir olay, çağrı bittikten sonra gelebilir. Tamamlanmış çağrıların kimliklerini sınırlı bir kümede tutup geç gelen olayları yok sayın; yoksa kapanmış bir çağrı tek satırlık bir transkriptle "yeniden canlanır".
Örnek kod
Python — en sade hâli
pip install websockets # 10.x ve 14+ ikisi de çalışır
export HIPCALL_TOKEN=<api token>
import asyncio, json, os, ssl, websockets
URL = "wss://stream.hipcall.com.tr/v1/stream"
async def main():
headers = {"Authorization": f"Bearer {os.environ['HIPCALL_TOKEN']}"}
async with websockets.connect(
URL,
additional_headers=headers, # websockets < 14 için: extra_headers
ssl=ssl.create_default_context(),
ping_interval=20,
ping_timeout=20,
) as socket:
await socket.send(json.dumps({"action": "subscribe", "v": 1}))
calls = {} # call_uuid -> {segment_id: segment}
async for frame in socket:
message = json.loads(frame)
if message.get("action"): # kontrol mesajı
print(message)
continue
call_uuid, data = message["call_uuid"], message["data"]
if message["type"] == "transcript.segment":
segments = calls.setdefault(call_uuid, {})
segments[data["segment_id"]] = data
print(call_uuid[:8],
"".join(s["text"] for _, s in sorted(segments.items())))
elif message["type"] == "transcript.completed":
text = "".join(s["text"] for s in data["segments"])
print(f"BİTTİ {call_uuid} (cdr {data['cdr_uuid']}): {text}")
calls.pop(call_uuid, None)
asyncio.run(main())
Bu kadarı akışı görmenizi sağlar ama üretime yetmez: bağlantı koptuğunda geri gelmez, kaçırdığı olayı fark etmez, tekrar teslim edilen olayı iki kez işler. Aşağıdaki tam istemci bu üçünü de kapsıyor.
Python — tam istemci
Olduğu gibi kopyalayıp hipcall_client.py olarak kaydedin ve çalıştırın:
pip install websockets # 10.x ve 14+ ikisi de çalışır
export HIPCALL_TOKEN=<api token>
# hesabın tüm çağrıları
python3 hipcall_client.py wss://stream.hipcall.com.tr/v1/stream
# tek bir çağrı
python3 hipcall_client.py wss://stream.hipcall.com.tr/v1/stream <call_uuid>
Kendi istemcinize taşımaya değer üç parça var: Transcript.apply, seq boşluk kontrolü ve yeniden bağlanma döngüsü. Gerisi ekrana yazdırmak.
#!/usr/bin/env python3
"""
Hipcall canlı transkript akışı için çalışan bir istemci.
Canlı bir hesaba karşı çalıştırın; her çağrının transkriptini konuşuldukça
ekrana yazar.
pip install websockets
export HIPCALL_TOKEN=<api token>
python3 hipcall_client.py wss://stream.hipcall.com.tr/v1/stream [call_uuid]
"""
import asyncio
import json
import os
import ssl
import sys
from collections import OrderedDict
from typing import Optional
import websockets
# websockets 10.x alt modüllerini tembel yükler, bu import gereksiz değil.
import websockets.exceptions
# Bu istemcinin anladığı zarf sürümü. Sunucu daha yeni olayları bu sürüme
# indirger, indiremiyorsa aboneliği reddeder — her iki durumda da beklemediğiniz
# bir biçimi sessizce almazsınız.
VERSION = 1
# websockets 14.0'da header argümanının adı değişti ve istisnalar yeniden
# şekillendi. Dağıtımlar hâlâ 10.x gönderdiği için bu örnek ikisinde de çalışır.
_WS_MAJOR = int(websockets.__version__.split(".")[0])
HEADER_KWARG = "additional_headers" if _WS_MAJOR >= 14 else "extra_headers"
# 14+ InvalidStatus, öncesi InvalidStatusCode fırlatır. İkisi de yeniden
# denemeye değmez: ikisi de yükseltmenin 401 ya da 403 ile reddedildiği
# anlamına gelir. hasattr yerine sürüme bakılıyor, çünkü 14+ eski adı hâlâ
# gösteriyor ve ona dokunmak DeprecationWarning üretiyor.
REFUSED = (
getattr(
websockets.exceptions,
"InvalidStatus" if _WS_MAJOR >= 14 else "InvalidStatusCode",
),
)
def refusal_detail(error) -> str:
"""
Sunucunun 401 ya da 403 ile birlikte gönderdiği gövde.
Sunucu sebebi yazar — invalid_token, revoked, expired, forbidden — ve o
olmadan bir ret sadece "HTTP 401"dir; bu da beş saniyelik bir düzeltmeyle
bir öğleden sonra arasındaki fark. Gövdeyi yalnızca websockets 14+ açar;
eski sürümlerde sebebi görmek için curl kullanın.
"""
response = getattr(error, "response", None)
body = getattr(response, "body", None) if response is not None else None
if body:
return f" - {body.decode('utf-8', 'replace').strip()}"
return " (sebebi görmek için websockets>=14 kullanın ya da curl ile bakın)"
def close_code(error) -> tuple[Optional[int], str]:
"""Sunucunun gönderdiği kapanma çerçevesi, iki istisna biçimi için de."""
received = getattr(error, "rcvd", None)
if received is not None:
return received.code, received.reason
return getattr(error, "code", None), getattr(error, "reason", "")
class Transcript:
"""
Bir çağrının transkripti, olaylardan yeniden kurulmuş hâli.
Kuralın tamamı: **`segment_id`'ye göre upsert, `is_final` donmuş demek.**
Hata gibi görünen ama hata olmayan iki davranış:
* Bir parçanın metni **kısalabilir**. Geçici kuyruğun bir sonraki
konuşmacıya ait olduğu anlaşılırsa, parça yalnızca kesinleşmiş metniyle
donar ve kuyruk bir sonraki `segment_id` altında yeniden görünür. Upsert
bunu siz fark etmeden halleder.
* `speaker`, konuşmacı ayrımı yapılmadıysa `None`'dır; dolu olduğunda da
çağrıya özel bir etikettir ("1", "2"), kimlik değil. Çağrılar arasında
taşımayın.
"""
def __init__(self) -> None:
self.segments: dict[int, dict] = {}
def apply(self, segment: dict) -> None:
self.segments[segment["segment_id"]] = segment
def replace_all(self, segments: list[dict]) -> None:
"""`transcript.completed` nihaidir — her şeyin yerine geçer."""
self.segments = {segment["segment_id"]: segment for segment in segments}
def text(self) -> str:
return "".join(
segment["text"] for _id, segment in sorted(self.segments.items())
)
def settled(self) -> str:
"""Yalnızca donmuş kısım — çağrı sürerken bir modele vereceğiniz metin."""
return "".join(
segment["text"]
for _id, segment in sorted(self.segments.items())
if segment["is_final"]
)
class Session:
"""Bir bağlantıdaki çağrıları ve bir şey kaçırıp kaçırmadığımızı izler."""
# Aynı olay yeniden teslim edilebilir ve bu tekrar, çağrı bittikten *sonra*
# gelebilir. Bu olmadan eski bir parça tamamlanmış bir çağrıyı diriltir ve
# elinizde canlı görünen tek satırlık bir transkript kalır. Uzun çalışan bir
# istemcide sınırsız büyümesin diye sınırlı tutuluyor.
FINISHED_MEMORY = 1_000
def __init__(self) -> None:
self.calls: dict[str, Transcript] = {}
self.last_seq: dict[str, int] = {}
self.finished: OrderedDict[str, None] = OrderedDict()
def check_gap(self, call_uuid: str, seq: int) -> None:
"""
`seq`, çağrının ürettiği her olayı sayar — sunucunun yük altında
düşürdüklerini de. Yani boşluk bir hata değil, sunucunun canlı
önizlemeyi yetişmek için düşürdüğünün göstergesidir. Donmuş parçalar ve
`transcript.completed` asla düşmez, dolayısıyla metin yine gelir;
gelmeyen sadece harf harf önizlemedir.
Tekrar da mümkündür. `seq`'in geriye gitmesi "bunu zaten gördüm"
demektir, "sıra karıştı" değil.
"""
previous = self.last_seq.get(call_uuid)
self.last_seq[call_uuid] = seq
if previous is None:
return
if seq <= previous:
print(f" [tekrar teslim: seq {seq}, öncesi {previous}]")
elif seq > previous + 1:
print(f" [boşluk: seq {seq} öncesinde {seq - previous - 1} olay düştü]")
def handle(self, event: dict) -> None:
kind = event["type"]
call_uuid = event["call_uuid"]
data = event["data"]
if call_uuid in self.finished:
print(f" [{call_uuid[:8]} kapanmış çağrıya geç {kind}, yok sayıldı]")
return
self.check_gap(call_uuid, event["seq"])
if kind == "transcript.started":
self.calls[call_uuid] = Transcript()
print(
f"\n== {call_uuid} başladı "
f"({data['direction']}, {data['language']}, "
f"{data['from']} -> {data['to']})"
)
elif kind == "transcript.segment":
# Çağrının ortasında bağlanan bir istemci `transcript.started`
# görmemiştir, o yüzden çağrının bilindiğini varsaymayın.
transcript = self.calls.setdefault(call_uuid, Transcript())
transcript.apply(data)
marker = "*" if data["is_final"] else " "
print(f"{marker} {call_uuid[:8]} {transcript.text()}")
elif kind == "transcript.failed":
# Sonlandırıcı: bu çağrıda artık metin gelmez. Öncesinde geleni
# atmayın, `transcript.completed` yine de gelecek.
print(f"!! {call_uuid[:8]} transkript başarısız: {data['reason']}")
elif kind == "transcript.completed":
transcript = self.calls.setdefault(call_uuid, Transcript())
transcript.replace_all(data["segments"])
print(f"\n== {call_uuid} tamamlandı (cdr {data['cdr_uuid']})")
print(f" {transcript.text()}\n")
self.calls.pop(call_uuid, None)
self.last_seq.pop(call_uuid, None)
self.finished[call_uuid] = None
while len(self.finished) > self.FINISHED_MEMORY:
self.finished.popitem(last=False)
else:
# Yeni olay tipi eklenmesi kırıcı bir değişiklik değildir.
# Tanımadığınızda hata vermek yerine yok sayın.
print(f"?? {call_uuid[:8]} bilinmeyen olay {kind}")
async def stream(url: str, token: str, call_uuid: Optional[str]) -> None:
session = Session()
# Kimlik bilgisi header'da gider, asla query string'de: bu sunucudan
# sunucuya bir entegrasyon ve `?token=` uzun ömürlü bir kimlik bilgisini
# aradaki her erişim kaydına yazardı.
headers = {"Authorization": f"Bearer {token}"}
options = {
HEADER_KWARG: headers,
# Boşta kalan bağlantıyı canlı tut — bir çağrı dakikalarca tamamen
# sessiz kalabilir.
"ping_interval": 20,
"ping_timeout": 20,
}
if url.startswith("wss://"):
options["ssl"] = ssl.create_default_context()
async with websockets.connect(url, **options) as socket:
subscribe = {"action": "subscribe", "v": VERSION}
if call_uuid:
subscribe["call_uuid"] = call_uuid
await socket.send(json.dumps(subscribe))
async for frame in socket:
message = json.loads(frame)
# Kontrol mesajları "action", transkript olayları "type" taşır.
action = message.get("action")
if action == "subscribed":
scope = message["call_uuid"] or "tüm çağrılar"
print(f"abone olundu: {scope} (v{message['v']})")
elif action == "error":
print(f"hata [{message['code']}]: {message['message']}")
elif action == "pong":
pass
else:
session.handle(message)
async def main() -> None:
if len(sys.argv) < 2:
print(__doc__)
sys.exit(1)
url = sys.argv[1]
call_uuid = sys.argv[2] if len(sys.argv) > 2 else None
token = os.environ.get("HIPCALL_TOKEN")
if not token:
print("HIPCALL_TOKEN tanımlı değil.")
sys.exit(1)
# Geri çekilerek yeniden bağlan. Sunucu, kimlik bilgisi iptal edildiğinde
# ya da süresi dolduğunda, hesabın yetkisi kalktığında veya bu istemci
# yetişemeyecek kadar geride kaldığında bağlantıyı kapatır — bunlardan
# yalnızca sonuncusu yeniden denemeye değer, o yüzden kapanma koduna
# bakılıyor.
backoff = 1.0
while True:
try:
await stream(url, token, call_uuid)
print("bağlantıyı sunucu kapattı")
except REFUSED as error:
# Yükseltmede 401 ya da 403: yeniden denemek hiçbir şeyi düzeltmez.
print(f"reddedildi: {error}{refusal_detail(error)}")
return
except websockets.exceptions.ConnectionClosed as error:
code, reason = close_code(error)
if code == 1008:
# İptal edilmiş, süresi dolmuş ya da yetkisiz.
print(f"politika gereği kapatıldı: {reason}")
return
print(f"bağlantı koptu: {error}")
except OSError as error:
print(f"bağlanılamadı: {error}")
# Yeniden bağlandığınızda, ayrı kaldığınız sürede söylenenler tekrar
# oynatılmaz. O çağrı için `transcript.completed` hâlâ bekliyorsa tam
# metni orada alırsınız.
print(f"{backoff:.0f}s sonra yeniden bağlanılıyor")
await asyncio.sleep(backoff)
backoff = min(backoff * 2, 30.0)
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
pass
Node.js
import WebSocket from "ws"; // npm install ws
const socket = new WebSocket("wss://stream.hipcall.com.tr/v1/stream", {
headers: { Authorization: `Bearer ${process.env.HIPCALL_TOKEN}` },
});
const calls = new Map(); // call_uuid -> Map(segment_id -> segment)
socket.on("open", () => socket.send(JSON.stringify({ action: "subscribe", v: 1 })));
socket.on("message", (frame) => {
const message = JSON.parse(frame);
if (message.action) { // subscribed / pong / error
console.log(message);
return;
}
const { type, call_uuid, data } = message;
if (type === "transcript.segment") {
if (!calls.has(call_uuid)) calls.set(call_uuid, new Map());
calls.get(call_uuid).set(data.segment_id, data);
const text = [...calls.get(call_uuid).entries()]
.sort(([a], [b]) => a - b)
.map(([, segment]) => segment.text)
.join("");
console.log(call_uuid.slice(0, 8), text);
}
if (type === "transcript.completed") {
console.log("BİTTİ", call_uuid, data.segments.map((s) => s.text).join(""));
calls.delete(call_uuid);
}
});
socket.on("close", (code, reason) => {
console.log("kapandı", code, reason.toString());
// 1008 dışında: geri çekilerek yeniden bağlanın
});
Bağlantı yönetimi
Üretimde tek eksik parça yeniden bağlanma döngüsüdür. Kuralları şöyle:
| Durum | Ne yapmalı |
|---|---|
| Yükseltme isteğine 401 | Yeniden denemeyin. Token yanlış, silinmiş ya da süresi dolmuş. Sebep yanıt gövdesinde yazar. |
| Yükseltme isteğine 403 | Yeniden denemeyin. Hesap canlı akışa yetkili değil; ayarları 1. Adım'a göre kontrol edin. |
| Kapanma kodu 1008 | Yeniden denemeyin. Token iptal edildi, süresi doldu ya da yetki kalktı. |
Kapanma kodu 1011 + too_slow |
Yeniden bağlanın. Mesajları yeterince hızlı okumuyorsunuz. |
| Diğer kopmalar (ağ, dağıtım, zaman aşımı) | Geri çekilerek (1s → 2s → 4s … en çok 30s) yeniden bağlanın. |
Ayrıca:
- Bağlantıda 12 saat boyunca hiç veri alınmazsa düşürülür. Düzenli ping bunu zaten engeller; yine de uzun süreli istemcilerin yeniden bağlanma döngüsü olmalı.
- Ayrıldığınız sürede söylenenler tekrar oynatılmaz. Akış bir kuyruk değil, canlı bir yayındır. Yeniden bağlandığınızda o çağrı için
transcript.completedhâlâ bekliyorsa tam metni orada alırsınız; almadıysanız transkript Hipcall panelinde çağrı kaydının yanında durmaya devam eder. - Mesajları okumazsanız sunucu tamponu büyütmez, bağlantıyı kapatır. Böylece yavaş bir tüketici sessizce bozuk bir transkript biriktirmez.
Hata kodları
{"action": "error", "code": "...", "message": "..."} ile gelir.
code |
Anlamı |
|---|---|
malformed_json |
Gönderdiğiniz çerçeve geçerli JSON değil. |
missing_action |
Çerçevede action yok. |
unknown_action |
action tanınmadı. |
invalid_version |
v tam sayı olmalı. |
unsupported_version |
İstenen v bu servisin konuştuğu sürümden büyük (ya da 1'den küçük). |
already_subscribed |
Bu bağlantı zaten abone. Bir bağlantı = bir abonelik. |
unsupported_frame |
Metin dışı (binary) çerçeve gönderildi. |
feed_unavailable |
Abonelik şu an kurulamadı; geri çekilerek tekrar deneyin. |
too_slow |
Çok geride kaldınız, bağlantı kapatıldı. |
forbidden |
Hesap canlı akışa yetkili değil. |
revoked |
Kimlik bilgisi artık geçerli değil. |
expired |
Kimlik bilgisinin süresi dolmuş. |
HTTP yükseltme adımında reddedilirseniz sebep yanıt gövdesinde düz metin olarak döner. curl ile görebilirsiniz:
curl -i -H "Authorization: Bearer $HIPCALL_TOKEN" \
-H "Connection: Upgrade" -H "Upgrade: websocket" \
-H "Sec-WebSocket-Version: 13" \
-H "Sec-WebSocket-Key: $(openssl rand -base64 16)" \
https://stream.hipcall.com.tr/v1/stream
Payload'da bilerek olmayanlar
| Yok | Neden |
|---|---|
| Konuşma tanıma sağlayıcısının adı | Sağlayıcı değişebilir. Ona göre dallanan bir entegrasyon, sağlayıcı değişimini kırıcı hâle getirirdi. |
| Ham sağlayıcı yanıtı | Mesajın büyük kısmını kaplar ve kimsenin API'si değildir. |
confidence (güven skoru) |
Sağlayıcıya göre kalibre edilir, sağlayıcılar arasında karşılaştırılamaz. Bilgilendirmekten çok yanıltır. |
| Kanal/leg kimlikleri | Bugünkü kurulumda çağrı kimliğiyle çakışması bir tesadüf; ona güvenen entegrasyon sessizce kırılır. |
Alan eklemek kırıcı bir değişiklik değildir, çıkarmak kırıcıdır. Bu yüzden tanımadığınız type ve alanları hata vermeden yok sayacak şekilde yazın.
Sorun giderme
Bağlanıyorum ama hiç olay gelmiyor
Sırayla kontrol edin:
subscribedyanıtını aldınız mı? Almadıysanızsubscribemesajınız gitmemiştir.- Ayarlarda Çağrı sürerken seçili mi? Çağrı bittikten sonra seçiliyse akış yoktur.
- Hangi çağrılar altında test ettiğiniz yön seçili mi?
- Jeton bakiyeniz var mı? Bakiye bittiğinde yeni transkript üretilmez.
- Çağrı gerçekten bağlandı mı? Transkript oturumu çağrı karşılandığında değil, taraflar birbirine bağlandığında başlar. Karşılama anonsu, IVR ve kuyrukta beklerken duyulan müzik yazıya dökülmez.
403 alıyorum
Hesap canlı akışa yetkili değil. Canlı transkript akışı anahtarı açık mı, mod Çağrı sürerken mi, transkript ana anahtarı açık mı? Üçü birden gerekir. Anahtar kilit simgesiyle görünüyorsa planınızda API erişimi yok demektir.
401 alıyorum, token'ın doğru olduğundan eminim
Yanıt gövdesindeki sebebe bakın. Token silinmiş, süresi dolmuş ya da başka bir hesaba ait olabilir. Token'ı panelde yeniden oluşturup deneyin.
Metin "CCumCumartesi" gibi birikiyor
Parçaları birbirine ekliyorsunuz. segment_id'ye göre üzerine yazın; bkz. Transkripti yeniden kurma.
Aynı cümleyi iki kez işliyorum
seq'i takip edip geriye giden olayları yok sayın ve tamamlanmış çağrılara ait geç olayları eleyin.
Bir konuşma sırasının metni kısaldı
Beklenen davranış. Kuyruk bir sonraki konuşmacıya aitmiş; bir sonraki segment_id altında gelecek.
Sık sorulan sorular
Akışı açmak Hipcall ekranlarında bir şeyi değiştirir mi?
Hayır. Transkript zaten çağrı kaydının yanında görünür. Bu anahtar yalnızca metni kendi sisteminize taşımak içindir.
Ayrı bir paket almam gerekir mi?
Hayır. Transkript dakika başına jetonla ücretlendirilir. Akışın kendisi için ek bir ücret yoktur.
Tarayıcıdan bağlanabilir miyim?
Hayır. Kimlik bilgisi Authorization header'ı ile gider ve tarayıcı WebSocket API'si özel header göndermeye izin vermez. Ayrıca API token'ını tarayıcıya koymak istemezsiniz. Sunucunuzdan bağlanın, gerekiyorsa kendi istemcilerinize oradan dağıtın.
Aynı anda kaç bağlantı açabilirim?
Aynı hesap için birden çok bağlantı açabilirsiniz; hepsi aynı çağrıları alır. Ama her bağlantı tek bir aboneliğe sahiptir.
Çağrı sırasında metni bir yapay zekâ modeline verebilir miyim?
Evet, ama yalnızca is_final: true olan parçaları kullanın. Donmamış parçalar hâlâ değişebilir.
Geçmiş bir çağrının transkriptini bu akıştan alabilir miyim?
Hayır. Akış yalnızca canlıdır, geçmişi oynatmaz. Geçmiş transkriptler Hipcall panelinde çağrı kaydının yanındadır.
Konuşmacıları ayırt edebilir miyim?
speaker alanı doluysa evet, ama bu bir kimlik değil çağrıya özel bir etikettir. "1" bir çağrıda müşteri, başka bir çağrıda temsilci olabilir.