Skip to content

Commit a648c99

Browse files
blob42simon3z
andcommitted
feat: merge upstream PR sigoden#1508 (audio transcription)
Co-Authored-By: Federico Simoncelli <federico.simoncelli@gmail.com>
1 parent d3a8812 commit a648c99

10 files changed

Lines changed: 749 additions & 745 deletions

File tree

models.yaml

Lines changed: 562 additions & 743 deletions
Large diffs are not rendered by default.

src/client/azure_openai.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ impl_client_trait!(
3838
),
3939
(prepare_embeddings, openai_embeddings),
4040
(noop_prepare_rerank, noop_rerank),
41+
(noop_audio_transcriptions),
4142
);
4243

4344
fn prepare_chat_completions(

src/client/claude.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ impl_client_trait!(
3636
),
3737
(noop_prepare_embeddings, noop_embeddings),
3838
(noop_prepare_rerank, noop_rerank),
39+
(noop_audio_transcriptions),
3940
);
4041

4142
fn prepare_chat_completions(

src/client/cohere.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ impl_client_trait!(
3636
),
3737
(prepare_embeddings, embeddings),
3838
(prepare_rerank, generic_rerank),
39+
(noop_audio_transcriptions),
3940
);
4041

4142
fn prepare_chat_completions(

src/client/common.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,21 @@ pub trait Client: Sync + Send {
148148
bail!("The client doesn't support rerank api")
149149
}
150150

151+
async fn audio_transcriptions(&self, data: TranscriptionData) -> Result<String> {
152+
let client = self.build_client()?;
153+
self.audio_transcriptions_inner(&client, data)
154+
.await
155+
.context("Failed to call audio-transcriptions api")
156+
}
157+
158+
async fn audio_transcriptions_inner(
159+
&self,
160+
_client: &ReqwestClient,
161+
_data: TranscriptionData,
162+
) -> Result<String> {
163+
bail!("The client doesn't support audio transcriptions api")
164+
}
165+
151166
fn request_builder(
152167
&self,
153168
client: &reqwest::Client,
@@ -343,6 +358,19 @@ pub struct RerankResult {
343358
pub relevance_score: f64,
344359
}
345360

361+
pub struct TranscriptionData {
362+
pub path: std::path::PathBuf,
363+
pub prompt: Option<String>,
364+
}
365+
366+
pub async fn noop_audio_transcriptions<T: Client>(
367+
_self: &T,
368+
_client: &ReqwestClient,
369+
_data: TranscriptionData,
370+
) -> Result<String> {
371+
bail!("The client doesn't support audio transcriptions api")
372+
}
373+
346374
pub type PromptAction<'a> = (&'a str, &'a str, Option<&'a str>);
347375

348376
pub async fn create_config(

src/client/gemini.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ impl_client_trait!(
3535
),
3636
(prepare_embeddings, embeddings),
3737
(noop_prepare_rerank, noop_rerank),
38+
(noop_audio_transcriptions),
3839
);
3940

4041
fn prepare_chat_completions(

src/client/macros.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ macro_rules! impl_client_trait {
173173
($prepare_chat_completions:path, $chat_completions:path, $chat_completions_streaming:path),
174174
($prepare_embeddings:path, $embeddings:path),
175175
($prepare_rerank:path, $rerank:path),
176+
($audio_transcriptions:path),
176177
) => {
177178
#[async_trait::async_trait]
178179
impl $crate::client::Client for $crate::client::$client {
@@ -218,6 +219,14 @@ macro_rules! impl_client_trait {
218219
let builder = self.request_builder(client, request_data);
219220
$rerank(builder, self.model()).await
220221
}
222+
223+
async fn audio_transcriptions_inner(
224+
&self,
225+
client: &reqwest::Client,
226+
data: $crate::client::TranscriptionData,
227+
) -> Result<String> {
228+
$audio_transcriptions(self, client, data).await
229+
}
221230
}
222231
};
223232
}

src/client/openai.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ impl_client_trait!(
3737
),
3838
(prepare_embeddings, openai_embeddings),
3939
(noop_prepare_rerank, noop_rerank),
40+
(openai_audio_transcriptions),
4041
);
4142

4243
fn prepare_chat_completions(
@@ -406,3 +407,90 @@ fn normalize_function_id(value: &str) -> Option<String> {
406407
Some(value.to_string())
407408
}
408409
}
410+
411+
pub async fn openai_audio_transcriptions(
412+
self_: &OpenAIClient,
413+
client: &reqwest::Client,
414+
data: TranscriptionData,
415+
) -> Result<String> {
416+
let api_key = self_.get_api_key()?;
417+
let api_base = self_
418+
.get_api_base()
419+
.unwrap_or_else(|_| API_BASE.to_string());
420+
let model_name = self_.model.real_name().to_string();
421+
openai_compatible_audio_transcriptions(client, &api_base, Some(&api_key), &model_name, data)
422+
.await
423+
}
424+
425+
pub async fn openai_compatible_audio_transcriptions(
426+
client: &reqwest::Client,
427+
api_base: &str,
428+
api_key: Option<&str>,
429+
model_name: &str,
430+
data: TranscriptionData,
431+
) -> Result<String> {
432+
let url = format!("{}/audio/transcriptions", api_base.trim_end_matches('/'));
433+
434+
let file_bytes = tokio::fs::read(&data.path)
435+
.await
436+
.with_context(|| format!("Failed to read audio file '{}'", data.path.display()))?;
437+
438+
let file_name = data
439+
.path
440+
.file_name()
441+
.and_then(|n| n.to_str())
442+
.unwrap_or("audio")
443+
.to_string();
444+
445+
let mime_type = audio_mime_type(&data.path);
446+
447+
let file_part = reqwest::multipart::Part::bytes(file_bytes)
448+
.file_name(file_name)
449+
.mime_str(&mime_type)
450+
.context("Invalid MIME type for audio file")?;
451+
452+
let mut form = reqwest::multipart::Form::new()
453+
.part("file", file_part)
454+
.text("model", model_name.to_string());
455+
456+
if let Some(prompt) = data.prompt {
457+
form = form.text("prompt", prompt);
458+
}
459+
460+
let mut request = client.post(&url).multipart(form);
461+
if let Some(key) = api_key {
462+
request = request.bearer_auth(key);
463+
}
464+
465+
let res = request.send().await?;
466+
let status = res.status();
467+
let body: serde_json::Value = res.json().await?;
468+
469+
if !status.is_success() {
470+
catch_error(&body, status.as_u16())?;
471+
}
472+
473+
body["text"]
474+
.as_str()
475+
.map(|s| s.to_string())
476+
.ok_or_else(|| anyhow::anyhow!("Invalid transcription response: {body}"))
477+
}
478+
479+
fn audio_mime_type(path: &std::path::Path) -> String {
480+
let ext = path
481+
.extension()
482+
.and_then(|e| e.to_str())
483+
.unwrap_or("")
484+
.to_lowercase();
485+
match ext.as_str() {
486+
"mp3" | "mpeg" | "mpga" => "audio/mpeg",
487+
"mp4" => "audio/mp4",
488+
"m4a" => "audio/m4a",
489+
"ogg" | "oga" => "audio/ogg",
490+
"wav" => "audio/wav",
491+
"webm" => "audio/webm",
492+
"flac" => "audio/flac",
493+
_ => "application/octet-stream",
494+
}
495+
.to_string()
496+
}

src/client/openai_compatible.rs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ impl_client_trait!(
3333
),
3434
(prepare_embeddings, openai_embeddings),
3535
(prepare_rerank, generic_rerank),
36+
(openai_compatible_client_audio_transcriptions),
3637
);
3738

3839
fn prepare_chat_completions(
@@ -141,6 +142,24 @@ pub struct GenericRerankResBody {
141142
pub results: RerankOutput,
142143
}
143144

145+
pub async fn openai_compatible_client_audio_transcriptions(
146+
self_: &OpenAICompatibleClient,
147+
client: &reqwest::Client,
148+
data: TranscriptionData,
149+
) -> Result<String> {
150+
let api_key = self_.get_api_key().ok();
151+
let api_base = get_api_base_ext(self_)?;
152+
let model_name = self_.model.real_name().to_string();
153+
openai_compatible_audio_transcriptions(
154+
client,
155+
&api_base,
156+
api_key.as_deref(),
157+
&model_name,
158+
data,
159+
)
160+
.await
161+
}
162+
144163
pub fn generic_build_rerank_body(data: &RerankData, model: &Model) -> Value {
145164
let RerankData {
146165
query,

src/repl/mod.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ use self::completer::ReplCompleter;
66
use self::highlighter::ReplHighlighter;
77
use self::prompt::ReplPrompt;
88

9-
use crate::client::{call_chat_completions, call_chat_completions_streaming};
9+
use crate::client::{call_chat_completions, call_chat_completions_streaming, init_client, TranscriptionData};
1010
use crate::config::{
1111
macro_execute, AgentVariables, AssertState, Config, GlobalConfig, Input, LastMessage,
1212
StateFlags,
1313
};
14+
use crate::function::ToolResult;
1415
use crate::render::render_error;
1516
use crate::run_command;
1617
use crate::utils::{
@@ -32,7 +33,7 @@ use std::{env, fs, process};
3233

3334
const MENU_NAME: &str = "completion_menu";
3435

35-
static REPL_COMMANDS: LazyLock<[ReplCommand; 37]> = LazyLock::new(|| {
36+
static REPL_COMMANDS: LazyLock<[ReplCommand; 38]> = LazyLock::new(|| {
3637
[
3738
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
3839
ReplCommand::new(".info", "Show system info", AssertState::pass()),
@@ -172,6 +173,11 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 37]> = LazyLock::new(|| {
172173
"Include files, directories, URLs or commands",
173174
AssertState::pass(),
174175
),
176+
ReplCommand::new(
177+
".transcript",
178+
"Transcribe an audio file",
179+
AssertState::pass(),
180+
),
175181
ReplCommand::new(
176182
".continue",
177183
"Continue previous response",
@@ -655,6 +661,37 @@ pub async fn run_repl_command(
655661
.file %% -- translate last reply to english"#
656662
),
657663
},
664+
".transcript" => match args {
665+
Some(args) => {
666+
let (files, text) = split_args_text(args, cfg!(windows));
667+
if files.is_empty() {
668+
println!("Usage: .transcript <audio-file> [-- <prompt>]");
669+
} else {
670+
let path = std::path::PathBuf::from(&files[0]);
671+
let prompt = if text.is_empty() { None } else { Some(text.to_string()) };
672+
let data = TranscriptionData { path, prompt };
673+
let model = config.read().current_model().clone();
674+
let client = init_client(config, Some(model))?;
675+
let transcript = abortable_run_with_spinner(
676+
client.audio_transcriptions(data),
677+
"Transcribing",
678+
abort_signal.clone(),
679+
)
680+
.await?;
681+
config.read().print_markdown(&transcript)?;
682+
let input = Input::from_str(config, &files[0], None);
683+
config
684+
.write()
685+
.after_chat_completion(&input, &transcript, &[] as &[ToolResult])?;
686+
}
687+
}
688+
None => println!(
689+
r#"Usage: .transcript <audio-file> [-- <prompt>]
690+
691+
.transcript meeting.mp3
692+
.transcript interview.wav -- Interview about Rust programming"#
693+
),
694+
},
658695
".continue" => {
659696
let LastMessage {
660697
mut input, output, ..

0 commit comments

Comments
 (0)