ocrai
This commit is contained in:
@@ -183,6 +183,10 @@ export function Login(username: string, password: string): $CancellablePromise<$
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PerformGeminiOcr(base64Image: string): $CancellablePromise<string> {
|
||||||
|
return $Call.ByID(3593860722, base64Image);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register creates a new registration application
|
* Register creates a new registration application
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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"
|
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"
|
||||||
></textarea>
|
></textarea>
|
||||||
<div class="flex justify-between items-center mt-2 border-t border-base-200 pt-2">
|
<div class="flex justify-between items-center mt-2 border-t border-base-200 pt-2">
|
||||||
<div class="flex gap-2">
|
<div class="flex items-center gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@click="triggerOcrUpload"
|
@click="triggerOcrUpload"
|
||||||
@@ -220,6 +220,15 @@
|
|||||||
<CameraIcon :size="12" />
|
<CameraIcon :size="12" />
|
||||||
종이 답안 인식 (OCR)
|
종이 답안 인식 (OCR)
|
||||||
</button>
|
</button>
|
||||||
|
<label class="label cursor-pointer py-0 gap-1.5" title="체크 시 온디바이스 Tesseract 대신 Gemini AI를 이용해 손글씨를 고정밀도로 인식합니다">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
v-model="useAiForOcr"
|
||||||
|
:disabled="showFeedback || ocrLoading"
|
||||||
|
class="checkbox checkbox-xs checkbox-secondary"
|
||||||
|
/>
|
||||||
|
<span class="label-text text-[10px] sm:text-xs opacity-70 font-bold">AI 인식 사용</span>
|
||||||
|
</label>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref="ocrFileInput"
|
ref="ocrFileInput"
|
||||||
@@ -573,6 +582,7 @@ const ocrProgress = ref(0)
|
|||||||
const ocrStatusText = ref('')
|
const ocrStatusText = ref('')
|
||||||
const ocrResultText = ref<string | null>(null)
|
const ocrResultText = ref<string | null>(null)
|
||||||
const uploadedOcrImageBase64 = ref('')
|
const uploadedOcrImageBase64 = ref('')
|
||||||
|
const useAiForOcr = ref(false)
|
||||||
|
|
||||||
function triggerOcrUpload() {
|
function triggerOcrUpload() {
|
||||||
if (ocrFileInput.value) {
|
if (ocrFileInput.value) {
|
||||||
@@ -580,6 +590,15 @@ function triggerOcrUpload() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readFileAsDataURL(file: File): Promise<string> {
|
||||||
|
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) {
|
async function onOcrFileChange(event: Event) {
|
||||||
const target = event.target as HTMLInputElement
|
const target = event.target as HTMLInputElement
|
||||||
const file = target.files?.[0]
|
const file = target.files?.[0]
|
||||||
@@ -587,46 +606,51 @@ async function onOcrFileChange(event: Event) {
|
|||||||
|
|
||||||
ocrLoading.value = true
|
ocrLoading.value = true
|
||||||
ocrProgress.value = 0
|
ocrProgress.value = 0
|
||||||
ocrStatusText.value = '인식 엔진 준비 중...'
|
ocrStatusText.value = '이미지 준비 중...'
|
||||||
ocrResultText.value = null
|
ocrResultText.value = null
|
||||||
uploadedOcrImageBase64.value = ''
|
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 {
|
try {
|
||||||
const result = await Tesseract.recognize(
|
const base64Data = await readFileAsDataURL(file)
|
||||||
file,
|
uploadedOcrImageBase64.value = base64Data
|
||||||
'kor+eng',
|
|
||||||
{
|
if (useAiForOcr.value) {
|
||||||
logger: (m) => {
|
ocrStatusText.value = 'AI 인식 진행 중 (Gemini)...'
|
||||||
if (m && m.status) {
|
ocrProgress.value = 50
|
||||||
switch (m.status) {
|
|
||||||
case 'loading tesseract core':
|
const result = await QuizService.PerformGeminiOcr(base64Data)
|
||||||
ocrStatusText.value = '인식 엔진 불러오는 중...'
|
ocrProgress.value = 100
|
||||||
break
|
ocrResultText.value = result || ''
|
||||||
case 'initializing api':
|
} else {
|
||||||
ocrStatusText.value = '언어 데이터(한/영) 초기화 중...'
|
ocrStatusText.value = '인식 엔진 로딩 중...'
|
||||||
break
|
const result = await Tesseract.recognize(
|
||||||
case 'recognizing text':
|
file,
|
||||||
ocrStatusText.value = '글자 판독 및 텍스트 변환 중...'
|
'kor+eng',
|
||||||
break
|
{
|
||||||
default:
|
logger: (m) => {
|
||||||
ocrStatusText.value = m.status
|
if (m && m.status) {
|
||||||
}
|
switch (m.status) {
|
||||||
if (typeof m.progress === 'number') {
|
case 'loading tesseract core':
|
||||||
ocrProgress.value = Math.round(m.progress * 100)
|
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) {
|
} catch (err: any) {
|
||||||
console.error('OCR 실패:', err)
|
console.error('OCR 실패:', err)
|
||||||
uploadedOcrImageBase64.value = ''
|
uploadedOcrImageBase64.value = ''
|
||||||
|
|||||||
@@ -768,6 +768,40 @@ func (s *QuizService) AskGemini(source string, id int, prompt string, base64Imag
|
|||||||
return "started", nil
|
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 {
|
func (s *QuizService) attachQuestionAssets(parts []genai.Part, source string, id int) []genai.Part {
|
||||||
var questionText, choice1, choice2, choice3, choice4, choice5, passage, explanation, imageURL string
|
var questionText, choice1, choice2, choice3, choice4, choice5, passage, explanation, imageURL string
|
||||||
queryQ := `
|
queryQ := `
|
||||||
|
|||||||
Reference in New Issue
Block a user