Skip to content
Closed
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
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,13 +142,21 @@ Send from client to restart transcription after timeout or final message:
}
```

#### Pause/Resume Transcription
Toggle live inference without disconnecting:
```json
{ "type": "pause" }
{ "type": "resume" }
```

### Usage Pattern

1. Connect to WebSocket endpoint
2. Receive real-time word messages during transcription
3. Receive final message when session ends (timeout or silence)
4. Send restart command to begin new transcription session
5. Repeat as needed
5. Optionally send pause/resume commands to temporarily stop inference
6. Repeat as needed

## Model

Expand Down
37 changes: 34 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,10 @@ pub enum WebSocketMessage {
pub enum WebSocketCommand {
#[serde(rename = "restart")]
Restart,
#[serde(rename = "pause")]
Pause,
#[serde(rename = "resume")]
Resume,
}

pub struct TranscriptionOptions {
Expand Down Expand Up @@ -414,7 +418,7 @@ impl Model {
use futures::{SinkExt, StreamExt};
use std::io::Write;
use std::sync::Arc;
use tokio::sync::{broadcast, mpsc};
use tokio::sync::{broadcast, mpsc, watch};
use tokio_tungstenite::{accept_async, tungstenite::Message};

// WebSocket broadcast channel
Expand All @@ -425,14 +429,20 @@ impl Model {
let (restart_tx, mut restart_rx) = mpsc::unbounded_channel();
let restart_tx = Arc::new(restart_tx);

// Watch channel used to pause or resume transcription
let (pause_tx, _pause_rx) = watch::channel(false);
let pause_tx = Arc::new(pause_tx);

// Spawn WebSocket server
let listener = tokio::net::TcpListener::bind(format!("127.0.0.1:{}", ws_port)).await?;
let ws_tx_clone = ws_tx.clone();
let restart_tx_clone = restart_tx.clone();
let pause_tx_clone = pause_tx.clone();
tokio::spawn(async move {
while let Ok((stream, _)) = listener.accept().await {
let ws_tx = ws_tx_clone.clone();
let restart_tx = restart_tx_clone.clone();
let pause_tx = pause_tx_clone.clone();
tokio::spawn(async move {
let ws_stream = match accept_async(stream).await {
Ok(ws) => ws,
Expand All @@ -452,8 +462,16 @@ impl Model {
Ok(Message::Text(text)) => {
if let Ok(cmd) = serde_json::from_str::<WebSocketCommand>(&text)
{
if let WebSocketCommand::Restart = cmd {
let _ = restart_tx.send(());
match cmd {
WebSocketCommand::Restart => {
let _ = restart_tx.send(());
}
WebSocketCommand::Pause => {
let _ = pause_tx.send(true);
}
WebSocketCommand::Resume => {
let _ = pause_tx.send(false);
}
}
}
}
Expand Down Expand Up @@ -482,6 +500,7 @@ impl Model {

// Bridge blocking audio receiver to async channel
let (pcm_tx, mut pcm_rx) = mpsc::unbounded_channel();
let mut pause_rx = pause_tx.subscribe();
std::thread::spawn(move || {
while let Ok(chunk) = audio_rx.recv() {
if pcm_tx.send(chunk).is_err() {
Expand All @@ -501,6 +520,7 @@ impl Model {
let mut printed_eot = false;
let mut last_voice_activity: Option<std::time::Instant> = None;
let mut restart = false;
let mut paused = *pause_rx.borrow();

eprintln!("Starting transcription session...");

Expand All @@ -511,7 +531,18 @@ impl Model {
restart = true;
break;
}
_ = pause_rx.changed() => {
paused = *pause_rx.borrow();
if paused {
eprintln!("Transcription paused");
} else {
eprintln!("Transcription resumed");
}
}
Some(pcm_chunk) = pcm_rx.recv() => {
if paused {
continue;
}
if save_audio.is_some() {
all_audio.extend_from_slice(&pcm_chunk);
}
Expand Down
25 changes: 25 additions & 0 deletions websocket_example.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ <h1>eaRS WebSocket Transcription Client</h1>
<input type="text" id="wsUrl" value="ws://localhost:8080/" size="30">
<button onclick="connect()">Connect</button>
<button onclick="disconnect()">Disconnect</button>
<button id="pauseBtn" onclick="togglePause()" disabled>Pause</button>
</div>

<div>Status: <span id="status">Disconnected</span></div>
Expand All @@ -54,8 +55,10 @@ <h3>Instructions:</h3>

<script>
let ws = null;
let isPaused = false;
const messagesDiv = document.getElementById('messages');
const statusSpan = document.getElementById('status');
const pauseBtn = document.getElementById('pauseBtn');

function connect() {
const url = document.getElementById('wsUrl').value;
Expand All @@ -70,6 +73,9 @@ <h3>Instructions:</h3>
ws.onopen = function() {
statusSpan.textContent = 'Connected';
statusSpan.style.color = 'green';
pauseBtn.disabled = false;
pauseBtn.textContent = 'Pause';
isPaused = false;
addMessage('Connected to eaRS WebSocket server', 'info');
};

Expand All @@ -85,6 +91,9 @@ <h3>Instructions:</h3>
ws.onclose = function() {
statusSpan.textContent = 'Disconnected';
statusSpan.style.color = 'red';
pauseBtn.disabled = true;
pauseBtn.textContent = 'Pause';
isPaused = false;
addMessage('Disconnected from server', 'info');
ws = null;
};
Expand All @@ -103,6 +112,22 @@ <h3>Instructions:</h3>
ws.close();
ws = null;
}
pauseBtn.disabled = true;
pauseBtn.textContent = 'Pause';
isPaused = false;
}

function togglePause() {
if (!ws) return;
if (isPaused) {
ws.send(JSON.stringify({ type: 'resume' }));
pauseBtn.textContent = 'Pause';
isPaused = false;
} else {
ws.send(JSON.stringify({ type: 'pause' }));
pauseBtn.textContent = 'Resume';
isPaused = true;
}
}

function handleMessage(message) {
Expand Down