fix(rework): long-op timeout, dedup query, clearer error, logging

Review follow-ups (safe fixes):
- pythonRestTemplate bean (10min read timeout) for /transcribe and /render so
  ffmpeg encoding / Whisper don't hit the shared 120s timeout; resolved by bean
  name so existing restTemplate injections are unaffected
- getScriptData: fetch the script row once instead of 3 separate queries
- renderTrimmed "no segments" now IllegalArgumentException (400 + visible message)
  instead of IllegalStateException swallowed as generic 500
- ProductionService: System.out.println -> log.info (3x)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hehihoho3@gmail.com 2026-06-12 20:06:00 +09:00
parent 8178d45209
commit fa6342f97e
4 changed files with 27 additions and 13 deletions

View File

@ -34,6 +34,7 @@ public class ChannelService {
private final ChannelRepository channelRepository; private final ChannelRepository channelRepository;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final RestTemplate pythonRestTemplate; // 전사/렌더 장시간 호출용( read timeout). 이름으로 구분 주입.
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final ChannelVideoRepository channelVideoRepository; private final ChannelVideoRepository channelVideoRepository;
@ -410,7 +411,7 @@ public class ChannelService {
} }
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers); HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<String> response = restTemplate.postForEntity(apiUrl, request, String.class); ResponseEntity<String> response = pythonRestTemplate.postForEntity(apiUrl, request, String.class);
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
throw new RuntimeException("Transcribe API failed with status: " + response.getStatusCode()); throw new RuntimeException("Transcribe API failed with status: " + response.getStatusCode());
@ -479,7 +480,8 @@ public class ChannelService {
public byte[] renderTrimmed(Long channelVideoId, MultipartFile file, double pad, double minGap, double speed) { public byte[] renderTrimmed(Long channelVideoId, MultipartFile file, double pad, double minGap, double speed) {
List<ScriptSegment> segments = getSegments(channelVideoId); List<ScriptSegment> segments = getSegments(channelVideoId);
if (segments.isEmpty()) { if (segments.isEmpty()) {
throw new IllegalStateException("세그먼트가 없습니다. 먼저 전사를 실행하세요."); // 사용자에게 이유가 보이도록 400(IllegalArgumentException GlobalExceptionHandler 메시지 노출).
throw new IllegalArgumentException("세그먼트가 없습니다. 먼저 영상 업로드·전사를 실행하세요.");
} }
KeepIntervalPlanner.Plan plan = KeepIntervalPlanner.plan(segments, pad, minGap); KeepIntervalPlanner.Plan plan = KeepIntervalPlanner.plan(segments, pad, minGap);
@ -494,7 +496,7 @@ public class ChannelService {
body.add("speed", String.valueOf(speed)); body.add("speed", String.valueOf(speed));
HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers); HttpEntity<MultiValueMap<String, Object>> request = new HttpEntity<>(body, headers);
ResponseEntity<byte[]> response = restTemplate.postForEntity(apiUrl, request, byte[].class); ResponseEntity<byte[]> response = pythonRestTemplate.postForEntity(apiUrl, request, byte[].class);
if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) { if (!response.getStatusCode().is2xxSuccessful() || response.getBody() == null) {
throw new RuntimeException("Render API failed with status: " + response.getStatusCode()); throw new RuntimeException("Render API failed with status: " + response.getStatusCode());
} }

View File

@ -91,17 +91,16 @@ public class ChannelVideoCurationService {
/** 스크립트 전체(평문 + 영상 싱크 세그먼트) 조회. 에디터의 세그먼트 리스트용. */ /** 스크립트 전체(평문 + 영상 싱크 세그먼트) 조회. 에디터의 세그먼트 리스트용. */
public Map<String, Object> getScriptData(Long videoId) { public Map<String, Object> getScriptData(Long videoId) {
ChannelVideo v = find(videoId); ChannelVideo v = find(videoId);
String transcript = channelVideoScriptRepository.findFirstByVideoIdOrderByIdDesc(v.getVideoId()) ChannelVideoScript script = channelVideoScriptRepository
.map(ChannelVideoScript::getTranscript) .findFirstByVideoIdOrderByIdDesc(v.getVideoId())
.orElse(null); .orElse(null);
String language = channelVideoScriptRepository.findFirstByVideoIdOrderByIdDesc(v.getVideoId()) String transcript = (script != null) ? script.getTranscript() : null;
.map(ChannelVideoScript::getLanguage) List<ScriptSegment> segments = (script != null)
.orElse(null); ? channelService.parseSegments(script.getSegmentsJson()) : List.of();
List<ScriptSegment> segments = channelService.getSegments(v.getId());
Map<String, Object> result = new LinkedHashMap<>(); Map<String, Object> result = new LinkedHashMap<>();
result.put("hasScript", transcript != null); result.put("hasScript", transcript != null);
result.put("language", language); result.put("language", (script != null) ? script.getLanguage() : null);
result.put("transcript", transcript == null ? "" : transcript); result.put("transcript", transcript == null ? "" : transcript);
result.put("segments", segments); result.put("segments", segments);
return result; return result;

View File

@ -226,7 +226,7 @@ public class ProductionService {
String oldSummary = text.substring(oldStart + oldSummaryMarker.length(), newStart).trim(); String oldSummary = text.substring(oldStart + oldSummaryMarker.length(), newStart).trim();
String newSummary = text.substring(newStart + newSummaryMarker.length()).trim(); String newSummary = text.substring(newStart + newSummaryMarker.length()).trim();
System.out.println("Saving summaries for video " + videoId); log.info("Saving summaries for video {}", videoId);
script.setOldScriptSummary(oldSummary); script.setOldScriptSummary(oldSummary);
script.setNewScriptSummary(newSummary); script.setNewScriptSummary(newSummary);
@ -270,7 +270,7 @@ public class ProductionService {
// 1. Fetch content using API // 1. Fetch content using API
String text = readGoogleDoc(docId); String text = readGoogleDoc(docId);
System.out.println("Saving final script for video " + videoId); log.info("Saving final script for video {}", videoId);
// 2. Save to DB // 2. Save to DB
script.setFinalScript(text); script.setFinalScript(text);
@ -307,7 +307,7 @@ public class ProductionService {
// 1. Fetch content using API // 1. Fetch content using API
String text = readGoogleDoc(docId); String text = readGoogleDoc(docId);
System.out.println("Saving opening script for video " + videoId); log.info("Saving opening script for video {}", videoId);
// 2. Save to DB // 2. Save to DB
script.setOpeningScript(text); script.setOpeningScript(text);

View File

@ -21,4 +21,17 @@ public class RestTemplateConfig {
}) })
.build(); .build();
} }
/**
* Python 마이크로서비스(Whisper 전사 /transcribe, ffmpeg 렌더 /render) RestTemplate.
* 전사·인코딩은 수십 ~ 분이 걸릴 있어 read timeout 길게 둔다.
* (주입 이름 {@code pythonRestTemplate} 구분 다른 곳의 restTemplate 주입에는 영향 없음)
*/
@Bean
public RestTemplate pythonRestTemplate(RestTemplateBuilder builder) {
return builder
.connectTimeout(Duration.ofSeconds(20))
.readTimeout(Duration.ofMinutes(10))
.build();
}
} }