import { writeFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; const API = 'https://api.apify.com/v2'; const input = { urls: ['https://storage.googleapis.com/cloud-samples-data/speech/commercial_mono.wav'], timestamps: 'segment', }; export async function main() { if (!process.env.APIFY_TOKEN) throw new Error('Set APIFY_TOKEN first'); const headers = { Authorization: `Bearer ${process.env.APIFY_TOKEN}` }; async function request(path, options = {}) { const response = await fetch(`${API}${path}`, { ...options, headers: { ...headers, ...options.headers }, signal: AbortSignal.timeout(30_000) }); if (!response.ok) throw new Error(`Apify HTTP ${response.status}; inspect the existing run before retrying`); return response; } function incomplete(item) { return item.error || item.stop_processing || item.billing?.stop || ['partial', 'unknown'].includes(item.billing?.status); } // Send this POST once. A lost response requires checking Apify Console before another start. let run = (await (await request('/actors/andronixmd~speech-to-text-transcriber/runs?maxTotalChargeUsd=0.1&memory=4096&timeout=180', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(input), })).json()).data; console.log(`Run: ${run.id}`); const terminal = ['SUCCEEDED', 'FAILED', 'TIMED-OUT', 'ABORTED']; for (let attempt = 0; attempt < 12 && !terminal.includes(run.status); attempt++) { run = (await (await request(`/actor-runs/${run.id}?waitForFinish=20`)).json()).data; } if (!terminal.includes(run.status)) throw new Error(`Run ${run.id} is still ${run.status}; inspect it or continue GET polling, do not start again`); if (run.status !== 'SUCCEEDED') throw new Error(`Run ${run.id}: ${run.status}; inspect its saved output`); let offset = 0, failures = 0; while (true) { const response = await request(`/datasets/${run.defaultDatasetId}/items?format=json&offset=${offset}&limit=100`); const rows = await response.json(); for (const [pageIndex, row] of rows.entries()) { const index = offset + pageIndex; let item = row; if (!incomplete(row) && row.result_record_key) { const record = await (await request(`/key-value-stores/${run.defaultKeyValueStoreId}/records/${encodeURIComponent(row.result_record_key)}`)).json(); item = record.value; // Unwrap the saved result; this example does not verify its signature. } if (!item || incomplete(row) || incomplete(item) || typeof item.text !== 'string' || item.artifact_errors?.length) { console.error(`Item ${index} incomplete: ${item?.error ?? row.error ?? 'billing, content or subtitle publication requires inspection'}`); failures++; continue; } console.log(item.text); for (const extension of ['srt', 'vtt']) { const filename = `transcript-${String(index).padStart(3, '0')}.${extension}`; const subtitle = await request(`/key-value-stores/${run.defaultKeyValueStoreId}/records/${filename}`); await writeFile(filename, await subtitle.text()); console.log(`Saved ${filename}`); } } offset += rows.length; if (offset >= Number(response.headers.get('X-Apify-Pagination-Total'))) break; if (!rows.length) throw new Error('Dataset pagination stopped before all results were read'); } if (!offset) throw new Error('No results; inspect the run budget and saved output'); if (failures || offset !== input.urls.length) throw new Error('One or more results are incomplete; inspect this run before retrying'); } if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { main().catch(error => { console.error(error.message); process.exitCode = 1; }); }