Dokumentimi
Nisja e shpejtë
API i Fjalim kthen shqipen e folur në tekst, me HTTPS dhe JSON. Adresa bazë: https://api.fjalim.com. Transkriptimi kushton 0,012 € në minutë, i faturuar për sekondë të nisur.
Kjo faqe është një hyrje e shkurtër në shqip. Udhëzuesi i plotë (webhook-e, formati i transkriptit, titrat, gabimet, kufijtë) është në anglisht, dhe referenca e API-t gjenerohet nga skedari OpenAPI.
Nga skedari te teksti, në pesë hapa
- Merrni një çelës. Hyni në aplikacion dhe krijoni një çelës API. Fillon me
fj_live_dhe shfaqet vetëm një herë. Llogaritë e reja marrin 15 minuta falas pas verifikimit të emailit. - Ngarkoni.
POST /v1/uploadskthen një URL të nënshkruar. BëniPUTtë skedarit aty, pastaj thirrniPOST /v1/uploads/{id}/complete. - Krijoni.
POST /v1/transcriptionsmeupload_id. Përgjigjja është202me një objekt transkriptimi në gjendjenqueued. - Prisni. Pyesni
GET /v1/transcriptions/{id}çdo disa sekonda, ose vendosni njëwebhook_urldhe ne ju thërrasim vetë. - Eksportoni.
GET /v1/transcriptions/{id}/transcript?format=json|txt|srt|vtt.
curl
export FJALIM_KEY="fj_live_..." # create a key in the web app
API=https://api.fjalim.com
FILE=seanca.mp3
SIZE=$(wc -c < "$FILE" | tr -d ' ')
# 1. Ask for an upload URL
UPLOAD=$(curl -sS "$API/v1/uploads" \
-H "Authorization: Bearer $FJALIM_KEY" \
-H "Content-Type: application/json" \
-d "{\"filename\":\"$FILE\",\"content_type\":\"audio/mpeg\",\"size_bytes\":$SIZE}")
UPLOAD_ID=$(echo "$UPLOAD" | jq -r .id)
UPLOAD_URL=$(echo "$UPLOAD" | jq -r .upload_url)
# 2. PUT the bytes, with the headers from the response
HEADERS=()
while IFS= read -r h; do HEADERS+=(-H "$h"); done \
< <(echo "$UPLOAD" | jq -r '.headers | to_entries[] | "\(.key): \(.value)"')
curl -sS -X PUT "$UPLOAD_URL" "${HEADERS[@]}" --data-binary @"$FILE"
# 3. Confirm the upload
curl -sS -X POST "$API/v1/uploads/$UPLOAD_ID/complete" \
-H "Authorization: Bearer $FJALIM_KEY"
# 4. Create the transcription
JOB=$(curl -sS "$API/v1/transcriptions" \
-H "Authorization: Bearer $FJALIM_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{\"upload_id\":\"$UPLOAD_ID\"}" | jq -r .id)
# 5. Poll until it finishes (or use a webhook)
while :; do
STATUS=$(curl -sS "$API/v1/transcriptions/$JOB" \
-H "Authorization: Bearer $FJALIM_KEY" | jq -r .status)
echo "$STATUS"
[ "$STATUS" = completed ] && break
[ "$STATUS" = failed ] || [ "$STATUS" = canceled ] && exit 1
sleep 3
done
# 6. Export
curl -sS "$API/v1/transcriptions/$JOB/transcript?format=srt" \
-H "Authorization: Bearer $FJALIM_KEY" -o seanca.srt JavaScript
// Node 20+ (global fetch). Run as an ES module.
import { readFile, writeFile } from 'node:fs/promises';
import { randomUUID } from 'node:crypto';
const API = 'https://api.fjalim.com';
const KEY = process.env.FJALIM_KEY; // fj_live_...
async function call(path, init = {}) {
const res = await fetch(API + path, {
...init,
headers: { Authorization: `Bearer ${KEY}`, ...init.headers },
});
if (!res.ok) {
const { error } = await res.json();
throw new Error(`${error.code}: ${error.message}`);
}
return res;
}
const json = { 'Content-Type': 'application/json' };
const file = await readFile('seanca.mp3');
// 1. Ask for an upload URL
const upload = await (await call('/v1/uploads', {
method: 'POST',
headers: json,
body: JSON.stringify({ filename: 'seanca.mp3', content_type: 'audio/mpeg', size_bytes: file.length }),
})).json();
// 2. PUT the bytes, with the headers from the response
const put = await fetch(upload.upload_url, { method: upload.method, headers: upload.headers, body: file });
if (!put.ok) throw new Error(`upload failed: ${put.status}`);
// 3. Confirm the upload
await call(`/v1/uploads/${upload.id}/complete`, { method: 'POST' });
// 4. Create the transcription
const job = await (await call('/v1/transcriptions', {
method: 'POST',
headers: { ...json, 'Idempotency-Key': randomUUID() },
body: JSON.stringify({ upload_id: upload.id }),
})).json();
// 5. Poll until it finishes (or use a webhook)
let t;
do {
await new Promise((r) => setTimeout(r, 3000));
t = await (await call(`/v1/transcriptions/${job.id}`)).json();
} while (!['completed', 'failed', 'canceled'].includes(t.status));
if (t.status !== 'completed') throw new Error(t.error?.message ?? t.status);
// 6. Export
const srt = await (await call(`/v1/transcriptions/${job.id}/transcript?format=srt`)).text();
await writeFile('seanca.srt', srt); Python
# pip install httpx
import os
import time
import uuid
import httpx
API = "https://api.fjalim.com"
client = httpx.Client(
base_url=API,
headers={"Authorization": f"Bearer {os.environ['FJALIM_KEY']}"}, # fj_live_...
timeout=30,
)
def check(res: httpx.Response) -> httpx.Response:
if res.is_error:
err = res.json()["error"]
raise RuntimeError(f"{err['code']}: {err['message']}")
return res
with open("seanca.mp3", "rb") as f:
data = f.read()
# 1. Ask for an upload URL
upload = check(client.post("/v1/uploads", json={
"filename": "seanca.mp3",
"content_type": "audio/mpeg",
"size_bytes": len(data),
})).json()
# 2. PUT the bytes, with the headers from the response
check(httpx.put(upload["upload_url"], headers=upload["headers"], content=data, timeout=600))
# 3. Confirm the upload
check(client.post(f"/v1/uploads/{upload['id']}/complete"))
# 4. Create the transcription
job = check(client.post(
"/v1/transcriptions",
json={"upload_id": upload["id"]},
headers={"Idempotency-Key": str(uuid.uuid4())},
)).json()
# 5. Poll until it finishes (or use a webhook)
while True:
time.sleep(3)
t = check(client.get(f"/v1/transcriptions/{job['id']}")).json()
if t["status"] in ("completed", "failed", "canceled"):
break
if t["status"] != "completed":
raise RuntimeError((t.get("error") or {}).get("message", t["status"]))
# 6. Export
srt = check(client.get(f"/v1/transcriptions/{job['id']}/transcript", params={"format": "srt"})).text
open("seanca.srt", "w", encoding="utf-8").write(srt) Një sekondë audio kushton 200 µ€ (mikro-euro). Faturimi bëhet kur punëtori ka matur kohëzgjatjen e audios. Nëse krediti është nën 200 µ€, përgjigjja është 402 insufficient_credits.
Autentikimi
Dërgoni çelësin në titullin Authorization në çdo kërkesë:
Authorization: Bearer fj_live_... - Çelësat krijohen dhe hiqen në aplikacion. Ruajmë vetëm një hash, ndaj një çelës i humbur nuk mund të shfaqet sërish. Krijoni një të ri.
- Mbajini çelësat në serverin tuaj. Kurrë në shfletues ose në aplikacion celulari.
- Një çelës që mungon ose është i gabuar kthen
401 unauthorized.
Transkripto një URL
Në vend që ta ngarkoni, mund të dërgoni një audio_url publik. Dërgoni vetëm njërin nga upload_id dhe audio_url. URL-ja duhet të jetë https dhe nuk duhet të çojë te adresa private, loopback ose link-local.
curl
curl https://api.fjalim.com/v1/transcriptions \
-H "Authorization: Bearer $FJALIM_KEY" \
-H "Content-Type: application/json" \
-d '{"audio_url": "https://example.com/podcast/episodi-42.mp3"}' JavaScript
const res = await fetch('https://api.fjalim.com/v1/transcriptions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.FJALIM_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ audio_url: 'https://example.com/podcast/episodi-42.mp3' }),
});
const job = await res.json(); // 202 { id, status: "queued", ... } Python
job = httpx.post(
"https://api.fjalim.com/v1/transcriptions",
headers={"Authorization": f"Bearer {os.environ['FJALIM_KEY']}"},
json={"audio_url": "https://example.com/podcast/episodi-42.mp3"},
).json() # 202 {"id": ..., "status": "queued", ...} Webhook-e
Vendosni webhook_url kur krijoni një transkriptim. Kur përfundon ose dështon, ne dërgojmë një POST me JSON-in e transkriptimit. Çdo kërkesë ka titullin Fjalim-Signature: t=<sekonda unix>,v1=<hex>.
- Lexoni trupin të papërpunuar, para se ta kaloni nëpër parser JSON.
- Ndajeni titullin në
tdhev1. - Refuzojeni kërkesën nëse
tndryshon më shumë se 5 minuta (300 sekonda) nga ora juaj. - Llogaritni HMAC-SHA256 me sekretin e webhook-ut mbi tekstin
"<t>.<trupi i papërpunuar>"dhe krahasojeni mev1me krahasim në kohë konstante.
Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';
/**
* rawBody: the exact bytes received (Buffer or string), before any JSON parsing.
* secret: your webhook secret, the full string including the "whsec_" prefix.
*/
export function verifyFjalimSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
String(header ?? '').split(',').map((p) => p.trim().split('=', 2)),
);
const t = parts.t;
const v1 = parts.v1;
if (!/^\d+$/.test(t ?? '') || !/^[0-9a-f]+$/i.test(v1 ?? '')) return false;
// Reject old (or future) timestamps: replay protection.
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, 'utf8');
const expected = createHmac('sha256', secret)
.update(Buffer.concat([Buffer.from(`${t}.`), body]))
.digest();
const given = Buffer.from(v1, 'hex');
return given.length === expected.length && timingSafeEqual(given, expected);
}
// Express: read the body as raw bytes so the signature matches.
import express from 'express';
const app = express();
app.post('/hooks/fjalim', express.raw({ type: 'application/json' }), (req, res) => {
if (!verifyFjalimSignature(req.body, req.get('Fjalim-Signature'), process.env.FJALIM_WEBHOOK_SECRET)) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body.toString('utf8'));
// Handle event.event === 'transcription.completed' | 'transcription.failed'.
// Deliveries can repeat: de-duplicate on event.id.
res.sendStatus(200);
}); Python
import hashlib
import hmac
import os
import time
def verify_fjalim_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
"""raw_body: the exact bytes received. secret: the full whsec_... string."""
try:
parts = dict(p.strip().split("=", 1) for p in (header or "").split(","))
t, v1 = parts["t"], parts["v1"]
timestamp = int(t)
except (KeyError, ValueError):
return False
# Reject old (or future) timestamps: replay protection.
if abs(time.time() - timestamp) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)
# FastAPI
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
@app.post("/hooks/fjalim")
async def fjalim_hook(request: Request):
raw = await request.body() # bytes, before JSON parsing
secret = os.environ["FJALIM_WEBHOOK_SECRET"] # the full whsec_... string
if not verify_fjalim_signature(raw, request.headers.get("Fjalim-Signature", ""), secret):
raise HTTPException(status_code=400)
event = await request.json()
# De-duplicate on event["id"]; handle transcription.completed / .failed
return {"ok": True} Sekreti fillon me whsec_, dhe e përdorni të plotë si çelës. Mund ta shihni ose ndërroni në aplikacion. Nëse përgjigja nuk është 2xx brenda 10 sekondash, riprovojmë pas 1 minute, 5 minutash, 30 minutash, 2 orësh, 6 orësh dhe 24 orësh. E njëjta ngjarje mund të vijë më shumë se një herë: filtrojeni sipas id.
Gabimet
Çdo gabim ka të njëjtën formë JSON dhe një status HTTP përkatës:
HTTP/1.1 402 Payment Required
Content-Type: application/json
{ "error": { "code": "insufficient_credits", "message": "Your balance is too low. Add credit to start a transcription." } } | Kodi | Statusi | Kuptimi |
|---|---|---|
invalid_request | 400 | Trupi ose një parametër nuk është i vlefshëm. |
unauthorized | 401 | Çelësi mungon, është i gabuar ose është hequr. |
insufficient_credits | 402 | Krediti është nën 200 µ€. Shtoni kredi. |
not_found | 404 | Burimi nuk ekziston ose nuk është i juaji. |
not_ready | 409 | Transkripti nuk është gati ende. |
version_conflict | 409 | Versioni në If-Match është i vjetër. |
rate_limited | 429 | Më shumë se 60 kërkesa në minutë. Prisni sekondat e Retry-After. |
internal_error | 500 | Gabim nga ana jonë. Riprovoni me të njëjtin Idempotency-Key. |
Lista e plotë e kodeve dhe e gabimeve të një pune që dështon (invalid_audio, audio_too_long, source_unreachable e të tjera) është në udhëzuesin anglisht.
Kufijtë
| Kufiri | Vlera |
|---|---|
| Madhësia e skedarit | 2 GB për skedar |
| Kohëzgjatja e audios | 4 orë për punë |
| Kërkesat API | 60 në minutë për llogari |
| Prova falas në faqe | 60 sekondat e para të një skedari, deri në 100 MB, 3 në 24 orë për adresë IP |
Audioja ruhet 90 ditë në ruajtje të shpejtë dhe fshihet pas 365 ditësh. Mund ta fshini kurdo me DELETE /v1/transcriptions/{id}. Më shumë te ruajtja e të dhënave.