1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
//! The [`Synthesizer`] trait, its [`TtsError`], and the [`TtsAudioStream`] alias.
use async_trait;
use ;
use MaybeSendSync;
/// The audio a [`Synthesizer::synthesize`] call yields: a boxed stream of
/// [`AudioChunk`] results, delivered incrementally.
///
/// `BoxStream` on native and `LocalBoxStream` on `wasm32`, behind one cfg'd
/// alias — the same `Send`-where-it-exists split as
/// [`MaybeSend`](pipecrab_runtime::MaybeSend): the pipeline is one logical task
/// that stays `Send` for a work-stealing executor natively, while on `wasm32`
/// (one thread, `!Send` JS handles) that bound must vanish.
pub type TtsAudioStream = BoxStream;
/// The audio a [`Synthesizer::synthesize`] call yields: a boxed stream of
/// [`AudioChunk`] results, delivered incrementally.
pub type TtsAudioStream = LocalBoxStream;
/// The swappable text-to-speech capability: text in, audio out incrementally.
///
/// This is the durable interface. A native engine and a browser engine (in a Web
/// Worker) both implement this one trait, so [`TtsStage`](crate::TtsStage) — and
/// the pipeline above it — never names a concrete model.
///
/// # Streaming is a barge-in requirement
///
/// [`synthesize`](Synthesizer::synthesize) yields audio a chunk at a time rather
/// than one buffer at the end: every stream item is a preemption point the run
/// loop can drop an in-flight synthesis at, so a user barging in stops playback
/// within one chunk instead of after a whole utterance. Dropping the stream is
/// how the *stage* stops pulling; [`cancel`](Synthesizer::cancel) is how the
/// *engine* stops producing.
///
/// [`cancel`](Synthesizer::cancel) is a *control call* (see
/// [`Processor`](pipecrab_core::Processor)'s control-call carve-out): it flips an
/// atomic the engine's worker observes, so it is synchronous, non-blocking, and
/// safe to invoke directly from a stage's `decide_*` where the barge-in is
/// decided. [`synthesize`](Synthesizer::synthesize) is async because it hands
/// text to that worker and returns its audio stream.
///
/// `?Send` on `wasm32` matches pipecrab's single-threaded execution model, so
/// one implementation runs unchanged on a current-thread executor and in the
/// browser, where `Send` bounds cannot be satisfied.
/// Why a [`Synthesizer::synthesize`] call failed.
///
/// Mirrors the message-plus-kind shape of the pipeline's other error types (e.g.
/// `pipecrab-stt`'s `SttError`) so the conversion at the stage boundary
/// (`impl From<TtsError> for StageError`) is direct. A synthesizer produces
/// audio rather than consuming a caller-chosen format, so it has no
/// `UnsupportedFormat` variant to reject with.