Skip to content


Repository files navigation

SVideo for Scratch

Convert your videos into an animated Scratch project!

Technical Achievements

Many frames per costume

Instead of the naiive method of assigning each frame of a video to a costume, each costume contains many frames, arranged in a grid (default of 30x30). This allows the PNG or JPEG compression to work between more frames (partially interframe compression) rather than per frame (intraframe compression).

Frames are instead displayed as a spritesheet.

Skip (immediate) duplicate frames

To be able to cram more photos per spritesheet and improve overall compression, as well as reduce the number of bitmaps Scratch needs to load, frames that look similar to the immediate previous frame will be discarded, and will be repeated in the video player.


이달의소녀탐구 #1 (LOONA TV #1)

A gif of the above video


npm i -g svideo

Command Line Interface

svideo -i input.mp4 -o output.sb3
Command Line Arguments
      --help                        Show help                                                                                 [boolean]
      --version                     Show version number                                                                       [boolean]
      --rows, --row                 The number of rows to place in the grid                                      [number] [default: 10]
      --columns, --col              The number of columns to place in the grid                                   [number] [default: 10]
  -i, --input                       Input file to convert                                                           [string] [required]
  -o, --output                      Destination file for the Scratch `.sb3` archive                                 [string] [required]
  -w, --width                       The width of each frame                                                     [number] [default: 480]
  -t, --temporaryFolder             Path to a temporary folder for use while building the project           [string] [default: "temp/"]
  -f, --imageFileFormat             The file format of frames in the output                    [choices: "jpg", "png"] [default: "jpg"]
  -r, --frameRate                   The framerate of the output                                                                [number]
  -q, --compressionLevel            The compression level of the image. 1-100 for PNG and 1-32 for JPEG                        [number]
  -a, --audioInterval               The number of seconds between cuts in the audio                         [number] [default: No cuts]
  -s, --subtitles                   Hardcode (burn) subtitles onto the video                                                   [string]
      --videoFilters, --vf          Additional video filters to pass to FFMPEG, such as crop                                   [string]
      --backgroundColour, --colour  Set the colour of the padded region around the video                    [string] [default: "black"]
      --startPosition, --ss         Seek to a start position                                                                   [string]
      --endPosition, --to           Stop at an end position                                                                    [string]


You can interface with svideo with the application programming interface instead if you wish. Comments for each method will be included at a later date. (or if someone does it before I do)

  • yarn: yarn add svideo
  • npm: npm i --save svideo
// ES6 Import Statements
import svideo from "svideo";

// Not ES6 Require Statement
// const svideo = require("svideo");

// Create a new SVideo Application
const app = new svideo.App();

(async () => {
  // Set the input file
  await app.setFile("wooper.mp4");

  // Set the subtitles file

  // Set the output file

  // Print information

  // Convert and commit changes
  await app.convert();


Convert correctly numbered LOONA TV episodes in the tv/ folder, while hardcoding subtitles if available
import { existsSync, readdirSync, renameSync } from "fs";
import { resolve, parse } from "path";
import SVideo from "../dist/index.js";

const videos = readdirSync("tv")
  .map((file) => parse(file))
  .filter((video) => [".mp4", ".webm", ".mkv"].includes(video.ext))
  .sort((a, b) => parseInt(, 10) - parseInt(, 10))
  .map((video) => {
    const videoPath = resolve("tv", video.base);
    const subtitlePath = resolve("tv", + ".vtt");
    let subtitle;

    if (!existsSync(videoPath)) throw new Error("Cannot find " + videoPath);
    if (!existsSync(subtitlePath)) {
      console.log("Cannot find " + subtitlePath);
    } else {
      subtitle = "tv/" + + ".vtt";

    return {
      folder: resolve("tv"),
      video: videoPath,


for (const video of videos) {
  const converter = new SVideo.App();


  await converter.setFile(;
  if (video.subtitle) converter.setSubtitlesFile(video.subtitle);
      `이달의소녀탐구 #${} (LOONA TV #${}).sb3`


  await converter.convert();


This project is licenced under the MIT licence. Because it's MIT Scratch. Haha.