변경2
This commit is contained in:
212
src/main/java/kr/co/ragone/service/VisionService.java
Normal file
212
src/main/java/kr/co/ragone/service/VisionService.java
Normal file
@@ -0,0 +1,212 @@
|
||||
package kr.co.ragone.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.pdfbox.pdmodel.PDDocument;
|
||||
import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class VisionService {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
// RestTemplate with 10 minute timeout
|
||||
public VisionService(ObjectMapper objectMapper) {
|
||||
this.objectMapper = objectMapper;
|
||||
|
||||
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
|
||||
factory.setConnectTimeout(600000); // 10분
|
||||
factory.setReadTimeout(600000); // 10분
|
||||
this.restTemplate = new RestTemplate(factory);
|
||||
}
|
||||
|
||||
@Value("${openai.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
@Value("${rag.vision.enabled:false}")
|
||||
private boolean visionEnabled;
|
||||
|
||||
@Value("${rag.vision.model:gpt-4o-mini}")
|
||||
private String visionModel;
|
||||
|
||||
private static final String OPENAI_VISION_URL = "https://api.openai.com/v1/chat/completions";
|
||||
|
||||
/**
|
||||
* Vision 처리 활성화 여부
|
||||
*/
|
||||
public boolean isEnabled() {
|
||||
return visionEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* PDF 파일을 Vision 모델로 분석하여 텍스트 추출
|
||||
*/
|
||||
public String processPdfWithVision(String pdfPath) {
|
||||
if (!visionEnabled) {
|
||||
log.info("[Vision] 비활성화 상태");
|
||||
return null;
|
||||
}
|
||||
|
||||
StringBuilder allDescriptions = new StringBuilder();
|
||||
|
||||
try (PDDocument document = PDDocument.load(new File(pdfPath))) {
|
||||
PDFRenderer renderer = new PDFRenderer(document);
|
||||
int pageCount = document.getNumberOfPages();
|
||||
|
||||
log.info("[Vision] PDF 분석 시작: {} 페이지", pageCount);
|
||||
|
||||
for (int i = 0; i < pageCount; i++) {
|
||||
try {
|
||||
log.info("[Vision] 페이지 {}/{} 분석 중...", i + 1, pageCount);
|
||||
|
||||
// 페이지를 이미지로 변환 (150 DPI)
|
||||
BufferedImage image = renderer.renderImageWithDPI(i, 150, ImageType.RGB);
|
||||
|
||||
// 이미지를 Base64로 인코딩
|
||||
String base64Image = encodeImageToBase64(image);
|
||||
|
||||
// Vision API 호출
|
||||
String description = callVisionApi(base64Image, i + 1, pageCount);
|
||||
|
||||
if (description != null && !description.isEmpty()) {
|
||||
allDescriptions.append("\n\n=== 페이지 ").append(i + 1).append(" ===\n");
|
||||
allDescriptions.append(description);
|
||||
}
|
||||
|
||||
// API 속도 제한 방지 (1초 대기)
|
||||
Thread.sleep(1000);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("[Vision] 페이지 {} 분석 실패: {}", i + 1, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
log.info("[Vision] PDF 분석 완료");
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Vision] PDF 처리 실패: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
return allDescriptions.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI Vision API 직접 호출
|
||||
*/
|
||||
private String callVisionApi(String base64Image, int pageNum, int totalPages) {
|
||||
try {
|
||||
// 요청 헤더
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.setBearerAuth(apiKey);
|
||||
|
||||
// 프롬프트 구성
|
||||
String prompt = """
|
||||
이 문서 페이지(페이지 %d/%d)의 내용을 분석해주세요.
|
||||
|
||||
【분석 지침】
|
||||
1. 모든 텍스트 내용을 정확하게 추출하세요
|
||||
2. 표(테이블)가 있으면 마크다운 표 형식으로 변환하세요
|
||||
3. 요구사항 ID, 제목, 내용이 있으면 구조화해서 정리하세요
|
||||
4. 다이어그램/차트가 있으면 설명을 추가하세요
|
||||
5. 중요한 항목은 놓치지 마세요
|
||||
|
||||
【중요: 제외할 내용】
|
||||
- 목차 페이지는 간단히 "목차 페이지입니다"라고만 응답하세요
|
||||
- 페이지 번호와 점선(...)만 있는 내용은 제외하세요
|
||||
- 표지, 개정이력 등 의미없는 페이지는 간단히 "표지/개정이력 페이지입니다"라고만 응답
|
||||
|
||||
【출력 형식】
|
||||
- 한국어로 응답
|
||||
- 원본 구조를 최대한 유지
|
||||
- 표는 마크다운 형식 사용
|
||||
""".formatted(pageNum, totalPages);
|
||||
|
||||
// 요청 바디 구성
|
||||
Map<String, Object> requestBody = new HashMap<>();
|
||||
requestBody.put("model", visionModel);
|
||||
|
||||
// 모델에 따라 파라미터 다르게 설정
|
||||
if (visionModel.startsWith("gpt-5") || visionModel.startsWith("o3") || visionModel.startsWith("o4")) {
|
||||
requestBody.put("max_completion_tokens", 4096);
|
||||
} else {
|
||||
requestBody.put("max_tokens", 4096);
|
||||
}
|
||||
|
||||
// 메시지 구성 (Vision API 형식)
|
||||
List<Map<String, Object>> messages = new ArrayList<>();
|
||||
Map<String, Object> userMessage = new HashMap<>();
|
||||
userMessage.put("role", "user");
|
||||
|
||||
// content 배열 (텍스트 + 이미지)
|
||||
List<Map<String, Object>> content = new ArrayList<>();
|
||||
|
||||
// 텍스트 부분
|
||||
Map<String, Object> textContent = new HashMap<>();
|
||||
textContent.put("type", "text");
|
||||
textContent.put("text", prompt);
|
||||
content.add(textContent);
|
||||
|
||||
// 이미지 부분
|
||||
Map<String, Object> imageContent = new HashMap<>();
|
||||
imageContent.put("type", "image_url");
|
||||
Map<String, String> imageUrl = new HashMap<>();
|
||||
imageUrl.put("url", "data:image/png;base64," + base64Image);
|
||||
imageUrl.put("detail", "high"); // high quality 분석
|
||||
imageContent.put("image_url", imageUrl);
|
||||
content.add(imageContent);
|
||||
|
||||
userMessage.put("content", content);
|
||||
messages.add(userMessage);
|
||||
requestBody.put("messages", messages);
|
||||
|
||||
// API 호출
|
||||
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||
ResponseEntity<String> response = restTemplate.exchange(
|
||||
OPENAI_VISION_URL,
|
||||
HttpMethod.POST,
|
||||
entity,
|
||||
String.class
|
||||
);
|
||||
|
||||
// 응답 파싱
|
||||
if (response.getStatusCode() == HttpStatus.OK) {
|
||||
JsonNode root = objectMapper.readTree(response.getBody());
|
||||
String result = root.path("choices").get(0).path("message").path("content").asText();
|
||||
log.debug("[Vision] 페이지 {} 분석 결과: {} 글자", pageNum, result.length());
|
||||
return result;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Vision] API 호출 실패: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미지를 Base64로 인코딩
|
||||
*/
|
||||
private String encodeImageToBase64(BufferedImage image) throws Exception {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
ImageIO.write(image, "png", baos);
|
||||
byte[] imageBytes = baos.toByteArray();
|
||||
return Base64.getEncoder().encodeToString(imageBytes);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user