Memo AIDocs
Руководства

Загрузка и расшифровка

Пошаговый сценарий — загрузите аудио через API, дождитесь расшифровки и заберите текст, отчеты или Markdown-экспорт. Плюс пакетная загрузка папки.

Весь цикл — три вызова: загрузить файл, опросить статус до готовности, прочитать результат. В этом руководстве соберем небольшой рабочий скрипт — и прогоним через него целую папку записей.

Создайте ключ с правом загрузки

Для загрузки нужно явное разрешение на ключе. В Настройки → API & MCP нажмите Создать ключ и включите Разрешить загрузку файлов. Ключи без этой опции (включая все старые) — только для чтения и получают 403 upload_scope_required при загрузке.

Создать API-ключ

Загрузите файл

Один POST с файлом в теле запроса (полный справочник):

import httpx

BASE = "https://app.memoai.tech/api/v1/developer"
HEADERS = {"Authorization": "Bearer mk_live_your_key_here"}

def upload(path: str, language: str | None = None, ai_metadata: bool = False) -> str:
    """Загружает медиафайл, возвращает UUID расшифровки."""
    params = {"ai_metadata": str(ai_metadata).lower()}
    if language:
        params["language"] = language
    with open(path, "rb") as f:
        resp = httpx.post(
            f"{BASE}/transcriptions",
            params=params,
            headers={
                **HEADERS,
                "Content-Disposition": f'attachment; filename="{path.split("/")[-1]}"',
            },
            content=f,
            timeout=httpx.Timeout(10, write=None),
        )
    resp.raise_for_status()
    return resp.json()["uuid"]

# ai_metadata=True → сгенерировать и AI-слой (саммари, отчеты) — нужен в шаге 3;
# по умолчанию API-загрузка дает только сырой текст — быстрее.
uuid = upload("standup.mp3", language="ru", ai_metadata=True)
print("в очереди:", uuid)

Файлы до 2 ГБ загружаются точно так же — тело стримится с диска.

Дождитесь расшифровки

Опрашивайте раз в 5–10 секунд. Короткая запись обычно готова за пару минут:

import time

def wait(uuid: str, poll_seconds: int = 10) -> dict:
    """Опрашивает статус до терминального."""
    while True:
        t = httpx.get(f"{BASE}/transcriptions/{uuid}", headers=HEADERS).json()
        if t["status"] == "completed":
            return t
        if t["status"] == "failed":
            raise RuntimeError(f"ошибка расшифровки: {t.get('error_code')}")
        if t["status"] == "insufficient_balance":
            raise RuntimeError("не хватило минут — пополните баланс и загрузите заново")
        time.sleep(poll_seconds)   # queued / processing

t = wait(uuid)
print(t["title"])
print(t["text"][:500])
for report in t["prompt_results"]:
    print("Отчет:", report["name"])

queued означает лишь, что файл ждет свободный слот параллельной обработки вашего тарифа — он стартует сам, делать ничего не нужно.

Используйте результат

text и AI-отчеты уже пришли на прошлом шаге. Нужен файл? Все read-эндпоинты работают с загруженными расшифровками:

resp = httpx.get(
    f"{BASE}/transcriptions/{uuid}/export",
    params={"format": "md"},
    headers=HEADERS,
)
with open("standup.md", "wb") as f:
    f.write(resp.content)

Загрузка целой папки

Пачкой загружать безопасно: файлы сверх параллельности тарифа просто ждут в queued, очередь дренируется сама. Сначала загрузите все, потом опрашивайте:

import random
import uuid as uuidlib
from pathlib import Path

AUDIO = {".mp3", ".m4a", ".wav", ".flac", ".ogg", ".mp4", ".mov", ".mkv"}

def upload_with_retry(path: str, attempts: int = 5) -> str:
    # Один Idempotency-Key на ФАЙЛ, общий для всех попыток: даже если ответ
    # потерялся в полете, повтор вернет оригинальный uuid, а не дубль
    # (и не второе списание).
    idem_key = str(uuidlib.uuid4())
    for attempt in range(attempts):
        try:
            with open(path, "rb") as f:
                resp = httpx.post(
                    f"{BASE}/transcriptions",
                    headers={
                        **HEADERS,
                        "Content-Disposition": f'attachment; filename="{Path(path).name}"',
                        "Idempotency-Key": idem_key,
                    },
                    content=f,
                    timeout=httpx.Timeout(10, write=None),
                )
            resp.raise_for_status()
            return resp.json()["uuid"]
        except httpx.HTTPStatusError as e:
            if e.response.status_code in (409, 429):     # в полете / лимит
                # Ждем СЛУЧАЙНУЮ долю Retry-After, а не ровно столько: иначе все
                # заблокированные клиенты проснутся одновременно и снова создадут
                # ту же очередь, которая только что разошлась.
                wait = int(e.response.headers.get("Retry-After", "30"))
                time.sleep(random.uniform(0, wait))
            else:
                raise                                     # прочие 4xx: чинить, не повторять
        except httpx.TransportError:                      # сетевой сбой — безопасно благодаря ключу
            time.sleep(random.uniform(1, 5))
    raise RuntimeError(f"не получилось: {path}")

uuids = [
    upload_with_retry(str(p))
    for p in Path("~/записи").expanduser().iterdir()
    if p.suffix.lower() in AUDIO
]
results = [wait(u, poll_seconds=30) for u in uuids]
print(f"готово: {len(results)} расшифровок")

Здесь встречаются два кода 429, оба временные:

  • too_many_concurrent_uploads — слишком много параллельных POST одновременно. Загружайте последовательно или уважайте Retry-After (в секундах).
  • too_many_queued_files — больше 100 файлов в ожидании. Очередь дренируется; повторите после паузы.

С Idempotency-Key ретраи становятся скучными — в хорошем смысле

Один ключ на файл, общий для всех попыток, — и потерянный ответ не может стоить вам дубля расшифровки: повтор просто вернет оригинальный uuid. Детали — в справочнике загрузки.

Контроль расходов

Минуты резервируются при старте обработки, поэтому пачка не может уйти в минус: файлы, на которые баланса не хватило, завершаются статусом insufficient_balance (и не сохраняются). Бюджет перед большой пачкой проверяйте программно через GET /workspace:

ws = httpx.get(f"{BASE}/workspace", headers=HEADERS).json()
minutes_left = ws["balance"]["available_minutes"]   # уже за вычетом активных резерваций
if minutes_left < expected_batch_minutes:
    raise SystemExit(f"сначала пополните баланс: осталось {minutes_left} минут")

Дальше

На этой странице