Speech2Text API - Полная документация
Базовый URL
http://localhost:8020
Аутентификация
Все защищенные endpoints требуют JWT токен в заголовке Authorization:
Authorization: Bearer YOUR_JWT_TOKEN
Для программного доступа в том же заголовке передаётся API-ключ:
Authorization: Bearer sk-XXXXXXXXXXXXXXXX...
Ключу можно задать секретное слово и потребовать, чтобы каждый запрос этим ключом был подписан, — см. Подпись HMAC-SHA256. По умолчанию подпись выключена, и ключ работает как раньше.
Содержание
- Аутентификация
- Транскрибация
- Управление пользователями
- API ключи
- Метрики
- Администрирование
- Загрузка по ссылке и обратный вызов
- Подпись HMAC-SHA256
1. Аутентификация
Вход в систему
POST /auth/login
Получение JWT токена для доступа к API.
Параметры запроса:
{
"username": "string",
"password": "string"
}
Ответ:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer"
}
Примеры:
Python:
import requests
url = "http://localhost:8020/auth/login"
data = {
"username": "admin",
"password": "admin123"
}
response = requests.post(url, json=data)
token = response.json()["access_token"]
print(f"Token: {token}")
PHP:
<?php
$url = "http://localhost:8020/auth/login";
$data = array(
"username" => "admin",
"password" => "admin123"
);
$options = array(
'http' => array(
'header' => "Content-type: application/json\r\n",
'method' => 'POST',
'content' => json_encode($data)
)
);
$context = stream_context_create($options);
$result = file_get_contents($url, false, $context);
$response = json_decode($result, true);
$token = $response["access_token"];
echo "Token: " . $token;
?>
JavaScript:
async function login() {
const response = await fetch('http://localhost:8020/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: 'admin123'
})
});
const data = await response.json();
const token = data.access_token;
console.log('Token:', token);
return token;
}
cURL:
curl -X POST "http://localhost:8020/auth/login" \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"admin123"}'
Регистрация пользователя
POST /auth/register
Создание нового пользователя.
Параметры запроса:
{
"username": "string",
"email": "user@example.com",
"password": "string"
}
Ответ:
{
"id": "uuid",
"username": "string",
"email": "user@example.com",
"is_active": true,
"is_admin": false,
"created_at": "2024-01-01T00:00:00"
}
Примеры:
Python:
import requests
url = "http://localhost:8020/auth/register"
data = {
"username": "newuser",
"email": "newuser@example.com",
"password": "secure_password123"
}
response = requests.post(url, json=data)
user = response.json()
print(f"Created user: {user['username']} with ID: {user['id']}")
JavaScript:
async function registerUser() {
const response = await fetch('http://localhost:8020/auth/register', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'newuser',
email: 'newuser@example.com',
password: 'secure_password123'
})
});
const user = await response.json();
console.log('Created user:', user);
return user;
}
Получить текущего пользователя
GET /auth/me
Получение информации о текущем авторизованном пользователе.
Заголовки:
Authorization: Bearer YOUR_JWT_TOKEN
Ответ:
{
"id": "uuid",
"username": "string",
"email": "user@example.com",
"is_active": true,
"is_admin": false,
"created_at": "2024-01-01T00:00:00"
}
Примеры:
Python:
import requests
url = "http://localhost:8020/auth/me"
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(url, headers=headers)
user = response.json()
print(f"Current user: {user['username']}")
2. Транскрибация
Что можно загружать: форматы, видео, размер
Принимаются расширения .mp3, .mp4, .wav, .m4a, .ogg, .flac, .mp2, .mov, .mxf,
.webm. Всё, что ниже, одинаково относится и к POST /transcribe/, и к POST /transcribe/async,
и к загрузке по ссылке (url).
Видеофайлы отдавайте как есть. .mov, .mxf, .webm (и .mp4) — это видеоконтейнеры; звук
из них извлекает сам сервис через ffmpeg. Отдельно вырезать звуковую дорожку перед загрузкой не
нужно.
Несколько звуковых дорожек сводятся в одну. Вещательный MXF обычно несёт по отдельной моно-дорожке на микрофон. Сервис смешивает все дорожки (с усреднением громкости), а не выбирает первую попавшуюся, — иначе целый микрофон, то есть целый говорящий, выпал бы из расшифровки. Одинаковые дорожки при сведении звучат ровно как исходная, перегрузки по громкости не возникает. Выбрать конкретную дорожку через API нельзя.
Размер. Лимит — MAX_FILE_SIZE_MB, по умолчанию 5000 МБ (раньше 500). Видеоконтейнер с
материалом целиком — это гигабайты, тогда как звук в нём занимает проценты; раз извлечением
занимается сервис, лимит пропускает исходник. Тем же лимитом ограничена и загрузка по ссылке.
Превышение — 413.
Файл без звука. Если в файле нет ни одной звуковой дорожки, задача завершается со статусом
failed, а в error_message приходит: «В файле нет звуковой дорожки — распознавать нечего.»
Транскрибировать аудио
POST /transcribe/
Загрузка и транскрибация аудио- или видеофайла.
Параметры:
- file (form-data): Аудио- или видеофайл (MP3, MP4, WAV, M4A, OGG, FLAC, MP2, MOV, MXF, WEBM — см. «Что можно загружать»). Взаимоисключающе с
url: нужно задать ровно один источник - url (form-data, optional): Ссылка
http(s)на аудио — сервис скачает файл сам. Подробности и ошибки скачивания: раздел 7 - callback_url (form-data, optional): Адрес
http(s), на который придётPOSTс готовой транскрипцией. Подробности: раздел 7 - language (optional): Код языка (ru, en, es, etc.) или пустой для автоопределения
- task (optional): "transcribe" или "translate" (по умолчанию "transcribe")
- return_segments (optional): true/false для получения временных сегментов
- return_timestamps (optional): true/false для получения пословных меток времени (word-level timestamps)
- diarize (optional): true/false — разделение по собеседникам (диаризация). Каждый сегмент получает метку
speaker, в ответ добавляютсяdiarization.speakersи готовый диалоговый текстdiarization.text_with_speakers - num_speakers (optional): точное число собеседников, если известно (например, 2 для телефонного опроса) — повышает стабильность разметки
- min_speakers / max_speakers (optional): границы для автоматического определения числа собеседников
Ответ:
{
"id": "uuid",
"filename": "audio.mp3",
"text": "Транскрибированный текст...",
"language": "ru",
"duration": 120.5,
"processing_time": 15.3,
"status": "completed",
"created_at": "2024-01-01T00:00:00",
"segments": [
{
"id": 0,
"start": 0.0,
"end": 5.2,
"text": "Первый сегмент текста",
"speaker": "SPEAKER_01", // Только если diarize=true
"words": [ // Только если return_timestamps=true
{
"word": "Первый",
"start": 0.0,
"end": 0.8,
"probability": 0.95
},
{
"word": "сегмент",
"start": 0.8,
"end": 1.5,
"probability": 0.98
}
]
}
],
"has_diarization": true, // Только если diarize=true
"diarization": { // Только если diarize=true
"num_speakers": 2,
"speakers": [
{"id": "SPEAKER_01", "label": "Собеседник 1", "talk_time": 75.4, "segments": 12},
{"id": "SPEAKER_02", "label": "Собеседник 2", "talk_time": 44.1, "segments": 11}
],
"text_with_speakers": "Собеседник 1: Здравствуйте!...\n\nСобеседник 2: Добрый день..."
}
}
В ответе (и в GET /transcriptions/{id}, и в теле обратного вызова) присутствуют ещё поля источника
и доставки — они заполняются, только если запись пришла по ссылке и/или был заказан callback:
| Поле | Значение |
|---|---|
source_url |
Ссылка, с которой скачано аудио, с вырезанными секретами (null для загрузки файлом) |
callback_url |
Куда доставляется результат (null, если вызов не заказан) |
callback_status |
null · pending · sending · delivered · failed |
callback_attempts |
Сколько попыток доставки сделано |
callback_error |
Причина последней неудачи |
callback_delivered_at |
Когда доставлено (UTC) |
Пример с диаризацией (разделение собеседников):
data = {
"language": "ru",
"diarize": "true", # Включить разделение по собеседникам
"num_speakers": "2" # Опционально: точное число собеседников
}
response = requests.post(url, headers=headers, files=files, data=data)
result = response.json()
print(result["diarization"]["text_with_speakers"]) # Готовый диалог
for seg in result["segments"]:
print(f"[{seg.get('speaker')}] {seg['start']:.1f}-{seg['end']:.1f}: {seg['text']}")
Примеры:
Python:
import requests
url = "http://localhost:8020/transcribe/"
headers = {
"Authorization": f"Bearer {token}"
}
with open("audio.mp3", "rb") as audio_file:
files = {"file": audio_file}
data = {
"language": "ru",
"return_segments": "true",
"return_timestamps": "true" # Включить пословные метки времени
}
response = requests.post(url, headers=headers, files=files, data=data)
result = response.json()
print(f"Transcription ID: {result['id']}")
print(f"Text: {result['text'][:100]}...")
print(f"Duration: {result['duration']} seconds")
# Обработка меток времени
if "segments" in result:
for segment in result["segments"]:
print(f"\nSegment {segment['id']}: {segment['start']:.1f}s - {segment['end']:.1f}s")
if "words" in segment:
for word in segment["words"]:
print(f" {word['word']}: {word['start']:.1f}s - {word['end']:.1f}s")
PHP:
<?php
$url = "http://localhost:8020/transcribe/";
$token = "YOUR_JWT_TOKEN";
$file = new CURLFile('audio.mp3');
$data = array(
'file' => $file,
'language' => 'ru',
'return_segments' => 'false'
);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Authorization: Bearer " . $token
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$result = json_decode($response, true);
echo "Text: " . $result['text'];
?>
JavaScript:
async function transcribeAudio(file, token) {
const formData = new FormData();
formData.append('file', file);
formData.append('language', 'ru');
formData.append('return_segments', 'false');
const response = await fetch('http://localhost:8020/transcribe/', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`
},
body: formData
});
const result = await response.json();
console.log('Transcription:', result.text);
return result;
}
// Использование с input файла
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
await transcribeAudio(file, token);
});
cURL:
curl -X POST "http://localhost:8020/transcribe/" \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-F "file=@audio.mp3" \
-F "language=ru" \
-F "return_segments=false"
⏳ Асинхронная транскрибация (очередь)
POST /transcribe/async
Ставит запись в очередь и сразу возвращает задачу со статусом pending, не удерживая
соединение. Параметры — те же, что у /transcribe/, включая url и callback_url.
Именно здесь пара url + callback_url работает как задумано: клиент отдаёт ссылку на запись
и адрес приёмника, получает id и больше ничего не опрашивает — готовый текст приедет сам
(см. раздел 7).
Все загрузки (и синхронные, и асинхронные) проходят через одну персистентную очередь: задача хранится в базе вместе с параметрами запуска, поэтому перезапуск или отключение питания её не теряет — при следующем старте сервер сам возвращает прерванные задачи в очередь и доводит их до конца. Одновременно обрабатывается одна запись, остальные ждут своей очереди.
Ответ (202 Accepted):
{
"id": "uuid",
"filename": "audio.mp3",
"status": "pending",
"text": null,
"created_at": "2026-07-27T17:31:17"
}
Жизненный цикл задачи:
pending → processing → completed | failed | cancelled
Прогресс и позицию в очереди отдаёт GET /transcriptions/{id}/status, готовый результат —
GET /transcriptions/{id}.
Пример:
import requests, time
headers = {"Authorization": f"Bearer {token}"}
with open("audio.mp3", "rb") as f:
job = requests.post(
"http://localhost:8020/transcribe/async",
headers=headers,
files={"file": f},
data={"language": "ru", "return_timestamps": True},
).json()
while True:
state = requests.get(
f"http://localhost:8020/transcriptions/{job['id']}/status", headers=headers
).json()
if state["status"] in ("completed", "failed", "cancelled"):
break
print(f"{state['status']}: {state['progress']}%, в очереди перед вами: {state['queue_position']}")
time.sleep(5)
result = requests.get(
f"http://localhost:8020/transcriptions/{job['id']}", headers=headers
).json()
print(result["text"])
Синхронный
POST /transcribe/работает как раньше и возвращает готовый текст, но если обработка не уложилась вQUEUE_SYNC_WAIT_SECONDS(по умолчанию час), он ответит504с id задачи — задача при этом не теряется и продолжает выполняться.
История транскрибаций
GET /transcribe/history
Получение списка всех транскрибаций пользователя.
Параметры:
- skip (optional): Количество записей для пропуска (пагинация)
- limit (optional): Максимальное количество записей (по умолчанию 100)
Ответ:
[
{
"id": "uuid",
"filename": "audio.mp3",
"status": "completed",
"duration": 120.5,
"language": "ru",
"created_at": "2024-01-01T00:00:00"
}
]
Примеры:
Python:
import requests
url = "http://localhost:8020/transcribe/history"
headers = {
"Authorization": f"Bearer {token}"
}
params = {
"skip": 0,
"limit": 50
}
response = requests.get(url, headers=headers, params=params)
history = response.json()
for item in history:
print(f"{item['filename']} - {item['status']} - {item['created_at']}")
Получить транскрибацию
GET /transcribe/{transcription_id}
Получение полной информации о конкретной транскрибации.
Ответ:
{
"id": "uuid",
"filename": "audio.mp3",
"text": "Полный текст транскрибации...",
"language": "ru",
"duration": 120.5,
"processing_time": 15.3,
"status": "completed",
"created_at": "2024-01-01T00:00:00",
"segments": []
}
Стриминг аудио
GET /transcriptions/{transcription_id}/audio
Получение аудиофайла для воспроизведения.
Примеры:
Python:
import requests
url = f"http://localhost:8020/transcriptions/{transcription_id}/audio"
headers = {
"Authorization": f"Bearer {token}"
}
response = requests.get(url, headers=headers, stream=True)
# Сохранить аудио
with open("downloaded_audio.mp3", "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
JavaScript:
async function playAudio(transcriptionId, token) {
const response = await fetch(
`http://localhost:8020/transcriptions/${transcriptionId}/audio`,
{
headers: {
'Authorization': `Bearer ${token}`
}
}
);
const blob = await response.blob();
const audioUrl = URL.createObjectURL(blob);
const audio = new Audio(audioUrl);
audio.play();
}
Статус транскрибации
GET /transcriptions/{transcription_id}/status
Проверка статуса обработки транскрибации.
Ответ:
{
"id": "uuid",
"status": "processing",
"progress": 42,
"queue_position": null,
"attempts": 1,
"filename": "audio.mp3",
"created_at": "2024-01-01T00:00:00",
"queued_at": "2024-01-01T00:00:00",
"started_at": "2024-01-01T00:00:05",
"completed_at": null,
"duration": null,
"processing_time": null,
"error_message": null
}
- status:
pending(ждёт в очереди),processing,completed,failed,cancelled - queue_position: место в очереди для
pending, иначеnull - attempts: сколько раз задачу брали в работу (растёт, если её прервал перезапуск сервера)
- error_message: причина при
failed. Частый случай для видео — «В файле нет звуковой дорожки — распознавать нечего.»: контейнер приняли, но распознавать в нём нечего
Примеры:
Python:
import requests
import time
def wait_for_transcription(transcription_id, token):
url = f"http://localhost:8020/transcriptions/{transcription_id}/status"
headers = {"Authorization": f"Bearer {token}"}
while True:
response = requests.get(url, headers=headers)
status = response.json()
if status['status'] == 'completed':
print("Transcription completed!")
return True
elif status['status'] == 'failed':
print(f"Transcription failed: {status['error_message']}")
return False
print(f"Status: {status['status']}... waiting")
time.sleep(2)
Скачать транскрибацию
GET /transcriptions/{transcription_id}/download
Скачивание текста транскрибации в различных форматах.
Параметры:
- format: "txt" или "srt" (по умолчанию "txt")
Примеры:
Python:
import requests
# Скачать как текст
url = f"http://localhost:8020/transcriptions/{transcription_id}/download?format=txt"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
with open("transcription.txt", "w", encoding="utf-8") as f:
f.write(response.text)
# Скачать как субтитры SRT
url = f"http://localhost:8020/transcriptions/{transcription_id}/download?format=srt"
response = requests.get(url, headers=headers)
with open("transcription.srt", "w", encoding="utf-8") as f:
f.write(response.text)
3. Управление пользователями
Список пользователей (Admin)
GET /admin/users
Получение списка всех пользователей (требуются права администратора).
Ответ:
[
{
"id": "uuid",
"username": "user1",
"email": "user1@example.com",
"is_active": true,
"is_admin": false,
"created_at": "2024-01-01T00:00:00"
}
]
Обновить пользователя (Admin)
PATCH /admin/users/{user_id}
Обновление данных пользователя.
Параметры:
{
"username": "new_username",
"email": "new@example.com",
"password": "new_password",
"is_active": true,
"is_admin": false
}
Примеры:
Python:
import requests
url = f"http://localhost:8020/admin/users/{user_id}"
headers = {
"Authorization": f"Bearer {admin_token}",
"Content-Type": "application/json"
}
data = {
"is_active": False,
"is_admin": True
}
response = requests.patch(url, headers=headers, json=data)
updated_user = response.json()
print(f"Updated user: {updated_user}")
Удалить пользователя (Admin)
DELETE /admin/users/{user_id}
Удаление пользователя из системы.
Примеры:
JavaScript:
async function deleteUser(userId, token) {
const response = await fetch(`http://localhost:8020/admin/users/${userId}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
console.log('User deleted successfully');
} else {
console.error('Failed to delete user');
}
}
4. API Ключи
Создать API ключ
POST /api-keys/
Создание нового API ключа для программного доступа.
Параметры:
{
"name": "My Application",
"permissions": ["transcribe", "read"],
"rate_limit": 100,
"expires_in_days": 365,
"webhook_secret": null,
"generate_webhook_secret": false,
"sign_callbacks": false,
"require_request_signature": false
}
| Поле подписи | По умолчанию | Описание |
|---|---|---|
webhook_secret |
null |
Своё секретное слово, от 8 до 256 символов (иначе 422) |
generate_webhook_secret |
false |
Пусть секрет придумает сервер: whsec_ + 43 случайных символа |
sign_callbacks |
false |
Подписывать исходящие вызовы на callback_url |
require_request_signature |
false |
Требовать подпись у входящих запросов этим ключом |
Задать одновременно webhook_secret и generate_webhook_secret нельзя, включить любой из
выключателей без секретного слова — тоже (400, тексты — в разделе 8).
Ответ:
{
"id": "uuid",
"name": "My Application",
"api_key": "sk-abcdef123456789...",
"key_prefix": "sk-abcdef1...",
"is_active": true,
"permissions": ["transcribe", "read"],
"rate_limit": 100,
"created_at": "2024-01-01T00:00:00",
"last_used_at": null,
"expires_at": "2025-01-01T00:00:00",
"sign_callbacks": false,
"require_request_signature": false,
"webhook_secret_set": false,
"webhook_secret_hint": null,
"webhook_secret": null
}
Важно: Полный ключ (api_key) показывается только один раз при создании!
Поле webhook_secret заполняется, только если секрет был задан или сгенерирован этим же
вызовом: его надо увидеть один раз, чтобы передать интеграции. В списке ключей его больше не будет —
там видно лишь webhook_secret_set (есть ли секрет) и маску webhook_secret_hint
(whsec_••••••••). Полное значение отдаёт отдельная ручка
GET /api-keys/{key_id}/webhook-secret.
Примеры:
Python:
import requests
url = "http://localhost:8020/api-keys/"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
data = {
"name": "Python Client",
"permissions": ["transcribe"],
"rate_limit": 1000,
"expires_in_days": 90
}
response = requests.post(url, headers=headers, json=data)
api_key = response.json()
print(f"API Key created: {api_key['key']}")
print("Save this key securely! It won't be shown again.")
# Использование API ключа вместо JWT токена
api_headers = {
"X-API-Key": api_key['key']
}
Список API ключей
GET /api-keys/
Получение списка всех API ключей пользователя.
Ответ:
[
{
"id": "uuid",
"name": "My Application",
"key_prefix": "sk-abcdef1...",
"is_active": true,
"permissions": ["transcribe", "read"],
"rate_limit": 100,
"created_at": "2024-01-01T00:00:00",
"last_used_at": "2024-01-15T12:00:00",
"expires_at": "2025-01-01T00:00:00",
"sign_callbacks": true,
"require_request_signature": false,
"webhook_secret_set": true,
"webhook_secret_hint": "whsec_••••••••"
}
]
Сам секрет в список не попадает никогда — только признак его наличия и маска.
Изменить ключ (имя и настройки подписи)
PATCH /api-keys/{key_id}
Меняет только переданные поля; чего нет в теле — остаётся как было. Доступны только собственные
ключи пользователя, иначе 404 {"detail": "API-ключ не найден"}.
Параметры:
{
"name": "Production key",
"webhook_secret": "my-shared-secret",
"generate_webhook_secret": false,
"sign_callbacks": true,
"require_request_signature": false
}
| Поле | Значение |
|---|---|
name |
Новое имя. null — не менять |
webhook_secret |
Новое секретное слово. null — не менять, "" — удалить секрет |
generate_webhook_secret |
true — сервер сам придумает новый секрет и вернёт его в ответе |
sign_callbacks |
true/false — включить/выключить подпись callback. null — не менять |
require_request_signature |
true/false — требовать подпись входящих. null — не менять |
Ответ:
Запись ключа (APIKeyFullResponse, то есть с полем api_key) с актуальными sign_callbacks,
require_request_signature, webhook_secret_set, webhook_secret_hint.
Поле webhook_secret заполняется только при
generate_webhook_secret: true — своё слово вызывающий и так знает, а сгенерированное больше
нигде не показать.
Ошибки 400:
| Текст | Когда |
|---|---|
Укажите либо своё секретное слово, либо генерацию — но не оба сразу |
Переданы и webhook_secret, и generate_webhook_secret: true |
Включить подпись нельзя: у ключа не задано секретное слово |
Итоговое состояние — выключатель включён, секрета нет |
Сначала выключите подпись — без секретного слова она не работает |
webhook_secret: "" при хотя бы одном включённом выключателе |
Чтобы удалить секрет, гасите подпись тем же запросом:
curl -X PATCH "http://localhost:8020/api-keys/$KEY_ID" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"webhook_secret":"","sign_callbacks":false,"require_request_signature":false}'
Показать секретное слово
GET /api-keys/{key_id}/webhook-secret
Возвращает секрет полностью — его нужно как-то передать той стороне, которая будет
проверять подписи. Только для своих ключей (иначе 404). Факт раскрытия попадает в лог сервера
(кто и у какого ключа смотрел; само значение в лог не пишется).
Ответ:
{
"id": "uuid",
"webhook_secret": "whsec_2Vd1kZ0oQ7pR..."
}
Если секрет не задан — "webhook_secret": null.
5. Метрики
Системные метрики
GET /metrics/system
Получение метрик системы и статистики использования.
Ответ:
{
"cpu_usage": 45.2,
"memory_usage": 62.8,
"disk_usage": 35.1,
"gpu_available": true,
"gpu_memory_used": "8.5 GB",
"whisper_model": "large-v3",
"active_users": 12,
"total_transcriptions_24h": 156,
"average_processing_time": 12.3,
"uptime_seconds": 86400
}
Примеры:
Python:
import requests
url = "http://localhost:8020/metrics/system"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get(url, headers=headers)
metrics = response.json()
print(f"CPU Usage: {metrics['cpu_usage']}%")
print(f"GPU Available: {metrics['gpu_available']}")
print(f"Model: {metrics['whisper_model']}")
print(f"Transcriptions (24h): {metrics['total_transcriptions_24h']}")
Пользовательские метрики
GET /metrics/user
Получение статистики использования для текущего пользователя.
Ответ:
{
"total_transcriptions": 523,
"total_duration_seconds": 125840.5,
"total_characters": 2456789,
"total_api_calls": 1523,
"monthly_transcriptions": 45,
"monthly_duration_seconds": 10234.2,
"monthly_characters": 234567,
"monthly_api_calls": 156,
"last_reset_date": "2024-01-01T00:00:00"
}
6. Администрирование
Все транскрибации (Admin)
GET /admin/transcriptions
Получение списка всех транскрибаций в системе.
Ответ:
[
{
"id": "uuid",
"filename": "audio.mp3",
"status": "completed",
"duration": 120.5,
"language": "ru",
"created_at": "2024-01-01T00:00:00",
"user_id": "uuid",
"username": "user1"
}
]
7. Загрузка по ссылке и обратный вызов
Оба эндпоинта транскрибации — POST /transcribe/ и POST /transcribe/async — принимают два
дополнительных поля формы: url (откуда взять аудио) и callback_url (куда отдать результат).
Вместе они закрывают сценарий «отдал ссылку — забыл»: клиенту не нужно ни держать соединение с
файлом на сотни мегабайт, ни опрашивать статус.
Ссылка вместо файла (url)
curl -X POST "http://localhost:8020/transcribe/async" \
-H "Authorization: Bearer sk-XXXXXXXX..." \
-F "url=https://example.com/records/call-2026-09-03.mp3" \
-F "language=ru"
Правила:
- Источник должен быть ровно один — либо
file, либоurl. Пустая строка вurlи часть формы с пустым именем файла считаются «не задано», так что клиент может слать оба поля всегда. Если задано и то и другое (или ничего) —400:Укажите ровно один источник: файл (file) или ссылку (url). - Схема — только
http://илиhttps://. - Скачивание идёт внутри этого запроса, потоком: недоступная ссылка — это ошибка сразу, а не
упавшая через десять минут задача. Файл кладётся в
uploads/temp, как и обычная загрузка. - Лимит размера тот же, что у загрузки файлом —
MAX_FILE_SIZE_MB(по умолчанию 5000 МБ; поднят с 500 МБ ради видеоконтейнеров, которые весят гигабайты). Он проверяется и по заявленномуContent-Length, и по мере прихода байтов: соврать в заголовке не поможет. - Имя файла берётся из
Content-Disposition, иначе из последнего сегмента пути. Расширение проверяется так же строго, как у загруженного файла (.mp3,.mp4,.wav,.m4a,.ogg,.flac,.mp2,.mov,.mxf,.webm); при несовпадении —400с перечислением допустимых, а скачанный файл сразу удаляется. - Ссылка на видео — это нормально:
.mov,.mxf,.webmи.mp4скачиваются целиком, а звук из них извлекает сервис (несколько дорожек при этом сводятся в одну — см. «Что можно загружать»). FETCH_TIMEOUT_SEC(по умолчанию 120 с) — абсолютный дедлайн на всё скачивание, а не таймаут одного чтения: «капающий» источник тоже обрывается.
Ошибки скачивания:
| Код | Когда |
|---|---|
400 |
Не та схема, битый URL, 4xx от источника, пустой ответ, неподдерживаемое расширение |
403 |
Адрес запрещён политикой хостов (см. предупреждение ниже) |
413 |
Аудио больше MAX_FILE_SIZE_MB |
502 |
Источник недоступен или ответил 5xx |
504 |
Источник не уложился в FETCH_TIMEOUT_SEC |
⚠️ Внутренняя сеть. По умолчанию
FETCH_ALLOW_PRIVATE_HOSTS=false: адреса из приватных, loopback- и link-local-диапазонов запрещены и как источник (url), и как приёмник (callback_url). Проверяется каждый редирект, а имя резолвится один раз, и соединение идёт именно по проверенному адресу — иначе смена DNS между проверкой и подключением обходила бы политику. ВключённыйFETCH_ALLOW_PRIVATE_HOSTS=trueпревращает любой API-ключ в HTTP-прокси внутрь вашего периметра: держатель ключа сможет заставить сервис ходить по внутренним адресам. Включайте, только если держатели ключей и так имеют доступ в эту сеть — например, когда записи лежат на файловом сервере в LAN.
source_url сохраняется и отдаётся с вырезанными секретами: ?X-Amz-Signature=…, ?token=…,
https://user:pass@host/… и подобное заменяется на REDACTED. Presigned-ссылке нечего делать ни
в истории, ни в теле обратного вызова на чужом приёмнике.
Обратный вызов (callback_url)
curl -X POST "http://localhost:8020/transcribe/async" \
-H "Authorization: Bearer sk-XXXXXXXX..." \
-F "url=https://example.com/records/call.mp3" \
-F "callback_url=https://example.com/hooks/asr.php" \
-F "language=ru" \
-F "return_segments=true"
Адрес проверяется сразу при постановке (та же политика хостов, что и для url): опечатка —
это 400 с телом {"detail": "callback_url: ..."} на том запросе, который её сделал, а не
молчаливый провал через десять минут.
Когда задача доходит до конечного состояния — completed, failed или cancelled — сервис
отправляет POST:
POST /hooks/asr.php HTTP/1.1
Content-Type: application/json; charset=utf-8
X-ASR-Task-Id: 9191b72b-23d1-4bc8-b2e9-24075e66a5af
X-ASR-Event: transcription.completed
User-Agent: Speech2Text ASR callback
Тело — тот же JSON, что отдаёт GET /transcriptions/{id} (text, language, duration,
segments, diarization, поля доставки). Приёмник получает его в php://input — в $_POST
его не будет, это JSON, а не форма.
Подпись вызова. Если у API-ключа, создавшего задачу, задано секретное слово и включён
sign_callbacks, к заголовкам добавляются X-Webhook-Timestamp и X-Webhook-Signature —
см. раздел 8. Ключ без секрета или с выключенным sign_callbacks
шлёт вызовы без этих заголовков, как раньше.
Повторы. Доставкой занимается отдельный фоновый поток, который раз в
CALLBACK_POLL_SECONDS (по умолчанию 2 с) забирает готовые задачи с заказанным вызовом.
Ответ 2xx — доставлено. Иначе делается до CALLBACK_RETRIES повторов (по умолчанию 3) с
паузами 2 / 10 / 60 секунд, то есть всего до четырёх попыток; после этого
callback_status становится failed, а причина остаётся в callback_error (из тела ответа
приёмника читается не больше 2 КБ). Таймаут одной попытки — CALLBACK_TIMEOUT_SEC (30 с).
Неудачная доставка никогда не меняет статус самой задачи: аудио распознано, текст лежит в
базе и доступен по GET /transcriptions/{id}, доставка — отдельное дело.
Редиректы не выполняются. Ответ 3xx от приёмника — это неудача с понятным текстом
(callback_url redirected to '…'; redirects are not followed — register the final URL), а не
переход: 301/302 молча превратили бы POST в GET без тела, а 307 отправил бы всю
транскрипцию на хост, который политику хостов не проходил. Регистрируйте конечный адрес.
Адрес проверяется дважды — при постановке и ещё раз непосредственно перед каждой отправкой: между ними проходят минуты, и имя за это время могло быть перенаправлено.
Если сервис перезапустился, пока доставка была в полёте, вызовы, застрявшие в sending,
возвращаются в pending при старте и уходят заново — «в процессе отправки» не остаётся навсегда.
Ход доставки виден в GET /transcriptions/{id}:
| Поле | Значение |
|---|---|
callback_status |
null (вызов не заказан) · pending · sending · delivered · failed |
callback_attempts |
Сколько попыток сделано |
callback_error |
Причина последней неудачи |
callback_delivered_at |
Когда доставлено (UTC) |
Приёмник должен быть готов к повтору уже доставленного вызова (сеть могла оборвать ответ):
идентификатор задачи в X-ASR-Task-Id и поле id в теле — то, по чему дубль опознаётся.
8. Подпись HMAC-SHA256
Помимо Bearer-ключа сервис умеет подписывать трафик по схеме HMAC-SHA256 + Base64. Подпись
настраивается поключево: владелец задаёт секретное слово конкретному API-ключу и независимо
включает два выключателя.
| Выключатель | Поле ключа | Что делает |
|---|---|---|
| Подписывать callback | sign_callbacks |
Сервис добавляет X-Webhook-Timestamp / X-Webhook-Signature к исходящему POST на callback_url, чтобы приёмник мог убедиться: вызов пришёл именно отсюда |
| Требовать подпись запросов | require_request_signature |
Сервис отвергает неподписанные запросы этим ключом: украденный ключ без секретного слова бесполезен |
ℹ️ Оба выключателя по умолчанию выключены и независимы друг от друга. Обновление ничего не меняет для существующих интеграций: пока ключу не задано секретное слово и не включён соответствующий флаг, ни один запрос и ни один callback не требует подписи. Включить подпись, не задав секрет, нельзя — API управления ключами вернёт
400.
Секретное слово
Секрет привязан к API-ключу, а не к сервису целиком: у каждой интеграции он свой, и компрометация одной не затрагивает остальных. Задать его можно двумя способами:
- своё слово — от 8 до 256 символов (длина вне диапазона отклоняется валидацией,
422); - сгенерированное — сервис выдаёт значение вида
whsec_+ 43 случайных символа (generate_webhook_secret: trueили кнопка «Сгенерировать» в админ-панели).
Наружу секрет отдаётся только владельцу ключа: в списке видно лишь webhook_secret_set и маску
webhook_secret_hint (whsec_••••••••), полное значение возвращает
GET /api-keys/{key_id}/webhook-secret. В логи и в тексты ошибок
секрет не попадает никогда.
Секрет хранится в базе в открытом виде — иначе им нельзя было бы считать HMAC: это ключ подписи, а не пароль, и из хеша его не восстановить.
Заголовки
| Заголовок | Направление | Значение |
|---|---|---|
X-Signature-Timestamp |
клиент → сервис | Unix-время в секундах (целое) на момент отправки запроса |
X-Signature |
клиент → сервис | base64(HMAC-SHA256(timestamp + "." + МЕТОД + "." + путь, секрет)) |
X-Webhook-Timestamp |
сервис → приёмник callback | Unix-время в секундах на момент этой попытки доставки |
X-Webhook-Signature |
сервис → приёмник callback | base64(HMAC-SHA256(id задачи + "." + timestamp, секрет)) |
Обе схемы — одна и та же операция: HMAC-SHA256 от строки, ключ — секретное слово в UTF-8, результат — Base64 от сырых байт дайджеста (не от hex-строки). Различаются только подписываемые строки.
Схема 1. Входящий запрос
Подписывается строка
<timestamp>.<МЕТОД>.<путь со строкой запроса>
<timestamp>— ровно то же число, что уходит в заголовкеX-Signature-Timestamp;<МЕТОД>— HTTP-метод заглавными буквами (POST,GET,DELETE);<путь со строкой запроса>— путь запроса и, если строка запроса непуста,?и она же — байт в байт, в том же percent-encoding, в каком запрос реально уходит на сервер.
Пример подписываемой строки для POST /transcribe/async:
1769670760.POST./transcribe/async
и для GET /transcriptions/history?skip=0&limit=50:
1769670760.GET./transcriptions/history?skip=0&limit=50
⚠️ Путь и строка запроса подписываются «как есть». Сервис берёт их из самой строки запроса HTTP, в неразобранном виде; заголовок
Hostна подпись не влияет. URL нужно собрать самому и передать библиотеке готовым: если отдать параметры отдельным словарём (params=вrequests,http_build_queryв PHP), библиотека может закодировать их иначе, чем вы при подписи, — и подпись не сойдётся, хотя адрес выглядит тем же.
Проверка выполняется там же, где проверяется API-ключ: после проверки is_active и срока
действия и до обновления last_used_at. Запрос, отклонённый по подписи, не считается
использованием ключа.
Требование действует на все эндпоинты, которые авторизуются обычным путём: /transcribe/,
/transcribe/async, /transcriptions/*, /api-keys/*, /metrics/user, /auth/me. Сессия
администратора (JWT) подписи не требует никогда — выключатель относится к ключу, а не к
пользователю. OpenAI-совместимые /v1/... проверяют ключ собственным кодом и подпись не
требуют: если ключ используется и там, помните, что этот путь остаётся без подписи.
Тело запроса в подпись не входит: загрузка может достигать MAX_FILE_SIZE_MB (5000 МБ), и
буферизовать её ради HMAC сервис не станет. Подпись подтверждает, что запрос выпущен держателем
секрета, и привязывает его к методу, адресу и моменту времени; целостность содержимого
обеспечивает TLS.
Ошибки 401
| Тело | Когда |
|---|---|
{ "detail": "API key requires signed requests but has no signing secret configured" } |
Флаг включён, а секретного слова у ключа нет (штатным путём такое состояние не создаётся) |
{ "detail": "Missing signature headers (X-Signature-Timestamp, X-Signature)" } |
Нет одного из двух заголовков |
{ "detail": "Signature timestamp outside the allowed window" } |
Метка устарела, пришла из будущего или это не число |
{ "detail": "Invalid request signature" } |
Подпись не сошлась |
Схема 2. Исходящий callback
Подписывается строка
<id задачи>.<timestamp>
где <id задачи> — поле id из тела вызова (оно же в заголовке X-ASR-Task-Id), а
<timestamp> — значение X-Webhook-Timestamp.
Метка времени и подпись пересчитываются на каждую попытку доставки, а не один раз на задачу: иначе повтор через минуту принёс бы метку минутной давности и не прошёл бы окно свежести у приёмника. У четырёх попыток будут четыре разные пары заголовков, и проверять надо ту, что пришла с текущим запросом.
Окно свежести
| Параметр | По умолчанию | Описание |
|---|---|---|
SIGNATURE_MAX_SKEW_SEC |
300 |
Допустимый разбег часов для X-Signature-Timestamp на входящих подписанных запросах, секунды |
Запрос принимается, если |сейчас − timestamp| ≤ SIGNATURE_MAX_SKEW_SEC, то есть подпись живёт
5 минут в обе стороны — перехваченный запрос нельзя повторить назавтра. Та же метка кладётся в
исходящий callback, чтобы приёмник мог применить своё окно той же ширины. Если часы убегают,
лечится это синхронизацией времени (NTP), а не расширением окна.
Пример: подписанный запрос (bash + openssl)
BASE=http://localhost:8020
API_KEY=sk-XXXXXXXX...
SECRET=whsec_2Vd1kZ0oQ7pR... # секретное слово этого ключа
REQ_PATH="/transcribe/async"
TS=$(date +%s)
SIG=$(printf '%s' "$TS.POST.$REQ_PATH" \
| openssl dgst -sha256 -hmac "$SECRET" -binary \
| base64)
curl -s -X POST "$BASE$REQ_PATH" \
-H "Authorization: Bearer $API_KEY" \
-H "X-Signature-Timestamp: $TS" \
-H "X-Signature: $SIG" \
-F "file=@./audio.mp3" \
-F "language=ru"
printf '%s' здесь важен: echo добавил бы перевод строки в подписываемые данные.
Пример: подпись запроса на Python
import base64
import hashlib
import hmac
import time
import requests
BASE = "http://localhost:8020"
API_KEY = "sk-XXXXXXXX..."
SECRET = "whsec_2Vd1kZ0oQ7pR..."
def signed_headers(method: str, path_with_query: str, secret: str) -> dict:
ts = str(int(time.time()))
data = f"{ts}.{method.upper()}.{path_with_query}"
digest = hmac.new(secret.encode("utf-8"), data.encode("utf-8"), hashlib.sha256).digest()
return {
"X-Signature-Timestamp": ts,
"X-Signature": base64.b64encode(digest).decode("ascii"),
}
# URL собираем сами — так подписанный путь и отправленный совпадают символ в символ
path = "/transcribe/async"
headers = {"Authorization": f"Bearer {API_KEY}"}
headers.update(signed_headers("POST", path, SECRET))
resp = requests.post(
BASE + path,
headers=headers,
data={
"url": "https://example.com/records/call.mp3",
"callback_url": "https://example.com/hooks/asr",
"language": "ru",
},
timeout=300,
)
resp.raise_for_status()
print(resp.json()["id"])
Пример: проверка callback на Python (Flask)
import base64
import hashlib
import hmac
import time
from flask import Flask, jsonify, request
SECRET = "whsec_2Vd1kZ0oQ7pR..."
MAX_SKEW_SEC = 300
app = Flask(__name__)
@app.post("/hooks/asr")
def asr_callback():
ts = request.headers.get("X-Webhook-Timestamp", "")
received = request.headers.get("X-Webhook-Signature", "")
if not ts or not received:
return jsonify(error="Missing signature headers"), 401
try:
if abs(int(time.time()) - int(ts)) > MAX_SKEW_SEC:
return jsonify(error="Stale timestamp"), 401
except ValueError:
return jsonify(error="Bad timestamp"), 401
task = request.get_json(silent=True) or {}
data = f"{task.get('id', '')}.{ts}" # id задачи из тела, он же в X-ASR-Task-Id
expected = base64.b64encode(
hmac.new(SECRET.encode("utf-8"), data.encode("utf-8"), hashlib.sha256).digest()
).decode("ascii")
if not hmac.compare_digest(expected, received):
return jsonify(error="Invalid signature"), 401
# подпись верна — телу можно доверять
print(task["id"], task["status"], len(task.get("text") or ""))
return jsonify(status="received"), 200
Пример: проверка callback на PHP
<?php
header('Content-Type: application/json');
$secret = getenv('ASR_WEBHOOK_SECRET');
$maxSkew = 300;
$ts = $_SERVER['HTTP_X_WEBHOOK_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_WEBHOOK_SIGNATURE'] ?? '';
if ($ts === '' || $sig === '') {
http_response_code(401);
exit(json_encode(['error' => 'Missing signature headers']));
}
if (abs(time() - (int) $ts) > $maxSkew) {
http_response_code(401);
exit(json_encode(['error' => 'Stale timestamp']));
}
$task = json_decode(file_get_contents('php://input'), true); // тело — JSON, не форма
$taskId = $task['id'] ?? ''; // он же в X-ASR-Task-Id
$expected = base64_encode(hash_hmac('sha256', $taskId . '.' . $ts, $secret, true));
if (!hash_equals($expected, $sig)) {
http_response_code(401);
exit(json_encode(['error' => 'Invalid signature']));
}
// подпись верна — обрабатываем $task['status'] и $task['text']
http_response_code(200);
echo json_encode(['status' => 'received']);
Переменные окружения
Все новые настройки задаются в .env и имеют рабочие значения по умолчанию — менять их для
включения подписи или callback не требуется.
| Переменная | По умолчанию | Описание |
|---|---|---|
FETCH_TIMEOUT_SEC |
120 |
Абсолютный дедлайн на скачивание аудио по url, секунды |
FETCH_MAX_REDIRECTS |
5 |
Сколько редиректов разрешено пройти при скачивании (каждый проверяется политикой хостов) |
FETCH_ALLOW_PRIVATE_HOSTS |
false |
Разрешить приватные/loopback/link-local адреса в url и callback_url. См. предупреждение в разделе 7: включённый, он превращает любой API-ключ в HTTP-прокси внутрь периметра |
CALLBACK_TIMEOUT_SEC |
30 |
Таймаут одной попытки доставки callback, секунды |
CALLBACK_RETRIES |
3 |
Сколько повторов после первой неудачной попытки (паузы 2 / 10 / 60 с) |
CALLBACK_POLL_SECONDS |
2.0 |
Как часто фоновый поток ищет готовые задачи с заказанным вызовом, секунды |
SIGNATURE_MAX_SKEW_SEC |
300 |
Окно свежести X-Signature-Timestamp у входящих подписанных запросов, секунды |
Обработка ошибок
Все ошибки возвращаются в стандартном формате:
{
"detail": "Описание ошибки"
}
Коды ошибок:
- 400: Неверный запрос (в том числе битый
url/callback_urlи неподдерживаемый формат по ссылке) - 401: Не авторизован (в том числе неверная или отсутствующая подпись — см. раздел 8)
- 403: Доступ запрещен (в том числе адрес, запрещённый политикой хостов)
- 404: Ресурс не найден
- 413: Файл слишком большой (больше
MAX_FILE_SIZE_MB, по умолчанию 5000 МБ) - 429: Превышен лимит запросов
- 500: Внутренняя ошибка сервера
- 502: Источник по
urlнедоступен или ответил5xx - 504: Источник по
urlне уложился вFETCH_TIMEOUT_SEC
Не всякая неудача — это HTTP-код: файл приняли, а сломалась уже обработка. Тогда задача переходит
в failed, а причина лежит в error_message (GET /transcriptions/{id}/status). Так, файл, в
котором вовсе нет звуковой дорожки, даёт: «В файле нет звуковой дорожки — распознавать нечего.»
Пример обработки ошибок (Python):
import requests
def safe_api_call(url, headers, method="GET", **kwargs):
try:
if method == "GET":
response = requests.get(url, headers=headers, **kwargs)
elif method == "POST":
response = requests.post(url, headers=headers, **kwargs)
if response.status_code == 200:
return response.json()
elif response.status_code == 401:
print("Authentication required. Please login.")
elif response.status_code == 403:
print("Access denied. Check permissions.")
elif response.status_code == 404:
print("Resource not found.")
elif response.status_code == 429:
print("Rate limit exceeded. Please wait.")
else:
error = response.json()
print(f"Error: {error.get('detail', 'Unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
return None
Полный пример использования
Python - Полный workflow:
import requests
import time
class Speech2TextClient:
def __init__(self, base_url="http://localhost:8020"):
self.base_url = base_url
self.token = None
def login(self, username, password):
"""Авторизация в системе"""
response = requests.post(
f"{self.base_url}/auth/login",
json={"username": username, "password": password}
)
if response.status_code == 200:
self.token = response.json()["access_token"]
print("Login successful!")
return True
return False
def transcribe(self, audio_path, language=None):
"""Транскрибация аудиофайла"""
if not self.token:
print("Please login first")
return None
headers = {"Authorization": f"Bearer {self.token}"}
with open(audio_path, "rb") as f:
files = {"file": f}
data = {"language": language} if language else {}
response = requests.post(
f"{self.base_url}/transcribe/",
headers=headers,
files=files,
data=data
)
if response.status_code == 200:
return response.json()
else:
print(f"Error: {response.text}")
return None
def wait_for_completion(self, transcription_id, timeout=300):
"""Ожидание завершения транскрибации"""
headers = {"Authorization": f"Bearer {self.token}"}
start_time = time.time()
while time.time() - start_time < timeout:
response = requests.get(
f"{self.base_url}/transcriptions/{transcription_id}/status",
headers=headers
)
if response.status_code == 200:
status = response.json()
if status["status"] == "completed":
return True
elif status["status"] == "failed":
print(f"Transcription failed: {status.get('error_message')}")
return False
time.sleep(2)
print("Timeout waiting for transcription")
return False
def get_transcription(self, transcription_id):
"""Получение результата транскрибации"""
headers = {"Authorization": f"Bearer {self.token}"}
response = requests.get(
f"{self.base_url}/transcribe/{transcription_id}",
headers=headers
)
if response.status_code == 200:
return response.json()
return None
def download_text(self, transcription_id, output_path):
"""Скачивание текста транскрибации"""
headers = {"Authorization": f"Bearer {self.token}"}
response = requests.get(
f"{self.base_url}/transcriptions/{transcription_id}/download",
headers=headers
)
if response.status_code == 200:
with open(output_path, "w", encoding="utf-8") as f:
f.write(response.text)
print(f"Transcription saved to {output_path}")
return True
return False
# Использование
if __name__ == "__main__":
client = Speech2TextClient()
# Авторизация
if client.login("admin", "admin123"):
# Транскрибация
result = client.transcribe("podcast.mp3", language="ru")
if result:
print(f"Transcription ID: {result['id']}")
print(f"Status: {result['status']}")
# Ожидание завершения
if result['status'] == 'processing':
if client.wait_for_completion(result['id']):
# Получение результата
full_result = client.get_transcription(result['id'])
print(f"Text: {full_result['text'][:200]}...")
# Сохранение в файл
client.download_text(result['id'], "transcription.txt")
JavaScript - Полный workflow:
class Speech2TextClient {
constructor(baseUrl = 'http://localhost:8020') {
this.baseUrl = baseUrl;
this.token = null;
}
async login(username, password) {
const response = await fetch(`${this.baseUrl}/auth/login`, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({username, password})
});
if (response.ok) {
const data = await response.json();
this.token = data.access_token;
console.log('Login successful!');
return true;
}
return false;
}
async transcribe(file, language = null) {
if (!this.token) {
console.error('Please login first');
return null;
}
const formData = new FormData();
formData.append('file', file);
if (language) {
formData.append('language', language);
}
const response = await fetch(`${this.baseUrl}/transcribe/`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.token}`
},
body: formData
});
if (response.ok) {
return await response.json();
}
return null;
}
async waitForCompletion(transcriptionId, timeout = 300000) {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const response = await fetch(
`${this.baseUrl}/transcriptions/${transcriptionId}/status`,
{
headers: {'Authorization': `Bearer ${this.token}`}
}
);
if (response.ok) {
const status = await response.json();
if (status.status === 'completed') {
return true;
} else if (status.status === 'failed') {
console.error('Transcription failed:', status.error_message);
return false;
}
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
console.error('Timeout waiting for transcription');
return false;
}
async getTranscription(transcriptionId) {
const response = await fetch(
`${this.baseUrl}/transcribe/${transcriptionId}`,
{
headers: {'Authorization': `Bearer ${this.token}`}
}
);
if (response.ok) {
return await response.json();
}
return null;
}
}
// Использование
async function main() {
const client = new Speech2TextClient();
// Авторизация
if (await client.login('admin', 'admin123')) {
// Выбор файла
const fileInput = document.getElementById('fileInput');
fileInput.addEventListener('change', async (e) => {
const file = e.target.files[0];
// Транскрибация
const result = await client.transcribe(file, 'ru');
if (result) {
console.log('Transcription ID:', result.id);
// Ожидание завершения
if (result.status === 'processing') {
if (await client.waitForCompletion(result.id)) {
// Получение результата
const fullResult = await client.getTranscription(result.id);
console.log('Text:', fullResult.text);
// Отображение результата
document.getElementById('result').textContent = fullResult.text;
}
}
}
});
}
}
main();
Советы и рекомендации
Оптимизация производительности:
- Формат файлов: Конвертируйте в WAV 16kHz моно для быстрой обработки
- Размер файлов: Разбивайте большие файлы на части по 30 минут
- Язык: Указывайте язык явно для ускорения обработки
- Batch обработка: Загружайте несколько файлов параллельно
- Видео:
.mov,.mxf,.webm,.mp4можно слать как есть — звук извлечёт сервис. Но по сети едет весь контейнер (реальный MXF — 3.88 ГБ ради процентов полезных данных), так что на узком канале дешевле вырезать звук у себя и прислать, например,.m4a
Безопасность:
- Храните токены безопасно: Никогда не включайте токены в код
- Используйте HTTPS: В продакшене всегда используйте HTTPS
- Ротация ключей: Регулярно обновляйте API ключи
- Ограничение прав: Давайте минимально необходимые права
- Подпись запросов: Включите
require_request_signature— украденный ключ без секретного слова станет бесполезен (раздел 8) - Проверяйте подпись callback: Включите
sign_callbacksи проверяйтеX-Webhook-Signatureна приёмнике — иначе кто угодно может прислать вам «результат»
Обработка ошибок:
- Retry логика: Реализуйте повторные попытки при временных ошибках
- Таймауты: Устанавливайте разумные таймауты для долгих операций
- Логирование: Логируйте все ошибки для отладки
- Graceful degradation: Предусмотрите fallback сценарии
Полезные ссылки
- Swagger UI: http://localhost:8020/docs
- ReDoc: http://localhost:8020/redoc
- Admin Panel: http://localhost:8020/admin/
- GitHub: https://github.com/yourusername/speech2text
- Поддержка: support@speech2text.com
Поддержка
При возникновении проблем: 1. Проверьте логи сервера 2. Убедитесь в правильности токена 3. Проверьте формат и размер файла 4. Обратитесь к документации API
Для технической поддержки создайте issue на GitHub или напишите на support@speech2text.com