///|
/// Main entry point for mdcourse CLI
async fn main {
let cli_args = @env.args()
// Define CLI arguments
let parser = @clap.Parser::new(
prog="mdcourse",
description="Marp+SSML video generation tool",
args={
"output": @clap.Arg::named(
short='o',
nargs=AtMost(1),
help="Output video file path (default: .mp4)",
),
"parse-only": @clap.Arg::flag(help="Only parse file"),
"slides-only": @clap.Arg::flag(help="Only generate slide images"),
"audio-only": @clap.Arg::flag(help="Only generate audio"),
"compose-only": @clap.Arg::flag(help="Only compose video"),
"force-audio": @clap.Arg::flag(help="Force regenerate all audio"),
"keep-temp": @clap.Arg::flag(
help="Keep temporary video segments for debugging",
),
"voice": @clap.Arg::named(
nargs=One,
defaults=["zh-CN-YunyiMultilingualNeural"],
help="Specify TTS voice",
),
"fps": @clap.Arg::named(
nargs=One,
defaults=["30"],
help="Video frame rate",
),
"resolution": @clap.Arg::named(
nargs=One,
defaults=["1080p"],
help="Video resolution (4k, 1440p, 1080p, 720p, 480p)",
),
"default-duration": @clap.Arg::named(
nargs=One,
defaults=["3.0"],
help="Default duration for slides without audio (seconds)",
),
"input": @clap.Arg::positional(nargs=One, help="Input .mbt.md file"),
},
)
let args = if cli_args.length() > 0 { cli_args[1:] } else { [] }
// Parse arguments
let value = @clap.SimpleValue::new("mdcourse")
let help_msg = parser.parse(value, args) catch {
e => {
println("Error: \{e}")
return
}
}
// Show help if requested
if help_msg is Some(msg) {
println(msg)
return
}
// Get input file (positional argument)
let input_file : String = value.get_positional()[0] catch {
e => {
println("Error: \{e}")
println("")
println("Usage: mdcourse [options]")
println("Run 'mdcourse --help' for full help")
return
}
}
// Get flags
let parse_only = value.get_flag("parse-only").unwrap_or(false)
let slides_only = value.get_flag("slides-only").unwrap_or(false)
let audio_only = value.get_flag("audio-only").unwrap_or(false)
let compose_only = value.get_flag("compose-only").unwrap_or(false)
let force_audio = value.get_flag("force-audio").unwrap_or(false)
let keep_temp = value.get_flag("keep-temp").unwrap_or(false)
// Get string options with defaults
let voice : String = value.get_one("voice") catch {
_ => "zh-CN-YunyiMultilingualNeural"
}
let fps_str : String = value.get_one("fps") catch { _ => "30" }
let resolution : String = value.get_one("resolution") catch { _ => "1080p" }
let default_duration_str : String = value.get_one("default-duration") catch {
_ => "3.0"
}
let output_file : String? = value.get_option("output") catch { _ => None }
// Compute default output filename from input if not specified
let final_output_file : String? = match output_file {
Some(path) => Some(path)
None => {
// Derive output filename from input: lec7-1.mbt.md -> lec7-1.mp4
let base = if input_file.has_suffix(".mbt.md") {
let len = input_file.length()
input_file[0:len - 7].to_owned()
} else if input_file.has_suffix(".md") {
let len = input_file.length()
input_file[0:len - 3].to_owned()
} else {
input_file
}
Some(base + ".mp4")
}
}
// Parse numeric values
let fps = @string.parse_int(fps_str) catch {
_ => {
println("Error: Invalid fps value: \{fps_str}")
return
}
}
let default_duration = @string.parse_double(default_duration_str) catch {
_ => {
println("Error: Invalid default-duration value: \{default_duration_str}")
return
}
}
// Get course name from input file
let course_name = @mdcourse.get_course_name_from_path(input_file)
let output_dir = @mdcourse.target_dir(course_name)
let options = @mdcourse.Options::new(
voice, fps, resolution, force_audio, keep_temp, default_duration, final_output_file,
)
println("mdcourse v0.1.0 - MoonBit Implementation")
println("=========================================")
println("Input: \{input_file}")
println("Output: \{output_dir}\n")
// Step 1: Parse markdown and extract SSML (unless only composing)
if !compose_only {
let parse_success = @mdcourse.parse_file(input_file, output_dir)
if !parse_success {
println("\n[ERROR] Parsing failed. Stopping workflow.")
return
}
if parse_only {
println("\nDone (parse only)")
return
}
}
// Step 2: Generate slide images (unless only audio or compose)
if !audio_only && !compose_only {
let slides_success = @mdcourse.generate_slides(
input_file, output_dir, options,
)
if !slides_success {
println("\n[ERROR] Slide generation failed. Stopping workflow.")
return
}
if slides_only {
println("\nDone (slides only)")
return
}
}
// Step 3: Generate audio (unless only composing or only slides)
if !compose_only && !slides_only {
let audio_success = @mdcourse.generate_audio(output_dir, options)
if !audio_success {
println("\n[ERROR] Audio generation failed. Stopping workflow.")
return
}
if audio_only {
println("\nDone (audio only)")
return
}
}
// Step 4: Compose video (unless parse-only, audio-only, or slides-only)
if !parse_only && !audio_only && !slides_only {
let compose_success = @mdcourse.compose_video(
output_dir,
options.output_file,
options,
)
if !compose_success {
println("\n[ERROR] Video composition failed.")
return
}
println("\nAll done!")
if final_output_file is Some(path) {
println("Video location: \{path}")
}
}
}