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
3 changes: 1 addition & 2 deletions 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.1.0
Version: 0.2.0
Authors@R:
person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"))
Description: A minimal-dependency R client for 'OpenAI'-compatible speech-to-text
Expand All @@ -16,4 +16,3 @@ Imports:
Suggests:
tinytest,
whisper
RoxygenNote: 7.3.3
11 changes: 9 additions & 2 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# sttapi 0.1.0
# stt.api 0.2.0

* Initial CRAN release
* Remove audio.whisper backend
* Remove gpu.ctl integration
* Remove processx dependency (never implemented)
* Backends are now: whisper (native R torch) and OpenAI-compatible API

# stt.api 0.1.0

* Initial release
* Support for OpenAI-compatible speech-to-text APIs
* Local server support (LM Studio, OpenWebUI, Whisper containers)
* Optional whisper package integration for local transcription
Expand Down
237 changes: 113 additions & 124 deletions R/internal_api.R
Original file line number Diff line number Diff line change
@@ -1,133 +1,122 @@
# Internal: Transcribe via OpenAI-compatible API
.via_api <- function(
file,
model = NULL,
language = NULL,
response_format = "json",
prompt = NULL
) {

base_url <- .get_api_base(required = TRUE)
api_key <- .get_api_key()
timeout <- .get_timeout()

# Build endpoint URL

url <- paste0(base_url, "/v1/audio/transcriptions")

# Prepare multipart form data
form_data <- list(
file = curl::form_file(file)
)

if (!is.null(model)) {
form_data$model <- model
}

if (!is.null(language)) {
form_data$language <- language
}

if (!is.null(prompt)) {
form_data$prompt <- prompt
}

form_data$response_format <- response_format

# Build headers (curl expects "Name: Value" format)
headers <- "Accept: application/json"
if (!is.null(api_key) && nchar(api_key) > 0) {
headers <- c(headers, paste0("Authorization: Bearer ", api_key))
}

# Create curl handle
h <- curl::new_handle()
curl::handle_setopt(h,
timeout = timeout,
httpheader = headers
)
curl::handle_setform(h, .list = form_data)

# Make request
response <- tryCatch(
curl::curl_fetch_memory(url, handle = h),
error = function(e) {
stop(
"API request failed: ", conditionMessage(e), "\n",
"URL: ", url,
call. = FALSE
)
.via_api <- function(file, model = NULL, language = NULL,
response_format = "json", prompt = NULL) {
base_url <- .get_api_base(required = TRUE)
api_key <- .get_api_key()
timeout <- .get_timeout()

# Build endpoint URL

url <- paste0(base_url, "/v1/audio/transcriptions")

# Prepare multipart form data
form_data <- list(file = curl::form_file(file))

if (!is.null(model)) {
form_data$model <- model
}
)

# Check HTTP status
if (response$status_code >= 400) {
body <- rawToChar(response$content)
error_msg <- tryCatch(
{
parsed <- jsonlite::fromJSON(body, simplifyVector = FALSE)
if (!is.null(parsed$error$message)) {
parsed$error$message
} else {
body
}
},
error = function(e) body
)
stop(
"API error (HTTP ", response$status_code, "): ", error_msg,
call. = FALSE
if (!is.null(language)) {
form_data$language <- language
}

if (!is.null(prompt)) {
form_data$prompt <- prompt
}

form_data$response_format <- response_format

# Build headers (curl expects "Name: Value" format)
headers <- "Accept: application/json"
if (!is.null(api_key) && nchar(api_key) > 0) {
headers <- c(headers, paste0("Authorization: Bearer ", api_key))
}

# Create curl handle
h <- curl::new_handle()
curl::handle_setopt(h, timeout = timeout, httpheader = headers)
curl::handle_setform(h, .list = form_data)

# Make request
response <- tryCatch(
curl::curl_fetch_memory(url, handle = h),
error = function(e) {
stop(
"API request failed: ", conditionMessage(e), "\n",
"URL: ", url,
call. = FALSE
)
}
)
}

# Parse response
body <- rawToChar(response$content)

if (response_format == "text") {
return(list(
text = body,
segments = NULL,
language = language,
backend = "api",
raw = body
))
}

# Parse JSON response
parsed <- tryCatch(
jsonlite::fromJSON(body, simplifyVector = FALSE),
error = function(e) {
stop("Failed to parse API response as JSON: ", conditionMessage(e),
call. = FALSE)

# Check HTTP status
if (response$status_code >= 400) {
body <- rawToChar(response$content)
error_msg <- tryCatch(
{
parsed <- jsonlite::fromJSON(body, simplifyVector = FALSE)
if (!is.null(parsed$error$message)) {
parsed$error$message
} else {
body
}
},
error = function(e) body
)
stop(
"API error (HTTP ", response$status_code, "): ", error_msg,
call. = FALSE
)
}

# Parse response
body <- rawToChar(response$content)

if (response_format == "text") {
return(list(
text = body,
segments = NULL,
language = language,
backend = "api",
raw = body
))
}
)

# Extract segments if available (verbose_json format)
segments <- NULL
if (!is.null(parsed$segments) && length(parsed$segments) > 0) {
segments <- tryCatch(
{
do.call(rbind, lapply(parsed$segments, function(s) {
data.frame(
start = s$start,
end = s$end,
text = s$text,
stringsAsFactors = FALSE
)

# Parse JSON response
parsed <- tryCatch(
jsonlite::fromJSON(body, simplifyVector = FALSE),
error = function(e) {
stop("Failed to parse API response as JSON: ", conditionMessage(e),
call. = FALSE)
}
)

# Extract segments if available (verbose_json format)
segments <- NULL
if (!is.null(parsed$segments) && length(parsed$segments) > 0) {
segments <- tryCatch(
{
do.call(rbind, lapply(parsed$segments, function(s) {
data.frame(
start = s$start,
end = s$end,
text = s$text,
stringsAsFactors = FALSE
)
}))
},
error = function(e) NULL
},
error = function(e) NULL
)
# Normalize to numeric seconds
segments <- .normalize_segments(segments)
}

list(
text = parsed$text %||% "",
segments = segments,
language = parsed$language %||% language,
backend = "api",
raw = parsed
)
# Normalize to numeric seconds
segments <- .normalize_segments(segments)
}

list(
text = parsed$text %||% "",
segments = segments,
language = parsed$language %||% language,
backend = "api",
raw = parsed
)
}

Loading