Release 1.1: Pre-meeting models, Calendar improvements, Logs, Compact Recorder

This commit is contained in:
michael.borak
2026-01-20 17:15:14 +01:00
parent f61bcf1cc3
commit 79f509951c
16 changed files with 2011 additions and 321 deletions

View File

@@ -131,8 +131,15 @@ impl AudioProcessor {
while self.vad_buffer.len() >= self.vad_chunk_size {
let vad_chunk: Vec<f32> = self.vad_buffer.drain(0..self.vad_chunk_size).collect();
// Run Detection
let probability = self.vad.predict(vad_chunk);
let is_speech = probability > 0.5;
// Run Detection
let probability = self.vad.predict(vad_chunk.clone());
// Calculate RMS for this chunk to use as fallback/hybrid detection
let sq_sum: f32 = vad_chunk.iter().map(|x| x * x).sum();
let rms = (sq_sum / vad_chunk.len() as f32).sqrt();
// Hybrid VAD: Probability > 0.4 OR RMS > 0.005 (approx -46dB)
let is_speech = probability > 0.4 || rms > 0.005;
if is_speech {
self.is_speech_active = true;
@@ -141,8 +148,14 @@ impl AudioProcessor {
// Emit VAD event periodically (every 500ms)
if self.last_event_time.elapsed().as_millis() > 500 {
// Calculate simple RMS of the current chunk for debugging
let sq_sum: f32 = vad_chunk.iter().map(|x| x * x).sum();
let rms = (sq_sum / vad_chunk.len() as f32).sqrt();
// Print debug info to stdout (viewable in terminal)
println!("VAD Debug: Prob={:.4}, RMS={:.6}, Speech={}", probability, rms, is_speech);
if let Some(app) = &self.app_handle {
// Calculate crude RMS for visualization or just send probability
// Just sending probability is enough for now
#[derive(serde::Serialize, Clone)]
struct VadEvent {

96
src-tauri/src/email.rs Normal file
View File

@@ -0,0 +1,96 @@
use tauri::AppHandle;
use lettre::{Message, SmtpTransport, Transport, AsyncTransport};
use lettre::transport::smtp::authentication::Credentials;
use lettre::transport::smtp::AsyncSmtpTransport;
use lettre::Tokio1Executor;
#[derive(serde::Deserialize)]
pub struct SmtpConfig {
host: String,
port: u16,
username: String,
password: String,
sender_email: String,
sender_name: Option<String>,
}
#[derive(serde::Deserialize)]
pub struct EmailMessage {
to: Vec<String>,
subject: String,
body_html: String,
}
#[tauri::command]
pub async fn send_smtp_email(app: AppHandle, config: SmtpConfig, message: EmailMessage) -> Result<String, String> {
println!("SMTP: Preparing to send email to {:?}", message.to);
// 1. Build Message
let sender_str = if let Some(name) = &config.sender_name {
format!("{} <{}>", name, config.sender_email)
} else {
config.sender_email.clone()
};
let mut builder = Message::builder()
.from(sender_str.parse().map_err(|e: lettre::address::AddressError| e.to_string())?)
.subject(message.subject);
for recipient in message.to {
builder = builder.to(recipient.parse().map_err(|e: lettre::address::AddressError| e.to_string())?);
}
let email = builder
.header(lettre::message::header::ContentType::TEXT_HTML)
.body(message.body_html)
.map_err(|e| e.to_string())?;
// 2. Build Transport
println!("SMTP: Connecting to {}:{}...", config.host, config.port);
let creds = Credentials::new(config.username, config.password);
// TLS Configuration
// Proton Mail and others on 465 require Implicit TLS (Wrapper).
// Others on 587 use STARTTLS (Opportunistic).
use lettre::transport::smtp::client::{Tls, TlsParameters};
let tls_params = TlsParameters::builder(config.host.clone())
.build()
.map_err(|e| format!("TLS Params error: {}", e))?;
let builder = AsyncSmtpTransport::<Tokio1Executor>::relay(&config.host)
.map_err(|e| e.to_string())?
.port(config.port)
.credentials(creds);
let mailer: AsyncSmtpTransport<Tokio1Executor> = if config.port == 465 {
println!("SMTP: Using Implicit TLS (Wrapper) for port 465");
builder.tls(Tls::Wrapper(tls_params)).build()
} else {
println!("SMTP: Using Opportunistic TLS (STARTTLS) for port {}", config.port);
builder.tls(Tls::Opportunistic(tls_params)).build()
};
// 3. Send with Timeout
println!("SMTP: Sending...");
let send_task = mailer.send(email);
match tokio::time::timeout(std::time::Duration::from_secs(15), send_task).await {
Ok(result) => match result {
Ok(_) => {
println!("SMTP: Success!");
Ok("Email sent successfully".to_string())
},
Err(e) => {
println!("SMTP: Failed: {}", e);
// Hint at common issues
Err(format!("Failed to send: {}. (Check Host/Port/Password)", e))
}
},
Err(_) => {
println!("SMTP: Timeout");
Err("Connection timed out after 15s. Try changing Port (465 vs 587) or check VPN/Firewall.".to_string())
}
}
}

View File

@@ -8,6 +8,7 @@ use tokio::time::sleep;
mod audio_processor;
use audio_processor::AudioProcessor;
mod auth;
mod email;
// State to hold the active recording stream
struct AppState {
@@ -625,9 +626,26 @@ async fn save_text_file(app: AppHandle, path: String, content: String) -> Result
#[tauri::command]
async fn read_log_file(app: AppHandle) -> Result<String, String> {
let log_path = app.path().app_log_dir().map_err(|e| e.to_string())?.join("hearbit-ai.log");
if log_path.exists() {
let content = std::fs::read_to_string(&log_path).map_err(|e| e.to_string())?;
Ok(content)
} else {
Ok("No log file found yet.".to_string())
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_log::Builder::default()
.targets([
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::Stdout),
tauri_plugin_log::Target::new(tauri_plugin_log::TargetKind::LogDir { file_name: Some("hearbit-ai.log".to_string()) }),
])
.build())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
@@ -650,7 +668,9 @@ pub fn run() {
create_hearbit_audio_device,
auth::start_auth_flow,
auth::get_calendar_events,
save_text_file
save_text_file,
read_log_file,
email::send_smtp_email
])
.run(tauri::generate_context!())
.expect("error while running tauri application");