Lataus palauttaa työn, ei litteraattia. Lisää Idempotency-Key jokaiseen luontikutsuun, jotta uudelleenyritys ei koskaan luo tai veloita toista litterointia. Kysele sitten työn tilaa, kunnes se on valmis, ja lue lauseet, sanat tai vienti samasta tunnisteesta.
Tiedoston litterointi on yksinkertaisin asia, jonka VocalStackin API tekee, ja sillä on asynkronisen työn muoto: lataa, odota, lue. Lataus vastaa heti litteroinnilla ja sen työllä, mutta vielä ilman sanoja. Kun ohjelmasi kysyy työn tilaa, palvelin litteroi tiedoston moninkertaisesti nopeammin kuin ääni toistuu. Kun tila on done, luet litteraatin lauseina puhujineen ja ajoituksineen tai viet sen tiedostona yhdessä neljästä muodosta, joko puhutulla kielellä tai käännettynä.
Tämä muoto siirtyy mihin tahansa erien käsittelyyn tarkoitettuun API:in: luonti, joka palauttaa tunnisteen, tila jota kysellään ja tulos jonka haet jälkeenpäin. Vain kenttien nimet ovat VocalStackille tyypillisiä, ja tämä opas käy ne läpi TypeScript- ja Python-SDK:illa sekä HTTP-kutsuilla silloin, kun ne auttavat.
Avaa hallintapaneelista Developers → API keys ja luo avain oikeuksilla Read ja Write. Avain näytetään vain kerran; kopioi se ympäristömuuttujaan:
export VOCALSTACK_API_KEY="<your API key>"
Avain on tarkoitettu palvelimille ja skripteille. Kaikki, mikä toimitetaan selaimeen, toimittaa avaimen mukanaan, joten pidä se pois sivukoodista ja URL-osoitteista, jotka päätyvät lokeihin. Jos haluat suoratoiston selaimesta, palvelimesi luo sille reaaliaikaisen istunnon; siitä kertoo toinen opas.
Jokainen kutsu tunnistautuu avaimella bearer-tokenina:
Authorization: Bearer vs_key_...
Kun avain on ympäristössä, seuraavaksi tarvitaan asiakas, joka lähettää sen.
SDK:ita on kaksi. Kummassakin on yksi asiakasobjekti, VocalStack, jonka metodit on ryhmitelty resurssien mukaan: vs.transcriptions, vs.jobs, vs.recordings, vs.realtime ja muutama muu. TypeScript-SDK toimii Node 20:ssä tai uudemmassa sekä selaimissa; Python-SDK perustuu asyncioon ja vaatii Python 3.10:n tai uudemman.
SDK:t eivät ole npm:ssä eikä PyPI:ssa. API tarjoaa koontiversiot jokaiselle tilille. Asennuskomennot ympäristöösi sopivina ovat pikaoppaan Install-osiossa, ja ne lähettävät avaimesi vain API-isännälle. Jos et halua asentaa mitään, alla olevat HTTP-kutsut tarvitsevat vain curl:n ja jq:n.
Valitsit kumman tahansa, ensimmäinen kutsu on lataus.
POST /v1/transcriptions ottaa tiedoston multipart/form-data-muodossa: file-osan ja valinnaisen options-osan, joka sisältää JSON-dataa. Vastaus on 201 litterointeineen. Sen job.recordingId on tiedostosta tehdyn tallenteen tunniste, ja kaikki seuraavat kutsut osoitetaan sillä tunnisteella.
Ensimmäisellä ajolla kaksi asetusta ovat tärkeitä, molemmat kohdassa options.settings:
spokenLanguage: kielikoodi, esimerkiksi en. Jos jätät sen pois, kieli tunnistetaan. Sen nimeäminen parantaa tarkkuutta kohinaisilla tallenteilla ja voimakkaalla aksentilla.speakerLabels: auto, on tai off. Kun arvo on on, jokaisella lauseella on puhujanumero; arvolla auto nimiöt näkyvät vasta, kun toinen puhuja on kuultu.Useimmat ääni- ja videotiedostot hyväksytään: mp3, wav, flac, ogg, m4a, mp4 ja muut. Video säilyttää vain äänensä. Muoto vaikuttaa tarkkuuteen vähän, koska puheentunnistus toimii 16 kHz:n monona ja kaikki muunnetaan siihen saapuessaan. 48 kHz:n stereo-WAV, jossa yksi henkilö puhuu, kuluttaa suurimman osan tavuistaan hiljaisuuden kuvaamiseen korkealla tarkkuudella. Se litteroituu täsmälleen yhtä hyvin kuin saman puheen 64 kbps:n mono-MP3 tai Opus-tiedosto, mutta se on noin kaksikymmentä kertaa suurempi, joten sen lataus kestää kaksikymmentä kertaa kauemmin. Jos muunnat ennen latausta, kaksi asetusta kannattaa muuttaa: mono ja kohtuullinen bittinopeus.
Lähetä Idempotency-Key jokaisen latauksen mukana ja ymmärrä, miksi. Verkon aikakatkaisu ei kerro, mitä palvelin teki: se ei ehkä koskaan nähnyt pyyntöä, tai se tallensi tiedoston ja jonotti työn ennen kuin vastaus katosi. Sokea uudelleenyritys voi siis aiheuttaa toisen litteroinnin ja toisen veloituksen. Avain ratkaisee tämän. Kun pyyntö toistetaan samalla avaimella ja samalla rungolla 24 tunnin sisällä, palvelin palauttaa ensimmäisen vastauksen eikä luo mitään, mikä muuttaa ”vähintään kerran” muotoon ”käytännössä kerran”. Sääntö on siis yksi avain yhtä loogista latausta kohden, luotu ennen ensimmäistä yritystä ja käytetty uudelleen jokaisessa uudelleenyrityksessä. Uusi avain jokaiselle yritykselle tekisi jokaisesta uudelleenyrityksestä uuden latauksen, eikä otsake suojaisi mitään.
import { VocalStack } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: '{{api}}', apiKey: process.env.VOCALSTACK_API_KEY });
const uploadKey = crypto.randomUUID(); // one key for this upload, however many attempts it takes
const tx = await vs.transcriptions.create(
{ file: './talk.mp3', name: 'Talk', settings: { spokenLanguage: 'en', speakerLabels: 'on' } },
{ idempotencyKey: uploadKey },
);
const recordingId = tx.job!.recordingId;
import asyncio
import os
import uuid
from vocalstack import VocalStack
async def main() -> None:
upload_key = str(uuid.uuid4()) # one key for this upload, however many attempts it takes
async with VocalStack("{{api}}", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
tx = await vs.transcriptions.create(
file="./talk.mp3",
name="Talk",
settings={"spokenLanguage": "en", "speakerLabels": "on"},
idempotency_key=upload_key,
)
assert tx.job is not None
recording_id = tx.job.recording_id
Sama kutsu curl-komennolla:
API={{api}}
AUTH="Authorization: Bearer $VOCALSTACK_API_KEY"
UPLOAD_KEY=$(uuidgen)
REC=$(curl -sSf "$API/v1/transcriptions" -H "$AUTH" -H "Idempotency-Key: $UPLOAD_KEY" \
-F file=@talk.mp3 \
-F 'options={"name":"Talk","settings":{"spokenLanguage":"en","speakerLabels":"on"}}' \
| jq -r .job.recordingId)
201 tulee ennen kuin yhtäkään sanaa on litteroitu; saat käsiisi työn tunnisteen, joten seuraava askel on odottaa sitä.
Työ siirtyy tilojen queued → transcribing → done läpi, tai päätyy tilaan failed, mukanaan error-viesti ja, kun syy on koneluettava, errorCode. Neljäs tila, cropping, tarkoittaa, että työ on pysäytetty odottamaan, kunnes joku valitsee, mikä tallenteen osa litteroidaan, kuten hallintapaneelin rajausvaihe tekee. Tavallinen lataus ei koskaan siirry siihen, vaikka jobs.wait palautuu myös silloin, kun työ saavuttaa sen. Lue GET /v1/recordings/{id}/job, kunnes tila on done tai failed.
Webhookeja ei ole, ja se on tietoinen valinta. Webhook vaatii julkisen HTTPS-päätepisteen omalla puolellasi, allekirjoituksen tarkistuksen, jotta kukaan muu ei voi kutsua sitä, ja uudelleenyrityskäytännön silloin, kun päätepisteesi on alhaalla. Se on paljon putkistoa työlle, joka kestää minuutin. Kysely ei tarvitse mitään tästä. Skriptisi kysyy ”valmis?” joka kahden sekunnin välein, kuin takapenkillä istuva lapsi, mutta toisin kuin lapsi se saa vastaukseksi prosenttiluvun.
Jo auki oleva verkkosivu voi seurata litteroinnin tapahtumasoketta (WS /v1/transcriptions/{id}/events, job.progress-kehys työn edetessä). Skriptille kysely on oikea työkalu.
SDK:t tekevät kyselyn: vs.jobs.wait lukee työn kahden sekunnin välein ja antaa jokaisen luvun onProgress-funktiolle (Pythonissa on_progress). Työn progress sisältää pct:n (0–100) ja processedS:n, litteroidut mediasekunnit tähän asti. Alla olevat ohjelmat tulostavat rivin vain silloin, kun tila tai prosenttiluku muuttuu.
let last = '';
const job = await vs.jobs.wait(recordingId, {
onProgress: (j) => {
const line = `${j.state} ${j.progress.pct}%`;
if (line !== last) console.log((last = line));
},
});
if (job.state !== 'done') throw new Error(`transcription ${job.state}: ${job.error ?? ''}`);
last = ""
def on_progress(j) -> None:
nonlocal last
line = f"{j.state} {j.progress.pct}%"
if line != last:
last = line
print(line)
job = await vs.jobs.wait(recording_id, on_progress=on_progress)
if job.state != "done":
raise RuntimeError(f"transcription {job.state}: {job.error}")
Huomaa, että wait ei heitä virhettä tilassa failed; se palauttaa työn ja jättää tarkistuksen sinulle. Kaksi sekuntia sopii puheelle. Kolmen tunnin tallenteelle tai satojen tiedostojen erälle pidennä väliä (intervalMs, Pythonissa interval_s) tai hidasta kyselyä työn edetessä: ensimmäiset luvut kertovat eniten, ja myöhemmät vahvistavat lähinnä, että palvelin on yhä kiireinen. Ilman SDK:ta kirjoitat saman silmukan käsin:
while :; do
JOB=$(curl -sSf "$API/v1/recordings/$REC/job" -H "$AUTH")
STATE=$(jq -r .state <<<"$JOB")
echo "$STATE $(jq -r .progress.pct <<<"$JOB")%"
case $STATE in
done) break ;;
failed) jq -r .error <<<"$JOB" >&2; exit 1 ;;
esac
sleep 2
done
Kun tila on done, litteraatti on valmis.
GET /v1/recordings/{id}/transcript palauttaa koko litteraatin yhdessä dokumentissa:
sentences: se, mitä ihmisille näytetään. Jokaisella on text, speaker sekä start ja end sekunteina.segments: moottorin rivit, joilla kullakin on sanatason ajoitukset kentässä words (jokaisella sanalla on text, start ja end).speakers: yksi merkintä puhujanumeroa kohden, nimineen name.Puhujat numeroidaan nollasta alkaen. speaker-arvo -1 tarkoittaa, että puhujanimiöt olivat pois päältä tallenteelta. Lisää ?lang=es, jos haluat lukea litteraatin espanjaksi käännettynä; lauseet palautuvat sillä kielellä.
const { sentences } = await vs.recordings.transcript(recordingId);
for (const s of sentences) console.log(s.speaker >= 0 ? `[speaker ${s.speaker}] ${s.text}` : s.text);
snapshot = await vs.recordings.transcript(recording_id)
for s in snapshot.sentences:
print(f"[speaker {s.speaker}] {s.text}" if s.speaker >= 0 else s.text)
curl -sSf "$API/v1/recordings/$REC/transcript" -H "$AUTH" \
| jq -r '.sentences[] | if .speaker >= 0 then "[speaker \(.speaker)] \(.text)" else .text end'
Kaksi ajoitustasoa palvelevat kahta tehtävää. Lauseet ovat lukemisen yksikkö, joten niitä tekstitys tai litteraattisivu näyttää. Sanat ovat osoittamisen yksikkö, joten karaoke-tyylinen korostus, hakuosuma tai klipin leikkaus kulkee segments[].words-kentän läpi. Vientimuodot rakentuvat lauseista.
GET /v1/recordings/{id}/export palauttaa oikean tiedoston. format on yksi arvoista srt, json, xlsx tai pdf, ja tiedosto sisältää kaikki korjaukset. Lisää lang= saadaksesi käännetyn tiedoston: vienti lukee silloin sillä kielellä, yksi rivi lausetta kohden, ja kaikki vielä kääntämättömät lauseet käännetään ensin.
SRT kannattaa tuntea ulkoa, koska se on tekstitysmuoto, jonka lähes jokainen soitin hyväksyy, ja se on pelkkää tekstiä. Tiedosto on lista tekstijaksoja (cue), jotka erotetaan tyhjillä riveillä; jokainen jakso koostuu laskurista, aikavälistä ja tekstistä. Aikaleimoissa millisekuntien edessä on pilkku, ranskalainen tapa, jota formaatti ei koskaan päässyt eroon:
1
00:00:01,000 --> 00:00:04,200
Welcome, everyone. Let's get started.
Yksi lause per jakso on oikea karkeus: jokainen jakso ehditään lukea sillä ajalla, jonka se on ruudulla, eikä mikään katkea kesken ajatuksen. Kun tallenteessa oli puhujanimiöt, jokaisen jakson teksti alkaa puhujan nimellä ja kaksoispisteellä.
TypeScript-SDK:ssa vs.recordings.export palauttaa Response-olion; lue sen tavut metodilla arrayBuffer(). Pythonissa se palauttaa bytes-arvon.
import { writeFile } from 'node:fs/promises';
const srt = await vs.recordings.export(recordingId, 'srt');
await writeFile('talk.srt', Buffer.from(await srt.arrayBuffer()));
const pdf = await vs.recordings.export(recordingId, 'pdf', { lang: 'es' });
await writeFile('talk.es.pdf', Buffer.from(await pdf.arrayBuffer()));
srt = await vs.recordings.export(recording_id, "srt")
with open("talk.srt", "wb") as f:
f.write(srt)
pdf = await vs.recordings.export(recording_id, "pdf", lang="es")
with open("talk.es.pdf", "wb") as f:
f.write(pdf)
asyncio.run(main())
curl -sSf "$API/v1/recordings/$REC/export?format=srt" -H "$AUTH" -o talk.srt
curl -sSf "$API/v1/recordings/$REC/export?format=pdf&lang=es" -H "$AUTH" -o talk.es.pdf
Litterointi tallennetaan, joten se näkyy hallintapaneelin kohdassa Transcriptions, sen voi jakaa linkillä ja sen voi lukea uudelleen milloin tahansa myöhemmin. Ydinkierto on siis: luo, odota, lue, vie. Loppu oppaasta käsittelee muunnelmat: verkossa jo olevan median, vastaan tulevat virheet ja sen, mitä tiedosto maksaa.
Jos media on jo verkossa, anna API:lle linkki, ja se hakee tiedoston itse: julkinen http(s)-osoite (mp3, video, podcast-jakso) tai YouTube-video. Kutsu on sama POST /v1/transcriptions, ja JSON-runko { "url": "..." } korvaa multipart-latauksen; name ja settings ovat ennallaan. SDK:ssa vs.transcriptions.createFromUrl(url) (TypeScript) ja vs.transcriptions.create(url=...) (Python) tekevät saman pyynnön.
Ennen minkään luomista GET /v1/transcriptions/probe?url= kertoo, mikä linkki on. Sen kind on yksi kolmesta:
file: tallenne, jolla on loppu; litteroi se täältä.live: YouTube-suoratoisto, loputon radiovirta tai HLS-soittolista ilman loppua. Live-linkki ei koskaan pääty, joten sitä ei voi litteroida tiedostona; käytä sitä lähteenä suoralle litteroinnille.unknown: palvelin ei osannut päättää.Kun linkki ei avaudu mediana lainkaan (esimerkiksi verkkosivu), ok on false ja problem selittää syyn yhdessä lauseessa. Probe on valinnainen ja rajattu 60 kutsuun minuutissa.
Linkkityö alkaa tilasta fetching ennen kuin se siirtyy tiloihin transcribing ja done. Hakemisen aikana progress.phase kertoo, mitä palvelin tekee: fetching, kun tavuja saapuu (kenttien fetchedBytes ja fetchTotalBytes kanssa, jos toinen pää kertoo koon), tai extracting, kun ääntä irrotetaan videosta. progress.phasePct kertoo, kuinka pitkällä vaiheessa työ on. Pitkä video vie aikansa, joten kysele muutaman sekunnin välein; vs.jobs.wait ottaa siihen intervalMs:n (Pythonissa interval_s).
const probe = await vs.transcriptions.probe(url);
if (!probe.ok) throw new Error(`that link did not open: ${probe.problem}`);
if (probe.kind === 'live') throw new Error('a live link: record it with a live transcription instead');
const linkKey = crypto.randomUUID();
const tx = await vs.transcriptions.createFromUrl(url, { name: 'From a link' }, { idempotencyKey: linkKey });
const job = await vs.jobs.wait(tx.job!.recordingId, {
intervalMs: 3000,
onProgress: (j) => {
const p = j.progress;
if (j.state === 'fetching') console.log(`${p.phase ?? 'fetching'} ${p.phasePct ?? 0}%`);
else console.log(`${j.state} ${p.pct}%`);
},
});
Litteroi linkki-ohjeessa on koko ohjelma molemmilla kielillä sekä HTTP:nä. Linkkien hakemiseen liittyy lisää virhetilanteita, joten tässä ovat ne virheet, joihin oikeasti törmäät.
Jokainen epäonnistunut kutsu vastaa yhdellä JSON-kääreellä. code on merkkijono, jonka perusteella haaroitat, ja status HTTP-tila; message on ihmisille ja requestId tukea varten. data:n alla retryable kertoo, voiko saman pyynnön lähettäminen uudelleen auttaa, ja details sisältää, mitä koodi tarvitsee. SDK:t heittävät yhden virhetyypin, VocalStackError:n, samoilla kentillä.
Kolme koodia kannattaa käsitellä heti ensimmäisenä päivänä.
402 insufficient_credit: tilin saldo on loppunut, eikä mitään veloitettu. details.balance on saldo dollareina ja details.topUpUrl kertoo, mistä saldoa ladataan. Kun kustannus tiedettiin etukäteen, kuten tiedoston pituuden ollessa tiedossa, details.required kertoo, mitä se olisi ollut.429 rate_limited: odota Retry-After-sekunnit. Kun details.metric on creates_per_hour, olet ylittänyt paketin luontinopeuden (uudet litteroinnit tunnissa), et purskerajaa; limit ja resetsAt kertovat rajan ja sen, milloin se vapautuu.503 unavailable: palvelu on hetkellisesti poissa käytöstä. Lähetä pyyntö uudelleen Retry-After-ajan jälkeen.SDK:t yrittävät uudelleen 429:n ja 503:n Retry-After-ajan jälkeen, oletuksena kaksi kertaa (maxRetries / max_retries asiakkaassa). 503:a kuitenkin yritetään uudelleen vain kutsulle, jonka voi turvallisesti lähettää kahdesti: GET-pyynnölle tai luonnille, jolla on Idempotency-Key. Luonti ilman avainta jätetään sinulle, koska SDK ei voi tietää, meneekö ensimmäinen yritys perille. Se on käytännön syy laittaa avain jokaiseen lataukseen.
Jokainen vastaus kantaa myös RateLimit-Limit-, RateLimit-Remaining- ja RateLimit-Reset-otsakkeita, joten kiireinen putki voi hidastaa ennen 429:ää.
{
"code": "insufficient_credit",
"status": 402,
"message": "this account has no credit left; add credit to continue",
"data": {
"retryable": false,
"details": { "balance": -0.42, "required": 0.075, "currency": "usd", "plan": "payg", "topUpUrl": "https://…" }
},
"requestId": "01J…"
}
Koko kooditaulukko on kohdassa Virheet, uudelleenyritykset ja rajat. Tuo 402 on ainoa rahaan liittyvä virhe, joten katsotaan, mitä tiedosto maksaa.
Tiedostot maksavat $0.20 tunnilta äänimateriaalia, veloitus sekunnin tarkkuudella ($0.0033 minuutilta) ja 5 minuutin vähimmäiskesto: 30 minuutin tiedosto maksaa $0.10. Se on kymmenen senttiä, vähemmän kuin kahvi, jonka juot, kun edistymispalkki täyttyy. Hinta on sama jokaisella paketilla ja sama API:n kautta kuin hallintapaneelissa. Uusi tili alkaa kertaluonteisella $5:n rekisteröitymiskrediitillä, eikä korttia tarvita.
Kolme paketin rajaa koskee tätä opasta.
GET /v1/realtime/capabilities ilmoittaa kenttänä limits.maxUploadBytes. Pienempi raja on voimassa, ja tiedosto, joka ylittää sen, hylätään 413:lla ennen kuin mitään litteroidaan. Pitkän tallenteen kohdalla tämä on yksi lisäsyy ladata pakattu mono-tiedosto WAV:n sijaan.GET /v1/billing kertoo pakettisi, saldosi ja rajasi milloin tahansa; koko taulukko on hinnoittelusivulla.
Seuraavat askeleet ovat äänen suoratoisto reaaliajassa, kun et voi odottaa tiedoston valmistumista, tai AI-agentin yhdistäminen samaan tiliin MCP-palvelimen kautta, jolloin agentti litteroi ja hakee puolestasi ilman yhtäkään yllä olevaa koodia.