Created
August 2, 2026 04:48
-
-
Save jordancrawford/2f97f7cbc20444fa36287a62bde20422 to your computer and use it in GitHub Desktop.
Video chapter CSV to text
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 | |
| # Converts a CSV of video chapters into a text file, converting HH:MM:SS:FF to HH:MM:SS.ms | |
| # From https://jc.kiwi/modernising-camcorder-footage/ | |
| # | |
| # Run with: | |
| # ./clips_csv_to_txt.py input.csv output.txt --framerate 50 | |
| import argparse | |
| import csv | |
| from pathlib import Path | |
| from typing import List | |
| def parse_args() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description="Convert a clip CSV directly into the plain-text timecode list format." | |
| ) | |
| parser.add_argument("csv_path", help="Path to the input CSV file") | |
| parser.add_argument("output_path", help="Path to the output .txt file") | |
| parser.add_argument( | |
| "--framerate", | |
| required=True, | |
| type=float, | |
| help="Source framerate used to convert frame numbers to milliseconds", | |
| ) | |
| return parser.parse_args() | |
| def format_timecode(timecode: str, framerate: float) -> str: | |
| parts = timecode.split(":") | |
| if len(parts) != 4: | |
| raise ValueError(f"Invalid timecode '{timecode}'. Expected HH:MM:SS:FF format.") | |
| hours, minutes, seconds, frames = (int(part) for part in parts) | |
| milliseconds = round((frames / framerate) * 1000) | |
| milliseconds = min(milliseconds, 999) | |
| return f"{hours:02d}:{minutes:02d}:{seconds:02d}.{milliseconds:03d}" | |
| def build_lines(csv_path: Path, framerate: float) -> List[str]: | |
| lines = [] | |
| with csv_path.open(newline="", encoding="utf-8-sig") as handle: | |
| reader = csv.DictReader(handle) | |
| for row in reader: | |
| name = (row.get("Name") or "").strip() | |
| in_timecode = (row.get("Timecode In") or "").strip() | |
| if not name: | |
| raise ValueError("Each CSV row must include a Name value.") | |
| if not in_timecode: | |
| raise ValueError("Each CSV row must include a Timecode In value.") | |
| formatted_timecode = format_timecode(in_timecode, framerate) | |
| lines.append(f"{formatted_timecode} {name}") | |
| return lines | |
| def main() -> int: | |
| args = parse_args() | |
| csv_path = Path(args.csv_path) | |
| output_path = Path(args.output_path) | |
| if not csv_path.exists(): | |
| raise SystemExit(f"CSV file not found: {csv_path}") | |
| if args.framerate <= 0: | |
| raise SystemExit("Framerate must be a positive number") | |
| lines = build_lines(csv_path, args.framerate) | |
| output_path.parent.mkdir(parents=True, exist_ok=True) | |
| output_path.write_text("\n".join(lines) + "\n", encoding="utf-8") | |
| print(f"Written to {output_path}") | |
| return 0 | |
| if __name__ == "__main__": | |
| raise SystemExit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment