GenAI: chat 모델 OpenAI(gpt-5.6) 호환 + 노트 하단 사용모델 표기

- OciGenAiService.chat(): 모델 인지 분기. openai.* 모델은 maxTokens/
  temperature(0.3)를 거부하므로 maxCompletionTokens 사용 + temperature 생략.
  gemini/cohere 등 기존 모델은 maxTokens+temperature 유지(하위호환).
- NoteController: 노트 최종 결과 하단에 사용 모델 푸터 표기
  (음성변환 모델 + 교정·요약 모델). 모델명은 설정값에서 조회해 자동 반영.
- STT(음성→텍스트)는 OpenRouter Gemini 유지, 변경 없음.
- OCI_GENAI_MODEL은 .env에서 openai.gpt-5.6-terra로 전환(커밋 제외).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 07:34:55 +00:00
parent 446969b862
commit e1e71dc954
2 changed files with 35 additions and 12 deletions

View File

@@ -133,7 +133,8 @@ public class NoteController {
noteRepository.updateContent(id, polished + "\n\n--- 요약 생성 중... ---"); noteRepository.updateContent(id, polished + "\n\n--- 요약 생성 중... ---");
String summary = summarizeTranscription(polished); String summary = summarizeTranscription(polished);
String result = "# 요약\n\n" + summary + "\n\n---\n\n# 전문\n\n" + polished; String result = "# 요약\n\n" + summary + "\n\n---\n\n# 전문\n\n" + polished
+ modelFooter(null);
String newTitle = generateAudioTitle(summary, java.time.LocalDateTime.now()); String newTitle = generateAudioTitle(summary, java.time.LocalDateTime.now());
noteRepository.update(id, null, newTitle, result, null); noteRepository.update(id, null, newTitle, result, null);
noteRepository.updateNoteType(id, isAudio ? "AUDIO" : "TEXT"); noteRepository.updateNoteType(id, isAudio ? "AUDIO" : "TEXT");
@@ -202,11 +203,13 @@ public class NoteController {
private void transcribeAsync(String noteId, Path audioFile, String inputTitle) throws IOException, InterruptedException { private void transcribeAsync(String noteId, Path audioFile, String inputTitle) throws IOException, InterruptedException {
// === Step 1: STT (OpenRouter Gemini) === // === Step 1: STT (OpenRouter Gemini) ===
String rawResult = null; String rawResult = null;
String sttModel = null;
if (openRouterApiKey != null && !openRouterApiKey.isBlank()) { if (openRouterApiKey != null && !openRouterApiKey.isBlank()) {
try { try {
noteRepository.updateContent(noteId, "Gemini로 음성 변환 중..."); noteRepository.updateContent(noteId, "Gemini로 음성 변환 중...");
rawResult = transcribeWithOpenRouter(audioFile); rawResult = transcribeWithOpenRouter(audioFile);
sttModel = openRouterModel;
log.info("OpenRouter STT: {} chars", rawResult != null ? rawResult.length() : 0); log.info("OpenRouter STT: {} chars", rawResult != null ? rawResult.length() : 0);
} catch (Exception e) { } catch (Exception e) {
log.warn("OpenRouter STT failed: {}", e.getMessage()); log.warn("OpenRouter STT failed: {}", e.getMessage());
@@ -219,6 +222,7 @@ public class NoteController {
try { try {
noteRepository.updateContent(noteId, "Gemma로 음성 변환 중..."); noteRepository.updateContent(noteId, "Gemma로 음성 변환 중...");
rawResult = transcribeWithGemma(audioFile); rawResult = transcribeWithGemma(audioFile);
sttModel = "gemma4:e4b";
} catch (Exception e) { } catch (Exception e) {
log.error("All STT failed for note {}", noteId, e); log.error("All STT failed for note {}", noteId, e);
noteRepository.updateContent(noteId, "모든 음성 변환 실패: " + e.getMessage()); noteRepository.updateContent(noteId, "모든 음성 변환 실패: " + e.getMessage());
@@ -257,7 +261,8 @@ public class NoteController {
log.info("Summary complete: {} chars", summary.length()); log.info("Summary complete: {} chars", summary.length());
// 최종 결과 저장 // 최종 결과 저장
String result = "# 요약\n\n" + summary + "\n\n---\n\n# 전문\n\n" + polished; String result = "# 요약\n\n" + summary + "\n\n---\n\n# 전문\n\n" + polished
+ modelFooter(sttModel);
String finalTitle = inputTitle.equals("음성 변환 중...") String finalTitle = inputTitle.equals("음성 변환 중...")
? generateAudioTitle(summary, java.time.LocalDateTime.now()) ? generateAudioTitle(summary, java.time.LocalDateTime.now())
: inputTitle; : inputTitle;
@@ -271,8 +276,18 @@ public class NoteController {
} }
/** /**
* Gemma 4 E4B를 사용하여 오디오 파일을 텍스트로 변환 * 노트 하단에 사용 모델을 표기하는 푸터를 만든다.
* sttModel이 있으면 음성변환 모델도 함께 표기하고, 없으면 교정·요약 모델만 표기한다.
* 모델명은 genAiService.getDefaultModel()/openRouterModel에서 가져오므로 설정 변경 시 자동 반영된다.
*/ */
private String modelFooter(String sttModel) {
String polishModel = genAiService.getDefaultModel();
if (sttModel != null && !sttModel.isBlank()) {
return "\n\n---\n\n*🤖 음성변환: " + sttModel + " · 교정·요약: " + polishModel + "*";
}
return "\n\n---\n\n*🤖 교정·요약: " + polishModel + "*";
}
/** /**
* 오디오 파일을 wav로 변환한다 (Ollama 호환성). * 오디오 파일을 wav로 변환한다 (Ollama 호환성).
*/ */

View File

@@ -13,6 +13,7 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse; import java.net.http.HttpResponse;
import java.time.Duration; import java.time.Duration;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -84,21 +85,28 @@ public class OciGenAiService {
modelId = defaultModel; modelId = defaultModel;
} }
Map<String, Object> chatRequest = new LinkedHashMap<>();
chatRequest.put("apiFormat", "GENERIC");
chatRequest.put("messages", List.of(
Map.of("role", "SYSTEM", "content", List.of(Map.of("type", "TEXT", "text", systemMessage))),
Map.of("role", "USER", "content", List.of(Map.of("type", "TEXT", "text", userMessage)))
));
// OpenAI 계열(openai.gpt-5.x 등)은 maxTokens / temperature(0.3)를 거부한다.
// maxCompletionTokens를 사용하고, temperature는 기본값(1)만 허용되므로 생략한다.
if (modelId.startsWith("openai.")) {
chatRequest.put("maxCompletionTokens", 65536);
} else {
chatRequest.put("maxTokens", 65536);
chatRequest.put("temperature", 0.3);
}
Map<String, Object> payload = Map.of( Map<String, Object> payload = Map.of(
"compartmentId", compartment, "compartmentId", compartment,
"servingMode", Map.of( "servingMode", Map.of(
"servingType", "ON_DEMAND", "servingType", "ON_DEMAND",
"modelId", modelId "modelId", modelId
), ),
"chatRequest", Map.of( "chatRequest", chatRequest
"apiFormat", "GENERIC",
"messages", List.of(
Map.of("role", "SYSTEM", "content", List.of(Map.of("type", "TEXT", "text", systemMessage))),
Map.of("role", "USER", "content", List.of(Map.of("type", "TEXT", "text", userMessage)))
),
"maxTokens", 65536,
"temperature", 0.3
)
); );
String body = objectMapper.writeValueAsString(payload); String body = objectMapper.writeValueAsString(payload);