mdcourse

    Download zip
    Author
    Version
    0.1.1
    License
    Apache-2.0
    Last updated
    1 hour ago
    Downloads
    1

    #mdcourse - Marp + SSML Video Generation Tool

    Generate educational videos from Marp markdown presentations with text-to-speech narration.

    #Installation

    #Prerequisites

    1. System Tools:
      # macOS brew install ffmpeg poppler # Ubuntu/Debian sudo apt install ffmpeg poppler-utils # Windows # Install ffmpeg from https://ffmpeg.org/download.html # Install poppler from https://blog.alivate.com.au/poppler-windows/

    2. Node.js:
      # Required for Marp CLI (will be installed locally by install.sh) # Install from: https://nodejs.org/

    3. MoonBit Toolchain:
      # Install MoonBit from https://www.moonbitlang.com/ curl -fsSL https://cli.moonbitlang.com/install/unix.sh | bash

    #Build mdcourse

    cd mdcourse moon install # Install dependencies moon build # Build the binary

    Run the installer script to install mdcourse to ~/.local/bin:

    ./install.sh

    This will:
    • Build the native binary
    • Install to ~/.mdcourse/ with all resources (engine.mjs, custom.css, etc.)
    • Install npm dependencies (@marp-team/marp-cli, shiki) locally in ~/.mdcourse/
    • Create a wrapper script in ~/.local/bin/mdcourse
    • Make the mdcourse command available system-wide

    Note: Marp CLI and its dependencies are installed locally in ~/.mdcourse/, so you don't need to install @marp-team/marp-cli globally.

    After installation, you can run:

    mdcourse input.mbt.md

    Uninstall:
    rm -rf ~/.mdcourse rm ~/.local/bin/mdcourse

    #Development Usage (Without Installation)

    If you're developing mdcourse, you can run it directly without installation:

    cd mdcourse moon run cmd/main -- ../course7/lec7-1.mbt.md

    #Azure TTS Setup

    Set up environment variables for text-to-speech:

    export TTS_KEY="your-azure-subscription-key" export TTS_REGION="eastus" # or your preferred region

    Get your Azure credentials from Azure Cognitive Services.

    #Usage

    #Basic Usage

    # Generate complete video with audio moon run cmd/main -- input.mbt.md # Specify output file moon run cmd/main -- -o output.mp4 input.mbt.md

    #Partial Generation

    # Only parse markdown and extract SSML moon run cmd/main -- --parse-only input.mbt.md # Only generate slide images moon run cmd/main -- --slides-only input.mbt.md # Only generate audio (requires previous parse) moon run cmd/main -- --audio-only input.mbt.md # Only compose video (requires slides + audio) moon run cmd/main -- --compose-only input.mbt.md

    #Options

    -o, --output <OUTPUT> Output video file path (default: <input-basename>.mp4) --parse-only Only parse file --slides-only Only generate slide images --audio-only Only generate audio --compose-only Only compose video --force-audio Force regenerate all audio --keep-temp Keep temporary video segments for debugging --voice <VOICE> Specify TTS voice --fps <FPS> Video frame rate --resolution <RESOLUTION> Video resolution (4k, 1440p, 1080p, 720p, 480p) --default-duration <DEFAULT-DURATION> Default duration for slides without audio (seconds)

    #Example

    # Generate 4K video with custom voice moon run cmd/main -- \ -p 4k \ -v zh-CN-XiaoxiaoNeural \ -o course7-video.mp4 \ ../course7/lec7-1.mbt.md

    #Writing Course Content

    #Basic Structure

    Create a Marp markdown file with .mbt.md extension:

    --- marp: true theme: custom headingDivider: 1 --- # Slide Title Slide content here. <!-- ssml 这是第一页的旁白文字。 --> # Another Slide More content. <!-- ssml 这是第二页的旁白文字。 -->

    #SSML Narration

    Add narration using HTML comments with ssml keyword:

    <!-- ssml 普通文字会被朗读。 <break time="500ms"/> 可以使用 SSML 标签来控制语音。 -->

    #Inserting Videos

    Insert video clips between slides:

    # Slide Before Video Content here. <!-- video path/to/video.mp4 --> # Slide After Video More content.

    Add narration to videos (optional):

    <!-- video path/to/video.mp4 ssml 这是视频的配音。 -->

    #Slide Splitting

    Option 1: Manual dividers - Use --- to separate slides:

    --- marp: true --- # First Slide Content. --- # Second Slide More content.

    Option 2: Automatic splitting - Use headingDivider: 1 to split on # headings:

    --- marp: true headingDivider: 1 --- # First Slide Content. # Second Slide More content.

    #SSML Features

    Common SSML tags supported by Azure TTS:

    <!-- ssml 这是<emphasis>强调</emphasis>的文字。 <break time="1s"/> 暂停一秒。 <prosody rate="slow">慢速朗读。</prosody> <prosody pitch="+10%">提高音调。</prosody> -->

    See Azure SSML documentation for full reference.

    #Output Structure

    After running mdcourse, you'll get:

    mdcoursetarget/ └── course-name/ ├── manifest.json # Slide and video metadata ├── audio/ │ ├── slide-001.ssml # Original SSML content │ ├── slide-001.wav # Generated audio │ ├── slide-001.meta.json # Audio duration metadata │ └── ... ├── slides/ │ ├── slide-001.png # Slide images (300 DPI) │ └── ... ├── segments/ # Temporary video segments (if --keep-temp) │ ├── segment-001.mp4 │ └── ... └── output.mp4 # Final video

    #Tips

    1. Audio Caching: Audio files are cached by content hash. Only modified slides will regenerate audio.
    2. Incremental Builds: Use stage flags (--parse-only, --slides-only, etc.) for faster iteration.
    3. Custom Themes: Place your Marp theme CSS in the project root as custom.css.
    4. Custom Engine: Customize Marp behavior by editing engine.mjs.
    5. Large Videos: Use --keep-temp to debug if video composition fails.
    6. Output Directory: All generated files go to ./mdcoursetarget/ to keep them separate from build artifacts.

    #Troubleshooting

    Problem: Audio generation fails with TTS_KEY error
    • Solution: Make sure TTS_KEY and TTS_REGION environment variables are set

    Problem: Marp CLI not found
    • Solution: Install with npm install -g @marp-team/marp-cli

    Problem: pdftoppm not found
    • Solution: Install poppler-utils (contains pdftoppm)

    Problem: ffmpeg not found
    • Solution: Install ffmpeg via your package manager

    Problem: Video has wrong slide numbering
    • Solution: Check headingDivider setting in frontmatter matches your slide structure

    #License

    Same as parent repository (see top-level LICENSE).

    Options

    pub struct Options {
    input_file : String
    output_file : String?
    parse_only : Bool
    slides_only : Bool
    audio_only : Bool
    compose_only : Bool
    force_audio : Bool
    keep_temp : Bool
    voice : String
    fps : Int
    resolution : String
    default_duration : Double
    theme : String
    engine : String
    }

    CLI options

    Options::new

    fn Options::new(voice : String, fps : Int, resolution : String, force_audio : Bool, keep_temp : Bool, default_duration : Double, output_file : String?) -> Options

    Create Options from parsed CLI arguments

    compose_video

    async fn compose_video(output_dir : String, output_file : String?, options : Options) -> Bool

    Compose final video from slides and audio

    Workflow:
    1. Read manifest.json to get slides and their metadata
    2. For each slide:
      • Create video segment from PNG + audio (or static duration)
      • Use ffmpeg to combine image + audio
    3. Concatenate all segments into final video
    4. Apply resolution scaling and FPS settings

    generate_audio

    async fn generate_audio(output_dir : String, options : Options) -> Bool

    Generate audio files from SSML using Azure TTS

    Workflow:
    1. Read manifest.json to get list of slides with SSML
    2. For each slide with audio:
      • Check if audio already exists and hasn't changed (via hash)
      • Read SSML file
      • Wrap SSML in proper XML document
      • Call Azure TTS REST API
      • Save WAV file
      • Calculate duration from WAV header
      • Save metadata (duration + hash)

    generate_slides

    async fn generate_slides(input_file : String, output_dir : String, options : Options) -> Bool

    Generate slide images from markdown file Step 1: Use Marp CLI to generate PDF Step 2: Use pdftoppm to convert PDF to PNG images

    get_course_name_from_path

    fn get_course_name_from_path(input_file : String) -> String

    Get course name from input file path

    get_video_duration

    async fn get_video_duration(video_path : String) -> Double

    Get video duration using ffprobe

    hash_file

    async fn hash_file(file_path : String) -> String

    Hash file contents using SHA256

    parse_file

    async fn parse_file(input_path : String, output_dir : String) -> Bool

    Parse a markdown file and generate manifest

    target_dir

    fn target_dir(course_name : String) -> String

    Build target directory path from course name