Last active
February 28, 2025 05:41
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-- 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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