diff --git a/frontend/bindings/changeme/quizservice.ts b/frontend/bindings/changeme/quizservice.ts index 679abe5..d7a37a8 100644 --- a/frontend/bindings/changeme/quizservice.ts +++ b/frontend/bindings/changeme/quizservice.ts @@ -183,6 +183,10 @@ export function Login(username: string, password: string): $CancellablePromise<$ }); } +export function PerformGeminiOcr(base64Image: string): $CancellablePromise { + return $Call.ByID(3593860722, base64Image); +} + /** * Register creates a new registration application */ diff --git a/frontend/src/views/QuizView.vue b/frontend/src/views/QuizView.vue index d5fbd77..59ba6cb 100644 --- a/frontend/src/views/QuizView.vue +++ b/frontend/src/views/QuizView.vue @@ -209,7 +209,7 @@ class="textarea textarea-ghost focus:bg-base-200/30 w-full min-h-[160px] text-sm sm:text-base leading-relaxed resize-none p-2 mt-2 border-0 focus:border-0 focus:outline-none focus:ring-0" >
-
+
+ (null) const uploadedOcrImageBase64 = ref('') +const useAiForOcr = ref(false) function triggerOcrUpload() { if (ocrFileInput.value) { @@ -580,6 +590,15 @@ function triggerOcrUpload() { } } +function readFileAsDataURL(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.onload = (e) => resolve(e.target?.result as string || '') + reader.onerror = (err) => reject(err) + reader.readAsDataURL(file) + }) +} + async function onOcrFileChange(event: Event) { const target = event.target as HTMLInputElement const file = target.files?.[0] @@ -587,46 +606,51 @@ async function onOcrFileChange(event: Event) { ocrLoading.value = true ocrProgress.value = 0 - ocrStatusText.value = '인식 엔진 준비 중...' + ocrStatusText.value = '이미지 준비 중...' ocrResultText.value = null uploadedOcrImageBase64.value = '' - // Convert file to base64 synchronously - const reader = new FileReader() - reader.onload = (e) => { - uploadedOcrImageBase64.value = e.target?.result as string || '' - } - reader.readAsDataURL(file) - try { - const result = await Tesseract.recognize( - file, - 'kor+eng', - { - logger: (m) => { - if (m && m.status) { - switch (m.status) { - case 'loading tesseract core': - ocrStatusText.value = '인식 엔진 불러오는 중...' - break - case 'initializing api': - ocrStatusText.value = '언어 데이터(한/영) 초기화 중...' - break - case 'recognizing text': - ocrStatusText.value = '글자 판독 및 텍스트 변환 중...' - break - default: - ocrStatusText.value = m.status - } - if (typeof m.progress === 'number') { - ocrProgress.value = Math.round(m.progress * 100) + const base64Data = await readFileAsDataURL(file) + uploadedOcrImageBase64.value = base64Data + + if (useAiForOcr.value) { + ocrStatusText.value = 'AI 인식 진행 중 (Gemini)...' + ocrProgress.value = 50 + + const result = await QuizService.PerformGeminiOcr(base64Data) + ocrProgress.value = 100 + ocrResultText.value = result || '' + } else { + ocrStatusText.value = '인식 엔진 로딩 중...' + const result = await Tesseract.recognize( + file, + 'kor+eng', + { + logger: (m) => { + if (m && m.status) { + switch (m.status) { + case 'loading tesseract core': + ocrStatusText.value = '인식 엔진 불러오는 중...' + break + case 'initializing api': + ocrStatusText.value = '언어 데이터(한/영) 초기화 중...' + break + case 'recognizing text': + ocrStatusText.value = '글자 판독 및 텍스트 변환 중...' + break + default: + ocrStatusText.value = m.status + } + if (typeof m.progress === 'number') { + ocrProgress.value = Math.round(m.progress * 100) + } } } } - } - ) - - ocrResultText.value = result.data.text || '' + ) + ocrResultText.value = result.data.text || '' + } } catch (err: any) { console.error('OCR 실패:', err) uploadedOcrImageBase64.value = '' diff --git a/quiz_service.go b/quiz_service.go index 0169fba..73823c1 100644 --- a/quiz_service.go +++ b/quiz_service.go @@ -768,6 +768,40 @@ func (s *QuizService) AskGemini(source string, id int, prompt string, base64Imag return "started", nil } +func (s *QuizService) PerformGeminiOcr(base64Image string) (string, error) { + if base64Image == "" { + return "", fmt.Errorf("Empty image data") + } + + mimeType, decodedData, err := parseBase64Image(base64Image) + if err != nil { + return "", fmt.Errorf("Failed to parse base64 image: %v", err) + } + + ctx := context.Background() + prompt := "이 이미지에서 손글씨 또는 인쇄된 글자(한글/영어 등)를 오직 텍스트로만 정확하게 추출해 주세요. 질문에 대한 해설이나 부가적인 설명(예: '추출한 텍스트:', 마크다운 장식 등)은 일절 제외하고, 이미지에 써져 있는 내용 그대로만 텍스트로 반환해야 합니다." + + parts := []genai.Part{ + genai.Text(prompt), + genai.Blob{ + MIMEType: mimeType, + Data: decodedData, + }, + } + + resp, err := s.executeGeminiWithRetry(ctx, parts) + if err != nil { + return "", fmt.Errorf("Gemini API call failed: %v", err) + } + + if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 { + return "", fmt.Errorf("No text detected by Gemini") + } + + resultText := fmt.Sprintf("%v", resp.Candidates[0].Content.Parts[0]) + return strings.TrimSpace(resultText), nil +} + func (s *QuizService) attachQuestionAssets(parts []genai.Part, source string, id int) []genai.Part { var questionText, choice1, choice2, choice3, choice4, choice5, passage, explanation, imageURL string queryQ := `