""" Export singing segments to various formats: - EDL (DaVinci Resolve timeline import) - CSV (human-readable) - DaVinci Resolve Markers CSV - JSON (programmatic) - ffmpeg concat script (auto-cut) """ import csv import json import os import sys from pathlib import Path from utils.formats import format_timecode, seconds_to_smpte def export_edl(segments: list, output_path: str, fps: float = 29.97, title: str = "Singing Segments", source_filename: str = None): """Export EDL file for DaVinci Resolve.""" if source_filename: clip_name = source_filename reel_name_safe = Path(source_filename).stem[:32].replace(" ", "_") else: clip_name = None reel_name_safe = "001" rec_offset = 3600.0 with open(output_path, "w", encoding="utf-8") as f: f.write(f"TITLE: {title}\n") f.write("FCM: NON-DROP FRAME\n\n") current_rec_pos = rec_offset for seg in segments: edit_num = f"{seg['index']:03d}" src_in = seconds_to_smpte(seg["start"], fps) src_out = seconds_to_smpte(seg["end"], fps) rec_in = seconds_to_smpte(current_rec_pos, fps) rec_out = seconds_to_smpte(current_rec_pos + seg["duration"], fps) f.write(f"{edit_num} {reel_name_safe} V C {src_in} {src_out} {rec_in} {rec_out}\n") if clip_name: f.write(f"* FROM CLIP NAME: {clip_name}\n") f.write(f"* COMMENT: Song {seg['index']} - Duration {seg['duration']:.0f}s\n\n") current_rec_pos += seg["duration"] def export_csv(segments: list, output_path: str): """Export singing segments as CSV.""" with open(output_path, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["index", "start_sec", "end_sec", "duration_sec", "start_timecode", "end_timecode"]) for seg in segments: writer.writerow([ seg["index"], f"{seg['start']:.2f}", f"{seg['end']:.2f}", f"{seg['duration']:.1f}", format_timecode(seg["start"]), format_timecode(seg["end"]), ]) def export_markers_csv(segments: list, output_path: str, fps: float = 29.97): """Export DaVinci Resolve Markers CSV.""" with open(output_path, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["#", "Color", "Name", "Start TC", "End TC", "Duration TC", "Notes"]) for seg in segments: writer.writerow([ seg["index"], "Blue", f"Song {seg['index']}", seconds_to_smpte(seg["start"], fps), seconds_to_smpte(seg["end"], fps), seconds_to_smpte(seg["duration"], fps), f"Duration: {seg['duration']:.0f}s", ]) def export_json(segments: list, output_path: str): """Export as JSON.""" with open(output_path, "w", encoding="utf-8") as f: json.dump(segments, f, indent=2, ensure_ascii=False) def export_ffmpeg_script(segments: list, video_path: str, output_dir: str): """ Export a script that uses ffmpeg to cut and concatenate all singing segments into a single video file. """ video_name = Path(video_path).stem ext = Path(video_path).suffix out_video = str(Path(output_dir) / f"{video_name}_singing_only{ext}") is_windows = os.name == "nt" or sys.platform == "win32" script_ext = ".bat" if is_windows else ".sh" script_path = str(Path(output_dir) / f"{video_name}_autocut{script_ext}") concat_list_path = str(Path(output_dir) / f"{video_name}_concat_list.txt") lines = [] if is_windows: lines.append("@echo off") lines.append("REM Auto-generated ffmpeg script to extract singing segments") lines.append(f'REM Source: {video_path}') lines.append("") else: lines.append("#!/bin/bash") lines.append("# Auto-generated ffmpeg script to extract singing segments") lines.append(f'# Source: {video_path}') lines.append("") segment_files = [] for seg in segments: seg_file = f"_seg_{seg['index']:03d}{ext}" seg_path = str(Path(output_dir) / seg_file) segment_files.append(seg_file) cmd = (f'ffmpeg -y -ss {seg["start"]:.2f} -i "{video_path}" ' f'-t {seg["duration"]:.2f} -c copy "{seg_path}"') lines.append(f"echo Extracting Song {seg['index']}...") lines.append(cmd) lines.append("") lines.append("echo Creating concat list...") if is_windows: lines.append("(") for sf in segment_files: seg_path = str(Path(output_dir) / sf) lines.append(f" echo file '{seg_path}'") lines.append(f') > "{concat_list_path}"') else: for i, sf in enumerate(segment_files): seg_path = str(Path(output_dir) / sf) op = ">" if i == 0 else ">>" lines.append(f"echo \"file '{seg_path}'\" {op} \"{concat_list_path}\"") lines.append("") lines.append("echo Concatenating all segments...") lines.append(f'ffmpeg -y -f concat -safe 0 -i "{concat_list_path}" -c copy "{out_video}"') lines.append("") lines.append("echo Cleaning up temp files...") for sf in segment_files: seg_path = str(Path(output_dir) / sf) if is_windows: lines.append(f'del "{seg_path}"') else: lines.append(f'rm -f "{seg_path}"') if is_windows: lines.append(f'del "{concat_list_path}"') else: lines.append(f'rm -f "{concat_list_path}"') lines.append("") lines.append(f'echo Done! Output: {out_video}') if is_windows: lines.append("pause") with open(script_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) if not is_windows: os.chmod(script_path, 0o755) return script_path def export_raw_segments(segments: list, output_path: str): """Export ALL raw segments (speech/music/noise) as CSV for debugging.""" with open(output_path, "w", newline="", encoding="utf-8") as f: writer = csv.writer(f) writer.writerow(["label", "start_sec", "end_sec", "duration_sec", "start_timecode", "end_timecode"]) for label, start, end in segments: writer.writerow([ label, f"{start:.2f}", f"{end:.2f}", f"{end - start:.1f}", format_timecode(start), format_timecode(end), ])