Skip to content

Instantly share code, notes, and snippets.

@LaptopDev
Created March 16, 2025 05:15
Show Gist options
  • Save LaptopDev/ac02fcd55d929323c722ad71919ff4cd to your computer and use it in GitHub Desktop.
Save LaptopDev/ac02fcd55d929323c722ad71919ff4cd to your computer and use it in GitHub Desktop.
Convert youtube .vtt to .srt
local input_file = "tG1hCDXoE-I.en.vtt" -- Change this to your VTT file path
local output_file = "output.srt"
-- Define input and output file paths.
-- Read the entire VTT file into an array of lines.
local lines = {}
for line in io.lines(input_file) do
table.insert(lines, line)
end
-- Arrays for storing parsed values:
local timestamp_str_start = {} -- Starting timestamp extracted from VTT (from line start to space after '>')
local timestamp_str_start_len = {} -- Length of the starting timestamp string (first element number value)
local timestamp_str_ending = {} -- Ending timestamp snippet (the first non-blank line after three blank lines)
local top_subtitle = {} -- Top subtitle (first line of caption)
local bottom_subtitle = {} -- Bottom subtitle (second line of caption)
local timestamp = {} -- Combined SRT-formatted timestamp (start --> end)
-- Initialize iterator for subtitle entries.
local i = 0
local line_index = 1
local first_timestamp_skipped = false -- Flag to track if the first timestamp has been skipped.
-- Loop through the VTT lines until the end of the file.
while line_index <= #lines do
local line = lines[line_index]
-- Detect a line with a timestamp (pattern: HH:MM:SS.mmm followed by whitespace)
if string.match(line, "%d%d:%d%d:%d%d%.%d%d%d%s") then
-- If the first timestamp has not been skipped yet, skip this one.
if not first_timestamp_skipped then
first_timestamp_skipped = true
else
-- Extract and store the starting timestamp.
timestamp_str_start[i] = line:match("(%d%d:%d%d:%d%d%.%d%d%d)%s")
-- Save the length of the starting timestamp as required.
timestamp_str_start_len[i] = #timestamp_str_start[i]
-- Read the following line as the top subtitle.
line_index = line_index + 1
top_subtitle[i] = lines[line_index] or ""
-- Skip lines until three blank lines are encountered.
local blank_count = 0
while blank_count < 3 and line_index < #lines do
line_index = line_index + 1
if lines[line_index] == "" then
blank_count = blank_count + 1
end
end
-- After three blank lines, read the next line for the ending timestamp snippet.
line_index = line_index + 1
if lines[line_index] then
timestamp_str_ending[i] = lines[line_index]:match("(%d%d:%d%d:%d%d%.%d%d%d)")
else
timestamp_str_ending[i] = nil
end
-- Read the next line as the bottom subtitle.
line_index = line_index + 1
bottom_subtitle[i] = lines[line_index] or ""
-- Combine the starting and ending timestamps into proper SRT format.
-- Replace periods with commas.
if timestamp_str_start[i] and timestamp_str_ending[i] then
timestamp[i] = string.gsub(timestamp_str_start[i], "%.", ",") .. " --> " ..
string.gsub(timestamp_str_ending[i], "%.", ",")
end
-- Increase the iterator to process the next subtitle block.
i = i + 1
end
end
-- Move to the next line in the file.
line_index = line_index + 1
end
-- Write the parsed subtitles into the SRT output file.
local out = io.open(output_file, "w")
for j = 0, i - 1 do
if timestamp[j] then
-- Write the entry number.
out:write(j + 1 .. "\n")
-- Write the combined timestamp.
out:write(timestamp[j] .. "\n")
-- Write the top subtitle (first line of caption).
out:write(top_subtitle[j] .. "\n")
-- Write the bottom subtitle (second line) if it exists.
if bottom_subtitle[j] ~= "" then
out:write(bottom_subtitle[j] .. "\n")
end
-- Blank line separating entries.
out:write("\n")
end
end
out:close()
print("Conversion complete. Output saved as " .. output_file)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment