Skip to content

Commit 70bee67

Browse files
authored
Merge pull request #57 from pragmatrix/google-v2-integration
Google Transcribe V2 Integration
2 parents d407ee5 + c2b88ac commit 70bee67

15 files changed

Lines changed: 1058 additions & 227 deletions

File tree

.github/copilot-instructions.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
- Prefer imports over deeply-qualified module paths. As a rule of thumb, avoid using more than one module prefix inline (for example, prefer importing a type and using `TypeName` instead of writing `foo::bar::TypeName` repeatedly).
55
- Prefer high-level flow first: when practical, place local supporting definitions (for example helper structs, impls, functions, and type aliases) below their first use.
66
- Keep imports grouped and sorted to match existing file style.
7+
- Avoid `maybe_` prefixes for optional variables; use neutral names and rely on type/context for optionality.
8+
- Avoid `_ref` suffixes for local variable names; use descriptive neutral names instead.
79

810
## Change Communication
911
- Include a short rationale for each non-trivial code change.
@@ -12,3 +14,11 @@
1214
- Avoid defensive code unless there is concrete evidence it is necessary.
1315
- Avoid redundant logic and repeated calls; keep only the minimal behavior required for correctness.
1416
- Do not add tests unless explicitly requested by the user.
17+
18+
## Control Flow Style
19+
- Prefer exhaustive `match` statements for enum-based control flow instead of `if matches!(...)` shortcuts.
20+
21+
## Memory Promotion
22+
- When a durable repository-specific preference is learned during a session, write it into this file as a concise bullet if it can help future sessions.
23+
- Keep additions short, actionable, and scoped to coding behavior in this repository.
24+
- Do not add temporary experiment details or one-off debugging notes.

.vscode/settings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"cSpell.words": [
3+
"alsa",
4+
"aristech",
5+
"denoiser",
6+
"subtag"
7+
]
8+
}

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ azure = { workspace = true }
3333
azure-speech = { workspace = true }
3434
aristech = { workspace = true }
3535
elevenlabs = { workspace = true }
36+
google-transcribe = { workspace = true }
3637

3738
# basic
3839

@@ -63,14 +64,15 @@ tracing-subscriber = { workspace = true }
6364
context-switch-core = { workspace = true }
6465

6566
dotenvy = { workspace = true }
67+
clap = { version = "4.6.1", features = ["derive"] }
6668

6769
# audio input
6870
cpal = "0.17.3"
6971
rodio = { workspace = true, features = ["playback"] }
7072

7173
azure = { workspace = true }
7274
aristech = { workspace = true }
73-
google-transcribe = { path = "services/google-transcribe" }
75+
google-transcribe = { workspace = true }
7476

7577
tokio = { workspace = true, features = ["rt-multi-thread"] }
7678

@@ -91,6 +93,7 @@ azure = { path = "services/azure" }
9193
playback = { path = "services/playback" }
9294
aristech = { path = "services/aristech" }
9395
elevenlabs = { path = "services/elevenlabs" }
96+
google-transcribe = { path = "services/google-transcribe" }
9497

9598
anyhow = "1.0.102"
9699
derive_more = { version = "2.1.1", features = ["full"] }

core/src/conversation.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,13 +119,13 @@ impl Conversation {
119119
}
120120
}
121121

122-
pub fn require_text_output(&self, interim: bool) -> Result<()> {
122+
pub fn require_text_output(&self, supports_interim_text: bool) -> Result<()> {
123123
for modality in &self.output_modalities {
124124
match modality {
125125
OutputModality::Audio { .. } => bail!("No audio output expected"),
126126
OutputModality::Text => {}
127127
OutputModality::InterimText => {
128-
if !interim {
128+
if !supports_interim_text {
129129
bail!("Interim text is unsupported")
130130
}
131131
}

core/src/language.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::fmt;
22

3+
use derive_more::Deref;
34
use isolang::Language;
45
use oxilangtag::LanguageTag;
56

@@ -24,6 +25,86 @@ impl fmt::Display for LanguageCodeError {
2425

2526
impl std::error::Error for LanguageCodeError {}
2627

28+
#[derive(Debug, Clone, PartialEq, Eq)]
29+
pub enum LanguageListError {
30+
Empty,
31+
MultipleValues { count: usize },
32+
}
33+
34+
impl fmt::Display for LanguageListError {
35+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36+
match self {
37+
LanguageListError::Empty => write!(f, "language list must contain at least one value"),
38+
LanguageListError::MultipleValues { count } => {
39+
write!(f, "expected exactly one language value, got {count}")
40+
}
41+
}
42+
}
43+
}
44+
45+
impl std::error::Error for LanguageListError {}
46+
47+
/// A non-empty list of normalized language codes.
48+
///
49+
/// Values are trimmed and empty entries are removed during construction.
50+
/// Construction fails if no non-empty values remain.
51+
#[derive(Debug, Clone, PartialEq, Eq, Deref)]
52+
pub struct Languages(Vec<String>);
53+
54+
impl Languages {
55+
/// Creates a non-empty language list from raw values.
56+
pub fn new(values: Vec<String>) -> Result<Self, LanguageListError> {
57+
let values = values
58+
.into_iter()
59+
.map(|x| x.trim().to_owned())
60+
.filter(|x| !x.is_empty())
61+
.collect::<Vec<_>>();
62+
63+
if values.is_empty() {
64+
return Err(LanguageListError::Empty);
65+
}
66+
67+
Ok(Self(values))
68+
}
69+
70+
/// Creates a non-empty language list from a comma-separated string.
71+
pub fn from_csv(language: &str) -> Result<Self, LanguageListError> {
72+
Self::new(
73+
language
74+
.split(',')
75+
.map(str::trim)
76+
.filter(|x| !x.is_empty())
77+
.map(str::to_owned)
78+
.collect(),
79+
)
80+
}
81+
82+
/// Returns the first language code.
83+
///
84+
/// The list is guaranteed to be non-empty after construction.
85+
pub fn first(&self) -> &String {
86+
&self.0[0]
87+
}
88+
89+
/// Returns the single language code.
90+
///
91+
/// Fails if multiple language codes are present.
92+
pub fn single(&self) -> Result<&String, LanguageListError> {
93+
if self.0.len() == 1 {
94+
Ok(self.first())
95+
} else {
96+
Err(LanguageListError::MultipleValues {
97+
count: self.0.len(),
98+
})
99+
}
100+
}
101+
102+
/// Returns the language codes joined by commas.
103+
pub fn join_csv(&self) -> String {
104+
self.0.join(",")
105+
}
106+
}
107+
27108
/// Converts a BCP 47 language tag into its ISO 639-3 language code.
28109
///
29110
/// The conversion uses the primary language subtag only and ignores script, region, variant,

examples/azure-transcribe.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,15 @@ async fn recognize(format: AudioFormat, mut input_consumer: AudioConsumer) -> Re
121121

122122
let (output_producer, mut output_consumer) = unbounded_channel();
123123
// For now this is more or less unbounded, because we push complete audio files for recognition.
124-
let (conv_input_producer, conv_input_consumer) = channel(16384);
124+
let (conversation_input_producer, conversation_input_consumer) = channel(16384);
125125

126126
let azure = AzureTranscribe;
127127
let mut conversation = azure.conversation(
128128
params,
129129
Conversation::new(
130130
InputModality::Audio { format },
131131
[OutputModality::Text, OutputModality::InterimText],
132-
conv_input_consumer,
132+
conversation_input_consumer,
133133
output_producer,
134134
),
135135
);
@@ -144,7 +144,7 @@ async fn recognize(format: AudioFormat, mut input_consumer: AudioConsumer) -> Re
144144
// Forward audio input
145145
input = input_consumer.consume() => {
146146
if let Some(frame) = input {
147-
conv_input_producer.try_send(Input::Audio {frame})?;
147+
conversation_input_producer.try_send(Input::Audio {frame})?;
148148
}
149149
else {
150150
println!("End of input");

0 commit comments

Comments
 (0)