1#[cfg(not(feature = "std"))]
9extern crate alloc;
10use crate::parser::{
11 ast::{EventType, Span},
12 Event,
13};
14#[cfg(not(feature = "std"))]
15use alloc::{
16 fmt::Write,
17 format,
18 string::{String, ToString},
19 vec::Vec,
20};
21#[cfg(feature = "std")]
22use std::fmt::Write;
23
24pub struct ScriptGenerator {
26 pub title: String,
28 pub styles_count: usize,
30 pub events_count: usize,
32 pub complexity_level: ComplexityLevel,
34}
35
36#[derive(Debug, Clone, Copy)]
38pub enum ComplexityLevel {
39 Simple,
41 Moderate,
43 Complex,
45 Extreme,
47 AnimeRealistic,
49 MovieRealistic,
51 KaraokeRealistic,
53 SignRealistic,
55 EducationalRealistic,
57}
58
59impl ScriptGenerator {
60 #[must_use]
62 pub fn simple(events_count: usize) -> Self {
63 Self {
64 title: "Simple Benchmark Script".to_string(),
65 styles_count: 1,
66 events_count,
67 complexity_level: ComplexityLevel::Simple,
68 }
69 }
70
71 #[must_use]
73 pub fn moderate(events_count: usize) -> Self {
74 Self {
75 title: "Moderate Benchmark Script".to_string(),
76 styles_count: 5,
77 events_count,
78 complexity_level: ComplexityLevel::Moderate,
79 }
80 }
81
82 #[must_use]
84 pub fn complex(events_count: usize) -> Self {
85 Self {
86 title: "Complex Benchmark Script".to_string(),
87 styles_count: 10,
88 events_count,
89 complexity_level: ComplexityLevel::Complex,
90 }
91 }
92
93 #[must_use]
95 pub fn extreme(events_count: usize) -> Self {
96 Self {
97 title: "Extreme Benchmark Script".to_string(),
98 styles_count: 20,
99 events_count,
100 complexity_level: ComplexityLevel::Extreme,
101 }
102 }
103
104 #[must_use]
106 pub fn anime_realistic(events_count: usize) -> Self {
107 Self {
108 title: "Anime Subtitles".to_string(),
109 styles_count: 15,
110 events_count,
111 complexity_level: ComplexityLevel::AnimeRealistic,
112 }
113 }
114
115 #[must_use]
117 pub fn movie_realistic(events_count: usize) -> Self {
118 Self {
119 title: "Movie Subtitles".to_string(),
120 styles_count: 3,
121 events_count,
122 complexity_level: ComplexityLevel::MovieRealistic,
123 }
124 }
125
126 #[must_use]
128 pub fn karaoke_realistic(events_count: usize) -> Self {
129 Self {
130 title: "Karaoke Script".to_string(),
131 styles_count: 8,
132 events_count,
133 complexity_level: ComplexityLevel::KaraokeRealistic,
134 }
135 }
136
137 #[must_use]
139 pub fn sign_realistic(events_count: usize) -> Self {
140 Self {
141 title: "Sign Translation".to_string(),
142 styles_count: 12,
143 events_count,
144 complexity_level: ComplexityLevel::SignRealistic,
145 }
146 }
147
148 #[must_use]
150 pub fn educational_realistic(events_count: usize) -> Self {
151 Self {
152 title: "Educational Content".to_string(),
153 styles_count: 6,
154 events_count,
155 complexity_level: ComplexityLevel::EducationalRealistic,
156 }
157 }
158
159 #[must_use]
161 pub fn generate(&self) -> String {
162 let mut script =
163 String::with_capacity(1000 + (self.styles_count * 200) + (self.events_count * 150));
164
165 script.push_str(&self.generate_script_info());
167 script.push('\n');
168
169 script.push_str(&self.generate_styles());
171 script.push('\n');
172
173 script.push_str(&self.generate_events());
175
176 script
177 }
178
179 fn generate_script_info(&self) -> String {
181 format!(
182 r"[Script Info]
183Title: {}
184ScriptType: v4.00+
185WrapStyle: 0
186ScaledBorderAndShadow: yes
187PlayResX: 1920
188PlayResY: 1080",
189 self.title
190 )
191 }
192
193 fn generate_styles(&self) -> String {
195 let mut styles = String::from(
196 "[V4+ Styles]\n\
197 Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n"
198 );
199
200 for i in 0..self.styles_count {
201 let style_name_string;
202 let style_name = if i == 0 {
203 "Default"
204 } else {
205 style_name_string = format!("Style{i}");
206 &style_name_string
207 };
208 let fontsize = 20 + (i * 2);
209 let color = format!("&H00{:06X}&", i * 0x0011_1111);
210
211 writeln!(
212 styles,
213 "Style: {style_name},Arial,{fontsize},{color},{color},{color},&H00000000&,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1"
214 ).unwrap();
215 }
216
217 styles
218 }
219
220 fn generate_events(&self) -> String {
222 let mut events = String::from(
223 "[Events]\n\
224 Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
225 );
226
227 for i in 0..self.events_count {
228 let start_cs = u32::try_from(i * 3000).unwrap_or(u32::MAX);
229 let end_cs = u32::try_from(i * 3000 + 2500).unwrap_or(u32::MAX);
230 let start_time = Self::format_time(start_cs); let end_time = Self::format_time(end_cs); let style = if self.styles_count > 1 {
233 format!("Style{}", i % self.styles_count)
234 } else {
235 "Default".to_string()
236 };
237 let text = self.generate_dialogue_text(i);
238
239 writeln!(
240 events,
241 "Dialogue: 0,{start_time},{end_time},{style},Speaker,0,0,0,,{text}"
242 )
243 .unwrap();
244 }
245
246 events
247 }
248
249 fn format_time(centiseconds: u32) -> String {
251 let hours = centiseconds / 360_000;
252 let minutes = (centiseconds % 360_000) / 6_000;
253 let seconds = (centiseconds % 6000) / 100;
254 let cs = centiseconds % 100;
255 format!("{hours}:{minutes:02}:{seconds:02}.{cs:02}")
256 }
257
258 fn generate_dialogue_text(&self, event_index: usize) -> String {
260 let base_text = format!("This is dialogue line number {}", event_index + 1);
261
262 match self.complexity_level {
263 ComplexityLevel::Simple => base_text,
264 ComplexityLevel::Moderate => {
265 format!(r"{{\b1}}{base_text}{{\b0}} with {{\i1}}some{{\i0}} formatting")
266 }
267 ComplexityLevel::Complex => {
268 format!(
269 r"{{\pos(100,200)\fad(500,500)\b1\i1\c&H00FF00&}}{base_text}{{\b0\i0\c&HFFFFFF&}} with {{\t(0,1000,\frz360)}}animation{{\t(1000,2000,\frz0)}}"
270 )
271 }
272 ComplexityLevel::Extreme => {
273 format!(
274 r"{{\pos(100,200)\move(100,200,500,400)\fad(300,300)\t(0,500,\fscx120\fscy120)\t(500,1000,\fscx100\fscy100)\b1\i1\u1\s1\bord2\shad2\c&H00FF00&\3c&H0000FF&\4c&H000000&\alpha&H00\3a&H80}}{base_text}{{\b0\i0\u0\s0\r}} {{\k50}}with {{\k30}}karaoke {{\k40}}timing {{\k60}}and {{\k45}}complex {{\k35}}animations"
275 )
276 }
277 ComplexityLevel::AnimeRealistic => {
278 Self::generate_anime_dialogue(event_index, &base_text)
279 }
280 ComplexityLevel::MovieRealistic => {
281 Self::generate_movie_dialogue(event_index, &base_text)
282 }
283 ComplexityLevel::KaraokeRealistic => {
284 Self::generate_karaoke_dialogue(event_index, &base_text)
285 }
286 ComplexityLevel::SignRealistic => Self::generate_sign_dialogue(event_index, &base_text),
287 ComplexityLevel::EducationalRealistic => {
288 Self::generate_educational_dialogue(event_index, &base_text)
289 }
290 }
291 }
292
293 fn generate_anime_dialogue(event_index: usize, base_text: &str) -> String {
295 let patterns = [
296 format!(
298 r"{{\an8\pos(960,80)\fad(250,250)\bord3\shad0\c&H00FFFFFF&\3c&H00FF8C00&}}{base_text}"
299 ),
300 format!(
302 r"{{\an5\pos(960,540)\fad(500,500)\alpha&H80&\bord2\c&H00E6E6FA&\3c&H00483D8B&}}{base_text}"
303 ),
304 format!(
306 r"{{\an2\pos(960,980)\fad(300,800)\b1\bord4\shad3\c&H0000FFFF&\3c&H000000FF&\4c&H00000000&\t(0,2000,\c&H00FF0000&)}}{base_text}"
307 ),
308 format!(
310 r"{{\an7\pos(200,400)\fad(200,200)\bord2\c&H00FFFFFF&\3c&H00800080&}}{base_text}"
311 ),
312 ];
313 patterns[event_index % patterns.len()].clone()
314 }
315
316 fn generate_movie_dialogue(event_index: usize, base_text: &str) -> String {
318 let patterns = [
319 base_text.to_string(),
321 format!(r"{{\i1}}{base_text}{{\i0}}"),
323 format!(r"{{\b1}}{base_text}{{\b0}}"),
325 format!(r"{{\an8}}{base_text}"),
327 ];
328 patterns[event_index % patterns.len()].clone()
329 }
330
331 fn generate_karaoke_dialogue(event_index: usize, base_text: &str) -> String {
333 let words: Vec<&str> = base_text.split_whitespace().collect();
334 let mut karaoke_text = String::new();
335
336 karaoke_text.push_str(
338 r"{\an5\pos(960,540)\fad(200,200)\b1\bord2\shad1\c&H00FFFFFF&\3c&H00FF6347&}",
339 );
340
341 for (i, word) in words.iter().enumerate() {
343 let timing = 50 + (i * 30); write!(karaoke_text, r"{{\k{timing}}}{word} ").unwrap();
345 }
346
347 if event_index % 3 == 0 {
349 karaoke_text.push_str(r"{\t(2000,3000,\fscx120\fscy120\alpha&HFF&)}");
350 }
351
352 karaoke_text
353 }
354
355 fn generate_sign_dialogue(event_index: usize, base_text: &str) -> String {
357 let positions = [
358 (r"{\an8\pos(960,100)", "RESTAURANT"),
360 (r"{\an9\pos(1700,150)", "EXIT"),
361 (r"{\an7\pos(220,120)", "HOTEL"),
362 (r"{\an5\pos(960,540)", "NEWS FLASH"),
364 (r"{\an2\pos(960,950)", "SUBWAY"),
366 (r"{\an4\pos(100,540)", "STORE"),
368 ];
369
370 let (pos_tag, sign_type) = &positions[event_index % positions.len()];
371 let sign_text = if base_text.contains("number") {
372 format!(
373 "{sign_type}: {}",
374 base_text.replace("dialogue line", "sign")
375 )
376 } else {
377 (*sign_type).to_string()
378 };
379
380 format!(
381 r"{pos_tag}\fad(500,500)\bord3\shad2\c&H00000000&\3c&H00FFFFFF&\fn{{Arial}}\fs36}}{sign_text}"
382 )
383 }
384
385 fn generate_educational_dialogue(event_index: usize, base_text: &str) -> String {
387 let patterns = [
388 format!(
390 r"{{\an2\pos(960,900)\fad(200,200)\bord1\c&H00FFFFFF&}}{base_text} - This explains the concept in detail with proper formatting."
391 ),
392 format!(
394 r"{{\an8\pos(960,150)\fad(200,200)\b1\c&H0000FFFF&}}Question {}: {base_text}",
395 event_index + 1
396 ),
397 format!(r"{{\an7\pos(100,400)\fad(200,200)\i1\c&H0000FF00&}}Answer: {base_text}"),
399 format!(
401 r"{{\an5\pos(960,540)\fad(200,200)\bord2\c&H00FFFFFF&\3c&H000080FF&}}Definition: {base_text}"
402 ),
403 format!(r"{{\an1\pos(100,900)\fad(200,200)\c&H00FFFF00&}}Example: {base_text}"),
405 format!(r"{{\an9\pos(1700,100)\fad(200,200)\b1\c&H00FF8000&}}Summary: {base_text}"),
407 ];
408 patterns[event_index % patterns.len()].clone()
409 }
410}
411
412#[must_use]
426pub const fn create_test_event<'a>(start: &'a str, end: &'a str, text: &'a str) -> Event<'a> {
427 Event {
428 event_type: EventType::Dialogue,
429 layer: "0",
430 start,
431 end,
432 style: "Default",
433 name: "",
434 margin_l: "0",
435 margin_r: "0",
436 margin_v: "0",
437 margin_t: None,
438 margin_b: None,
439 effect: "",
440 text,
441 span: Span::new(0, 0, 0, 0),
442 }
443}
444
445#[must_use]
460pub fn generate_script_with_issues(event_count: usize) -> String {
461 let mut script = String::from(
462 "[Script Info]\n\
463 Title: Test Script\n\n\
464 [V4+ Styles]\n\
465 Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding\n\
466 Style: Default,Arial,20,&H00FFFFFF&,&H000000FF&,&H00000000&,&H00000000&,0,0,0,0,100,100,0,0,1,2,0,2,10,10,10,1\n\n\
467 [Events]\n\
468 Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
469 );
470
471 for i in 0..event_count {
472 let start_time = format!("0:{:02}:{:02}.00", i / 60, i % 60);
473 let end_time = format!("0:{:02}:{:02}.50", i / 60, i % 60);
474
475 let text_string;
477 let text = if i % 10 == 0 {
478 r"Text with {\} empty tag and {\invalidtag} unknown tag"
479 } else if i % 7 == 0 {
480 r"{\pos(100,200)\move(100,200,500,400,0,5000)\t(0,1000,\frz360)\t(1000,2000,\fscx200\fscy200)\t(2000,3000,\alpha&HFF&)\t(3000,4000,\alpha&H00&)\t(4000,5000,\c&HFF0000&)}Performance heavy animation"
482 } else {
483 let line_num = i + 1;
484 text_string = format!("Normal dialogue line {line_num}");
485 &text_string
486 };
487
488 writeln!(
489 script,
490 "Dialogue: 0,{start_time},{end_time},Default,Speaker,0,0,0,,{text}"
491 )
492 .unwrap();
493 }
494
495 script
496}
497
498#[must_use]
512pub fn generate_overlapping_script(event_count: usize) -> String {
513 let mut script = String::from(
514 r"[V4+ Styles]
515Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
516Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1
517
518[Events]
519Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
520",
521 );
522
523 for i in 0..event_count {
524 let start_time = i * 2; let end_time = start_time + 5; writeln!(
528 &mut script,
529 "Dialogue: 0,0:{:02}:{:02}.00,0:{:02}:{:02}.00,Default,,0,0,0,,Event {} text",
530 start_time / 60,
531 start_time % 60,
532 end_time / 60,
533 end_time % 60,
534 i
535 )
536 .unwrap();
537 }
538
539 script
540}
541
542#[cfg(test)]
543mod tests {
544 use super::*;
545
546 #[test]
547 fn script_generator_simple() {
548 let generator = ScriptGenerator::simple(5);
549 assert_eq!(generator.events_count, 5);
550 assert_eq!(generator.styles_count, 1);
551 assert!(matches!(
552 generator.complexity_level,
553 ComplexityLevel::Simple
554 ));
555
556 let script = generator.generate();
557 assert!(script.contains("[Script Info]"));
558 assert!(script.contains("[V4+ Styles]"));
559 assert!(script.contains("[Events]"));
560 assert!(script.contains("Simple Benchmark Script"));
561 }
562
563 #[test]
564 fn script_generator_moderate() {
565 let generator = ScriptGenerator::moderate(3);
566 assert_eq!(generator.events_count, 3);
567 assert_eq!(generator.styles_count, 5);
568 assert!(matches!(
569 generator.complexity_level,
570 ComplexityLevel::Moderate
571 ));
572 }
573
574 #[test]
575 fn script_generator_complex() {
576 let generator = ScriptGenerator::complex(2);
577 assert_eq!(generator.events_count, 2);
578 assert_eq!(generator.styles_count, 10);
579 assert!(matches!(
580 generator.complexity_level,
581 ComplexityLevel::Complex
582 ));
583 }
584
585 #[test]
586 fn script_generator_extreme() {
587 let generator = ScriptGenerator::extreme(1);
588 assert_eq!(generator.events_count, 1);
589 assert_eq!(generator.styles_count, 20);
590 assert!(matches!(
591 generator.complexity_level,
592 ComplexityLevel::Extreme
593 ));
594 }
595
596 #[test]
597 fn format_time_zero() {
598 assert_eq!(ScriptGenerator::format_time(0), "0:00:00.00");
599 }
600
601 #[test]
602 fn format_time_basic() {
603 assert_eq!(ScriptGenerator::format_time(6150), "0:01:01.50");
604 }
605
606 #[test]
607 fn format_time_hours() {
608 assert_eq!(ScriptGenerator::format_time(360_000), "1:00:00.00");
609 }
610
611 #[test]
612 fn create_test_event_basic() {
613 let event = create_test_event("0:00:00.00", "0:00:05.00", "Test text");
614 assert_eq!(event.start, "0:00:00.00");
615 assert_eq!(event.end, "0:00:05.00");
616 assert_eq!(event.text, "Test text");
617 assert_eq!(event.style, "Default");
618 assert!(matches!(event.event_type, EventType::Dialogue));
619 }
620
621 #[test]
622 fn generate_script_with_issues_basic() {
623 let script = generate_script_with_issues(5);
624 assert!(script.contains("[Script Info]"));
625 assert!(script.contains("[V4+ Styles]"));
626 assert!(script.contains("[Events]"));
627 assert!(script.contains("Dialogue:"));
628 }
629
630 #[test]
631 fn generate_script_with_issues_contains_problems() {
632 let script = generate_script_with_issues(20);
633 assert!(script.lines().count() > 10);
635 assert!(script.contains("empty tag") || script.contains("unknown tag"));
637 }
638
639 #[test]
640 fn generate_overlapping_script_basic() {
641 let script = generate_overlapping_script(3);
642 assert!(script.contains("[V4+ Styles]"));
643 assert!(script.contains("[Events]"));
644 assert!(script.contains("Event 0 text"));
645 assert!(script.contains("Event 1 text"));
646 assert!(script.contains("Event 2 text"));
647 }
648
649 #[test]
650 fn generate_overlapping_script_timing() {
651 let script = generate_overlapping_script(2);
652 assert!(script.contains("0:00:00.00"));
655 assert!(script.contains("0:00:05.00"));
656 assert!(script.contains("0:00:02.00"));
657 assert!(script.contains("0:00:07.00"));
658 }
659
660 #[test]
661 fn dialogue_text_complexity_simple() {
662 let generator = ScriptGenerator::simple(1);
663 let text = generator.generate_dialogue_text(0);
664 assert_eq!(text, "This is dialogue line number 1");
665 }
666
667 #[test]
668 fn dialogue_text_complexity_moderate() {
669 let generator = ScriptGenerator::moderate(1);
670 let text = generator.generate_dialogue_text(0);
671 assert!(text.contains(r"{\b1}"));
672 assert!(text.contains(r"{\i1}"));
673 assert!(text.contains("This is dialogue line number 1"));
674 }
675
676 #[test]
677 fn dialogue_text_complexity_complex() {
678 let generator = ScriptGenerator::complex(1);
679 let text = generator.generate_dialogue_text(0);
680 assert!(text.contains(r"{\pos("));
681 assert!(text.contains(r"{\t("));
682 assert!(text.contains("animation"));
683 }
684
685 #[test]
686 fn dialogue_text_complexity_extreme() {
687 let generator = ScriptGenerator::extreme(1);
688 let text = generator.generate_dialogue_text(0);
689 assert!(text.contains(r"{\k"));
690 assert!(text.contains("karaoke"));
691 assert!(text.contains("animations"));
692 }
693
694 #[test]
695 fn anime_realistic_generator() {
696 let generator = ScriptGenerator::anime_realistic(5);
697 assert_eq!(generator.events_count, 5);
698 assert_eq!(generator.styles_count, 15);
699 assert!(matches!(
700 generator.complexity_level,
701 ComplexityLevel::AnimeRealistic
702 ));
703
704 let script = generator.generate();
705 assert!(script.contains("Anime Subtitles"));
706 }
707
708 #[test]
709 fn movie_realistic_generator() {
710 let generator = ScriptGenerator::movie_realistic(3);
711 assert_eq!(generator.events_count, 3);
712 assert_eq!(generator.styles_count, 3);
713 assert!(matches!(
714 generator.complexity_level,
715 ComplexityLevel::MovieRealistic
716 ));
717 }
718
719 #[test]
720 fn karaoke_realistic_generator() {
721 let generator = ScriptGenerator::karaoke_realistic(2);
722 assert_eq!(generator.events_count, 2);
723 assert_eq!(generator.styles_count, 8);
724 assert!(matches!(
725 generator.complexity_level,
726 ComplexityLevel::KaraokeRealistic
727 ));
728
729 let text = generator.generate_dialogue_text(0);
730 assert!(text.contains(r"{\k"));
731 }
732
733 #[test]
734 fn sign_realistic_generator() {
735 let generator = ScriptGenerator::sign_realistic(4);
736 assert_eq!(generator.events_count, 4);
737 assert_eq!(generator.styles_count, 12);
738 assert!(matches!(
739 generator.complexity_level,
740 ComplexityLevel::SignRealistic
741 ));
742
743 let text = generator.generate_dialogue_text(0);
744 assert!(text.contains(r"{\pos(") || text.contains(r"{\an"));
745 }
746
747 #[test]
748 fn educational_realistic_generator() {
749 let generator = ScriptGenerator::educational_realistic(6);
750 assert_eq!(generator.events_count, 6);
751 assert_eq!(generator.styles_count, 6);
752 assert!(matches!(
753 generator.complexity_level,
754 ComplexityLevel::EducationalRealistic
755 ));
756
757 let text = generator.generate_dialogue_text(1);
758 assert!(
759 text.contains("Question") || text.contains("Answer") || text.contains("Definition")
760 );
761 }
762
763 #[test]
764 fn script_generator_generate_has_correct_event_count() {
765 let generator = ScriptGenerator::simple(3);
766 let script = generator.generate();
767 assert_eq!(
768 script
769 .lines()
770 .filter(|line| line.starts_with("Dialogue:"))
771 .count(),
772 3
773 );
774 }
775
776 #[test]
777 fn script_generator_generate_has_correct_style_count() {
778 let generator = ScriptGenerator::moderate(1); let script = generator.generate();
780 assert_eq!(
781 script
782 .lines()
783 .filter(|line| line.starts_with("Style:"))
784 .count(),
785 5
786 );
787 }
788}