Nakala ya wakati halisi hufika mara mbili: kwanza kama rasimu inayoendelea kubadilika, kisha kama mstari wa mwisho usiobadilika. Onyesha rasimu kama mkia unaoweza kubadilishwa na mstari wa mwisho kama historia, na kila kitu kingine cha kionyeshi cha manukuu mubashara ni mabomba ya kawaida tu.
Kipindi cha wakati halisi ndicho kitu kidogo kabisa ambacho API ya VocalStack hufanya. Seva yako hutuma wito mmoja wa HTTP na kupokea streamUrl iliyotiwa sahihi. Mteja wako hufungua WebSocket kwenda URL hiyo, hutuma sauti ghafi inaponaswa, na husoma nakala ya maneno kama fremu za maandishi za JSON, sekunde chache nyuma ya msemaji. Sauti ikiisha, unatuma eof; seva hukamilisha, huripoti takwimu alizopima kipindi, kisha hufunga.
Hakuna kinachohifadhiwa. Nakala ipo kwenye waya pekee, ambayo ndiyo mahitaji ya kionyeshi cha manukuu, mwingiliano wa mkutano au kipengele cha sauti ndani ya programu yako mwenyewe. Ukitaka maneno yahifadhiwe, yashirikiwe na kuhamishwa baadaye, unahitaji unukuzi mubashara na matangazo badala yake; sehemu ya mwisho inaelekeza huko.
Mpangilio huu ndio ambao API nyingi za unukuzi wa usemi wa wakati halisi hufuata: ubadilishanaji wa kitambulisho juu ya HTTP, soketi ndefu ambapo sauti huingia na JSON hutoka, na njia fulani ya kutofautisha maandishi ya muda kutoka maandishi ya mwisho. Majina hutofautiana kati ya API moja na nyingine; sehemu zinazosogea mara chache hutofautiana. Hapa, kwa mpangilio:
POST /v1/realtime/sessions kwa ufunguo wako wa API, kwenye seva yako. Jibu ni { sessionId, streamUrl, expiresAt }.streamUrl.session.loading hadi session.ready.transcript.draft (badilisha mkia) na transcript.committed (ongeza mstari wa mwisho).{"type":"eof"}; soma session.done.Hatua ya 1 pekee inahitaji ufunguo wako, kwa hiyo hufanyika kwenye seva yako.
Tengeneza ufunguo chini ya Developers, API keys wenye ruhusa ya Write. Ufunguo ni wa seva na hati za script pekee: API hukataa ufunguo unaotumwa kutoka ukurasa wa wavuti au ndani ya URL, kwa hiyo unaunda kipindi nyuma ya njia yako mwenyewe, na ukurasa huona streamUrl pekee.
URL hubeba tokeni yake yenyewe kwa sababu ya kikomo cha kivinjari ambacho kinafaa kujulikana kwa ujumla. Kijenzi cha WebSocket cha kivinjari hakiwezi kuweka vichwa vya ombi, kwa hiyo hakuna kichwa cha Authorization cha kuweka ufunguo ndani yake; kila API ya usemi inayofikiwa na kivinjari huthibitisha soketi yake kwa tokeni fupi ndani ya URL (au kuiingiza kwenye jina la subprotocol). Ufupi ndio kiini: URL huishia kwenye kumbukumbu na historia, kwa hiyo kilichomo lazima kiishe muda. Unganisha kabla ya expiresAt.
SDK zinapakuliwa kutoka eneo la Developers, si kutoka rejista ya vifurushi; sehemu ya Install ya mwongozo wa kuanza haraka inaonyesha amri za mazingira haya. Kwa SDK ya TypeScript, njia ya seva inaonekana hivi:
// 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 };
}
Bila SDK ni fetch moja. Hii pia inaomba Kihispania na inazima mkusanyiko wa sentensi, ikilingana na sampuli ya waya iliyo chini:
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 };
Kila chaguo lina thamani chaguo-msingi. Yanayofaa kujulikana mwanzoni ni spokenLanguage (ukiliacha, lugha hutambuliwa yenyewe), translate (orodha ya lugha lengwa), speakerLabels na latencyMode; mengine yataja yanapohitajika hapa chini.
Mpe streamUrl chochote kinachonasa sauti; kuanzia hapa, hakuna ufunguo unaohusika.
Unganisha kwenda streamUrl jinsi ilivyo, bila kichwa chochote. Kipindi kinapoanza, seva hutuma mapigo ya moyo ya session.loading. Kisha session.ready hufika. Usitume sauti kabla yake. Sauti iliyotumwa mapema haihifadhiwi kwa baadaye; seva huichukulia kama hitilafu, hutuma fremu ya error na kufunga soketi.
Soketi ikishafunguka, kila hitilafu huchukua umbo la fremu hiyo: {"type":"error","code":"…","message":"…","retryable":true|false}. code ni orodha iliyofungwa (invalid_audio, invalid_options, invalid_token, capacity_exceeded, session_expired, slow_consumer, engine_unavailable, internal), message ni kwa kumbukumbu zako, na retryable inakuambia kama kipindi kipya kinafaa kujaribiwa. Shughulikia tangu fremu ya kwanza:
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;
}
});
Mteja wa kawaida lazima ashughulikie mambo mawili zaidi mwenyewe; SDK tayari zinayafanya. Kwanza, uboreshaji ulioshindwa hubeba bahasha ya kawaida ya hitilafu ya HTTP ya API: 401 invalid_token, 410 session_expired, au 503 unavailable yenye kichwa cha Retry-After cha kuheshimu.
Pili, kujaa kunaonekana kama ukimya, si hitilafu. Ikiwa hakuna fremu yoyote inayofika ndani ya sekunde 30, ichukulie kama capacity_exceeded na uunganishe tena; kipindi kipya mara nyingi hupita. Ikiwa mapigo ya loading yanaendelea kufika lakini session.ready haifiki kamwe, ichukulie kama engine_unavailable, ambayo pia inaweza kujaribiwa tena. Katika SDK, RealtimeSession.connect hukamilika session.ready inapofika, na hushindwa baada ya readyDeadlineMs kwa msimbo sahihi.
Mara session.ready inapofika, seva inatarajia sauti kwa kasi ambayo inasemwa.
Sauti huingia kama fremu za binary za PCM ghafi: float ya biti 32, little-endian, 16 kHz, mono (pcm_f32le, umbizo chaguo-msingi la audio la kipindi). Itume inaponaswa, kwa vipande vya takriban 100 hadi 250 ms, bila kusubiri nakala kati ya vipande.
Hesabu ya baiti inafaa kufanywa mara moja. Sampuli 16,000 kwa sekunde kwa baiti 4 kila moja ni baiti 64,000 kwa sekunde, kwa hiyo 100 ms ni baiti 6,400 na 250 ms ni baiti 16,000. Miundo ya usemi kwa kawaida hufunzwa kwa 16 kHz mono, ndiyo maana API nyingi za wakati halisi zinauomba, wakati vipaza sauti na vivinjari hunasa kwa 44.1 au 48 kHz na mara nyingi kwa stereo. Kitu fulani lazima kibadilishe kiwango cha sampuli na kuunganisha vituo kuwa kimoja kabla baiti hazijatoka: hapa ni ffmpeg, na kwenye kivinjari ni msaidizi wa maikrofoni wa SDK. Kutuma sauti ya 48 kHz huku ukitangaza 16 kHz hakushindwi; inanukuu usemi wa mwendo wa polepole, vibaya.
Kwenye Node, ffmpeg ndicho kifaa rahisi zaidi cha kunasa:
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 haizuii; inapanga foleni. bufferedAmount ni ukubwa wa foleni hiyo, na kwenye kiungo chenye afya hukaa karibu na sifuri. Sauti ya moja kwa moja haiwezi kusimamishwa, kwa hiyo jibu linalofaa foleni inapokua ni kuitambua, si kuangusha fremu. Seva nayo hutumia backpressure. Hukubali sauti sekunde chache mbele ya muda halisi (limits.audioBufferS), kisha huacha kusoma hadi ifikie. Njia ya kurudi: thibitisho zinapangwa foleni bila kupotea huku rasimu zikiunganishwa, kwa hiyo msomaji polepole huona rasimu chache na mpya zaidi. Mteja asiyeweza kuendana hata na thibitisho hupokea fremu ya error yenye slow_consumer na retryable: false, kisha anakatwa; ukiona hili, kishughulikiaji chako cha ujumbe kinafanya kazi nyingi mno.
Kwenye kivinjari, streamMicrophone kutoka @vocalstack/sdk/mic huomba maikrofoni, hubadilisha kiwango cha sampuli kuwa 16 kHz, na kutuma vipande pamoja na fremu za clock, zinazoiwezesha seva kuweka alama za mapengo kivinjari kinapopunguza kasi ya kichupo. Ukurasa hupata kipindi kutoka njia yako na haumwoni ufunguo kamwe:
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, '');
}
}
}
Huo mstari wa render(committed, tail) ndio muundo mzima wa UI; sehemu inayofuata inaeleza kwa nini.
Fremu mbili ndizo zinazojenga nakala, na kuzichanganya ndicho kosa la kawaida zaidi.
Kwanza, kwa nini kuna mbili kabisa. Modeli ya usemi huamua neno lilikuwa lipi kwa kiasi kutokana na maneno yanayofuata: "I scream" na "ice cream" hufanana kwa sauti hadi sentensi iendelee. Kwa hiyo kitambua cha wakati halisi hutoa kubahatisha mapema, kisha hukirekebisha kadiri sauti zaidi inavyofika, na hutulia tu pale kinaposikia vya kutosha. Kila API ya unukuzi wa mtiririko huonyesha hili kwa namna fulani. Baadhi huita kubahatisha huko matokeo ya "interim" au "partial" na kuweka alama is_final kwenye yaliyotulia; msamiati unatofautiana, nidhamu haitofautiani. Hapa kubahatisha ni rasimu, na maandishi yaliyotulia ni thibitisho.
transcript.draft ni mkia mubashara: maneno yaliyosikika tangu thibitisho la mwisho, bado yanabadilika. Kila rasimu ni badiliko kamili la ile iliyotangulia. Chora upya mkia kwa segments zake; usiwahi kuiongeza. Rasimu hazina seq kwa makusudi, kwa hiyo usijaribu kuzilinganisha na thibitisho zinazozigeuka kuwa.
transcript.committed ni ya mwisho: segment moja yenye seq (0, 1, 2, bila mapengo), start, end, text, speaker na words zenye muda wa kila neno. Liongeze na uondoe rasimu inayolifunika; rasimu inayofuata huanzia pale thibitisho hili lilipomalizikia.
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
Rasimu inayoongezwa badala ya kubadilishwa huonyesha maandishi yaliyorudiwa na yanayomeremeta. Kionyeshi chako cha manukuu kitapata kigugumizi ambacho msemaji hakuwahi kuwa nacho. Onyesha sehemu zote zilizothibitishwa, kisha rasimu ya mwisho baada yake, na uifute rasimu pale thibitisho linapofika. Thibitisho linaweza kuwa na maneno ambayo hayakuwepo kwenye rasimu yoyote.
Hapa kuna sekunde chache za usemi kwenye waya, kutoka mfano wa fetch hapo juu (translate: ["es"], mkusanyiko wa sentensi umezimwa):
{"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}]}
Fremu mbili zaidi ziko karibu na hizi. transcript.amended hurekebisha sehemu iliyothibitishwa papo hapo (seq ile ile, text au speaker mpya) neno lake linaporekebishwa; itumie kwenye mstari uliokwisha kuuchora. transcript.words hutiririsha muda wa maneno ya mkia muda mfupi kabla ya kuthibitishwa. Inahitaji events.words, ambayo imewashwa kwa chaguo-msingi; thibitisho hubeba words zake siku zote, bila kujali chaguo za matukio. Mwisho, puuza aina za fremu usizozijua: mpya huongezwa bila taarifa.
Hapa ni mzunguko wa kusoma wa SDK ya Python, huku terminali ikiwa kionyeshi cha manukuu. Mkusanyiko wa sentensi umezimwa hapa pia, kwa hiyo tafsiri hufika kama transcript.translated, na fremu ya error inaibuka kama VocalStackError kutoka kwenye mzunguko:
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())
Tafsiri ya kila sehemu, kama hapo juu, ndiyo namna rahisi. Namna ambayo wasomaji wengi hupendelea inapata sehemu yake yenyewe.
translate: ["es", "fr"] huomba tafsiri kadiri kipindi kinavyoendelea. Jinsi zinavyofika inategemea mkusanyiko wa sentensi (sentences: { enabled: true }), ambao umewashwa kwa chaguo-msingi.
Ikiwa mkusanyiko wa sentensi umezimwa, kila thibitisho hufuatwa baada ya muda mfupi na fremu ya transcript.translated inayotaja seq yake na kubeba translations zilizopangwa kwa lugha; ziambatanishe kwenye sehemu hiyo. Ndivyo mifano yote hadi sasa ilivyotumia. Ni rahisi na inasomeka kama logi ya thibitisho: sehemu moja, tafsiri moja, mpaka wa sentensi popote mzungumzaji alipopumua.
Ikiwa mkusanyiko wa sentensi umewashwa, seva huunganisha thibitisho kuwa sentensi kamili na kutafsiri hizo, jambo ambalo husomeka vizuri zaidi. Kila fremu ya transcript.sentence ni badiliko kamili la sentensi moja, inayotambuliwa na sentenceId. status yake huenda kutoka open kwenda closed (au dropped, yaani itupwe), na translations zake ni za muda ikiwa open na za mwisho ikishafungwa. Sasisha kwa sentenceId (au uiongeze ikiwa haipo), usiiongeze tu. SDK ya TypeScript ina applySentence kwa kazi hii hasa:
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}`);
}
Tafsiri hufuata nakala kwa sekunde moja au mbili. Ukitaka zisomwe kwa sauti pia, ongeza voice: ["es"] (inahitaji sentensi). Kisha Kihispania cha kila sentensi iliyofungwa hufika kama fremu ya transcript.voice, ikibeba klipu kama Ogg/Opus ya base64 kwa 24 kHz mono. Sentensi ndefu hufika kwa vipande (part kati ya parts) vya kuchezwa mfululizo. Maandishi ya muda hayasomwi kamwe kwa sauti. Mwongozo wa tafsiri mubashara unaeleza sauti kwa kina, na sekunde zinakwenda wapi inaeleza mnyororo wa ucheleweshaji.
Lugha lengwa hazirekebishwi wakati wa kuunda kipindi.
Fremu ya maandishi ya session.update hubadilisha spokenLanguage, translate, speakerLabels, latencyMode, voice au voiceByLang kuanzia hapo kuendelea; sehemu usizozitaja hazibadiliki.
{"type":"session.update","translate":["es","fr"],"latencyMode":"fast"}
SDK zinaonyesha hili kama session.update(…). translate hubadilisha orodha nzima, na [] huzima tafsiri. Sehemu zilizokwisha kufika hazinukuliwi wala kutafsiriwa upya, na ukiwasha lebo za wasemaji, wasemaji hupewa lebo kuanzia hapo tu. Kilichokwisha semwa kinabaki kimesemwa; mipangilio inatawala kinachokuja tu.
Sauti ikiisha, tuma {"type":"eof"} (session.eof() kwenye SDK). Seva hunukuu kilichobaki, hutuma thibitisho la mwisho, kisha session.done na kufunga. Kutuma cancel badala yake husimamisha mara moja na kutupa kilichobaki.
{"type":"session.done","reason":"eof","stats":{"audioS":30.0,"processingS":30.6,"realtimeFactor":0.98}}
reason ni eof, max_duration (kipindi kimefika urefu wa juu wa mpango na kukamilisha kwa utaratibu uleule) au cancelled. stats hupimwa na seva: audioS ni sauti uliyotuma, processingS ni muda wa saa ya ukutani ambao kipindi kilikimbia, na realtimeFactor ni uwiano wao. Kipindi cha moja kwa moja hufika chini kidogo ya 1, kwa sababu muda wa saa ya ukutani unajumuisha umalizaji wa mwisho baada ya eof yako. Namba iliyo chini sana ya hapo inamaanisha kipindi kilichukua muda mrefu zaidi kuliko sauti ilivyodumu; mahali pa kwanza pa kuangalia ni kasi ambayo sauti ilifika. Kwa faili, namba hiyo hiyo inaonyesha mara ngapi nakala ilirudi kwa kasi kuliko uchezaji.
Kipindi kinadumu kwa upeo wa limits.maxDurationS kutoka GET /v1/realtime/capabilities, na sauti yake hutumia salio lako kwa kila sekunde kwa kiwango cha moja kwa moja cha mpango wako, kwa dakika 5 za chini kabisa kama unukuzi wowote: kila kipindi kinachopokea sauti hutozwa angalau dakika 5 (bei). Soketi hiyo hiyo pia inapokea faili zima: unda kipindi kwa input: { kind: "file" }, tuma baiti za faili yenyewe (mp3, wav, flac, ogg, m4a) na eof, na nakala hurudi mara nyingi zaidi kwa kasi kuliko sauti inavyochezwa, pamoja na fremu za transcript.progress njiani.
Hakuna kuendelea. Muunganisho uliokatika unamaliza kipindi; kuunganisha upya kunamaanisha kuunda kipindi kipya, kinachotozwa dakika zake za chini, na kutiririsha tena kutoka chanzo chako cha moja kwa moja, huku seq ikirudi 0. Kuunganisha upya ni mfumo wa kawaida wa API yoyote ya soketi, na unafaa kufanywa sawa mara moja:
retryable: true kwenye fremu ya error, 503 yenye Retry-After yake, au sekunde 30 bila fremu yoyote, ambazo zinawakilisha capacity_exceeded. invalid_token na session_expired zinamaanisha streamUrl mpya kutoka seva yako, si jaribio jingine kwa ile ya zamani.Fanya njia inayounda kipindi iwe nafuu kuitwa, na chukulia kufungwa kama mwanzo mpya. Kutokuwa na kuendelea ndiyo bei ya kutohifadhi chochote; nakala ikipaswa kuishi nje ya soketi, ihifadhi wewe mwenyewe.
Unapotaka mtiririko huo huo uhifadhiwe kama rekodi, urekebishwe baadaye, uhamishwe, na usomwe na hadhira katika lugha zao, uchapishe badala yake kama unukuzi mubashara. Mwongozo wa kwenda mubashara unapitia soketi hiyo hiyo ya PCM kupitia unukuzi na kiungo cha matangazo, na mwongozo wa kusikiliza unaonyesha upande wa msajili, ambapo kila msikilizaji huchagua lugha, na anayechelewa kujiunga hujipata kutoka kwenye kielekezi cha lastSeq. Hilo linavyoonekana kwa chumba ndicho mada ya Kiungo kimoja, kila lugha.
Kwa faili unalotaka kuhifadhi, njia iliyohifadhiwa ni kunukuu faili kwa API ya VocalStack. Marejeleo kamili ya fremu, kila sehemu ikiwemo, yako kwenye mwongozo wa Stream a microphone katika marejeleo ya API.