dot-emacs/local-lisp/nemo-whisper.el
2026-10-16 23:45:31 +02:00

222 lines
7.1 KiB
EmacsLisp

;;; -*- lexical-binding: t -*-
(require 'whisper)
;; Optional: install/load posframe if you want floating display.
;; `nemo-whisper-use-posframe' controls whether it is used.
(require 'posframe nil t)
(defgroup nemo-whisper nil
"Whisper recording utilities."
:group 'whisper)
(defcustom nemo-whisper-use-posframe t
"Display the Whisper buffer in a floating posframe when non-nil."
:type 'boolean
:group 'nemo-whisper)
(defcustom nemo-whisper-hide-posframe-after-transcription t
"Hide the posframe after copying the transcription to the kill ring."
:type 'boolean
:group 'nemo-whisper)
(defcustom nemo-whisper-posframe-width 80
"Width of the Whisper posframe."
:type 'integer
:group 'nemo-whisper)
(defcustom nemo-whisper-posframe-height 8
"Height of the Whisper posframe."
:type 'integer
:group 'nemo-whisper)
(defvar nemo-whisper--buffer nil
"Buffer currently used for Whisper recording.")
(defvar nemo-whisper--previous-buffer nil
"Buffer that was selected before Whisper capture started.")
(defvar nemo-whisper--recording-p nil
"Whether Whisper is currently recording.")
(defvar nemo-whisper--transcribing-p nil
"Whether Whisper is currently transcribing.")
(defvar nemo-whisper--posframe-active-p nil
"Whether the current capture buffer is displayed in a posframe.")
(defvar nemo-whisper--capture-map
(let ((map (make-sparse-keymap)))
;; Bind both forms because terminal and GUI Emacs can report Escape
;; differently.
(define-key map (kbd "ESC") #'nemo-whisper-escape)
(define-key map [escape] #'nemo-whisper-escape)
map)
"Keymap for a Whisper capture buffer.")
(define-minor-mode nemo-whisper-capture-mode
"Minor mode used by the Whisper capture buffer."
:init-value nil
:lighter " Whisper"
:keymap nemo-whisper--capture-map)
(defun nemo-whisper--display-buffer (buffer)
"Display BUFFER normally or in a posframe."
(if (and nemo-whisper-use-posframe
(fboundp 'posframe-show)
(display-graphic-p))
(progn
(setq nemo-whisper--posframe-active-p t)
(posframe-show
buffer
:position nil
:poshandler #'posframe-poshandler-frame-center
:width nemo-whisper-posframe-width
:height nemo-whisper-posframe-height
:border-width 2
:internal-border-width 8
:internal-border-color "gray50"
:accept-focus t)
;; Explicitly focus the child frame. Without this, keyboard input
;; may continue going to the previously selected Emacs frame.
(let ((window (get-buffer-window buffer t)))
(when (window-live-p window)
(select-frame-set-input-focus (window-frame window))
(select-window window))))
(setq nemo-whisper--posframe-active-p nil)
(pop-to-buffer buffer)))
(defun nemo-whisper--remove-recording-preview ()
"Remove the recording preview before Whisper inserts its result."
(let ((inhibit-read-only t))
(erase-buffer)
(goto-char (point-min))))
(defun nemo-whisper--stop-recording ()
"Stop recording and start Whisper transcription."
(when (and nemo-whisper--buffer
(buffer-live-p nemo-whisper--buffer)
nemo-whisper--recording-p)
(setq nemo-whisper--recording-p nil
nemo-whisper--transcribing-p t)
(with-current-buffer nemo-whisper--buffer
;; Remove “Recording...” so the transcription is the only text
;; inserted into the buffer.
(nemo-whisper--remove-recording-preview)
(whisper-run))
(message "Recording stopped; transcribing...")))
(defun nemo-whisper--copy-result-to-kill-ring ()
"Copy the completed transcription to the kill ring."
(let ((text (string-trim
(buffer-substring-no-properties
(point-min)
(point-max)))))
(unless (string-empty-p text)
(kill-new text)
(message "Whisper transcription copied to the kill ring"))
(setq nemo-whisper--transcribing-p nil)
;; Select the completed transcription. This makes a subsequent ESC
;; return to the previous buffer when the capture buffer remains visible.
(goto-char (point-min))
(push-mark (point-max) t t)
(when (and nemo-whisper--posframe-active-p
nemo-whisper-hide-posframe-after-transcription
(fboundp 'posframe-hide))
(posframe-hide (buffer-name)))))
(defun nemo-whisper--return-to-previous-buffer ()
"Return to the buffer active before Whisper capture started."
(when (and nemo-whisper--posframe-active-p
(fboundp 'posframe-hide)
nemo-whisper--buffer)
(posframe-hide (buffer-name nemo-whisper--buffer)))
(let ((previous-buffer nemo-whisper--previous-buffer))
(setq nemo-whisper--buffer nil
nemo-whisper--previous-buffer nil
nemo-whisper--recording-p nil
nemo-whisper--transcribing-p nil
nemo-whisper--posframe-active-p nil)
(when (buffer-live-p previous-buffer)
(pop-to-buffer previous-buffer))))
(defun nemo-whisper-escape ()
"Stop recording, or return to the previous buffer after transcription."
(interactive)
(cond
(nemo-whisper--recording-p
(nemo-whisper--stop-recording))
(nemo-whisper--transcribing-p
(message "Whisper is still transcribing; please wait"))
(t
(nemo-whisper--return-to-previous-buffer))))
(defun nemo-whisper-record-to-kill-ring ()
"Record speech and copy the transcription to the kill ring.
Invoke this command to create a capture buffer and start recording.
Press ESC to stop recording. After transcription completes, press ESC
again to return to the previously selected buffer."
(interactive)
(cond
;; If called while recording, treat the command as a stop command.
(nemo-whisper--recording-p
(nemo-whisper--stop-recording))
;; Do not interrupt an active transcription.
(nemo-whisper--transcribing-p
(message "Whisper is still transcribing; please wait"))
;; Start a new capture.
(t
(setq nemo-whisper--previous-buffer
(window-buffer (selected-window))
nemo-whisper--buffer
(generate-new-buffer "*Whisper capture*")
nemo-whisper--recording-p t
nemo-whisper--transcribing-p nil)
(with-current-buffer nemo-whisper--buffer
(text-mode)
(nemo-whisper-capture-mode 1)
;; Insert the temporary preview text.
(insert (propertize "Recording...\n"
'face 'font-lock-warning-face))
;; Ask Whisper to insert its result into this buffer.
(setq-local whisper-insert-text-at-point t)
;; Prevent Whisper from opening another transcription buffer.
(setq-local whisper-display-transcription-buffer nil)
;; This hook is buffer-local, so it will not run in Whisper's
;; separate transcription buffer.
(add-hook 'whisper-after-insert-hook
#'nemo-whisper--copy-result-to-kill-ring
nil
t))
(nemo-whisper--display-buffer nemo-whisper--buffer)
;; The first invocation starts recording.
(with-current-buffer nemo-whisper--buffer
(whisper-run))
(message "Recording started; press ESC when finished speaking"))))
(provide 'nemo-whisper)