Circassian AI API
Перевод и синтез речи для кабардинского и адыгейского языков. Один ключ, обычный REST, никаких SDK.
Быстрый старт
- Получите ключ. Регистрации нет — ключи выдаются вручную. Напишите владельцу сервиса, укажите, что собираетесь делать и примерный объём.
- Проверьте, что ключ жив. Один запрос, ничего не расходует.
curl https://api.circassian.ai/v1/me \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY"
import os, httpx
r = httpx.get(
"https://api.circassian.ai/v1/me",
headers={"Authorization": f"Bearer {os.environ['CIRCASSIAN_API_KEY']}"},
)
print(r.json())
const res = await fetch("https://api.circassian.ai/v1/me", {
headers: { Authorization: `Bearer ${process.env.CIRCASSIAN_API_KEY}` },
});
console.log(await res.json());
<?php
$ch = curl_init("https://api.circassian.ai/v1/me");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("CIRCASSIAN_API_KEY")],
]);
echo curl_exec($ch);
req, _ := http.NewRequest("GET", "https://api.circassian.ai/v1/me", nil)
req.Header.Set("Authorization", "Bearer "+os.Getenv("CIRCASSIAN_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
io.Copy(os.Stdout, resp.Body)
В ответе — имя ключа, его лимиты и признак того, сохраняется ли содержимое ваших запросов:
{
"name": "Мой продукт",
"prefix": "ck_a1b2c3d4e5f6",
"limits": {
"requests_per_minute": 60,
"requests_per_day": 5000,
"translate_chars_per_day": 500000,
"speech_seconds_per_day": 3600
},
"content_logging": true
}Ключ доступа
Ключ передаётся в каждом запросе заголовком Authorization:
Authorization: Bearer ck_a1b2c3d4e5f6.LmNvbnRlbnQtc2VjcmV0LXZhbHVlКлюч состоит из двух частей через точку: публичного префикса и секрета. Секрет показывается один раз при выдаче и нигде больше не отображается — сохраните его сразу.
Перевод текста
Языки: ru, en, tr, kbd (кабардинский), ady (адыгейский). Поле source_lang можно не указывать — язык определится сам, но с ним ответ приходит быстрее.
curl https://api.circassian.ai/v1/translate \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"text": "Привет, как дела?",
"source_lang": "ru",
"target_lang": "kbd"
}'
import os, httpx
API = "https://api.circassian.ai"
KEY = os.environ["CIRCASSIAN_API_KEY"]
def translate(text: str, target: str, source: str | None = None) -> str:
payload = {"text": text, "target_lang": target}
if source:
payload["source_lang"] = source
r = httpx.post(
f"{API}/v1/translate",
headers={"Authorization": f"Bearer {KEY}"},
json=payload,
timeout=60,
)
if r.status_code != 200:
raise RuntimeError(r.json()["error"]["message"])
return r.json()["text"]
print(translate("Привет, как дела?", "kbd", "ru"))
const API = "https://api.circassian.ai";
const KEY = process.env.CIRCASSIAN_API_KEY;
async function translate(text, targetLang, sourceLang) {
const res = await fetch(`${API}/v1/translate`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text,
target_lang: targetLang,
...(sourceLang && { source_lang: sourceLang }),
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
return data.text;
}
console.log(await translate("Привет, как дела?", "kbd", "ru"));
<?php
function translate(string $text, string $target, ?string $source = null): string
{
$payload = ["text" => $text, "target_lang" => $target];
if ($source !== null) {
$payload["source_lang"] = $source;
}
$ch = curl_init("https://api.circassian.ai/v1/translate");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("CIRCASSIAN_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => 60,
]);
$body = json_decode(curl_exec($ch), true);
if (isset($body["error"])) {
throw new RuntimeException($body["error"]["message"]);
}
return $body["text"];
}
echo translate("Привет, как дела?", "kbd", "ru");
type translateRequest struct {
Text string `json:"text"`
SourceLang string `json:"source_lang,omitempty"`
TargetLang string `json:"target_lang"`
}
type translateResponse struct {
Text string `json:"text"`
SourceLang string `json:"source_lang"`
TargetLang string `json:"target_lang"`
}
func Translate(text, source, target string) (string, error) {
body, _ := json.Marshal(translateRequest{text, source, target})
req, _ := http.NewRequest("POST",
"https://api.circassian.ai/v1/translate", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("CIRCASSIAN_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := (&http.Client{Timeout: 60 * time.Second}).Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("api: %s", resp.Status)
}
var out translateResponse
json.NewDecoder(resp.Body).Decode(&out)
return out.Text, nil
}
Ответ:
{
"text": "ФӀэхъус, дауэ ущыт?",
"source_lang": "ru",
"target_lang": "kbd"
}I, единица, вертикальная черта — распознаются и заменяются на настоящую палочку перед обращением к модели. Присылайте текст как есть.
Пакетный перевод
До 50 текстов и 20 000 символов суммарно за запрос. Результаты приходят в том же порядке, что и присланные тексты. Один пакет заметно быстрее, чем 50 отдельных запросов, и экономит лимит по частоте.
curl https://api.circassian.ai/v1/translate/batch \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"texts": ["Доброе утро", "Спасибо", "До свидания"],
"source_lang": "ru",
"target_lang": "ady"
}'
r = httpx.post(
f"{API}/v1/translate/batch",
headers={"Authorization": f"Bearer {KEY}"},
json={
"texts": ["Доброе утро", "Спасибо", "До свидания"],
"source_lang": "ru",
"target_lang": "ady",
},
timeout=120,
)
for source, result in zip(r.json()["translations"], ["Доброе утро", "Спасибо"]):
print(result, "→", source)
const res = await fetch(`${API}/v1/translate/batch`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
texts: ["Доброе утро", "Спасибо", "До свидания"],
source_lang: "ru",
target_lang: "ady",
}),
});
const { translations } = await res.json();
translations.forEach((t) => console.log(t));
<?php
$payload = [
"texts" => ["Доброе утро", "Спасибо", "До свидания"],
"source_lang" => "ru",
"target_lang" => "ady",
];
$ch = curl_init("https://api.circassian.ai/v1/translate/batch");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("CIRCASSIAN_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
]);
$body = json_decode(curl_exec($ch), true);
foreach ($body["translations"] as $line) {
echo $line, PHP_EOL;
}
payload := map[string]any{
"texts": []string{"Доброе утро", "Спасибо", "До свидания"},
"source_lang": "ru",
"target_lang": "ady",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST",
"https://api.circassian.ai/v1/translate/batch", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("CIRCASSIAN_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := (&http.Client{Timeout: 120 * time.Second}).Do(req)
defer resp.Body.Close()
var out struct {
Translations []string `json:"translations"`
}
json.NewDecoder(resp.Body).Decode(&out)
fmt.Println(out.Translations)
Словарь
Словарные значения одного слова. В отличие от перевода, не расходует квоту символов — считается как обычный запрос.
curl https://api.circassian.ai/v1/dictionary \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"word": "вода", "lang": "ru", "dialect": "kbd"}'Определение языка
Определяет язык текста, ничего не переводя. Полезно, когда нужно понять, на каком языке пишет пользователь, до того как решать, что с этим делать.
curl https://api.circassian.ai/v1/detect \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Сэлам алейкум"}'
# {"language": "kbd"}Синтез речи
Голоса: male и female — те же, что звучат в боте и на сайте. Языки синтеза: kbd, ady, ru, en.
"language": "tr" будет отклонён с ошибкой invalid_request.
Короткий текст (до 1500 символов) озвучивается сразу: приходит файл audio/wav, а длительность — в заголовке X-Audio-Duration.
curl https://api.circassian.ai/v1/speech \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "Сэлам, узыншэу ущыт", "language": "kbd", "voice": "female"}' \
--output speech.wav
r = httpx.post(
f"{API}/v1/speech",
headers={"Authorization": f"Bearer {KEY}"},
json={"text": "Сэлам, узыншэу ущыт", "language": "kbd", "voice": "female"},
timeout=120,
)
r.raise_for_status()
with open("speech.wav", "wb") as f:
f.write(r.content)
print("длительность:", r.headers["X-Audio-Duration"], "с")
import { writeFile } from "node:fs/promises";
const res = await fetch(`${API}/v1/speech`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "Сэлам, узыншэу ущыт",
language: "kbd",
voice: "female",
}),
});
if (!res.ok) throw new Error((await res.json()).error.message);
await writeFile("speech.wav", Buffer.from(await res.arrayBuffer()));
console.log("длительность:", res.headers.get("X-Audio-Duration"), "с");
<?php
$payload = [
"text" => "Сэлам, узыншэу ущыт",
"language" => "kbd",
"voice" => "female",
];
$ch = curl_init("https://api.circassian.ai/v1/speech");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("CIRCASSIAN_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE),
CURLOPT_TIMEOUT => 120,
]);
file_put_contents("speech.wav", curl_exec($ch));
payload := map[string]string{
"text": "Сэлам, узыншэу ущыт",
"language": "kbd",
"voice": "female",
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST",
"https://api.circassian.ai/v1/speech", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("CIRCASSIAN_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := (&http.Client{Timeout: 120 * time.Second}).Do(req)
defer resp.Body.Close()
audio, _ := io.ReadAll(resp.Body)
os.WriteFile("speech.wav", audio, 0o644)
fmt.Println("длительность:", resp.Header.Get("X-Audio-Duration"), "с")
Свой голос вместо готового
Два дополнительных способа задать голос:
| Поле | Что делает |
|---|---|
instruct | Описание голоса словами по-английски: "calm elderly man, slow". Заменяет выбор voice. |
ref_audio_url | Ссылка на образец голоса — синтез повторит его тембр. Файл используется только во время генерации и не сохраняется. |
speed | Скорость речи, от 0.2 до 2.0. По умолчанию 0.85. |
Длинный текст: задания
Текст длиннее 1500 символов не озвучивается в одном запросе — соединение не переживёт долгую генерацию. Вместо аудио приходит идентификатор задания, а результат забирается отдельно.
Ответ на длинный текст:
{
"job_id": "5960d90303ab4626acccf69d980d1593",
"status": "queued",
"poll_url": "/v1/jobs/5960d90303ab4626acccf69d980d1593"
}Состояния задания: queued → running → done либо failed. Когда задание готово, в ответе появляются ссылка на аудио и длительность. Ссылка живёт час, потом файл удаляется.
Готовый клиент, который сам разбирается, вернулся ли файл сразу или пришло задание:
import os, time, httpx
API = "https://api.circassian.ai"
KEY = os.environ["CIRCASSIAN_API_KEY"]
AUTH = {"Authorization": f"Bearer {KEY}"}
def synthesize(text: str, language: str = "kbd", voice: str = "male") -> bytes:
"""Возвращает wav независимо от длины текста."""
r = httpx.post(
f"{API}/v1/speech",
headers=AUTH,
json={"text": text, "language": language, "voice": voice},
timeout=120,
)
if r.status_code != 200:
raise RuntimeError(r.json()["error"]["message"])
# Короткий текст — аудио пришло сразу.
if r.headers["content-type"].startswith("audio/"):
return r.content
# Длинный текст — ждём задание.
job_id = r.json()["job_id"]
while True:
time.sleep(2)
job = httpx.get(f"{API}/v1/jobs/{job_id}", headers=AUTH, timeout=30).json()
if job["status"] == "done":
audio = httpx.get(f"{API}{job['audio_url']}", headers=AUTH, timeout=60)
return audio.content
if job["status"] == "failed":
raise RuntimeError(job["error"])
with open("long.wav", "wb") as f:
f.write(synthesize("Длинный текст…" * 200))
const API = "https://api.circassian.ai";
const auth = { Authorization: `Bearer ${process.env.CIRCASSIAN_API_KEY}` };
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function synthesize(text, language = "kbd", voice = "male") {
const res = await fetch(`${API}/v1/speech`, {
method: "POST",
headers: { ...auth, "Content-Type": "application/json" },
body: JSON.stringify({ text, language, voice }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
// Короткий текст — аудио пришло сразу.
if (res.headers.get("content-type").startsWith("audio/")) {
return Buffer.from(await res.arrayBuffer());
}
// Длинный текст — ждём задание.
const { job_id } = await res.json();
for (;;) {
await sleep(2000);
const job = await (
await fetch(`${API}/v1/jobs/${job_id}`, { headers: auth })
).json();
if (job.status === "done") {
const audio = await fetch(`${API}${job.audio_url}`, { headers: auth });
return Buffer.from(await audio.arrayBuffer());
}
if (job.status === "failed") throw new Error(job.error);
}
}
# 1. Отправляем длинный текст — получаем задание
JOB=$(curl -s https://api.circassian.ai/v1/speech \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text": "…длинный текст…", "language": "kbd"}' \
| jq -r .job_id)
# 2. Ждём готовности
while true; do
STATUS=$(curl -s "https://api.circassian.ai/v1/jobs/$JOB" \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY")
STATE=$(echo "$STATUS" | jq -r .status)
[ "$STATE" = "done" ] && break
[ "$STATE" = "failed" ] && echo "$STATUS" | jq -r .error && exit 1
sleep 2
done
# 3. Забираем аудио
URL=$(echo "$STATUS" | jq -r .audio_url)
curl -s "https://api.circassian.ai$URL" \
-H "Authorization: Bearer $CIRCASSIAN_API_KEY" --output long.wav
Голоса и языки
Списки лучше запрашивать, чем зашивать в код — они пополняются.
curl https://api.circassian.ai/v1/voices -H "Authorization: Bearer $CIRCASSIAN_API_KEY"
{
"voices": [
{"id": "male", "title": "Male"},
{"id": "female", "title": "Female"}
],
"languages": [
{"id": "kbd", "title": "Kabardian"},
{"id": "ady", "title": "Adyghe"},
{"id": "ru", "title": "Russian"},
{"id": "en", "title": "English"}
]
}Лимиты
У каждого ключа четыре независимых ограничения. Текущие значения — в GET /v1/me.
| Ограничение | Что считает | Что приходит при превышении |
|---|---|---|
| Запросов в минуту | Любые обращения, включая неудачные | rate_limited, заголовок Retry-After: 60 |
| Запросов в сутки | То же, за календарные сутки UTC | quota_exceeded |
| Символов перевода в сутки | Символы входа и выхода | quota_exceeded |
| Секунд синтеза в сутки | Длительность полученного аудио | quota_exceeded |
Отдельно ограничено число одновременных обращений к моделям. Если свободного места нет дольше положенного, приходит queue_timeout — соединение при этом не рвётся, запрос просто можно повторить. Синтез выполняется по одной задаче за раз, поэтому в час пик длинные тексты лучше отправлять заданиями, а не ждать.
Ошибки
Формат ответа при любой ошибке одинаков — разбирайте error.type, а не текст сообщения:
{
"error": {
"type": "quota_exceeded",
"message": "daily quota of 500000 characters is spent (499880 used), it resets in about 7h"
}
}type машиночитаемо и стабильно — стройте логику на нём. Текст в message написан для разработчика и не предназначен для показа вашим пользователям как есть.
| Тип | Код | Что делать |
|---|---|---|
unauthorized | 401 | Проверить ключ. Возможно, он отозван — запросить новый |
invalid_request | 422 | Исправить запрос: язык, длину, набор полей. Повтор не поможет |
rate_limited | 429 | Подождать Retry-After секунд и повторить |
quota_exceeded | 429 | Дневной лимит исчерпан. Повторять до обновления бесполезно |
queue_timeout | 503 | Модель занята. Повторить через несколько секунд |
upstream_unavailable | 503, 504 | Модель недоступна. Повторить с нарастающей паузой |
upstream_error | 502 | Сбой на стороне модели. Повторить один раз, затем сообщить |
not_found | 404 | Задание или файл не существует, либо срок ссылки истёк |
internal_error | 500 | Ошибка сервиса. Повторить позже и сообщить, если повторяется |
Повтор с нарастающей паузой — только для 429, 502, 503 и 504. Ошибки 4xx кроме 429 повторять бессмысленно, запрос надо исправить.
import time, httpx
RETRIABLE = {429, 502, 503, 504}
def request_with_retry(method: str, url: str, attempts: int = 4, **kwargs):
for attempt in range(attempts):
r = httpx.request(method, url, **kwargs)
if r.status_code not in RETRIABLE:
return r
# Сервис сам говорит, когда повторять — слушаем его.
pause = int(r.headers.get("Retry-After", 2 ** attempt))
if r.json().get("error", {}).get("type") == "quota_exceeded":
break # суточный лимит: ждать до завтра, а не повторять
time.sleep(pause)
return r
const RETRIABLE = new Set([429, 502, 503, 504]);
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function requestWithRetry(url, options, attempts = 4) {
let res;
for (let attempt = 0; attempt < attempts; attempt++) {
res = await fetch(url, options);
if (!RETRIABLE.has(res.status)) return res;
const body = await res.clone().json();
// Суточный лимит: ждать до завтра, а не повторять.
if (body.error?.type === "quota_exceeded") break;
const pause = Number(res.headers.get("Retry-After")) || 2 ** attempt;
await sleep(pause * 1000);
}
return res;
}
Данные
Что сохраняется по каждому обращению всегда: время, ключ, операция, модель, объём в символах или секундах аудио, успех или тип ошибки, адрес обращения. Это нужно для учёта расхода и разбора сбоев.
Содержимое запросов и ответов — исходный текст и результат — хранится 30 дней, после чего удаляется автоматически. Это нужно, чтобы разбирать жалобы на качество перевода и сбои.
Сохранение содержимого отключается для отдельного ключа: напишите владельцу, и тексты по вашему ключу перестанут сохраняться. Учёт расхода при этом продолжит работать — лимиты и счётчики от логов не зависят.
Референсное аудио для клонирования голоса не сохраняется: оно используется во время генерации, в логе остаётся только отметка о факте использования. Сгенерированные файлы удаляются вместе с истечением срока ссылки.
Все операции
| Метод | Путь | Назначение | Ключ |
|---|---|---|---|
POST | /v1/translate | Перевод текста | нужен |
POST | /v1/translate/batch | Перевод пакета текстов | нужен |
POST | /v1/dictionary | Словарные значения слова | нужен |
POST | /v1/detect | Определение языка | нужен |
POST | /v1/speech | Синтез речи | нужен |
GET | /v1/jobs/{job_id} | Состояние задания | нужен |
GET | /v1/audio/{artifact_id} | Готовое аудио | нужен |
GET | /v1/voices | Голоса и языки синтеза | нужен |
GET | /v1/languages | Языки перевода | нужен |
GET | /v1/me | Сведения о ключе и лимитах | нужен |
GET | /health | Состояние сервиса | не нужен |
GET | /openapi.json | Схема OpenAPI | не нужен |
Схема OpenAPI подходит для генерации клиента под ваш язык.