""" src/export/edl_writer.py — EditTimeline → CMX 3600 EDL Generates a standard CMX 3600 Edit Decision List compatible with Avid, DaVinci Resolve, Premiere Pro, and most NLEs. CMX 3600 format reference: https://en.wikipedia.org/wiki/Edit_decision_list#CMX_3600 """ from __future__ import annotations import logging from pathlib import Path from src.core.config import AppConfig from src.core.models import EditClip, EditTimeline from src.export.timecode import seconds_to_smpte logger = logging.getLogger(__name__) # --------------------------------------------------------------------------- # EDL line builders # --------------------------------------------------------------------------- def _edl_header(title: str) -> str: return f"TITLE: {title}\nFCM: NON-DROP FRAME\n" def _edl_event( event_num: int, clip: EditClip, fps: float, ) -> str: """ Build one CMX 3600 event block for a single clip. Format: NNN AX V C * FROM CLIP NAME: ... * COMMENT: ... """ src_in = seconds_to_smpte(clip.match.in_point_s, fps) source_duration_s = clip.source_timeline_duration_s src_out = seconds_to_smpte(clip.match.in_point_s + source_duration_s, fps) rec_in = seconds_to_smpte(clip.timeline_start_s, fps) rec_out = seconds_to_smpte(clip.timeline_start_s + source_duration_s, fps) event_line = f"{event_num:03d} AX V C {src_in} {src_out} {rec_in} {rec_out}" name_line = f"* FROM CLIP NAME: {clip.match.source_path.name}" comment_line = ( f"* BEAT {clip.beat.beat_id:03d} | {clip.beat.beat_type.name} | " f"score={clip.match.match_score:.3f}" ) return "\n".join([event_line, name_line, comment_line, ""]) def _edl_black_tail_event(event_num: int, clip: EditClip, fps: float) -> str: rec_in = seconds_to_smpte(clip.timeline_start_s + clip.source_timeline_duration_s, fps) rec_out = seconds_to_smpte(clip.timeline_end_s, fps) event_line = f"{event_num:03d} BL V C 00:00:00:00 00:00:00:00 {rec_in} {rec_out}" comment_line = ( f"* BEAT {clip.beat.beat_id:03d} TRAILER-ONLY TAIL | " "add fade/dissolve to black" ) return "\n".join([event_line, "* FROM CLIP NAME: BLACK", comment_line, ""]) # --------------------------------------------------------------------------- # Public API # --------------------------------------------------------------------------- def write_edl( timeline: EditTimeline, cfg: AppConfig, output_path: Path | None = None, ) -> Path: """ Write the EditTimeline as a CMX 3600 EDL file. Args: timeline: EditTimeline from build_timeline(). cfg: Application configuration. output_path: Override destination. Defaults to /.edl. Returns: Path to the written .edl file. """ if output_path is None: output_path = cfg.paths.output_dir / f"{timeline.title}.edl" output_path.parent.mkdir(parents=True, exist_ok=True) fps = timeline.frame_rate lines = [_edl_header(timeline.title), "\n"] event_num = 1 for clip in sorted(timeline.clips, key=lambda c: c.clip_index): lines.append(_edl_event(event_num, clip, fps)) event_num += 1 if clip.trailer_tail_s > 0: lines.append("\n") lines.append(_edl_black_tail_event(event_num, clip, fps)) event_num += 1 lines.append("\n") edl_text = "\n".join(lines) output_path.write_text(edl_text, encoding="utf-8") logger.info("EDL written → %s (%d events)", output_path, timeline.clip_count) return output_path