Coverage Report

Created: 2026-11-04 02:14

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
super-tts-app/src/daemon/client/v1/speak.rs
Line
Count
Source
1
// SPDX-License-Identifier: GPL-3.0-only
2
//! `/speak` — synthesize text and play it through the daemon.
3
//!
4
//! Both calls are ordinary requests, not streams: the daemon answers as soon as
5
//! the utterance is queued and keeps playing after the connection closes. The
6
//! Speech page follows what happens next through the `speaking_state` /
7
//! `speech_progress` topics on `GET /events`, which is also what keeps its
8
//! badge right when something *else* asked the daemon to speak.
9
10
use crate::daemon::client::internal::response::require_unit;
11
use crate::daemon::client::internal::session::with_settings_token;
12
use super_tts_shared::daemon::http_client;
13
use super_tts_shared::daemon::http_client::HttpResult;
14
15
/// `POST /speak` — speak `text` with the active model and its default voice.
16
///
17
/// Returns the utterance id the daemon assigned, so the caller can tell its own
18
/// utterance apart from one another client started.
19
0
pub async fn speak_command(text: String) -> HttpResult<Option<String>> {
20
0
    with_settings_token(|socket, token| {
21
0
        let text = text.clone();
22
0
        async move {
23
0
            let resp = http_client::speak(socket, &token, &text).await?;
24
0
            let utterance_id = resp.utterance_id.clone();
25
0
            require_unit(resp, "speak_command")?;
26
0
            Ok(utterance_id)
27
0
        }
28
0
    })
29
0
    .await
30
0
}
31
32
/// Select `voice` for the model on `stage`, then speak `text` in it.
33
///
34
/// The Voices page's preview: a cloned voice is only judgeable by ear, and the
35
/// daemon already owns the output device, so hearing one is a normal utterance
36
/// rather than anything the app plays itself.
37
///
38
/// Two calls, because a speak request has no voice field: which voice the
39
/// machine speaks in is a setting, so auditioning one *is* selecting it. That
40
/// is the honest shape — the preview a user listens to is the voice they will
41
/// get from every other client afterwards, rather than a sound only this page
42
/// can produce. The selection is left in place, which is what the button means.
43
///
44
/// The utterance id is discarded — a preview is not something the page later
45
/// correlates or cancels, and the speaking badge is driven by events either
46
/// way.
47
0
pub async fn speak_in_voice(
48
0
    stage: u32,
49
0
    model: String,
50
0
    voice: String,
51
0
    text: String,
52
0
) -> HttpResult<()> {
53
0
    super::pipeline::voice::set_model_voice(stage, model, voice).await?;
54
0
    speak_command(text).await.map(|_| ())
55
0
}
56
57
/// `POST /speak/stop` — stop the current utterance and drop queued audio.
58
0
pub async fn stop_speaking_command() -> HttpResult<()> {
59
0
    with_settings_token(|socket, token| async move {
60
0
        let resp = http_client::speak_stop(socket, &token).await?;
61
0
        require_unit(resp, "stop_speaking_command")
62
0
    })
63
0
    .await
64
0
}