package com.hlab.yanalyst.domain.channel; import java.util.ArrayList; import java.util.Comparator; import java.util.List; /** * 무음 구간 제거에 따른 자막 타임스탬프 재매핑(순수 유틸). * *

무음을 잘라낸 뒤 남는 구간을 이어붙이면 전체 타임라인이 앞으로 당겨진다. * 원본 시각 {@code t} 는 그 앞에 제거된 무음 길이만큼 빼서 새 시각으로 매핑된다: * {@code f(t) = t - (t 이전에 제거된 무음 총량)}. * *

배속은 여기서 적용하지 않는다. 재매핑은 "무음 제거 후, 배속 전" 타임라인을 만들고, * 배속은 {@link SrtFormatter}(÷speed)와 ffmpeg(atempo) 단계에서 일관되게 적용한다. */ public final class TimelineRemapper { private TimelineRemapper() { } public static List remap(List segments, List silences) { if (segments == null || segments.isEmpty()) { return List.of(); } List merged = merge(silences); List out = new ArrayList<>(segments.size()); for (ScriptSegment seg : segments) { double ns = mapPoint(seg.start(), merged); double ne = mapPoint(seg.end(), merged); if (ne < ns) { ne = ns; } out.add(new ScriptSegment(ns, ne, seg.text())); } return out; } /** 원본 길이에서 (병합된) 무음 총량을 뺀 새 전체 길이. */ public static double newDuration(double originalDuration, List silences) { double removed = 0; for (TimeInterval s : merge(silences)) { double a = Math.max(0, s.start()); double b = Math.min(originalDuration, s.end()); if (b > a) { removed += b - a; } } return Math.max(0, originalDuration - removed); } /** f(t) = t - (t 이전에 제거된 무음 총량). 무음 내부의 점은 그 무음 시작으로 클램프된다. */ private static double mapPoint(double t, List merged) { double removed = 0; for (TimeInterval s : merged) { if (s.end() <= t) { removed += s.end() - s.start(); } else if (s.start() < t) { removed += t - s.start(); // t 가 무음 내부 → 경계로 클램프 break; } else { break; // 정렬되어 있으니 이후는 t 이후 } } return t - removed; } /** 겹치거나 정렬 안 된 구간을 정렬·병합한다. */ private static List merge(List silences) { if (silences == null || silences.isEmpty()) { return List.of(); } List sorted = new ArrayList<>(silences); sorted.sort(Comparator.comparingDouble(TimeInterval::start)); List merged = new ArrayList<>(); double curStart = sorted.get(0).start(); double curEnd = sorted.get(0).end(); for (int i = 1; i < sorted.size(); i++) { TimeInterval iv = sorted.get(i); if (iv.start() <= curEnd) { curEnd = Math.max(curEnd, iv.end()); } else { merged.add(new TimeInterval(curStart, curEnd)); curStart = iv.start(); curEnd = iv.end(); } } merged.add(new TimeInterval(curStart, curEnd)); return merged; } }