Rauntímauppskrift berst tvisvar: fyrst sem drög sem breytast stöðugt, síðan sem endanleg lína sem gerir það ekki. Teiknaðu drögin sem skiptanlegan hala og endanlegu línuna sem sögu, og restin af lifandi textaskjá er pípulagnavinna.
Rauntímalota er það minnsta sem VocalStack API gerir. Þjónninn þinn gerir eitt HTTP-kall og fær til baka undirritaða streamUrl. Viðskiptavinurinn þinn opnar WebSocket-tengingu við þá slóð, sendir hrátt hljóð um leið og það er tekið upp og les uppskriftina til baka sem JSON-textarramma aðeins á eftir hátalaranum. Þegar hljóðið endar sendirðu eof; þjónninn skolar biðminnið, skilar tölfræði lotunnar og lokar.
Ekkert er geymt. Uppskriftin er aðeins til á vírnum, sem er það sem textaskjár, yfirlag á fundi eða raddeiginleiki í þínu eigin forriti vill. Ef þú vilt geyma orðin, deila þeim og flytja þau út síðar, þarftu í staðinn umritun í beinni og útsendingu; síðasti kaflinn vísar þangað.
Uppbyggingin er sú sem flest straumandi tal-API deila: auðkenningarskipti yfir HTTP, langlíf tenging þar sem hljóð fer inn og JSON kemur út, og einhver leið til að greina bráðabirgðatexta frá endanlegum texta. Heitin eru mismunandi milli API-anna; hreyfanlegu hlutirnir eru nánast alltaf þeir sömu. Hér, í röð:
POST /v1/realtime/sessions með API-lyklinum þínum, á þjóninum þínum. Svarið er { sessionId, streamUrl, expiresAt }.streamUrl.session.loading-hjartslátt þar til session.ready kemur.transcript.draft (skiptu út halanum) og transcript.committed (bættu við endanlegri línu).{"type":"eof"}; lestu session.done.Aðeins skref 1 þarf lykilinn þinn, þannig að það gerist á þjóninum þínum.
Búðu til lykil undir Developers, API keys með heimildinni Write. Lykill er ætlaður þjónum og skriptum: API-ið hafnar lykli sem er sendur af vefsíðu eða í slóð, þannig að þú býrð til lotuna á bak við eigin leið, og síðan sér vefsíðan aðeins streamUrl.
Slóðin ber sitt eigið auðkenni vegna takmörkunar í vafranum sem vert er að þekkja almennt. Smíðin WebSocket í vafranum getur ekki sett hausa í beiðnir, þannig að ekki er hægt að setja lykil í Authorization-haus; sérhvert tal-API sem vafrinn notar auðkennir tenginguna með skammlífu auðkenni í slóðinni (eða falið í heiti undirsamskiptareglu). Skammlífið skiptir máli: slóðir enda í skrám og ferli, þannig að það sem þær bera verður að renna út. Tengstu áður en expiresAt rennur út.
SDK-in eru sótt úr Developers-svæðinu, ekki af pakkaskrá; flýtileiðbeiningarnar sýna uppsetningarskipanirnar fyrir þetta umhverfi í kaflanum Uppsetning. Með TypeScript SDK lítur leið á þjóninum svona út:
// On your server, behind a route of yours (POST /api/transcribe, say):
import { VocalStack } from '@vocalstack/sdk';
const vs = new VocalStack({ baseUrl: '{{api}}', apiKey: process.env.VOCALSTACK_API_KEY });
export async function createStream(): Promise<{ sessionId: string; streamUrl: string }> {
const { sessionId, streamUrl } = await vs.realtime.createSession({ spokenLanguage: 'en' });
return { sessionId, streamUrl };
}
Án SDK er þetta eitt fetch-kall. Þetta biður líka um spænsku og slekkur á setningasamsetningu, til samræmis við vírsýnið hér neðar:
const res = await fetch('{{api}}/v1/realtime/sessions', {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.VOCALSTACK_API_KEY}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ spokenLanguage: 'en', translate: ['es'], sentences: { enabled: false } }),
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`); // the error envelope: { code, message, … }
const { streamUrl } = (await res.json()) as { sessionId: string; streamUrl: string; expiresAt: string };
Sérhver valkostur hefur sjálfgefið gildi. Þeir sem vert er að þekkja strax eru spokenLanguage (sleppirðu honum greinist tungumálið sjálfkrafa), translate (listi yfir markmálin), speakerLabels og latencyMode; hitt kemur til sögunnar þegar þörf er á því hér að neðan.
Afhentu streamUrl hverju því sem tekur upp hljóðið; frá þessu stigi kemur enginn lykill við sögu.
Tengstu við streamUrl eins og hún er, án nokkurs hauss. Á meðan lotan ræsist sendir þjónninn session.loading-hjartslátt. Síðan kemur session.ready. Sendu ekkert hljóð fyrr. Hljóð sem er sent of snemma er ekki geymt til síðari nota; þjónninn lítur á það sem villu, sendir error-ramma og lokar tengingunni.
Þegar tengingin er opin tekur sérhver villa form þess ramma: {"type":"error","code":"…","message":"…","retryable":true|false}. code er lokaður listi (invalid_audio, invalid_options, invalid_token, capacity_exceeded, session_expired, slow_consumer, engine_unavailable, internal), message er ætlað atvikaskrám þínum, og retryable segir þér hvort vert sé að prófa nýja lotu. Meðhöndlaðu hann strax frá fyrsta ramma:
const ws = new WebSocket(streamUrl); // the token is in the URL: no header
ws.binaryType = 'arraybuffer';
ws.addEventListener('message', (m: MessageEvent) => {
if (typeof m.data !== 'string') return;
const f = JSON.parse(m.data);
switch (f.type) {
case 'session.loading':
console.error('starting…');
break;
case 'session.ready':
startMicrophone(); // send audio only after session.ready
break;
case 'error':
console.error(`${f.code}: ${f.message}`);
if (f.retryable) scheduleReconnect(); // a new session, with backoff; see "Stop"
break;
}
});
Tvennt til viðbótar þarf hrár viðskiptavinur að sjá um sjálfur; SDK-in gera það nú þegar. Í fyrsta lagi ber hafnað uppfærsla venjulega HTTP-villuumslag API-sins: 401 invalid_token, 410 session_expired eða 503 unavailable með Retry-After-haus sem þarf að virða.
Í öðru lagi lítur mettun út eins og þögn, ekki villa. Berist engin rammi innan 30 sekúndna skaltu meðhöndla það sem capacity_exceeded og tengjast aftur; ný lota kemst oftast í gegn. Ef hjartsláttarrammar halda áfram að berast en session.ready kemur aldrei, skaltu meðhöndla það sem engine_unavailable, sem einnig er hægt að endurreyna. Í SDK-unum skilar RealtimeSession.connect þegar session.ready kemur og mistekst eftir readyDeadlineMs með réttum kóða.
Þegar session.ready berst býst þjónninn við hljóði í þeim takti sem talað er.
Hljóð fer inn sem tvíundarrammar af hráu PCM: 32 bita flotgildi, little-endian, 16 kHz, einrása (pcm_f32le, sjálfgefið audio-snið lotunnar). Sendu það um leið og það er tekið upp, í bútum upp á um 100 til 250 ms, án þess að bíða eftir uppskrift á milli búta.
Það borgar sig að reikna bæti einu sinni. 16.000 sýni á sekúndu með 4 bætum hvert eru 64.000 bæti á sekúndu, þannig að 100 ms eru 6.400 bæti og 250 ms eru 16.000. Talgreiningarlíkön eru oft þjálfuð á 16 kHz einrása hljóði, og þess vegna biðja svo mörg rauntíma-API um það, en hljóðnemar og vafrar taka upp á 44,1 eða 48 kHz og oft í steríó. Eitthvað þarf að endursýnataka hljóðið og leggja rásirnar í eina áður en bætin fara út: hér er það ffmpeg, en í vafranum sér hljóðnemahjálp SDK-sins um það. Sendir þú 48 kHz hljóð en lýsir því sem 16 kHz mistekst það ekki; það umritar tal í hægagangi, illa.
Í Node er ffmpeg einfaldasta hljóðtökutækið:
import { spawn } from 'node:child_process';
function startMicrophone(): void {
// Raw PCM: f32le, 16 kHz, mono, as binary frames. macOS: -f avfoundation -i :0, Linux: -f pulse -i default.
const mic = spawn('ffmpeg', ['-loglevel', 'error', '-f', 'avfoundation', '-i', ':0', '-f', 'f32le', '-ac', '1', '-ar', '16000', '-']);
mic.stdout.on('data', (chunk: Buffer) => {
// Backpressure: bytes the socket has accepted but not yet put on the wire.
// Two seconds of audio (128,000 bytes) queued means the link, not the server, is behind.
if (ws.bufferedAmount > 128_000) console.error(`link behind by ${ws.bufferedAmount} bytes`);
ws.send(new Uint8Array(chunk));
});
mic.on('close', () => ws.send(JSON.stringify({ type: 'eof' })));
process.once('SIGINT', () => mic.kill());
}
ws.send bíður aldrei; það setur í biðröð. bufferedAmount er stærð þeirrar biðraðar, og á heilbrigðri tengingu er hún nálægt núlli. Í beinni er ekki hægt að gera hlé á hljóði, þannig að gagnlegasta viðbragðið við vaxandi biðröð er að taka eftir henni, ekki henda römmum. Þjónninn beitir líka bakþrýstingi. Hann tekur við hljóði nokkrum sekúndum á undan rauntíma (limits.audioBufferS) og hættir síðan að lesa þar til hann hefur náð upp. Á bakleiðinni er staðfestingum raðað í biðröð án taps á meðan drögum er slegið saman, þannig að hægur lesandi sér færri en ferskari drög. Viðskiptavinur sem ræður ekki við þetta, jafnvel með staðfestingunum, fær error-ramma með slow_consumer og retryable: false, og er aftengdur. Sjáir þú hann er skilaboðahandlarinn þinn að gera of mikið.
Í vafra biður streamMicrophone úr @vocalstack/sdk/mic um hljóðnemann, endursýnatekur í 16 kHz og sendir bútana ásamt clock-römmum, sem gera þjóninum kleift að merkja eyður þegar vafrinn heftir flipann. Síðan fær vefsíðan lotuna frá leiðinni þinni og sér aldrei lykilinn:
import { RealtimeSession } from '@vocalstack/sdk';
import { streamMicrophone } from '@vocalstack/sdk/mic';
export async function transcribeMicrophone(
stream: { sessionId: string; streamUrl: string },
render: (committed: string, tail: string) => void,
): Promise<void> {
const session = await RealtimeSession.connect(stream);
const mic = await streamMicrophone(session); // asks for the microphone, sends 16 kHz PCM
document.querySelector('#stop')?.addEventListener('click', () => {
void mic.stop();
session.eof();
});
let committed = '';
for await (const ev of session) {
if (ev.type === 'transcript.draft') render(committed, ev.segments.map((s) => s.text).join(' '));
if (ev.type === 'transcript.committed') {
committed += `${ev.segment.text} `;
render(committed, '');
}
}
}
Þessi undirskrift render(committed, tail) er allt notendaviðmótsmynstrið; næsti kafli útskýrir af hverju.
Tvær tegundir ramma byggja upp uppskriftina, og það að rugla þeim saman eru algengustu mistökin.
Fyrst, af hverju eru þær tvær yfirhöfuð. Talgreiningarlíkan ákvarðar hvað orð var að hluta út frá orðunum sem á eftir koma: „I scream“ og „ice cream“ hljóma eins þangað til setningin heldur áfram. Rauntímagreinir gefur því fyrst ágiskun og endurskoðar hana síðan þegar meira hljóð berst, og festir hana ekki fyrr en hann hefur heyrt nóg. Sérhvert straumandi tal-API sýnir þetta á einhvern hátt. Sum kalla ágiskanirnar „interim“ eða „partial“ niðurstöður og merkja þær föstu með is_final; orðaforðinn er mismunandi, agareglan ekki. Hér er ágiskunin drög og fasti textinn staðfesting.
transcript.draft er lifandi halinn: orðin sem heyrst hafa frá síðustu staðfestingu, og þau eru enn á hreyfingu. Hvert drag kemur í stað þess síðasta í heild. Teiknaðu halann aftur með segments; bættu honum aldrei við. Drög bera ekki seq af hönnun, þannig að ekki reyna að para þau við staðfestingarnar sem þau verða að.
transcript.committed er endanlegt: eitt segment með seq (0, 1, 2, án eyða), start, end, text, speaker og words með tímasetningum hvers orðs. Bættu því við og fjarlægðu drögin sem það nær yfir; næsta drag byrjar þar sem þessi staðfesting endaði.
committed: [seq 0] [seq 1]
draft: "how are" ← frame 1
draft: "how are you do" ← frame 2 REPLACES frame 1
committed: [seq 0] [seq 1] [seq 2 "How are you doing?"]
← the commit consumes the tail; drop the draft
Viðmót sem bætir við drögum sýnir afrituð, flöktandi orð. Textaskjárinn þinn fær stam sem ræðumaðurinn hafði aldrei. Teiknaðu allar staðfestu einingarnar, síðan nýjasta dragið á eftir þeim, og hreinsaðu dragið þegar staðfesting kemur. Staðfesting getur innihaldið orð sem komu aldrei fram í neinu dragi.
Hér eru nokkrar sekúndur af tali á vírnum, úr fetch-dæminu að ofan (translate: ["es"], setningasamsetning slökkt):
{"type":"session.loading"}
{"type":"session.ready"}
{"type":"transcript.draft","segments":[{"start":0.0,"text":"Good","speaker":0,"tentative":true}]}
{"type":"transcript.draft","segments":[{"start":0.0,"text":"Good morning every","speaker":0,"tentative":true}]}
{"type":"transcript.committed","segment":{"seq":0,"start":0.0,"end":1.4,"text":"Good morning, everyone.","speaker":0,"words":[{"start":0.0,"end":0.3,"text":"Good"},{"start":0.3,"end":0.8,"text":"morning,"},{"start":0.8,"end":1.4,"text":"everyone."}]}}
{"type":"transcript.translated","seq":0,"translations":{"es":"Buenos días a todos."}}
{"type":"transcript.draft","segments":[{"start":1.6,"text":"Thanks for","speaker":0,"tentative":true}]}
Tveir rammar til viðbótar standa í kringum þessa. transcript.amended leiðréttir staðfesta einingu á sínum stað (sama seq, nýr text eða speaker) þegar orð í henni hefur verið leiðrétt; settu það á línuna sem þú hefur þegar teiknað. transcript.words streymir tímasetningum orða fyrir halann rétt áður en hann er staðfestur. Það þarf events.words, sem er sjálfgefið á; staðfesting inniheldur alltaf words, hverjar sem atburðastillingarnar eru. Loks skaltu hunsa rammategundir sem þú þekkir ekki: nýjar bætast við án fyrirvara.
Hér er lesálkan úr Python SDK, með terminalinn sem textaskjá. Setningasamsetning er líka slökkt, þannig að þýðingar koma sem transcript.translated, og error-rammi kemur fram sem VocalStackError sem er varpað út úr lykkjunni:
import asyncio
import os
from vocalstack import RealtimeSession, VocalStack, VocalStackError
MIC = ["ffmpeg", "-loglevel", "error", "-f", "avfoundation", "-i", ":0",
"-f", "f32le", "-ac", "1", "-ar", "16000", "-"]
async def main() -> None:
async with VocalStack("{{api}}", api_key=os.environ["VOCALSTACK_API_KEY"]) as vs:
created = await vs.realtime.create_session(
spoken_language="en", translate=["es"], sentences={"enabled": False}
)
session = await RealtimeSession.connect(created.session_id, created.stream_url)
mic = await asyncio.create_subprocess_exec(*MIC, stdout=asyncio.subprocess.PIPE)
async def feed() -> None:
assert mic.stdout is not None
while chunk := await mic.stdout.read(8000): # 125 ms of audio
await session.send_audio(chunk)
await session.eof() # the server flushes, then sends session.done
feeder = asyncio.create_task(feed())
try:
async with session:
async for event in session:
if event.type == "transcript.draft":
# The live tail: replace it, never append it.
tail = " ".join(s.text for s in event.segments)
print(f"\r\x1b[K\x1b[2m{tail}\x1b[0m", end="", flush=True)
elif event.type == "transcript.committed":
# Final: append it (seq 0, 1, 2...), and the tail it covers is gone.
print(f"\r\x1b[K{event.segment.text}", flush=True)
elif event.type == "transcript.translated":
print(f" es: {event.translations.get('es')}")
await feeder
print(f"done ({session.done_reason})", session.stats)
except VocalStackError as e:
# An `error` frame mid-stream, or a refused connect.
print(f"\nerror {e.code}: {e.message} (retryable: {e.retryable})")
finally:
if mic.returncode is None:
mic.kill()
asyncio.run(main())
Þýðing eftir einingu, eins og hér að ofan, er einfalda formið. Formið sem lesendur kjósa fær sinn eigin kafla.
translate: ["es", "fr"] biður um þýðingar á meðan lotan gengur. Hvernig þær berast fer eftir setningasamsetningu (sentences: { enabled: true }), sem er sjálfgefið á.
Með slökkt á setningum fylgir hverri staðfestingu augnabliki síðar rammi transcript.translated sem nefnir seq hennar og ber translations lyklaða eftir tungumáli; festu þær við þá einingu. Þetta er það sem dæmin hingað til hafa notað. Það er einfalt og les eins og staðfestingaskrá: ein eining, ein þýðing, og setningamörkin þar sem ræðumaðurinn dró andann.
Með setningum á samsetur þjónninn staðfestingarnar í heilar setningar og þýðir þær, sem les mun betur. Sérhver transcript.sentence-rammi er heil skipti á einni setningu, lykluð eftir sentenceId. status fer úr open í closed (eða dropped, sem þýðir að henda henni), og translations eru til bráðabirgða meðan setningin er opin og endanlegar þegar hún lokast. Uppfærðu eftir sentenceId, bættu aldrei við. TypeScript SDK-ið sendir með applySentence nákvæmlega í þetta:
import { applySentence, type SentenceSegment } from '@vocalstack/sdk';
let sentences: SentenceSegment[] = [];
for await (const ev of session) {
if (ev.type !== 'transcript.sentence') continue;
sentences = applySentence(sentences, ev.sentence); // upsert by sentenceId
if (ev.sentence.status === 'closed') console.log(`${ev.sentence.text}\n es: ${ev.sentence.translations?.es}`);
}
Þýðingar fylgja uppskriftinni eftir með einni eða tveimur sekúndum. Ef þú vilt að þær séu líka lesnar upp skaltu bæta við voice: ["es"] (það þarf setningar). Spænska fyrir hverja lokaða setningu berst þá sem transcript.voice-rammi, með hljóðbútnum sem base64 Ogg/Opus á 24 kHz einrása. Löng setning kemur í bútum (part af parts) til að spila hvern á eftir öðrum. Bráðabirgðatexti er aldrei lesinn upp. Leiðbeiningarnar um lifandi þýðingu fjalla um raddir í smáatriðum, og hvert sekúndurnar fara útskýrir tafakeðjuna.
Markmálin eru ekki föst við stofnun.
Textarammi session.update breytir spokenLanguage, translate, speakerLabels, latencyMode, voice eða voiceByLang frá þeim punkti áfram; reitir sem þú sleppir haldast óbreyttir.
{"type":"session.update","translate":["es","fr"],"latencyMode":"fast"}
SDK-in bjóða það sem session.update(…). translate kemur í stað alls listans, og [] slekkur á þýðingu. Hlutar sem þegar hafa borist eru ekki umritaðir eða þýddir aftur, og kveikirðu á merkingum mælenda eru mælendur aðeins merktir frá þeim punkti. Það sem hefur verið sagt er áfram sagt; stillingarnar gilda aðeins um það sem kemur næst.
Þegar hljóðið endar sendirðu {"type":"eof"} (session.eof() í SDK-unum). Þjónninn umritar það sem hann á enn eftir, sendir síðustu staðfestingarnar, síðan session.done og lokar. Sendir þú cancel í staðinn stöðvast allt þegar í stað og afgangurinn er felldur niður.
{"type":"session.done","reason":"eof","stats":{"audioS":30.0,"processingS":30.6,"realtimeFactor":0.98}}
reason er eof, max_duration (lotan náði hámarkslengd áætlunarinnar og keyrði sömu lokauppskolun) eða cancelled. stats er mælt af þjóninum: audioS er það hljóð sem þú sendir, processingS er tíminn á veggklukkunni sem lotan stóð, og realtimeFactor er hlutfall þeirra. Lifandi lota lendir rétt undir 1, því veggklukkutíminn inniheldur lokauppskolunina eftir eof-ið þitt. Tala langt þar fyrir neðan þýðir að lotan tók verulega lengri tíma en hljóðið stóð; það fyrsta sem þarf að skoða er hversu hratt hljóðið barst. Fyrir skrá segir sama tala hversu sinnum hraðar en spilun uppskriftin kom til baka.
Lota stendur í mesta lagi limits.maxDurationS úr GET /v1/realtime/capabilities, og hljóð hennar eyðir inneigninni þinni á sekúndu á lifandi gjaldi áskriftarleiðarinnar, með sama 5 mínútna lágmarki og sérhver umritun: hver lota sem tekur á móti hljóði er rukkuð um minnst 5 mínútur (verð). Sama tenging tekur líka á móti heilli skrá: búðu til lotuna með input: { kind: "file" }, sendu eigin bæti skráarinnar (mp3, wav, flac, ogg, m4a) og eof, og uppskriftin kemur til baka margfalt hraðar en hljóðið spilast, með transcript.progress-römmum á leiðinni.
Engin endurupptaka er til. Rofni tengingin endar lotan; að tengjast aftur þýðir nýja lotu, rukkaða með sitt eigið lágmark, og streymi aftur frá lifandi uppsprettu þinni, með seq aftur í 0. Endurtengingin sjálf er venjulegt mynstur fyrir hvaða socket-API sem er, og þess virði að gera rétt einu sinni:
retryable: true á error-ramma, 503 með Retry-After, eða 30 sekúndur án ramma, sem stendur fyrir capacity_exceeded. invalid_token og session_expired þýða nýja streamUrl frá þjóninum þínum, ekki aðra tilraun með þeirri gömlu.Haltu lotu-útbúandi leiðinni ódýrri í kalli og meðhöndlaðu lokun sem nýja byrjun. Að geta ekki haldið áfram er verðið fyrir að geyma ekkert; ef uppskriftin þarf að lifa lengur en tengingin, geymdu hana.
Þegar þú vilt að sami straumur sé geymdur sem upptaka, leiðréttur síðar, fluttur út og lesinn af áhorfendum á eigin tungumálum, birtu hann sem umritun í beinni í staðinn. Leiðbeiningarnar um að fara í beina útsendingu fara yfir sama PCM-socket í gegnum umritun og útsendingartengil, og leiðbeiningarnar um hlustun sýna hlið áskrifandans, þar sem hver hlustandi velur tungumál og seinni komumaður nær upp með lastSeq-bendi. Hvernig það lítur út fyrir herbergi er efni eins tengils, allra tungumála.
Fyrir skrá sem þú vilt geyma er geymda leiðin umritun skráar með API. Fullt römmasafnið, með hverjum reit, er leiðbeiningin Streymdu hljóðnema í API-tilvísuninni.