Skip to main content

voirs_cli/commands/interactive/
session.rs

1//! Session management for interactive mode
2//!
3//! Handles:
4//! - Synthesis history tracking
5//! - Session state persistence
6//! - Export capabilities
7//! - Auto-save functionality
8
9use crate::error::{Result, VoirsCliError};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13use std::path::{Path, PathBuf};
14
15/// Maximum number of synthesis entries to keep in history
16const MAX_HISTORY_SIZE: usize = 1000;
17
18/// Session data structure
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SessionData {
21    /// Session metadata
22    pub metadata: SessionMetadata,
23
24    /// Current voice settings
25    pub voice_settings: VoiceSettings,
26
27    /// Synthesis history
28    pub history: Vec<SynthesisEntry>,
29
30    /// Session statistics
31    pub stats: SessionStats,
32}
33
34/// Session metadata
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SessionMetadata {
37    /// Session creation time
38    pub created_at: DateTime<Utc>,
39
40    /// Last modified time
41    pub modified_at: DateTime<Utc>,
42
43    /// Session name
44    pub name: Option<String>,
45
46    /// Session description
47    pub description: Option<String>,
48
49    /// Session version for compatibility
50    pub version: String,
51}
52
53/// Voice and synthesis settings
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct VoiceSettings {
56    /// Current voice
57    pub current_voice: Option<String>,
58
59    /// Speed setting
60    pub speed: f32,
61
62    /// Pitch setting
63    pub pitch: f32,
64
65    /// Volume setting
66    pub volume: f32,
67}
68
69/// Individual synthesis entry
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SynthesisEntry {
72    /// Timestamp of synthesis
73    pub timestamp: DateTime<Utc>,
74
75    /// Text that was synthesized
76    pub text: String,
77
78    /// Voice used for synthesis
79    pub voice: Option<String>,
80
81    /// Synthesis parameters at time of synthesis
82    pub parameters: VoiceSettings,
83
84    /// Duration of synthesis (if available)
85    pub duration_ms: Option<u64>,
86
87    /// Audio file path (if saved)
88    pub audio_file: Option<PathBuf>,
89}
90
91/// Session statistics
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SessionStats {
94    /// Total number of synthesis operations
95    pub total_syntheses: usize,
96
97    /// Total characters synthesized
98    pub total_characters: usize,
99
100    /// Total synthesis time (if tracked)
101    pub total_time_ms: u64,
102
103    /// Voices used in this session
104    pub voices_used: Vec<String>,
105
106    /// Session duration
107    pub session_duration_ms: u64,
108}
109
110/// Session manager
111pub struct SessionManager {
112    /// Current session data
113    session_data: SessionData,
114
115    /// Auto-save enabled
116    auto_save: bool,
117
118    /// Current session file path
119    session_file: Option<PathBuf>,
120
121    /// History buffer for efficient operations
122    history_buffer: VecDeque<SynthesisEntry>,
123
124    /// Session start time
125    session_start: DateTime<Utc>,
126}
127
128impl SessionManager {
129    /// Create a new session manager
130    pub fn new(auto_save: bool) -> Self {
131        let now = Utc::now();
132
133        let session_data = SessionData {
134            metadata: SessionMetadata {
135                created_at: now,
136                modified_at: now,
137                name: None,
138                description: None,
139                version: env!("CARGO_PKG_VERSION").to_string(),
140            },
141            voice_settings: VoiceSettings {
142                current_voice: None,
143                speed: 1.0,
144                pitch: 0.0,
145                volume: 1.0,
146            },
147            history: Vec::new(),
148            stats: SessionStats {
149                total_syntheses: 0,
150                total_characters: 0,
151                total_time_ms: 0,
152                voices_used: Vec::new(),
153                session_duration_ms: 0,
154            },
155        };
156
157        Self {
158            session_data,
159            auto_save,
160            session_file: None,
161            history_buffer: VecDeque::new(),
162            session_start: now,
163        }
164    }
165
166    /// Load session from file
167    pub async fn load_session<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
168        let path = path.as_ref();
169
170        let content = tokio::fs::read_to_string(path).await.map_err(|e| {
171            VoirsCliError::IoError(format!(
172                "Failed to read session file '{}': {}",
173                path.display(),
174                e
175            ))
176        })?;
177
178        self.session_data = serde_json::from_str(&content).map_err(|e| {
179            VoirsCliError::SerializationError(format!("Failed to parse session file: {}", e))
180        })?;
181
182        // Rebuild history buffer
183        self.history_buffer.clear();
184        for entry in &self.session_data.history {
185            self.history_buffer.push_back(entry.clone());
186        }
187
188        self.session_file = Some(path.to_path_buf());
189
190        println!("✓ Session loaded from: {}", path.display());
191        if let Some(ref name) = self.session_data.metadata.name {
192            println!("  Session name: {}", name);
193        }
194        println!(
195            "  Created: {}",
196            self.session_data
197                .metadata
198                .created_at
199                .format("%Y-%m-%d %H:%M:%S UTC")
200        );
201        println!("  History entries: {}", self.session_data.history.len());
202
203        Ok(())
204    }
205
206    /// Save session to file
207    pub async fn save_session<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
208        let path = path.as_ref();
209
210        // Update session data before saving
211        self.sync_session_data();
212
213        let content = serde_json::to_string_pretty(&self.session_data).map_err(|e| {
214            VoirsCliError::SerializationError(format!("Failed to serialize session: {}", e))
215        })?;
216
217        // Ensure parent directory exists
218        if let Some(parent) = path.parent() {
219            tokio::fs::create_dir_all(parent).await.map_err(|e| {
220                VoirsCliError::IoError(format!(
221                    "Failed to create directory '{}': {}",
222                    parent.display(),
223                    e
224                ))
225            })?;
226        }
227
228        tokio::fs::write(path, content).await.map_err(|e| {
229            VoirsCliError::IoError(format!(
230                "Failed to write session file '{}': {}",
231                path.display(),
232                e
233            ))
234        })?;
235
236        self.session_file = Some(path.to_path_buf());
237
238        println!("✓ Session saved to: {}", path.display());
239
240        Ok(())
241    }
242
243    /// Auto-save session if enabled
244    pub async fn auto_save(&mut self) -> Result<()> {
245        if self.auto_save {
246            if let Some(ref session_file) = self.session_file.clone() {
247                self.save_session(session_file).await?;
248            }
249        }
250        Ok(())
251    }
252
253    /// Add a synthesis entry to the session
254    pub fn add_synthesis(&mut self, text: &str, voice: &Option<String>) {
255        let entry = SynthesisEntry {
256            timestamp: Utc::now(),
257            text: text.to_string(),
258            voice: voice.clone(),
259            parameters: self.session_data.voice_settings.clone(),
260            duration_ms: None,
261            audio_file: None,
262        };
263
264        // Add to buffer
265        self.history_buffer.push_back(entry.clone());
266
267        // Limit buffer size
268        if self.history_buffer.len() > MAX_HISTORY_SIZE {
269            self.history_buffer.pop_front();
270        }
271
272        // Update statistics
273        self.session_data.stats.total_syntheses += 1;
274        self.session_data.stats.total_characters += text.len();
275
276        if let Some(ref voice) = voice {
277            if !self.session_data.stats.voices_used.contains(voice) {
278                self.session_data.stats.voices_used.push(voice.clone());
279            }
280        }
281
282        self.session_data.metadata.modified_at = Utc::now();
283
284        // Auto-save if enabled
285        if self.auto_save {
286            // Note: In a real implementation, we'd want to do this asynchronously
287            // without blocking the current operation
288            tokio::spawn(async move {
289                // Auto-save logic would go here
290            });
291        }
292    }
293
294    /// Get current voice
295    pub fn get_current_voice(&self) -> Option<&String> {
296        self.session_data.voice_settings.current_voice.as_ref()
297    }
298
299    /// Set current voice
300    pub fn set_current_voice(&mut self, voice: String) {
301        self.session_data.voice_settings.current_voice = Some(voice.clone());
302
303        // Add to used voices if not already present
304        if !self.session_data.stats.voices_used.contains(&voice) {
305            self.session_data.stats.voices_used.push(voice);
306        }
307
308        self.session_data.metadata.modified_at = Utc::now();
309    }
310
311    /// Update voice settings
312    pub fn update_voice_settings(
313        &mut self,
314        speed: Option<f32>,
315        pitch: Option<f32>,
316        volume: Option<f32>,
317    ) {
318        if let Some(s) = speed {
319            self.session_data.voice_settings.speed = s;
320        }
321        if let Some(p) = pitch {
322            self.session_data.voice_settings.pitch = p;
323        }
324        if let Some(v) = volume {
325            self.session_data.voice_settings.volume = v;
326        }
327
328        self.session_data.metadata.modified_at = Utc::now();
329    }
330
331    /// Get synthesis history
332    pub fn get_history(&self) -> Vec<&SynthesisEntry> {
333        self.history_buffer.iter().collect()
334    }
335
336    /// Get recent history (last N entries)
337    pub fn get_recent_history(&self, count: usize) -> Vec<&SynthesisEntry> {
338        self.history_buffer.iter().rev().take(count).collect()
339    }
340
341    /// Clear session history
342    pub fn clear_history(&mut self) {
343        self.history_buffer.clear();
344        self.session_data.history.clear();
345        self.session_data.stats.total_syntheses = 0;
346        self.session_data.stats.total_characters = 0;
347        self.session_data.stats.total_time_ms = 0;
348        self.session_data.metadata.modified_at = Utc::now();
349    }
350
351    /// Get session statistics
352    pub fn get_stats(&self) -> &SessionStats {
353        &self.session_data.stats
354    }
355
356    /// Set session metadata
357    pub fn set_metadata(&mut self, name: Option<String>, description: Option<String>) {
358        self.session_data.metadata.name = name;
359        self.session_data.metadata.description = description;
360        self.session_data.metadata.modified_at = Utc::now();
361    }
362
363    /// Export session history to various formats
364    pub async fn export_history<P: AsRef<Path>>(
365        &self,
366        path: P,
367        format: ExportFormat,
368    ) -> Result<()> {
369        let path = path.as_ref();
370
371        match format {
372            ExportFormat::Json => {
373                let content = serde_json::to_string_pretty(&self.session_data).map_err(|e| {
374                    VoirsCliError::SerializationError(format!("Failed to serialize session: {}", e))
375                })?;
376
377                tokio::fs::write(path, content).await.map_err(|e| {
378                    VoirsCliError::IoError(format!("Failed to write export file: {}", e))
379                })?;
380            }
381            ExportFormat::Csv => {
382                let mut csv_content = String::from("timestamp,text,voice,speed,pitch,volume\\n");
383
384                for entry in &self.history_buffer {
385                    csv_content.push_str(&format!(
386                        "{},{},{},{},{},{}\\n",
387                        entry.timestamp.format("%Y-%m-%d %H:%M:%S UTC"),
388                        entry.text.replace(',', ";").replace("\\n", " "),
389                        entry.voice.as_deref().unwrap_or(""),
390                        entry.parameters.speed,
391                        entry.parameters.pitch,
392                        entry.parameters.volume
393                    ));
394                }
395
396                tokio::fs::write(path, csv_content).await.map_err(|e| {
397                    VoirsCliError::IoError(format!("Failed to write CSV file: {}", e))
398                })?;
399            }
400            ExportFormat::Text => {
401                let mut text_content = String::new();
402
403                for entry in &self.history_buffer {
404                    text_content.push_str(&format!(
405                        "[{}] {}: {}\\n",
406                        entry.timestamp.format("%H:%M:%S"),
407                        entry.voice.as_deref().unwrap_or("unknown"),
408                        entry.text
409                    ));
410                }
411
412                tokio::fs::write(path, text_content).await.map_err(|e| {
413                    VoirsCliError::IoError(format!("Failed to write text file: {}", e))
414                })?;
415            }
416        }
417
418        println!("✓ History exported to: {}", path.display());
419        Ok(())
420    }
421
422    /// Sync session data with current state
423    fn sync_session_data(&mut self) {
424        // Update history from buffer
425        self.session_data.history = self.history_buffer.iter().cloned().collect();
426
427        // Update session duration
428        let session_duration = Utc::now() - self.session_start;
429        self.session_data.stats.session_duration_ms = session_duration.num_milliseconds() as u64;
430
431        self.session_data.metadata.modified_at = Utc::now();
432    }
433}
434
435/// Export format options
436#[derive(Debug, Clone)]
437pub enum ExportFormat {
438    Json,
439    Csv,
440    Text,
441}