Skip to main content

ass_editor/commands/karaoke_commands/
generate.rs

1//! Generate karaoke timing tags for plain text with syllable detection.
2
3use 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/// Generate karaoke timing tags for text
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct GenerateKaraokeCommand {
18    /// Range of text to add karaoke to
19    pub range: Range,
20    /// Default syllable duration in centiseconds
21    pub default_duration: u32,
22    /// Type of karaoke tag to use (\k, \kf, \ko, \kt)
23    pub karaoke_type: KaraokeType,
24    /// Whether to automatically detect syllables
25    pub auto_detect_syllables: bool,
26}
27
28impl GenerateKaraokeCommand {
29    /// Create a new generate karaoke command
30    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    /// Set the karaoke type
40    #[must_use]
41    pub fn karaoke_type(mut self, karaoke_type: KaraokeType) -> Self {
42        self.karaoke_type = karaoke_type;
43        self
44    }
45
46    /// Disable automatic syllable detection
47    #[must_use]
48    pub fn manual_syllables(mut self) -> Self {
49        self.auto_detect_syllables = false;
50        self
51    }
52
53    /// Split text into syllables automatically
54    fn split_into_syllables(&self, text: &str) -> Vec<String> {
55        if !self.auto_detect_syllables {
56            return vec![text.to_string()];
57        }
58
59        // Simple syllable detection based on vowels and common patterns
60        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            // Split on spaces and common syllable boundaries
70            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                // Handle whitespace
79                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        // Add remaining text
97        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        // Return syllables or whole text if none found
105        if syllables.is_empty() {
106            vec![text.to_string()]
107        } else {
108            syllables
109        }
110    }
111
112    /// Check if position is a syllable boundary
113    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        // Split on vowel-consonant or consonant-vowel boundaries
122        let prev_vowel = "aeiouAEIOU".contains(prev);
123        let curr_vowel = "aeiouAEIOU".contains(curr);
124
125        // Simple heuristic: split when transitioning from vowel to consonant
126        // or when encountering certain consonant clusters
127        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        // Skip if text is already in override blocks
136        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                // First syllable gets the karaoke tag
149                karaoke_text.push_str(&format!("{{\\{tag}{}}}", self.default_duration));
150            } else if !syllable.trim().is_empty() {
151                // Subsequent syllables get their own timing
152                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}