Milligram AI texnologiyaları

API sənədləri

Audio faylı və ya video linkini mətnə (ASR), mətni isə səsə (TTS) çevirmək üçün REST API. Bütün sorğular X-API-Key header-i ilə gedir.

1 API açarı

Açar yalnız yaradılanda bir dəfə göstərilir — saxlayın. Balans bitənə qədər işləyir, istənilən vaxt ləğv edilə bilər.

import requests

BASE = "https://milligram.az/voice"   # birbaşa server: http://SERVER_IP:4009
KEY = "asr_..."                                 # yuxarıda yaratdığınız açar
H = {"X-API-Key": KEY}

me = requests.get(f"{BASE}/me", headers=H).json()
print(me["balance"], "saniyə balans")
BASE=https://milligram.az/voice
KEY=asr_...

curl -s "$BASE/me" -H "X-API-Key: $KEY"
# {"id":1,"username":"...","balance":33651}

2temp_id və record_id nədir?

NömrəHaradan gəlirHarada işlənirNə qədər yaşayır
temp_id POST /upload-file cavabında yalnız POST /transcribe-uploaded sorğusunda işə salana (və ya /discard-upload) qədər
record_id POST /transcribe-uploaded və ya POST /transcribe-link cavabında bundan sonrakı HƏR ŞEYDƏ: vəziyyət, endirmə, axın, redaktə, silmə daimi (siz silənə qədər)
record_id-i bilmirsiniz? GET /files bütün qeydlərinizi id ilə qaytarır — record_id məhz o id-dir. Buradakı 12 sadəcə nümunədir, sizin nömrəniz başqa olacaq.
# Nömrələr cavabdan götürülür, əl ilə yazılmır:
up = requests.post(f"{BASE}/upload-file", headers=H,
                   files={"file": ("audio.mp3", open("audio.mp3", "rb"))}).json()
# {"temp_id": "cd0463e8-8e7b-...", "duration": 25, "enough_balance": true}
temp_id = up["temp_id"]

job = requests.post(f"{BASE}/transcribe-uploaded", headers=H,
                    data={"temp_id": temp_id}).json()
# {"record_id": 12, "status": "processing", "punct": true}
rid = job["record_id"]          # <-- bundan sonra hər yerdə BU işlənir

requests.get(f"{BASE}/files/{rid}", headers=H)                       # vəziyyət
requests.get(f"{BASE}/files/{rid}/download", params={"fmt": "json"})  # nəticə

# Nömrəni itirsəniz - siyahıdan tapın:
for f in requests.get(f"{BASE}/files", headers=H).json():
    print(f["id"], f["original_name"], f["status"])
# jq varsa nömrələr avtomatik götürülür:
TEMP_ID=$(curl -s -X POST "$BASE/upload-file" -H "X-API-Key: $KEY" \
  -F "file=@audio.mp3" | jq -r .temp_id)
echo "$TEMP_ID"      # cd0463e8-8e7b-...

ID=$(curl -s -X POST "$BASE/transcribe-uploaded" -H "X-API-Key: $KEY" \
  -F "temp_id=$TEMP_ID" | jq -r .record_id)
echo "$ID"           # 12   <-- bundan sonra hər yerdə $ID işlənir

# jq yoxdursa cavabı ekranda görüb dəyəri əl ilə yazın:
#   {"record_id":12,"status":"processing",...}   ->   ID=12

# Nömrəni itirsəniz - siyahıdan tapın:
curl -s "$BASE/files" -H "X-API-Key: $KEY" | jq '.[] | {id, original_name, status}'

3 Transkripsiyanı başlat

İki yol var: kompüterdəki fayl və ya video linki. Hər ikisi arxa planda işləyir və dərhal record_id qaytarır.

punct=true (default) durğu işarələri, böyük hərflər və rəqəmlər bərpa olunur. punct=false xam mətn verir: «iki min iyirmi altıncı ilin dövlət büdcəsi» — true ilə isə «2026-cı ilin dövlət büdcəsi...»

# --- A) Fayl ---
with open("audio.mp3", "rb") as f:
    up = requests.post(f"{BASE}/upload-file", headers=H,
                       files={"file": ("audio.mp3", f)}).json()
print(up["duration_fmt"], "| balans yetərli:", up["enough_balance"])

job = requests.post(f"{BASE}/transcribe-uploaded", headers=H,
                    data={"temp_id": up["temp_id"], "punct": "true"}).json()
rid = job["record_id"]

# --- B) Link ---
URL = "https://www.youtube.com/watch?v=XXXXXXXXXXX"

# əvvəlcə yoxla (heç nə tutulmur): müddət nə qədər, balans çatırmı
probe = requests.post(f"{BASE}/probe-link", headers=H, data={"url": URL}).json()
print(probe["duration_fmt"], probe["enough_balance"])

job = requests.post(f"{BASE}/transcribe-link", headers=H,
                    data={"url": URL, "punct": "false"}).json()
rid = job["record_id"]
# --- A) Fayl ---
curl -s -X POST "$BASE/upload-file" -H "X-API-Key: $KEY" -F "file=@audio.mp3"
# {"temp_id":"cd0463e8-...","duration":25,"duration_fmt":"0 dəq. 25 san.",
#  "enough_balance":true,"balance":33651}

curl -s -X POST "$BASE/transcribe-uploaded" -H "X-API-Key: $KEY" \
  -F "temp_id=$TEMP_ID" -F "punct=true"
# {"record_id":12,"status":"processing","punct":true,...}

# --- B) Link ---
URL="https://www.youtube.com/watch?v=XXXXXXXXXXX"

curl -s -X POST "$BASE/probe-link" -H "X-API-Key: $KEY" -F "url=$URL"
# {"duration":189,"duration_fmt":"3 dəq. 9 san.","enough_balance":true}

curl -s -X POST "$BASE/transcribe-link" -H "X-API-Key: $KEY" \
  -F "url=$URL" -F "punct=false"
# {"record_id":13,"status":"downloading",...}

4 Hazır olmasını gözlə

İki üsul: polling (sadə — vəziyyəti təkrar soruşursunuz) və ya canlı axın (mətn seqment-seqment gəlir).

# --- A) Polling ---
import time

while True:
    info = requests.get(f"{BASE}/files/{rid}", headers=H).json()
    if not info["processing"]:
        break
    if info["progress"]:
        print(info["progress"]["current"], "/", info["progress"]["total"])
    time.sleep(3)

if info["status"] == "error":
    raise SystemExit("transkripsiya alınmadı")
print(info["transcript"])

# --- B) Canlı axın (SSE) ---
import json

# bilet 60 saniyəlik və birdəfəlikdir - açar URL-ə düşmür
ticket = requests.post(f"{BASE}/stream/{rid}/ticket", headers=H).json()["ticket"]

with requests.get(f"{BASE}/stream/{rid}", params={"ticket": ticket},
                  stream=True, timeout=(30, 3600)) as r:
    for line in r.iter_lines(decode_unicode=True):
        if not line or not line.startswith("data:"):
            continue                      # ": keep-alive" sətirləri buraxılır
        ev = json.loads(line[5:])
        if ev["type"] == "segment":
            print(ev["text"])             # XAM seqment (canlı)
        elif ev["type"] == "complete":
            print(ev["transcript"])       # punktuasiyalı YEKUN mətn
            break
        elif ev["type"] == "error":
            print("xəta:", ev["message"])
            break
# --- A) Polling ---
curl -s "$BASE/files/$ID" -H "X-API-Key: $KEY"
# {"status":"transcribing","processing":true,"progress":{"current":3,"total":38}}
# ... processing:false olana qədər təkrarlayın

# --- B) Canlı axın (SSE) ---
TICKET=$(curl -s -X POST "$BASE/stream/$ID/ticket" -H "X-API-Key: $KEY" | jq -r .ticket)
curl -N "$BASE/stream/$ID?ticket=$TICKET"
Axında segment hadisələri xam gəlir (canlı göstərmək üçün), punktuasiya isə yalnız sonda tam mətnə tətbiq olunur və complete ilə gəlir. Yəni interfeys sonda mətni complete ilə əvəz etməlidir.

5 Nəticəni al

Eyni record_id üç formatda verilir.

fmtNə qaytarırNə vaxt lazımdır
txt (default)düz mətnsadəcə oxumaq üçün
srtaltyazı faylıvideoya altyazı qoşmaq üçün
jsonmətn + cues + hər sözün vaxtıredaktor, axtarış, sinxron oxutma
dl = f"{BASE}/files/{rid}/download"

txt = requests.get(dl, headers=H, params={"fmt": "txt"}).text
srt = requests.get(dl, headers=H, params={"fmt": "srt"}).text
data = requests.get(dl, headers=H, params={"fmt": "json"}).json()

open("audio.srt", "w", encoding="utf-8").write(srt)

data["transcript"]      # tam mətn
data["duration"]        # saniyə
data["words"][0]        # {"word": "salam", "start": 1.24, "end": 1.61}
data["cues"][0]         # sətir: start, end, text, words
# $ID yuxarıda `record_id`-dən gəlir (məs. 12)
curl -s "$BASE/files/$ID/download?fmt=txt"  -H "X-API-Key: $KEY"
curl -s "$BASE/files/$ID/download?fmt=srt"  -H "X-API-Key: $KEY" -o audio.srt
curl -s "$BASE/files/$ID/download?fmt=json" -H "X-API-Key: $KEY" -o audio.json

6 Redaktə və silmə

# Sözləri vaxtı ilə yenidən yaz - mətn və SRT yenidən qurulur
words = [{"word": "Salam", "start": 0.0, "end": 0.4},
         {"word": "dünya.", "start": 0.4, "end": 0.9}]
requests.post(f"{BASE}/files/{rid}/words", headers=H, json={"words": words})

requests.get(f"{BASE}/files", headers=H).json()        # siyahı
requests.delete(f"{BASE}/files/{rid}", headers=H)      # sil
curl -s -X POST "$BASE/files/$ID/words" -H "X-API-Key: $KEY" \
  -H "Content-Type: application/json" \
  -d '{"words":[{"word":"Salam","start":0,"end":0.4}]}'

curl -s "$BASE/files" -H "X-API-Key: $KEY"             # siyahı
curl -s -X DELETE "$BASE/files/$ID" -H "X-API-Key: $KEY"   # sil

7 TTS — mətni səsə çevir

Referans səs (3–15 san, ilk 15 saniyə işlənir) + onda deyilən mətn + səsləndiriləcək mətn göndərilir. Yüklənən referans səs bazasında mətni ilə qalır (GET /tts/voices), sonrakı sorğularda faylı yenidən göndərmək lazım deyil: voice_id=<id> kifayətdir (ref_text boşdursa bazadakı mətn işlənir). Siyahıda öz səsləriniz və admin-in "ümumi" etdiyi səslər görünür. İş növbəyə düşür, GET /tts/files/{id} ilə izlənir. Balansdan yaranan səsin müddəti (yuxarı yuvarlaqlaşdırılmış saniyə) tutulur — ASR ilə eyni balans. Xəta olarsa heç nə tutulmur. Eyni səslə təkrar üçün faylı yenidən göndərmək lazım deyil: ref_from=<əvvəlki id>.

# (istəyə görə) referansın mətnini ASR ilə al
with open("ref.wav", "rb") as f:
    ref_text = requests.post(f"{BASE}/tts/ref-transcribe", headers=H,
                             files={"file": f}).json()["text"]

with open("ref.wav", "rb") as f:
    job = requests.post(f"{BASE}/tts/generate", headers=H, files={"ref_audio": f},
                        data={"ref_text": ref_text,
                              "gen_text": "Salam, bu sınaq mətnidir.",
                              "speed": 0,      # 0 = avtomatik
                              "seed": 44}).json()

while True:
    rec = requests.get(f"{BASE}/tts/files/{job['id']}", headers=H).json()
    if rec["status"] in ("completed", "error"):
        break
    time.sleep(1.5)

audio = requests.get(BASE + rec["audio_url"]).content   # WAV, 24 kHz (imzalı link, ~12 saat keçərli)
open("tts.wav", "wb").write(audio)
curl -s -X POST "$BASE/tts/ref-transcribe" -H "X-API-Key: $KEY" -F file=@ref.wav

curl -s -X POST "$BASE/tts/generate" -H "X-API-Key: $KEY" \
  -F ref_audio=@ref.wav -F "ref_text=Referans səsdə deyilən mətn" \
  -F "gen_text=Salam, bu sınaq mətnidir."
# -> {"id": 7, "status": "queued", "queue_position": 0, ...}

curl -s "$BASE/tts/files/7" -H "X-API-Key: $KEY"     # status: completed -> audio_url
curl -s "$BASE/media/..._tts.wav?e=...&s=..." -o tts.wav   # audio_url olduğu kimi; ~12 saat keçərli

Arayış

Cavab nümunəsi — GET /files/{id}

json
{
  "id": 12,                       // <- record_id məhz budur
  "original_name": "audio.mp3",
  "audio_url": "/media/6f1c..._audio.mp3?e=1791360000&s=9b1e...",   // imzalı link, ~12 saat
  "transcript": "2026-cı ilin dövlət büdcəsi layihəsində...",
  "duration": 189,
  "status": "completed",          // processing|transcribing|downloading|completed|error
  "processing": false,
  "progress": null,               // işləyəndə: {"current": 3, "total": 38}
  "cues": [
    {"id": 0, "start": 0.72, "end": 4.56, "text": "2026-cı ilin dövlət...",
     "words": [{"word": "2026-cı", "start": 0.72, "end": 1.52}]}
  ]
}

Xəta kodları

KodSəbəb
401Açar yoxdur, yanlışdır və ya ləğv olunub
402Balans kifayət etmir (cavabda lazım olan müddət yazılır)
400Dəstəklənməyən format, yanlış link, canlı yayım, boş/uzun TTS mətni
404Belə record_id yoxdur (və ya sizin deyil)
413Fayl çox böyükdür
429Sorğu limiti aşılıb - bir qədər gözləyin

Bütün endpointlər

MetodYolİzah
GET/meBalans və istifadəçi məlumatı
POST/api-keysAPI açarı yarat
GET/api-keysÖz açarlarının siyahısı
DELETE/api-keys/{id}Açarı ləğv et
POST/upload-fileFayl yüklə → temp_id
POST/discard-uploadYüklənmiş faylı sil (işə salmadan)
POST/transcribe-uploadedFaylı işə sal → record_id
POST/probe-linkLinki yoxla (müddət, qiymət)
POST/transcribe-linkLinki işə sal → record_id
GET/filesBütün qeydlər (id ilə)
GET/files/{id}Qeyd + söz vaxtları
POST/files/{id}/wordsMətni redaktə et
GET/files/{id}/download?fmt=txt | srt | json
DELETE/files/{id}Qeydi sil
POST/files/bulk-deleteBir neçəsini sil (JSON: file_ids)
POST/stream/{id}/ticketAxın bileti (60 san)
GET/stream/{id}?ticket=Canlı axın (SSE)
POST/tts/ref-transcribeReferans səsin mətni (ASR, ilk 15 san)
GET/tts/voicesSəs bazası: öz + ümumi səslər (audio_url, text)
POST/tts/voicesYeni səs (fayl; transcribe=true → mətn ASR ilə)
PATCH/tts/voices/{id}Ad / mətn dəyiş (öz səsi)
DELETE/tts/voices/{id}Səsi bazadan sil (öz səsi)
POST/tts/generateSəs yarat (voice_id və ya fayl) → id
GET/tts/filesTTS generasiyaları
GET/tts/files/{id}Vəziyyət, audio_url, müddət
DELETE/tts/files/{id}Generasiyanı və faylını sil
POST/tts/files/bulk-deleteBir neçəsini sil (JSON: file_ids)
POST/chat/sendÇat: mesaj göndər, cavab SSE ilə axır (JSON: text, conversation_id; yeni söhbət üçün agent: "ai" və ya "banx" - yalnız bank sualları; files: [id] - əlavə edilmiş fayllar, yalnız "ai")
POST/chat/filesÇata fayl/şəkil yüklə (multipart: file; pdf, doc, docx, odt, rtf, txt, md, csv, png, jpg, webp, gif, bmp; 20 MB) -> id, sonra /chat/send-də files
DELETE/chat/files/{id}Göndərilməmiş faylı sil
GET/chat/conversationsSöhbətlər (?agent=ai | banx)
GET/chat/conversations/{id}Söhbət + mesajlar
DELETE/chat/conversations/{id}Söhbəti sil
GET/healthServisin vəziyyəti (açarsız)