صدا، زنده و دوطرفه
این سرویس همان Realtime API است، نه زنجیرهٔ STT → LLM → TTS. رلهٔ ما دو سوکت را نگه میدارد و فریمها را دستنخورده رد میکند؛ کلاینتِ تو مستقیماً با پروتکلِ Realtime حرف میزند. به همین دلیل تشخیصِ پایانِ صحبت، قطعکردنِ حرفِ مدل، رونوشتِ زنده و function calling رفتارِ اصلیِ مدلاند و ما چیزی را بازسازی نکردهایم.
صدا از سرورِ لندنِ ما عبور میکند چون مسیرِ مستقیمِ مرورگر به api.openai.com از ایران باز نیست — و همین دلیلِ نبودِ WebRTC است.
کلید روی سرور، سکرت روی کلاینت
سوکت عمداً کلیدِ API را نمیپذیرد. سرورِ تو با کلیدش یک کلاینتسکرتِ کوتاهعمر میگیرد و آن را به فرانتاندِ خودش میدهد؛ آنچه در مرورگر قرار میگیرد فقط اجازهٔ بازکردنِ یک تماس است و دو دقیقه بعد میمیرد. گمشدنِ یک سکرت یعنی یک پنجرهٔ کوتاه روی یک حساب؛ گمشدنِ کلید یعنی کیفِ پول.
curl -X POST https://1xai.ir/v1/realtime/client_secret \
-H "Authorization: Bearer $ONEXAI_API_KEY"
# {
# "client_secret": { "value": "ek_...", "expires_at": 1756240000 },
# "model": "gpt-realtime",
# "url": "wss://1xai.ir/rt/voice"
# }# Your backend. The API key lives here and only here.
import httpx
from fastapi import FastAPI
app = FastAPI()
@app.post("/voice-token")
async def voice_token():
async with httpx.AsyncClient() as http:
r = await http.post(
"https://1xai.ir/v1/realtime/client_secret",
headers={"Authorization": f"Bearer {ONEXAI_API_KEY}"},
)
r.raise_for_status()
return r.json() # hand straight to your front endبازکردنِ تماس
مرورگر نمیتواند روی دستدادنِ WebSocket هدر بگذارد، پس سکرت در کوئریاسترینگ میرود؛ کلاینتِ سمتِ سرور هدر دارد و باید از هدر استفاده کند.
// Your page. Never sees the API key.
const { client_secret, url } = await fetch("/voice-token", { method: "POST" })
.then((r) => r.json());
const ws = new WebSocket(`${url}?client_secret=${client_secret.value}`);
ws.onopen = () => {
// Configure the session: voice, Persian transcription, turn detection.
ws.send(JSON.stringify({
type: "session.update",
session: {
instructions: "به فارسی و کوتاه پاسخ بده.",
audio: {
input: {
format: "pcm16",
transcription: { model: "whisper-1", language: "fa" },
// Server-side VAD: the model decides when you stopped talking,
// and interrupting it cancels its reply (barge-in).
turn_detection: { type: "server_vad", interrupt_response: true },
},
output: { format: "pcm16", voice: "alloy" },
},
tools: [{
type: "function",
name: "lookup_order",
description: "Look up an order in the ERP",
parameters: {
type: "object",
properties: { id: { type: "string" } },
required: ["id"],
},
}],
},
}));
};
ws.onmessage = (e) => {
const ev = JSON.parse(e.data);
switch (ev.type) {
case "conversation.item.input_audio_transcription.completed":
console.log("user said:", ev.transcript); // live transcript
break;
case "response.output_audio.delta":
playPCM(atob(ev.delta)); // model speaking
break;
case "response.function_call_arguments.done":
callYourERP(ev.name, JSON.parse(ev.arguments)); // function calling
break;
case "error":
// insufficient_funds arrives here before the socket closes
console.error(ev.error);
break;
}
};
// Microphone in: base64 PCM16 frames, as they are captured.
function sendMic(pcm16) {
ws.send(JSON.stringify({ type: "input_audio_buffer.append", audio: pcm16 }));
}// Server-side client (no browser). Header instead of query string —
// query strings reach access logs, headers do not.
import WebSocket from "ws";
const ws = new WebSocket("wss://1xai.ir/rt/voice", {
headers: { Authorization: `Bearer ${clientSecret}` },
});محدودیتها و هزینه
- مدل
- gpt-realtime — سمتِ سرور ثابت است و کلاینت نمیتواند عوضش کند. صدا گرانترین چیزی است که میفروشیم؛ انتخابِ مدلِ گرانتر دستِ تماسگیرنده نیست.
- ترابری
- WebSocket — بدون WebRTC.
- سقفِ زمانِ تماس
- سی دقیقه؛ پس از آن سوکت بسته میشود.
- عمرِ کلاینتسکرت
- دو دقیقه از لحظهٔ صدور — فقط برای بازکردنِ سوکت، نه برای طولِ تماس.
- حداقلِ اعتبار
- ۲٬۰۰۰ تومان برای شروعِ تماس. تماس در حینِ اجرا تسویه میشود و با تمامشدنِ اعتبار با رویدادِ insufficient_funds بسته میشود.
- هزینه
- توکنِ صوتی جدا از متن و با نرخِ صوتیِ ارائهدهنده بهعلاوهٔ ۲۰٪ حساب میشود و چند برابرِ متن است. تسویه بعد از هر پاسخِ مدل انجام میشود، نه در پایانِ تماس. نرخِ لحظهای در صفحهٔ قیمتها است.
- هر کاربر، یک نشست
- هر سوکت نشستِ مستقلِ خودش را دارد؛ چیزی بین تماسها مشترک نیست.
- این سرویس Realtime واقعی است یا زنجیرهٔ STT → LLM → TTS؟
- واقعی است. رلهٔ ما فریمها را دستنخورده بین اپ تو و Realtime API رد میکند؛ هیچ تبدیل متنبهگفتاری در میانه نیست. یعنی VAD، قطعکردنِ صحبتِ مدل، رونوشتِ زنده و function calling همانهایی هستند که خودِ مدل ارائه میدهد، نه شبیهسازیِ ما.
- چرا باید کلاینتسکرت بگیرم و کلید API را مستقیم روی سوکت نفرستم؟
- چون کلیدِ API از کیفِ پولِ تو خرج میکند و اگر در جاوااسکریپتِ سمتِ کاربر قرار بگیرد، در devtools هر بازدیدکننده هم هست. سوکت عمداً کلیدِ خام را نمیپذیرد و فقط مقدارِ ek_ را قبول میکند؛ عمرِ آن دو دقیقه است و تنها یک کار میکند: بازکردنِ یک تماس.
- WebRTC هم پشتیبانی میشود؟
- خیر — فقط WebSocket. WebRTC اتصالِ مستقیمِ مرورگر به سرورِ ارائهدهنده است و دقیقاً همان چیزی است که از داخلِ ایران کار نمیکند؛ صدا باید از سرورِ ما عبور کند.
- حداکثر زمان هر تماس چقدر است؟
- سی دقیقه. تماسی که از این بیشتر طول بکشد در عمل یک تبِ فراموششده است، نه گفتوگو — و بدون سقف تا خالیشدنِ کیفِ پول هزینه میتراشد.
- هزینهٔ صدا چطور حساب میشود؟
- توکنهای صوتی جدا از متن و با نرخِ صوتیِ خودِ ارائهدهنده بهعلاوهٔ ۲۰٪ حساب میشوند؛ صدا چند برابرِ متن است. هزینه بعد از هر پاسخِ مدل تسویه میشود، نه در پایانِ تماس، و نرخِ لحظهای در صفحهٔ قیمتها هست.
سؤالی ماند؟ بنویس یا مرجعِ کاملِ رویدادها را در مستندات ببین.