A small helpdesk system: an office Pixel running a patched GrapheneOS Dialer answers technician calls, records both call legs as separate channels, and a Ruby backend transcribes them through Whisper and files an AI summary against the caller. Squashed to a single commit for sharing. No credentials are included; secrets live outside the repo in /etc/helpdesk/env on the server or a gitignored .claude/env.local locally. See .claude/env.local.example for the shape. Start at README.md, then docs/architecture.md.
121 lines
7 KiB
Diff
121 lines
7 KiB
Diff
From: Helpdesk <helpdesk@local>
|
|
Subject: [PATCH] callrecord: runtime-selectable audio quality + a probe for per-speaker capture
|
|
|
|
Stock call recording is AAC at 16000 bits/sec over a 16000 Hz stream, about one bit per sample. That is
|
|
lossy enough to corrupt ordinary Czech words before Whisper ever sees them: production transcripts
|
|
render "balíček" as "badíček" and "zmeškaný" as "myštěný". Both Whisper models the service offers make
|
|
the SAME mistakes on those words, which is what rules out the model and points at the audio.
|
|
|
|
Rather than hardcode a different number, the bitrate and output format become persist.helpdesk.rec_*
|
|
properties. Each candidate can only be judged by placing a real call and scoring the transcript, and a
|
|
rebuild-and-reflash per experiment costs hours, so one image covers the whole comparison:
|
|
|
|
adb shell setprop persist.helpdesk.rec_bitrate 96000 # better AAC
|
|
adb shell setprop persist.helpdesk.rec_format 2 # lossless WAV
|
|
adb shell setprop persist.helpdesk.rec_bitrate "" # back to stock
|
|
|
|
Defaults reproduce stock behaviour exactly, so an unprovisioned device records as it always has.
|
|
|
|
Also adds an opt-in probe (persist.helpdesk.rec_probe_sources=1) logging whether VOICE_UPLINK and
|
|
VOICE_DOWNLINK can be opened. Google Dialer records those as two AudioRecords and interleaves them into
|
|
a 2-channel WAV so caller and callee are exactly separable rather than left to a diariser to infer (see
|
|
the note in WavLPCMRecorder). Whether this device permits it is decided by the audio HAL and sepolicy,
|
|
not by the source, so it needs answering on hardware before anyone writes the dual-capture path. The
|
|
probe opens and immediately releases both and changes nothing about the recording.
|
|
|
|
Applies on top of patches 0001-0009.
|
|
---
|
|
diff --git a/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java b/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
|
|
index a64d0fc..e0a1de6 100644
|
|
--- a/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
|
|
+++ b/java/com/android/dialer/callrecord/impl/BaseCallRecorder.java
|
|
@@ -107,6 +107,49 @@ public abstract class BaseCallRecorder implements Closeable {
|
|
mAudioRecord.getAudioFormat()));
|
|
Log.d(TAG, "mPcmBufferSize " + mPcmBufferSize);
|
|
mAudioBufferPool = new ByteBufferPool(BUFFER_POOL_NUM_BUFFERS, mPcmBufferSize);
|
|
+ if (com.android.dialer.helpdesk.HelpdeskConfig.recordingProbeSources()) {
|
|
+ probeSeparateCallLegs();
|
|
+ }
|
|
+ }
|
|
+
|
|
+ /**
|
|
+ * Helpdesk patch: report whether the two call legs can be captured separately on this device.
|
|
+ * Opt-in via persist.helpdesk.rec_probe_sources=1; opens and immediately releases two AudioRecords
|
|
+ * and changes nothing about the recording in progress.
|
|
+ *
|
|
+ * <p>Recording the legs as two channels (the way Google Dialer does, see the note in
|
|
+ * WavLPCMRecorder) would make caller and callee exactly separable instead of leaving a diariser to
|
|
+ * infer it. Whether those sources are capturable is decided by the audio HAL and sepolicy, so it can
|
|
+ * only be answered on hardware. STATE_INITIALIZED on both is the green light for building that path.
|
|
+ */
|
|
+ private void probeSeparateCallLegs() {
|
|
+ final int[] sources = {
|
|
+ android.media.MediaRecorder.AudioSource.VOICE_UPLINK,
|
|
+ android.media.MediaRecorder.AudioSource.VOICE_DOWNLINK,
|
|
+ };
|
|
+ final String[] names = {"VOICE_UPLINK", "VOICE_DOWNLINK"};
|
|
+ for (int i = 0; i < sources.length; i++) {
|
|
+ AudioRecord probe = null;
|
|
+ try {
|
|
+ int min = AudioRecord.getMinBufferSize(mAudioFormat.getSampleRate(),
|
|
+ AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
|
|
+ probe = new AudioRecord(sources[i], mAudioFormat.getSampleRate(),
|
|
+ AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, Math.max(min, 4096));
|
|
+ boolean ok = probe.getState() == AudioRecord.STATE_INITIALIZED;
|
|
+ Log.i(TAG, "helpdesk source probe: " + names[i] + " -> " + (ok ? "AVAILABLE" : "uninitialised"));
|
|
+ } catch (Throwable t) {
|
|
+ Log.i(TAG, "helpdesk source probe: " + names[i] + " -> REFUSED ("
|
|
+ + t.getClass().getSimpleName() + ": " + t.getMessage() + ")");
|
|
+ } finally {
|
|
+ if (probe != null) {
|
|
+ try {
|
|
+ probe.release();
|
|
+ } catch (Throwable ignored) {
|
|
+ // releasing a probe must never affect the real recording
|
|
+ }
|
|
+ }
|
|
+ }
|
|
+ }
|
|
}
|
|
|
|
protected final long computePresentationTimeUs(int bytesRead) {
|
|
diff --git a/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java b/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
|
|
index 71dc86b..0eb2dfe 100644
|
|
--- a/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
|
|
+++ b/java/com/android/dialer/callrecord/impl/CallRecorderServiceV2.java
|
|
@@ -106,6 +106,13 @@ public class CallRecorderServiceV2 extends Service {
|
|
}
|
|
|
|
private OutputFormat getOutputFormat() {
|
|
+ // Helpdesk patch: persist.helpdesk.rec_format wins when set, so the recording format can be
|
|
+ // changed on a flashed device without a rebuild. Unset (-1) keeps the user's own preference.
|
|
+ int formatOverride = com.android.dialer.helpdesk.HelpdeskConfig.recordingFormat();
|
|
+ if (formatOverride >= 0) {
|
|
+ Log.i(TAG, "output format overridden by persist.helpdesk.rec_format=" + formatOverride);
|
|
+ return OutputFormat.getOutputFormat(formatOverride);
|
|
+ }
|
|
String def = getString(R.string.call_recording_output_format_default);
|
|
int selectionId = parseInt(getPrefs().getString(KEY_CALL_RECORDING_OUTPUT_FORMAT, def));
|
|
return OutputFormat.getOutputFormat(selectionId);
|
|
diff --git a/java/com/android/dialer/callrecord/impl/MediaCodecRecorder.java b/java/com/android/dialer/callrecord/impl/MediaCodecRecorder.java
|
|
index c3783fa..a52fb97 100644
|
|
--- a/java/com/android/dialer/callrecord/impl/MediaCodecRecorder.java
|
|
+++ b/java/com/android/dialer/callrecord/impl/MediaCodecRecorder.java
|
|
@@ -52,7 +52,16 @@ public class MediaCodecRecorder extends BaseCallRecorder {
|
|
default:
|
|
throw new IllegalArgumentException("unexpected output format " + outputFormat);
|
|
}
|
|
- mMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, outputFormat.bitRate);
|
|
+ // Helpdesk patch: persist.helpdesk.rec_bitrate overrides the format's built-in bitrate. The stock
|
|
+ // 16000 bits/sec on a 16000 Hz stream is roughly one bit per sample and mangles speech badly enough
|
|
+ // to show up downstream as transcription errors on ordinary words. 0 = unset = keep stock.
|
|
+ int bitRateOverride = com.android.dialer.helpdesk.HelpdeskConfig.recordingBitRate();
|
|
+ int bitRate = bitRateOverride > 0 ? bitRateOverride : outputFormat.bitRate;
|
|
+ if (bitRateOverride > 0) {
|
|
+ Log.i(TAG, "bitrate overridden by persist.helpdesk.rec_bitrate: "
|
|
+ + outputFormat.bitRate + " -> " + bitRate);
|
|
+ }
|
|
+ mMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
|
|
|
|
final String encoderForFormat = new MediaCodecList(MediaCodecList.REGULAR_CODECS)
|
|
.findEncoderForFormat(mMediaFormat);
|