https://api.echonova.aiالمطورون
تفريغ الصوت المباشر
ابثّ الصوت المباشر عبر WebSocket واحصل على نص مفرّغ نهائي لكل مقطع، مع صيغة الصوت وحجم الإطار وتحديد نهاية المقاطع والفوترة والحدود وعميل Python كامل.
يستقبل بث تحويل الكلام إلى نص الصوتَ في أثناء نطقه، من ميكروفون أو مكالمة هاتفية أو بث مباشر، ويعيد النص المفرّغ لكل جزء من الكلام فور انتهاء ذلك الجزء. تشرح هذه الصفحة كيف تجهّز الصوت، وتتصل، وترسله، وتحدد أين ينتهي كل جزء، وتقرأ النتائج. كل رسالة وكل حقل موثّقان في بث تحويل الكلام إلى نص.
كل نص مفرّغ نهائي. لا يرسل البث أي نتائج مؤقتة أو جزئية ما دام المتحدث يتكلم: يصل نص المقطع مرة واحدة، بعد انتهاء المقطع. صمّم واجهتك حول عبارات كاملة، لا حول ترجمة تظهر كلمةً كلمة.
متى تستخدم البث
POST /v1/speech-to-text/sync |
POST /v1/speech-to-text |
البث | |
|---|---|---|---|
| المدخل | ملف مكتمل، حتى 10 دقائق | ملف مكتمل، حتى 10 دقائق | صوت مباشر، حتى 25 دقيقة لكل جلسة |
| النتيجة | نص مفرّغ واحد للملف | نص مفرّغ واحد للملف | نص مفرّغ لكل مقطع، والصوت ما زال يتدفق |
| صيغة الصوت | أي نوع ملف مدعوم | أي نوع ملف مدعوم | PCM خام أحادي القناة بعمق 16 بت فقط |
| السعر | 100 credits في الدقيقة | 100 credits في الدقيقة | 200 credits في الدقيقة، سعر تحويل الكلام إلى نص مباشرةً |
استخدم البث حين لا يكون الصوت ملفًا بعد، وتحتاج إلى الكلمات والمحادثة ما زالت جارية: وكيل صوتي يجب أن يردّ على المتصل، أو ترجمة نصية لحدث مباشر. أما التسجيل الموجود على القرص بالفعل فارفعه: كلفته النصف.
كيف تعمل الجلسة
you server
|-- upgrade: auth, ?sample_rate, ?language >|
|<------------------------- session.started |
|-- binary PCM frames (100 ms each) ------->|
|-- {"type":"end_segment"} ---------------->|
|<------------------------------ transcript |
|-- more frames, more end_segment ... ----->|
|-- {"type":"stop"} ----------------------->|
|<----------------- last transcripts, then |
|<--------------------------- session.ended |
|<------------------------------ close 1000 |ما تحتاج إليه
- مفتاح API تشمل خدماته تحويل الكلام إلى نص، أو لا يقيّد الخدمات أصلًا. انظر احصل على مفتاح API.
- خادم. لا تستطيع المتصفحات ضبط header
Authorizationعلى WebSocket، فلا يمكن لصفحة ويب أن تتصل. إن كان الصوت يأتي من متصفح، فأرسله إلى خادمك وافتح البث من هناك؛ ولا تضع المفتاح في الصفحة أبدًا. - رصيد على مساحة العمل. انظر الرصيد.
- لعميل Python أدناه: Python 3.9 أو أحدث، و
pip install "websockets>=14".
الخطوة 1: جهّز الصوت
يقبل البث صيغة واحدة: PCM بعمق 16 بت، بإشارة، وبترتيب little-endian، وبقناة واحدة (pcm_s16le)، بلا رأس ملف. اختر معدل عيّنات بين 8,000 و48,000 Hz وأبلغ الخادم به عبر sample_rate عند الاتصال. يكفي 16,000 Hz للكلام ويُبقي الإطارات صغيرة؛ ويناسب 8,000 Hz صوت الهاتف الذي لم يكن أعرض من ذلك قط.
حوّل الصوت الاستريو إلى أحادي، والصوت المضغوط إلى PCM، قبل إرساله. لتحويل ملف إلى ملف WAV تستطيع الأمثلة قراءته:
ffmpeg -i input.mp3 -ac 1 -ar 16000 -c:a pcm_s16le audio.wavملف WAV من هذا النوع رأسٌ قصير تليه هذه العيّنات بالضبط، ولهذا تقرأ الأمثلة المعدل من الرأس وترسل الباقي. أما المصدر المباشر كالميكروفون فيعطيك العيّنات مباشرة: التقط الصوت بالمعدل الذي تتصل به، كأعداد صحيحة بعمق 16 بت، وبقناة واحدة.
الخطوة 2: اتصل
افتح WebSocket إلى wss://api.echonova.ai/v1/speech-to-text/stream مع مفتاحك على طلب الترقية (upgrade):
| الجزء | القيمة |
|---|---|
Authorization |
Header. Bearer YOUR_API_KEY. |
sample_rate |
معامل استعلام، مطلوب. معدل عيّنات الصوت الذي ترسله، من 8000 إلى 48000. |
language |
معامل استعلام مطلوب. ar أو en. |
encoding |
معامل استعلام. pcm_s16le، وهي القيمة الافتراضية والوحيدة. |
حين يكون المفتاح والمعاملات والرصيد سليمة، يقبل الخادم الترقية ويرسل session.started. انتظرها قبل أن ترسل الصوت:
{ "type": "session.started", "sessionId": "c8a1f2e4-5b6d-4e7f-9a0b-1c2d3e4f5a6b", "language": "ar", "sampleRate": 16000, "maxSessionSeconds": 1500 }احتفظ بـ sessionId في سجلاتك: فهو يعرّف الجلسة إن تواصلت مع الدعم. وإن كان هناك خطأ قبل الترقية، يردّ الخادم بـ HTTP عادي والغلاف المعتاد للأخطاء بدلًا من ذلك، ولا يُخصم شيء. الرموز موجودة في الأخطاء.
الخطوة 3: أرسل إطارات الصوت
أرسل PCM في إطارات WebSocket ثنائية، بالترتيب. يعمل نحو 100 ميلي ثانية من الصوت في كل إطار بشكل جيد:
| معدل العيّنات | العيّنات في 100 ميلي ثانية | البايتات في إطار 100 ميلي ثانية |
|---|---|---|
| 8,000 Hz | 800 | 1,600 |
| 16,000 Hz | 1,600 | 3,200 |
| 24,000 Hz | 2,400 | 4,800 |
| 48,000 Hz | 4,800 | 9,600 |
لا يجوز أن يتجاوز الإطار 64 KiB. فالإطار الأكبر يتلقى الخطأ frame_too_large ويغلق الجلسة بالرمز 1008، والإطار الذي يتجاوز 1 MiB يغلقها بالرمز 1009 دون رسالة خطأ. اجعل حجم كل إطار عددًا زوجيًا من البايتات، حتى لا تنقسم عيّنة بين إطارين.
أرسل الصوت المباشر فور التقاطه. وحين تبث ملفًا، كما تفعل الأمثلة، توقف نحو 100 ميلي ثانية بين الإطارات حتى يراه الخادم كما يرى مصدرًا مباشرًا.
الخطوة 4: أنهِ كل مقطع
يفرّغ الخادم الصوت على مقاطع. ينتهي المقطع حين ترسل {"type":"end_segment"}، أو بعد 15 ثانية من الصوت إن لم ترسل شيئًا، ويصل نصه المفرّغ بعد ذلك بقليل. ولأنه لا توجد نتائج جزئية، فإن موضع انتهاء المقاطع يحدد متى ترى النص ومدى جودته.
الموصى به
- اكتشف فترات التوقف من جهتك، بكاشف نشاط صوتي (VAD) أو بعتبة طاقة بسيطة، وأرسل
end_segmentحين يصمت المتحدث مدة 300 إلى 700 ميلي ثانية. عندها يكون كل نص مفرّغ جملة كاملة أو دورًا كاملًا في الحديث. - في الوكيل الصوتي، أرسل
end_segmentفور أن يقرر اكتشاف الأدوار لديك أن المتصل أنهى كلامه، وأجب انطلاقًا من ذلك النص المفرّغ.
الأقل فاعلية
- عدم إرسال
end_segmentأبدًا. عندها يُقطع الكلام كل 15 ثانية، وغالبًا في منتصف كلمة، وتنتظر الكلمات الأولى حتى 15 ثانية. - إرسال
end_segmentبمؤقت ثابت، كل ثانية مثلًا. تفقد المقاطع القصيرة السياق الذي يجعل النص المفرّغ دقيقًا، ويُحتسب كل مقطع على حدة ويُقرَّب إلى رصيد كامل.
الخطوة 5: اقرأ النصوص المفرّغة
يعود كل مقطع في رسالة transcript واحدة:
{ "type": "transcript", "segmentId": "6720f3a1c4b5d6e7f8a9b0c1", "text": "مرحبا، كيف يمكنني مساعدتك اليوم؟", "language": "ar", "durationSeconds": 2.4, "creditsConsumed": 8, "isFinal": true }| الحقل | ما يحتويه |
|---|---|
segmentId |
يعرّف المقطع. استخدمه لمطابقة النصوص المفرّغة مع سجلاتك. |
text |
النص المفرّغ. يكون فارغًا إن لم يحتوِ المقطع على كلام. |
language |
اللغة المكتشفة، ar أو en. |
durationSeconds |
طول صوت المقطع، بالثواني. |
creditsConsumed |
الرصيد المخصوم لهذا المقطع. |
isFinal |
دائمًا true. لا تغيّر أي رسالة لاحقة هذا النص. |
قد يصل النص المفرّغ بعد لحظة من إرسالك end_segment، وأنت ما زلت ترسل صوت المقطع التالي. استمر في القراءة والإرسال في الوقت نفسه، كما تفعل الأمثلة بمهمة لكلٍّ منهما.
الخطوة 6: أوقف الجلسة
أرسل {"type":"stop"} حين ينتهي الصوت. يفرّغ الخادم ما تبقى، ويرسل آخر رسائل transcript، ثم session.ended، ثم يغلق المقبس بالرمز 1000:
{ "type": "session.ended", "reason": "stopped", "totalSeconds": 42.6, "creditsConsumed": 142 }totalSeconds وcreditsConsumed هما إجماليا الجلسة كلها. وإغلاق المقبس بنفسك ينهي الجلسة أيضًا، لكن الصوت الذي لم يُفرَّغ بعد يُهمل حينها.
عميل Python كامل
يبث هذا العميل audio.wav على إطارات من 100 ميلي ثانية، وينهي المقطع، ويطبع كل نص مفرّغ، ثم يوقف الجلسة. العميل نفسه بلغات JavaScript وTypeScript وGo وC# موجود في بث تحويل الكلام إلى نص.
import asyncio
import json
import wave
import websockets # pip install "websockets>=14"
URL = "wss://api.echonova.ai/v1/speech-to-text/stream"
API_KEY = "YOUR_API_KEY"
async def main():
# A 16-bit mono PCM WAV file; its header gives the sample rate.
with wave.open("audio.wav", "rb") as wav:
if wav.getsampwidth() != 2 or wav.getnchannels() != 1:
raise SystemExit("audio.wav must be 16-bit mono PCM")
rate = wav.getframerate()
pcm = wav.readframes(wav.getnframes())
async with websockets.connect(
f"{URL}?sample_rate={rate}&language=ar",
additional_headers={"Authorization": f"Bearer {API_KEY}"},
) as ws:
print(json.loads(await ws.recv())) # session.started
async def send_audio():
step = rate // 10 * 2 # 100 ms of 16-bit samples, in bytes
for i in range(0, len(pcm), step):
await ws.send(pcm[i : i + step]) # bytes: a binary frame
await asyncio.sleep(0.1) # real time, like a live source
await ws.send(json.dumps({"type": "end_segment"}))
await ws.send(json.dumps({"type": "stop"}))
sender = asyncio.create_task(send_audio())
async for raw in ws: # ends when the server closes after session.ended
message = json.loads(raw)
if message["type"] == "transcript":
print(message["text"])
elif message["type"] == "error":
print("error:", message["code"], message["message"])
elif message["type"] == "session.ended":
print("ended:", message["reason"], "credits:", message["creditsConsumed"])
await sender
asyncio.run(main())أعد الاتصال بعد الفشل
قد تنتهي الجلسة قبل أن ترسل stop. تذكر session.ended السبب، ويبيّن رمز الإغلاق ما عليك فعله:
| رمز الإغلاق | السبب | ما تفعله |
|---|---|---|
1000 |
stopped |
لا شيء: أرسلت stop أو أغلقت المقبس. |
1001 |
لا يوجد | يُعاد تشغيل الخادم. أعد الاتصال وتابع. |
1008 |
لا يوجد | أصلح الإطار: JSON غير صالح، أو type غير معروف، أو حجم يتجاوز 64 KiB. |
1009 |
لا يوجد | أرسل إطارات لا تتجاوز 64 KiB؛ فقد تجاوز هذا الإطار 1 MiB. |
1011 |
upstream_error |
أعد الاتصال بعد انتظار قصير وتابع. |
4002 |
insufficient_balance |
أضف رصيدًا قبل أن تعيد الاتصال. |
4008 |
idle_timeout أو max_duration |
أعد الاتصال. أرسل صوتًا أو ping مرة كل 120 ثانية على الأقل، وابدأ جلسة جديدة قبل 25 دقيقة. |
4029 |
spending_limit |
ارفع حد الرصيد للمفتاح، أو انتظر دورة الفوترة التالية. |
حين تنتهي الجلسة بخطأ، لا يُفرَّغ الصوت المرسل بعد آخر نص مفرّغ استلمته، ولا يُخصم ثمنه. إن كانت تلك الكلمات مهمة، فاحتفظ بالثواني القليلة الأخيرة من الصوت لديك وأرسلها من جديد في الجلسة الجديدة.
الفوترة
يُحتسب البث بسعر تحويل الكلام إلى نص مباشرةً، 200 credits لكل دقيقة من الصوت، بالثانية. يُخصم ثمن كل مقطع حين يُرسل نصه المفرّغ، ويُقرَّب إلى رصيد كامل؛ فالمقطع ذو الـ 2.4 ثانية أعلاه يكلّف 8 credits. والمقطع الخالي من الكلام يعود بقيمة text فارغة، ومع ذلك يُحتسب بحسب طوله، فلا تبث فترات صمت طويلة لا تحتاج إلى تفريغها. ينطبق حد الرصيد الخاص بالمفتاح على كل مقطع. انظر الرصيد.
الحدود
| الحد | القيمة |
|---|---|
| حجم الإطار | 64 KiB لكل إطار ثنائي؛ ويُنصح بنحو 100 ميلي ثانية من الصوت |
| طول المقطع | 15 ثانية؛ يُقطع الكلام الأطول ويُفرَّغ على أجزاء |
| طول الجلسة | 25 دقيقة؛ ثم تنتهي الجلسة بالسبب max_duration |
| مهلة الخمول | 120 ثانية دون أي إطار |
| البثوث المتزامنة | 10 بثوث مفتوحة لكل مساحة عمل، لتحويل الكلام إلى نص وتحويل النص إلى كلام معًا |
| معدل الاتصال | 100 محاولة اتصال في الدقيقة لكل مفتاح |
انظر حدود معدل الطلبات لمعرفة كيف تُحتسب حدود البث.
حل المشكلات
تفشل الترقية بالخطأ 400 sample_rate must be an integer from 8000 to 48000
sample_rate مفقود أو خارج النطاق. اتصل مع ضبط ?sample_rate= على معدل عيّنات صوتك.
تفشل الترقية بالخطأ 400 language must be one of ar, en
language غير موجود في الاستعلام، أو يحمل قيمة غير ar أو en. اتصل مع ?language=ar للكلام العربي والمختلط، أو ?language=en للكلام الإنجليزي كله.
تفشل الترقية بالخطأ 429 Too many concurrent streams
لدى مساحة العمل 10 بثوث مفتوحة بالفعل، بما فيها بثوث تحويل النص إلى كلام. أغلق واحدًا منها، أو تحقق من أن الكود يغلق المقبس حين تنتهي المكالمة أو الجلسة.
لا يصل أي نص مفرّغ
لا يُفرَّغ شيء حتى ينتهي المقطع. أرسل end_segment حين يتوقف المتحدث، أو stop في النهاية.
كل النصوص المفرّغة فارغة أو مشوّهة
الصوت لا يطابق ما صرّحت به. تحقق من أنه بعمق 16 بت، وبترتيب little-endian، وأحادي القناة، وأنه بلا رأس ملف، وأن sample_rate هو المعدل الذي سُجّل به. فالمعدل الخاطئ يجعل الكلام يبدو للنموذج أسرع أو أبطأ من حقيقته.
يُغلق المقبس بالرمز 1008
تجاوز إطارٌ 64 KiB، أو لم يكن إطار نصي JSON صالحًا، أو كان type فيه غير معروف للبث. اقرأ رسالة error المرسلة قبيل الإغلاق مباشرة لمعرفة التفاصيل.
الأسئلة الشائعة
هل يمكنني الحصول على الكلمات والمتحدث ما زال يتكلم؟
لا. يغطي كل نص مفرّغ مقطعًا كاملًا ويُرسل مرة واحدة، حين ينتهي المقطع. أرسل end_segment عند كل توقف لتحصل على النص أسرع.
هل يمكنني الاتصال من متصفح؟
لا. لا يستطيع المتصفح ضبط header Authorization على WebSocket، ويجب ألا يصل المفتاح إلى صفحة ويب أبدًا. التقط الصوت في المتصفح، وأرسله إلى خادمك، وافتح البث من هناك.
هل يمكنني إرسال صوت MP3 أو Opus أو استريو؟
لا. فكّ ترميزه وحوّله أولًا إلى PCM أحادي القناة بعمق 16 بت، كما في الخطوة 1.
هل الجلسة مهمةٌ أيضًا؟
لا. لا ينشئ البث أي مهمة، فلا يظهر في GET /v1/speech-to-text/jobs، ولا يرسل أي Webhook. النصوص المفرّغة موجودة فقط في الرسائل التي تستلمها، فخزّنها فور وصولها.