Skip to content
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
24ec847
[BOOK-95] chore: core:ocr 모듈 추가
seoyoon513 Jul 12, 2025
17bfe34
[BOOK-95] chore: MLKit 의존성 추가
seoyoon513 Jul 12, 2025
187d939
Merge branch 'refs/heads/develop' into BOOK-95-feature/#29
seoyoon513 Jul 23, 2025
8a2c3f0
Merge branch 'develop' into BOOK-95-feature/#29
seoyoon513 Jul 23, 2025
5b18709
[BOOK-95] fix: BookItem 내부 패딩 영역 수정
seoyoon513 Jul 23, 2025
8728b06
Merge branch 'develop' into BOOK-95-feature/#29
seoyoon513 Jul 24, 2025
f848112
[BOOK-95] feat: Ocr 화면 UI 구현
seoyoon513 Jul 25, 2025
73bf085
[BOOK-95] core: CameraX 라이브러리 추가
seoyoon513 Jul 25, 2025
6e0b57b
[BOOK-95] feat: 카메라 런타임 권한 추가 및 카메라 Preview 구현
seoyoon513 Jul 25, 2025
b0f810e
[BOOK-95] feat: 실시간 텍스트 분석 객체 구현
seoyoon513 Jul 25, 2025
220fb21
[BOOK-95] feat: 정적 이미지 텍스트 분석 객체 구현 및 파라미터 타입 변경 (InputImage -> Image…
seoyoon513 Jul 25, 2025
1b24ca0
[BOOK-95] fix: StillTextAnalyzer와 LiveTextAnalyzer를 @AssistedInject +…
seoyoon513 Jul 25, 2025
6571758
[BOOK-95] feat: Ocr 기능 연동
seoyoon513 Jul 25, 2025
18fc591
[BOOK-95] feat: 인식된 문장을 선택하고 기록에 반영하는 기능 구현
seoyoon513 Jul 26, 2025
2b7ca0e
[BOOK-95] feat: 카메라 미리보기 회면에서 SystemBarColor 변경
seoyoon513 Jul 26, 2025
5b9abed
[BOOK-95] refactor: 불필요한 콜백 매개변수 삭제
seoyoon513 Jul 26, 2025
288fabc
[BOOK-95] feat: 카메라 커스텀 프레임 구현
seoyoon513 Jul 26, 2025
1cf563e
[BOOK-95] refactor: TextAnalyzer에서 예외가 있는 경우에만 로깅하도록 변경
seoyoon513 Jul 26, 2025
7ea032e
[BOOK-95] chore: code style check success
seoyoon513 Jul 26, 2025
daa3691
[BOOK-95] chore: 미사용 의존성 삭제
seoyoon513 Jul 26, 2025
5a28077
[BOOK-95] fix: Navigation BackStack pop 시 데이터 전달하도록 수정
seoyoon513 Jul 26, 2025
e86c7a1
[BOOK-95] feat: 인식된 문장이 없을 경우 인식 실패 로직 구현
seoyoon513 Jul 27, 2025
c3342b0
[BOOK-95] feat: 다시 촬영 시 다이얼로그 확인 요청 구현
seoyoon513 Jul 27, 2025
77a0155
[BOOK-95] feat: 수집된 문장을 마침표, 물음표, 느낌표 기준으로 단위 쪼개기
seoyoon513 Jul 27, 2025
c6f037f
[BOOK-95] chore: code style check success
seoyoon513 Jul 27, 2025
f9f7dea
[BOOK-95] refactor: 프레임마다 새로운 analyzer 인스턴스를 생성하지 않도록 present 함수 내에 선언
seoyoon513 Jul 28, 2025
d6a5332
[BOOK-95] refactor: 카메라 리소스와 imageAnalyzer의 생명주기를 Composable의 생명주기에 맞…
seoyoon513 Jul 28, 2025
48f4939
[BOOK-95] feat: 카메라 권한 요청 거절 시 대응 구현
seoyoon513 Jul 28, 2025
51516b8
[BOOK-95] refactor: data class에서 var로 선언한 변수 val로 변경 (불변성 원칙 위배)
seoyoon513 Jul 28, 2025
34d9234
[BOOK-95] refactor: TextAnalyzer에 CoroutineScope cancel 할 수 있는 로직 추가
seoyoon513 Jul 28, 2025
c7caa58
Merge branch 'develop' into BOOK-95-feature/#29
seoyoon513 Jul 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ dependencies {
projects.core.model,
projects.core.network,
projects.core.ui,
projects.core.ocr,

projects.feature.home,
projects.feature.library,
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<uses-feature
android:name="android.hardware.camera"
android:required="false" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />

<application
android:name=".BooketApplication"
Expand Down
1 change: 1 addition & 0 deletions core/ocr/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
19 changes: 19 additions & 0 deletions core/ocr/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@file:Suppress("INLINE_FROM_HIGHER_PLATFORM")

plugins {
alias(libs.plugins.booket.android.library)
alias(libs.plugins.booket.android.hilt)
}

android {
namespace = "com.ninecraft.booket.core.ocr"
}

dependencies {
implementations(
libs.logger,
libs.androidx.camera.core,

libs.google.mlkit.text.recognition.korean,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.ninecraft.booket.core.ocr.analyzer

import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.text.TextRecognizer
import com.orhanobut.logger.Logger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

/**
* 실시간 카메라 스트림에서 프레임 단위로 텍스트 분석하는 Analyzer 클래스
*
* ML Kit의 TextRecognizer를 사용하여 `ImageProxy` 객체로부터 텍스트를 추출한다
*
* @param textRecognizer ML Kit의 TextRecognizer 인스턴스
* @param onTextDetected 텍스트 인식 성공 시 호출되는 콜백 (인식된 전체 텍스트 전달)
*
* 안정적인 연속 프레임 분석을 위해 CoroutineScope에 [SupervisorJob]을 사용하여
* 한 프레임 분석에서 예외가 발생해도 다음 프레임 분석에 영향을 주지 않도록 설계
*/
class LiveTextAnalyzer @AssistedInject constructor(
private val textRecognizer: TextRecognizer,
@Assisted private val onTextDetected: (String) -> Unit,
) : TextAnalyzer {

companion object {
const val THROTTLE_TIMEOUT_MS = 1_000L // 프레임 처리 간 인터벌
}

private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())

@OptIn(ExperimentalGetImage::class)
override fun analyze(imageProxy: ImageProxy) {
scope.launch {
val mediaImage = imageProxy.image ?: run { imageProxy.close(); return@launch }
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

suspendCoroutine { continuation ->
textRecognizer.process(inputImage)
.addOnCompleteListener { visionText ->
onTextDetected(visionText.result.text)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
.addOnFailureListener { exception ->
Logger.e(exception.message ?: "Unknown error")
}
.addOnCompleteListener {
continuation.resume(Unit)
}
}
delay(THROTTLE_TIMEOUT_MS)
}.invokeOnCompletion { exception ->
if (exception != null) {
Logger.e(exception.message ?: "Unknown error")
}
imageProxy.close()
}
}

@AssistedFactory
interface Factory {
fun create(
onTextDetected: (String) -> Unit,
): LiveTextAnalyzer
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.ninecraft.booket.core.ocr.analyzer

import androidx.annotation.OptIn
import androidx.camera.core.ExperimentalGetImage
import androidx.camera.core.ImageProxy
import com.google.mlkit.vision.common.InputImage
import com.google.mlkit.vision.text.TextRecognizer
import com.orhanobut.logger.Logger
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

/**
* 정적인 카메라 이미지에서 텍스트를 분석하는 클래스
*
* CameraX의 단일 ImageProxy 프레임을 받아 ML Kit을 통해 텍스트를 추출하고 결과를 콜백으로 전달한다.
*
* @param textRecognizer ML Kit의 TextRecognizer 인스턴스
* @param onTextDetected 텍스트 인식 성공 시 호출되는 콜백 (인식된 전체 텍스트 전달)
* @param onFailure 인식 실패 시 호출되는 콜백
*
* 분석이 끝난 후 반드시 imageProxy.close() 호출하여 리소스 해제
*/
class StillTextAnalyzer @AssistedInject constructor(
private val textRecognizer: TextRecognizer,
@Assisted private val onTextDetected: (String) -> Unit,
@Assisted private val onFailure: () -> Unit,
) : TextAnalyzer {

val scope = CoroutineScope(Dispatchers.IO)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

@OptIn(ExperimentalGetImage::class)
override fun analyze(imageProxy: ImageProxy) {
scope.launch {
val mediaImage = imageProxy.image ?: run { imageProxy.close(); return@launch }
val inputImage = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)

suspendCoroutine { continuation ->
textRecognizer.process(inputImage)
.addOnCompleteListener { visionText ->
onTextDetected(visionText.result.text)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
.addOnFailureListener {
onFailure()
}
.addOnCompleteListener {
continuation.resume(Unit)
}
}
}.invokeOnCompletion { exception ->
if (exception != null) {
Logger.e(exception.message ?: "Unknown error")
}
imageProxy.close()
}
}

@AssistedFactory
interface Factory {
fun create(
onTextDetected: (String) -> Unit,
onFailure: () -> Unit,
): StillTextAnalyzer
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.ninecraft.booket.core.ocr.analyzer

import androidx.camera.core.ImageProxy

interface TextAnalyzer {
fun analyze(imageProxy: ImageProxy)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.ninecraft.booket.core.ocr.di

import com.google.mlkit.vision.text.TextRecognition
import com.google.mlkit.vision.text.TextRecognizer
import com.google.mlkit.vision.text.korean.KoreanTextRecognizerOptions
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
object OcrModule {

@Provides
@Singleton
fun provideTextRecognizer(): TextRecognizer =
TextRecognition.getClient(KoreanTextRecognizerOptions.Builder().build())
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.ninecraft.booket.core.designsystem.R
import com.ninecraft.booket.core.designsystem.theme.Neutral950
import com.ninecraft.booket.core.designsystem.theme.ReedTheme
import com.ninecraft.booket.core.designsystem.theme.White

@Composable
fun ReedTopAppBar(
modifier: Modifier = Modifier,
isDark: Boolean = false,
title: String = "",
@DrawableRes startIconRes: Int? = null,
startIconDescription: String = "",
Expand All @@ -38,7 +41,7 @@ fun ReedTopAppBar(
modifier = modifier
.fillMaxWidth()
.height(60.dp)
.background(color = White)
.background(color = if (isDark) Neutral950 else White)
.padding(horizontal = ReedTheme.spacing.spacing2),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically,
Expand All @@ -50,6 +53,7 @@ fun ReedTopAppBar(
Icon(
painter = painterResource(id = startIconRes),
contentDescription = startIconDescription,
tint = if (isDark) White else Color.Unspecified,
)
}
} else {
Expand All @@ -70,6 +74,7 @@ fun ReedTopAppBar(
Icon(
painter = painterResource(id = endIconRes),
contentDescription = endIconDescription,
tint = if (isDark) White else Color.Unspecified,
)
}
} else {
Expand All @@ -81,11 +86,13 @@ fun ReedTopAppBar(
@Composable
fun ReedBackTopAppBar(
modifier: Modifier = Modifier,
isDark: Boolean = false,
title: String = "",
onBackClick: () -> Unit = {},
) {
ReedTopAppBar(
modifier = modifier,
isDark = isDark,
title = title,
startIconRes = R.drawable.ic_chevron_left,
startIconDescription = "Back",
Expand All @@ -96,11 +103,13 @@ fun ReedBackTopAppBar(
@Composable
fun ReedCloseTopAppBar(
modifier: Modifier = Modifier,
isDark: Boolean = false,
title: String = "",
onClose: () -> Unit = {},
) {
ReedTopAppBar(
modifier = modifier,
isDark = isDark,
title = title,
endIconRes = R.drawable.ic_close,
endIconDescription = "Close",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,7 @@ fun LibraryBookItem(
.clip(RoundedCornerShape(size = ReedTheme.radius.sm)),
placeholder = painterResource(designR.drawable.ic_placeholder),
)
Column(
modifier = Modifier
.weight(1f)
.padding(end = ReedTheme.spacing.spacing5),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = book.bookTitle,
color = ReedTheme.colors.contentPrimary,
Expand Down
8 changes: 8 additions & 0 deletions feature/record/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ ksp {

dependencies {
implementations(
projects.core.ocr,

libs.compose.system.ui.controller,

libs.androidx.activity.compose,
libs.androidx.camera.camera2,
libs.androidx.camera.lifecycle,
libs.androidx.camera.view,

libs.logger,
)
}
Loading
Loading