From 0accc2ba527efffca2a148cded04dc0824eaa44b Mon Sep 17 00:00:00 2001 From: TroyHernandez Date: Wed, 18 Mar 2026 00:07:07 -0500 Subject: [PATCH] Remove audio.whisper and gpu.ctl backends for CRAN submission Strip audio.whisper backend, gpu.ctl integration, and processx from the package. These were either not on CRAN or never implemented. --- CLAUDE.md | 2 - DESCRIPTION | 7 +- NAMESPACE | 4 -- NEWS.md | 2 +- R/gpuctl_integration.R | 62 ----------------- R/internal_audio_whisper.R | 130 ----------------------------------- R/internal_backend.R | 34 ++------- R/stt.R | 43 ++++-------- R/stt_health.R | 32 ++++----- README.md | 15 ++-- cran-comments.md | 8 --- man/clear_whisper_cache.Rd | 11 --- man/dot-get_whisper_model.Rd | 17 ----- man/dot-via_audio_whisper.Rd | 21 ------ man/stt.Rd | 22 +++--- man/stt_health.Rd | 2 +- 16 files changed, 47 insertions(+), 365 deletions(-) delete mode 100644 R/gpuctl_integration.R delete mode 100644 R/internal_audio_whisper.R delete mode 100644 man/clear_whisper_cache.Rd delete mode 100644 man/dot-get_whisper_model.Rd delete mode 100644 man/dot-via_audio_whisper.Rd diff --git a/CLAUDE.md b/CLAUDE.md index f7f3ac6..c9c84b6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,6 @@ Speech-to-text API client for R. | Backend | Description | Segments | |---------|-------------|----------| | `whisper` | Native R torch whisper | **No** (text only) | -| `audio.whisper` | audio.whisper R package | Yes | | `openai` | OpenAI Whisper API | Yes | | `auto` | Try backends in order | Depends | @@ -38,7 +37,6 @@ stt("audio.wav", backend = "whisper", model = "large-v3") options(stt.api_base = "https://api.openai.com") # API endpoint options(stt.api_key = "sk-...") # API key options(stt.timeout = 120) # Request timeout (seconds) -options(stt.gpuctl = TRUE) # Enable GPU management ``` ## Known Issues diff --git a/DESCRIPTION b/DESCRIPTION index 7ca7605..87cd55b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -3,10 +3,9 @@ Title: 'OpenAI' Compatible Speech-to-Text API Client Version: 0.1.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 - APIs with optional local fallbacks. Supports 'OpenAI', local servers - ('LM Studio', 'OpenWebUI', 'Whisper' containers), and the 'audio.whisper' - package. +Description: A minimal-dependency R client for 'OpenAI'-compatible speech-to-text + APIs 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 diff --git a/NAMESPACE b/NAMESPACE index e5820ba..5c8c80a 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,11 +1,7 @@ # tinyrox says don't edit this manually, but it can't stop you! export(clear_native_whisper_cache) -export(clear_whisper_cache) export(set_stt_base) export(set_stt_key) export(stt) export(stt_health) - -importFrom(stats,predict) -importFrom(utils,capture.output) diff --git a/NEWS.md b/NEWS.md index 915058f..8c8accd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -3,5 +3,5 @@ * Initial CRAN release * Support for OpenAI-compatible speech-to-text APIs * Local server support (LM Studio, OpenWebUI, Whisper containers) -* Optional audio.whisper package integration for local transcription +* Optional whisper package integration for local transcription * Segment-level timestamps with word-level timing when available diff --git a/R/gpuctl_integration.R b/R/gpuctl_integration.R deleted file mode 100644 index 4b5795c..0000000 --- a/R/gpuctl_integration.R +++ /dev/null @@ -1,62 +0,0 @@ -# gpu.ctl integration for stt.api -# -# Optionally acquires GPU resources before API calls when gpu.ctl is available. -# Enable with: options(stt.gpuctl = TRUE) - -# Service configuration for Whisper STT -.stt_gpu_service <- list( - name = "whisper", - port = 8200, - vram = 6, - container = "whisper", - health = "/health" -) - -#' Check if gpu.ctl integration is enabled -#' @noRd -.gpuctl_enabled <- function() { - isTRUE(getOption("stt.gpuctl", FALSE)) && - requireNamespace("gpu.ctl", quietly = TRUE) -} - -#' Register stt.api service with gpu.ctl -#' @noRd -.gpuctl_register_service <- function() { - if (!.gpuctl_enabled()) return(invisible(FALSE)) - - tryCatch({ - # Only register if not already registered - existing <- gpu.ctl::gpu_services() - if (!.stt_gpu_service$name %in% existing$name) { - gpu.ctl::gpu_register( - name = .stt_gpu_service$name, - port = .stt_gpu_service$port, - vram = .stt_gpu_service$vram, - container = .stt_gpu_service$container, - health_endpoint = .stt_gpu_service$health - ) - } - }, error = function(e) { - # Silently ignore registration errors - }) - invisible(TRUE) -} - -#' Acquire GPU for whisper if gpu.ctl is enabled -#' -#' @return Invisible TRUE if acquired, FALSE if not using gpu.ctl -#' @noRd -.gpuctl_acquire <- function() { - if (!.gpuctl_enabled()) return(invisible(FALSE)) - - .gpuctl_register_service() - - tryCatch({ - gpu.ctl::gpu_acquire(.stt_gpu_service$name) - invisible(TRUE) - }, error = function(e) { - warning("gpu.ctl: ", e$message, call. = FALSE) - invisible(FALSE) - }) -} - diff --git a/R/internal_audio_whisper.R b/R/internal_audio_whisper.R deleted file mode 100644 index 6b590f8..0000000 --- a/R/internal_audio_whisper.R +++ /dev/null @@ -1,130 +0,0 @@ -# Module-level whisper model cache -.whisper_cache <- new.env(parent = emptyenv()) - -#' Get or create cached whisper model -#' @param model Model name (e.g., "tiny", "base", "small", "medium", "large-v3") -#' @return Loaded whisper model object -#' @keywords internal -.get_whisper_model <- function(model) { - if (is.null(.whisper_cache[[model]])) { - message("Loading whisper model: ", model, "...") - .whisper_cache[[model]] <- tryCatch( - audio.whisper::whisper(model), - error = function(e) { - stop( - "Failed to load whisper model '", model, "': ", conditionMessage(e), - call. = FALSE - ) - } - ) - message("Whisper model loaded and cached.") - } - .whisper_cache[[model]] -} - -#' Clear whisper model cache -#' -#' Removes cached whisper models from memory. Call this to free GPU/RAM -#' after batch processing is complete. -#' -#' @export -clear_whisper_cache <- function() { - models <- ls(.whisper_cache) - if (length(models) > 0) { - rm(list = models, envir = .whisper_cache) - gc() # Force garbage collection - message("Cleared ", length(models), " cached whisper model(s).") - } else { - message("Whisper cache is empty.") - } - invisible(NULL) -} - -#' Internal: Transcribe via audio.whisper package -#' -#' @param file Character. Path to the audio file to transcribe. -#' @param model Character or NULL. Whisper model name (e.g., "tiny", "base", "small"). -#' @param language Character or NULL. Language code for transcription. -#' @return List with transcription results. -#' @keywords internal -#' @importFrom stats predict -#' @importFrom utils capture.output -.via_audio_whisper <- function( - file, - model = NULL, - language = NULL, - token_timestamps = FALSE -) { - - if (!.has_audio_whisper()) { - stop( - "audio.whisper package is not installed.\n", - "Install with: install.packages('audio.whisper', repos = 'https://bnosac.github.io/drat')", - call. = FALSE - ) - } - - # Default model if not specified - if (is.null(model)) { - model <- "tiny" - } - - # Get cached model (loads only once per model type) - whisper_model <- .get_whisper_model(model) - - # Build predict arguments - predict_args <- list( - object = whisper_model, - newdata = file, - token_timestamps = token_timestamps - ) - - if (!is.null(language)) { - predict_args$language <- language - } - - # Run transcription (suppress verbose whisper output) - result <- tryCatch({ - invisible(capture.output( - res <- do.call(predict, predict_args), - type = "output" - )) - res - }, error = function(e) { - stop( - "Transcription failed: ", conditionMessage(e), - call. = FALSE - ) - } - ) - - # Build segments data frame if available - segments <- NULL - if (!is.null(result$data) && nrow(result$data) > 0) { - segments <- result$data - # Normalize to numeric seconds with start/end column names - segments <- .normalize_segments(segments) - } - - # Combine all text segments - text <- "" - if (!is.null(result$data$text)) { - text <- paste(result$data$text, collapse = " ") - } - - out <- list( - text = text, - segments = segments, - language = language, - backend = "audio.whisper", - raw = result - ) - - # Pass through word-level tokens if available - if ("token_from" %in% names(result$tokens)) { - out$tokens <- result$tokens - } - - out -} - diff --git a/R/internal_backend.R b/R/internal_backend.R index 788efef..4a6eaab 100644 --- a/R/internal_backend.R +++ b/R/internal_backend.R @@ -22,12 +22,6 @@ getOption("stt.timeout", default = 60) } -# Check if audio.whisper is available -.has_audio_whisper <- function() { - - requireNamespace("audio.whisper", quietly = TRUE) -} - #' Convert time string to numeric seconds #' @param time_str Time string in "HH:MM:SS.mmm" or "MM:SS.mmm" format #' @return Numeric seconds @@ -73,11 +67,10 @@ } # Choose backend based on availability and user preference -.choose_backend <- function(backend = c("auto", "whisper", "audio.whisper", "openai")) { +.choose_backend <- function(backend = c("auto", "whisper", "openai")) { backend <- match.arg(backend) if (backend == "openai") { - # Explicit OpenAI API request - verify it's configured if (is.null(.get_api_base())) { stop( "Backend 'openai' requested but no API base URL is set.\n", @@ -89,41 +82,23 @@ } if (backend == "whisper") { - # Explicit native whisper request - verify it's available if (!.has_whisper()) { stop( "Backend 'whisper' requested but package is not installed.\n", - "Install with: remotes::install_github('cornball-ai/whisper')", + "Install with: install.packages('whisper')", call. = FALSE ) } return("whisper") } - if (backend == "audio.whisper") { - # Explicit audio.whisper request - verify it's available - if (!.has_audio_whisper()) { - stop( - "Backend 'audio.whisper' requested but package is not installed.\n", - "Install with: install.packages('audio.whisper', repos = 'https://bnosac.github.io/drat')", - call. = FALSE - ) - } - return("audio.whisper") - } - # Auto mode: try backends in priority order # 1. Native whisper (fastest, no external dependencies) if (.has_whisper()) { return("whisper") } - # 2. audio.whisper (local, no API needed) - if (.has_audio_whisper()) { - return("audio.whisper") - } - - # 3. OpenAI API (if configured) + # 2. OpenAI-compatible API (if configured) if (!is.null(.get_api_base())) { return("openai") } @@ -131,8 +106,7 @@ stop( "No transcription backend available.\n", "Either:\n", - " - Install whisper: remotes::install_github('cornball-ai/whisper'), or\n", - " - Install audio.whisper: install.packages('audio.whisper', repos = 'https://bnosac.github.io/drat'), or\n", + " - Install whisper: install.packages('whisper'), or\n", " - Set an API endpoint with set_stt_base()", call. = FALSE ) diff --git a/R/stt.R b/R/stt.R index 6b339b6..90e91bf 100644 --- a/R/stt.R +++ b/R/stt.R @@ -1,27 +1,30 @@ #' Speech to Text #' -#' Convert an audio file to text using an OpenAI-compatible API or -#' local audio.whisper backend. +#' Convert an audio file to text using a local whisper backend or +#' an OpenAI-compatible API. #' #' @param file Path to the audio file to convert. #' @param model Model name to use for transcription. For API backends, this -#' is passed directly (e.g., "whisper-1"). For audio.whisper, this is +#' 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. #' @param language Language code (e.g., "en", "es", "fr"). Optional hint #' to improve transcription accuracy. #' @param response_format Response format for API backend. One of "text", -#' "json", or "verbose_json". Ignored for audio.whisper backend. +#' "json", or "verbose_json". Ignored for whisper backend. #' @param backend Which backend to use: "auto" (default), "whisper", -#' "audio.whisper", or "openai". Auto mode tries native whisper first, -#' then audio.whisper, then openai API (if configured). +#' or "openai". Auto mode tries whisper first, then openai API +#' (if configured). +#' @param 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. #' #' @return A list with components: #' \describe{ #' \item{text}{The transcribed text as a single string.} #' \item{segments}{A data.frame of segments with timing info, or NULL.} #' \item{language}{The detected or specified language code.} -#' \item{backend}{Which backend was used ("api" or "audio.whisper").} +#' \item{backend}{Which backend was used ("api" or "whisper").} #' \item{raw}{The raw response from the backend.} #' } #' @@ -36,23 +39,15 @@ #' # Using local server #' set_stt_base("http://localhost:4123") #' result <- stt("speech.wav") -#' -#' # Using audio.whisper directly -#' result <- stt("speech.wav", backend = "audio.whisper") #' } #' -#' @param 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 audio.whisper backend -#' (not supported by underlying library). -#' #' @export stt <- function( file, model = NULL, language = NULL, response_format = c("json", "text", "verbose_json"), - backend = c("auto", "whisper", "audio.whisper", "openai"), + backend = c("auto", "whisper", "openai"), prompt = NULL ) { @@ -67,12 +62,6 @@ stt <- function( # Resolve backend resolved_backend <- .choose_backend(backend) - # Acquire GPU for local whisper API (not for openai.com) - if (resolved_backend == "openai" && - !grepl("openai\\.com", getOption("stt.api_base", ""))) { - .gpuctl_acquire() - } - # Dispatch to appropriate backend if (resolved_backend == "openai") { .via_api( @@ -82,20 +71,12 @@ stt <- function( response_format = response_format, prompt = prompt ) - } else if (resolved_backend == "whisper") { + } else { .via_whisper( file = file, model = model, language = language ) - } else { - # audio.whisper backend - .via_audio_whisper( - file = file, - model = model, - language = language, - token_timestamps = TRUE - ) } } diff --git a/R/stt_health.R b/R/stt_health.R index 046293d..ef9b9b3 100644 --- a/R/stt_health.R +++ b/R/stt_health.R @@ -5,7 +5,7 @@ #' @return A list with components: #' \describe{ #' \item{ok}{Logical. TRUE if a backend is available.} -#' \item{backend}{Character. The available backend ("api", "audio.whisper"), +#' \item{backend}{Character. The available backend ("api" or "whisper"), #' or NULL if none available.} #' \item{message}{Character. Status message with details.} #' } @@ -21,34 +21,26 @@ #' @export stt_health <- function() { - # Check API backend first - api_base <- .get_api_base() - if (!is.null(api_base)) { - health_result <- .check_api_health(api_base) - if (health_result$ok) { - return(health_result) - } - # API configured but not healthy - still report it - return(health_result) - } - - # Check audio.whisper - if (.has_audio_whisper()) { + # Check whisper package first + if (.has_whisper()) { return(list( ok = TRUE, - backend = "audio.whisper", - message = "audio.whisper package is available" + backend = "whisper", + message = "whisper package is available" )) } + # Check API backend + api_base <- .get_api_base() + if (!is.null(api_base)) { + return(.check_api_health(api_base)) + } + # No backend available list( ok = FALSE, backend = NULL, - message = paste0( - "No backend available. ", - "Set stt.api_base or install audio.whisper." - ) + message = "No backend available. Install whisper or set stt.api_base." ) } diff --git a/README.md b/README.md index 7d8fc56..671af72 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ It lets you transcribe audio in R **without caring which backend actually perfor * `{whisper}` (native R torch, local GPU/CPU) * OpenAI `/v1/audio/transcriptions` (cloud or local servers) - * `{audio.whisper}` (whisper.cpp) * Designed for scripting, Shiny apps, containers, and reproducible pipelines ### ❌ What it is *not* @@ -24,7 +23,7 @@ It lets you transcribe audio in R **without caring which backend actually perfor * Not a model manager * Not a GPU / CUDA helper * Not an audio preprocessing toolkit -* Not a replacement for `{whisper}` or `{audio.whisper}` +* Not a replacement for `{whisper}` --- @@ -42,8 +41,6 @@ Required dependencies are minimal: Optional backends: * `{whisper}` (recommended, on CRAN) -* `{audio.whisper}` (whisper.cpp alternative) - --- @@ -84,9 +81,8 @@ This works with OpenAI, Whisper containers, LM Studio, OpenWebUI, AnythingLLM, o When you call `stt()` without specifying a backend, it picks the first available: 1. `{whisper}` (native R torch, if installed) -2. `{audio.whisper}` (whisper.cpp, if installed) -3. OpenAI-compatible API (if `stt.api_base` is set) -4. Error with guidance +2. OpenAI-compatible API (if `stt.api_base` is set) +3. Error with guidance --- @@ -99,7 +95,7 @@ list( text = "Transcribed text", segments = NULL | data.frame(...), language = "en", - backend = "api" | "whisper" | "audio.whisper", + backend = "api" | "whisper", raw = ) ``` @@ -135,7 +131,6 @@ Explicit backend choice: ```r stt("speech.wav", backend = "openai") stt("speech.wav", backend = "whisper") -stt("speech.wav", backend = "audio.whisper") ``` Automatic selection (default): @@ -193,7 +188,7 @@ Example: ``` Error in stt(): No transcription backend available. -Install whisper, install audio.whisper, or set stt.api_base. +Install whisper or set stt.api_base. ``` --- diff --git a/cran-comments.md b/cran-comments.md index 4852dfd..39bf35f 100644 --- a/cran-comments.md +++ b/cran-comments.md @@ -9,11 +9,3 @@ * local: Ubuntu 24.04, R 4.5.2 * GitHub Actions: ubuntu-latest, macos-latest * win-builder: R-devel - -## Notes - -The NOTE about "Suggests or Enhances not in mainstream repositories" refers to: -- `audio.whisper`: Available on GitHub (bnosac/audio.whisper) -- `gpu.ctl`: Internal package for GPU resource management - -These are optional backends and the package works without them. diff --git a/man/clear_whisper_cache.Rd b/man/clear_whisper_cache.Rd deleted file mode 100644 index 1596b3b..0000000 --- a/man/clear_whisper_cache.Rd +++ /dev/null @@ -1,11 +0,0 @@ -% tinyrox says don't edit this manually, but it can't stop you! -\name{clear_whisper_cache} -\alias{clear_whisper_cache} -\title{Clear whisper model cache} -\usage{ -clear_whisper_cache() -} -\description{ -Removes cached whisper models from memory. Call this to free GPU/RAM -after batch processing is complete. -} diff --git a/man/dot-get_whisper_model.Rd b/man/dot-get_whisper_model.Rd deleted file mode 100644 index 66e8a3a..0000000 --- a/man/dot-get_whisper_model.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% tinyrox says don't edit this manually, but it can't stop you! -\name{.get_whisper_model} -\alias{.get_whisper_model} -\title{Get or create cached whisper model} -\usage{ -.get_whisper_model(model) -} -\arguments{ -\item{model}{Model name (e.g., "tiny", "base", "small", "medium", "large-v3")} -} -\value{ -Loaded whisper model object -} -\description{ -Get or create cached whisper model -} -\keyword{internal} diff --git a/man/dot-via_audio_whisper.Rd b/man/dot-via_audio_whisper.Rd deleted file mode 100644 index 3e9e277..0000000 --- a/man/dot-via_audio_whisper.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% tinyrox says don't edit this manually, but it can't stop you! -\name{.via_audio_whisper} -\alias{.via_audio_whisper} -\title{Internal: Transcribe via audio.whisper package} -\usage{ -.via_audio_whisper(file, model = NULL, language = NULL, token_timestamps = FALSE) -} -\arguments{ -\item{file}{Character. Path to the audio file to transcribe.} - -\item{model}{Character or NULL. Whisper model name (e.g., "tiny", "base", "small").} - -\item{language}{Character or NULL. Language code for transcription.} -} -\value{ -List with transcription results. -} -\description{ -Internal: Transcribe via audio.whisper package -} -\keyword{internal} diff --git a/man/stt.Rd b/man/stt.Rd index e7b6a74..2f3022e 100644 --- a/man/stt.Rd +++ b/man/stt.Rd @@ -5,13 +5,13 @@ \usage{ stt(file, model = NULL, language = NULL, response_format = c("json", "text", "verbose_json"), - backend = c("auto", "whisper", "audio.whisper", "openai"), prompt = NULL) + backend = c("auto", "whisper", "openai"), prompt = NULL) } \arguments{ \item{file}{Path to the audio file to convert.} \item{model}{Model name to use for transcription. For API backends, this -is passed directly (e.g., "whisper-1"). For audio.whisper, this is +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.} @@ -19,16 +19,15 @@ If NULL, uses the backend's default.} to improve transcription accuracy.} \item{response_format}{Response format for API backend. One of "text", -"json", or "verbose_json". Ignored for audio.whisper backend.} +"json", or "verbose_json". Ignored for whisper backend.} \item{backend}{Which backend to use: "auto" (default), "whisper", -"audio.whisper", or "openai". Auto mode tries native whisper first, -then audio.whisper, then openai API (if configured).} +or "openai". Auto mode tries whisper first, then openai API +(if configured).} \item{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 audio.whisper backend -(not supported by underlying library).} +acronyms, or domain-specific terms. Ignored for whisper backend.} } \value{ A list with components: @@ -36,13 +35,13 @@ A list with components: \item{text}{The transcribed text as a single string.} \item{segments}{A data.frame of segments with timing info, or NULL.} \item{language}{The detected or specified language code.} - \item{backend}{Which backend was used ("api" or "audio.whisper").} + \item{backend}{Which backend was used ("api" or "whisper").} \item{raw}{The raw response from the backend.} } } \description{ -Convert an audio file to text using an OpenAI-compatible API or -local audio.whisper backend. +Convert an audio file to text using a local whisper backend or +an OpenAI-compatible API. } \examples{ \dontrun{ @@ -55,9 +54,6 @@ result$text # Using local server set_stt_base("http://localhost:4123") result <- stt("speech.wav") - -# Using audio.whisper directly -result <- stt("speech.wav", backend = "audio.whisper") } } diff --git a/man/stt_health.Rd b/man/stt_health.Rd index 42eaed4..774c058 100644 --- a/man/stt_health.Rd +++ b/man/stt_health.Rd @@ -9,7 +9,7 @@ stt_health() A list with components: \describe{ \item{ok}{Logical. TRUE if a backend is available.} - \item{backend}{Character. The available backend ("api", "audio.whisper"), + \item{backend}{Character. The available backend ("api" or "whisper"), or NULL if none available.} \item{message}{Character. Status message with details.} }