https://api.echonova.aiالمطورون
بث الكلام أثناء توليده
أبقِ WebSocket مفتوحًا، وأرسل النص، وشغّل صوت PCM وهو ما زال قيد التوليد، مع الردود جملةً جملة، وقطع الكلام بالأمر cancel عند المقاطعة، والفوترة والحدود وعميل Python كامل.
يرسل بث تحويل النص إلى كلام الصوتَ وهو ما زال قيد التوليد، فيسمع المستمع الكلمات الأولى قبل أن يكتمل الرد كله بوقت طويل. يبقى مقبس واحد مفتوحًا طوال المحادثة، ويستقبل النصوص واحدًا تلو الآخر. تشرح هذه الصفحة كيف تتصل، وتشغّل الصوت فور وصوله، وتغذّي الرد جملةً جملة، وتتوقف عن الكلام حين يقاطعك المستمع. كل رسالة وكل حقل موثّقان في بث تحويل النص إلى كلام.
متى تستخدم البث
POST /v1/text-to-speech/sync |
البث | |
|---|---|---|
| ما تحصل عليه | رابط إلى ملف WAV مكتمل | إطارات PCM خام أحادية القناة بعمق 16 بت أثناء توليدها |
| أول صوت | حين يُنطق النص كله | بعد توليد الثواني القليلة الأولى من الكلام |
| النصوص في كل طلب | نص واحد | أي عدد، واحدًا تلو الآخر، على مقبس واحد |
| الإيقاف في المنتصف | غير ممكن | cancel، عند حدّ الدفعة التالية |
| السعر | 1 credit لكل حرف | 1 credit لكل حرف |
استخدم البث حين ينتظر أحدٌ سماع الرد: وكيل صوتي، أو مكالمة هاتفية، أو مساعد مباشر. أما الصوت الذي تخزّنه أو تقدّمه لاحقًا، فتعطيك نقطة النهاية المتزامنة ملفًا ومهمة.
كيف تعمل الجلسة
you server
|-- upgrade: Authorization --------------->|
|-- {"type":"synthesize", ...} ----------->|
|<-------------------------------- start |
|<---------------- binary PCM frames ... |
|<-------------------------------- done |
|-- {"type":"synthesize", ...} ----------->| the next text, same socket
|<-------------------------------- start |
|<---------------- binary PCM frames ... |
|-- {"type":"cancel"} -------------------->| the listener interrupts
|<---------------------------- cancelled |ما تحتاج إليه
- مفتاح API تشمل خدماته تحويل النص إلى كلام، أو لا يقيّد الخدمات أصلًا. انظر احصل على مفتاح API.
- قيمة
voiceIdمن عرض الأصوات المتاحة. - خادم. لا تستطيع المتصفحات ضبط header
Authorizationعلى WebSocket، فلا يمكن لصفحة ويب أن تتصل. ابثّ على خادمك ومرّر الصوت إلى المشغّل لديك؛ ولا تضع المفتاح في صفحة أبدًا. - رصيد على مساحة العمل. انظر الرصيد.
- لعميل Python أدناه: Python 3.9 أو أحدث، و
pip install "websockets>=14".
الخطوة 1: اتصل
افتح WebSocket إلى wss://api.echonova.ai/v1/text-to-speech/stream مع Authorization: Bearer YOUR_API_KEY على طلب الترقية (upgrade). لا يأخذ البث أي معاملات استعلام. وإن كان هناك خطأ قبل الترقية، يردّ الخادم بـ HTTP عادي والغلاف المعتاد للأخطاء، ولا يُخصم شيء؛ انظر الأخطاء.
افتح المقبس قبل أن تحتاج إليه، عند بدء المكالمة مثلًا، وأبقه مفتوحًا طوال المحادثة. إعادة استخدامه توفّر وقت الاتصال في كل رد، ولا يُحتسب إلا بثًّا واحدًا من بثوثك المتزامنة.
الخطوة 2: أرسل رسالة synthesize
{ "type": "synthesize", "requestId": "reply-1", "text": "أهلاً بك، كيف يمكنني مساعدتك اليوم؟", "voiceId": "66e5ef3a9c1d2b4f8a7e6c01" }| الحقل | مطلوب | القيمة |
|---|---|---|
type |
نعم | synthesize |
text |
نعم | من 1 إلى 500 حرف. |
voiceId |
نعم | صوت من قائمة الأصوات. الصوت هو الذي يحدد اللهجة. |
requestId |
لا | معرّفك لهذا النص، حتى 64 حرفًا، ويُعاد في start وdone وcancelled وerror. |
speed |
لا | من 0.5 إلى 2، والافتراضي 1. |
cfg |
لا | من 1 إلى 5، والافتراضي 2. |
numSteps |
لا | 16 أو 32 أو 48 أو 64 أو 128، والافتراضي 48. |
tashkeel |
لا | يضيف التشكيل إلى النص العربي قبل قراءته. لأصوات الفصحى فقط. الافتراضي false. |
normalize |
لا | يقرأ الأعداد والتواريخ والمبالغ المالية ونحوها كما ينطقها متحدث تلك اللهجة. الافتراضي true. |
للحقول المعنى نفسه الذي لها في توليد الكلام بشكل متزامن؛ انظر الأصوات والخيارات في تحويل النص إلى كلام.
لا يُنطق على المقبس إلا نص واحد في كل مرة. ورسالة synthesize تُرسل بينما أخرى ما زالت قيد التشغيل تلغي الجارية أولًا، تمامًا كأنك أرسلت cancel ثم synthesize.
الخطوة 3: شغّل الصوت فور وصوله
يردّ الخادم بـ start، ثم إطارات ثنائية، ثم done:
{ "type": "start", "requestId": "reply-1", "sampleRate": 24000, "encoding": "pcm_s16le", "channels": 1 }كل إطار ثنائي هو PCM أحادي القناة بعمق 16 بت، بإشارة، وبترتيب little-endian، بمعدل العيّنات sampleRate المذكور في start. وهو 24 kHz حاليًا، ولا تقبل synthesize معدلًا خاصًا بها، لكن اقرأه من start بدلًا من تثبيته في الكود. شغّل الإطارات بترتيب وصولها، أو ألحقها بملف. يُولَّد الصوت بضع ثوانٍ من الكلام في كل مرة، فتصل الإطارات على دفعات: أعطِ المشغّل مخزنًا مؤقتًا صغيرًا، بضع مئات من الميلي ثانية، حتى لا ينفد ما لديه بين الدفعات.
{ "type": "done", "requestId": "reply-1", "durationSeconds": 2.9, "characters": 36, "creditsConsumed": 36 }تعني done أن كل إطارات ذلك النص قد أُرسلت. لا تحمل الإطارات الثنائية requestId؛ فالإطارات الواقعة بين start وdone أو cancelled التابعة له تنتمي إلى ذلك الطلب.
مع الحزمة sounddevice مثلًا، يكون التشغيل تدفقًا تكتب فيه الإطارات:
import sounddevice # pip install sounddevice
player = sounddevice.RawOutputStream(samplerate=24000, channels=1, dtype="int16")
player.start()
# for every binary frame received:
player.write(frame)الخطوة 4: انطق الرد جملةً جملة
حين يأتي النص من نموذج لغوي، لا تنتظر الرد كله. قسّمه إلى جمل أثناء توليده، وأرسل كل جملة في synthesize خاصة بها، فيسمع المستمع الجملة الأولى والنموذج ما زال يكتب البقية.
لا ترسل الجملة التالية إلا بعد done الخاصة بالجملة السابقة. فرسالة synthesize المرسلة قبل ذلك تلغي الجملة التي ما زالت تُنطق. احتفظ بطابور (queue) من جهتك:
- حين تكتمل جملة (تنتهي بـ
.أو?أو!أو؟أو بفاصل سطر)، أضفها إلى الطابور. - حين يكون المقبس خاملًا، أرسل أول جملة في الطابور.
- عند
done، أرسل الجملة التالية.
الموصى به
- رسالة
synthesizeواحدة لكل جملة أو فقرة قصيرة، كلٌّ منها أقل من 500 حرف، تُرسل واحدة تلو الأخرى على مقبس واحد.
الأقل فاعلية
- رسالة
synthesizeلكل بضع كلمات. تُنطق كل قطعة وحدها، فيفقد الصوت إيقاع الجملة. - مقبس جديد لكل جملة. كل مقبس يضيف اتصالًا، ويُحتسب ضمن حد البثوث المتزامنة.
الخطوة 5: توقف عن الكلام حين يقاطعك المستمع
حين يبدأ المستمع الكلام فوق الرد، أرسل:
{ "type": "cancel" }يوقف الخادم التوليد ويردّ بـ {"type":"cancelled","requestId":"reply-1"}. لا يصل بعد cancelled أي إطار لذلك الطلب.
يسري الإلغاء عند حدّ الدفعة التالية: فقد تصل بضعة إطارات وُلّدت بالفعل بين cancel الذي أرسلته وcancelled. أوقف المشغّل وأفرغ مخزنه المؤقت فورًا من جهتك، وتجاهل كل إطار حتى تصل cancelled. ثم أرسل synthesize التالية، أو أرسلها فورًا: فرسالة synthesize الجديدة تلغي الجارية أيضًا.
يُحتسب التوليد الملغى كاملًا. يُخصم الرصيد حين يبدأ التوليد، عن كل حرف من text، أيًّا كان الجزء الذي نُطق منه. والرسائل بحجم الجملة تُبقي ما تهدره المقاطعة قليلًا.
عميل Python كامل
يرسل هذا العميل نصًا واحدًا، ويجمع إطارات PCM، ويكتبها في speech.wav. العميل نفسه بلغات JavaScript وTypeScript وGo وC# موجود في بث تحويل النص إلى كلام.
import asyncio
import json
import wave
import websockets # pip install "websockets>=14"
URL = "wss://api.echonova.ai/v1/text-to-speech/stream"
API_KEY = "YOUR_API_KEY"
async def main():
pcm = bytearray()
rate = 24000
async with websockets.connect(
URL, additional_headers={"Authorization": f"Bearer {API_KEY}"}
) as ws:
await ws.send(json.dumps({
"type": "synthesize",
"requestId": "reply-1",
"text": "أهلاً بك، كيف يمكنني مساعدتك اليوم؟",
"voiceId": "66e5ef3a9c1d2b4f8a7e6c01",
}))
while True:
message = await ws.recv()
if isinstance(message, bytes): # a binary frame of PCM16
pcm.extend(message)
continue
event = json.loads(message)
if event["type"] == "start":
rate = event["sampleRate"]
elif event["type"] == "done":
print("seconds:", event["durationSeconds"], "credits:", event["creditsConsumed"])
break
elif event["type"] in ("error", "cancelled"):
raise SystemExit(f"{event['type']}: {event.get('message', '')}")
with wave.open("speech.wav", "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(rate)
wav.writeframes(bytes(pcm))
asyncio.run(main())الأخطاء على مقبس مفتوح
تحمل رسالة error الحقلين code وmessage، وكذلك requestId حين يتعلق الخطأ بنص واحد:
code |
ما حدث | ما تفعله |
|---|---|---|
validation_failed |
حقل في synthesize مفقود أو خارج النطاق. |
أصلح الرسالة وأرسلها من جديد. يبقى المقبس مفتوحًا. |
voice_not_found |
لا يوجد صوت بهذا voiceId. |
خذ معرّفًا من قائمة الأصوات. |
invalid_message |
إطار نصي ليس JSON صالحًا، أو يحمل type غير معروف. |
يُغلق المقبس بالرمز 1008. أصلح الرسالة وأعد الاتصال. |
frame_too_large |
إطار أكبر من 64 KiB. | يُغلق المقبس بالرمز 1008، أو بالرمز 1009 دون رسالة إذا تجاوز 1 MiB. أرسل نصًا أقصر. |
insufficient_balance |
نفد رصيد مساحة العمل. | يُغلق المقبس بالرمز 4002. أضف رصيدًا. |
spending_limit |
بلغ المفتاح حد الرصيد الخاص به. | يُغلق المقبس بالرمز 4029. ارفع الحد. |
upstream_error |
فشل توليد الكلام. | أرسل النص من جديد. يبقى المقبس مفتوحًا. |
كل رموز الإغلاق مذكورة في الأخطاء.
الفوترة
تكلّف كل synthesize رصيدًا واحدًا (1 credit) لكل حرف من text، بما في ذلك المسافات، وهو السعر نفسه في نقاط نهاية تحويل النص إلى كلام الأخرى. يُخصم الرصيد حين يبدأ التوليد، قبل أول إطار، وتكرر done المقدار في creditsConsumed. والتوليد الملغى، سواء بـ cancel أو بـ synthesize أحدث، يُحتسب مع ذلك. وإن فشل التوليد قبل إرسال أي صوت، يُسترد الرصيد. ينطبق حد الرصيد الخاص بالمفتاح على كل synthesize. انظر الرصيد.
الحدود
| الحد | القيمة |
|---|---|
| طول النص | 500 حرف لكل synthesize |
| عمليات التوليد المتزامنة | 1 لكل مقبس؛ ورسالة synthesize جديدة تلغي الجارية |
| طول الجلسة | 30 دقيقة |
| مهلة الخمول | 120 ثانية دون أي إطار؛ أرسل ping لإبقاء مقبس هادئ مفتوحًا |
| البثوث المتزامنة | 10 بثوث مفتوحة لكل مساحة عمل، لتحويل الكلام إلى نص وتحويل النص إلى كلام معًا |
| معدل الاتصال | 100 محاولة اتصال في الدقيقة لكل مفتاح |
انظر حدود معدل الطلبات لمعرفة كيف تُحتسب حدود البث.
حل المشكلات
قطعت جملتي الثانية الجملة الأولى
رسالة synthesize المرسلة قبل done الخاصة بالسابقة تلغيها. ضع الجمل في طابور وأرسل كلًّا منها عند done السابقة، كما في الخطوة 4.
تستمر الإطارات في الوصول بعد إرسال cancel
يتوقف التوليد عند حدّ الدفعة التالية، فقد تتبع cancel بضعة إطارات. تجاهل كل إطار حتى تصل cancelled.
يُشغَّل الصوت سريعًا جدًا أو بطيئًا جدًا أو كضجيج
شغّله بمعدل العيّنات sampleRate المذكور في start، كعيّنات أحادية القناة بعمق 16 بت، بإشارة، وبترتيب little-endian. المشغّل المضبوط على معدل آخر، أو على استريو، أو على 8 بت، يسيء قراءة الإطارات.
يُغلق المقبس بعد دقيقتين من الصمت
مهلة الخمول 120 ثانية دون أي إطار. أرسل {"type":"ping"} كل دقيقة تقريبًا ما دامت المحادثة هادئة؛ ويردّ الخادم بـ pong.
الأسئلة الشائعة
هل يمكنني الحصول على MP3 أو Opus بدلًا من PCM؟
لا. يرسل البث PCM خامًا أحادي القناة بعمق 16 بت. رمّزه بنفسك إن احتجت إلى صيغة أخرى، أو استخدم توليد الكلام بشكل متزامن للحصول على ملف WAV.
هل يمكن لمقبس واحد أن يستخدم عدة أصوات؟
نعم. voiceId جزء من كل synthesize، فيمكن لكل نص أن يستخدم صوتًا مختلفًا، نصًا واحدًا في كل مرة.
هل يمكنني الاتصال من متصفح؟
لا. لا يستطيع المتصفح ضبط header Authorization على WebSocket، ويجب ألا يصل المفتاح إلى صفحة ويب أبدًا. ابثّ على خادمك ومرّر الصوت.