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 | } |