feat: v1.1.0 - Long meeting support, email in history, custom logo
- Add MP3 conversion and chunking for files >18MB - Support meetings up to 2+ hours - Add email button directly in history view - Implement custom logo upload in Settings for white-labeling - Add read_image_as_base64 Rust command - Update README with new features and ffmpeg requirement
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -1741,6 +1741,7 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||
name = "hearbit-ai"
|
||||
version = "0.1.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"cpal",
|
||||
"hound",
|
||||
|
||||
@@ -37,3 +37,4 @@ url = "2.5"
|
||||
lettre = { version = "0.11", features = ["tokio1", "tokio1-native-tls", "builder"] }
|
||||
tauri-plugin-log = "2.0.0"
|
||||
tauri-plugin-shell = "2.3.4"
|
||||
base64 = "0.22"
|
||||
|
||||
@@ -9,6 +9,7 @@ use std::process::Command;
|
||||
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
use base64::Engine;
|
||||
|
||||
mod audio_processor;
|
||||
use audio_processor::AudioProcessor;
|
||||
@@ -627,6 +628,118 @@ fn get_audio_metadata(app: AppHandle, file_path: String) -> Result<AudioMetadata
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn convert_to_mp3(app: AppHandle, wav_path: String) -> Result<String, String> {
|
||||
emit_log(&app, "INFO", &format!("Converting to MP3: {}", wav_path));
|
||||
|
||||
let mp3_path = wav_path.replace(".wav", ".mp3");
|
||||
|
||||
let output = Command::new("ffmpeg")
|
||||
.args([
|
||||
"-i", &wav_path,
|
||||
"-codec:a", "libmp3lame",
|
||||
"-b:a", "64k",
|
||||
"-y", // overwrite
|
||||
&mp3_path
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to execute ffmpeg: {}", e))?;
|
||||
|
||||
if output.status.success() {
|
||||
emit_log(&app, "SUCCESS", &format!("MP3 created: {}", mp3_path));
|
||||
Ok(mp3_path)
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
emit_log(&app, "ERROR", &format!("MP3 conversion failed: {}", error));
|
||||
Err(format!("MP3 conversion failed: {}", error))
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn chunk_audio(app: AppHandle, file_path: String, chunk_minutes: u32) -> Result<Vec<String>, String> {
|
||||
emit_log(&app, "INFO", &format!("Chunking audio: {} ({}min chunks)", file_path, chunk_minutes));
|
||||
|
||||
let chunk_seconds = chunk_minutes * 60;
|
||||
|
||||
// Get total duration using ffprobe
|
||||
let duration_output = Command::new("ffprobe")
|
||||
.args([
|
||||
"-v", "error",
|
||||
"-show_entries", "format=duration",
|
||||
"-of", "default=noprint_wrappers=1:nokey=1",
|
||||
&file_path
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to get duration: {}", e))?;
|
||||
|
||||
let duration_str = String::from_utf8_lossy(&duration_output.stdout);
|
||||
let duration: f64 = duration_str.trim().parse()
|
||||
.map_err(|_| "Failed to parse duration".to_string())?;
|
||||
|
||||
let num_chunks = (duration / chunk_seconds as f64).ceil() as usize;
|
||||
emit_log(&app, "INFO", &format!("Total duration: {}s, creating {} chunks", duration, num_chunks));
|
||||
|
||||
let mut chunk_paths = Vec::new();
|
||||
let base_path = file_path.replace(".mp3", "");
|
||||
|
||||
for i in 0..num_chunks {
|
||||
let start_time = i as u32 * chunk_seconds;
|
||||
let chunk_path = format!("{}_chunk_{}.mp3", base_path, i);
|
||||
|
||||
let output = Command::new("ffmpeg")
|
||||
.args([
|
||||
"-i", &file_path,
|
||||
"-ss", &start_time.to_string(),
|
||||
"-t", &chunk_seconds.to_string(),
|
||||
"-c", "copy",
|
||||
"-y",
|
||||
&chunk_path
|
||||
])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to create chunk {}: {}", i, e))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let error = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(format!("Chunk {} failed: {}", i, error));
|
||||
}
|
||||
|
||||
chunk_paths.push(chunk_path);
|
||||
}
|
||||
|
||||
emit_log(&app, "SUCCESS", &format!("Created {} chunks", chunk_paths.len()));
|
||||
Ok(chunk_paths)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn read_image_as_base64(app: AppHandle, file_path: String) -> Result<String, String> {
|
||||
emit_log(&app, "INFO", &format!("Reading image as base64: {}", file_path));
|
||||
|
||||
let bytes = std::fs::read(&file_path)
|
||||
.map_err(|e| format!("Failed to read file: {}", e))?;
|
||||
|
||||
// Detect image type from extension
|
||||
let extension = std::path::Path::new(&file_path)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("png")
|
||||
.to_lowercase();
|
||||
|
||||
let mime_type = match extension.as_str() {
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"png" => "image/png",
|
||||
"svg" => "image/svg+xml",
|
||||
"gif" => "image/gif",
|
||||
_ => "image/png"
|
||||
};
|
||||
|
||||
// Use base64 encoding
|
||||
let base64_str = base64::prelude::BASE64_STANDARD.encode(&bytes);
|
||||
let data_url = format!("data:{};base64,{}", mime_type, base64_str);
|
||||
|
||||
emit_log(&app, "SUCCESS", &format!("Image converted to base64 ({} bytes)", base64_str.len()));
|
||||
Ok(data_url)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn open_audio_midi_setup() -> Result<(), String> {
|
||||
Command::new("open")
|
||||
@@ -781,6 +894,9 @@ pub fn run() {
|
||||
save_text_file,
|
||||
read_log_file,
|
||||
get_audio_metadata,
|
||||
convert_to_mp3,
|
||||
chunk_audio,
|
||||
read_image_as_base64,
|
||||
email::send_smtp_email
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
|
||||
Reference in New Issue
Block a user