Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: stt.api
Title: 'OpenAI' Compatible Speech-to-Text API Client
Version: 0.2.1.2
Version: 0.2.1.3
Authors@R: c(
person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"),
comment = c(ORCID = "0009-0005-4248-604X")),
Expand Down
8 changes: 8 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# stt.api 0.2.1.3

* The API backend now requests and parses word-level timestamps: with
`response_format = "verbose_json"` it sends `timestamp_granularities[]` for
both `segment` and `word` (requesting word alone can suppress segments on
OpenAI) and returns `result$words` (word/start/end), matching the native
whisper backend. Works against OpenAI and a self-hosted `whisper::serve()`.

# stt.api 0.2.1.2

* `stt()` gains a `source` axis ("auto", "api", "package"), mirroring
Expand Down
30 changes: 29 additions & 1 deletion R/internal_api.R
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@

form_data$response_format <- response_format

# Request word-level timestamps for verbose_json, so result$words is
# populated like the in-process whisper backend. OpenAI treats word and
# segment as separate granularities, and requesting word alone can suppress
# segments -- so ask for BOTH (two array-style fields). whisper::serve
# honors either.
if (identical(response_format, "verbose_json")) {
gran <- list("segment", "word")
names(gran) <- c("timestamp_granularities[]", "timestamp_granularities[]")
form_data <- c(form_data, gran)
}

# Build headers (curl expects "Name: Value" format)
headers <- "Accept: application/json"
if (!is.null(api_key) && nchar(api_key) > 0) {
Expand Down Expand Up @@ -111,12 +122,29 @@
segments <- .normalize_segments(segments)
}

list(
# Extract word-level timestamps if available (verbose_json + word
# granularity), mirroring the native whisper backend's result$words.
words <- NULL
if (!is.null(parsed$words) && length(parsed$words) > 0) {
words <- tryCatch(
do.call(rbind, lapply(parsed$words, function(w) {
data.frame(word = w$word, start = w$start, end = w$end,
stringsAsFactors = FALSE)
})),
error = function(e) NULL
)
}

out <- list(
text = parsed$text %||% "",
segments = segments,
language = parsed$language %||% language,
backend = "api",
raw = parsed
)
if (!is.null(words)) {
out$words <- words
}
out
}

Loading