From b151204267c668f0b4217363c49a3e23398bb592 Mon Sep 17 00:00:00 2001 From: "hehihoho3@gmail.com" Date: Thu, 25 Jun 2026 18:43:47 +0900 Subject: [PATCH] =?UTF-8?q?fix(gemini):=20=ED=99=94=EB=A9=B4=20=EC=9E=90?= =?UTF-8?q?=EB=A7=89=20=ED=83=80=EC=9E=84=EC=8A=A4=ED=83=AC=ED=94=84=200/?= =?UTF-8?q?=EB=B9=88=EA=B2=B0=EA=B3=BC=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 원인: Gemini 2.5 Flash의 thinking(타임스탬프 추론)+JSON 출력이 자막 많은 영상에서 출력 토큰을 초과해 JSON이 잘림 → 0개 또는 타임스탬프 전부 0. - maxOutputTokens 65536(최대)로 상향 — thinking+JSON 잘림 방지(핵심) - temperature 0 — 일관성 - 프롬프트: 시작/끝 시각 정확히, 전부 0 금지, 누락 없이 추출하도록 보강 - 빈 결과면 최대 3회 재시도(영상이해 비결정성 보완) 검증: 470·560(자막 많은 영상) 모두 진행형 타임스탬프로 정상 추출. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../service/GeminiSubtitleService.java | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/hlab/yanalyst/service/GeminiSubtitleService.java b/src/main/java/com/hlab/yanalyst/service/GeminiSubtitleService.java index f20ee6a..8e03306 100644 --- a/src/main/java/com/hlab/yanalyst/service/GeminiSubtitleService.java +++ b/src/main/java/com/hlab/yanalyst/service/GeminiSubtitleService.java @@ -28,10 +28,12 @@ import java.util.Map; public class GeminiSubtitleService { private static final String PROMPT = - "이 유튜브 영상에서 화면에 표시되는(영상에 박힌) 자막만 추출해 한국어로 번역하세요. " + "이 영상을 처음부터 끝까지 분석해서, 화면에 표시되는(영상에 박힌) 모든 자막을 빠짐없이 추출하세요. " + "음성/말소리는 무시하고 화면의 글자만 대상으로 합니다. " + + "각 자막이 화면에 나타나는 시작 시각과 사라지는 끝 시각을 '영상 시작 기준 초'로 최대한 정확히 적으세요(예: 0, 2.5, 8.1). " + + "중요: 모든 항목의 시간을 0으로 두지 마세요. 자막이 바뀌는 순서대로 start 가 증가해야 합니다. " + "JSON 배열로만 출력: [{\"start\": 시작초, \"end\": 끝초, \"text\": \"한국어 번역\"}]. " - + "start/end 는 영상 시작 기준 초(소수 가능), text 는 한국어. 화면 자막이 없으면 빈 배열 []."; + + "text 는 한국어 번역. 화면 자막이 전혀 없으면 빈 배열 []."; private final RestTemplate geminiRestTemplate; private final ObjectMapper objectMapper; @@ -42,8 +44,21 @@ public class GeminiSubtitleService { @Value("${gemini.model:gemini-2.5-flash}") private String model; - /** 유튜브 영상의 화면 자막을 한국어 세그먼트로 반환. */ + /** + * 유튜브 영상의 화면 자막을 한국어 세그먼트로 반환. + * Gemini 영상이해는 비결정적이라 빈 결과가 나올 수 있어, 0개면 최대 3회 재시도한다(에러는 즉시 전파). + */ public List fetchKoreanScreenSubtitles(String videoId) { + for (int attempt = 1; attempt <= 3; attempt++) { + List segs = callGeminiOnce(videoId); + if (!segs.isEmpty()) return segs; + log.info("Gemini 화면자막 0개(video {}, {}/3) — 재시도", videoId, attempt); + } + return List.of(); + } + + /** Gemini 단일 호출(화면 자막 → 한국어 세그먼트). HTTP/키 오류는 예외로 전파, 정상이면 세그먼트(빈 가능). */ + private List callGeminiOnce(String videoId) { String key = sanitizeApiKey(apiKey); if (key.isEmpty()) { throw new IllegalArgumentException("GEMINI_API_KEY 가 설정되지 않았습니다."); @@ -110,8 +125,12 @@ public class GeminiSubtitleService { "start", Map.of("type", "NUMBER"), "end", Map.of("type", "NUMBER"), "text", Map.of("type", "STRING"))); + // Gemini 2.5 Flash 는 'thinking' 모델: 정확한 타임스탬프는 thinking 으로 추론한다(끄면 시간이 전부 0). + // 단 thinking 토큰이 출력 한도를 먹어 JSON 이 잘리면 0개가 되므로 maxOutputTokens 를 크게 준다. Map genConfig = Map.of( "responseMimeType", "application/json", + "temperature", 0, + "maxOutputTokens", 65536, "responseSchema", Map.of("type", "ARRAY", "items", itemSchema)); return Map.of("contents", List.of(content), "generationConfig", genConfig);