voirs_cli/commands/interactive/
session.rs1use crate::error::{Result, VoirsCliError};
10use chrono::{DateTime, Utc};
11use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13use std::path::{Path, PathBuf};
14
15const MAX_HISTORY_SIZE: usize = 1000;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SessionData {
21 pub metadata: SessionMetadata,
23
24 pub voice_settings: VoiceSettings,
26
27 pub history: Vec<SynthesisEntry>,
29
30 pub stats: SessionStats,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SessionMetadata {
37 pub created_at: DateTime<Utc>,
39
40 pub modified_at: DateTime<Utc>,
42
43 pub name: Option<String>,
45
46 pub description: Option<String>,
48
49 pub version: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct VoiceSettings {
56 pub current_voice: Option<String>,
58
59 pub speed: f32,
61
62 pub pitch: f32,
64
65 pub volume: f32,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SynthesisEntry {
72 pub timestamp: DateTime<Utc>,
74
75 pub text: String,
77
78 pub voice: Option<String>,
80
81 pub parameters: VoiceSettings,
83
84 pub duration_ms: Option<u64>,
86
87 pub audio_file: Option<PathBuf>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct SessionStats {
94 pub total_syntheses: usize,
96
97 pub total_characters: usize,
99
100 pub total_time_ms: u64,
102
103 pub voices_used: Vec<String>,
105
106 pub session_duration_ms: u64,
108}
109
110pub struct SessionManager {
112 session_data: SessionData,
114
115 auto_save: bool,
117
118 session_file: Option<PathBuf>,
120
121 history_buffer: VecDeque<SynthesisEntry>,
123
124 session_start: DateTime<Utc>,
126}
127
128impl SessionManager {
129 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 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 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 pub async fn save_session<P: AsRef<Path>>(&mut self, path: P) -> Result<()> {
208 let path = path.as_ref();
209
210 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 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 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 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 self.history_buffer.push_back(entry.clone());
266
267 if self.history_buffer.len() > MAX_HISTORY_SIZE {
269 self.history_buffer.pop_front();
270 }
271
272 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 if self.auto_save {
286 tokio::spawn(async move {
289 });
291 }
292 }
293
294 pub fn get_current_voice(&self) -> Option<&String> {
296 self.session_data.voice_settings.current_voice.as_ref()
297 }
298
299 pub fn set_current_voice(&mut self, voice: String) {
301 self.session_data.voice_settings.current_voice = Some(voice.clone());
302
303 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 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 pub fn get_history(&self) -> Vec<&SynthesisEntry> {
333 self.history_buffer.iter().collect()
334 }
335
336 pub fn get_recent_history(&self, count: usize) -> Vec<&SynthesisEntry> {
338 self.history_buffer.iter().rev().take(count).collect()
339 }
340
341 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 pub fn get_stats(&self) -> &SessionStats {
353 &self.session_data.stats
354 }
355
356 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 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 fn sync_session_data(&mut self) {
424 self.session_data.history = self.history_buffer.iter().cloned().collect();
426
427 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#[derive(Debug, Clone)]
437pub enum ExportFormat {
438 Json,
439 Csv,
440 Text,
441}