https://api.echonova.aiالمطورون
أمثلة الكود
برنامجان كاملان لكل لغة، الأول لمهمة تحويل نص إلى كلام غير متزامنة من الطلب حتى ملف صوتي محفوظ، والثاني لمهمة تحويل كلام إلى نص غير متزامنة من الرفع حتى النص المفرّغ، مع الغلاف الاستجابي الذي تقرؤه كلها.
يعمل كل برنامج في هذه الصفحة من الأعلى إلى الأسفل، مع تعديل واحد: مفتاح API الخاص بك. الأول يحوّل جملة إلى ملف صوتي محفوظ عبر مهمة تحويل نص إلى كلام غير متزامنة. الثاني يرفع ملفًا صوتيًا، وينتظر مهمة تحويل الكلام إلى نص غير المتزامنة، ثم يطبع النص المفرّغ. تشرح دلائل البدء كل استدعاء على حدة، وهذه الصفحة تجمعها معًا. وبرنامج ثالث قصير ينقّي تسجيلًا.
ما تحتاج إليه
- مفتاح API له وصول إلى الخدمات الثلاث معًا، أو بلا تقييد للخدمات. راجع الحصول على مفتاح API.
- الرابط الأساسي. يعرض التنبيه في أعلى هذه الصفحة المضيف الذي تستدعيه هذه الأمثلة. راجع الرابط الأساسي.
- رصيد على مساحة العمل. تُخصم كل مهمة قبل أن تعمل. راجع الرصيد.
- cURL مع
jq، أو Python 3 معrequests، أو Node 18 أو أحدث. - للبرنامج الثاني، ملف صوتي اسمه
audio.mp3بمدة تصل إلى 10 دقائق وحجم يصل إلى 150 MB. لملفات WAV أو OGG، غيّرCONTENT_TYPEإلىaudio/wavأوaudio/ogg.
الخطوات
الخطوة 1: اقرأ الغلاف مرة واحدة
كل استجابة، نجاحًا أو خطأً، كائن JSON واحد يحمل الحقول الأربعة نفسها. يُعيد الاستدعاء الناجح الرمز HTTP 200، بما فيه استدعاءات POST، وتكون النتيجة في data:
{
"success": true,
"statusCode": 200,
"errorMessage": null,
"data": {}
}عند الخطأ، تكون قيمة success هي false، وdata هي null، وstatusCode يطابق رمز HTTP، وerrorMessage يحمل نصًا واحدًا:
{
"data": null,
"statusCode": 402,
"success": false,
"errorMessage": "Insufficient subscription balance"
}يغلّف كل برنامج أدناه استدعاءاته في دالة مساعدة واحدة تتحقق من success وتعيد data، فيقرأ باقي الكود النتيجة وحدها. يتوقف البرنامج عند أول خطأ. يعرض إعادة الطلبات بأمان النسخة التي تتراجع وتعيد المحاولة.
الخطوة 2: تحويل النص إلى كلام، من الطلب إلى ملف صوتي
يعرض البرنامج الأصوات، ويختار أول صوت قيمة language فيه ar، ثم يبدأ مهمة غير متزامنة، ويستعلم عنها كل 3 ثوانٍ حتى تصبح completed أو failed، ثم ينزّل audioUrl إلى speech.wav. الرابط موقّع، لذلك لا يرسل التنزيل header Authorization.
set -eo pipefail
BASE_URL="https://api.echonova.ai"
AUTH="Authorization: Bearer YOUR_API_KEY"
TEXT="مرحبا بكم في إيكونوڤا"
# api METHOD PATH [curl options]: prints .data, or exits on an error envelope
api() {
local method=$1 path=$2 body
shift 2
body=$(curl -s -X "$method" "$BASE_URL$path" -H "$AUTH" "$@")
if [ "$(echo "$body" | jq -r '.success')" != "true" ]; then
echo "error $(echo "$body" | jq -r '.statusCode'): $(echo "$body" | jq -r '.errorMessage')" >&2
exit 1
fi
echo "$body" | jq '.data'
}
# 1. Pick an Arabic voice
VOICE_ID=$(api GET /v1/text-to-speech/voices \
| jq -r '[.items[] | select(.language == "ar")][0].id')
# 2. Start the job
JOB_ID=$(api POST /v1/text-to-speech -H "Content-Type: application/json" \
-d "$(jq -n --arg text "$TEXT" --arg voiceId "$VOICE_ID" '{text: $text, voiceId: $voiceId}')" \
| jq -r '.jobId')
echo "job $JOB_ID"
# 3. Poll until it ends
while true; do
JOB=$(api GET "/v1/text-to-speech/jobs/$JOB_ID")
STATUS=$(echo "$JOB" | jq -r '.status')
echo "$STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then break; fi
sleep 3
done
if [ "$STATUS" = "failed" ]; then
echo "failed: $(echo "$JOB" | jq -r '.error')" >&2
exit 1
fi
# 4. Download the audio. The link is signed; send no Authorization header.
curl -s -o speech.wav "$(echo "$JOB" | jq -r '.audioUrl')"
echo "saved speech.wav"
echo "$JOB" | jq '{durationSeconds, creditsConsumed}'import time
import requests
BASE_URL = "https://api.echonova.ai"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
TEXT = "مرحبا بكم في إيكونوڤا"
def call(method, path, **kwargs):
# Every response uses the same envelope: raise when success is false, return data.
response = requests.request(
method, f"{BASE_URL}{path}", headers=HEADERS, timeout=60, **kwargs
)
body = response.json()
if not body["success"]:
raise RuntimeError(f'{body["statusCode"]}: {body["errorMessage"]}')
return body["data"]
# 1. Pick an Arabic voice
voices = call("GET", "/v1/text-to-speech/voices")["items"]
voice_id = next(voice["id"] for voice in voices if voice["language"] == "ar")
# 2. Start the job
job_id = call("POST", "/v1/text-to-speech", json={"text": TEXT, "voiceId": voice_id})["jobId"]
print("job", job_id)
# 3. Poll until it ends
while True:
job = call("GET", f"/v1/text-to-speech/jobs/{job_id}")
print(job["status"])
if job["status"] in ("completed", "failed"):
break
time.sleep(3)
if job["status"] == "failed":
raise SystemExit(f'failed: {job.get("error")}')
# 4. Download the audio. The link is signed; send no Authorization header.
audio = requests.get(job["audioUrl"], timeout=60)
audio.raise_for_status()
with open("speech.wav", "wb") as f:
f.write(audio.content)
print("saved speech.wav")
print(job["durationSeconds"], "seconds,", job["creditsConsumed"], "credits")import { writeFile } from 'node:fs/promises';
const BASE_URL = 'https://api.echonova.ai';
const HEADERS = { Authorization: 'Bearer YOUR_API_KEY' };
const TEXT = 'مرحبا بكم في إيكونوڤا';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Every response uses the same envelope: throw when success is false, return data.
async function call(path, init = {}) {
const response = await fetch(`${BASE_URL}${path}`, {
...init,
headers: { ...HEADERS, ...(init.headers ?? {}) },
});
const body = await response.json();
if (!body.success) throw new Error(`${body.statusCode}: ${body.errorMessage}`);
return body.data;
}
// 1. Pick an Arabic voice
const { items: voices } = await call('/v1/text-to-speech/voices');
const voiceId = voices.find((voice) => voice.language === 'ar').id;
// 2. Start the job
const { jobId } = await call('/v1/text-to-speech', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text: TEXT, voiceId }),
});
console.log('job', jobId);
// 3. Poll until it ends
let job;
for (;;) {
job = await call(`/v1/text-to-speech/jobs/${jobId}`);
console.log(job.status);
if (job.status === 'completed' || job.status === 'failed') break;
await sleep(3000);
}
if (job.status === 'failed') throw new Error(`failed: ${job.error}`);
// 4. Download the audio. The link is signed; send no Authorization header.
const audio = await fetch(job.audioUrl);
if (!audio.ok) throw new Error(`download failed: ${audio.status}`);
await writeFile('speech.wav', Buffer.from(await audio.arrayBuffer()));
console.log('saved speech.wav');
console.log(job.durationSeconds, 'seconds,', job.creditsConsumed, 'credits');يُعيد POST الكائن { "jobId", "status" } وقيمة status هي processing. يسمّي كائن المهمة المستعلَم معرّفه id، ويضيف text وvoiceId وcreatedAt وcreditsConsumed. يظهر audioUrl وdurationSeconds عندما تصبح المهمة completed، ويظهر error عندما تصبح failed. الملف بصيغة WAV. راجع دليل البدء لتحويل النص إلى كلام لكل استدعاء على حدة، وخيارات الأصوات في تحويل النص إلى كلام تشرح speed وcfg وnumSteps وtashkeel.
الخطوة 3: تحويل الكلام إلى نص، من الرفع إلى النص المفرّغ
يرسل البرنامج الملف audio.mp3 بوصفه الجزء file من طلب multipart/form-data، ويستعلم عن المهمة كل 5 ثوانٍ، ويطبع transcription وdurationSeconds عندما تصبح completed. يجب أن يبدأ Content-Type للجزء file بـ audio/، وكل عينة تضبطه صراحة.
set -eo pipefail
BASE_URL="https://api.echonova.ai"
AUTH="Authorization: Bearer YOUR_API_KEY"
AUDIO_FILE="audio.mp3"
CONTENT_TYPE="audio/mpeg"
# api METHOD PATH [curl options]: prints .data, or exits on an error envelope
api() {
local method=$1 path=$2 body
shift 2
body=$(curl -s -X "$method" "$BASE_URL$path" -H "$AUTH" "$@")
if [ "$(echo "$body" | jq -r '.success')" != "true" ]; then
echo "error $(echo "$body" | jq -r '.statusCode'): $(echo "$body" | jq -r '.errorMessage')" >&2
exit 1
fi
echo "$body" | jq '.data'
}
# 1. Upload the file and start the job
JOB_ID=$(api POST /v1/speech-to-text -F "file=@$AUDIO_FILE;type=$CONTENT_TYPE" | jq -r '.jobId')
echo "job $JOB_ID"
# 2. Poll until it ends
while true; do
JOB=$(api GET "/v1/speech-to-text/jobs/$JOB_ID")
STATUS=$(echo "$JOB" | jq -r '.status')
echo "$STATUS"
if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then break; fi
sleep 5
done
if [ "$STATUS" = "failed" ]; then
echo "failed: $(echo "$JOB" | jq -r '.error')" >&2
exit 1
fi
# 3. Read the transcript
echo "$JOB" | jq -r '.transcription'
echo "$JOB" | jq '{durationSeconds, creditsConsumed}'import time
import requests
BASE_URL = "https://api.echonova.ai"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
AUDIO_FILE = "audio.mp3"
CONTENT_TYPE = "audio/mpeg"
def call(method, path, **kwargs):
# Every response uses the same envelope: raise when success is false, return data.
response = requests.request(
method, f"{BASE_URL}{path}", headers=HEADERS, timeout=300, **kwargs
)
body = response.json()
if not body["success"]:
raise RuntimeError(f'{body["statusCode"]}: {body["errorMessage"]}')
return body["data"]
# 1. Upload the file and start the job
with open(AUDIO_FILE, "rb") as audio:
job_id = call(
"POST",
"/v1/speech-to-text",
files={"file": (AUDIO_FILE, audio, CONTENT_TYPE)},
)["jobId"]
print("job", job_id)
# 2. Poll until it ends
while True:
job = call("GET", f"/v1/speech-to-text/jobs/{job_id}")
print(job["status"])
if job["status"] in ("completed", "failed"):
break
time.sleep(5)
if job["status"] == "failed":
raise SystemExit(f'failed: {job.get("error")}')
# 3. Read the transcript
print(job["transcription"])
print(job["durationSeconds"], "seconds,", job["creditsConsumed"], "credits")import { readFile } from 'node:fs/promises';
const BASE_URL = 'https://api.echonova.ai';
const HEADERS = { Authorization: 'Bearer YOUR_API_KEY' };
const AUDIO_FILE = 'audio.mp3';
const CONTENT_TYPE = 'audio/mpeg';
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Every response uses the same envelope: throw when success is false, return data.
async function call(path, init = {}) {
const response = await fetch(`${BASE_URL}${path}`, {
...init,
headers: { ...HEADERS, ...(init.headers ?? {}) },
});
const body = await response.json();
if (!body.success) throw new Error(`${body.statusCode}: ${body.errorMessage}`);
return body.data;
}
// 1. Upload the file and start the job
const formData = new FormData();
formData.append('file', new Blob([await readFile(AUDIO_FILE)], { type: CONTENT_TYPE }), AUDIO_FILE);
const { jobId } = await call('/v1/speech-to-text', { method: 'POST', body: formData });
console.log('job', jobId);
// 2. Poll until it ends
let job;
for (;;) {
job = await call(`/v1/speech-to-text/jobs/${jobId}`);
console.log(job.status);
if (job.status === 'completed' || job.status === 'failed') break;
await sleep(5000);
}
if (job.status === 'failed') throw new Error(`failed: ${job.error}`);
// 3. Read the transcript
console.log(job.transcription);
console.log(job.durationSeconds, 'seconds,', job.creditsConsumed, 'credits');أثناء بقاء المهمة في processing، تحمل durationSeconds تقديرًا مستخرجًا من الملف، ولا يظهر transcription. عندما تصبح completed، يحمل الحقلان قيمهما النهائية. وتحمل المهمة أيضًا language وnumbersAsDigits، وإذا أُنشئت مع identify_speakers=true وكانت لديها بيانات المتحدثين، فإنها تحمل segments وspeakers الموقّتة؛ وبدونها تكون هاتان القائمتان فارغتين. للحصول على ترجمة، نزّل المهمة من GET /v1/speech-to-text/jobs/{jobId}/export?format=srt. راجع دليل البدء لتحويل الكلام إلى نص لكل استدعاء على حدة، وصيغ تحويل الكلام إلى نص ومخرجاته للمدخلات المقبولة.
الخطوة 4: تنقية الصوت، رفع وتنزيل
تنقية الصوت متزامنة، لذلك لا يوجد استعلام دوري. يرفع البرنامج الملف noisy.wav (10 دقائق أو أقل، و150 MB أو أقل)، ويقرأ outputUrl من الاستجابة، ويحفظ ملف WAV المنقّى باسم cleaned.wav. يجب أن يطابق Content-Type للجزء file امتداد الملف.
set -eo pipefail
BASE_URL="https://api.echonova.ai"
AUTH="Authorization: Bearer YOUR_API_KEY"
RESPONSE=$(curl -sS -X POST "$BASE_URL/v1/sound-cleaning" -H "$AUTH" -F "file=@noisy.wav;type=audio/wav")
if [ "$(echo "$RESPONSE" | jq -r '.success')" != "true" ]; then
echo "$RESPONSE" | jq -r '"\(.statusCode) \(.errorMessage)"' >&2
exit 1
fi
curl -sS -o cleaned.wav "$(echo "$RESPONSE" | jq -r '.data.outputUrl')"
echo "$RESPONSE" | jq -r '.data.creditsConsumed'import requests
BASE_URL = "https://api.echonova.ai"
HEADERS = {"Authorization": "Bearer YOUR_API_KEY"}
with open("noisy.wav", "rb") as audio:
body = requests.post(
f"{BASE_URL}/v1/sound-cleaning",
headers=HEADERS,
files={"file": ("noisy.wav", audio, "audio/wav")},
).json()
if not body["success"]:
raise SystemExit(f'{body["statusCode"]} {body["errorMessage"]}')
job = body["data"]
with open("cleaned.wav", "wb") as out:
out.write(requests.get(job["outputUrl"]).content)
print(job["creditsConsumed"], "credits")import { readFile, writeFile } from 'node:fs/promises';
const BASE_URL = 'https://api.echonova.ai';
const formData = new FormData();
formData.append('file', new Blob([await readFile('noisy.wav')], { type: 'audio/wav' }), 'noisy.wav');
const body = await fetch(`${BASE_URL}/v1/sound-cleaning`, {
method: 'POST',
headers: { Authorization: 'Bearer YOUR_API_KEY' },
body: formData,
}).then((r) => r.json());
if (!body.success) throw new Error(`${body.statusCode} ${body.errorMessage}`);
const audio = await fetch(body.data.outputUrl).then((r) => r.arrayBuffer());
await writeFile('cleaned.wav', Buffer.from(audio));
console.log(body.data.creditsConsumed, 'credits');راجع دليل البدء لتنقية الصوت لحقول الضبط والحدود.
ماذا يحدث بعد ذلك
- تُسرد المهام تحت مفتاحك، قائمة لكل خدمة:
GET /v1/text-to-speech/jobs، وGET /v1/speech-to-text/jobs، وGET /v1/sound-cleaning/jobs، الأحدث أولًا. يحمل كل منهاcreditsConsumedالذي خُصم له. - الملف
speech.wavصار على القرص. الرابطaudioUrlالذي جاء منه ينتهي؛ اقرأ المهمة من جديد للحصول على رابط جديد إذا احتجت الملف لاحقًا. - طُبع النص المفرّغ على الخرج القياسي. احفظه بنفسك؛ تحتفظ الواجهة به على المهمة، وهناك فقط.
- إذا انتهت أي مهمة بحالة
failed، فقد طُبعerrorالخاص بها، وأُعيدت أرصدتها. شغّل البرنامج مرة أخرى.
الحدود
| الحد | القيمة | ينطبق على |
|---|---|---|
طول text |
من 1 إلى 500 حرف | POST /v1/text-to-speech |
| مدة الصوت | 10 دقائق | POST /v1/speech-to-text |
| حجم الملف | 150 MB | POST /v1/speech-to-text |
Content-Type للجزء file |
يجب أن يبدأ بـ audio/ |
POST /v1/speech-to-text |
الوقت في processing |
10 دقائق، ثم تُعلَّم المهمة failed خلال 15 دقيقة من POST |
المهمتان |
| الطلبات | 100 لكل 60 ثانية لكل مفتاح ولكل نقطة نهاية | كل استدعاء، بما فيه الاستعلامات الدورية |
استكشاف المشكلات
البرنامج يطبع رمز حالة ورسالة ثم يتوقف
أطلقت الدالة المساعدة خطأً بسبب غلاف الخطأ. الرقم هو statusCode والنص هو errorMessage. يعني 401 Unauthorized أن المفتاح غير صالح: تحقق من أنك استبدلت YOUR_API_KEY، وأن المفتاح مفعّل. يعني 402 Insufficient subscription balance أن مساحة العمل لا تكفي للمهمة، ولم يعمل شيء. يعني 403 مع does not have access to أن المفتاح مقيّد بالخدمات. راجع المصادقة.
البرنامج يطبع failed مع رسالة خطأ
انتهت المهمة بحالة failed. تسمّي الرسالة السبب: وهي AI server generation failed أو AI server transcription failed عندما لا يُعطى سبب، وJob timed out — AI server did not respond عندما تبقى المهمة في processing لأكثر من 10 دقائق. أُعيدت الأرصدة. شغّل البرنامج مرة أخرى.
البحث عن الصوت لا يجد شيئًا
لا يوجد صوت نشط قيمة language فيه ar، فتُطلق next() في Python الاستثناء StopIteration، ويُعيد voices.find في Node القيمة undefined، وتصبح VOICE_ID في الصدفة null. اعرض الأصوات واختر id يدويًا؛ راجع عرض الأصوات المتاحة.
400 Unsupported file type: application/octet-stream. Please upload an audio file.
أُرسل الجزء file دون نوع محتوى من نوع audio/. أبقِ قيمة CONTENT_TYPE في العينة؛ ولا تحذف type= من الوسيط -F في cURL، ولا type من Blob في Node.
تنزيل الصوت يفشل
انتهت صلاحية الرابط الموقّع. استدعِ GET /v1/text-to-speech/jobs/{jobId} مرة أخرى؛ يعيد كل استدعاء رابط audioUrl جديدًا. نزّله فورًا.
429 ThrottlerException: Too Many Requests
تشترك نسخ عدة من البرنامج في مفتاح واحد، وتتراكم استعلاماتها لتتجاوز 100 طلب لكل 60 ثانية على نقطة نهاية المهام. انتظر عدد الثواني الموجود في Retry-After، ثم استخدم فترة أطول بين الاستعلامات، أو مفتاحًا لكل عملية. راجع حدود المعدل.
الأسئلة الشائعة
أين النسخ المتزامنة؟
في دلائل البدء. يعيد POST /v1/text-to-speech/sync القيمة audioUrl في الاستجابة نفسها، ويعيد POST /v1/speech-to-text/sync القيمة transcription، دون استعلام دوري. راجع دليل البدء لتحويل النص إلى كلام ودليل البدء لتحويل الكلام إلى نص، والاستدعاءات المتزامنة وغير المتزامنة لتختار بينهما.
لماذا أتحقق من success بدلًا من رمز HTTP؟
كلاهما يعمل. كل استجابة خطأ تحمل success بقيمة false وstatusCode يساوي رمز HTTP، فيستطيع العميل أن يعتمد على أيهما. فحص الغلاف سطر واحد، ويعطيك errorMessage في الخطوة نفسها.
هل أستطيع تخطي استدعاء الأصوات؟
نعم. اضبط voiceId على id تعرفه مسبقًا. يجب أن يكون الصوت نشطًا؛ فالمعرّف المجهول أو غير النشط يعيد 404 Voice not found.
هل أستطيع استلام النتيجة دون استعلام دوري؟
نعم. سجّل Webhook للحدثين job.completed وjob.failed. يحمل الحدث job.completed القيمة audioUrl أو transcription، فلا يلزم أي استدعاء إضافي. راجع إعداد Webhook وأحداث Webhook.
هل تعيد البرامج المحاولة؟
لا. تتوقف عند أول خطأ. راجع إعادة الطلبات بأمان لحلقة الاستعلام التي تحترم Retry-After وتتراجع عند 5xx.