package com.hlab.yanalyst.domain.channel; import java.util.ArrayList; import java.util.Comparator; import java.util.List; /** * 말(speech) 세그먼트로부터 "남길 구간(keep)"과 "잘라낼 구간(remove)"을 계산하는 순수 유틸. * *

오디오 무음(silencedetect)이 아니라 Whisper 세그먼트 기반으로 "아무도 말하지 않는 구간"을 * 잘라 영상을 타이트하게 만든다(배경음악이 깔려 있어도 동작). * *

* *

{@code remove} 는 첫 keep 시작 전(leading)과 keep 사이 간격만 포함한다(마지막 keep 이후 * 꼬리는 어떤 자막에도 영향을 주지 않으므로 재매핑 대상이 아니며, 영상 컷은 keep 만으로 충분). */ public final class KeepIntervalPlanner { private KeepIntervalPlanner() { } public record Plan(List keep, List remove) { } public static Plan plan(List segments, double pad, double minGap) { if (segments == null || segments.isEmpty()) { return new Plan(List.of(), List.of()); } // 1) 패딩 적용(시작 0 클램프) List padded = new ArrayList<>(segments.size()); for (ScriptSegment s : segments) { padded.add(new TimeInterval(Math.max(0, s.start() - pad), s.end() + pad)); } padded.sort(Comparator.comparingDouble(TimeInterval::start)); // 2) 겹치거나 간격이 minGap 이하면 병합 → keep List keep = new ArrayList<>(); double curStart = padded.get(0).start(); double curEnd = padded.get(0).end(); for (int i = 1; i < padded.size(); i++) { TimeInterval iv = padded.get(i); if (iv.start() - curEnd <= minGap) { curEnd = Math.max(curEnd, iv.end()); } else { keep.add(new TimeInterval(curStart, curEnd)); curStart = iv.start(); curEnd = iv.end(); } } keep.add(new TimeInterval(curStart, curEnd)); // 3) remove = [0, firstKeepStart) + keep 사이 간격 List remove = new ArrayList<>(); if (keep.get(0).start() > 1e-9) { remove.add(new TimeInterval(0.0, keep.get(0).start())); } for (int i = 1; i < keep.size(); i++) { remove.add(new TimeInterval(keep.get(i - 1).end(), keep.get(i).start())); } return new Plan(keep, remove); } /** keep 구간 총 길이 = 무음 제거 후 예상 영상 길이(배속 전). */ public static double keptDuration(Plan plan) { double total = 0; for (TimeInterval iv : plan.keep()) { total += iv.end() - iv.start(); } return total; } }