Skip to content

Instantly share code, notes, and snippets.

@chand1012
Last active February 28, 2025 05:41
Show Gist options
  • Save chand1012/72987804cfc0b9f201b2f6e7d086bedc to your computer and use it in GitHub Desktop.
Save chand1012/72987804cfc0b9f201b2f6e7d086bedc to your computer and use it in GitHub Desktop.
Mark timestamps to a CSV file with OBS on a hotkey. Use the Python file to process that CSV and recording into clips. https://chand1012.mit-license.org
-- OBS Timestamp Marker Script
-- Allows marking timestamps during recording and saves them to a CSV file
-- Set a hotkey to mark moments in your recording that you can review later
obs = obslua
local marks = {}
local recording_start_time = 0
local settings_hotkey = ""
local settings_directory = ""
-- Function to mark the current timestamp
function mark_timestamp()
if not obs.obs_frontend_recording_active() then
obs.script_log(obs.LOG_INFO, "Recording is not active. Timestamp not marked.")
return
end
local current_time = os.time()
local elapsed_seconds = current_time - recording_start_time
-- Format time as HH:MM:SS
local hours = math.floor(elapsed_seconds / 3600)
local minutes = math.floor((elapsed_seconds % 3600) / 60)
local seconds = elapsed_seconds % 60
local formatted_time = string.format("%02d:%02d:%02d", hours, minutes, seconds)
-- Add timestamp to the marks table
table.insert(marks, {
timestamp = elapsed_seconds,
formatted = formatted_time,
real_time = os.date("%Y-%m-%d %H:%M:%S")
})
-- Log the timestamp
obs.script_log(obs.LOG_INFO, "Timestamp marked at " .. formatted_time)
end
-- Function to save all timestamps to a CSV file
function save_timestamps_to_csv()
if #marks == 0 then
obs.script_log(obs.LOG_WARNING, "No timestamps to save.")
return false
end
-- Use the user-specified directory for the file path
local directory = settings_directory
if directory == "" then
directory = script_path() -- Default to script directory
end
-- Generate the filename in the same format as OBS (no spaces)
local date_str = os.date("%Y-%m-%d_%H-%M-%S")
local filepath = directory .. "/timestamps_" .. date_str .. ".csv"
-- Open the file for writing
local file = io.open(filepath, "w")
if file == nil then
obs.script_log(obs.LOG_ERROR, "Could not open file for writing: " .. filepath)
return false
end
-- Write CSV header
file:write("Timestamp (seconds),Formatted Time,Real Time\n")
-- Write each timestamp
for _, mark in ipairs(marks) do
file:write(mark.timestamp .. "," .. mark.formatted .. "," .. mark.real_time .. "\n")
end
file:close()
obs.script_log(obs.LOG_INFO, "Timestamps saved to " .. filepath)
return true
end
-- Hook into recording events
function on_event(event)
if event == obs.OBS_FRONTEND_EVENT_RECORDING_STARTED then
-- Reset marks and record the start time when recording begins
marks = {}
recording_start_time = os.time()
obs.script_log(obs.LOG_INFO, "Recording started. Timestamp marking enabled.")
elseif event == obs.OBS_FRONTEND_EVENT_RECORDING_STOPPED then
-- Save timestamps when recording ends
if #marks > 0 then
save_timestamps_to_csv()
end
end
end
-- Script hook registration
function script_description()
return "Timestamp Marker: Mark interesting moments during recording with a hotkey and save them to a CSV file."
end
function script_properties()
local props = obs.obs_properties_create()
-- Add hotkey setting
obs.obs_properties_add_text(props, "hotkey", "Hotkey Description (set the actual key below)", obs.OBS_TEXT_DEFAULT)
-- Add directory setting
local p = obs.obs_properties_add_path(props, "directory", "Directory for CSV Save", obs.OBS_PATH_DIRECTORY, "", nil)
obs.obs_property_set_long_description(p, "Choose the directory where the CSV file will be saved. The filename will be generated automatically.")
-- Add test buttons
obs.obs_properties_add_button(props, "button_mark", "Test: Mark Timestamp", function()
mark_timestamp()
return true
end)
obs.obs_properties_add_button(props, "button_save", "Test: Save Timestamps", function()
save_timestamps_to_csv()
return true
end)
return props
end
function script_update(settings)
settings_hotkey = obs.obs_data_get_string(settings, "hotkey")
settings_directory = obs.obs_data_get_string(settings, "directory")
end
function script_save(settings)
-- Automatically save timestamps when settings are saved (if needed)
if #marks > 0 then
save_timestamps_to_csv()
end
local hotkey_save_array = obs.obs_hotkey_save(hotkey_id)
obs.obs_data_set_array(settings, "mark_timestamp_hotkey", hotkey_save_array)
obs.obs_data_array_release(hotkey_save_array)
end
function script_load(settings)
-- Set up the hotkey
hotkey_id = obs.obs_hotkey_register_frontend("mark_timestamp_trig", "Mark Current Timestamp", mark_timestamp)
local hotkey_save_array = obs.obs_data_get_array(settings, "mark_timestamp_hotkey")
obs.obs_hotkey_load(hotkey_id, hotkey_save_array)
obs.obs_data_array_release(hotkey_save_array)
-- Register callback for recording events
obs.obs_frontend_add_event_callback(on_event)
end
function script_unload()
obs.obs_frontend_remove_event_callback(on_event)
end
#!/usr/bin/env python3
import argparse
import csv
import os
import subprocess
def parse_arguments():
parser = argparse.ArgumentParser(description='Cut video into clips based on timestamps in a CSV file.')
parser.add_argument('video', help='Path to the video file')
parser.add_argument('csv', help='Path to the CSV file containing timestamps')
parser.add_argument('--output', '-o', default='output_clips',
help='Output directory for the clips (default: output_clips)')
parser.add_argument('--start', '-s', type=float, default=120.0,
help='Seconds to start before each timestamp (default: 120.0)')
parser.add_argument('--end', '-e', type=float, default=15.0,
help='Seconds to end after each timestamp (default: 15.0)')
return parser.parse_args()
def read_timestamps(csv_path):
timestamps = []
with open(csv_path, 'r', newline='') as csvfile:
reader = csv.DictReader(csvfile)
# Check if the required column exists
if 'Timestamp (seconds)' not in reader.fieldnames:
raise ValueError("CSV must contain a 'Timestamp (seconds)' column")
# Read all timestamps and convert to float
for row in reader:
try:
timestamp = float(row['Timestamp (seconds)'])
timestamps.append(timestamp)
except ValueError:
print(f"Warning: Skipping invalid timestamp value: {row['Timestamp (seconds)']}")
# Remove duplicates while preserving order
unique_timestamps = []
seen = set()
for ts in timestamps:
if ts not in seen:
seen.add(ts)
unique_timestamps.append(ts)
# Sort timestamps
unique_timestamps.sort()
return unique_timestamps
def merge_overlapping_segments(segments):
if not segments:
return []
# Sort segments by start time
sorted_segments = sorted(segments, key=lambda x: x[0])
merged = [sorted_segments[0]]
for current in sorted_segments[1:]:
previous = merged[-1]
# If current segment starts before previous ends, merge them
if current[0] <= previous[1]:
merged[-1] = (previous[0], max(previous[1], current[1]))
else:
merged.append(current)
return merged
def cut_video(video_path, output_dir, segments, segment_padding=0.1):
"""Cut video into segments using ffmpeg with a small padding to avoid encoding issues"""
if not os.path.exists(output_dir):
os.makedirs(output_dir)
# Get the file extension from the original video
video_name, extension = os.path.splitext(os.path.basename(video_path))
for i, (start, end) in enumerate(segments):
# Ensure we don't have negative start times
start_time = max(0, start - segment_padding)
duration = end - start_time + segment_padding
output_file = os.path.join(output_dir, f"{video_name}_clip_{i+1}_{start:.2f}_{end:.2f}{extension}")
command = [
'ffmpeg',
'-i', video_path,
'-ss', str(start_time),
'-t', str(duration),
'-c:v', 'copy',
'-c:a', 'copy',
'-avoid_negative_ts', '1',
'-y',
output_file
]
try:
subprocess.run(command, check=True, stderr=subprocess.PIPE)
print(f"Created clip {i+1}: {output_file}")
except subprocess.CalledProcessError as e:
print(f"Error creating clip {i+1}: {e.stderr.decode()}")
def main():
args = parse_arguments()
# Check if files exist
if not os.path.isfile(args.video):
print(f"Error: Video file '{args.video}' does not exist")
return
if not os.path.isfile(args.csv):
print(f"Error: CSV file '{args.csv}' does not exist")
return
try:
# Read timestamps from CSV
timestamps = read_timestamps(args.csv)
if not timestamps:
print("No timestamps found in the CSV file")
return
# Create segments with start and end offsets
segments = [(max(0, t - args.start), t + args.end) for t in timestamps]
# Merge overlapping segments
merged_segments = merge_overlapping_segments(segments)
print(f"Found {len(timestamps)} unique timestamps, resulting in {len(merged_segments)} clips after merging overlaps")
# Cut the video
cut_video(args.video, args.output, merged_segments)
print(f"All clips saved to directory: {args.output}")
except Exception as e:
print(f"Error: {str(e)}")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment