feat(rework): 한국어 SRT 내보내기 추가
세그먼트를 한국어로 일괄번역해 기존 타임라인(타임코드·배속·말없는구간 제거)에 그대로 끼워 SRT로 내보낸다. 원본 SRT와 별개로 '한국어 SRT' 버튼 추가. - translateScript format=segments: 세그먼트와 1:1 정렬된 번역 배열 반환 - rework.html exportKoSrt(): texts 배열을 workingSegments에 매핑해 buildSrt로 생성 (텍스트만 한국어로 교체, 타임코드/배속은 기존 로직 재사용) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
198334c6f8
commit
e9ad800134
@ -188,9 +188,13 @@ public class ChannelVideoCurationService {
|
|||||||
String source = scriptOpt.map(ChannelVideoScript::getLanguage).orElse(null);
|
String source = scriptOpt.map(ChannelVideoScript::getLanguage).orElse(null);
|
||||||
String tgt = (target == null || target.isBlank()) ? "ko" : target.trim();
|
String tgt = (target == null || target.isBlank()) ? "ko" : target.trim();
|
||||||
boolean timeline = "timeline".equalsIgnoreCase(format);
|
boolean timeline = "timeline".equalsIgnoreCase(format);
|
||||||
|
boolean segmentsMode = "segments".equalsIgnoreCase(format); // SRT용: 세그먼트별 번역 배열 반환
|
||||||
|
|
||||||
String translated;
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
if (timeline) {
|
result.put("source", source);
|
||||||
|
result.put("target", tgt);
|
||||||
|
|
||||||
|
if (timeline || segmentsMode) {
|
||||||
List<ScriptSegment> segments = channelService.getSegments(v.getId());
|
List<ScriptSegment> segments = channelService.getSegments(v.getId());
|
||||||
if (segments.isEmpty()) {
|
if (segments.isEmpty()) {
|
||||||
throw new IllegalArgumentException("타임라인(시간 싱크 세그먼트)이 없습니다. 먼저 원본 다운로드/업로드로 전사하세요.");
|
throw new IllegalArgumentException("타임라인(시간 싱크 세그먼트)이 없습니다. 먼저 원본 다운로드/업로드로 전사하세요.");
|
||||||
@ -198,26 +202,28 @@ public class ChannelVideoCurationService {
|
|||||||
List<String> texts = segments.stream().map(ScriptSegment::text).toList();
|
List<String> texts = segments.stream().map(ScriptSegment::text).toList();
|
||||||
List<String> ko = translateService.translateBatch(texts, source, tgt);
|
List<String> ko = translateService.translateBatch(texts, source, tgt);
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
if (segmentsMode) {
|
||||||
for (int i = 0; i < segments.size(); i++) {
|
// 입력 세그먼트와 1:1 정렬된 번역 배열(프론트가 기존 SRT 로직에 끼워 넣음)
|
||||||
String line = (i < ko.size()) ? ko.get(i) : "";
|
result.put("format", "segments");
|
||||||
sb.append('[').append(mmss(segments.get(i).start())).append("] ")
|
result.put("texts", ko);
|
||||||
.append(line == null ? "" : line.trim()).append('\n');
|
} else {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < segments.size(); i++) {
|
||||||
|
String line = (i < ko.size()) ? ko.get(i) : "";
|
||||||
|
sb.append('[').append(mmss(segments.get(i).start())).append("] ")
|
||||||
|
.append(line == null ? "" : line.trim()).append('\n');
|
||||||
|
}
|
||||||
|
result.put("format", "timeline");
|
||||||
|
result.put("translatedText", sb.toString().stripTrailing());
|
||||||
}
|
}
|
||||||
translated = sb.toString().stripTrailing();
|
|
||||||
} else {
|
} else {
|
||||||
String text = scriptOpt.map(ChannelVideoScript::getTranscript).orElse(null);
|
String text = scriptOpt.map(ChannelVideoScript::getTranscript).orElse(null);
|
||||||
if (text == null || text.isBlank()) {
|
if (text == null || text.isBlank()) {
|
||||||
throw new IllegalArgumentException("번역할 원본 스크립트가 없습니다. 먼저 전사(원본 다운로드/업로드)하세요.");
|
throw new IllegalArgumentException("번역할 원본 스크립트가 없습니다. 먼저 전사(원본 다운로드/업로드)하세요.");
|
||||||
}
|
}
|
||||||
translated = translateService.translate(text, source, tgt);
|
result.put("format", "plain");
|
||||||
|
result.put("translatedText", translateService.translate(text, source, tgt));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
|
||||||
result.put("translatedText", translated);
|
|
||||||
result.put("source", source);
|
|
||||||
result.put("target", tgt);
|
|
||||||
result.put("format", timeline ? "timeline" : "plain");
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -201,6 +201,9 @@
|
|||||||
<button class="btn btn-primary px-3 py-2 flex items-center gap-1" onclick="exportSrt()">
|
<button class="btn btn-primary px-3 py-2 flex items-center gap-1" onclick="exportSrt()">
|
||||||
<i data-lucide="file-down" style="width:15px;"></i> SRT 내보내기
|
<i data-lucide="file-down" style="width:15px;"></i> SRT 내보내기
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn btn-secondary px-3 py-2 flex items-center gap-1" id="koSrtBtn" onclick="exportKoSrt()" title="세그먼트를 한국어로 번역해 타임코드 맞춰 SRT로 내보내기">
|
||||||
|
<i data-lucide="languages" style="width:15px;"></i> 한국어 SRT
|
||||||
|
</button>
|
||||||
<button class="btn btn-secondary px-3 py-2 flex items-center gap-1" id="renderBtn" onclick="renderVideo()">
|
<button class="btn btn-secondary px-3 py-2 flex items-center gap-1" id="renderBtn" onclick="renderVideo()">
|
||||||
<i data-lucide="scissors" style="width:15px;"></i> 영상 내보내기
|
<i data-lucide="scissors" style="width:15px;"></i> 영상 내보내기
|
||||||
</button>
|
</button>
|
||||||
@ -407,6 +410,25 @@
|
|||||||
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href);
|
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 세그먼트를 한국어로 번역해(서버 일괄번역) 기존 타임라인(배속·말없는구간 제거 반영)에 끼워 SRT 생성.
|
||||||
|
async function exportKoSrt(){
|
||||||
|
const btn = document.getElementById('koSrtBtn');
|
||||||
|
const orig = btn.innerHTML; btn.disabled = true; btn.innerHTML = '번역 중…';
|
||||||
|
try {
|
||||||
|
const r = await api(API + '/' + VIDEO_ID + '/translate?target=ko&format=segments', { method:'POST' });
|
||||||
|
const ko = (r && r.texts) || [];
|
||||||
|
if(!ko.length){ alert('번역할 세그먼트가 없습니다. 먼저 전사하세요.'); return; }
|
||||||
|
// 현재 작업 세그먼트(원본/무음제거 후)와 1:1 정렬 → 텍스트만 한국어로 교체(타임코드·배속 그대로)
|
||||||
|
const segs = workingSegments();
|
||||||
|
const koSegs = segs.map((sg, i) => ({ start: sg.start, end: sg.end, text: (ko[i] != null ? ko[i] : (sg.text || '')) }));
|
||||||
|
const blob = new Blob([buildSrt(koSegs, curSpeed())], {type:'application/x-subrip'});
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(blob); a.download = 'script_ko_' + VIDEO_ID + '.srt';
|
||||||
|
document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(a.href);
|
||||||
|
} catch(e){ alert('한국어 SRT 실패: ' + e.message); }
|
||||||
|
finally { btn.disabled = false; btn.innerHTML = orig; if(window.lucide) lucide.createIcons(); }
|
||||||
|
}
|
||||||
|
|
||||||
// ===== 말 없는 구간 제거 =====
|
// ===== 말 없는 구간 제거 =====
|
||||||
function onTrimToggle(){
|
function onTrimToggle(){
|
||||||
if(document.getElementById('trimOn').checked){ previewTrim(); }
|
if(document.getElementById('trimOn').checked){ previewTrim(); }
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user