Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions apps/mobile/ios/App/App/CrewCmdVoiceSessionPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public class CrewCmdVoiceSessionPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioPlay
private var playbackSuppressionUntil: TimeInterval = 0
private var noiseFloorRms: Double = 0
private var lastRouteRecoveryAt: TimeInterval = 0
private var reportedLongRecording = false
private var uiState = "idle"
private var uiActor: CrewCmdVoiceUiActor = .system
private var uiLevel: Double = 0
Expand All @@ -78,9 +79,9 @@ public class CrewCmdVoiceSessionPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioPlay

private let baseSilenceThreshold = 0.006
private let speechStartMs = 250.0
private let silenceEndMs = 1800.0
private let silenceEndMs = 3000.0
private let minRecordingMs = 600.0
private let maxRecordingMs = 20000.0
private let longRecordingDiagnosticMs = 20000.0

public override func load() {
super.load()
Expand Down Expand Up @@ -641,7 +642,14 @@ public class CrewCmdVoiceSessionPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioPlay
recordingSamples.append(contentsOf: intSamples)
let recordingMs = now - (recordingStartedAt ?? now)
let silenceMs = silenceStartedAt.map { now - $0 } ?? 0
if (recordingMs >= minRecordingMs && silenceMs >= silenceEndMs) || recordingMs >= maxRecordingMs {
if !reportedLongRecording && recordingMs >= longRecordingDiagnosticMs {
reportedLongRecording = true
notifyDiagnostic("native.recording.long-form", detail: [
"durationMs": recordingMs,
"samples": recordingSamples.count
])
}
if recordingMs >= minRecordingMs && silenceMs >= silenceEndMs {
finishRecording(recordingMs: recordingMs)
}
}
Expand Down Expand Up @@ -685,6 +693,7 @@ public class CrewCmdVoiceSessionPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioPlay
speechStartedAt = nil
silenceStartedAt = nil
recordingStartedAt = nil
reportedLongRecording = false
}

private func uploadWav(samples: [Int16], sampleRate: Double) {
Expand Down
15 changes: 12 additions & 3 deletions apps/mobile/scripts/ensure-ios-voice-session.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,13 @@ public class CrewCmdVoiceSessionPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioPlay
private var playbackSuppressionUntil: TimeInterval = 0
private var noiseFloorRms: Double = 0
private var lastRouteRecoveryAt: TimeInterval = 0
private var reportedLongRecording = false

private let baseSilenceThreshold = 0.006
private let speechStartMs = 250.0
private let silenceEndMs = 1800.0
private let silenceEndMs = 3000.0
private let minRecordingMs = 600.0
private let maxRecordingMs = 20000.0
private let longRecordingDiagnosticMs = 20000.0

public override func load() {
super.load()
Expand Down Expand Up @@ -623,7 +624,14 @@ public class CrewCmdVoiceSessionPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioPlay
recordingSamples.append(contentsOf: intSamples)
let recordingMs = now - (recordingStartedAt ?? now)
let silenceMs = silenceStartedAt.map { now - $0 } ?? 0
if (recordingMs >= minRecordingMs && silenceMs >= silenceEndMs) || recordingMs >= maxRecordingMs {
if !reportedLongRecording && recordingMs >= longRecordingDiagnosticMs {
reportedLongRecording = true
notifyDiagnostic("native.recording.long-form", detail: [
"durationMs": recordingMs,
"samples": recordingSamples.count
])
}
if recordingMs >= minRecordingMs && silenceMs >= silenceEndMs {
finishRecording(recordingMs: recordingMs)
}
}
Expand Down Expand Up @@ -667,6 +675,7 @@ public class CrewCmdVoiceSessionPlugin: CAPPlugin, CAPBridgedPlugin, AVAudioPlay
speechStartedAt = nil
silenceStartedAt = nil
recordingStartedAt = nil
reportedLongRecording = false
}

private func uploadWav(samples: [Int16], sampleRate: Double) {
Expand Down
15 changes: 14 additions & 1 deletion src/lib/native-voice-session-generator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,25 @@ describe("iOS native voice-session generator", () => {
it("uses native VAD timing that tolerates natural pauses and adaptive noise", () => {
expect(source).toContain("private let baseSilenceThreshold = 0.006");
expect(source).toContain("private let speechStartMs = 250.0");
expect(source).toContain("private let silenceEndMs = 1800.0");
expect(source).toContain("private let silenceEndMs = 3000.0");
expect(source).toContain("private let minRecordingMs = 600.0");
expect(source).toContain("private func currentSilenceThreshold()");
expect(source).toContain("noiseFloorRms * 3.0");
});

it("keeps recording long-form speech until the speaker pauses", () => {
const bufferHandler = source.slice(
source.indexOf("private func handleAudioBuffer"),
source.indexOf("private func finishRecording")
);

expect(source).not.toContain("maxRecordingMs");
expect(bufferHandler).toContain("recordingMs >= longRecordingDiagnosticMs");
expect(bufferHandler).toContain('notifyDiagnostic("native.recording.long-form"');
expect(bufferHandler).toContain("recordingMs >= minRecordingMs && silenceMs >= silenceEndMs");
expect(bufferHandler).not.toContain("recordingMs >= maxRecordingMs");
});

it("does not hard-restart the native engine for every route configuration change", () => {
const routeChangeHandler = source.slice(
source.indexOf("private func handleAudioRouteChange"),
Expand Down
Loading