summaryrefslogtreecommitdiff
path: root/tests/test-integration-recording-monitor-capture.el
blob: 7d7c5dfbd3ef3818af33b51e12c915b8c5dad2e3 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
;;; test-integration-recording-monitor-capture.el --- Integration test for monitor audio capture -*- lexical-binding: t; -*-

;; Author: Craig Jennings <c@cjennings.net>
;; Created: 2025-11-14

;;; Commentary:
;;
;; Integration test that verifies phone call audio (speaker output) is captured
;; during recording. This tests the actual PulseAudio device selection end-to-end.
;;
;; This test:
;; 1. Plays known speech through speakers (simulating phone call audio)
;; 2. Records it using the configured monitor device
;; 3. Transcribes the recording
;; 4. Verifies the expected text appears in the transcription
;;
;; Requirements:
;; - Audio system must be working (PulseAudio/PipeWire)
;; - Recording devices must be configured (C-; r c)
;; - paplay must be available
;; - Transcription backend must be configured
;;
;; This is a MANUAL integration test - not suitable for CI since it requires
;; working audio hardware.
;;
;; USAGE:
;;   M-x ert RET test-integration-recording-monitor-capture RET
;;
;;   Or from command line:
;;   make test-file FILE=tests/test-integration-recording-monitor-capture.el

;;; Code:

(require 'ert)
(require 'video-audio-recording)
(require 'transcription-config)

(defvar test-recording-monitor--test-audio-file
  (expand-file-name "tests/fixtures/audio/speaker-output-test.wav"
                    user-emacs-directory)
  "Test audio file with known speech for speaker output testing.")

(defvar test-recording-monitor--expected-phrases
  '("quick brown fox" "lazy dog" "speaker output test")
  "Phrases expected in transcription.
We use partial matches since transcription may not be 100% accurate.")

(defun test-recording-monitor--wait-for-transcription (audio-file timeout)
  "Wait for transcription of AUDIO-FILE to complete, up to TIMEOUT seconds.
Returns the path to the .txt file if successful, nil if timeout."
  (let* ((txt-file (concat (file-name-sans-extension audio-file) ".txt"))
         (deadline (time-add (current-time) (seconds-to-time timeout))))
    ;; Wait for .txt file to be created and have content
    (while (and (time-less-p (current-time) deadline)
                (or (not (file-exists-p txt-file))
                    (= 0 (file-attribute-size (file-attributes txt-file)))))
      (sleep-for 1)
      (message "Waiting for transcription... (%d seconds remaining)"
               (ceiling (float-time (time-subtract deadline (current-time))))))
    (if (and (file-exists-p txt-file)
             (> (file-attribute-size (file-attributes txt-file)) 0))
        txt-file
      nil)))

(defun test-recording-monitor--cleanup (recording-file)
  "Clean up RECORDING-FILE and associated transcription files."
  (when (and recording-file (file-exists-p recording-file))
    (let* ((base (file-name-sans-extension recording-file))
           (txt-file (concat base ".txt"))
           (log-file (concat base ".log")))
      (when (file-exists-p recording-file)
        (delete-file recording-file))
      (when (file-exists-p txt-file)
        (delete-file txt-file))
      (when (file-exists-p log-file)
        (delete-file log-file)))))

(ert-deftest test-integration-recording-monitor-capture ()
  "Integration test: verify speaker output (monitor) is captured during recording.

This test simulates a phone call scenario:
- Test audio plays through speakers (simulates remote person speaking)
- Recording should capture this via the monitor device
- Transcription should contain the expected speech

This verifies that the system audio monitor device selection is working correctly.

Components integrated:
- video-audio-recording.el (device selection, ffmpeg command generation)
- PulseAudio/PipeWire (actual audio routing)
- ffmpeg (recording process)
- transcription-config.el (speech-to-text verification)

Test category: Integration test (requires working audio hardware)"
  :tags '(:integration :manual :audio)

  ;; Skip if prerequisites not met
  (skip-unless (executable-find "paplay"))
  (skip-unless (executable-find "ffmpeg"))
  (skip-unless (file-exists-p test-recording-monitor--test-audio-file))
  (skip-unless (and cj/recording-mic-device cj/recording-system-device))

  (let ((recording-file nil)
        (playback-process nil))
    (unwind-protect
        (progn
          (message "\n========================================")
          (message "Starting monitor capture integration test")
          (message "========================================")
          (message "Test audio: %s" test-recording-monitor--test-audio-file)
          (message "Mic device: %s" cj/recording-mic-device)
          (message "Monitor device: %s" cj/recording-system-device)
          (message "Backend: %s" cj/transcribe-backend)

          ;; Step 1: Start recording
          (message "\n[1/6] Starting recording...")
          (let ((temp-dir (make-temp-file "recording-test-" t)))
            (cj/ffmpeg-record-audio temp-dir)
            ;; Give ffmpeg a moment to initialize
            (sleep-for 1)
            (should (process-live-p cj/audio-recording-ffmpeg-process))
            (message "✓ Recording started (process: %s)"
                     (process-name cj/audio-recording-ffmpeg-process))

            ;; Determine the recording filename (ffmpeg uses timestamp)
            ;; We'll find it after stopping
            (setq recording-file
                  (expand-file-name
                   (concat (format-time-string "%Y-%m-%d-%H-%M-%S") ".m4a")
                   temp-dir)))

          ;; Step 2: Play test audio through speakers
          (message "\n[2/6] Playing test audio through speakers...")
          (setq playback-process
                (start-process "test-audio-playback"
                               "*test-audio-playback*"
                               "paplay"
                               test-recording-monitor--test-audio-file))
          (message "✓ Playback started (process: %s)"
                   (process-name playback-process))

          ;; Step 3: Wait for playback to complete
          (message "\n[3/6] Waiting for playback to complete...")
          (let ((max-wait 10)
                (elapsed 0))
            (while (and (process-live-p playback-process)
                        (< elapsed max-wait))
              (sleep-for 0.5)
              (setq elapsed (+ elapsed 0.5)))
            (if (process-live-p playback-process)
                (progn
                  (kill-process playback-process)
                  (error "Playback did not complete within %d seconds" max-wait))
              (message "✓ Playback completed (%.1f seconds)" elapsed)))

          ;; Give audio a moment to be captured
          (sleep-for 1)

          ;; Step 4: Stop recording
          (message "\n[4/6] Stopping recording...")
          (cj/audio-recording-stop)
          (sleep-for 1)
          (should-not (process-live-p cj/audio-recording-ffmpeg-process))

          ;; Find the actual recording file (may differ by timestamp)
          (let* ((dir (file-name-directory recording-file))
                 (files (directory-files dir t "\\.m4a$")))
            (should (= 1 (length files)))
            (setq recording-file (car files)))

          (should (file-exists-p recording-file))
          (message "✓ Recording stopped")
          (message "  Recording file: %s" recording-file)
          (message "  File size: %d bytes"
                   (file-attribute-size (file-attributes recording-file)))

          ;; Step 5: Transcribe recording
          (message "\n[5/6] Transcribing recording (this may take 30-60 seconds)...")
          (cj/transcribe-audio recording-file)

          ;; Wait for transcription to complete
          (let ((txt-file (test-recording-monitor--wait-for-transcription
                           recording-file 120)))
            (should txt-file)
            (message "✓ Transcription completed")
            (message "  Transcript file: %s" txt-file)

            ;; Step 6: Verify transcription contains expected phrases
            (message "\n[6/6] Verifying transcription content...")
            (let ((transcript (with-temp-buffer
                                (insert-file-contents txt-file)
                                (downcase (buffer-string)))))
              (message "Transcript length: %d characters" (length transcript))
              (message "Transcript preview: %s"
                       (substring transcript 0 (min 100 (length transcript))))

              ;; Verify at least 2 of the expected phrases appear
              (let ((matches 0))
                (dolist (phrase test-recording-monitor--expected-phrases)
                  (when (string-match-p phrase transcript)
                    (setq matches (1+ matches))
                    (message "✓ Found expected phrase: '%s'" phrase)))

                (message "\nMatched %d of %d expected phrases"
                         matches (length test-recording-monitor--expected-phrases))

                (should (>= matches 2))
                (message "\n✓✓✓ TEST PASSED ✓✓✓")
                (message "Monitor device is correctly capturing speaker audio!")))))

      ;; Cleanup
      (message "\nCleaning up...")
      (when (process-live-p playback-process)
        (kill-process playback-process))
      (when (and cj/audio-recording-ffmpeg-process
                 (process-live-p cj/audio-recording-ffmpeg-process))
        (cj/audio-recording-stop))
      (test-recording-monitor--cleanup recording-file)
      (message "✓ Cleanup complete"))))

(provide 'test-integration-recording-monitor-capture)
;;; test-integration-recording-monitor-capture.el ends here