https://api.echonova.aiالمطورون
دليل البدء السريع لتحويل الكلام إلى نص
انسخ ملفًا صوتيًا قصيرًا في استدعاء واحد، واقرأ النص المفرّغ ومدته، وشغّل ملفًا طويلًا كمهمة غير متزامنة واستعلم عنها دوريًا حتى تنتهي، ثم نزّله كترجمة بتسميات المتحدثين.
تحويل الكلام إلى نص يحوّل ملفًا صوتيًا إلى نص مفرّغ. يُجري هذا الدليل استدعاءً متزامنًا واحدًا لملف قصير، ويقرأ الحقلين اللذين ستستخدمهما أكثر، ويشغّل ملفًا طويلًا كمهمة غير متزامنة تستعلم عنها دوريًا حتى تنتهي، ثم ينزّل مهمة كترجمة تبيّن من يتكلم.
ما تحتاج إليه
- مفتاح API. انظر احصل على مفتاح API. إذا كان المفتاح يقيّد الخدمات، فيحتاج إلى وصول على كلام إلى نص (STT).
- الرابط الأساسي. انظر الرابط الأساسي. تُظهر الملاحظة في أعلى هذه الصفحة المضيف الذي تستدعيه هذه الأمثلة.
- رصيد على مساحة العمل. يُخصم كل استدعاء قبل تشغيله. انظر الرصيد.
- ملف صوتي مدته 10 دقائق أو أقل وحجمه 150 ميجابايت أو أقل. يخضع كل من نقطة النهاية المتزامنة ونقطة النهاية غير المتزامنة للحدود نفسها.
الخطوات
الخطوة 1: انسخ ملفًا قصيرًا بشكل متزامن
أرسل الملف كجزء file في طلب multipart/form-data إلى POST https://api.echonova.ai/v1/speech-to-text/sync. ينتظر الاستدعاء حتى يكتمل النص المفرّغ، ثم يعيده في الاستجابة.
أضف الحقل الاختياري language: auto (الافتراضي) أو ar أو en.
curl -X POST "https://api.echonova.ai/v1/speech-to-text/sync" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@audio.mp3;type=audio/mpeg" \
-F "language=ar"import requests
with open("audio.mp3", "rb") as audio:
response = requests.post(
"https://api.echonova.ai/v1/speech-to-text/sync",
headers={"Authorization": "Bearer YOUR_API_KEY"},
files={"file": ("audio.mp3", audio, "audio/mpeg")},
data={"language": "ar"},
)
print(response.json())import { readFile } from 'node:fs/promises';
const formData = new FormData();
formData.append('file', new Blob([await readFile('audio.mp3')], { type: 'audio/mpeg' }), 'audio.mp3');
formData.append('language', 'ar');
const response = await fetch('https://api.echonova.ai/v1/speech-to-text/sync', {
method: 'POST',
headers: { Authorization: 'Bearer YOUR_API_KEY' },
body: formData,
});
console.log(await response.json());تفحص الواجهة قيمة Content-Type لجزء file، لا امتداد الملف، ويجب أن تبدأ بـ audio/. اضبطها صراحةً كما تفعل الأمثلة. بدونها يرسل cURL القيمة application/octet-stream، ولا يرسل Python requests أي نوع أصلًا فتقرأه الواجهة على أنه text/plain، ويرسل Blob في Node بلا type القيمة application/octet-stream. تُرفض الطلبات الثلاثة كلها بالرمز 400. استخدم audio/wav لملفات WAV، وaudio/mpeg لملفات MP3، وaudio/ogg لملفات OGG.
اذكر النوع على الجزء، لا على الطلب. لا يُقرأ امتداد الملف.
الموصى به
-F "file=@audio.wav;type=audio/wav"الأقل فاعلية
-F "file=@audio.wav" -H "Content-Type: audio/wav"الخطوة 2: اقرأ النص المفرّغ والمدة
يعيد الاستدعاء الناجح الرمز HTTP 200 والغلاف المعتاد. النتيجة موجودة في data:
{
"success": true,
"statusCode": 200,
"errorMessage": null,
"data": {
"jobId": "66e3f2a1c9d4b8a1f0e12345",
"status": "completed",
"transcription": "مرحبا بكم في إيكونوڤا",
"durationSeconds": 15.51,
"creditsConsumed": 26,
"language": "ar"
}
}| الحقل | النوع | ما يحتويه |
|---|---|---|
jobId |
string | معرّف المهمة التي أنشأها الاستدعاء. وهي مكتملة بالفعل، ويمكنك قراءتها مرة أخرى من GET /v1/speech-to-text/jobs/{jobId}. |
status |
string | دائمًا completed عند نجاح الاستدعاء المتزامن. |
transcription |
string | النص المفرّغ. |
durationSeconds |
number or null |
مدة الصوت كما قيست، بالثواني، مع الكسور العشرية. |
creditsConsumed |
number | الرصيد المخصوم لهذا الاستدعاء، وهو عدد صحيح. |
language |
string | اللغة التي طُلب بها التفريغ. |
numbersAsDigits |
boolean | هل كُتبت الأعداد المنطوقة أرقامًا. تكون true ما لم ترسل numbers_as_digits=false. |
identifySpeakers |
boolean | هل أرسلت identify_speakers=true. انظر الخطوة 4. |
segments |
array | النص المفرّغ في مقاطع موقّتة، على كل منها متحدث. فارغة ما لم تحمل المهمة بيانات المتحدثين. |
speakers |
array | المتحدثون الذين سُمعوا ومدة كلامهم. فارغة ما لم تحمل المهمة بيانات المتحدثين. |
لا يتضمن المثال numbersAsDigits وidentifySpeakers وsegments وspeakers، التي تحملها الاستجابة والمهمة أيضًا؛ راجع صيغ تحويل الكلام إلى نص ومخرجاته. transcription هو النص الذي تخزّنه أو تعرضه. وdurationSeconds يخبرك بمقدار الصوت الذي عُولج. تُحتسب الأرصدة حسب مدة الصوت، بمعدل 100 credits في الدقيقة، لذلك يتحركان معًا. الأسعار موجودة في صفحة الرصيد.
الخطوة 3: شغّل ملفًا بشكل غير متزامن
حين لا تريد إبقاء الاتصال مفتوحًا، أو تريد Webhook يُرسل عند انتهاء المهمة، أرسل الملف إلى POST https://api.echonova.ai/v1/speech-to-text بدلًا من ذلك. الطلب هو رفع multipart/form-data نفسه، مع الحقل الاختياري language نفسه. والحد هو 10 دقائق من الصوت، كما في نقطة النهاية المتزامنة. يعود الاستدعاء فور قبول الملف:
{
"success": true,
"statusCode": 200,
"errorMessage": null,
"data": {
"jobId": "66e3f2a1c9d4b8a1f0e12346",
"status": "processing"
}
}ثم استعلم عن GET https://api.echonova.ai/v1/speech-to-text/jobs/{jobId} دوريًا، حتى يصبح status إما completed أو failed:
curl -X POST "https://api.echonova.ai/v1/speech-to-text" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@audio.mp3;type=audio/mpeg" \
-F "language=ar"
# Repeat until data.status is "completed" or "failed"
curl "https://api.echonova.ai/v1/speech-to-text/jobs/JOB_ID" \
-H "Authorization: Bearer YOUR_API_KEY"import time
import requests
headers = {"Authorization": "Bearer YOUR_API_KEY"}
with open("audio.mp3", "rb") as audio:
started = requests.post(
"https://api.echonova.ai/v1/speech-to-text",
headers=headers,
files={"file": ("audio.mp3", audio, "audio/mpeg")},
data={"language": "ar"},
).json()
job_id = started["data"]["jobId"]
while True:
job = requests.get(
f"https://api.echonova.ai/v1/speech-to-text/jobs/{job_id}",
headers=headers,
).json()["data"]
if job["status"] in ("completed", "failed"):
break
time.sleep(5)
if job["status"] == "completed":
print(job["transcription"], job["durationSeconds"])
else:
print("failed:", job["error"])import { readFile } from 'node:fs/promises';
const headers = { Authorization: 'Bearer YOUR_API_KEY' };
const formData = new FormData();
formData.append('file', new Blob([await readFile('audio.mp3')], { type: 'audio/mpeg' }), 'audio.mp3');
formData.append('language', 'ar');
const started = await fetch('https://api.echonova.ai/v1/speech-to-text', {
method: 'POST',
headers,
body: formData,
}).then((r) => r.json());
const jobId = started.data.jobId;
let job;
while (true) {
job = await fetch(`https://api.echonova.ai/v1/speech-to-text/jobs/${jobId}`, {
headers,
})
.then((r) => r.json())
.then((body) => body.data);
if (job.status === 'completed' || job.status === 'failed') break;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
if (job.status === 'completed') {
console.log(job.transcription, job.durationSeconds);
} else {
console.log('failed:', job.error);
}هذا شكل مهمة مكتملة:
{
"success": true,
"statusCode": 200,
"errorMessage": null,
"data": {
"id": "66e3f2a1c9d4b8a1f0e12346",
"status": "completed",
"transcription": "مرحبا بكم في إيكونوڤا",
"durationSeconds": 312.4,
"creditsConsumed": 521,
"language": "ar",
"createdAt": "2026-09-15T10:21:04.512Z"
}
}أثناء بقاء المهمة في processing لا يكون transcription موجودًا، ويحمل durationSeconds تقديرًا مأخوذًا من الملف. عند اكتمال المهمة يظهر transcription، وتحل المدة المقاسة محل durationSeconds. وعند فشلها يظهر error مع السبب، وتُسترد الأرصدة. وحتى تُضبط هذه الحقول، يكون transcription وerror غائبين عن الكائن، لا قيمتهما null.
بدلًا من الاستعلام الدوري، سجّل نقطة نهاية Webhook في البوابة. يحمل الحدث job.completed القيمتين transcription وdurationSeconds، فلا تحتاج إلى استدعاء ثانٍ. ولا يحمل segments ولا speakers؛ اقرأ المهمة للحصول عليهما. انظر إعداد Webhook وأحداث Webhook.
الخطوة 4: حدّد المتحدثين ونزّل الترجمة
أضف identify_speakers=true إلى الرفع من الخطوة 3. وحين تصبح المهمة completed، نزّلها من GET https://api.echonova.ai/v1/speech-to-text/jobs/{jobId}/export مع format=srt. التنزيل هو الملف نفسه، لا غلاف JSON.
curl -X POST "https://api.echonova.ai/v1/speech-to-text" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@call.mp3;type=audio/mpeg" \
-F "language=ar" \
-F "identify_speakers=true"
# Once GET /v1/speech-to-text/jobs/JOB_ID shows "completed"
curl "https://api.echonova.ai/v1/speech-to-text/jobs/JOB_ID/export?format=srt&lang=en" \
-H "Authorization: Bearer YOUR_API_KEY" \
-o call.srtimport time
import requests
headers = {"Authorization": "Bearer YOUR_API_KEY"}
with open("call.mp3", "rb") as audio:
started = requests.post(
"https://api.echonova.ai/v1/speech-to-text",
headers=headers,
files={"file": ("call.mp3", audio, "audio/mpeg")},
data={"language": "ar", "identify_speakers": "true"},
).json()
job_id = started["data"]["jobId"]
while True:
job = requests.get(
f"https://api.echonova.ai/v1/speech-to-text/jobs/{job_id}",
headers=headers,
).json()["data"]
if job["status"] in ("completed", "failed"):
break
time.sleep(5)
if job["status"] == "completed":
subtitles = requests.get(
f"https://api.echonova.ai/v1/speech-to-text/jobs/{job_id}/export",
headers=headers,
params={"format": "srt", "lang": "en"},
)
subtitles.raise_for_status()
with open("call.srt", "wb") as out:
out.write(subtitles.content)
print(len(job["speakers"]), "speakers")
else:
print("failed:", job["error"])import { readFile, writeFile } from 'node:fs/promises';
const headers = { Authorization: 'Bearer YOUR_API_KEY' };
const formData = new FormData();
formData.append('file', new Blob([await readFile('call.mp3')], { type: 'audio/mpeg' }), 'call.mp3');
formData.append('language', 'ar');
formData.append('identify_speakers', 'true');
const started = await fetch('https://api.echonova.ai/v1/speech-to-text', {
method: 'POST',
headers,
body: formData,
}).then((r) => r.json());
const jobId = started.data.jobId;
let job;
while (true) {
job = await fetch(`https://api.echonova.ai/v1/speech-to-text/jobs/${jobId}`, {
headers,
})
.then((r) => r.json())
.then((body) => body.data);
if (job.status === 'completed' || job.status === 'failed') break;
await new Promise((resolve) => setTimeout(resolve, 5000));
}
if (job.status === 'completed') {
const subtitles = await fetch(`https://api.echonova.ai/v1/speech-to-text/jobs/${jobId}/export?format=srt&lang=en`, { headers });
if (!subtitles.ok) throw new Error((await subtitles.json()).errorMessage);
await writeFile('call.srt', await subtitles.text());
console.log(job.speakers.length, 'speakers');
} else {
console.log('failed:', job.error);
}تسرد المهمة المكتملة المتحدثين، والنص المفرّغ في مقاطع موقّتة. المتحدثون هم spk0 وspk1 وهكذا، بترتيب أول مرة يتكلمون فيها:
{
"identifySpeakers": true,
"segments": [
{ "start": 0.38, "end": 4.12, "text": "السلام عليكم، كيف حالك؟", "speaker": "spk0" },
{ "start": 4.3, "end": 6.4, "text": "بخير، الحمد لله.", "speaker": "spk1" }
],
"speakers": [
{ "id": "spk0", "talkTime": 3.74 },
{ "id": "spk1", "talkTime": 2.1 }
]
}يسمّي ملف SRT كل مقطع ترجمة بمتحدثه. والقيمة lang=ar تكتب [المتحدث 1] بدلًا من [Speaker 1]:
1
00:00:00,380 --> 00:00:04,120
[Speaker 1] السلام عليكم، كيف حالك؟
2
00:00:04,300 --> 00:00:06,400
[Speaker 2] بخير، الحمد لله.أرسل format=txt للنص المفرّغ وحده، أو format=json للنص المفرّغ والمتحدثين بتسمياتهم والمقاطع في ملف واحد. قواعد مقاطع الترجمة وأسماء الملفات وشكل JSON موجودة في صيغ تحويل الكلام إلى نص ومخرجاته، وكل معامل وخطأ موجود في تنزيل مهمة مكتملة لتحويل الكلام إلى نص.
قد لا يكون تحديد المتحدثين متاحًا لك بعد. وإلى أن يتاح، تكتمل المهمة المُنشأة مع identify_speakers=true بنصها المفرّغ، وتكون segments وspeakers فارغتين. ويحمل تنزيل SRT حينها مقطع ترجمة واحدًا للملف كله. تحقّق من أن segments ليست فارغة قبل أن تعتمد على تسميات المتحدثين.
ماذا يحدث بعد ذلك
- كل استدعاء، متزامنًا كان أو غير متزامن، ينشئ مهمة. تُسرد مهام الـ API في مساحة العمل عبر
GET /v1/speech-to-text/jobs، الأحدث أولًا، بعشرين مهمة في الصفحة افتراضيًا. يرى هذه المهام أي مفتاح في مساحة العمل له وصول إلى تحويل الكلام إلى نص، أيًا كان المفتاح الذي أنشأها. - تُخصم الأرصدة من مساحة العمل قبل بدء التفريغ، وتُسترد إذا فشلت المهمة. يُحدَّث عداد الاستخدام الخاص بالمفتاح بعد الاستدعاء. انظر الرصيد.
- المهمة غير المتزامنة التي تبقى في
processingلأكثر من 10 دقائق تُعلَّمfailedمع الخطأJob timed out — AI server did not respond، خلال 15 دقيقة منPOST. تُسترد الأرصدة. هذه المهلة لا ترسل حدثjob.failedعبر Webhook، لذلك يجب على العميل الذي يستمع إلى Webhook أن يستعلم أيضًا عن أي مهمة صامتة لمدة 15 دقيقة.
الحدود
| الحد | القيمة | ينطبق على |
|---|---|---|
| مدة الصوت | 10 دقائق | نقطتا الرفع كلتاهما |
| حجم الملف | 150 ميجابايت | نقطتا الرفع كلتاهما |
Content-Type لجزء file |
يجب أن تبدأ بـ audio/ |
نقطتا الرفع كلتاهما |
معامل الاستعلام limit |
من 1 إلى 100، والافتراضي 20 | GET /v1/speech-to-text/jobs |
معامل الاستعلام offset |
0 أو أكثر، والافتراضي 0 | GET /v1/speech-to-text/jobs |
| الطلبات لكل مفتاح | انظر حدود معدل الطلبات | كل نقطة نهاية |
معامل الاستعلام format |
txt أو srt أو json |
GET /v1/speech-to-text/jobs/{jobId}/export |
| مقطع الترجمة | 7 ثوانٍ على الأكثر، وسطران من 42 حرفًا | تنزيل SRT |
حل المشكلات
400 Unsupported file type: application/octet-stream. Please upload an audio file.
لم يُرسل جزء file بنوع audio/. النوع المذكور في الرسالة هو ما أرسله عميلك، وtext/plain يعني أنه لم يرسل أي نوع. اضبط نوع الجزء كما في الخطوة 1.
400 language must be one of: auto, ar, en
حقل language يحمل قيمة أخرى. تُقلَّم القيمة وتُقرأ دون اعتبار لحالة الأحرف، فالقيمتان AR وen مقبولتان. أرسل auto أو ar أو en، أو احذف الحقل.
400 File is required
لم يحتوِ الطلب على جزء اسمه file. تحقق من اسم الحقل، ومن أن الطلب multipart/form-data.
400 Audio exceeds the 10-minute limit.
الملف أطول من 10 دقائق. تُرفض نقطتا الرفع بالرسالة نفسها، فقسّم التسجيلات الأطول.
413 File too large
الرفع أكبر من 150 ميجابايت. يُقطع الطلب قبل قراءة الملف. أعد ترميز الصوت بمعدل بت أقل، أو كقناة واحدة (mono).
503 Transcription is temporarily unavailable. Please try again.
التفريغ غير متاح مؤقتًا، أو تعذّر فك ترميز الملف. تُبلغ نقطة النهاية المتزامنة عن الحالتين بهذه الرسالة الواحدة. يُسترد الرصيد. أعد المحاولة مرة واحدة. إذا فشل الملف نفسه في كل مرة، أرسله إلى POST /v1/speech-to-text: تنتهي المهمة بحالة failed مع السبب في حقل error. الحاويات التي يُفكّ ترميزها مذكورة في صيغ تحويل الكلام إلى نص ومخرجاته.
400 identify_speakers must be true or false
حقل identify_speakers يحمل قيمة غير true أو false أو 1 أو 0. أرسل true أو false، أو احذف الحقل.
400 Job is not completed
طُلب التنزيل لمهمة حالتها pending أو processing أو failed. انتظر job.completed، أو استعلم عن المهمة حتى يصبح status هو completed، ثم نزّلها. المهمة الفاشلة ليس لها نص مفرّغ يُنزَّل.
404 STT Job not found
المعرّف jobId غير موجود، أو يعود إلى مساحة عمل أخرى، أو أُنشئ من البوابة. المعرّف الخاطئ الشكل يعيد 400 Invalid identifier.
403 This API key does not have access to Speech-to-Text services
يمنح المفتاح وصولًا لخدمة واحدة على الأقل، وتحويل الكلام إلى نص ليس بينها. عدّل المفتاح من البوابة، أو استخدم مفتاحًا آخر.
402 Insufficient subscription balance
لا تغطي مساحة العمل تكلفة هذا الملف. لم يُشغَّل شيء. أضف رصيدًا، أو استخدم ملفًا أقصر.
الأسئلة الشائعة
أي لغة يكون بها النص المفرّغ؟
أرسل language: القيمة auto (الافتراضي) تترك الخدمة تقرر. وهي العربية حاليًا، وتتعامل كذلك مع الكلام المختلط بين العربية والإنجليزية، والكلام الإنجليزي الواضح. استخدم ar للكلام العربي والمختلط. استخدم en فقط للكلام الإنجليزي كله. العربية المُرسلة بقيمة en تخرج مشوّهة. وأي قيمة أخرى تعطي 400.
هل أحصل على طوابع زمنية للكلمات أو تسميات للمتحدثين؟
تسميات المتحدثين، نعم: أرسل identify_speakers=true فتحمل المهمة مقاطع موقّتة على كل منها متحدث، كما في الخطوة 4. أما الطوابع الزمنية للكلمات، فلا. انظر صيغ تحويل الكلام إلى نص ومخرجاته للقائمة الكاملة للحقول، وما لا تعيده.
هل يمكنني تفريغ تسجيل مدته 30 دقيقة؟
ليس في استدعاء واحد. ترفض نقطتا الرفع أي شيء أطول من 10 دقائق قبل بدء التفريغ، لذلك قسّم التسجيل وأرسل كل جزء.
هل الاستدعاء المتزامن مهمة أيضًا؟
نعم. ينشئ مهمة في حالة completed، ويعيد jobId الخاص بها. تظهر المهمة في GET /v1/speech-to-text/jobs مع transcription وdurationSeconds وcreditsConsumed نفسها.
هل يمكنني رفع الملف نفسه مرتين؟
نعم. كل رفع مهمة جديدة وخصم جديد، أيًا كان الملف.