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
142 changes: 125 additions & 17 deletions openless-all/app/crates/openless-core/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ pub struct LessComputerVoiceSession {
less_computer: Arc<dyn crate::domains::LessComputerApi>,
request: crate::domains::LessComputerRunRequest,
partials: Arc<VoiceTranscriptSink>,
received_bytes: AtomicU64,
archive_successful_recording: bool,
}

Expand Down Expand Up @@ -358,6 +357,22 @@ impl crate::ports::RecordingProgressSink for LessComputerRecordingProgress {
crate::ports::RecordingEvent::Level { elapsed_ms, level } => {
self.publish_level(elapsed_ms, level)
}
crate::ports::RecordingEvent::LimitReached => {
let session_id = self.session_id;
let less_computer = Arc::clone(&self.less_computer);
let control = Arc::clone(&self.control);
self.task_spawner.spawn(Box::pin(async move {
if less_computer.capture_cancelled(session_id) {
return;
}
if let Err(error) =
control.request(session_id, crate::events::RecordingControlAction::Stop)
{
log::warn!("failed to stop Less Computer capture at PCM limit: {error}");
}
}));
Ok(())
}
crate::ports::RecordingEvent::Fatal(error) => {
let session_id = self.session_id;
let less_computer = Arc::clone(&self.less_computer);
Expand Down Expand Up @@ -500,6 +515,10 @@ impl crate::ports::RecordingProgressSink for QaRecordingProgress {
crate::ports::RecordingEvent::Level { elapsed_ms, level } => {
self.publish_level(elapsed_ms, level)
}
crate::ports::RecordingEvent::LimitReached => {
self.submit_terminal(QaRecordingTerminal::Stop);
Ok(())
}
crate::ports::RecordingEvent::Fatal(error) => {
self.submit_terminal(QaRecordingTerminal::Fault(error));
Ok(())
Expand Down Expand Up @@ -556,6 +575,18 @@ impl crate::ports::RecordingProgressSink for SelectionVoiceRecordingProgress {
crate::ports::RecordingEvent::Level { elapsed_ms, level } => {
self.publish_level(elapsed_ms, level)
}
crate::ports::RecordingEvent::LimitReached => {
let session_id = self.session_id;
let control = Arc::clone(&self.control);
self.task_spawner.spawn(Box::pin(async move {
if let Err(error) =
control.request(session_id, crate::events::RecordingControlAction::Stop)
{
log::warn!("failed to stop selection voice at PCM limit: {error}");
}
}));
Ok(())
}
crate::ports::RecordingEvent::Fatal(error) => {
let session_id = self.session_id;
let selection_voice = Arc::clone(&self.selection_voice);
Expand Down Expand Up @@ -915,19 +946,6 @@ impl LessComputerVoiceSession {
"Less Computer PCM must be non-empty and contain complete 16-bit samples",
));
}
const MAX_PCM_BYTES: u64 = 128 * 1024 * 1024;
let next = self
.received_bytes
.fetch_add(pcm.len() as u64, Ordering::AcqRel)
.saturating_add(pcm.len() as u64);
if next > MAX_PCM_BYTES {
self.received_bytes
.fetch_sub(pcm.len() as u64, Ordering::AcqRel);
return Err(BackendError::new(
BackendErrorCode::InvalidArgument,
"Less Computer PCM exceeds the provider limit",
));
}
self.control.transcription.consume_pcm_chunk(pcm);
Ok(())
}
Expand Down Expand Up @@ -1722,6 +1740,29 @@ impl EngineProgressSink for BackendEngineProgress {
);
}
}
EngineProgress::RecordingLimitReached => {
let state = self.state.read().expect("backend state lock poisoned");
ensure_active_session(&state, session_id)?;
if !matches!(
state.dictation.phase,
DictationPhase::Starting | DictationPhase::Recording
) {
return Err(BackendError::new(
BackendErrorCode::InvalidState,
"recording limit arrived after recording stopped",
));
}
drop(state);
self.events.publish(
Some(session_id),
BackendEventKind::RecordingControlRequested(
crate::events::RecordingControlRequest {
session_id,
action: crate::events::RecordingControlAction::Stop,
},
),
);
}
EngineProgress::RecordingFault(error) => {
let mut state = self.state.write().expect("backend state lock poisoned");
ensure_active_session(&state, session_id)?;
Expand Down Expand Up @@ -2487,7 +2528,7 @@ impl OpenLessBackend {
session_id,
Arc::clone(&context),
Arc::clone(&partials) as Arc<dyn TextStreamSink>,
recording_progress,
Arc::clone(&recording_progress) as Arc<dyn crate::ports::RecordingProgressSink>,
resources.cancel.clone(),
),
discard_voice_capture,
Expand All @@ -2500,10 +2541,12 @@ impl OpenLessBackend {
match own_voice_start(
&self.deps.task_spawner,
Arc::clone(&resources),
self.deps.dictation_engine.start_transcription(
Arc::clone(&self.deps.dictation_engine).start_transcription_with_progress(
session_id,
Arc::clone(&context),
Arc::clone(&partials) as Arc<dyn TextStreamSink>,
Arc::clone(&recording_progress)
as Arc<dyn crate::ports::RecordingProgressSink>,
),
|transcription| transcription.cancel(),
)
Expand Down Expand Up @@ -2576,7 +2619,6 @@ impl OpenLessBackend {
less_computer: Arc::clone(&self.deps.services.less_computer),
request,
partials,
received_bytes: AtomicU64::new(0),
archive_successful_recording: context.recording.archive_successful_recording,
})
}
Expand Down Expand Up @@ -6617,6 +6659,7 @@ mod tests {
struct VoiceTranscription {
pcm: Mutex<Vec<u8>>,
cancelled: std::sync::atomic::AtomicBool,
starts: AtomicU64,
}

impl crate::ports::AudioConsumer for VoiceTranscription {
Expand Down Expand Up @@ -6660,6 +6703,7 @@ mod tests {
_context: Arc<DictationContext>,
_partials: Arc<dyn TextStreamSink>,
) -> BoxFuture<'static, Result<Arc<dyn TranscriptionSession>, BackendError>> {
self.0.starts.fetch_add(1, Ordering::AcqRel);
let session: Arc<dyn TranscriptionSession> = self.0.clone();
boxed(async move { Ok(session) })
}
Expand Down Expand Up @@ -6859,6 +6903,7 @@ mod tests {
.unwrap();
let mut preferences = backend.get_preferences();
preferences.coding_agent_enabled = true;
preferences.stable_transcription_enabled = true;
backend.set_preferences(preferences).unwrap();
let mut events = backend.subscribe();

Expand All @@ -6867,6 +6912,7 @@ mod tests {
.start_less_computer_voice(session_id, Arc::new(FakeRecordingControl::default()))
.await
.unwrap();
assert_eq!(transcription.starts.load(Ordering::Acquire), 0);
assert_eq!(host.actions(), vec![HostAction::ShowLessComputer]);
assert_eq!(
session.feed_pcm(&[]).unwrap_err().code,
Expand All @@ -6881,6 +6927,7 @@ mod tests {

assert_eq!(result.session_id, session_id);
assert_eq!(*transcription.pcm.lock().unwrap(), vec![1, 0, 2, 0]);
assert_eq!(transcription.starts.load(Ordering::Acquire), 1);
assert!(runtime.request.lock().unwrap().is_some());
let transcript_events = std::iter::from_fn(|| events.try_recv().ok())
.filter(|event| matches!(event.kind, BackendEventKind::TranscriptDelta(_)))
Expand All @@ -6890,6 +6937,13 @@ mod tests {
transcript_events[0].kind,
BackendEventKind::TranscriptDelta(crate::TranscriptDelta { is_final: true, .. })
));

let cancelled = backend
.start_less_computer_voice(SessionId::new(), Arc::new(FakeRecordingControl::default()))
.await
.unwrap();
cancelled.cancel().await.unwrap();
assert_eq!(transcription.starts.load(Ordering::Acquire), 1);
}

#[tokio::test]
Expand Down Expand Up @@ -7188,6 +7242,15 @@ mod tests {
))),
};
use crate::ports::RecordingProgressSink;
progress
.publish(crate::ports::RecordingEvent::LimitReached)
.unwrap();
tokio::task::yield_now().await;
assert_eq!(
*control.requests.lock().unwrap(),
vec![(stop_session, crate::events::RecordingControlAction::Stop)]
);
control.requests.lock().unwrap().clear();
progress.publish_level(10, 0.1).unwrap();
progress.publish_level(20, 0.1).unwrap();
progress.publish_level(30, 0.1).unwrap();
Expand Down Expand Up @@ -7276,6 +7339,22 @@ mod tests {
assert_eq!(qa.cancels.load(Ordering::Acquire), 0);
assert_eq!(qa.faults.load(Ordering::Acquire), 0);

let limit_progress = QaRecordingProgress {
session_id: SessionId::new(),
qa: Arc::clone(&qa) as Arc<dyn crate::domains::QaApi>,
progress: Arc::new(VoiceRecordingProgress),
task_spawner: Arc::new(TokioTaskSpawner),
started_at: std::time::Instant::now(),
silence: Mutex::new(None),
terminal: Mutex::new(QaRecordingTerminalState::default()),
};
limit_progress.arm();
limit_progress
.publish(crate::ports::RecordingEvent::LimitReached)
.unwrap();
tokio::task::yield_now().await;
assert_eq!(qa.stops.load(Ordering::Acquire), 2);

let started_at = std::time::Instant::now();
let no_speech = QaRecordingProgress {
session_id: SessionId::new(),
Expand Down Expand Up @@ -9439,6 +9518,35 @@ mod tests {
backend.shutdown().await.unwrap();
}

#[tokio::test]
async fn recording_limit_requests_the_normal_dictation_stop_path() {
let (backend, _) = backend();
backend.start().await.unwrap();
let session_id = backend.start_dictation().await.unwrap();
let mut events = backend.subscribe();

backend
.engine_progress_sink()
.publish(session_id, EngineProgress::RecordingLimitReached)
.unwrap();

let request = std::iter::from_fn(|| events.try_recv().ok()).find_map(|event| match event {
crate::events::BackendEvent {
session_id: Some(id),
kind: BackendEventKind::RecordingControlRequested(request),
..
} if id == session_id => Some(request),
_ => None,
});
assert_eq!(
request.map(|request| request.action),
Some(crate::events::RecordingControlAction::Stop)
);

backend.cancel_dictation(Some(session_id)).await.unwrap();
backend.shutdown().await.unwrap();
}

#[tokio::test]
async fn stop_time_context_update_failure_cancels_and_resets_the_session() {
use crate::testing::FixtureEngineAction;
Expand Down
Loading
Loading