#[cfg(not(feature = "std"))]
extern crate alloc;
use crate::parser::{
ast::{EventType, Span},
Event,
};
#[cfg(not(feature = "std"))]
use alloc::{
fmt::Write,
format,
string::{String, ToString},
vec::Vec,
};
#[cfg(feature = "std")]
use std::fmt::Write;
pub struct ScriptGenerator {
pub title: String,
pub styles_count: usize,
pub events_count: usize,
pub complexity_level: ComplexityLevel,
}
#[derive(Debug, Clone, Copy)]
pub enum ComplexityLevel {
Simple,
Moderate,
Complex,
Extreme,
AnimeRealistic,
MovieRealistic,
KaraokeRealistic,
SignRealistic,
EducationalRealistic,
}
impl ScriptGenerator {
#[must_use]
pub fn simple(events_count: usize) -> Self {
Self {
title: "Simple Benchmark Script".to_string(),
styles_count: 1,
events_count,
complexity_level: ComplexityLevel::Simple,
}
}
#[must_use]
pub fn moderate(events_count: usize) -> Self {
Self {
title: "Moderate Benchmark Script".to_string(),
styles_count: 5,
events_count,
complexity_level: ComplexityLevel::Moderate,
}
}
#[must_use]
pub fn complex(events_count: usize) -> Self {
Self {
title: "Complex Benchmark Script".to_string(),
styles_count: 10,
events_count,
complexity_level: ComplexityLevel::Complex,
}
}
#[must_use]
pub fn extreme(events_count: usize) -> Self {
Self {
title: "Extreme Benchmark Script".to_string(),
styles_count: 20,
events_count,
complexity_level: ComplexityLevel::Extreme,
}
}
#[must_use]
pub fn anime_realistic(events_count: usize) -> Self {
Self {
title: "Anime Subtitles".to_string(),
styles_count: 15,
events_count,
complexity_level: ComplexityLevel::AnimeRealistic,
}
}
#[must_use]
pub fn movie_realistic(events_count: usize) -> Self {
Self {
title: "Movie Subtitles".to_string(),
styles_count: 3,
events_count,
complexity_level: ComplexityLevel::MovieRealistic,
}
}
#[must_use]
pub fn karaoke_realistic(events_count: usize) -> Self {
Self {
title: "Karaoke Script".to_string(),
styles_count: 8,
events_count,
complexity_level: ComplexityLevel::KaraokeRealistic,
}
}
#[must_use]
pub fn sign_realistic(events_count: usize) -> Self {
Self {
title: "Sign Translation".to_string(),
styles_count: 12,
events_count,
complexity_level: ComplexityLevel::SignRealistic,
}
}
#[must_use]
pub fn educational_realistic(events_count: usize) -> Self {
Self {
title: "Educational Content".to_string(),
styles_count: 6,
events_count,
complexity_level: ComplexityLevel::EducationalRealistic,
}
}
#[must_use]
pub fn generate(&self) -> String {
let mut script =
String::with_capacity(1000 + (self.styles_count * 200) + (self.events_count * 150));
script.push_str(&self.generate_script_info());
script.push('\n');
script.push_str(&self.generate_styles());
script.push('\n');
script.push_str(&self.generate_events());
script
}
fn generate_script_info(&self) -> String {
format!(
r"[Script Info]
Title: {}
ScriptType: v4.00+
WrapStyle: 0
ScaledBorderAndShadow: yes
PlayResX: 1920
PlayResY: 1080",
self.title
)
}
fn generate_styles(&self) -> String {
let mut styles = String::from(
"[V4+ Styles]\n\
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"
);
for i in 0..self.styles_count {
let style_name_string;
let style_name = if i == 0 {
"Default"
} else {
style_name_string = format!("Style{i}");
&style_name_string
};
let fontsize = 20 + (i * 2);
let color = format!("&H00{:06X}&", i * 0x0011_1111);
writeln!(
styles,
"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"
).unwrap();
}
styles
}
fn generate_events(&self) -> String {
let mut events = String::from(
"[Events]\n\
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n",
);
for i in 0..self.events_count {
let start_cs = u32::try_from(i * 3000).unwrap_or(u32::MAX);
let end_cs = u32::try_from(i * 3000 + 2500).unwrap_or(u32::MAX);
let start_time = Self::format_time(start_cs); let end_time = Self::format_time(end_cs); let style = if self.styles_count > 1 {
format!("Style{}", i % self.styles_count)
} else {
"Default".to_string()
};
let text = self.generate_dialogue_text(i);
writeln!(
events,
"Dialogue: 0,{start_time},{end_time},{style},Speaker,0,0,0,,{text}"
)
.unwrap();
}
events
}
fn format_time(centiseconds: u32) -> String {
let hours = centiseconds / 360_000;
let minutes = (centiseconds % 360_000) / 6_000;
let seconds = (centiseconds % 6000) / 100;
let cs = centiseconds % 100;
format!("{hours}:{minutes:02}:{seconds:02}.{cs:02}")
}
fn generate_dialogue_text(&self, event_index: usize) -> String {
let base_text = format!("This is dialogue line number {}", event_index + 1);
match self.complexity_level {
ComplexityLevel::Simple => base_text,
ComplexityLevel::Moderate => {
format!(r"{{\b1}}{base_text}{{\b0}} with {{\i1}}some{{\i0}} formatting")
}
ComplexityLevel::Complex => {
format!(
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)}}"
)
}
ComplexityLevel::Extreme => {
format!(
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"
)
}
ComplexityLevel::AnimeRealistic => {
Self::generate_anime_dialogue(event_index, &base_text)
}
ComplexityLevel::MovieRealistic => {
Self::generate_movie_dialogue(event_index, &base_text)
}
ComplexityLevel::KaraokeRealistic => {
Self::generate_karaoke_dialogue(event_index, &base_text)
}
ComplexityLevel::SignRealistic => Self::generate_sign_dialogue(event_index, &base_text),
ComplexityLevel::EducationalRealistic => {
Self::generate_educational_dialogue(event_index, &base_text)
}
}
}
fn generate_anime_dialogue(event_index: usize, base_text: &str) -> String {
let patterns = [
format!(
r"{{\an8\pos(960,80)\fad(250,250)\bord3\shad0\c&H00FFFFFF&\3c&H00FF8C00&}}{base_text}"
),
format!(
r"{{\an5\pos(960,540)\fad(500,500)\alpha&H80&\bord2\c&H00E6E6FA&\3c&H00483D8B&}}{base_text}"
),
format!(
r"{{\an2\pos(960,980)\fad(300,800)\b1\bord4\shad3\c&H0000FFFF&\3c&H000000FF&\4c&H00000000&\t(0,2000,\c&H00FF0000&)}}{base_text}"
),
format!(
r"{{\an7\pos(200,400)\fad(200,200)\bord2\c&H00FFFFFF&\3c&H00800080&}}{base_text}"
),
];
patterns[event_index % patterns.len()].clone()
}
fn generate_movie_dialogue(event_index: usize, base_text: &str) -> String {
let patterns = [
base_text.to_string(),
format!(r"{{\i1}}{base_text}{{\i0}}"),
format!(r"{{\b1}}{base_text}{{\b0}}"),
format!(r"{{\an8}}{base_text}"),
];
patterns[event_index % patterns.len()].clone()
}
fn generate_karaoke_dialogue(event_index: usize, base_text: &str) -> String {
let words: Vec<&str> = base_text.split_whitespace().collect();
let mut karaoke_text = String::new();
karaoke_text.push_str(
r"{\an5\pos(960,540)\fad(200,200)\b1\bord2\shad1\c&H00FFFFFF&\3c&H00FF6347&}",
);
for (i, word) in words.iter().enumerate() {
let timing = 50 + (i * 30); write!(karaoke_text, r"{{\k{timing}}}{word} ").unwrap();
}
if event_index % 3 == 0 {
karaoke_text.push_str(r"{\t(2000,3000,\fscx120\fscy120\alpha&HFF&)}");
}
karaoke_text
}
fn generate_sign_dialogue(event_index: usize, base_text: &str) -> String {
let positions = [
(r"{\an8\pos(960,100)", "RESTAURANT"),
(r"{\an9\pos(1700,150)", "EXIT"),
(r"{\an7\pos(220,120)", "HOTEL"),
(r"{\an5\pos(960,540)", "NEWS FLASH"),
(r"{\an2\pos(960,950)", "SUBWAY"),
(r"{\an4\pos(100,540)", "STORE"),
];
let (pos_tag, sign_type) = &positions[event_index % positions.len()];
let sign_text = if base_text.contains("number") {
format!(
"{sign_type}: {}",
base_text.replace("dialogue line", "sign")
)
} else {
(*sign_type).to_string()
};
format!(
r"{pos_tag}\fad(500,500)\bord3\shad2\c&H00000000&\3c&H00FFFFFF&\fn{{Arial}}\fs36}}{sign_text}"
)
}
fn generate_educational_dialogue(event_index: usize, base_text: &str) -> String {
let patterns = [
format!(
r"{{\an2\pos(960,900)\fad(200,200)\bord1\c&H00FFFFFF&}}{base_text} - This explains the concept in detail with proper formatting."
),
format!(
r"{{\an8\pos(960,150)\fad(200,200)\b1\c&H0000FFFF&}}Question {}: {base_text}",
event_index + 1
),
format!(r"{{\an7\pos(100,400)\fad(200,200)\i1\c&H0000FF00&}}Answer: {base_text}"),
format!(
r"{{\an5\pos(960,540)\fad(200,200)\bord2\c&H00FFFFFF&\3c&H000080FF&}}Definition: {base_text}"
),
format!(r"{{\an1\pos(100,900)\fad(200,200)\c&H00FFFF00&}}Example: {base_text}"),
format!(r"{{\an9\pos(1700,100)\fad(200,200)\b1\c&H00FF8000&}}Summary: {base_text}"),
];
patterns[event_index % patterns.len()].clone()
}
}
#[must_use]
pub const fn create_test_event<'a>(start: &'a str, end: &'a str, text: &'a str) -> Event<'a> {
Event {
event_type: EventType::Dialogue,
layer: "0",
start,
end,
style: "Default",
name: "",
margin_l: "0",
margin_r: "0",
margin_v: "0",
margin_t: None,
margin_b: None,
effect: "",
text,
span: Span::new(0, 0, 0, 0),
}
}
#[must_use]
pub fn generate_script_with_issues(event_count: usize) -> String {
let mut script = String::from(
"[Script Info]\n\
Title: Test Script\n\n\
[V4+ Styles]\n\
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\
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\
[Events]\n\
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
);
for i in 0..event_count {
let start_time = format!("0:{:02}:{:02}.00", i / 60, i % 60);
let end_time = format!("0:{:02}:{:02}.50", i / 60, i % 60);
let text_string;
let text = if i % 10 == 0 {
r"Text with {\} empty tag and {\invalidtag} unknown tag"
} else if i % 7 == 0 {
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"
} else {
let line_num = i + 1;
text_string = format!("Normal dialogue line {line_num}");
&text_string
};
writeln!(
script,
"Dialogue: 0,{start_time},{end_time},Default,Speaker,0,0,0,,{text}"
)
.unwrap();
}
script
}
#[must_use]
pub fn generate_overlapping_script(event_count: usize) -> String {
let mut script = String::from(
r"[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default,Arial,20,&H00FFFFFF,&H000000FF,&H00000000,&H80000000,0,0,0,0,100,100,0,0,1,2,2,2,10,10,10,1
[Events]
Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
",
);
for i in 0..event_count {
let start_time = i * 2; let end_time = start_time + 5; writeln!(
&mut script,
"Dialogue: 0,0:{:02}:{:02}.00,0:{:02}:{:02}.00,Default,,0,0,0,,Event {} text",
start_time / 60,
start_time % 60,
end_time / 60,
end_time % 60,
i
)
.unwrap();
}
script
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn script_generator_simple() {
let generator = ScriptGenerator::simple(5);
assert_eq!(generator.events_count, 5);
assert_eq!(generator.styles_count, 1);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::Simple
));
let script = generator.generate();
assert!(script.contains("[Script Info]"));
assert!(script.contains("[V4+ Styles]"));
assert!(script.contains("[Events]"));
assert!(script.contains("Simple Benchmark Script"));
}
#[test]
fn script_generator_moderate() {
let generator = ScriptGenerator::moderate(3);
assert_eq!(generator.events_count, 3);
assert_eq!(generator.styles_count, 5);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::Moderate
));
}
#[test]
fn script_generator_complex() {
let generator = ScriptGenerator::complex(2);
assert_eq!(generator.events_count, 2);
assert_eq!(generator.styles_count, 10);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::Complex
));
}
#[test]
fn script_generator_extreme() {
let generator = ScriptGenerator::extreme(1);
assert_eq!(generator.events_count, 1);
assert_eq!(generator.styles_count, 20);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::Extreme
));
}
#[test]
fn format_time_zero() {
assert_eq!(ScriptGenerator::format_time(0), "0:00:00.00");
}
#[test]
fn format_time_basic() {
assert_eq!(ScriptGenerator::format_time(6150), "0:01:01.50");
}
#[test]
fn format_time_hours() {
assert_eq!(ScriptGenerator::format_time(360_000), "1:00:00.00");
}
#[test]
fn create_test_event_basic() {
let event = create_test_event("0:00:00.00", "0:00:05.00", "Test text");
assert_eq!(event.start, "0:00:00.00");
assert_eq!(event.end, "0:00:05.00");
assert_eq!(event.text, "Test text");
assert_eq!(event.style, "Default");
assert!(matches!(event.event_type, EventType::Dialogue));
}
#[test]
fn generate_script_with_issues_basic() {
let script = generate_script_with_issues(5);
assert!(script.contains("[Script Info]"));
assert!(script.contains("[V4+ Styles]"));
assert!(script.contains("[Events]"));
assert!(script.contains("Dialogue:"));
}
#[test]
fn generate_script_with_issues_contains_problems() {
let script = generate_script_with_issues(20);
assert!(script.lines().count() > 10);
assert!(script.contains("empty tag") || script.contains("unknown tag"));
}
#[test]
fn generate_overlapping_script_basic() {
let script = generate_overlapping_script(3);
assert!(script.contains("[V4+ Styles]"));
assert!(script.contains("[Events]"));
assert!(script.contains("Event 0 text"));
assert!(script.contains("Event 1 text"));
assert!(script.contains("Event 2 text"));
}
#[test]
fn generate_overlapping_script_timing() {
let script = generate_overlapping_script(2);
assert!(script.contains("0:00:00.00"));
assert!(script.contains("0:00:05.00"));
assert!(script.contains("0:00:02.00"));
assert!(script.contains("0:00:07.00"));
}
#[test]
fn dialogue_text_complexity_simple() {
let generator = ScriptGenerator::simple(1);
let text = generator.generate_dialogue_text(0);
assert_eq!(text, "This is dialogue line number 1");
}
#[test]
fn dialogue_text_complexity_moderate() {
let generator = ScriptGenerator::moderate(1);
let text = generator.generate_dialogue_text(0);
assert!(text.contains(r"{\b1}"));
assert!(text.contains(r"{\i1}"));
assert!(text.contains("This is dialogue line number 1"));
}
#[test]
fn dialogue_text_complexity_complex() {
let generator = ScriptGenerator::complex(1);
let text = generator.generate_dialogue_text(0);
assert!(text.contains(r"{\pos("));
assert!(text.contains(r"{\t("));
assert!(text.contains("animation"));
}
#[test]
fn dialogue_text_complexity_extreme() {
let generator = ScriptGenerator::extreme(1);
let text = generator.generate_dialogue_text(0);
assert!(text.contains(r"{\k"));
assert!(text.contains("karaoke"));
assert!(text.contains("animations"));
}
#[test]
fn anime_realistic_generator() {
let generator = ScriptGenerator::anime_realistic(5);
assert_eq!(generator.events_count, 5);
assert_eq!(generator.styles_count, 15);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::AnimeRealistic
));
let script = generator.generate();
assert!(script.contains("Anime Subtitles"));
}
#[test]
fn movie_realistic_generator() {
let generator = ScriptGenerator::movie_realistic(3);
assert_eq!(generator.events_count, 3);
assert_eq!(generator.styles_count, 3);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::MovieRealistic
));
}
#[test]
fn karaoke_realistic_generator() {
let generator = ScriptGenerator::karaoke_realistic(2);
assert_eq!(generator.events_count, 2);
assert_eq!(generator.styles_count, 8);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::KaraokeRealistic
));
let text = generator.generate_dialogue_text(0);
assert!(text.contains(r"{\k"));
}
#[test]
fn sign_realistic_generator() {
let generator = ScriptGenerator::sign_realistic(4);
assert_eq!(generator.events_count, 4);
assert_eq!(generator.styles_count, 12);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::SignRealistic
));
let text = generator.generate_dialogue_text(0);
assert!(text.contains(r"{\pos(") || text.contains(r"{\an"));
}
#[test]
fn educational_realistic_generator() {
let generator = ScriptGenerator::educational_realistic(6);
assert_eq!(generator.events_count, 6);
assert_eq!(generator.styles_count, 6);
assert!(matches!(
generator.complexity_level,
ComplexityLevel::EducationalRealistic
));
let text = generator.generate_dialogue_text(1);
assert!(
text.contains("Question") || text.contains("Answer") || text.contains("Definition")
);
}
#[test]
fn script_generator_generate_has_correct_event_count() {
let generator = ScriptGenerator::simple(3);
let script = generator.generate();
assert_eq!(
script
.lines()
.filter(|line| line.starts_with("Dialogue:"))
.count(),
3
);
}
#[test]
fn script_generator_generate_has_correct_style_count() {
let generator = ScriptGenerator::moderate(1); let script = generator.generate();
assert_eq!(
script
.lines()
.filter(|line| line.starts_with("Style:"))
.count(),
5
);
}
}