| Title: | 'OpenAI' Compatible Speech-to-Text API Client |
| Version: | 0.3.1 |
| Description: | A minimal-dependency R client for 'OpenAI'-compatible speech-to-text APIs (see https://developers.openai.com/api/reference/resources/audio) with optional local fallbacks. Supports 'OpenAI', local servers, and the 'whisper' package for local transcription. |
| License: | MIT + file LICENSE |
| Encoding: | UTF-8 |
| URL: | https://github.com/cornball-ai/stt.api |
| BugReports: | https://github.com/cornball-ai/stt.api/issues |
| Imports: | curl, jsonlite |
| Suggests: | tinytest, whisper |
| NeedsCompilation: | no |
| Packaged: | 2026-08-04 21:56:07 UTC; troy |
| Author: | Troy Hernandez |
| Maintainer: | Troy Hernandez <troy@cornball.ai> |
| Repository: | CRAN |
| Date/Publication: | 2026-08-04 22:40:02 UTC |
Encode an audio file as a data URI
Description
The speaker-reference fields take the clip inline as
data:<mime>;base64,<...> rather than as a file part, so the bytes
are read and encoded here. jsonlite is already an import, so this adds no
dependency despite having nothing to do with JSON.
Usage
.audio_data_uri(path)
Arguments
path |
Path to an audio file. |
Value
A data URI string.
MIME type for an audio file, from its extension
Description
Covers every container OpenAI accepts as transcription input, since a speaker reference may be in any of them. Deriving this from the extension rather than sniffing the file keeps the dependency list where it is; an unknown extension is an error rather than a guess, since a wrong MIME type fails server-side with a far less obvious message.
Usage
.audio_mime(path)
Arguments
path |
File path. |
Value
A MIME type string.
Get or create cached native whisper model
Description
Get or create cached native whisper model
Usage
.get_native_whisper_model(model, device = "auto")
Arguments
model |
Model name (e.g., "tiny", "base", "small", "medium", "large-v3") |
device |
Device to use ("auto", "cpu", "cuda") |
Value
Loaded whisper model object
Is this request headed for a diarizing model?
Description
Several of OpenAI's rules key on the model rather than the response
format, since a diarizing model also answers plain json and
text. It refuses prompt for "diarization models" whichever
format is asked for, requires chunking_strategy for them above 30
seconds of audio in any format, and only OpenAI serves them at all, so a
diarizing request also fixes the backend. Verified against the live
endpoint with response_format = "json":
Usage
.is_diarizing(model, response_format)
Arguments
model |
Model name, or NULL. |
response_format |
The resolved response format. |
Details
HTTP 400: Prompt is not supported for diarization models HTTP 400: chunking_strategy is required for diarization models
diarized_json implies a diarizing model, since nothing else
produces it. Beyond that the only signal available before the request is
the model name, so this matches on it. That is a heuristic: a self-hosted
model whose name happens to contain "diarize" is treated as diarizing.
The alternative is uploading the audio to be told the same thing.
Value
TRUE or FALSE.
Normalize segments to use numeric seconds
Description
Normalize segments to use numeric seconds
Usage
.normalize_segments(segments)
Arguments
segments |
Data frame with from/to or start/end columns |
Value
Data frame with numeric start/end columns
Build the segments data.frame from a parsed API response
Description
Shared by the timestamped formats. verbose_json segments carry
start/end/text; diarized_json segments add a speaker label. The
speaker column is added only when the response actually has it, so
is.null(x$segments$speaker) gives a straight answer either way.
Usage
.parse_api_segments(segs)
Arguments
segs |
The |
Value
A data.frame with numeric start/end, or NULL.
Convert time string to numeric seconds
Description
Convert time string to numeric seconds
Usage
.time_to_seconds(time_str)
Arguments
time_str |
Time string in "HH:MM:SS.mmm" or "MM:SS.mmm" format |
Value
Numeric seconds
Validate the known_speakers argument
Description
Validate the known_speakers argument
Usage
.validate_known_speakers(x)
Arguments
x |
A named character vector of audio file paths, or NULL. |
Value
x, invisibly, or an error.
Internal: Transcribe via native whisper package
Description
Uses the cornball-ai/whisper native R torch implementation.
Usage
.via_whisper(file, model = NULL, language = NULL)
Arguments
file |
Character. Path to the audio file to transcribe. |
model |
Character or NULL. Whisper model name (e.g., "tiny", "base", "small", "medium", "large-v3"). |
language |
Character or NULL. Language code for transcription. |
Value
List with transcription results in normalized format.
Clear native whisper model cache
Description
Removes cached native whisper models from memory. Call this to free GPU/RAM after batch processing is complete.
Usage
clear_native_whisper_cache()
Value
No return value, called for side effects (frees memory by removing cached models and triggers garbage collection).
Examples
clear_native_whisper_cache()
Fold Speaker Labels Into Caption Text
Description
Diarized results carry speaker labels on segments, but subtitle
tools read data, which is from/to/text only.
The labels are therefore dropped on the way to a caption file. This folds
them into the caption text so they survive:
Usage
label_speakers(x, sep = ": ", prefix = "", suffix = "")
Arguments
x |
A result from |
sep |
String placed between the label and the line. Defaults to
|
prefix |
String placed before the label, for styles like
|
suffix |
String placed after the label, for styles like
|
Details
HOUSTON: We copy you down, Eagle. ARMSTRONG: Tranquility Base here. The Eagle has landed.
Only data$text changes. segments keeps its own untouched
text and speaker columns, so the labels are still available
separately, and the class and "call_record" attribute are
preserved – the result still feeds
subtitles::whisper_to_srt() and whisper_to_ass() directly.
Value
x with data$text relabelled. Segments whose speaker
is missing are left alone rather than labelled NA, which happens
when a provider matches only some speakers to the references given in
known_speakers.
Karaoke
Pass karaoke = FALSE to
subtitles::whisper_to_ass() for a diarized result. That argument
defaults to TRUE and needs word-level timings, which OpenAI does
not return alongside diarization – so the default errors on any diarized
result, labelled or not. whisper_to_srt() is unaffected.
See Also
stt for known_speakers, which sets the
labels this uses.
Examples
# The shape stt() returns for a diarized request.
x <- structure(
list(
segments = data.frame(
start = c(0, 2.5), end = c(2.5, 5),
text = c("We copy you down, Eagle.", "The Eagle has landed."),
speaker = c("HOUSTON", "ARMSTRONG"),
stringsAsFactors = FALSE),
data = data.frame(
from = c("00:00:00.000", "00:00:02.500"),
to = c("00:00:02.500", "00:00:05.000"),
text = c("We copy you down, Eagle.", "The Eagle has landed."),
stringsAsFactors = FALSE)),
class = c("stt_result", "whisper_transcription"))
label_speakers(x)$data$text
label_speakers(x, prefix = "[", suffix = "]", sep = " ")$data$text
## Not run:
# Into a caption file. karaoke = FALSE is required for diarized results.
subtitles::whisper_to_srt(label_speakers(x), "meeting.srt")
subtitles::whisper_to_ass(label_speakers(x), "meeting.ass",
karaoke = FALSE)
## End(Not run)
Set the API Base URL
Description
Sets the base URL for OpenAI-compatible STT endpoints.
Usage
set_stt_base(url)
Arguments
url |
Character string. The base URL (e.g., "http://localhost:4123" or "https://api.openai.com"). |
Value
Invisibly returns the previous value.
Examples
set_stt_base("http://localhost:4123")
getOption("stt.api_base")
Set the API Key
Description
Sets the API key for hosted STT services (e.g., OpenAI). Local servers typically ignore this.
Usage
set_stt_key(key)
Arguments
key |
Character string. The API key. |
Value
Invisibly returns the previous value.
Examples
set_stt_key("test-key-123")
getOption("stt.api_key")
Speech to Text
Description
Convert an audio file to text using a local whisper backend or an OpenAI-compatible API.
Usage
stt(
file,
model = NULL,
language = NULL,
response_format = c("json", "text", "verbose_json", "diarized_json"),
backend = c("auto", "whisper", "openai"),
source = c("auto", "api", "package"),
prompt = NULL,
chunking_strategy = NULL,
known_speakers = NULL
)
Arguments
file |
Path to the audio file to convert. |
model |
Model name to use for transcription. For API backends, this
is passed directly (e.g., "whisper-1"). For whisper, this is
the model size (e.g., "tiny", "base", "small", "medium", "large").
If NULL, uses the backend's default. Which model you pick decides what
timing you can get back from OpenAI: "whisper-1" is the only one that
returns word-level timings (with |
language |
Language code (e.g., "en", "es", "fr"). Optional hint to improve transcription accuracy. |
response_format |
Response format for API backend. One of "text",
"json", "verbose_json", or "diarized_json". Ignored for whisper
backend, except that a diarizing request is an error there, whether it
is diarizing by format or by model (see |
backend |
Which engine to use: "auto" (default), "whisper",
or "openai". Auto mode tries whisper first, then the openai API
(if configured), except for a diarizing request, which only OpenAI
serves and so resolves straight to "openai". That covers
|
source |
Where the engine runs: "auto" (default), "api" for an HTTP
service (OpenAI, or a self-hosted whisper server; see
|
prompt |
Optional text to guide the transcription. For API backend,
this is passed as initial_prompt to help with spelling of names,
acronyms, or domain-specific terms. Ignored for whisper backend, and an
error for a diarizing model: OpenAI refuses a prompt for those
whichever |
chunking_strategy |
Optional chunking strategy passed to the API,
e.g. "auto". Defaults to "auto" for any request to a diarizing model
( |
known_speakers |
Optional named character vector of audio files, at
most four, giving a short reference clip per speaker. The names
are offered to the provider as labels: segments it matches to a
reference come back named, so
|
Value
A list with components:
- text
The transcribed text as a single string.
- segments
A data.frame of segments with timing info, or NULL. For
response_format = "diarized_json"it carries an extraspeakercolumn; that column is absent, not NA, for every other format.- words
A data.frame of word-level timestamps (word, start, end), present only when the API returns word granularity (verbose_json, and on OpenAI only from "whisper-1"; see
model); otherwise absent.- language
The detected or specified language code.
- backend
The legacy execution route ("api" or "whisper"). This reports *where* the engine ran, not the engine itself; the resolved
backend/sourcepair lives in the"call_record"attribute.- raw
The raw response from the backend.
When the result has usable segments (start/end/text
columns), it additionally carries the shape subtitle tooling expects: a
data data.frame with from/to timestamp strings
("HH:MM:SS.mmm") and text, and class
c("stt_result", "whisper_transcription"), so it feeds
subtitles::whisper_to_srt() and subtitles::whisper_to_ass()
directly. Note the API route returns segments only with
response_format = "verbose_json" or "diarized_json".
Results without usable segments are plain lists, as before.
The result also carries a "call_record" attribute (cornball_sidecar
v1, as in xtx.api/tts.api): the resolved request, elapsed seconds, and a
timestamp – provenance that rides with the transcription when callers
serialize it.
Examples
## Not run:
# Using OpenAI API
set_stt_base("https://api.openai.com")
set_stt_key(Sys.getenv("OPENAI_API_KEY"))
result <- stt("speech.wav", model = "whisper-1")
result$text
# Speaker-labelled segments
result <- stt("meeting.wav", model = "gpt-4o-transcribe-diarize",
response_format = "diarized_json")
result$segments[, c("start", "end", "speaker", "text")]
# ...with your own labels instead of generic ones
audio <- system.file("audio", package = "stt.api")
result <- stt(file.path(audio, "EagleHasLanded.mp3"),
model = "gpt-4o-transcribe-diarize",
response_format = "diarized_json",
known_speakers = c(
Armstrong = file.path(audio, "ref_armstrong.mp3"),
Houston = file.path(audio, "ref_houston.mp3")))
unique(result$segments$speaker)
# Using a self-hosted whisper serve() endpoint
set_stt_base("http://troy-g5:7809")
result <- stt("speech.wav", backend = "whisper", source = "api")
# In-process whisper package
result <- stt("speech.wav", backend = "whisper", source = "package")
## End(Not run)
Check STT Backend Health
Description
Checks whether a transcription backend is available and working.
Usage
stt_health()
Value
A list with components:
- ok
Logical. TRUE if a backend is available.
- backend
Character. The available backend ("api" or "whisper"), or NULL if none available.
- message
Character. Status message with details.
Examples
## Not run:
h <- stt_health()
if (h$ok) {
message("STT ready via ", h$backend)
}
## End(Not run)