|
| 1 | +/** |
| 2 | + * telnyx-agent stt — Speech-to-text transcription. |
| 3 | + * |
| 4 | + * Shells out to the telnyx CLI's `ai:audio transcribe` subcommand (the |
| 5 | + * OpenAI-compatible transcription endpoint) and surfaces the resulting |
| 6 | + * transcription text to the caller in either human-readable or JSON form. |
| 7 | + * |
| 8 | + * The transcribe endpoint accepts a hosted audio file URL (`--file-url`) |
| 9 | + * and returns the transcribed text. Note: the Go CLI's separate |
| 10 | + * `speech-to-text retrieve-transcription` command is a WebSocket streaming |
| 11 | + * API (no audio-URL support), so URL-based transcription must go through |
| 12 | + * `ai:audio transcribe`. |
| 13 | + */ |
| 14 | + |
| 15 | +import { telnyxCli, TelnyxCLIError } from "../telnyx-cli.ts"; |
| 16 | +import { printSuccess, printError, outputJson } from "../utils/output.ts"; |
| 17 | + |
| 18 | +/** |
| 19 | + * Default transcription model. `ai:audio transcribe` requires --model; |
| 20 | + * this mirrors the Go CLI's own default (lower latency, English-only). |
| 21 | + */ |
| 22 | +const DEFAULT_MODEL = "distil-whisper/distil-large-v2"; |
| 23 | + |
| 24 | +interface SttResult { |
| 25 | + audio_url: string; |
| 26 | + model: string; |
| 27 | + transcription: string; |
| 28 | + language?: string; |
| 29 | + response_format?: string; |
| 30 | + /** Full API response when a non-default --response-format is requested — |
| 31 | + * verbose_json carries timestamps/segments the flat fields drop. */ |
| 32 | + response?: unknown; |
| 33 | +} |
| 34 | + |
| 35 | +/** |
| 36 | + * Extract the transcription text from a telnyx CLI transcribe response. |
| 37 | + * The endpoint is OpenAI-compatible and returns the transcript under |
| 38 | + * `text`, but we defensively check a `data` envelope and a few common |
| 39 | + * field names. |
| 40 | + */ |
| 41 | +function extractTranscription(response: unknown): string { |
| 42 | + const data = (response as Record<string, unknown> | undefined)?.data ?? response; |
| 43 | + const obj = (data ?? {}) as Record<string, unknown>; |
| 44 | + |
| 45 | + for (const key of ["text", "transcription", "transcript"]) { |
| 46 | + const v = obj[key]; |
| 47 | + if (typeof v === "string" && v) return v; |
| 48 | + } |
| 49 | + |
| 50 | + return ""; |
| 51 | +} |
| 52 | + |
| 53 | +export async function sttCommand(flags: Record<string, string | boolean>): Promise<void> { |
| 54 | + const jsonOutput = flags.json === true; |
| 55 | + const audioUrl = flags["audio-url"] as string; |
| 56 | + const model = (flags.model as string) || DEFAULT_MODEL; |
| 57 | + // Only forward --language when explicitly provided: the default model |
| 58 | + // (distil-whisper/distil-large-v2) rejects the language parameter. |
| 59 | + const language = flags.language as string | undefined; |
| 60 | + const responseFormat = flags["response-format"] as string | undefined; |
| 61 | + |
| 62 | + if (!audioUrl) { |
| 63 | + printError("--audio-url is required (e.g., --audio-url https://example.com/audio.mp3)"); |
| 64 | + process.exit(1); |
| 65 | + } |
| 66 | + |
| 67 | + try { |
| 68 | + if (!jsonOutput) { |
| 69 | + console.log("\n🎙️ Transcribing audio...\n"); |
| 70 | + } |
| 71 | + |
| 72 | + const args = ["ai:audio", "transcribe", "--file-url", audioUrl, "--model", model]; |
| 73 | + if (language) args.push("--language", language); |
| 74 | + if (responseFormat) args.push("--response-format", responseFormat); |
| 75 | + |
| 76 | + const response = await telnyxCli(args); |
| 77 | + const transcription = extractTranscription(response); |
| 78 | + |
| 79 | + const verbose = responseFormat !== undefined && responseFormat !== "json"; |
| 80 | + const result: SttResult = { |
| 81 | + audio_url: audioUrl, |
| 82 | + model, |
| 83 | + transcription, |
| 84 | + ...(language ? { language } : {}), |
| 85 | + ...(responseFormat ? { response_format: responseFormat } : {}), |
| 86 | + ...(verbose |
| 87 | + ? { response: (response as Record<string, unknown> | undefined)?.data ?? response } |
| 88 | + : {}), |
| 89 | + }; |
| 90 | + |
| 91 | + if (jsonOutput) { |
| 92 | + outputJson(result); |
| 93 | + } else { |
| 94 | + const details: Record<string, string | number | boolean> = { |
| 95 | + "Audio URL": audioUrl, |
| 96 | + Model: model, |
| 97 | + }; |
| 98 | + if (language) details["Language"] = language; |
| 99 | + details["Transcription"] = transcription || "(no text returned)"; |
| 100 | + printSuccess("Transcription complete!", details); |
| 101 | + } |
| 102 | + } catch (err) { |
| 103 | + const msg = errorMsg(err); |
| 104 | + if (jsonOutput) { |
| 105 | + outputJson({ error: msg }); |
| 106 | + } else { |
| 107 | + printError(msg); |
| 108 | + } |
| 109 | + process.exit(1); |
| 110 | + } |
| 111 | +} |
| 112 | + |
| 113 | +function errorMsg(err: unknown): string { |
| 114 | + if (err instanceof TelnyxCLIError) return err.stderr || err.message; |
| 115 | + if (err instanceof Error) return err.message; |
| 116 | + return String(err); |
| 117 | +} |
0 commit comments