Same inaSpeechSegmenter engine split into reusable core/ (fftools, segmenter, exporters) + utils/ and a CustomTkinter multi-tab UI. Adds JSON and ffmpeg-script export. Extracted from the stream_tools app. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
189 lines
6.5 KiB
Python
189 lines
6.5 KiB
Python
"""
|
|
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),
|
|
])
|