aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-04-19 07:06:45 -0500
committerCraig Jennings <c@cjennings.net>2026-04-19 07:06:45 -0500
commit85d9127ea6a5cd624dd4567618ce87b12e491e8c (patch)
treea1d5ae757b83ccdfaa51b3062e9febdc6c4844d5
parentcb9fe91fbb35aef0a9de578a66e5f59df964321c (diff)
downloaddotemacs-85d9127ea6a5cd624dd4567618ce87b12e491e8c.tar.gz
dotemacs-85d9127ea6a5cd624dd4567618ce87b12e491e8c.zip
refactor(transcription): consolidate backends into descriptor alist
Introduce cj/--transcription-backends alist mapping each backend to (:script :auth-host :env-var). Replace: - two near-identical cj/--get-{openai,assemblyai}-api-key functions with a single parameterized cj/--auth-source-password helper - the pcase in cj/--transcription-script-path with an alist lookup - the pcase block in cj/--start-transcription-process that assembled the API-key env var with an alist-driven assembly Adding a new backend is now a single line in the alist. The existing tests plus retargeted API-key tests (now 10, covering the parameterized helper and the descriptor data) verify no behavior change.
-rw-r--r--modules/transcription-config.el68
-rw-r--r--tests/test-transcription-api-key.el115
2 files changed, 93 insertions, 90 deletions
diff --git a/modules/transcription-config.el b/modules/transcription-config.el
index 8e3eb798..2adbaa4d 100644
--- a/modules/transcription-config.el
+++ b/modules/transcription-config.el
@@ -62,6 +62,20 @@ Log files are always kept on error regardless of this setting.")
Each entry: (process audio-file start-time status)
Status: running, complete, error")
+;; ---------------------------- Backend Descriptors ---------------------------
+
+(defconst cj/--transcription-backends
+ '((openai-api :script "oai-transcribe" :auth-host "api.openai.com" :env-var "OPENAI_API_KEY")
+ (assemblyai :script "assemblyai-transcribe" :auth-host "api.assemblyai.com" :env-var "ASSEMBLYAI_API_KEY")
+ (local-whisper :script "local-whisper" :auth-host nil :env-var nil))
+ "Per-backend descriptors. Each entry: (SYMBOL :script S :auth-host H :env-var V).
+`:auth-host' and `:env-var' are nil for local backends that need no API key.")
+
+(defun cj/--backend-plist (backend)
+ "Return the descriptor plist for BACKEND, or signal if unknown."
+ (or (alist-get backend cj/--transcription-backends)
+ (user-error "Unknown transcription backend: %s" backend)))
+
;; ----------------------------- Pure Functions --------------------------------
(defun cj/--audio-file-p (file)
@@ -90,34 +104,15 @@ SUCCESS-P indicates whether transcription succeeded."
cj/transcription-keep-log-when-done))
(defun cj/--transcription-script-path ()
- "Return absolute path to transcription script based on backend."
- (let ((script-name (pcase cj/transcribe-backend
- ('openai-api "oai-transcribe")
- ('assemblyai "assemblyai-transcribe")
- ('local-whisper "local-whisper"))))
+ "Return absolute path to transcription script for the active backend."
+ (let ((script-name (plist-get (cj/--backend-plist cj/transcribe-backend) :script)))
(expand-file-name (concat "scripts/" script-name) user-emacs-directory)))
-(defun cj/--get-openai-api-key ()
- "Retrieve OpenAI API key from authinfo.gpg.
-Expects entry in authinfo.gpg:
- machine api.openai.com login api password sk-...
-Returns the API key string, or nil if not found."
- (when-let* ((auth-info (car (auth-source-search
- :host "api.openai.com"
- :require '(:secret))))
- (secret (plist-get auth-info :secret)))
- (if (functionp secret)
- (funcall secret)
- secret)))
-
-(defun cj/--get-assemblyai-api-key ()
- "Retrieve AssemblyAI API key from authinfo.gpg.
-Expects entry in authinfo.gpg:
- machine api.assemblyai.com login api password <key>
-Returns the API key string, or nil if not found."
- (when-let* ((auth-info (car (auth-source-search
- :host "api.assemblyai.com"
- :require '(:secret))))
+(defun cj/--auth-source-password (host)
+ "Retrieve password for HOST from authinfo.gpg.
+Expects entry like: machine HOST login api password <key>.
+Returns the password string, or nil if no matching entry exists."
+ (when-let* ((auth-info (car (auth-source-search :host host :require '(:secret))))
(secret (plist-get auth-info :secret)))
(if (functionp secret)
(funcall secret)
@@ -164,19 +159,14 @@ Returns the process object."
;; Start process with environment
(let* ((process-environment
- ;; Add API key to environment based on backend
- (pcase cj/transcribe-backend
- ('openai-api
- (if-let ((api-key (cj/--get-openai-api-key)))
- (cons (format "OPENAI_API_KEY=%s" api-key)
- process-environment)
- (user-error "OpenAI API key not found in authinfo.gpg for host api.openai.com")))
- ('assemblyai
- (if-let ((api-key (cj/--get-assemblyai-api-key)))
- (cons (format "ASSEMBLYAI_API_KEY=%s" api-key)
- process-environment)
- (user-error "AssemblyAI API key not found in authinfo.gpg for host api.assemblyai.com")))
- (_ process-environment)))
+ (let* ((desc (cj/--backend-plist cj/transcribe-backend))
+ (auth-host (plist-get desc :auth-host))
+ (env-var (plist-get desc :env-var)))
+ (if (and auth-host env-var)
+ (if-let ((api-key (cj/--auth-source-password auth-host)))
+ (cons (format "%s=%s" env-var api-key) process-environment)
+ (user-error "API key not found in authinfo.gpg for host %s" auth-host))
+ process-environment)))
(process (make-process
:name process-name
:buffer (get-buffer-create buffer-name)
diff --git a/tests/test-transcription-api-key.el b/tests/test-transcription-api-key.el
index 01a7a0ea..fb0fbf5a 100644
--- a/tests/test-transcription-api-key.el
+++ b/tests/test-transcription-api-key.el
@@ -1,10 +1,9 @@
-;;; test-transcription-api-key.el --- Tests for API-key retrieval -*- lexical-binding: t; -*-
+;;; test-transcription-api-key.el --- Tests for auth-source password helper -*- lexical-binding: t; -*-
;;; Commentary:
-;; Characterization tests for `cj/--get-openai-api-key' and
-;; `cj/--get-assemblyai-api-key'. Pin down current auth-source-search
-;; behavior so the upcoming consolidation into a single parameterized
-;; helper can be verified as a pure refactor.
+;; Tests for `cj/--auth-source-password', the parameterized helper that
+;; replaced the per-backend `cj/--get-openai-api-key' and
+;; `cj/--get-assemblyai-api-key' functions.
;;; Code:
@@ -18,14 +17,13 @@
(require 'transcription-config)
-;; Captures the most recent :host passed to auth-source-search so each
-;; test can assert that the right host was queried.
-(defvar test-api-key-requested-host nil)
+(defvar test-api-key-requested-host nil
+ "Captures the :host passed to the mocked `auth-source-search'.")
(defmacro test-api-key-with-auth-source (secret-value &rest body)
"Run BODY with `auth-source-search' stubbed to return SECRET-VALUE.
-SECRET-VALUE is either a string, a function returning a string, or nil
-\(to simulate no matching entry). The host passed to `auth-source-search'
+SECRET-VALUE is a string, a function returning a string, or nil (to
+simulate no matching entry). The host passed to `auth-source-search'
is captured in `test-api-key-requested-host'."
(declare (indent 1))
`(let ((test-api-key-requested-host nil))
@@ -36,55 +34,70 @@ is captured in `test-api-key-requested-host'."
(list (list :secret ,secret-value))))))
,@body)))
-;;; Normal Cases — OpenAI
+;;; Normal Cases
-(ert-deftest test-api-key-openai-normal-returns-string-secret ()
- "OpenAI key retrieval returns the string stored under :secret."
+(ert-deftest test-api-key-normal-returns-string-secret ()
+ "Returns the :secret value when stored as a string."
(test-api-key-with-auth-source "sk-abc123"
- (should (equal "sk-abc123" (cj/--get-openai-api-key)))
- (should (equal "api.openai.com" test-api-key-requested-host))))
+ (should (equal "sk-abc123" (cj/--auth-source-password "api.openai.com")))))
-(ert-deftest test-api-key-openai-normal-calls-function-secret ()
- "OpenAI key retrieval invokes :secret when it's a function."
- (test-api-key-with-auth-source (lambda () "sk-from-fn")
- (should (equal "sk-from-fn" (cj/--get-openai-api-key)))))
-
-;;; Normal Cases — AssemblyAI
-
-(ert-deftest test-api-key-assemblyai-normal-returns-string-secret ()
- "AssemblyAI key retrieval returns the string stored under :secret."
- (test-api-key-with-auth-source "aai-xyz789"
- (should (equal "aai-xyz789" (cj/--get-assemblyai-api-key)))
- (should (equal "api.assemblyai.com" test-api-key-requested-host))))
-
-(ert-deftest test-api-key-assemblyai-normal-calls-function-secret ()
- "AssemblyAI key retrieval invokes :secret when it's a function."
+(ert-deftest test-api-key-normal-calls-function-secret ()
+ "Invokes the :secret value when stored as a function."
(test-api-key-with-auth-source (lambda () "aai-from-fn")
- (should (equal "aai-from-fn" (cj/--get-assemblyai-api-key)))))
+ (should (equal "aai-from-fn" (cj/--auth-source-password "api.assemblyai.com")))))
+
+(ert-deftest test-api-key-normal-passes-host-through ()
+ "The caller-supplied HOST is forwarded to `auth-source-search'."
+ (test-api-key-with-auth-source "x"
+ (cj/--auth-source-password "api.openai.com")
+ (should (equal "api.openai.com" test-api-key-requested-host)))
+ (test-api-key-with-auth-source "x"
+ (cj/--auth-source-password "api.assemblyai.com")
+ (should (equal "api.assemblyai.com" test-api-key-requested-host)))
+ (test-api-key-with-auth-source "x"
+ (cj/--auth-source-password "example.com")
+ (should (equal "example.com" test-api-key-requested-host))))
;;; Boundary Cases
-(ert-deftest test-api-key-openai-boundary-returns-nil-when-entry-missing ()
- "OpenAI key retrieval returns nil when auth-source has no matching entry."
- (test-api-key-with-auth-source nil
- (should (null (cj/--get-openai-api-key)))))
-
-(ert-deftest test-api-key-assemblyai-boundary-returns-nil-when-entry-missing ()
- "AssemblyAI key retrieval returns nil when auth-source has no matching entry."
+(ert-deftest test-api-key-boundary-returns-nil-when-entry-missing ()
+ "Returns nil when `auth-source-search' finds no matching entry."
(test-api-key-with-auth-source nil
- (should (null (cj/--get-assemblyai-api-key)))))
-
-(ert-deftest test-api-key-openai-boundary-queries-correct-host ()
- "OpenAI key retrieval always queries `api.openai.com', never another host."
- (test-api-key-with-auth-source "sk-x"
- (cj/--get-openai-api-key)
- (should (equal "api.openai.com" test-api-key-requested-host))))
-
-(ert-deftest test-api-key-assemblyai-boundary-queries-correct-host ()
- "AssemblyAI key retrieval always queries `api.assemblyai.com'."
- (test-api-key-with-auth-source "aai-x"
- (cj/--get-assemblyai-api-key)
- (should (equal "api.assemblyai.com" test-api-key-requested-host))))
+ (should (null (cj/--auth-source-password "api.openai.com")))
+ (should (null (cj/--auth-source-password "api.assemblyai.com")))))
+
+(ert-deftest test-api-key-boundary-empty-string-host ()
+ "Empty host string is forwarded unchanged (auth-source decides)."
+ (test-api-key-with-auth-source "x"
+ (cj/--auth-source-password "")
+ (should (equal "" test-api-key-requested-host))))
+
+;;; Backend Descriptor Integration
+
+(ert-deftest test-api-key-integration-openai-backend-descriptor ()
+ "openai-api backend descriptor has the expected auth-host and env-var."
+ (let ((desc (cj/--backend-plist 'openai-api)))
+ (should (equal "api.openai.com" (plist-get desc :auth-host)))
+ (should (equal "OPENAI_API_KEY" (plist-get desc :env-var)))
+ (should (equal "oai-transcribe" (plist-get desc :script)))))
+
+(ert-deftest test-api-key-integration-assemblyai-backend-descriptor ()
+ "assemblyai backend descriptor has the expected auth-host and env-var."
+ (let ((desc (cj/--backend-plist 'assemblyai)))
+ (should (equal "api.assemblyai.com" (plist-get desc :auth-host)))
+ (should (equal "ASSEMBLYAI_API_KEY" (plist-get desc :env-var)))
+ (should (equal "assemblyai-transcribe" (plist-get desc :script)))))
+
+(ert-deftest test-api-key-integration-local-whisper-needs-no-key ()
+ "local-whisper descriptor explicitly has nil auth-host and env-var."
+ (let ((desc (cj/--backend-plist 'local-whisper)))
+ (should (null (plist-get desc :auth-host)))
+ (should (null (plist-get desc :env-var)))
+ (should (equal "local-whisper" (plist-get desc :script)))))
+
+(ert-deftest test-api-key-integration-unknown-backend-errors ()
+ "Requesting an unknown backend's descriptor signals user-error."
+ (should-error (cj/--backend-plist 'nonexistent-backend) :type 'user-error))
(provide 'test-transcription-api-key)
;;; test-transcription-api-key.el ends here