subtitle

repository·master·Indexed 19 days ago

https://github.com/gsantiago/subtitle.js

A stream-based library for parsing and manipulating subtitle files, primarily supporting SRT and WebVTT formats. It provides a high-performance API for Node.js streams to parse, stringify, map, filter, and resync subtitle timing. The library includes both asynchronous stream-based functions and synchronous alternatives like parseSync and stringifySync for processing subtitle content.

Tokens
3.6K
Snippets
18
Records
19
Agent score
65%

What's inside subtitle

  1. Understand the Subtitle Node structure

    master

    The library works with an array of Node objects. Currently, it supports two types of nodes:

    1. header: Represents the file header (e.g., WEBVTT - Header content).
    2. cue: Represents an actual subtitle entry containing timing and text.

    A cue node's data object contains:

    • start: Start time in milliseconds.
    • end: End time in milliseconds.
    • text: The subtitle text.
    • settings: (Optional) VTT settings like align or line.
    [
      {
        type: 'header',
        data: 'WEBVTT - Header content'
      },
      {
        type: 'cue',
        data: {
          start: 150066,
          end: 158952,
          text: 'With great power comes great responsibility'
        }
      }
    ]
  2. Convert subtitle formats using streams

    master

    The library is designed for high performance using a stream-based API. You can pipe a file stream through parse() to convert raw text into nodes, and then through stringify() to convert those nodes into a different format (SRT or WebVTT).

    import fs from 'fs'
    import { parse, resync, stringify } from 'subtitle'
    
    fs.createReadStream('./my-subtitles.srt')
      .pipe(parse())
      .pipe(resync(-100))
      .pipe(stringify({ format: 'WebVTT' }))
      .pipe(fs.createWriteStream('./my-subtitles.vtt'))
  3. Manipulate subtitle nodes with map and filter

    master

    You can use map and filter as Duplex streams to transform or remove subtitle cues during the streaming process. map allows you to modify node data (like converting text to uppercase), while filter allows you to exclude specific nodes based on a condition.

    import { parse, map, filter, stringify } from 'subtitle'
    
    inputStream
      .pipe(parse())
      .pipe(
        filter(
          // strips all cues that contains "𝅘𝅥𝅮"
          node => !(node.type === 'cue' && node.data.text.includes('𝅘𝅥𝅮'))
        )
      )
      .pipe(
        map(node => {
          if (node.type === 'cue') {
            // convert all cues to uppercase
            node.data.text = node.data.text.toUpperCase()
          }
    
          return node
        })
      )
      .pipe(stringify({ format: 'WebVTT' }))
      .pipe(outputStream)
  4. Use the subtitle library API

    master

    The subtitle library provides a suite of utilities for parsing, manipulating, and stringifying subtitle files. The main entry point exports several functional modules:

    • Parsing: parse (asynchronous) and parseSync (synchronous) to convert subtitle strings into structured data.
    • Stringifying: stringify (asynchronous) and stringifySync (synchronous) to convert structured subtitle data back into subtitle strings.
    • Transformation: map for transforming subtitle entries and filter for removing specific entries.
    • Timestamp Utilities: parseTimestamp, parseTimestamps, and formatTimestamp for handling time-based data.
    • Resyncing: resync for adjusting subtitle timings.
    • Types: Exported types for working with subtitle structures in TypeScript.
  5. Use Node and NodeList for subtitle structures

    master

    Subtitles can be represented as a NodeList (an array of Node objects). A Node is a discriminated union that can be either a NodeHeader or a NodeCue:

    • NodeHeader: Represents a structural header with type: 'header' and a data string.
    • NodeCue: Represents a subtitle cue with type: 'cue' and data containing a Cue object.
    export interface NodeHeader {
      type: 'header'
      data: string
    }
    
    export interface NodeCue {
      type: 'cue'
      data: Cue
    }
    
    export type Node = NodeHeader | NodeCue
    export type NodeList = Node[]
  6. Parse subtitle content synchronously with parseSync

    master

    If you have the entire subtitle content as a string and do not require streaming, use parseSync. Note that for better performance with large files, the stream-based parse function is preferred.

    import { parseSync } from 'subtitle'
    import fs from 'fs'
    
    const input = fs.readFileSync('awesome-movie.srt', 'utf8')
    const nodes = parseSync(input)
    
    // nodes is an array of objects like:
    // [
    //   {
    //     type: 'cue',
    //     data: {
    //       start: 20000,
    //       end: 24400,
    //       text: 'Bla Bla Bla Bla'
    //     }
    //   }
    // ]
  7. Resync subtitle timing with resync

    master

    The resync(time: number) Duplex stream shifts the timing of all cues by the specified number of milliseconds. A positive value advances the subtitles, while a negative value delays them.

    import { parse, resync } from 'subtitle'
    
    // Advance subtitles by 1s
    readableStream
      .pipe(parse())
      .pipe(resync(1000))
      .pipe(outputStream)
    
    // Delay 250ms
    stream.pipe(resync(-250))
  8. Convert nodes to string synchronously with stringifySync

    master

    Use stringifySync to convert an array of parsed nodes into a subtitle string. By default, it returns SRT format, but you can specify WebVTT via the options object.

    import { stringifySync } from 'subtitle'
    
    // Returns SRT
    const srt = stringifySync(nodes, { format: 'SRT' })
    
    // Returns WebVTT
    const vtt = stringifySync(nodes, { format: 'WebVTT' })
  9. Parse and format timestamps

    master

    The library provides utility functions for handling individual timestamps or timestamp ranges (like those found in SRT/VTT files).

    import { 
      parseTimestamp, 
      parseTimestamps, 
      formatTimestamp 
    } from 'subtitle'
    
    // Parse a single timestamp string to milliseconds
    parseTimestamp('00:00:24,400') // => 24400
    
    // Parse a range string (e.g., 'start --> end') to an object
    parseTimestamps('00:01:00,500 --> 00:01:10,800') 
    // => { start: 60500, end: 70800 }
    
    // Format milliseconds back to a timestamp string
    formatTimestamp(142542) // => '00:02:22,542'
    formatTimestamp(142542, { format: 'WebVTT' }) // => '00:02:22.542'
  10. Transform subtitle nodes using the map function

    master

    The map function creates a Node.js Transform stream in objectMode that allows you to transform subtitle nodes as they flow through a stream. It accepts a mapper function which is called for every Node in the stream. The mapper function receives the current Node and its zero-based index as arguments. This is useful for modifying node properties, filtering content (by returning null/undefined, though note that the stream will still emit the result of the mapper), or converting node types during stream processing.

    import { map } from 'subtitle';
    
    // Example: Incrementing a property on every node
    const transformer = map((node, index) => {
      return {
        ...node,
        customId: index
      };
    });
    
    // Usage with a stream pipeline
    // inputStream.pipe(transformer).pipe(outputStream);
  11. Filter subtitle nodes using the filter() stream function

    master

    The filter function creates a Node.js Transform stream in objectMode that allows you to selectively include or exclude subtitle Node objects from a stream.

    To use it, provide a callback function that accepts a Node and returns a boolean. If the callback returns true, the node is passed through the stream; if it returns false, the node is dropped.

    import { filter } from './path-to-subtitle/filter';
    
    // Example: Only keep nodes that are not empty
    const myFilter = filter((node) => node.content.trim().length > 0);
    
    // Usage in a pipeline
    // subtitleStream.pipe(myFilter).pipe(destination);