ass_editor/commands/karaoke_commands/
generate.rs1use super::KaraokeType;
4use crate::commands::{CommandResult, EditorCommand};
5use crate::core::{EditorDocument, EditorError, Position, Range, Result};
6
7#[cfg(not(feature = "std"))]
8use alloc::{
9 format,
10 string::{String, ToString},
11 vec,
12 vec::Vec,
13};
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct GenerateKaraokeCommand {
18 pub range: Range,
20 pub default_duration: u32,
22 pub karaoke_type: KaraokeType,
24 pub auto_detect_syllables: bool,
26}
27
28impl GenerateKaraokeCommand {
29 pub fn new(range: Range, default_duration: u32) -> Self {
31 Self {
32 range,
33 default_duration,
34 karaoke_type: KaraokeType::Standard,
35 auto_detect_syllables: true,
36 }
37 }
38
39 #[must_use]
41 pub fn karaoke_type(mut self, karaoke_type: KaraokeType) -> Self {
42 self.karaoke_type = karaoke_type;
43 self
44 }
45
46 #[must_use]
48 pub fn manual_syllables(mut self) -> Self {
49 self.auto_detect_syllables = false;
50 self
51 }
52
53 fn split_into_syllables(&self, text: &str) -> Vec<String> {
55 if !self.auto_detect_syllables {
56 return vec![text.to_string()];
57 }
58
59 let mut syllables = Vec::new();
61 let mut current_start = 0;
62 let chars: Vec<char> = text.chars().collect();
63
64 if chars.is_empty() {
65 return vec![text.to_string()];
66 }
67
68 for (i, &ch) in chars.iter().enumerate() {
69 if ch.is_whitespace() || (i > 0 && self.is_syllable_boundary(&chars, i)) {
71 if current_start < i {
72 let syllable: String = chars[current_start..i].iter().collect();
73 if !syllable.trim().is_empty() {
74 syllables.push(syllable);
75 }
76 }
77
78 if ch.is_whitespace() {
80 let mut end = i + 1;
81 while end < chars.len() && chars[end].is_whitespace() {
82 end += 1;
83 }
84 if end > i + 1 {
85 let whitespace: String = chars[i..end].iter().collect();
86 syllables.push(whitespace);
87 current_start = end;
88 continue;
89 }
90 }
91
92 current_start = i;
93 }
94 }
95
96 if current_start < chars.len() {
98 let remaining: String = chars[current_start..].iter().collect();
99 if !remaining.trim().is_empty() {
100 syllables.push(remaining);
101 }
102 }
103
104 if syllables.is_empty() {
106 vec![text.to_string()]
107 } else {
108 syllables
109 }
110 }
111
112 fn is_syllable_boundary(&self, chars: &[char], pos: usize) -> bool {
114 if pos == 0 || pos >= chars.len() {
115 return false;
116 }
117
118 let prev = chars[pos - 1];
119 let curr = chars[pos];
120
121 let prev_vowel = "aeiouAEIOU".contains(prev);
123 let curr_vowel = "aeiouAEIOU".contains(curr);
124
125 prev_vowel && !curr_vowel && !curr.is_whitespace()
128 }
129}
130
131impl EditorCommand for GenerateKaraokeCommand {
132 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
133 let original_text = document.text_range(self.range)?;
134
135 if original_text.contains('{') || original_text.contains('}') {
137 return Err(EditorError::command_failed(
138 "Cannot generate karaoke for text containing override blocks",
139 ));
140 }
141
142 let syllables = self.split_into_syllables(&original_text);
143 let tag = self.karaoke_type.tag_string();
144
145 let mut karaoke_text = String::new();
146 for (i, syllable) in syllables.iter().enumerate() {
147 if i == 0 {
148 karaoke_text.push_str(&format!("{{\\{tag}{}}}", self.default_duration));
150 } else if !syllable.trim().is_empty() {
151 karaoke_text.push_str(&format!("{{\\{tag}{}}}", self.default_duration));
153 }
154 karaoke_text.push_str(syllable);
155 }
156
157 document.replace_raw(self.range, &karaoke_text)?;
158
159 let end_pos = Position::new(self.range.start.offset + karaoke_text.len());
160 let range = Range::new(self.range.start, end_pos);
161
162 Ok(CommandResult::success_with_change(range, end_pos))
163 }
164
165 fn description(&self) -> &str {
166 "Generate karaoke timing"
167 }
168
169 fn memory_usage(&self) -> usize {
170 core::mem::size_of::<Self>()
171 }
172}