fix(gemini): 화면 자막 타임스탬프 0/빈결과 문제 해결
원인: 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) <noreply@anthropic.com>
This commit is contained in:
parent
99dbe80fdb
commit
b151204267
@ -28,10 +28,12 @@ import java.util.Map;
|
|||||||
public class GeminiSubtitleService {
|
public class GeminiSubtitleService {
|
||||||
|
|
||||||
private static final String PROMPT =
|
private static final String PROMPT =
|
||||||
"이 유튜브 영상에서 화면에 표시되는(영상에 박힌) 자막만 추출해 한국어로 번역하세요. "
|
"이 영상을 처음부터 끝까지 분석해서, 화면에 표시되는(영상에 박힌) 모든 자막을 빠짐없이 추출하세요. "
|
||||||
+ "음성/말소리는 무시하고 화면의 글자만 대상으로 합니다. "
|
+ "음성/말소리는 무시하고 화면의 글자만 대상으로 합니다. "
|
||||||
|
+ "각 자막이 화면에 나타나는 시작 시각과 사라지는 끝 시각을 '영상 시작 기준 초'로 최대한 정확히 적으세요(예: 0, 2.5, 8.1). "
|
||||||
|
+ "중요: 모든 항목의 시간을 0으로 두지 마세요. 자막이 바뀌는 순서대로 start 가 증가해야 합니다. "
|
||||||
+ "JSON 배열로만 출력: [{\"start\": 시작초, \"end\": 끝초, \"text\": \"한국어 번역\"}]. "
|
+ "JSON 배열로만 출력: [{\"start\": 시작초, \"end\": 끝초, \"text\": \"한국어 번역\"}]. "
|
||||||
+ "start/end 는 영상 시작 기준 초(소수 가능), text 는 한국어. 화면 자막이 없으면 빈 배열 [].";
|
+ "text 는 한국어 번역. 화면 자막이 전혀 없으면 빈 배열 [].";
|
||||||
|
|
||||||
private final RestTemplate geminiRestTemplate;
|
private final RestTemplate geminiRestTemplate;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
@ -42,8 +44,21 @@ public class GeminiSubtitleService {
|
|||||||
@Value("${gemini.model:gemini-2.5-flash}")
|
@Value("${gemini.model:gemini-2.5-flash}")
|
||||||
private String model;
|
private String model;
|
||||||
|
|
||||||
/** 유튜브 영상의 화면 자막을 한국어 세그먼트로 반환. */
|
/**
|
||||||
|
* 유튜브 영상의 화면 자막을 한국어 세그먼트로 반환.
|
||||||
|
* Gemini 영상이해는 비결정적이라 빈 결과가 나올 수 있어, 0개면 최대 3회 재시도한다(에러는 즉시 전파).
|
||||||
|
*/
|
||||||
public List<ScriptResponseDto.Segment> fetchKoreanScreenSubtitles(String videoId) {
|
public List<ScriptResponseDto.Segment> fetchKoreanScreenSubtitles(String videoId) {
|
||||||
|
for (int attempt = 1; attempt <= 3; attempt++) {
|
||||||
|
List<ScriptResponseDto.Segment> segs = callGeminiOnce(videoId);
|
||||||
|
if (!segs.isEmpty()) return segs;
|
||||||
|
log.info("Gemini 화면자막 0개(video {}, {}/3) — 재시도", videoId, attempt);
|
||||||
|
}
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Gemini 단일 호출(화면 자막 → 한국어 세그먼트). HTTP/키 오류는 예외로 전파, 정상이면 세그먼트(빈 가능). */
|
||||||
|
private List<ScriptResponseDto.Segment> callGeminiOnce(String videoId) {
|
||||||
String key = sanitizeApiKey(apiKey);
|
String key = sanitizeApiKey(apiKey);
|
||||||
if (key.isEmpty()) {
|
if (key.isEmpty()) {
|
||||||
throw new IllegalArgumentException("GEMINI_API_KEY 가 설정되지 않았습니다.");
|
throw new IllegalArgumentException("GEMINI_API_KEY 가 설정되지 않았습니다.");
|
||||||
@ -110,8 +125,12 @@ public class GeminiSubtitleService {
|
|||||||
"start", Map.of("type", "NUMBER"),
|
"start", Map.of("type", "NUMBER"),
|
||||||
"end", Map.of("type", "NUMBER"),
|
"end", Map.of("type", "NUMBER"),
|
||||||
"text", Map.of("type", "STRING")));
|
"text", Map.of("type", "STRING")));
|
||||||
|
// Gemini 2.5 Flash 는 'thinking' 모델: 정확한 타임스탬프는 thinking 으로 추론한다(끄면 시간이 전부 0).
|
||||||
|
// 단 thinking 토큰이 출력 한도를 먹어 JSON 이 잘리면 0개가 되므로 maxOutputTokens 를 크게 준다.
|
||||||
Map<String, Object> genConfig = Map.of(
|
Map<String, Object> genConfig = Map.of(
|
||||||
"responseMimeType", "application/json",
|
"responseMimeType", "application/json",
|
||||||
|
"temperature", 0,
|
||||||
|
"maxOutputTokens", 65536,
|
||||||
"responseSchema", Map.of("type", "ARRAY", "items", itemSchema));
|
"responseSchema", Map.of("type", "ARRAY", "items", itemSchema));
|
||||||
|
|
||||||
return Map.of("contents", List.of(content), "generationConfig", genConfig);
|
return Map.of("contents", List.of(content), "generationConfig", genConfig);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user