From 6a0a7294bbb2e742a31dcbfd638d04dc427cb57d Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Thu, 25 Jun 2026 14:53:42 +0900 Subject: [PATCH] =?UTF-8?q?feat(rework):=20=ED=99=94=EB=A9=B4=20=EC=9E=90?= =?UTF-8?q?=EB=A7=89=20OCR=20=EC=97=90=20=EC=9E=90=EB=A7=89=EC=98=81?= =?UTF-8?q?=EC=97=AD(crop)=20=EC=A0=84=EB=8B=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OCR 정확도·속도 향상을 위해 '자막영역 하단 N%' 입력을 추가, 플레이어 영상의 실제 해상도로 crop 픽셀(crop_x/y/width/height)을 환산해 /ocr_video 로 전달한다. 100%면 use_fullframe=true, 해상도 못 읽으면 생략(서버 기본 하단30%). - ocrFromCached(file, formParams Map)로 일반화 — 서버 필드명 그대로 전달 - CurationService/Controller에 useFullframe·cropX/Y/Width/Height 파라미터 추가 - rework.html: 자막영역 % 입력 + ocrScreen()이 영상 해상도로 crop 계산 검증: sample_fps=3 + 하단30% crop 으로 2m28s 완료(프록시 600s 적용 후 504 해소), crop 전달 정상 동작 확인. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../domain/channel/ChannelService.java | 16 ++++++++-------- .../ChannelVideoCurationController.java | 10 ++++++++-- .../channel/ChannelVideoCurationService.java | 18 ++++++++++++++++-- src/main/resources/templates/rework.html | 19 ++++++++++++++++++- 4 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java index 76dcc08..9be19b6 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelService.java @@ -452,13 +452,12 @@ public class ChannelService { * 음성 전사와 같은 자리(ChannelVideoScript)에 저장 → 스크립트 리스트/SRT/번역에 그대로 흐른다. */ @Transactional - public ScriptResponseDto ocrFromCached(Long channelVideoId, File file, String lang, - Integer sampleFps, Integer confThreshold) { + public ScriptResponseDto ocrFromCached(Long channelVideoId, File file, java.util.Map formParams) { ChannelVideo video = channelVideoRepository.findById(channelVideoId) .orElseThrow(() -> new IllegalArgumentException("Video not found: " + channelVideoId)); String apiUrl = pythonBaseUrl + "/ocr_video"; - log.info("Requesting screen-subtitle OCR for video {} (lang={}, fps={})", channelVideoId, lang, sampleFps); + log.info("Requesting screen-subtitle OCR for video {} (params={})", channelVideoId, formParams); try { HttpHeaders headers = new HttpHeaders(); @@ -466,12 +465,13 @@ public class ChannelService { MultiValueMap body = new LinkedMultiValueMap<>(); body.add("file", toFileResource(file)); - if (lang != null && !lang.isBlank()) { - body.add("lang", lang.trim()); + // 서버 필드명 그대로(lang, sample_fps, conf_threshold, use_fullframe, crop_x/y/width/height...). + // 빈 값은 보내지 않아 /ocr_video 기본값(하단30% crop, sample_fps=3, conf=60)을 따른다. + if (formParams != null) { + for (java.util.Map.Entry e : formParams.entrySet()) { + if (e.getValue() != null) body.add(e.getKey(), String.valueOf(e.getValue())); + } } - // null 이면 보내지 않아 Python /ocr_video 의 기본값(sample_fps=3, conf=60)을 따른다. - if (sampleFps != null) body.add("sample_fps", String.valueOf(sampleFps)); - if (confThreshold != null) body.add("conf_threshold", String.valueOf(confThreshold)); HttpEntity> request = new HttpEntity<>(body, headers); ResponseEntity response = pythonRestTemplate.postForEntity(apiUrl, request, String.class); diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java index d9d963b..e7f662f 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationController.java @@ -178,8 +178,14 @@ public class ChannelVideoCurationController { public ApiResponse> ocr(@PathVariable Long id, @RequestParam(value = "lang", required = false) String lang, @RequestParam(value = "sampleFps", required = false) Integer sampleFps, - @RequestParam(value = "confThreshold", required = false) Integer confThreshold) { - return ApiResponse.ok(curationService.ocrScreenSubtitles(id, lang, sampleFps, confThreshold)); + @RequestParam(value = "confThreshold", required = false) Integer confThreshold, + @RequestParam(value = "useFullframe", required = false) Boolean useFullframe, + @RequestParam(value = "cropX", required = false) Integer cropX, + @RequestParam(value = "cropY", required = false) Integer cropY, + @RequestParam(value = "cropWidth", required = false) Integer cropWidth, + @RequestParam(value = "cropHeight", required = false) Integer cropHeight) { + return ApiResponse.ok(curationService.ocrScreenSubtitles( + id, lang, sampleFps, confThreshold, useFullframe, cropX, cropY, cropWidth, cropHeight)); } @PostMapping("/{id}/translate") diff --git a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java index 90d7f5d..980de56 100644 --- a/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java +++ b/src/main/java/com/hlab/yanalyst/domain/channel/ChannelVideoCurationService.java @@ -165,12 +165,26 @@ public class ChannelVideoCurationService { * 음성 전사와 같은 자리에 저장되어 스크립트 리스트/SRT/번역에 그대로 흐른다. (기존 스크립트는 덮어씀) */ @Transactional - public Map ocrScreenSubtitles(Long videoId, String lang, Integer sampleFps, Integer confThreshold) { + public Map ocrScreenSubtitles(Long videoId, String lang, Integer sampleFps, Integer confThreshold, + Boolean useFullframe, Integer cropX, Integer cropY, + Integer cropWidth, Integer cropHeight) { ChannelVideo v = find(videoId); java.io.File file = videoDownloadService.cachedFile(v.getId()) .orElseThrow(() -> new IllegalArgumentException( "받은 원본이 없습니다. 먼저 '원본 다운로드'를 실행하세요.")); - ScriptResponseDto dto = channelService.ocrFromCached(v.getId(), file, lang, sampleFps, confThreshold); + + // /ocr_video 서버 필드명으로 폼 조립(null 은 빼서 서버 기본값 사용). + Map form = new LinkedHashMap<>(); + if (lang != null && !lang.isBlank()) form.put("lang", lang.trim()); + if (sampleFps != null) form.put("sample_fps", sampleFps); + if (confThreshold != null) form.put("conf_threshold", confThreshold); + if (useFullframe != null) form.put("use_fullframe", useFullframe); + if (cropX != null) form.put("crop_x", cropX); + if (cropY != null) form.put("crop_y", cropY); + if (cropWidth != null) form.put("crop_width", cropWidth); + if (cropHeight != null) form.put("crop_height", cropHeight); + + ScriptResponseDto dto = channelService.ocrFromCached(v.getId(), file, form); Map result = new LinkedHashMap<>(); result.put("hasScript", true); diff --git a/src/main/resources/templates/rework.html b/src/main/resources/templates/rework.html index 9227613..de5f173 100644 --- a/src/main/resources/templates/rework.html +++ b/src/main/resources/templates/rework.html @@ -152,6 +152,11 @@ + 자막영역 하단 + + % @@ -646,7 +651,19 @@ status.textContent = '화면 자막 OCR 중… (프레임 분석, 영상 길이에 따라 수십 초~)'; try { const lang = document.getElementById('langSel').value || 'ko'; - const s = await api(API + '/' + VIDEO_ID + '/ocr?lang=' + encodeURIComponent(lang), { method:'POST' }); + let q = '?lang=' + encodeURIComponent(lang); + // 자막영역(하단 N%)을 영상 실제 해상도로 환산해 crop 전달 → 정확도·속도↑ + const pct = Math.min(100, Math.max(10, parseInt(document.getElementById('ocrCropPct').value) || 30)); + const v = document.getElementById('localVideo'); + const vw = v ? v.videoWidth : 0, vh = v ? v.videoHeight : 0; + if (pct >= 100) { + q += '&useFullframe=true'; + } else if (vw > 0 && vh > 0) { + const ch = Math.round(vh * pct / 100); + q += '&useFullframe=false&cropX=0&cropWidth=' + vw + '&cropHeight=' + ch + '&cropY=' + (vh - ch); + } + // 영상 해상도를 못 읽으면 crop 생략 → 서버 기본(하단 30%) 적용 + const s = await api(API + '/' + VIDEO_ID + '/ocr' + q, { method:'POST' }); renderSegments(s.segments); document.getElementById('transcript').value = s.transcript || ''; status.style.color = '#4ade80';