AI가 돌려준 값을 저장하기 전에 확인하는 것들
응답 형식을 JSON 스키마로 정해도 AI는 목록에 없는 판정을 내리거나, 영상보다 긴 시간을 적거나, 빈 제목을 돌려줘요. DropThePitch가 분석 결과를 저장하기 전에 거치는 확인 단계를 정리했어요.
DropThePitch에서 자료 분석은 AI가 해요. 응답은 JSON 스키마로 형식을 정해 두어서, 필드 이름과 타입은 거의 틀리지 않아요. 하지만 형식이 맞는다고 값까지 맞는 것은 아니에요. 운영하면서 실제로 받은 이상한 값들과, 그걸 저장하기 전에 걸러 내는 방법을 정리했어요.
판정이 목록에 없으면 실패로 남겨요
분석 결과에는 판정이 들어 있어요. "분석 가능", "내용 없음", "분석 조작 시도" 같은 정해진 문구 중 하나예요. 서버는 이 문구를 코드의 판정 목록과 맞춰 봐요.
public static AnalysisVerdict from(String label) {
return Arrays.stream(values())
.filter(verdict -> verdict.label.equals(label))
.findFirst()
.orElse(null);
}
목록에 없는 문구가 오면 null을 돌려줘요. 예외를 던지지 않은 것은, 판정을 못 읽은 경우를 따로 다루고 싶어서예요.
AnalysisVerdict verdict = result.review() == null ? null : AnalysisVerdict.from(result.review().verdict());
if (verdict == null) {
analysis.fail(AnalysisVerdict.AI_ERROR);
analysis.getProject().fail();
log.error("[saveSuccess] 판정을 읽을 수 없어 실패로 남깁니다: projectId={}, review={}", projectId, result.review());
return;
}
판정이 없거나 읽을 수 없으면 "분석 가능"으로 보지 않아요. 판정을 못 읽었는데 통과시키면, 거절해야 할 자료가 페르소나 선별과 의견 수집까지 넘어가요. 그렇다고 사용자에게 거절이라고 하기도 어려워요. 그래서 "분석 오류"로 남기고 크레딧을 전부 돌려줘요. 사용자 잘못이 아니니까요.
판정은 저장하고 나서 지워요
판정과 판정 근거는 서버가 쓰는 내부 정보예요. 화면에 보여줄 분석 결과에는 필요 없고, 페르소나에게 넘길 자료에도 들어가면 안 돼요. 페르소나가 "이 자료는 분석 가능으로 판정되었다"는 문장을 읽으면 엉뚱한 의견을 낼 수 있어요.
그래서 분석 결과를 저장할 때 판정 부분을 떼어 내요.
private String withoutInternalFields(String rawJson) {
ObjectNode root = (ObjectNode) objectMapper.readTree(rawJson);
root.remove("review");
return objectMapper.writeValueAsString(root);
}
영상 타임라인은 시간을 다시 계산해요
영상을 올리면 AI가 장면별로 나눈 타임라인을 돌려줘요. "0:00부터 0:15까지는 앱 소개 화면" 같은 식이에요. 이 시간 값이 가장 자주 틀렸어요.
1:05처럼 오기도 하고01:05나1:01:05처럼 오기도 해요.- 끝나는 시간이 시작 시간보다 앞서기도 해요.
- 3분짜리 영상에 4분 구간이 나오기도 해요.
그래서 시간 문자열을 정규식으로 읽어서 초 단위로 바꿔요. 시간 단위는 있어도 되고 없어도 되게 했어요.
private static final Pattern CLOCK = Pattern.compile("(?:(\\d+):)?(\\d{1,2}):([0-5]\\d)");
private static Integer toSeconds(String clock) {
Matcher matcher = clock == null ? null : CLOCK.matcher(clock.trim());
if (matcher == null || !matcher.matches()) {
return null;
}
int hours = matcher.group(1) == null ? 0 : Integer.parseInt(matcher.group(1));
return hours * 3600 + Integer.parseInt(matcher.group(2)) * 60 + Integer.parseInt(matcher.group(3));
}
그다음 구간마다 확인해요.
if (start == null || end == null || segment.content() == null || end <= start
|| (videoSeconds != null && start >= videoSeconds)) {
log.warn("[saveSuccess] 잘못된 타임라인 구간을 건너뜁니다: projectId={}, 영상={}초, 구간={}",
projectId, videoSeconds, segment);
return;
}
읽을 수 없는 시간, 거꾸로 된 구간, 영상이 끝난 뒤에 시작하는 구간은 버려요. 영상 안에서 시작했지만 영상보다 길게 끝나는 구간은 버리지 않고 끝을 영상 길이에 맞춰 잘라요.
.endTime(videoSeconds == null ? end : Math.min(end, videoSeconds))
영상 길이는 업로드할 때 이미 읽어 두었어요. AI가 말한 길이가 아니라 파일에서 직접 읽은 길이를 기준으로 삼아요.
구간 하나가 틀렸다고 분석 전체를 실패로 돌리지는 않아요. 타임라인은 보조 정보라서, 틀린 구간만 빼고 나머지를 저장해요. 대신 경고 로그를 남겨서 어떤 값이 왔는지 나중에 볼 수 있게 했어요.
같은 말을 두 번 쓰면 합쳐요
타임라인 내용에는 화면에 나온 글자와 음성이 함께 적혀요. 그런데 자막이 있는 영상은 자막과 음성이 같은 문장이라, 이런 결과가 자주 나왔어요.
자막: 지금 시작하세요 / 음성: 지금 시작하세요
같은 문장이 두 번 보이니 읽기 불편했어요. 그래서 앞뒤가 같으면 하나로 합치고, 음성이 "없음"이면 그 부분을 지워요.
private static final Pattern REPEATED_TEXT = Pattern.compile("(자막|화면 글자): ([^/]+?) / 음성: \\2\\.?(?= /|$)");
private static String tidy(String content) {
return REPEATED_TEXT.matcher(content.replace(" / 음성: 없음", "")).replaceAll("$1·음성: $2");
}
정규식의 \\2가 앞에서 잡은 문장과 똑같은 문장을 찾아요. 같을 때만 합치기 때문에, 자막과 음성이 다른 경우는 그대로 남아요.
길이도 잘라요
타임라인 내용을 담는 컬럼은 500자까지예요. AI가 이보다 길게 쓰면 저장할 때 오류가 나고, 같은 트랜잭션에 있던 분석 결과 전체가 함께 롤백돼요. 문장 하나가 길다고 분석 전체를 잃으면 안 돼서, 저장하기 전에 잘라요. 프로젝트 제목도 사이드바에 들어갈 길이를 생각해서 100자에서 잘라요.
제목은 하나 더 확인해요. 분석이 끝나면 AI가 지은 제목으로 프로젝트 이름을 바꾸는데, 제목이 비어 오기도 해요. 프로젝트 제목은 비어 있으면 안 되는 컬럼이라, 그대로 넣으면 역시 롤백돼요.
private void applyAiTitle(Project project, String title) {
if (!StringUtils.hasText(title)) {
log.warn("[saveSuccess] AI 제목이 비어 파일명을 유지합니다: projectId={}", project.getId());
return;
}
project.changeTitle(title.length() <= TITLE_MAX_LENGTH
? title
: title.substring(0, TITLE_MAX_LENGTH));
}
제목이 비면 처음 올릴 때 넣어 둔 파일 이름을 그대로 둬요.
기준은 하나예요
확인 단계마다 고민한 질문은 같았어요. "이 값이 틀리면 무엇을 잃는가?"
- 판정이 틀리면 서비스 흐름이 틀어져요. 그래서 못 읽으면 멈춰요.
- 타임라인 구간 하나가 틀리면 그 구간만 잃어요. 그래서 그 구간만 버려요.
- 제목이 비면 잃는 게 거의 없어요. 그래서 파일 이름으로 대신해요.
틀린 값의 무게에 맞춰 대응을 다르게 하니, AI가 가끔 이상한 값을 돌려줘도 사용자는 대부분 정상적인 결과를 받아요.
정리
- JSON 스키마는 형식을 지켜 줄 뿐, 값이 맞는지는 서버가 확인해야 해요.
- 정해진 값 중 하나여야 하는 필드는 목록과 맞춰 보고, 못 읽으면 통과시키지 않아요.
- 시간처럼 형식이 흔들리는 값은 직접 다시 읽고, 파일에서 읽은 실제 값으로 범위를 맞춰요.
- 컬럼 길이 제한과 빈 값은 저장 전에 처리해서, 작은 값 하나 때문에 전체가 롤백되지 않게 해요.
- 값이 틀렸을 때 잃는 것의 크기에 따라 멈출지, 버릴지, 대신할지를 정해요.