use crate::error::{Result, VoirsCliError};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::path::{Path, PathBuf};
const MAX_HISTORY_SIZE: usize = 1000;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionData {
pub metadata: SessionMetadata,
pub voice_settings: VoiceSettings,
pub history: Vec<SynthesisEntry>,
pub stats: SessionStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionMetadata {
pub created_at: DateTime<Utc>,
pub modified_at: DateTime<Utc>,
pub name: Option<String>,
pub description: Option<String>,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VoiceSettings {
pub current_voice: Option<String>,
pub speed: f32,
pub pitch: f32,
pub volume: f32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SynthesisEntry {
pub timestamp: DateTime<Utc>,
pub text: String,
pub voice: Option<String>,
pub parameters: VoiceSettings,
pub duration_ms: Option<u64>,
pub audio_file: Option<PathBuf>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionStats {
pub total_syntheses: usize,
pub total_characters: usize,
pub total_time_ms: u64,
pub voices_used: Vec<String>,
pub session_duration_ms: u64,
}
pub struct SessionManager {
session_data: SessionData,
auto_save: bool,
session_file: Option<PathBuf>,
history_buffer: VecDeque<SynthesisEntry>,
session_start: DateTime<Utc>,
}
impl SessionManager {
pub fn new(auto_save: bool) -> Self {
let now = Utc::now();
let session_data = SessionData {
metadata: SessionMetadata {
created_at: now,
modified_at: now,
name: None,
description: None,
version: env!("CARGO_PKG_VERSION").to_string(),
},
voice_settings: VoiceSettings {
current_voice: None,
speed: 1.0,
pitch: 0.0,
volume: 1.0,
},
history: Vec::new(),
stats: SessionStats {
total_syntheses: 0,
total_characters: 0,
total_time_ms: 0,
voices_used: Vec::new(),
session_duration_ms: 0,
},
};
Self {
session_data,
auto_save,
session_file: None,
history_buffer: VecDeque::new(),
session_start: now,
}
}
pub async fn load_session<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let path = path.as_ref();
let content = tokio::fs::read_to_string(path).await.map_err(|e| {
VoirsCliError::IoError(format!(
"Failed to read session file '{}': {}",
path.display(),
e
))
})?;
self.session_data = serde_json::from_str(&content).map_err(|e| {
VoirsCliError::SerializationError(format!("Failed to parse session file: {}", e))
})?;
self.history_buffer.clear();
for entry in &self.session_data.history {
self.history_buffer.push_back(entry.clone());
}
self.session_file = Some(path.to_path_buf());
println!("✓ Session loaded from: {}", path.display());
if let Some(ref name) = self.session_data.metadata.name {
println!(" Session name: {}", name);
}
println!(
" Created: {}",
self.session_data
.metadata
.created_at
.format("%Y-%m-%d %H:%M:%S UTC")
);
println!(" History entries: {}", self.session_data.history.len());
Ok(())
}
pub async fn save_session<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
let path = path.as_ref();
self.sync_session_data();
let content = serde_json::to_string_pretty(&self.session_data).map_err(|e| {
VoirsCliError::SerializationError(format!("Failed to serialize session: {}", e))
})?;
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|e| {
VoirsCliError::IoError(format!(
"Failed to create directory '{}': {}",
parent.display(),
e
))
})?;
}
tokio::fs::write(path, content).await.map_err(|e| {
VoirsCliError::IoError(format!(
"Failed to write session file '{}': {}",
path.display(),
e
))
})?;
self.session_file = Some(path.to_path_buf());
println!("✓ Session saved to: {}", path.display());
Ok(())
}
pub async fn auto_save(&mut self) -> Result<()> {
if self.auto_save {
if let Some(ref session_file) = self.session_file.clone() {
self.save_session(session_file).await?;
}
}
Ok(())
}
pub fn add_synthesis(&mut self, text: &str, voice: &Option<String>) {
let entry = SynthesisEntry {
timestamp: Utc::now(),
text: text.to_string(),
voice: voice.clone(),
parameters: self.session_data.voice_settings.clone(),
duration_ms: None,
audio_file: None,
};
self.history_buffer.push_back(entry.clone());
if self.history_buffer.len() > MAX_HISTORY_SIZE {
self.history_buffer.pop_front();
}
self.session_data.stats.total_syntheses += 1;
self.session_data.stats.total_characters += text.len();
if let Some(ref voice) = voice {
if !self.session_data.stats.voices_used.contains(voice) {
self.session_data.stats.voices_used.push(voice.clone());
}
}
self.session_data.metadata.modified_at = Utc::now();
if self.auto_save {
tokio::spawn(async move {
});
}
}
pub fn get_current_voice(&self) -> Option<&String> {
self.session_data.voice_settings.current_voice.as_ref()
}
pub fn set_current_voice(&mut self, voice: String) {
self.session_data.voice_settings.current_voice = Some(voice.clone());
if !self.session_data.stats.voices_used.contains(&voice) {
self.session_data.stats.voices_used.push(voice);
}
self.session_data.metadata.modified_at = Utc::now();
}
pub fn update_voice_settings(
&mut self,
speed: Option<f32>,
pitch: Option<f32>,
volume: Option<f32>,
) {
if let Some(s) = speed {
self.session_data.voice_settings.speed = s;
}
if let Some(p) = pitch {
self.session_data.voice_settings.pitch = p;
}
if let Some(v) = volume {
self.session_data.voice_settings.volume = v;
}
self.session_data.metadata.modified_at = Utc::now();
}
pub fn get_history(&self) -> Vec<&SynthesisEntry> {
self.history_buffer.iter().collect()
}
pub fn get_recent_history(&self, count: usize) -> Vec<&SynthesisEntry> {
self.history_buffer.iter().rev().take(count).collect()
}
pub fn clear_history(&mut self) {
self.history_buffer.clear();
self.session_data.history.clear();
self.session_data.stats.total_syntheses = 0;
self.session_data.stats.total_characters = 0;
self.session_data.stats.total_time_ms = 0;
self.session_data.metadata.modified_at = Utc::now();
}
pub fn get_stats(&self) -> &SessionStats {
&self.session_data.stats
}
pub fn set_metadata(&mut self, name: Option<String>, description: Option<String>) {
self.session_data.metadata.name = name;
self.session_data.metadata.description = description;
self.session_data.metadata.modified_at = Utc::now();
}
pub async fn export_history<P: AsRef<Path>>(
&self,
path: P,
format: ExportFormat,
) -> Result<()> {
let path = path.as_ref();
match format {
ExportFormat::Json => {
let content = serde_json::to_string_pretty(&self.session_data).map_err(|e| {
VoirsCliError::SerializationError(format!("Failed to serialize session: {}", e))
})?;
tokio::fs::write(path, content).await.map_err(|e| {
VoirsCliError::IoError(format!("Failed to write export file: {}", e))
})?;
}
ExportFormat::Csv => {
let mut csv_content = String::from("timestamp,text,voice,speed,pitch,volume\\n");
for entry in &self.history_buffer {
csv_content.push_str(&format!(
"{},{},{},{},{},{}\\n",
entry.timestamp.format("%Y-%m-%d %H:%M:%S UTC"),
entry.text.replace(',', ";").replace("\\n", " "),
entry.voice.as_deref().unwrap_or(""),
entry.parameters.speed,
entry.parameters.pitch,
entry.parameters.volume
));
}
tokio::fs::write(path, csv_content).await.map_err(|e| {
VoirsCliError::IoError(format!("Failed to write CSV file: {}", e))
})?;
}
ExportFormat::Text => {
let mut text_content = String::new();
for entry in &self.history_buffer {
text_content.push_str(&format!(
"[{}] {}: {}\\n",
entry.timestamp.format("%H:%M:%S"),
entry.voice.as_deref().unwrap_or("unknown"),
entry.text
));
}
tokio::fs::write(path, text_content).await.map_err(|e| {
VoirsCliError::IoError(format!("Failed to write text file: {}", e))
})?;
}
}
println!("✓ History exported to: {}", path.display());
Ok(())
}
fn sync_session_data(&mut self) {
self.session_data.history = self.history_buffer.iter().cloned().collect();
let session_duration = Utc::now() - self.session_start;
self.session_data.stats.session_duration_ms = session_duration.num_milliseconds() as u64;
self.session_data.metadata.modified_at = Utc::now();
}
}
#[derive(Debug, Clone)]
pub enum ExportFormat {
Json,
Csv,
Text,
}