1use super::{CommandResult, EditorCommand};
8use crate::core::{EditorDocument, EditorError, Position, Range, Result};
9
10#[cfg(not(feature = "std"))]
11use alloc::{
12 format,
13 string::{String, ToString},
14 vec,
15 vec::Vec,
16};
17
18#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct GenerateKaraokeCommand {
21 pub range: Range,
23 pub default_duration: u32,
25 pub karaoke_type: KaraokeType,
27 pub auto_detect_syllables: bool,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum KaraokeType {
34 Standard,
36 Fill,
38 Outline,
40 Transition,
42}
43
44impl KaraokeType {
45 pub fn tag_string(self) -> &'static str {
47 match self {
48 KaraokeType::Standard => "k",
49 KaraokeType::Fill => "kf",
50 KaraokeType::Outline => "ko",
51 KaraokeType::Transition => "kt",
52 }
53 }
54}
55
56impl GenerateKaraokeCommand {
57 pub fn new(range: Range, default_duration: u32) -> Self {
59 Self {
60 range,
61 default_duration,
62 karaoke_type: KaraokeType::Standard,
63 auto_detect_syllables: true,
64 }
65 }
66
67 #[must_use]
69 pub fn karaoke_type(mut self, karaoke_type: KaraokeType) -> Self {
70 self.karaoke_type = karaoke_type;
71 self
72 }
73
74 #[must_use]
76 pub fn manual_syllables(mut self) -> Self {
77 self.auto_detect_syllables = false;
78 self
79 }
80
81 fn split_into_syllables(&self, text: &str) -> Vec<String> {
83 if !self.auto_detect_syllables {
84 return vec![text.to_string()];
85 }
86
87 let mut syllables = Vec::new();
89 let mut current_start = 0;
90 let chars: Vec<char> = text.chars().collect();
91
92 if chars.is_empty() {
93 return vec![text.to_string()];
94 }
95
96 for (i, &ch) in chars.iter().enumerate() {
97 if ch.is_whitespace() || (i > 0 && self.is_syllable_boundary(&chars, i)) {
99 if current_start < i {
100 let syllable: String = chars[current_start..i].iter().collect();
101 if !syllable.trim().is_empty() {
102 syllables.push(syllable);
103 }
104 }
105
106 if ch.is_whitespace() {
108 let mut end = i + 1;
109 while end < chars.len() && chars[end].is_whitespace() {
110 end += 1;
111 }
112 if end > i + 1 {
113 let whitespace: String = chars[i..end].iter().collect();
114 syllables.push(whitespace);
115 current_start = end;
116 continue;
117 }
118 }
119
120 current_start = i;
121 }
122 }
123
124 if current_start < chars.len() {
126 let remaining: String = chars[current_start..].iter().collect();
127 if !remaining.trim().is_empty() {
128 syllables.push(remaining);
129 }
130 }
131
132 if syllables.is_empty() {
134 vec![text.to_string()]
135 } else {
136 syllables
137 }
138 }
139
140 fn is_syllable_boundary(&self, chars: &[char], pos: usize) -> bool {
142 if pos == 0 || pos >= chars.len() {
143 return false;
144 }
145
146 let prev = chars[pos - 1];
147 let curr = chars[pos];
148
149 let prev_vowel = "aeiouAEIOU".contains(prev);
151 let curr_vowel = "aeiouAEIOU".contains(curr);
152
153 prev_vowel && !curr_vowel && !curr.is_whitespace()
156 }
157}
158
159impl EditorCommand for GenerateKaraokeCommand {
160 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
161 let original_text = document.text_range(self.range)?;
162
163 if original_text.contains('{') || original_text.contains('}') {
165 return Err(EditorError::command_failed(
166 "Cannot generate karaoke for text containing override blocks",
167 ));
168 }
169
170 let syllables = self.split_into_syllables(&original_text);
171 let tag = self.karaoke_type.tag_string();
172
173 let mut karaoke_text = String::new();
174 for (i, syllable) in syllables.iter().enumerate() {
175 if i == 0 {
176 karaoke_text.push_str(&format!("{{\\{tag}{}}}", self.default_duration));
178 } else if !syllable.trim().is_empty() {
179 karaoke_text.push_str(&format!("{{\\{tag}{}}}", self.default_duration));
181 }
182 karaoke_text.push_str(syllable);
183 }
184
185 document.replace_raw(self.range, &karaoke_text)?;
186
187 let end_pos = Position::new(self.range.start.offset + karaoke_text.len());
188 let range = Range::new(self.range.start, end_pos);
189
190 Ok(CommandResult::success_with_change(range, end_pos))
191 }
192
193 fn description(&self) -> &str {
194 "Generate karaoke timing"
195 }
196
197 fn memory_usage(&self) -> usize {
198 core::mem::size_of::<Self>()
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct SplitKaraokeCommand {
205 pub range: Range,
207 pub split_positions: Vec<usize>,
209 pub new_duration: Option<u32>,
211}
212
213impl SplitKaraokeCommand {
214 pub fn new(range: Range, split_positions: Vec<usize>) -> Self {
216 Self {
217 range,
218 split_positions,
219 new_duration: None,
220 }
221 }
222
223 #[must_use]
225 pub fn duration(mut self, duration: u32) -> Self {
226 self.new_duration = Some(duration);
227 self
228 }
229}
230
231impl EditorCommand for SplitKaraokeCommand {
232 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
233 let original_text = document.text_range(self.range)?;
234 let processed_text = self.split_karaoke_text(&original_text)?;
235
236 document.replace_raw(self.range, &processed_text)?;
237
238 let end_pos = Position::new(self.range.start.offset + processed_text.len());
239 let range = Range::new(self.range.start, end_pos);
240
241 Ok(CommandResult::success_with_change(range, end_pos))
242 }
243
244 fn description(&self) -> &str {
245 "Split karaoke timing"
246 }
247
248 fn memory_usage(&self) -> usize {
249 core::mem::size_of::<Self>() + self.split_positions.len() * core::mem::size_of::<usize>()
250 }
251}
252
253impl SplitKaraokeCommand {
254 fn split_karaoke_text(&self, text: &str) -> Result<String> {
256 let mut result = String::new();
259 let mut last_pos = 0;
260
261 for &pos in &self.split_positions {
262 if pos <= last_pos || pos >= text.len() {
263 continue;
264 }
265
266 let segment = &text[last_pos..pos];
267 if !segment.is_empty() {
268 let duration = self.new_duration.unwrap_or(50);
269 result.push_str(&format!("{{\\k{duration}}}{segment}"));
270 }
271 last_pos = pos;
272 }
273
274 if last_pos < text.len() {
276 let segment = &text[last_pos..];
277 if !segment.is_empty() {
278 let duration = self.new_duration.unwrap_or(50);
279 result.push_str(&format!("{{\\k{duration}}}{segment}"));
280 }
281 }
282
283 Ok(result)
284 }
285}
286
287#[derive(Debug, Clone, PartialEq)]
289pub struct AdjustKaraokeCommand {
290 pub range: Range,
292 pub adjustment: TimingAdjustment,
294}
295
296#[derive(Debug, Clone, PartialEq)]
298pub enum TimingAdjustment {
299 Scale(f32),
301 Offset(i32),
303 SetAll(u32),
305 Custom(Vec<u32>),
307}
308
309impl AdjustKaraokeCommand {
310 pub fn scale(range: Range, factor: f32) -> Self {
312 Self {
313 range,
314 adjustment: TimingAdjustment::Scale(factor),
315 }
316 }
317
318 pub fn offset(range: Range, offset: i32) -> Self {
320 Self {
321 range,
322 adjustment: TimingAdjustment::Offset(offset),
323 }
324 }
325
326 pub fn set_all(range: Range, duration: u32) -> Self {
328 Self {
329 range,
330 adjustment: TimingAdjustment::SetAll(duration),
331 }
332 }
333
334 pub fn custom(range: Range, timings: Vec<u32>) -> Self {
336 Self {
337 range,
338 adjustment: TimingAdjustment::Custom(timings),
339 }
340 }
341}
342
343impl EditorCommand for AdjustKaraokeCommand {
344 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
345 let original_text = document.text_range(self.range)?;
346 let adjusted_text = self.adjust_karaoke_timing(&original_text)?;
347
348 document.replace_raw(self.range, &adjusted_text)?;
349
350 let end_pos = Position::new(self.range.start.offset + adjusted_text.len());
351 let range = Range::new(self.range.start, end_pos);
352
353 Ok(CommandResult::success_with_change(range, end_pos))
354 }
355
356 fn description(&self) -> &str {
357 match self.adjustment {
358 TimingAdjustment::Scale(_) => "Scale karaoke timing",
359 TimingAdjustment::Offset(_) => "Offset karaoke timing",
360 TimingAdjustment::SetAll(_) => "Set karaoke timing",
361 TimingAdjustment::Custom(_) => "Apply custom karaoke timing",
362 }
363 }
364
365 fn memory_usage(&self) -> usize {
366 let adjustment_size = match &self.adjustment {
367 TimingAdjustment::Custom(vec) => vec.len() * core::mem::size_of::<u32>(),
368 _ => 0,
369 };
370 core::mem::size_of::<Self>() + adjustment_size
371 }
372}
373
374impl AdjustKaraokeCommand {
375 fn adjust_karaoke_timing(&self, text: &str) -> Result<String> {
377 use ass_core::analysis::events::tags::parse_override_block_with_registry;
378 use ass_core::plugin::{tags::karaoke::create_karaoke_handlers, ExtensionRegistry};
379
380 let mut registry = ExtensionRegistry::new();
382 for handler in create_karaoke_handlers() {
383 registry.register_tag_handler(handler).map_err(|e| {
384 crate::core::errors::EditorError::ValidationError {
385 message: format!("Failed to register karaoke handler: {e:?}"),
386 }
387 })?;
388 }
389
390 let mut result = String::new();
391 let mut chars = text.chars().peekable();
392 let mut custom_index = 0;
393
394 while let Some(ch) = chars.next() {
395 if ch == '{' {
396 let mut override_content = String::new();
398 let mut brace_count = 1;
399
400 for inner_ch in chars.by_ref() {
401 if inner_ch == '{' {
402 brace_count += 1;
403 } else if inner_ch == '}' {
404 brace_count -= 1;
405 if brace_count == 0 {
406 break;
407 }
408 }
409 override_content.push(inner_ch);
410 }
411
412 let mut tags = Vec::new();
414 let mut diagnostics = Vec::new();
415 parse_override_block_with_registry(
416 &override_content,
417 0,
418 &mut tags,
419 &mut diagnostics,
420 Some(®istry),
421 );
422
423 let processed_content = self.adjust_karaoke_tags_with_registry(
425 &override_content,
426 &tags,
427 &mut custom_index,
428 )?;
429
430 result.push('{');
431 result.push_str(&processed_content);
432 result.push('}');
433 } else {
434 result.push(ch);
435 }
436 }
437
438 Ok(result)
439 }
440
441 fn adjust_karaoke_tags_with_registry(
443 &self,
444 original_content: &str,
445 tags: &[ass_core::analysis::events::tags::OverrideTag],
446 custom_index: &mut usize,
447 ) -> Result<String> {
448 let mut result = original_content.to_string();
449
450 for tag in tags.iter().rev() {
452 if tag.name().starts_with('k') {
453 let tag_name = tag.name();
455 let args = tag.args();
456
457 let current_duration: u32 = args.trim().parse().unwrap_or(0);
459
460 let new_duration = match &self.adjustment {
462 TimingAdjustment::Scale(factor) => {
463 ((current_duration as f32 * factor) as u32).max(1)
464 }
465 TimingAdjustment::Offset(offset) => {
466 ((current_duration as i32 + offset).max(1)) as u32
467 }
468 TimingAdjustment::SetAll(duration) => *duration,
469 TimingAdjustment::Custom(timings) => {
470 if *custom_index < timings.len() {
471 let timing = timings[*custom_index];
472 *custom_index += 1;
473 timing
474 } else {
475 current_duration
476 }
477 }
478 };
479
480 let old_tag = format!("\\{tag_name}{current_duration}");
482 let new_tag = format!("\\{tag_name}{new_duration}");
483 result = result.replace(&old_tag, &new_tag);
484 }
485 }
486
487 Ok(result)
488 }
489}
490
491#[derive(Debug, Clone, PartialEq)]
493pub struct ApplyKaraokeCommand {
494 pub event_range: Range,
496 pub karaoke_template: KaraokeTemplate,
498}
499
500#[derive(Debug, Clone, PartialEq)]
502pub enum KaraokeTemplate {
503 Equal {
505 syllable_duration: u32,
506 karaoke_type: KaraokeType,
507 },
508 Beat {
510 beats_per_minute: u32,
511 beats_per_syllable: f32,
512 karaoke_type: KaraokeType,
513 },
514 Pattern {
516 durations: Vec<u32>,
517 karaoke_type: KaraokeType,
518 },
519 ImportFrom { source_event_index: usize },
521}
522
523impl ApplyKaraokeCommand {
524 pub fn equal(event_range: Range, duration: u32, karaoke_type: KaraokeType) -> Self {
526 Self {
527 event_range,
528 karaoke_template: KaraokeTemplate::Equal {
529 syllable_duration: duration,
530 karaoke_type,
531 },
532 }
533 }
534
535 pub fn beat(
537 event_range: Range,
538 bpm: u32,
539 beats_per_syllable: f32,
540 karaoke_type: KaraokeType,
541 ) -> Self {
542 Self {
543 event_range,
544 karaoke_template: KaraokeTemplate::Beat {
545 beats_per_minute: bpm,
546 beats_per_syllable,
547 karaoke_type,
548 },
549 }
550 }
551
552 pub fn pattern(event_range: Range, durations: Vec<u32>, karaoke_type: KaraokeType) -> Self {
554 Self {
555 event_range,
556 karaoke_template: KaraokeTemplate::Pattern {
557 durations,
558 karaoke_type,
559 },
560 }
561 }
562
563 pub fn import_from(event_range: Range, source_event_index: usize) -> Self {
565 Self {
566 event_range,
567 karaoke_template: KaraokeTemplate::ImportFrom { source_event_index },
568 }
569 }
570}
571
572impl EditorCommand for ApplyKaraokeCommand {
573 fn execute(&self, document: &mut EditorDocument) -> Result<CommandResult> {
574 let original_text = document.text_range(self.event_range)?;
575 let karaoke_text = self.apply_karaoke_template(&original_text, document)?;
576
577 document.replace_raw(self.event_range, &karaoke_text)?;
578
579 let end_pos = Position::new(self.event_range.start.offset + karaoke_text.len());
580 let range = Range::new(self.event_range.start, end_pos);
581
582 Ok(CommandResult::success_with_change(range, end_pos))
583 }
584
585 fn description(&self) -> &str {
586 match &self.karaoke_template {
587 KaraokeTemplate::Equal { .. } => "Apply equal karaoke timing",
588 KaraokeTemplate::Beat { .. } => "Apply beat-based karaoke timing",
589 KaraokeTemplate::Pattern { .. } => "Apply pattern-based karaoke timing",
590 KaraokeTemplate::ImportFrom { .. } => "Import karaoke timing",
591 }
592 }
593
594 fn memory_usage(&self) -> usize {
595 let template_size = match &self.karaoke_template {
596 KaraokeTemplate::Pattern { durations, .. } => {
597 durations.len() * core::mem::size_of::<u32>()
598 }
599 _ => 0,
600 };
601 core::mem::size_of::<Self>() + template_size
602 }
603}
604
605impl ApplyKaraokeCommand {
606 fn apply_karaoke_template(&self, text: &str, _document: &EditorDocument) -> Result<String> {
608 let clean_text = self.extract_clean_text(text);
610 let syllables = self.detect_syllables(&clean_text);
611
612 match &self.karaoke_template {
613 KaraokeTemplate::Equal {
614 syllable_duration,
615 karaoke_type,
616 } => self.apply_equal_timing(&syllables, *syllable_duration, *karaoke_type),
617 KaraokeTemplate::Beat {
618 beats_per_minute,
619 beats_per_syllable,
620 karaoke_type,
621 } => self.apply_beat_timing(
622 &syllables,
623 *beats_per_minute,
624 *beats_per_syllable,
625 *karaoke_type,
626 ),
627 KaraokeTemplate::Pattern {
628 durations,
629 karaoke_type,
630 } => self.apply_pattern_timing(&syllables, durations, *karaoke_type),
631 KaraokeTemplate::ImportFrom {
632 source_event_index: _,
633 } => {
634 Ok(text.to_string())
636 }
637 }
638 }
639
640 fn extract_clean_text(&self, text: &str) -> String {
642 let mut result = String::new();
643 let mut chars = text.chars();
644
645 while let Some(ch) = chars.next() {
646 if ch == '{' {
647 let mut brace_count = 1;
649 for inner_ch in chars.by_ref() {
650 if inner_ch == '{' {
651 brace_count += 1;
652 } else if inner_ch == '}' {
653 brace_count -= 1;
654 if brace_count == 0 {
655 break;
656 }
657 }
658 }
659 } else {
660 result.push(ch);
661 }
662 }
663
664 result
665 }
666
667 fn detect_syllables(&self, text: &str) -> Vec<String> {
669 text.split_whitespace()
671 .flat_map(|word| {
672 vec![word.to_string()]
675 })
676 .collect()
677 }
678
679 fn apply_equal_timing(
681 &self,
682 syllables: &[String],
683 duration: u32,
684 karaoke_type: KaraokeType,
685 ) -> Result<String> {
686 let tag = karaoke_type.tag_string();
687 let mut result = String::new();
688
689 for (i, syllable) in syllables.iter().enumerate() {
690 if i > 0 {
691 result.push(' '); }
693 result.push_str(&format!("{{\\{tag}{duration}}}{syllable}"));
694 }
695
696 Ok(result)
697 }
698
699 fn apply_beat_timing(
701 &self,
702 syllables: &[String],
703 bpm: u32,
704 beats_per_syllable: f32,
705 karaoke_type: KaraokeType,
706 ) -> Result<String> {
707 let duration = ((60.0 / bpm as f32) * beats_per_syllable * 100.0) as u32;
709 self.apply_equal_timing(syllables, duration, karaoke_type)
710 }
711
712 fn apply_pattern_timing(
714 &self,
715 syllables: &[String],
716 durations: &[u32],
717 karaoke_type: KaraokeType,
718 ) -> Result<String> {
719 let tag = karaoke_type.tag_string();
720 let mut result = String::new();
721
722 for (i, syllable) in syllables.iter().enumerate() {
723 if i > 0 {
724 result.push(' ');
725 }
726 let duration = durations.get(i % durations.len()).copied().unwrap_or(50);
727 result.push_str(&format!("{{\\{tag}{duration}}}{syllable}"));
728 }
729
730 Ok(result)
731 }
732}
733
734#[cfg(test)]
735mod tests {
736 use super::*;
737 use crate::core::EditorDocument;
738 #[cfg(not(feature = "std"))]
739 use alloc::vec;
740 #[cfg(not(feature = "std"))]
741 #[test]
742 fn generate_karaoke_basic() {
743 let mut doc = EditorDocument::from_content("Hello World").unwrap();
744 let range = Range::new(Position::new(0), Position::new(11));
745 let command = GenerateKaraokeCommand::new(range, 50);
746
747 let result = command.execute(&mut doc).unwrap();
748 assert!(result.success);
749 assert!(doc.text().contains("\\k50"));
750 }
751
752 #[test]
753 fn split_karaoke() {
754 let mut doc = EditorDocument::from_content("Hello World").unwrap();
755 let range = Range::new(Position::new(0), Position::new(11));
756 let command = SplitKaraokeCommand::new(range, vec![5]).duration(30);
757
758 let result = command.execute(&mut doc).unwrap();
759 assert!(result.success);
760 assert!(doc.text().contains("\\k30"));
761 }
762
763 #[test]
764 fn adjust_karaoke_scale() {
765 let mut doc = EditorDocument::from_content("{\\k50}Hello").unwrap();
766 let range = Range::new(Position::new(0), Position::new(doc.text().len()));
767 let command = AdjustKaraokeCommand::scale(range, 2.0);
768
769 let result = command.execute(&mut doc).unwrap();
770 assert!(result.success);
771 assert!(doc.text().contains("\\k100"));
772 }
773
774 #[test]
775 fn apply_karaoke_equal() {
776 let mut doc = EditorDocument::from_content("Hello World").unwrap();
777 let range = Range::new(Position::new(0), Position::new(11));
778 let command = ApplyKaraokeCommand::equal(range, 40, KaraokeType::Fill);
779
780 let result = command.execute(&mut doc).unwrap();
781 assert!(result.success);
782 assert!(doc.text().contains("\\kf40"));
783 }
784
785 #[test]
786 fn apply_karaoke_beat() {
787 let mut doc = EditorDocument::from_content("Hello World").unwrap();
788 let range = Range::new(Position::new(0), Position::new(11));
789 let command = ApplyKaraokeCommand::beat(range, 120, 0.5, KaraokeType::Standard);
790
791 let result = command.execute(&mut doc).unwrap();
792 assert!(result.success);
793 assert!(doc.text().contains("\\k25"));
795 }
796
797 #[test]
798 fn karaoke_types() {
799 assert_eq!(KaraokeType::Standard.tag_string(), "k");
800 assert_eq!(KaraokeType::Fill.tag_string(), "kf");
801 assert_eq!(KaraokeType::Outline.tag_string(), "ko");
802 assert_eq!(KaraokeType::Transition.tag_string(), "kt");
803 }
804}