From e4de276215db7690b62a98c00934c81231dc7429 Mon Sep 17 00:00:00 2001 From: TTolsun <112919054+TTolsun@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:27:39 +0900 Subject: [PATCH 1/4] Add offline depth-based bokeh for people and objects --- .omm/data-flow/description.md | 2 + .../camera-engines/app-bokeh/description.md | 5 + .../camera-engines/app-bokeh/meta.yaml | 5 + .../camera-engines/description.md | 6 +- .../camera-engines/meta.yaml | 1 + .../portrait-engine/description.md | 2 +- .omm/state-transitions/description.md | 2 + .../engine-lifecycle/description.md | 8 +- .../portrait-lifecycle/description.md | 2 +- .omm/ui-tool-handoff/description.md | 2 + app/build.gradle.kts | 35 ++++ .../dev/halcamera/camera/AppBokehChecks.kt | 35 ++++ .../halcamera/cli/CliStoreInstrumentation.kt | 15 ++ app/src/main/assets/licenses/MiDaS.txt | 21 +++ .../main/java/dev/halcamera/MainActivity.kt | 34 +++- .../java/dev/halcamera/MainAppBokehBridge.kt | 62 +++++++ .../java/dev/halcamera/MainCameraPicker.kt | 3 +- .../main/java/dev/halcamera/MainCliBridge.kt | 1 + .../java/dev/halcamera/MainDepthBridge.kt | 14 +- .../dev/halcamera/camera/AppBokehProcessor.kt | 108 ++++++++++++ .../dev/halcamera/camera/RelativeDepth.kt | 98 +++++++++++ .../java/dev/halcamera/ui/AppBokehEditor.kt | 165 ++++++++++++++++++ .../dev/halcamera/camera/DepthBokehTest.kt | 47 +++++ docs/design/APP-BOKEH.md | 31 ++++ docs/validation/app-bokeh.md | 30 ++++ guide/_bindings.yaml | 11 ++ guide/_content/_reference/engine/camera2.md | 3 + .../_content/_reference/engine/comparison.md | 2 + guide/_content/_reference/engine/contract.md | 5 +- guide/_content/architecture/constraints.md | 5 + guide/_content/architecture/module-roles.md | 5 + guide/_content/architecture/overview.md | 2 +- guide/_content/architecture/runtime-flow.md | 5 + guide/live.md | 15 ++ tools/docgen/regression.test.mjs | 2 +- tools/docgen/state/evidence.json | 30 ++-- 36 files changed, 781 insertions(+), 38 deletions(-) create mode 100644 .omm/overall-architecture/camera-engines/app-bokeh/description.md create mode 100644 .omm/overall-architecture/camera-engines/app-bokeh/meta.yaml create mode 100644 app/src/androidTest/java/dev/halcamera/camera/AppBokehChecks.kt create mode 100644 app/src/main/assets/licenses/MiDaS.txt create mode 100644 app/src/main/java/dev/halcamera/MainAppBokehBridge.kt create mode 100644 app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt create mode 100644 app/src/main/java/dev/halcamera/camera/RelativeDepth.kt create mode 100644 app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt create mode 100644 app/src/test/java/dev/halcamera/camera/DepthBokehTest.kt create mode 100644 docs/design/APP-BOKEH.md create mode 100644 docs/validation/app-bokeh.md diff --git a/.omm/data-flow/description.md b/.omm/data-flow/description.md index 6dd75040..719b0331 100644 --- a/.omm/data-flow/description.md +++ b/.omm/data-flow/description.md @@ -15,3 +15,5 @@ CLI는 직접 인자 또는 base64url JSON을 받고 shell UID·허용 설정· Multi 모드의 ConcurrentSession은 독립 장치마다 출력과 저장을 관리합니다. 사진은 이미지·CaptureResult 센서 시각을 연결한 장치별 JPEG이며 다른 장치 영상을 합성하지 않습니다. ConcurrentPhotoStore는 JPEG를 DCIM/HALCamera에, 공통 촬영 ID·장치 ID·성공 또는 실패를 Download/HALCamera의 JSON에 기록합니다. 두 엔진은 MultiRecording을 공유하며, 하나의 마이크 AAC를 장치별 MP4에 결합한 뒤 공개합니다. 녹음이나 파일 하나라도 실패하면 해당 녹화 묶음의 공개 파일을 회수합니다. Split 배치는 프리뷰만 바꾸며 인코더 크기는 장치 스트림과 회전에서 정합니다. 이 경로는 Benchmark 계산과 분리됩니다. Portrait는 Depth 메뉴에서 선택하지만 별도 확장 세션의 컬러 미리보기와 OEM JPEG를 받습니다. PortraitCameraEngine은 JPEG를 MediaLibrary로 저장하고 효과·해상도·시각만 촬영 JSON에 남깁니다. 확장 콜백은 벤치마크나 일반 CaptureResult 지표에 섞지 않습니다. + +App Bokeh는 일반 Camera2 Preview+JPEG 세션에서 원본 JPEG를 저장한 뒤 RGB로 디코딩합니다. MiDaS 모델의 Float 상대 깊이와 사용자 선택 지점을 DepthBokeh에 전달하여 배경을 흐리고, 보케 JPEG·깊이 PNG·처리 메타데이터를 MediaLibrary로 저장합니다. HAL Depth나 분석용 YUV를 입력으로 사용하지 않습니다. diff --git a/.omm/overall-architecture/camera-engines/app-bokeh/description.md b/.omm/overall-architecture/camera-engines/app-bokeh/description.md new file mode 100644 index 00000000..74b831c5 --- /dev/null +++ b/.omm/overall-architecture/camera-engines/app-bokeh/description.md @@ -0,0 +1,5 @@ +App Bokeh는 일반 Camera2의 Preview와 JPEG 두 출력을 사용한다. MainAppBokehBridge가 JPEG 촬영·저장을 마친 뒤 AppBokehEditor에 URI를 전달한다. 제조사 확장과 분석용 YUV, 센서 Depth 스트림을 사용하지 않는다. + +AppBokehProcessor는 원본을 EXIF 방향에 맞추고 긴 변을 최대 1600픽셀로 제한한다. MiDaS v2.1 모델은 RGB 사진 한 장에서 256×256 Float 상대 역깊이를 추론한다. RelativeDepth와 DepthBokeh는 Android 의존성 없이 깊이 정규화·선택 위치 표본·배경 혼합과 blur를 계산한다. 결과는 JPEG로 재인코딩하며 별도 깊이 PNG와 모델·원본·선택 위치·강도 메타데이터를 저장한다. 원본은 수정하지 않는다. + +편집기의 worker가 추론·렌더·저장을 처리한다. 변경 세대가 지난 렌더는 표시하지 않고 화면 종료 후에는 UI를 갱신하지 않는다. 화면 복귀는 보관한 원본 URI로 편집을 다시 시작하며 이전 강도·선택 위치는 초기화한다. 상대 깊이는 실제 거리나 센서 DEPTH16이 아니다. Benchmark와 incident ZIP에는 이미지 픽셀을 넣지 않는다. diff --git a/.omm/overall-architecture/camera-engines/app-bokeh/meta.yaml b/.omm/overall-architecture/camera-engines/app-bokeh/meta.yaml new file mode 100644 index 00000000..3a17edbc --- /dev/null +++ b/.omm/overall-architecture/camera-engines/app-bokeh/meta.yaml @@ -0,0 +1,5 @@ +kind: nested-class +title: app-bokeh +children: [] +parentPath: + - camera-engines diff --git a/.omm/overall-architecture/camera-engines/description.md b/.omm/overall-architecture/camera-engines/description.md index d3fab77b..26f285cc 100644 --- a/.omm/overall-architecture/camera-engines/description.md +++ b/.omm/overall-architecture/camera-engines/description.md @@ -1,2 +1,4 @@ -camera/는 CameraEngine 인터페이스와 Camera2Engine·CameraXEngine·DepthCameraEngine·PortraitCameraEngine을 제공합니다. Camera2Engine은 기본 크기 선택 경로와 벤치마크용 명시적 StreamSpec 경로를 구분합니다. Depth는 맵 보기, Portrait는 제조사 Bokeh JPEG 촬영이며 벤치마크 엔진과 구분합니다. 세션별 콜백과 close(done) 완료 통지가 러너에 연결됩니다. -StreamConfiguration은 실제 카메라 출력으로 Callback 목록을 만듭니다. 앱의 YUV JPEG 변환·PIP 합성·GPU 인코더 출력은 Callback에 추가하지 않습니다. +CameraEngine 구현은 Camera2·CameraX·Depth·Portrait로 나뉩니다. Camera2는 Live 기본 크기와 Benchmark의 명시적 StreamSpec을 구분합니다. Depth는 맵 보기, Portrait는 제조사 Bokeh JPEG 촬영입니다. 콜백과 close(done)는 러너에 연결됩니다. +StreamConfiguration의 Callback 목록은 실제 카메라 출력만 포함하며 앱 변환·합성·GPU 출력은 제외합니다. + +App Bokeh는 일반 JPEG 저장 후 앱에서 깊이를 추정하고 배경을 흐립니다. 구현은 app-bokeh 요소에 설명합니다. diff --git a/.omm/overall-architecture/camera-engines/meta.yaml b/.omm/overall-architecture/camera-engines/meta.yaml index 2777768f..a7e6f162 100644 --- a/.omm/overall-architecture/camera-engines/meta.yaml +++ b/.omm/overall-architecture/camera-engines/meta.yaml @@ -8,6 +8,7 @@ children: - camera2-engine - depth-engine - portrait-engine + - app-bokeh - camerax-engine - endpoint-model - endpoint-resolver diff --git a/.omm/overall-architecture/camera-engines/portrait-engine/description.md b/.omm/overall-architecture/camera-engines/portrait-engine/description.md index 9f27bc79..def254c7 100644 --- a/.omm/overall-architecture/camera-engines/portrait-engine/description.md +++ b/.omm/overall-architecture/camera-engines/portrait-engine/description.md @@ -1,5 +1,5 @@ PortraitCameraEngine은 API 31 이상에서 제조사 BOKEH의 CameraExtensionSession을 소유합니다. 공개 카메라의 확장 지원·SurfaceTexture 미리보기 크기·JPEG 크기를 각각 조회하고, 선택한 PortraitConfig의 두 크기로 세션을 구성합니다. 일반 StreamConfigurationMap 크기를 대용으로 쓰거나 실패 때 다른 크기로 재시도하지 않습니다. -Live의 Depth 메뉴는 Off·Portrait (Bokeh)·Depth Map을 구분합니다. Portrait는 컬러 미리보기와 JPEG 촬영이며 센서 Depth 맵을 반환하지 않습니다. 선택하면 Camera2로 전환하고 기존 close(done) 뒤 확장 세션을 엽니다. TextureView의 첫 표시 프레임 뒤 촬영을 허용합니다. 미리보기 15초·JPEG 60초 시간 제한, 실패 후 해제, 늦은 JPEG 차단과 단일 촬영의 한 번뿐인 완료 전달을 둡니다. close(done)는 열기 중인 장치도 닫고 CameraDevice.onClosed 뒤 자원을 회수합니다. 이미 받은 JPEG의 저장 작업은 완료할 수 있습니다. +Live의 Depth 메뉴는 Off·App Bokeh·Portrait (Bokeh)·Depth Map을 구분합니다. Portrait는 컬러 미리보기와 JPEG 촬영이며 센서 Depth 맵을 반환하지 않습니다. 선택하면 Camera2로 전환하고 기존 close(done) 뒤 확장 세션을 엽니다. TextureView의 첫 표시 프레임 뒤 촬영을 허용합니다. 미리보기 15초·JPEG 60초 시간 제한, 실패 후 해제, 늦은 JPEG 차단과 단일 촬영의 한 번뿐인 완료 전달을 둡니다. close(done)는 열기 중인 장치도 닫고 CameraDevice.onClosed 뒤 자원을 회수합니다. 이미 받은 JPEG의 저장 작업은 완료할 수 있습니다. 사진은 OEM JPEG를 그대로 MediaLibrary로 저장하고 JPEG 크기·미리보기 크기·manufacturer_bokeh 효과·시각을 촬영 JSON에 기록합니다. 수동 제어·줌·터치 측광·연사·AEB·RAW·PIP·동영상은 제공하지 않습니다. 일반 Camera2 콜백 지표로 확장 처리를 측정하지 않으며 Benchmark와 별도 경로입니다. Photo·Video·카메라·엔진 전환과 새 CLI 촬영 준비는 Portrait를 종료합니다. diff --git a/.omm/state-transitions/description.md b/.omm/state-transitions/description.md index b0a2ce00..a47c0d99 100644 --- a/.omm/state-transitions/description.md +++ b/.omm/state-transitions/description.md @@ -9,3 +9,5 @@ API 전환은 기존 close 완료 후 새 엔진을 엽니다. 목록 열기와 CLI는 accepted → preparing → running → saving → succeeded로 진행합니다. 실패·취소·중단, ID 중복과 충돌 처리는 하위 cli-request에서 설명합니다. 프로세스 재시작은 미완료 요청을 재실행하지 않습니다. 사진 저장 취소는 실제 저장과 경합할 수 있습니다. Live 스트림 적용은 사진·녹화·저장·권한 요청·CLI 작업 중에 거절합니다. 기존 close(done)가 끝난 뒤 새 엔진에 설정을 전달하고 구성 성공값을 카메라·엔진별로 보관합니다. 실패하면 크기를 자동 대체하지 않으며 사용자가 직전 정상 구성으로 복구할 수 있습니다. Activity 재생성에서도 요청값과 정상 구성을 복원합니다. + +App Bokeh는 JPEG 저장 후 편집하며 중복 촬영·CLI를 막습니다. worker는 오래된 결과를 버리고 실패해도 원본을 보존합니다. 중단 시 URI를 보관해 복귀 후 다시 편집합니다. 상세는 app-bokeh 요소에 있습니다. diff --git a/.omm/state-transitions/engine-lifecycle/description.md b/.omm/state-transitions/engine-lifecycle/description.md index b2e01ef7..9748e2e3 100644 --- a/.omm/state-transitions/engine-lifecycle/description.md +++ b/.omm/state-transitions/engine-lifecycle/description.md @@ -1,7 +1,7 @@ -Camera2Engine은 HandlerThread에서 카메라를 열고 세션과 반복 요청을 구성합니다. CameraXEngine은 ProcessCameraProvider를 사용하며, 자세한 수명 주기는 하위 요소 camerax-lifecycle에 있습니다. 두 엔진은 active 상태로 늦은 신호를 걸러 내며 close(done) 완료 후 다음 열기를 진행합니다. +Camera2Engine은 HandlerThread에서 열기·세션·반복 요청을 처리합니다. CameraXEngine의 ProcessCameraProvider 수명주기는 camerax-lifecycle에 있습니다. 두 엔진은 active로 늦은 신호를 차단하고 close(done) 뒤 다음 엔진을 엽니다. -Live 녹화는 선택한 설정, Benchmark 녹화는 profile을 사용합니다. 녹화 상태는 overall-architecture/camera-engines/camera2-engine/live-recorder, 측정 순서는 runner-sequence에 있습니다. PIP 종료 전에 메인 사진 대기를 취소하여 순환 대기를 막습니다. +Live 녹화는 선택 설정, Benchmark는 profile을 사용합니다. 녹화 상태는 overall-architecture/camera-engines/camera2-engine/live-recorder에 있습니다. PIP 종료 전 메인 사진 대기를 취소하여 순환 대기를 막습니다. -단계 전이의 시간 제한과 close 완료 처리는 runner-sequence에 있습니다. +측정 순서·시간 제한·close 처리는 runner-sequence에 있습니다. -DepthCameraEngine은 DEPTH16만 구성합니다. 열기 중 취소도 onOpened→onClosed 후 완료하며 active로 늦은 신호를 차단합니다. 첫 맵은 요청 후 8초까지 기다리고 해상도 대체·일반 Camera2 재시도는 하지 않습니다. +센서 Depth의 별도 세션과 시간 제한은 depth-engine 요소에 설명합니다. diff --git a/.omm/state-transitions/engine-lifecycle/portrait-lifecycle/description.md b/.omm/state-transitions/engine-lifecycle/portrait-lifecycle/description.md index cb948315..b6af1796 100644 --- a/.omm/state-transitions/engine-lifecycle/portrait-lifecycle/description.md +++ b/.omm/state-transitions/engine-lifecycle/portrait-lifecycle/description.md @@ -1 +1 @@ -Depth 메뉴의 Off·Portrait (Bokeh)·Depth Map을 구분합니다. Portrait 선택은 기존 close(done) 뒤 CameraExtensionSession을 열며, 첫 표시 프레임 이후 셔터를 허용합니다. JPEG를 받기 전 종료되면 촬영을 실패로 한 번 완료하고, 이미 받은 JPEG의 저장은 마칠 수 있습니다. Photo·Video·카메라·엔진 전환과 새 CLI 촬영은 Portrait를 종료합니다. 실패 뒤에도 Photo 복귀와 다른 해상도 선택을 허용합니다. +Depth 메뉴의 Off·App Bokeh·Portrait (Bokeh)·Depth Map을 구분합니다. Portrait 선택은 기존 close(done) 뒤 CameraExtensionSession을 열며, 첫 표시 프레임 이후 셔터를 허용합니다. JPEG를 받기 전 종료되면 촬영을 실패로 한 번 완료하고, 이미 받은 JPEG의 저장은 마칠 수 있습니다. Photo·Video·카메라·엔진 전환과 새 CLI 촬영은 Portrait를 종료합니다. 실패 뒤에도 Photo 복귀와 다른 해상도 선택을 허용합니다. diff --git a/.omm/ui-tool-handoff/description.md b/.omm/ui-tool-handoff/description.md index faefc451..9846a761 100644 --- a/.omm/ui-tool-handoff/description.md +++ b/.omm/ui-tool-handoff/description.md @@ -11,3 +11,5 @@ Multi도 Live의 close(done) 뒤 ConcurrentCameraActivity를 엽니다. Live에 Depth 보기에서도 Lab 진입 전에 close(done)를 기다립니다. 촬영 모드를 바꾸지 않고 Lab에서 돌아오면 선택한 Depth 해상도를 다시 엽니다. Depth 보기 중에는 크기 표시로 Live Streams를 여는 동작을 비활성화합니다. Portrait에서도 Lab 진입 전에 close(done)를 기다리고 복귀 때 선택 해상도를 다시 엽니다. Portrait의 크기 표시를 누르면 일반 Live Streams 대신 Depth 메뉴를 엽니다. Lab에서 일반 Live Streams 설정을 적용하면 Portrait를 종료하고 해당 설정을 사용합니다. + +App Bokeh도 Depth 메뉴에서 선택하지만 일반 Preview+JPEG 세션을 사용합니다. 셔터 완료 후 편집 Dialog가 원본 URI를 받아 열립니다. 편집 중에는 Lab·카메라·모드 전환을 막고 닫은 뒤 다시 허용합니다. 카메라·엔진 변경, 일반 Live Streams 적용이나 새 CLI 촬영 준비는 App Bokeh 모드를 종료합니다. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d66d0773..f253a118 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,4 +1,8 @@ import java.util.Properties +import java.security.MessageDigest +import java.net.URI +import java.nio.file.Files +import java.nio.file.StandardCopyOption plugins { id("com.android.application") @@ -49,8 +53,39 @@ android { kotlinOptions { jvmTarget = "17" } lint { abortOnError = true } sourceSets.getByName("androidTest").assets.srcDir("../tools/halcam/fixtures") + sourceSets.getByName("main").assets.srcDir(layout.buildDirectory.dir("generated/depthAssets")) + androidResources { noCompress += "tflite" } } + +// Official, MIT-licensed MiDaS v2.1 mobile model. Never accept an unverified download. +val depthModel = layout.buildDirectory.file("generated/depthAssets/midas-v2_1.tflite") +val prepareDepthModel by tasks.registering { + outputs.file(depthModel) + val expected = "93d871071edff1218973ce25ee27ce95ccd20450c70a55e1b89efa3f5a772cdd" + fun hash(file: File): String = MessageDigest.getInstance("SHA-256").let { digest -> + file.inputStream().use { input -> + val bytes = ByteArray(65536) + while (true) { val n = input.read(bytes); if (n < 0) break; digest.update(bytes, 0, n) } + } + digest.digest().joinToString("") { "%02x".format(it) } + } + outputs.upToDateWhen { depthModel.get().asFile.let { it.exists() && hash(it) == expected } } + doLast { + val destination = depthModel.get().asFile + destination.parentFile.mkdirs() + val temporary = File(destination.parentFile, "model.download") + try { + val connection = URI("https://github.com/isl-org/MiDaS/releases/download/v2_1/model_opt.tflite").toURL().openConnection() + connection.connectTimeout = 30000; connection.readTimeout = 120000 + connection.getInputStream().use { input -> temporary.outputStream().use { input.copyTo(it) } } + check(hash(temporary) == expected) { "MiDaS model SHA-256 mismatch" } + Files.move(temporary.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING) + } finally { temporary.delete() } + } +} +tasks.named("preBuild").configure { dependsOn(prepareDepthModel) } dependencies { + implementation("com.google.ai.edge.litert:litert:1.4.2") implementation("androidx.activity:activity-ktx:1.13.0") implementation("androidx.core:core-ktx:1.18.0") implementation("androidx.exifinterface:exifinterface:1.4.2") diff --git a/app/src/androidTest/java/dev/halcamera/camera/AppBokehChecks.kt b/app/src/androidTest/java/dev/halcamera/camera/AppBokehChecks.kt new file mode 100644 index 00000000..a7dc50a6 --- /dev/null +++ b/app/src/androidTest/java/dev/halcamera/camera/AppBokehChecks.kt @@ -0,0 +1,35 @@ +package dev.halcamera.camera + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import java.io.File + +object AppBokehChecks { + fun run(context: Context, path: String, focusX: Float = .5f, focusY: Float = .5f): String { + val processor = AppBokehProcessor(context) + val source = Uri.fromFile(File(path)) + val scene = processor.load(source) + check(scene.depth.width == 256 && scene.depth.height == 256) + check(scene.original.width <= 1600 && scene.original.height <= 1600) + val dir = File(context.getExternalFilesDir(null), "app-bokeh-check").apply { mkdirs() } + File(dir, "original.png").outputStream().use { scene.original.compress(Bitmap.CompressFormat.PNG, 100, it) } + File(dir, "depth.png").outputStream().use { AppBokehProcessor.depthBitmap(scene.depth).compress(Bitmap.CompressFormat.PNG, 100, it) } + for (strength in listOf(50, 100)) { + val rendered = processor.render(scene, focusX, focusY, strength) + File(dir, "bokeh-$strength.jpg").outputStream().use { rendered.compress(Bitmap.CompressFormat.JPEG, 95, it) } + rendered.recycle() + } + val result = processor.render(scene, focusX, focusY, 80) + File(dir, "bokeh.jpg").outputStream().use { result.compress(Bitmap.CompressFormat.JPEG, 95, it) } + val zero = processor.render(scene, focusX, focusY, 0) + check(zero.sameAs(scene.original)) { "Zero strength must preserve source pixels" } + val artifacts = processor.save(scene, result, source, focusX, focusY, 80) + check(artifacts.size == 3 && artifacts.all { it.bytes > 0 }) + artifacts.filter { it.mime.startsWith("image/") }.forEach { artifact -> + context.contentResolver.openInputStream(artifact.uri)!!.use { check(BitmapFactory.decodeStream(it) != null) } + } + return "APP_BOKEH_OK source=${scene.original.width}x${scene.original.height} depth=256x256 focus=$focusX,$focusY loadAndInferenceMs=${scene.elapsedMs} saved=${artifacts.joinToString { it.name }}" + } +} diff --git a/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt b/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt index a4a992fe..1f6070d8 100644 --- a/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt +++ b/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt @@ -9,6 +9,9 @@ import java.util.UUID /** Device tests use real JSONObject and AtomicFile, with a deterministic clock and isolated files. */ class CliStoreInstrumentation : Instrumentation() { + private var appBokehSource: String? = null + private var appBokehFocusX = .5f + private var appBokehFocusY = .5f private var exportReports = false private var probeLaunchDiagnostics = false private var snapshotFailures = false @@ -24,6 +27,9 @@ class CliStoreInstrumentation : Instrumentation() { private var portraitUi = false override fun onCreate(arguments: Bundle?) { super.onCreate(arguments) + appBokehSource = arguments?.getString("app_bokeh_source") + appBokehFocusX = arguments?.getString("app_bokeh_focus_x")?.toFloat() ?: .5f + appBokehFocusY = arguments?.getString("app_bokeh_focus_y")?.toFloat() ?: .5f exportReports = arguments?.getString("export_reports") == "true" probeLaunchDiagnostics = arguments?.getString("probe_launch_diagnostics") == "true" snapshotFailures = arguments?.getString("snapshot_failures") == "true" @@ -41,6 +47,15 @@ class CliStoreInstrumentation : Instrumentation() { } override fun onStart() { + appBokehSource?.let { path -> + try { + val result = dev.halcamera.camera.AppBokehChecks.run(targetContext, path, appBokehFocusX, appBokehFocusY) + finish(Activity.RESULT_OK, Bundle().apply { putString("stream", "$result\n") }) + } catch (error: Throwable) { + finish(Activity.RESULT_CANCELED, Bundle().apply { putString("stream", "APP_BOKEH_FAILED: ${error.stackTraceToString()}\n") }) + } + return + } if (portraitChecks || portraitMetadata || portraitUi) { try { val result = when { diff --git a/app/src/main/assets/licenses/MiDaS.txt b/app/src/main/assets/licenses/MiDaS.txt new file mode 100644 index 00000000..277b5c11 --- /dev/null +++ b/app/src/main/assets/licenses/MiDaS.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Intel ISL (Intel Intelligent Systems Lab) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/app/src/main/java/dev/halcamera/MainActivity.kt b/app/src/main/java/dev/halcamera/MainActivity.kt index 4e4d7b9b..b29ca903 100644 --- a/app/src/main/java/dev/halcamera/MainActivity.kt +++ b/app/src/main/java/dev/halcamera/MainActivity.kt @@ -69,7 +69,7 @@ class MainActivity : ComponentActivity() { private var engineCameraId: String? = null internal var closing = false internal var resumed = false - private var destroyed = false + internal var destroyed = false internal var paused = false internal var engineName = "CameraX" internal var cameraId = "" @@ -132,6 +132,10 @@ class MainActivity : ComponentActivity() { internal var videoMode = false internal var depthSize: LiveSize? = null internal var portraitConfig: PortraitConfig? = null + internal var appBokehMode = false + internal var appBokehCapturing = false + internal var appBokehEditor: AppBokehEditor? = null + internal var appBokehSource: String? = null internal lateinit var depthModeButton: Button internal var recordingVideo = false internal var stoppingRecording = false @@ -178,6 +182,7 @@ class MainActivity : ComponentActivity() { @Suppress("DEPRECATION") val settings = data.getSerializableExtra(LiveStreamsActivity.EXTRA_SETTINGS) as? LiveStreamSettings portraitConfig = null + appBokehMode = false if (settings == null) streamSettings.remove(streamKey()) else streamSettings[streamKey()] = settings telemetry.event(sessionId, "live_streams_changed", settings?.metadata() ?: mapOf("mode" to "default")) paused = false @@ -255,6 +260,8 @@ class MainActivity : ComponentActivity() { depthSize = savedInstanceState?.getSerializable("depthSize") as? LiveSize @Suppress("DEPRECATION") portraitConfig = savedInstanceState?.getSerializable("portraitConfig") as? PortraitConfig + appBokehMode = savedInstanceState?.getBoolean("appBokehMode") ?: false + appBokehSource = savedInstanceState?.getString("appBokehSource") @Suppress("DEPRECATION", "UNCHECKED_CAST") (savedInstanceState?.getSerializable("liveStreams") as? HashMap)?.let(streamSettings::putAll) @Suppress("DEPRECATION", "UNCHECKED_CAST") @@ -271,6 +278,8 @@ class MainActivity : ComponentActivity() { outState.putBoolean("videoMode", videoMode) outState.putSerializable("depthSize", depthSize) outState.putSerializable("portraitConfig", portraitConfig) + outState.putBoolean("appBokehMode", appBokehMode) + outState.putString("appBokehSource", appBokehSource) outState.putSerializable("liveStreams", HashMap(streamSettings)) outState.putSerializable("goodStreams", HashMap(goodStreams)) outState.putBoolean("returningFromLab", returningFromLab) @@ -290,6 +299,7 @@ class MainActivity : ComponentActivity() { } override fun onResume() { super.onResume() + appBokehSource?.takeIf { appBokehEditor == null }?.let { openAppBokehEditor(android.net.Uri.parse(it)) } when { returningFromLab -> { returningFromLab = false @@ -307,6 +317,9 @@ class MainActivity : ComponentActivity() { } } override fun onStop() { + val retainedBokehSource = appBokehSource + appBokehEditor?.dismiss() + appBokehSource = retainedBokehSource cliSequence.cancel() cli.detach(liveCli) zoomControl.collapse(animate = false) @@ -645,9 +658,12 @@ class MainActivity : ComponentActivity() { else if(recordingVideo) stopRecording() else if(videoMode) { withMediaPermissions(true) { (engine as? MediaCapture)?.startRecording() } - } else withMediaPermissions(false) { controlBar.controls.let { if (it.bracket) bursts.bracket(sessionId, it, controlBar.support) else engine?.capture() } } + } else withMediaPermissions(false) { + if (appBokehMode) captureAppBokeh() + else controlBar.controls.let { if (it.bracket) bursts.bracket(sessionId, it, controlBar.support) else engine?.capture() } + } } - bindBurstInput({ !videoMode && portraitConfig == null && !mediaBusy() && !controlBar.controls.bracket && cli.active == null }, + bindBurstInput({ !videoMode && !appBokehMode && portraitConfig == null && !mediaBusy() && !controlBar.controls.bracket && cli.active == null }, { withMediaPermissions(false,it) }, { bursts.hold(sessionId) }, bursts::release) } captureRow.addView(mediaButton,LinearLayout.LayoutParams(captureSize,captureSize).apply { marginStart=dp(12); marginEnd=dp(12) }) @@ -714,7 +730,7 @@ class MainActivity : ComponentActivity() { val idle=!recordingVideo && bursts.run==null cli.setUiBusy(liveCli, recordingVideo || stoppingRecording || pendingPermissionAction != null || mediaBusy()) listOf(photoModeButton,videoModeButton).forEachIndexed { index, button -> - val selected=depthSize == null && portraitConfig == null && (index==1)==videoMode + val selected=depthSize == null && portraitConfig == null && !appBokehMode && (index==1)==videoMode button.isSelected=selected button.isEnabled=(ready || depthSize != null || portraitConfig != null) && idle && !closing && !mediaBusy() && pendingPermissionAction == null button.setTextColor(if(selected) Look.onDark else Look.onDarkMuted) @@ -779,6 +795,12 @@ class MainActivity : ComponentActivity() { } updateDepthControls(callbackGraph.isVisible, topBar.height + dp(8), bottomBar.height + dp(64)) updatePortraitControls() + if (appBokehMode) { + controlBar.bind(false, false, false); manualPanel.close() + physicalPipButton.isEnabled = false; physicalPipButton.alpha = .4f + mediaButton.contentDescription = "Take photo for App Bokeh" + if (appBokehCapturing || appBokehEditor != null) mediaButton.isEnabled = false + } listOf(engineButton,cameraShortcut,photoModeButton,videoModeButton,mediaButton,galleryButton,labButton).forEach { it.alpha=if(it.isEnabled) 1f else 0.4f } @@ -789,8 +811,10 @@ class MainActivity : ComponentActivity() { engineName=name; resetModeSettings(); updateCameraChoices(); restartCamera() } internal fun resetModeSettings() { + if (appBokehMode) captureFeedback.clearResult() depthSize = null portraitConfig = null + appBokehMode = false if (::callbackGraph.isInitialized) showCallbacks(false) pendingPip = null pipUi.resetMode() @@ -864,7 +888,7 @@ class MainActivity : ComponentActivity() { } private fun pipSources(): List = (engine as? PipCamera)?.pipSources.orEmpty() - internal fun mediaBusy() = pipUi.busy || (engine as? MediaCapture)?.mediaBusy == true || bursts.run != null + internal fun mediaBusy() = appBokehCapturing || appBokehEditor != null || pipUi.busy || (engine as? MediaCapture)?.mediaBusy == true || bursts.run != null /** One snapshot at a time; failures leave recording active and unsupported cameras explain why. */ private fun takeSnapshot() { if (cli.active != null) return diff --git a/app/src/main/java/dev/halcamera/MainAppBokehBridge.kt b/app/src/main/java/dev/halcamera/MainAppBokehBridge.kt new file mode 100644 index 00000000..964638cb --- /dev/null +++ b/app/src/main/java/dev/halcamera/MainAppBokehBridge.kt @@ -0,0 +1,62 @@ +package dev.halcamera + +import android.net.Uri +import dev.halcamera.camera.MediaCapture +import dev.halcamera.camera.liveStreamSupport +import dev.halcamera.ui.AppBokehEditor +import java.util.UUID + +/** Uses the ordinary photo session; the saved JPEG is the only inference input. */ +internal fun MainActivity.chooseAppBokeh() { + if (!resumed || closing || mediaBusy() || cli.active != null || recordingVideo || stoppingRecording) return + val id = cameraId + io.execute { + val selected = runCatching { + liveStreamSupport(manager.getCameraCharacteristics(id)).defaults().copy(yuv = null).also { + require(it.jpeg != null) { "App Bokeh requires a JPEG output on this camera" } + } + } + main.post { + if (!resumed || closing || cameraId != id || mediaBusy() || cli.active != null || recordingVideo || stoppingRecording) return@post + selected.fold({ settings -> + recorder.finish("app_bokeh_selected")?.let(::exportCliIncident) + resetModeSettings() + engineName = "Camera2"; videoMode = false; appBokehMode = true; paused = false + streamSettings[streamKey()] = settings + captureFeedback.showResult("App Bokeh · Capture, then tap the subject") + updateCameraChoices(); restartCamera() + }, { captureFeedback.showResult("App Bokeh unavailable: ${it.message}") }) + } + } +} + +internal fun MainActivity.captureAppBokeh() { + if (!resumed || !ready || closing || mediaBusy() || cli.active != null) return + val camera = engine as? MediaCapture ?: return + appBokehCapturing = true + updateMediaControls() + camera.capturePhoto(UUID.randomUUID().toString()) { result -> + main.post { + appBokehCapturing = false + result.fold({ photo -> + val jpeg = photo.artifacts.firstOrNull { it.mime == "image/jpeg" } + if (jpeg == null) { if (resumed) captureFeedback.showResult("App Bokeh needs a JPEG photo. Enable JPEG in Live Streams.") } + else { + appBokehSource = jpeg.uri.toString() + if (resumed && !destroyed) openAppBokehEditor(jpeg.uri) + } + }, { if (resumed) captureFeedback.showResult("App Bokeh capture failed: ${it.message}") }) + if (!destroyed) updateMediaControls() + } + } +} + +internal fun MainActivity.openAppBokehEditor(uri: Uri) { + if (!resumed || destroyed || appBokehEditor != null) return + appBokehSource = uri.toString() + appBokehEditor = AppBokehEditor(this, uri) { + appBokehEditor = null; appBokehSource = null + if (!destroyed) updateMediaControls() + }.also { it.show() } + updateMediaControls() +} diff --git a/app/src/main/java/dev/halcamera/MainCameraPicker.kt b/app/src/main/java/dev/halcamera/MainCameraPicker.kt index 875268d3..1ea55a5d 100644 --- a/app/src/main/java/dev/halcamera/MainCameraPicker.kt +++ b/app/src/main/java/dev/halcamera/MainCameraPicker.kt @@ -22,7 +22,8 @@ internal fun MainActivity.selectCamera() { pendingPermissionAction = null recorder.finish("camera_changed")?.let(::exportCliIncident) cameraId = chosen; zoomRatio = 1f - depthSize = null; portraitConfig = null + if (appBokehMode) captureFeedback.clearResult() + depthSize = null; portraitConfig = null; appBokehMode = false resetControls(); updateCameraChoices(); restartCamera() } } diff --git a/app/src/main/java/dev/halcamera/MainCliBridge.kt b/app/src/main/java/dev/halcamera/MainCliBridge.kt index f50d0759..af771f76 100644 --- a/app/src/main/java/dev/halcamera/MainCliBridge.kt +++ b/app/src/main/java/dev/halcamera/MainCliBridge.kt @@ -34,6 +34,7 @@ internal fun MainActivity.createLiveCli(): LiveController = LiveController(cli, captureFeedback.clearResult() depthSize = null portraitConfig = null + appBokehMode = false showCallbacks(false) val modeChanged = engineName != (command.engine ?: "Camera2") || videoMode != command.videoMode cameraId = requireNotNull(command.camera); engineName = command.engine ?: "Camera2" diff --git a/app/src/main/java/dev/halcamera/MainDepthBridge.kt b/app/src/main/java/dev/halcamera/MainDepthBridge.kt index 1ce3bb1f..367e293b 100644 --- a/app/src/main/java/dev/halcamera/MainDepthBridge.kt +++ b/app/src/main/java/dev/halcamera/MainDepthBridge.kt @@ -20,18 +20,20 @@ internal fun MainActivity.chooseDepth() { if (!resumed || closing || cameraId != id || cli.active != null || mediaBusy() || recordingVideo || stoppingRecording || pendingPermissionAction != null) return@post val choices = listOf(LiveChoiceSheet.Choice("Off", "Return to Photo"), + LiveChoiceSheet.Choice("App Bokeh", if (appBokehMode) "Active · Tap shutter, then choose the subject" else "People and objects · Offline estimated depth · Edit after capture"), LiveChoiceSheet.Choice("Portrait (Bokeh)", portraitConfig?.let { "Active · JPEG ${it.jpeg} · Change resolution" } ?: if (portrait.available) "Manufacturer background blur · Photos" else portrait.unavailableReason.orEmpty(), portrait.available), LiveChoiceSheet.Choice("Depth Map", depthSize?.let { "Active · DEPTH16 $it · Change resolution" } ?: if (depth?.available == true) "View sensor DEPTH16 · No photo capture" else depth?.unavailableReason ?: "Cannot read Depth capabilities.", depth?.available == true)) LiveChoiceSheet.show(this, "Depth · Portrait", choices, - if (depthSize == null && portraitConfig == null && !videoMode) 0 else -1) { index -> + if (appBokehMode) 1 else if (depthSize == null && portraitConfig == null && !videoMode) 0 else -1) { index -> if (!resumed || closing || cameraId != id || cli.active != null || mediaBusy() || recordingVideo || stoppingRecording || pendingPermissionAction != null) return@show when (index) { 0 -> selectMode(false) - 1 -> choosePortrait(portrait) - 2 -> depth?.let { chooseDepthMap(it) } + 1 -> chooseAppBokeh() + 2 -> choosePortrait(portrait) + 3 -> depth?.let { chooseDepthMap(it) } } } } @@ -80,12 +82,12 @@ internal fun MainActivity.createDepthEngine(released: String?, status: (String, internal fun MainActivity.updateDepthControls(callbacks: Boolean, topInset: Int, bottomInset: Int) { depthModeButton.isEnabled = resumed && !closing && !recordingVideo && bursts.run == null && !stoppingRecording && !mediaBusy() && cli.active == null && pendingPermissionAction == null && cameraId.isNotEmpty() - val selected = depthSize != null || portraitConfig != null + val selected = depthSize != null || portraitConfig != null || appBokehMode depthModeButton.isSelected = selected depthModeButton.setTextColor(if (selected) Look.onDark else Look.onDarkMuted) depthModeButton.setTypeface(null, if (selected) Typeface.BOLD else Typeface.NORMAL) depthModeButton.alpha = if (depthModeButton.isEnabled) 1f else .4f - depthModeButton.contentDescription = "Depth and Portrait: choose manufacturer Bokeh photos or a sensor Depth map" + depthModeButton.contentDescription = "Depth and Portrait: choose App Bokeh, manufacturer Bokeh, or a sensor Depth map" ViewCompat.setStateDescription(depthModeButton, if (selected) "Selected" else "Not selected") if (depthSize != null) { mediaButton.isEnabled = false @@ -105,7 +107,7 @@ internal fun MainActivity.updateDepthControls(callbacks: Boolean, topInset: Int, internal fun MainActivity.selectMode(video: Boolean) { if (cli.active != null) return if (recordingVideo || stoppingRecording || closing || pendingPermissionAction != null || - mediaBusy() || (depthSize == null && portraitConfig == null && (!ready || videoMode==video))) return + mediaBusy() || (depthSize == null && portraitConfig == null && !appBokehMode && (!ready || videoMode==video))) return pendingPermissionAction=null videoMode=video resetModeSettings() diff --git a/app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt b/app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt new file mode 100644 index 00000000..ff7b9e46 --- /dev/null +++ b/app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt @@ -0,0 +1,108 @@ +package dev.halcamera.camera + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.net.Uri +import androidx.exifinterface.media.ExifInterface +import org.tensorflow.lite.DataType +import org.tensorflow.lite.Interpreter +import java.io.FileInputStream +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.channels.FileChannel + +/** Offline single-RGB relative-depth inference. No camera extension, person detector or network. */ +class AppBokehProcessor(context: Context) { + private val context = context.applicationContext + data class Scene(val original: Bitmap, val depth: RelativeDepth, val elapsedMs: Long) + + fun load(uri: Uri): Scene { + val start = android.os.SystemClock.elapsedRealtime() + val resolver = context.contentResolver + val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } + resolver.openInputStream(uri)!!.use { BitmapFactory.decodeStream(it, null, bounds) } + require(bounds.outWidth > 0 && bounds.outHeight > 0) { "Cannot read source photo" } + var sample = 1 + while (maxOf(bounds.outWidth, bounds.outHeight) / sample > 3200) sample *= 2 + val decoded = resolver.openInputStream(uri)!!.use { + BitmapFactory.decodeStream(it, null, BitmapFactory.Options().apply { inSampleSize = sample; inPreferredConfig = Bitmap.Config.ARGB_8888 }) + } ?: error("Cannot decode source photo") + val exif = resolver.openInputStream(uri)!!.use { ExifInterface(it) } + val matrix = Matrix().apply { + if (exif.isFlipped) postScale(-1f, 1f) + postRotate(exif.rotationDegrees.toFloat()) + } + val upright = Bitmap.createBitmap(decoded, 0, 0, decoded.width, decoded.height, matrix, true) + if (upright !== decoded) decoded.recycle() + val scale = minOf(1f, MAX_EDGE.toFloat() / maxOf(upright.width, upright.height)) + val original = Bitmap.createScaledBitmap(upright, (upright.width * scale).toInt().coerceAtLeast(1), + (upright.height * scale).toInt().coerceAtLeast(1), true) + if (original !== upright) upright.recycle() + val depth = infer(original) + return Scene(original, depth, android.os.SystemClock.elapsedRealtime() - start) + } + + fun infer(bitmap: Bitmap): RelativeDepth { + val model = context.assets.openFd("midas-v2_1.tflite").use { asset -> + FileInputStream(asset.fileDescriptor).use { it.channel.map(FileChannel.MapMode.READ_ONLY, asset.startOffset, asset.declaredLength) } + } + return Interpreter(model, Interpreter.Options().setNumThreads(4)).use { interpreter -> + val inputTensor = interpreter.getInputTensor(0) + val outputTensor = interpreter.getOutputTensor(0) + val shape = inputTensor.shape() + check(inputTensor.dataType() == DataType.FLOAT32 && shape.contentEquals(intArrayOf(1, 256, 256, 3))) { "Unexpected depth input tensor" } + check(outputTensor.dataType() == DataType.FLOAT32 && outputTensor.numElements() == 256 * 256) { "Unexpected depth output tensor" } + val resized = Bitmap.createScaledBitmap(bitmap, 256, 256, true) + val pixels = IntArray(256 * 256) + resized.getPixels(pixels, 0, 256, 0, 0, 256, 256) + if (resized !== bitmap) resized.recycle() + val input = ByteBuffer.allocateDirect(pixels.size * 3 * 4).order(ByteOrder.nativeOrder()) + // Matches the official MiDaS Android model_opt.tflite example (RGB, mean 115, std 58). + pixels.forEach { pixel -> for (shift in intArrayOf(16, 8, 0)) input.putFloat((((pixel ushr shift) and 255) - 115f) / 58f) } + input.rewind() + val output = ByteBuffer.allocateDirect(pixels.size * 4).order(ByteOrder.nativeOrder()) + interpreter.run(input, output) + output.rewind() + val raw = FloatArray(pixels.size) + output.asFloatBuffer().get(raw) + RelativeDepth.normalize(256, 256, raw) + } + } + + fun render(scene: Scene, x: Float, y: Float, strength: Int): Bitmap { + val bitmap = scene.original + if (strength == 0) return bitmap + val pixels = IntArray(bitmap.width * bitmap.height) + bitmap.getPixels(pixels, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height) + return Bitmap.createBitmap(DepthBokeh.render(pixels, bitmap.width, bitmap.height, scene.depth, x, y, strength), + bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888) + } + + fun save(scene: Scene, result: Bitmap, source: Uri, x: Float, y: Float, strength: Int): List { + val library = MediaLibrary(context) + val name = library.name() + "_APP_BOKEH" + return library.transaction { + val jpeg = save("$name.jpg", "image/jpeg") { check(result.compress(Bitmap.CompressFormat.JPEG, 95, it)) } + val depth = save("${name}_depth.png", "image/png") { check(depthBitmap(scene.depth).compress(Bitmap.CompressFormat.PNG, 100, it)) } + val metadata = mapOf("schema" to 1, "effect" to "app_depth_bokeh", "sourceUri" to source.toString(), + "model" to "MiDaS v2.1 model_opt", "modelSha256" to MODEL_SHA256, "depthKind" to "relative_inverse_depth", + "depthNear" to "white", "metricDepth" to false, "focusX" to x, "focusY" to y, + "focusDepth" to scene.depth.focus(x, y), "strength" to strength, + "width" to result.width, "height" to result.height, "inferenceMs" to scene.elapsedMs, + "outputs" to listOf(jpeg.name, depth.name)) + val json = save("${name}_metadata.json", "application/json") { it.write(org.json.JSONObject(metadata).toString(2).toByteArray()) } + listOf(jpeg, depth, json) + } + } + + companion object { + const val MAX_EDGE = 1600 + const val MODEL_SHA256 = "93d871071edff1218973ce25ee27ce95ccd20450c70a55e1b89efa3f5a772cdd" + fun depthBitmap(depth: RelativeDepth): Bitmap = Bitmap.createBitmap(IntArray(depth.values.size) { + val value = (depth.values[it] * 255).toInt().coerceIn(0, 255) + -0x1000000 or (value shl 16) or (value shl 8) or value + }, depth.width, depth.height, Bitmap.Config.ARGB_8888) + } +} diff --git a/app/src/main/java/dev/halcamera/camera/RelativeDepth.kt b/app/src/main/java/dev/halcamera/camera/RelativeDepth.kt new file mode 100644 index 00000000..65570fd1 --- /dev/null +++ b/app/src/main/java/dev/halcamera/camera/RelativeDepth.kt @@ -0,0 +1,98 @@ +package dev.halcamera.camera + +import kotlin.math.roundToInt + +/** Relative inverse depth, not metres: 1 is nearer, 0 is farther. */ +class RelativeDepth(val width: Int, val height: Int, val values: FloatArray) { + init { require(width > 0 && height > 0 && values.size == width * height && values.all { it.isFinite() && it in 0f..1f }) } + + fun at(x: Float, y: Float): Float { + val px = x.coerceIn(0f, 1f) * (width - 1) + val py = y.coerceIn(0f, 1f) * (height - 1) + val x0 = px.toInt(); val y0 = py.toInt() + val x1 = (x0 + 1).coerceAtMost(width - 1); val y1 = (y0 + 1).coerceAtMost(height - 1) + val a = px - x0; val b = py - y0 + return (values[y0 * width + x0] * (1 - a) + values[y0 * width + x1] * a) * (1 - b) + + (values[y1 * width + x0] * (1 - a) + values[y1 * width + x1] * a) * b + } + + fun focus(x: Float, y: Float): Float { + val samples = FloatArray(9) { i -> at(x + (i % 3 - 1).toFloat() / width, y + (i / 3 - 1).toFloat() / height) } + samples.sort() + return samples[4] + } + + companion object { + fun normalize(width: Int, height: Int, raw: FloatArray): RelativeDepth { + require(raw.size == width * height && raw.all(Float::isFinite)) { "Depth model returned invalid values" } + val sorted = raw.sorted() + val low = sorted[(sorted.size * .02f).toInt()] + val high = sorted[((sorted.size - 1) * .98f).toInt()] + require(high - low > 1e-5f) { "Cannot distinguish depth in this scene. Try a textured subject and background." } + return RelativeDepth(width, height, FloatArray(raw.size) { ((raw[it] - low) / (high - low)).coerceIn(0f, 1f) }) + } + } +} + +/** CPU background-only blur. Foreground colour is excluded from the blur kernel to limit halos. */ +object DepthBokeh { + fun amount(depth: Float, focus: Float): Float { + val t = ((focus - depth - .04f) / .25f).coerceIn(0f, 1f) + return t * t * (3f - 2f * t) + } + + fun render(pixels: IntArray, width: Int, height: Int, depth: RelativeDepth, + focusX: Float, focusY: Float, strength: Int): IntArray { + require(width > 0 && height > 0 && pixels.size == width * height && strength in 0..100) + require(focusX.isFinite() && focusY.isFinite()) + if (strength == 0) return pixels.copyOf() + val focus = depth.focus(focusX, focusY) + val weights = FloatArray(pixels.size) { i -> amount(depth.at((i % width).toFloat() / (width - 1).coerceAtLeast(1), + (i / width).toFloat() / (height - 1).coerceAtLeast(1)), focus) } + val radius = (maxOf(width, height) * .018f * strength / 100).roundToInt().coerceAtLeast(1) + val blurredWeight = blur(weights, width, height, radius) + val channels = arrayOf(16, 8, 0).map { shift -> + blur(FloatArray(pixels.size) { ((pixels[it] ushr shift) and 255) * weights[it] }, width, height, radius) + } + return IntArray(pixels.size) { i -> + val blend = weights[i] + var result = -0x1000000 + for (c in 0..2) { + val shift = (2 - c) * 8 + val original = (pixels[i] ushr shift) and 255 + val softened = if (blurredWeight[i] > .001f) channels[c][i] / blurredWeight[i] else original.toFloat() + result = result or ((original * (1 - blend) + softened * blend).roundToInt().coerceIn(0, 255) shl shift) + } + result + } + } + + // Three separable box passes approximate a Gaussian, with bounded O(width * height) work. + private fun blur(input: FloatArray, width: Int, height: Int, radius: Int): FloatArray { + var source = input + repeat(3) { + val horizontal = FloatArray(input.size) + for (y in 0 until height) { + var sum = 0f + for (x in 0..minOf(radius, width - 1)) sum += source[y * width + x] + for (x in 0 until width) { + horizontal[y * width + x] = sum / (minOf(width - 1, x + radius) - maxOf(0, x - radius) + 1) + if (x - radius >= 0) sum -= source[y * width + x - radius] + if (x + radius + 1 < width) sum += source[y * width + x + radius + 1] + } + } + val vertical = FloatArray(input.size) + for (x in 0 until width) { + var sum = 0f + for (y in 0..minOf(radius, height - 1)) sum += horizontal[y * width + x] + for (y in 0 until height) { + vertical[y * width + x] = sum / (minOf(height - 1, y + radius) - maxOf(0, y - radius) + 1) + if (y - radius >= 0) sum -= horizontal[(y - radius) * width + x] + if (y + radius + 1 < height) sum += horizontal[(y + radius + 1) * width + x] + } + } + source = vertical + } + return source + } +} diff --git a/app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt b/app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt new file mode 100644 index 00000000..d06a67f2 --- /dev/null +++ b/app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt @@ -0,0 +1,165 @@ +package dev.halcamera.ui + +import android.app.Activity +import android.app.Dialog +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.RectF +import android.net.Uri +import android.os.Handler +import android.os.Looper +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.widget.LinearLayout +import android.widget.SeekBar +import dev.halcamera.camera.AppBokehProcessor +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicInteger + +/** One captured image, one estimated map. All inference/render/save work stays off the UI thread. */ +class AppBokehEditor(private val activity: Activity, private val source: Uri, private val closed: () -> Unit) { + private val processor = AppBokehProcessor(activity) + private val worker = Executors.newSingleThreadExecutor() + private val main = Handler(Looper.getMainLooper()) + private val revision = AtomicInteger() + @Volatile private var alive = true + private var saving = false + private var scene: AppBokehProcessor.Scene? = null + private var result: Bitmap? = null + private var depthImage: Bitmap? = null + private var focusX = .5f + private var focusY = .5f + private var strength = 65 + private var display = 0 + private val dialog = Dialog(activity) + private val info = Look.text(activity, "Estimating depth on this device…", 13, Look.onDarkMuted) + private val picture = Picture(activity) { x, y -> focusX = x; focusY = y; render() } + private val save = Look.galleryButton(activity, "Save copy", true) { saveCopy() } + private val slider = SeekBar(activity).apply { max = 100; progress = strength; contentDescription = "Background blur strength" } + + fun show() { + val body = LinearLayout(activity).apply { + orientation = LinearLayout.VERTICAL; setBackgroundColor(Look.gallerySurface) + val pad = Look.dp(activity, 16); setPadding(pad, pad, pad, pad) + } + body.addView(Look.text(activity, "App Bokeh", 22, Look.onDark)) + body.addView(Look.text(activity, "Tap a subject. Adjust background blur.\nEstimated depth · Output up to 1600 px", 13, Look.onDarkMuted)) + body.addView(picture, LinearLayout.LayoutParams(-1, 0, 1f)) + val views = LinearLayout(activity).apply { isBaselineAligned = false } + listOf("Bokeh", "Original", "Depth").forEachIndexed { index, label -> + views.addView(Look.galleryButton(activity, label) { display = index; updateImage() }.apply { + textSize = 13f + setSingleLine(true) + val padding = Look.dp(activity, 8) + setPadding(padding, Look.dp(activity, 12), padding, Look.dp(activity, 12)) + }, LinearLayout.LayoutParams(0, -2, 1f)) + } + body.addView(views) + body.addView(info) + body.addView(slider) + slider.isEnabled = false + slider.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged(bar: SeekBar, progress: Int, fromUser: Boolean) { strength = progress; if (fromUser) render() } + override fun onStartTrackingTouch(bar: SeekBar) = Unit + override fun onStopTrackingTouch(bar: SeekBar) = Unit + }) + val actions = LinearLayout(activity) + actions.addView(Look.galleryButton(activity, "Close") { if (!saving) dismiss() }, LinearLayout.LayoutParams(0, -2, 1f)) + actions.addView(save, LinearLayout.LayoutParams(0, -2, 1f)) + body.addView(actions) + save.isEnabled = false + dialog.setContentView(body) + dialog.setOnDismissListener { dispose() } + dialog.show() + dialog.window?.setLayout(WindowManager.LayoutParams.MATCH_PARENT, WindowManager.LayoutParams.MATCH_PARENT) + worker.execute { + val loaded = runCatching { processor.load(source) } + main.post { + if (!alive) return@post + loaded.fold({ scene = it; depthImage = AppBokehProcessor.depthBitmap(it.depth); slider.isEnabled = true; render() }, + { info.text = "Depth estimation failed: ${it.message}. Original photo is saved." }) + } + } + } + + private fun render() { + val current = scene ?: return + if (!alive || saving) return + val token = revision.incrementAndGet() + val x = focusX; val y = focusY; val amount = strength + save.isEnabled = false + info.text = "Applying blur $amount%…" + worker.execute { + if (!alive || revision.get() != token) return@execute + val rendered = runCatching { processor.render(current, x, y, amount) } + main.post { + if (!alive || revision.get() != token) return@post + rendered.fold({ result = it; save.isEnabled = true; info.text = "Blur $amount% · Depth ${current.elapsedMs} ms · Original kept"; updateImage() }, + { info.text = "Blur failed: ${it.message}. Original photo is saved." }) + } + } + } + + private fun updateImage() { + picture.bitmap = when (display) { 1 -> scene?.original; 2 -> depthImage; else -> result ?: scene?.original } + // The depth image is square internally, but corresponds to the entire original frame. + picture.aspect = scene?.original?.let { it.width.toFloat() / it.height } ?: 1f + picture.focusX = focusX; picture.focusY = focusY + picture.contentDescription = listOf("Bokeh result", "Original photo", "Estimated relative depth")[display] + picture.invalidate() + } + + private fun saveCopy() { + val current = scene ?: return + val rendered = result ?: return + if (!alive || saving || !save.isEnabled) return + saving = true; save.isEnabled = false; slider.isEnabled = false; picture.isEnabled = false + dialog.setCancelable(false) + val x = focusX; val y = focusY; val amount = strength + info.text = "Saving Bokeh and estimated depth…" + worker.execute { + val saved = runCatching { processor.save(current, rendered, source, x, y, amount) } + main.post { + if (!alive) return@post + saving = false; slider.isEnabled = true; picture.isEnabled = true; save.isEnabled = true + dialog.setCancelable(true) + info.text = saved.fold({ "Saved Bokeh JPEG + depth PNG. Original kept." }, { "Save failed: ${it.message}" }) + } + } + } + + fun dismiss() { dialog.dismiss(); dispose() } + private fun dispose() { + if (!alive) return + alive = false; revision.incrementAndGet(); worker.shutdown(); closed() + } + + private class Picture(context: android.content.Context, val selected: (Float, Float) -> Unit) : View(context) { + var bitmap: Bitmap? = null + var aspect = 1f + var focusX = .5f; var focusY = .5f + private val rect = RectF() + private val imagePaint = Paint(Paint.FILTER_BITMAP_FLAG) + private val ring = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Look.cameraActive; style = Paint.Style.STROKE; strokeWidth = Look.dp(context, 2).toFloat() } + init { isClickable = true; isFocusable = true; contentDescription = "Bokeh result: tap subject to keep sharp" } + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val image = bitmap ?: return + val w = minOf(width.toFloat(), height * aspect); val h = w / aspect + rect.set((width - w) / 2, (height - h) / 2, (width + w) / 2, (height + h) / 2) + canvas.drawBitmap(image, null, rect, imagePaint) + canvas.drawCircle(rect.left + w * focusX, rect.top + h * focusY, Look.dp(context, 12).toFloat(), ring) + } + override fun onTouchEvent(event: MotionEvent): Boolean { + if (!isEnabled || bitmap == null || !rect.contains(event.x, event.y)) return false + if (event.action == MotionEvent.ACTION_UP) { + selected((event.x - rect.left) / rect.width(), (event.y - rect.top) / rect.height()) + performClick() + } + return true + } + override fun performClick(): Boolean { super.performClick(); return true } + } +} diff --git a/app/src/test/java/dev/halcamera/camera/DepthBokehTest.kt b/app/src/test/java/dev/halcamera/camera/DepthBokehTest.kt new file mode 100644 index 00000000..ee9f0778 --- /dev/null +++ b/app/src/test/java/dev/halcamera/camera/DepthBokehTest.kt @@ -0,0 +1,47 @@ +package dev.halcamera.camera + +import org.junit.Assert.* +import org.junit.Test + +class DepthBokehTest { + @Test fun `relative depth preserves near far order without claiming metres`() { + val depth = RelativeDepth.normalize(10, 10, FloatArray(100) { 10f + it }) + assertEquals(0f, depth.at(0f, 0f), .001f) + assertEquals(1f, depth.at(1f, 1f), .001f) + assertTrue(depth.at(.8f, .8f) > depth.at(.2f, .2f)) + } + + @Test fun `invalid and flat model outputs fail instead of fabricated depth`() { + for (raw in listOf(floatArrayOf(Float.NaN, 1f), floatArrayOf(1f, 1f))) { + assertThrows(IllegalArgumentException::class.java) { RelativeDepth.normalize(2, 1, raw) } + } + } + + @Test fun `foreground stays exact while textured background is blurred`() { + val w = 128; val h = 64 + val pixels = IntArray(w * h) { if (it % w < 64) 0xffff0000.toInt() else if (it % 2 == 0) -1 else 0xff000000.toInt() } + val depth = RelativeDepth(w, h, FloatArray(pixels.size) { if (it % w < 64) 1f else 0f }) + val result = DepthBokeh.render(pixels, w, h, depth, .25f, .5f, 100) + for (y in 0 until h) for (x in 0 until 64) assertEquals(pixels[y * w + x], result[y * w + x]) + val background = result[32 * w + 90] + assertTrue((background and 255) in 30..225) + // Red foreground must not bleed into the blurred monochrome background. + for (x in 65..75) { + val pixel = result[32 * w + x] + assertEquals((pixel ushr 16) and 255, pixel and 255) + } + } + + @Test fun `zero strength is identity and choosing background protects its plane`() { + val pixels = IntArray(64) { if (it % 2 == 0) -1 else 0xff123456.toInt() } + val depth = RelativeDepth(8, 8, FloatArray(64) { if (it % 8 < 4) 1f else 0f }) + assertArrayEquals(pixels, DepthBokeh.render(pixels, 8, 8, depth, .1f, .5f, 0)) + assertArrayEquals(pixels, DepthBokeh.render(pixels, 8, 8, depth, .9f, .5f, 100)) + } + + @Test fun `constant colours and one pixel dimensions are stable`() { + val pixels = IntArray(20) { 0xffabcdef.toInt() } + val depth = RelativeDepth(1, 20, FloatArray(20) { it / 19f }) + assertArrayEquals(pixels, DepthBokeh.render(pixels, 1, 20, depth, 0f, 1f, 100)) + } +} diff --git a/docs/design/APP-BOKEH.md b/docs/design/APP-BOKEH.md new file mode 100644 index 00000000..398a6040 --- /dev/null +++ b/docs/design/APP-BOKEH.md @@ -0,0 +1,31 @@ +# App Bokeh 처리 계약 + +App Bokeh는 일반 Photo 촬영이 저장한 JPEG 한 장을 입력으로 사용한다. 제조사 확장 세션이나 센서 Depth 스트림을 요청하지 않는다. 앱의 `MainAppBokehBridge`가 `MediaCapture.capturePhoto` 완료 후 JPEG URI를 편집기에 전달한다. + +App Bokeh 진입 시 Camera2의 일반 Preview와 촬영 JPEG 두 출력만 구성한다. 분석용 YUV 스트림은 끈다. 이미지 버퍼는 JPEG ImageReader로 받고 CaptureResult는 일반 촬영 메타데이터에만 사용한다. JPEG 디코딩 후 RGB 텐서를 만들고 모델 출력 Float 배열을 깊이 맵으로 사용하므로 HAL의 DEPTH16 버퍼와 관계없다. + +## 처리와 저장 + +1. `AppBokehProcessor`가 EXIF 방향을 반영하고 긴 변을 최대 1600픽셀로 줄인다. 원본 파일은 수정하지 않는다. +2. RGB를 256×256으로 리사이즈하고 공식 Android 예제와 같은 `(channel - 115) / 58` 정규화를 적용한다. +3. LiteRT Interpreter의 CPU 스레드 4개로 MiDaS 상대 역깊이를 추론한다. 상·하위 2% 범위로 0–1 정규화한다. 값이 클수록 가까우며 실제 거리로 해석하지 않는다. +4. 사용자가 선택한 지점 주변 3×3 깊이의 중앙값을 기준으로 더 먼 영역에 연속적인 혼합 가중치를 부여한다. 전경 색이 배경으로 번지는 현상을 줄이기 위해 배경 가중치로 색을 정규화한 세 번의 분리형 box blur를 적용한다. 완전한 광학적 보케나 가려진 배경의 복원은 보장하지 않는다. +5. JPEG, 깊이 PNG, 원본 URI와 모델 해시를 포함한 메타데이터를 `MediaLibrary.transaction`으로 저장한다. 원본은 이미 독립적으로 저장되어 있어 처리·저장 실패에도 남는다. Benchmark/incident에는 픽셀을 추가하지 않는다. + +모델 추론과 렌더·저장은 편집기 전용 worker에서 실행한다. 슬라이더 변경 시 이전 세대의 결과는 화면에 적용하지 않는다. Activity 종료 시 편집기를 닫고 후속 UI 갱신을 막는다. 화면 재진입 시 원본 URI로 다시 계산하며 편집 매개변수는 초기화한다. + +## 모델과 재현 가능한 빌드 + +- 공식 배포: [MiDaS v2.1](https://github.com/isl-org/MiDaS/releases/tag/v2_1), `model_opt.tflite`, 66,338,288 bytes. +- SHA-256: `93d871071edff1218973ce25ee27ce95ccd20450c70a55e1b89efa3f5a772cdd`. +- 라이선스: MIT, Copyright (c) 2019 Intel ISL. 원문을 `app/src/main/assets/licenses/MiDaS.txt`로 APK에 포함한다. +- 전처리 근거: [공식 Android 예제](https://github.com/isl-org/MiDaS/blob/master/mobile/android/lib_support/src/main/java/org/tensorflow/lite/examples/classification/tflite/ClassifierFloatEfficientNet.java). +- 런타임: `com.google.ai.edge.litert:litert:1.4.2`, Interpreter API, CPU. + +`prepareDepthModel`은 공식 배포에서 빌드 디렉터리로 모델을 받아 해시를 확인한 뒤 assets에 포함한다. 첫 빌드와 clean 이후 빌드에는 다운로드 연결이 필요하다. 모델 바이트는 Git에 커밋하지 않는다. 캐시 파일의 해시가 바뀌면 다시 검증된 모델을 준비한다. 실행 중에는 다운로드하지 않는다. + +## 한계 + +깊이는 단일 이미지 추정이므로 인물·사물 모두 사용할 수 있지만 모든 장면의 정확성을 보장하지 않는다. 실시간 효과, 전경 흐림, 모델 학습, 센서 깊이와의 융합은 이 경로에서 수행하지 않는다. CPU 추론 시간과 메모리 사용은 기기에 따라 다르다. 강도 0의 픽셀 동일성은 방향 보정·축소 후의 처리용 원본을 기준으로 하며, JPEG 재인코딩까지 무손실이라는 뜻이 아니다. + +사용 절차는 [Live 가이드](../../guide/live.md)를 참고한다. diff --git a/docs/validation/app-bokeh.md b/docs/validation/app-bokeh.md new file mode 100644 index 00000000..c1c0d317 --- /dev/null +++ b/docs/validation/app-bokeh.md @@ -0,0 +1,30 @@ +# App Bokeh 실기기 검증 + +2026-10-24에 SM-S936N, Android 16(API 36), 후면 카메라 ID 0에서 확인했다. 제조사 Bokeh와 센서 DEPTH16 없이 사물 사진에 배경 흐림을 적용하고 원본을 보존하는 경로를 검증했다. 사진과 화면 캡처는 로컬 검증 자료로만 보관하며 저장소와 PR에 올리지 않는다. + +## 실기기 결과 + +| 시나리오 | 관찰 결과 | +| --- | --- | +| Depth → App Bokeh 진입 | 일반 Camera2 Preview 1280×720 + JPEG 1920×1080을 구성했다. 카메라 서비스 덤프의 출력은 PRIVATE와 BLOB 두 개이며 분석 YUV·DEPTH16·입력 스트림이 없었다. | +| 사물 촬영 | 가까운 마우스·책상과 먼 문·수납장이 보이는 장면을 촬영했다. 원본 JPEG 저장 후 편집기가 열렸다. | +| 동일 원본 비교 | 처리용 원본은 EXIF 방향 보정 후 900×1600이었다. 강도 50·100에서 배경 경계와 무늬가 단계적으로 흐려지고 마우스와 가까운 책상은 선명하게 남았다. | +| 깊이·기준면 | 256×256 상대 역깊이 맵에서 책상과 먼 배경이 구분됐다. 먼 배경을 선택하면 추가 흐림이 줄었다. | +| 강도 0 | 실기기 검사에서 처리용 원본과 `Bitmap.sameAs`가 참이었다. JPEG 재인코딩의 무손실을 뜻하지 않는다. | +| 저장 | UI의 Save copy로 Bokeh JPEG·깊이 PNG·JSON을 저장했다. JSON에서 원본 URI, 강도 100, 선택 좌표, 모델 해시, 900×1600 크기를 확인했다. 원본 파일은 유지됐다. | +| 반복 처리 검사 | 같은 원본에 강도 50·80·100을 적용하고 저장한 이미지들을 다시 디코딩했다. `APP_BOKEH_OK`를 받았다. | +| 화면 복귀 | Home 후 Live 복귀 시 같은 원본으로 편집기를 다시 열었다. 매개변수는 기본값으로 돌아갔다. 닫기 후 Photo 전환 시 일반 셔터와 YUV 포함 기본 세션으로 복귀했다. | +| 카메라 변경 | App Bokeh에서 후면 카메라 ID 2로 바꾸면 Photo가 선택되고 일반 셔터·기본 스트림으로 복귀했다. | +| 처리 시간 | 사진 로드·방향 보정·축소·깊이 추론을 합친 관찰값은 332, 533, 541 ms였다. 렌더·JPEG 저장 시간을 포함하지 않으며 성능 보장값이 아니다. | + +## 자동 검사와 리뷰 + +순수 Kotlin 검사에서는 상대 깊이 정규화, 잘못된·평탄한 출력 거절, 전경 보존과 배경 흐림, 전경 색 번짐 억제, 강도 0, 기준면 변경, 일정한 색과 폭 1 이미지를 다룬다. 전체 `testDebugUnitTest`와 `lintDebug`, 앱·검사 APK 빌드를 별도로 실행했다. 기기 검사는 JVM 검사와 구분한다. + +코드 리뷰에서 카메라 변경 후 App Bokeh 상태가 남는 경로를 발견해 다른 전용 모드와 함께 해제하도록 수정했다. 모드 종료 시 이전 App Bokeh 안내도 지운다. UI에서 Original 버튼이 줄바꿈되던 문제도 수정했다. + +## 확인 범위와 한계 + +이번 새 장면은 후면 사물 한 장이다. 다른 기기·전면·인물·역광·반사체·얇은 경계의 품질을 검증한 것으로 해석하지 않는다. 마우스 주변과 가까운 책상은 보존됐지만 책상 뒤쪽 경계처럼 깊이가 바뀌는 곳에는 흐림이 번지는 부분이 있다. 단안 추정과 저해상도 깊이 맵의 한계이며, 정밀한 피사체 마스크나 광학 보케와 같지 않다. 최대 출력 긴 변은 1600픽셀이며 실시간 미리보기 효과는 제공하지 않는다. + +처리·모델·스트림 계약은 [App Bokeh 설계](../design/APP-BOKEH.md)에 있다. diff --git a/guide/_bindings.yaml b/guide/_bindings.yaml index d0bc05f8..e95aab17 100644 --- a/guide/_bindings.yaml +++ b/guide/_bindings.yaml @@ -131,6 +131,12 @@ sources: - app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt - app/src/main/java/dev/halcamera/camera/PortraitCapabilities.kt - app/src/main/java/dev/halcamera/camera/PortraitSupport.kt + camera-engines/app-bokeh: + evidence: + - app/src/main/java/dev/halcamera/MainAppBokehBridge.kt + - app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt + - app/src/main/java/dev/halcamera/camera/RelativeDepth.kt + - app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt camera-engines/camera2-engine: evidence: - app/src/main/java/dev/halcamera/camera/Camera2Engine.kt @@ -496,6 +502,9 @@ sources: # 통째로 잡았는데, 그러면 이 관점과 무관한 파일 하나를 고쳐도 원고 전체가 재검토 대상이 되었습니다. # 새로 생긴 파일은 coverage --check 가 담당 요소 없음으로 잡아내므로, 그때 elements 와 이 목록에 함께 적습니다. evidence: + - app/src/main/java/dev/halcamera/MainAppBokehBridge.kt + - app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt + - app/src/main/java/dev/halcamera/camera/RelativeDepth.kt - app/src/main/java/dev/halcamera/GalleryActivity.kt - app/src/main/java/dev/halcamera/MainActivity.kt - app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt @@ -606,6 +615,8 @@ sources: diagram_type: sequence # data-flow 와 같은 규칙입니다: 요소 근거와 원고 인용의 합집합만 적습니다. evidence: + - app/src/main/java/dev/halcamera/MainAppBokehBridge.kt + - app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt - app/src/main/java/dev/halcamera/MainActivity.kt - app/src/main/java/dev/halcamera/MainDepthBridge.kt - app/src/main/java/dev/halcamera/MainPortraitBridge.kt diff --git a/guide/_content/_reference/engine/camera2.md b/guide/_content/_reference/engine/camera2.md index 6bea2e38..196b466b 100644 --- a/guide/_content/_reference/engine/camera2.md +++ b/guide/_content/_reference/engine/camera2.md @@ -3,6 +3,7 @@ based_on: [overall-architecture] confidence: code sources: - app/src/main/java/dev/halcamera/camera/Camera2Engine.kt + - app/src/main/java/dev/halcamera/MainAppBokehBridge.kt - app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt - app/src/main/java/dev/halcamera/camera/LivePipSession.kt - app/src/main/java/dev/halcamera/camera/PipSessionQuery.kt @@ -119,6 +120,8 @@ Live의 기본 세션은 프리뷰, YUV_420_888, JPEG 세 스트림으로 구성 | YUV (`analysis_acquire_latest`) | 640×480 이하에서 가장 큰 크기 | 프레임 도착 기록, 앱 변환을 선택한 사진 입력 | | JPEG (`still`) | 1920×1080 이하에서 가장 큰 크기 | 카메라 JPEG 사진, 벤치마크 still | +App Bokeh에서는 이 기본 크기의 Preview와 JPEG만 구성하고 분석 YUV는 끕니다. 저장한 JPEG를 앱의 깊이 추정 모델에 전달하므로 촬영 세션에 DEPTH16 출력을 추가하지 않습니다. + 벤치마크 세션은 profile의 `StreamSpec` 크기를 그대로 사용합니다. 지원하지 않는 크기이면 작은 크기로 대체하지 않고 구성 단계에서 실패합니다. 같은 profile ID로 다른 크기를 측정하면 비교가 무의미해지기 때문입니다. 명시한 Live 설정도 작은 크기로 대체하지 않습니다. diff --git a/guide/_content/_reference/engine/comparison.md b/guide/_content/_reference/engine/comparison.md index a9f76039..3cac9a85 100644 --- a/guide/_content/_reference/engine/comparison.md +++ b/guide/_content/_reference/engine/comparison.md @@ -15,6 +15,7 @@ sources: - app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt - app/src/main/java/dev/halcamera/camera/CameraXControls.kt - app/src/main/java/dev/halcamera/MainActivity.kt + - app/src/main/java/dev/halcamera/MainAppBokehBridge.kt - app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt - app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt - app/src/main/java/dev/halcamera/camera/LivePipSession.kt @@ -27,6 +28,7 @@ verifications: [] | 항목 | Camera2 | CameraX | | --- | --- | --- | +| App Bokeh | 일반 Preview+JPEG로 촬영하고 앱에서 상대 깊이를 추정해 배경 흐림을 적용합니다. 인물·사물 모두 선택할 수 있으며 긴 변 최대 1600픽셀로 저장합니다. | App Bokeh를 선택하면 Camera2로 전환합니다. | | Depth 보기 | DEPTH_OUTPUT과 DEPTH16 해상도를 공개한 카메라에서 별도 Depth 전용 세션을 엽니다. 맵 표시만 제공합니다. | Depth 해상도를 선택하면 Camera2로 전환합니다. | | 제조사 Portrait | API 31 이상의 BOKEH 확장 세션으로 전용 미리보기와 JPEG를 받습니다. Depth 메뉴에서 선택하며 수동 제어·RAW·PIP·영상은 제공하지 않습니다. | Portrait를 선택하면 Camera2로 전환합니다. | | 수동 촬영 | ISO·노출 시간·초점과 WB를 프리뷰·사진·녹화에 적용합니다. 지원 여부와 실제 적용값을 표시합니다. | Manual 버튼을 미지원 상태로 비활성화합니다. | diff --git a/guide/_content/_reference/engine/contract.md b/guide/_content/_reference/engine/contract.md index 6433ba7e..c5da3558 100644 --- a/guide/_content/_reference/engine/contract.md +++ b/guide/_content/_reference/engine/contract.md @@ -7,6 +7,7 @@ sources: - app/src/main/java/dev/halcamera/camera/CameraXEngine.kt - app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt - app/src/main/java/dev/halcamera/MainActivity.kt + - app/src/main/java/dev/halcamera/MainAppBokehBridge.kt - app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt - app/src/main/java/dev/halcamera/ui/LiveControlBar.kt - app/src/main/java/dev/halcamera/ui/FocusRing.kt @@ -61,7 +62,9 @@ classDiagram 종료 통지와 다음 엔진의 대기 순서는 [엔진 전환 그림](architecture.md#카메라-열기와-닫기)을 확인하세요. -Photo·Video·Depth·Multi · P·Multi · V 사이에서 모드를 바꾸면 기존 세션을 닫고 스트림을 다시 구성합니다. 같은 모드를 다시 누르면 유지하며 Depth 버튼은 해상도 선택창을 엽니다. 일반 단일 카메라는 새 프리뷰의 첫 화면 갱신 뒤, Multi는 모든 장치의 합성 프리뷰가 준비된 뒤에 촬영 버튼을 활성화합니다. Depth는 첫 맵 수신 후에도 촬영 버튼을 비활성화합니다. CLI 호환용 Dual도 두 프리뷰가 모두 갱신된 뒤에 촬영할 수 있습니다. 모드 선택 자체로 사진이나 녹화를 시작하지는 않습니다. +Photo·Video·Depth·Multi · P·Multi · V 사이에서 모드를 바꾸면 기존 세션을 닫고 스트림을 다시 구성합니다. 같은 모드를 다시 누르면 유지하며 Depth 버튼은 App Bokeh·제조사 Portrait·센서 Depth 선택창을 엽니다. 일반 단일 카메라는 새 프리뷰의 첫 화면 갱신 뒤, Multi는 모든 장치의 합성 프리뷰가 준비된 뒤에 촬영 버튼을 활성화합니다. 센서 Depth는 첫 맵 수신 후에도 촬영 버튼을 비활성화합니다. CLI 호환용 Dual도 두 프리뷰가 모두 갱신된 뒤에 촬영할 수 있습니다. 모드 선택 자체로 사진이나 녹화를 시작하지는 않습니다. + +App Bokeh는 Camera2의 일반 Preview+JPEG 세션에서 촬영한 원본을 편집기로 전달합니다. 분석 YUV와 센서 Depth를 사용하지 않으며, 모델로 추정한 상대 깊이에 따라 저장할 복사본에 배경 흐림을 적용합니다. 카메라·엔진 변경이나 일반 스트림 설정 적용 시 App Bokeh 모드를 종료합니다. ### Benchmark와 CLI가 쓰는 엔진 diff --git a/guide/_content/architecture/constraints.md b/guide/_content/architecture/constraints.md index 763bb436..7295ee23 100644 --- a/guide/_content/architecture/constraints.md +++ b/guide/_content/architecture/constraints.md @@ -2,6 +2,9 @@ based_on: [overall-architecture] confidence: code sources: + - app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt + - app/src/main/java/dev/halcamera/camera/RelativeDepth.kt + - app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt - app/src/main/java/dev/halcamera/camera/MultiSession.kt - app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt - app/src/main/java/dev/halcamera/camera/MultiRecording.kt @@ -45,6 +48,8 @@ Depth 보기는 컬러와 Depth를 같은 요청에 포함하지 않는 별도 C 제조사 Portrait도 일반 촬영과 별도의 확장 세션을 사용합니다. Bokeh 확장 전용 해상도로 컬러 미리보기와 JPEG만 요청하며, 세션이 거절되면 다른 크기로 자동 대체하지 않습니다. 단일 촬영의 JPEG를 받기 전에 실패하거나 종료되면 세션을 해제하여 늦은 이미지를 다음 촬영에 연결하지 않습니다. 이미 받은 JPEG의 저장 작업은 종료 후에도 마칠 수 있습니다. +App Bokeh는 제조사 Portrait와 달리 일반 JPEG 저장 이후의 앱 처리입니다. 상대 깊이는 센서 거리나 DEPTH16으로 해석하지 않으며, 편집·저장 해상도는 긴 변 최대 1600픽셀로 제한합니다. 원본은 수정하지 않습니다. 추론·렌더·추가 저장 실패는 원본을 삭제하지 않으며, Activity 중단 후에는 편집기의 늦은 결과를 화면에 적용하지 않습니다. + Multi 모드의 `MultiSession`은 `CameraEngine`과 별도입니다. Camera2는 선택한 독립 장치를 연 뒤 세션을 구성하며, CameraX는 광고된 동시 조합을 바인딩합니다. 모든 장치의 종료 통지를 확인해야 출력을 해제하고 다음 동시 세션을 열 수 있습니다. 한 명령의 여러 사진을 센서 동기 촬영으로 해석하지 않습니다. diff --git a/guide/_content/architecture/module-roles.md b/guide/_content/architecture/module-roles.md index eacd8525..65401b7f 100644 --- a/guide/_content/architecture/module-roles.md +++ b/guide/_content/architecture/module-roles.md @@ -2,6 +2,10 @@ based_on: [overall-architecture] confidence: code sources: + - app/src/main/java/dev/halcamera/MainAppBokehBridge.kt + - app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt + - app/src/main/java/dev/halcamera/camera/RelativeDepth.kt + - app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt - app/src/main/java/dev/halcamera/camera/MultiSession.kt - app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt - app/src/main/java/dev/halcamera/camera/MultiRecording.kt @@ -98,6 +102,7 @@ verifications: [] - 엔진 계약, Camera2·CameraX 구현, 엔드포인트 열거를 제공합니다. `DepthCameraEngine`은 컬러 출력 없이 DEPTH16 맵만 받으며 `DepthFrame`이 순수 Kotlin으로 표시용 표본을 만듭니다. - `PortraitCameraEngine`은 LIVE의 제조사 BOKEH 전용 세션입니다. MainPortraitBridge가 JPEG 크기를 선택하고 MainDepthBridge가 Depth Map과 Portrait의 진입점을 구분합니다. 이 경로는 벤치마크를 실행하거나 센서 Depth 맵을 저장하지 않습니다. - `close(done)` 완료 전에 다음 카메라를 열지 않습니다. +- `AppBokehProcessor`는 일반 JPEG를 RGB로 읽고 기기 안에서 상대 깊이를 추정합니다. `RelativeDepth`와 `DepthBokeh`는 깊이 정규화·선택 지점·배경 흐림을 순수 Kotlin으로 계산합니다. `MainAppBokehBridge`와 `AppBokehEditor`가 촬영 후 편집을 연결하며 원본과 보케 결과를 별도로 보관합니다. - 엔진 계약(`MediaCapture`, `LiveTuning`, `TouchMetering`)과 두 엔진의 구현, 두 엔진의 차이는 [Engine Comparison](engine.md)에 있습니다. - 벤치마크 RECORD 단계의 recorder 상태 기계는 `BenchmarkRecorder`가 담당하고 `Camera2Engine`은 그 호출을 위임합니다. - 카메라를 화면에 적는 이름(`Camera · 0 (Wide · Rear)`)도 이 패키지의 `CameraLabel` 하나가 만들며, Live·Benchmark·Probe·CTS가 모두 그것을 부릅니다. diff --git a/guide/_content/architecture/overview.md b/guide/_content/architecture/overview.md index 32be428c..2712bbf4 100644 --- a/guide/_content/architecture/overview.md +++ b/guide/_content/architecture/overview.md @@ -32,7 +32,7 @@ verifications: [] | 화면 | 할 수 있는 일 | | --- | --- | -| Live · `MainActivity` | 앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영하며, 지원 카메라의 제조사 Portrait와 Depth 맵을 선택합니다. | +| Live · `MainActivity` | 앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영합니다. App Bokeh로 촬영 후 배경 흐림을 조절하며, 지원 카메라의 제조사 Portrait와 Depth 맵도 선택합니다. | | Multi · `ConcurrentCameraActivity` | 독립 카메라 장치별 사진·영상을 제공합니다. Live의 Multi · P와 Multi · V에서 엽니다. | | Lab · `WorkbenchActivity` | 기기 정보, 검사 도구, 저장된 결과와 설정을 엽니다. | | Benchmark · `BenchmarkActivity` | 정해진 조건(profile)으로 측정하고 결과를 저장합니다. | diff --git a/guide/_content/architecture/runtime-flow.md b/guide/_content/architecture/runtime-flow.md index e4540b4c..1ffd8764 100644 --- a/guide/_content/architecture/runtime-flow.md +++ b/guide/_content/architecture/runtime-flow.md @@ -2,6 +2,9 @@ based_on: [data-flow] confidence: code sources: + - app/src/main/java/dev/halcamera/MainAppBokehBridge.kt + - app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt + - app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt - app/src/main/java/dev/halcamera/cli/CliProvider.kt - app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt - tools/halcam/halcam/cli.py @@ -44,6 +47,8 @@ Live의 Depth 보기는 `DepthCameraEngine → ImageReader → DepthFrame → De 같은 Depth 메뉴의 Portrait는 `PortraitCameraEngine → CameraExtensionSession → JPEG ImageReader → MediaLibrary`로 연결합니다. 제조사 Bokeh가 적용된 컬러 사진을 저장하며 별도의 센서 Depth 맵을 받지 않습니다. 확장 처리 이벤트는 일반 Camera2 프레임 지표와 구분하고, 사진 픽셀은 측정 파일에 넣지 않습니다. +App Bokeh는 일반 Camera2의 Preview+JPEG로 촬영한 원본을 저장한 뒤 `MainAppBokehBridge → AppBokehEditor → AppBokehProcessor`로 전달합니다. JPEG를 RGB로 디코딩하고 MiDaS 모델이 256×256 Float 상대 깊이를 추론합니다. 사용자가 선택한 깊이보다 먼 영역을 앱에서 흐린 뒤 보케 JPEG·깊이 PNG·처리 메타데이터를 별도 저장합니다. 원본은 그대로 유지하며 분석용 YUV·HAL Depth·제조사 보케 버퍼는 사용하지 않습니다. + 벤치마크는 별도 화면인 `BenchmarkActivity`가 준비를 마친 뒤 `BenchmarkRunner.start()`를 호출하여 시작합니다. `Telemetry.callback(...)`이 반환한 Camera2 콜백의 `onCaptureStarted`는 프레임워크 신호를 기록하며, Live 셔터와 벤치마크 시작을 연결하는 메서드가 아닙니다. diff --git a/guide/live.md b/guide/live.md index 87d6daaa..d332f7c5 100644 --- a/guide/live.md +++ b/guide/live.md @@ -8,6 +8,7 @@ title: Live Photo로 한 장을 찍고, Video로 움직임을 담아 보세요. 처음에는 기본값이면 충분합니다. - **[촬영](#live에서-촬영하세요)** · 사진·연사·AEB·동영상을 촬영합니다. +- **[App Bokeh](#앱에서-배경을-흐리세요)** · 인물이나 사물의 깊이를 추정하고 촬영 후 배경 흐림을 조절합니다. - **[Portrait](#제조사-portrait로-배경을-흐리세요)** · Depth 메뉴에서 제조사 Bokeh 사진을 선택합니다. - **[수동 설정](#수동-촬영-조건을-고정하세요)** · ISO·노출 시간·초점을 고정합니다. - **[스트림 설정](#live-스트림을-설정하세요)** · 출력 크기·FPS·포맷을 바꿉니다. @@ -26,6 +27,20 @@ Photo로 한 장을 찍고, Video로 움직임을 담아 보세요. 처음에는 **모드나 엔진을 바꾸면 새 촬영으로 시작합니다.** PIP·스트림·줌·촬영 제어는 초기화됩니다. Callback의 고정 시간은 사용자 설정으로 유지합니다. +### 앱에서 배경을 흐리세요 + +1. `Depth → App Bokeh`를 선택합니다. 제조사 Bokeh나 센서 Depth 지원 여부와 관계없이 일반 JPEG 촬영을 사용합니다. +2. 셔터를 누릅니다. 원본을 저장한 뒤 기기 안에서 깊이를 추정합니다. LIVE 미리보기에는 이 효과가 적용되지 않습니다. +3. 편집 화면에서 선명하게 유지할 인물이나 사물을 누릅니다. 선택한 지점보다 멀다고 추정된 영역을 흐리게 처리합니다. +4. 슬라이더로 강도를 조절하고 `Bokeh`, `Original`, `Depth`로 결과를 비교합니다. 강도 0은 처리용 원본 픽셀을 유지합니다. +5. `Save copy`를 누릅니다. 보케 JPEG와 추정 깊이 PNG는 `DCIM/HALCamera`, 모델·원본 URI·선택 지점·강도 기록은 `Download/HALCamera`에 저장합니다. 촬영 원본은 변경하지 않습니다. + +**App Bokeh는 RGB 사진 한 장으로 상대적인 깊이를 추정합니다.** MiDaS v2.1 모바일 모델을 LiteRT CPU에서 실행하고 앱이 배경 흐림과 JPEG 인코딩을 수행합니다. 모델은 앱에 포함되며 사진을 서버로 보내지 않습니다. 깊이 맵의 흰색은 가까운 영역, 검은색은 먼 영역을 뜻하고 미터 단위 거리나 센서 DEPTH16이 아닙니다. + +편집·저장 결과는 메모리 사용을 제한하기 위해 긴 변이 최대 1600픽셀입니다. 원본 해상도는 별도로 유지하며, 깊이 추론 해상도는 256×256입니다. 얇은 물체, 반사·투명 물체, 머리카락 경계나 무늬가 적은 장면에서는 깊이와 경계가 부정확할 수 있습니다. 깊이를 구분하지 못하면 실패 이유를 표시하고 원본을 유지합니다. 선택한 지점과 같은 깊이 또는 더 가까운 물체는 선명하게 남습니다. + +App Bokeh는 사진 한 장씩 처리합니다. 연사·AEB·PIP·수동 제어는 제공하지 않으며, `Photo`나 `Video`를 누르면 일반 촬영으로 돌아갑니다. 화면을 다시 열면 원본에서 편집을 다시 시작하므로 저장 전 선택 지점과 강도는 초기화됩니다. [모델 출처와 빌드 절차](https://github.com/TTolsun/hal-camera/blob/main/docs/design/APP-BOKEH.md)를 참고하세요. + ### 제조사 Portrait로 배경을 흐리세요 1. `Lab → Probe`에서 카메라별 `Portrait · Manufacturer Bokeh` 지원 여부와 전용 미리보기·JPEG 해상도를 확인합니다. diff --git a/tools/docgen/regression.test.mjs b/tools/docgen/regression.test.mjs index fadd9a57..c2ca0c60 100644 --- a/tools/docgen/regression.test.mjs +++ b/tools/docgen/regression.test.mjs @@ -114,7 +114,7 @@ test('all configured elements fit the prompt budget and exclude descendant field assert.ok(input.files.length > 0); count++; } } - assert.equal(count, 115); // Depth/Portrait engines, Portrait lifecycle and LIVE camera picker own their evidence. + assert.equal(count, 116); // App Bokeh owns its capture, inference, rendering and editor evidence. `)); }); diff --git a/tools/docgen/state/evidence.json b/tools/docgen/state/evidence.json index 9a039687..8833354e 100644 --- a/tools/docgen/state/evidence.json +++ b/tools/docgen/state/evidence.json @@ -3,7 +3,7 @@ "entries": { "omm:overall-architecture": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "057f692f5c7ba25d", @@ -293,7 +293,7 @@ }, "omm:data-flow": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "b415ef0c3c1996d6", @@ -344,7 +344,7 @@ }, "omm:state-transitions": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "f98fe1d594f993e7", @@ -391,7 +391,7 @@ }, "content:architecture.md/overview": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "37a6ba8e14f48c93", @@ -684,7 +684,7 @@ }, "content:architecture.md/runtime-flow": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "56197667ece1fbbb", @@ -1022,7 +1022,7 @@ } }, "observed": { - "state": "fresh" + "state": "stale" } }, "content:architecture.md/constraints": { @@ -1312,7 +1312,7 @@ } }, "observed": { - "state": "fresh" + "state": "stale" } }, "omm:ui-camera-label": { @@ -1333,7 +1333,7 @@ }, "omm:ui-tool-handoff": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "af533493626cd705", @@ -1349,7 +1349,7 @@ }, "omm:ui-zoom": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "9142002fc98a5ef3", @@ -1367,7 +1367,7 @@ }, "content:_reference/engine.md/contract": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "057f692f5c7ba25d", @@ -1657,7 +1657,7 @@ }, "content:_reference/engine.md/camera2": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "057f692f5c7ba25d", @@ -1947,7 +1947,7 @@ }, "content:_reference/engine.md/camerax": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "057f692f5c7ba25d", @@ -2237,7 +2237,7 @@ }, "content:_reference/engine.md/comparison": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "057f692f5c7ba25d", @@ -2527,7 +2527,7 @@ }, "content:_reference/troubleshooting.md/layer-isolation": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "c7377f64a346f1ea", @@ -2592,7 +2592,7 @@ }, "content:_reference/evidence.md/device-notes": { "observed": { - "state": "fresh" + "state": "stale" }, "accepted": { "codeHash": "057f692f5c7ba25d", From d098038029845ab66c3a17efade77f1220fefd12 Mon Sep 17 00:00:00 2001 From: TTolsun <112919054+TTolsun@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:28:47 +0900 Subject: [PATCH 2/4] Refresh App Bokeh guides and reviewed documentation --- docs/.site-manifest.json | 4 +- docs/architecture.html | 38 ++-- docs/live.html | 12 + guide/_reference/engine.md | 31 +-- guide/_reference/evidence.md | 6 +- guide/_reference/troubleshooting.md | 8 +- guide/architecture.md | 39 ++-- tools/docgen/state/evidence.json | 337 ++++++++++++++++------------ 8 files changed, 275 insertions(+), 200 deletions(-) diff --git a/docs/.site-manifest.json b/docs/.site-manifest.json index 92e4193d..d590ae3e 100644 --- a/docs/.site-manifest.json +++ b/docs/.site-manifest.json @@ -4,7 +4,7 @@ ".nojekyll": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "_inputs/decisions.html": "aadc3cad5fb3b8e37af148aa62ae79f9be0ba19a076fd2837f85c471b1629ded", "_inputs/device-verification.yaml": "1852f48d52f75733b4db973418fc7fad1f0f9d340a8b4b555cf9d49aa7016215", - "architecture.html": "2e724bfe40ab9f43def45e11f6a079574f798dbf974875ebcf591d1dfcc0c1ab", + "architecture.html": "18d8812937585f1958bf42d5bb4320b890eb131c9fa68fe3b9b14d751507f954", "assets/docflow-design.css": "1cc4f34d8df94abdcf70ab2e9c51e456d8ef7f52283a2ef3adb986a6bd38d44c", "assets/media/automation/bridge.png": "bdbd7cbdc28010caa9b8e320e2d311efd35bb2f66cbb9b5cb322af638b7ccbff", "assets/media/automation/opening-sensor.mp4": "8e26f5eeeb0b92cd241b2235aab7ffd122cbf2ac94b66c37fafb440da4ae1178", @@ -72,7 +72,7 @@ "getting-started.html": "75b356487a9be6930c095686fcdc47229ecdd53b73b77f971a770f1e0f387e0f", "glossary.html": "a26775e3f8a0d6af19b7531b2dc86ea7630a9c0511bb34a308e3a630522462f1", "index.html": "34466a88605267322b92082ec1592c72a48a9fd2e3cdee59e66cea770d79372a", - "live.html": "7b43bcd9f7eaaf71491a9555d58e54fb037f785481720ac92512b264cc39e971", + "live.html": "dbe72922ded6e87c9e2c3e7a13109f432296ae90fd9ac9634b97fb22630531df", "probe.html": "1a6eca6e50a2a21ede1c88bd122a000ca4be349733e4866ab6870b676f76447a", "troubleshooting.html": "819baee906880fbaab14d0a649623326227868e748966ddbfe2d0272a50e53cb" } diff --git a/docs/architecture.html b/docs/architecture.html index 3fa2ac2a..f8f00095 100644 --- a/docs/architecture.html +++ b/docs/architecture.html @@ -79,7 +79,7 @@

One system.
Clear responsibilities.

측정·판 Live · MainActivity -앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영하며, 지원 카메라의 제조사 Portrait와 Depth 맵을 선택합니다. +앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영합니다. App Bokeh로 촬영 후 배경 흐림을 조절하며, 지원 카메라의 제조사 Portrait와 Depth 맵도 선택합니다. Multi · ConcurrentCameraActivity @@ -113,7 +113,7 @@

코드를 처음 읽는 순서

근거와 검토 정보
  • 근거 파일: app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, tools/halcam/halcam/cli.py, app/src/main/java/dev/halcamera/MainActivity.kt, app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt, app/src/main/java/dev/halcamera/WorkbenchActivity.kt, app/src/main/java/dev/halcamera/camera/CameraEngine.kt, app/src/main/java/dev/halcamera/telemetry/Telemetry.kt, app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt, app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt, app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt
  • 근거 수준: 코드 확인
  • -
  • 검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review
  • +
  • 검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review

전체 구조도

화살표는 코드를 사용하는 방향입니다. 이벤트와 파일이 이동하는 순서는 데이터 흐름에서 확인하세요.

@@ -214,6 +214,8 @@

코드를 처음 읽는 순서

  • close(done) 완료 전에 다음 카메라를 열지 않습니다.

  • +
  • AppBokehProcessor는 일반 JPEG를 RGB로 읽고 기기 안에서 상대 깊이를 추정합니다. RelativeDepth와 DepthBokeh는 깊이 정규화·선택 지점·배경 흐림을 순수 Kotlin으로 계산합니다. MainAppBokehBridge와 AppBokehEditor가 촬영 후 편집을 연결하며 원본과 보케 결과를 별도로 보관합니다.

    +
  • 엔진 계약(MediaCapture, LiveTuning, TouchMetering)과 두 엔진의 구현, 두 엔진의 차이는 Engine Comparison에 있습니다.

  • 벤치마크 RECORD 단계의 recorder 상태 기계는 BenchmarkRecorder가 담당하고 Camera2Engine은 그 호출을 위임합니다.

    @@ -292,9 +294,9 @@

    코드를 처음 읽는 순서

    ui/RecentMediaButton은 받은 썸네일을 원형으로 그리며, 항목이 동영상이면 가운데에 재생 표시를 겹쳐 접근성 설명뿐 아니라 화면으로도 종류를 알 수 있게 합니다.

    근거와 검토 정보
      -
    • 근거 파일: app/src/main/java/dev/halcamera/camera/MultiSession.kt, app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt, app/src/main/java/dev/halcamera/camera/MultiRecording.kt, app/src/main/java/dev/halcamera/camera/SplitDragGesture.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, app/src/main/java/dev/halcamera/cli/LiveController.kt, app/src/main/java/dev/halcamera/cli/BenchmarkController.kt, app/src/main/java/dev/halcamera/cli/CliLibrary.kt, app/src/main/java/dev/halcamera/MainCliBridge.kt, app/src/main/java/dev/halcamera/DualCliBridge.kt, app/src/main/java/dev/halcamera/camera/CameraEngine.kt, app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt, app/src/main/java/dev/halcamera/benchmark/platform/EnvironmentProbe.kt, app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt, app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunIndex.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkCsv.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt, app/src/main/java/dev/halcamera/MainActivity.kt, app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt, app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt, app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt, app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt, app/src/main/java/dev/halcamera/ui/RecentMediaButton.kt, app/src/main/java/dev/halcamera/ui/Look.kt, app/src/main/java/dev/halcamera/cts/recording/BasicRecordingRules.kt, app/src/main/java/dev/halcamera/cts/CtsEntryActivity.kt, app/src/main/java/dev/halcamera/cts/vendored/VendoredCaseActivity.kt, app/src/main/java/dev/halcamera/cts/vendored/VendoredCtsListActivity.kt, app/src/main/java/dev/halcamera/cts/CtsCaseActivity.kt, app/src/main/java/dev/halcamera/cts/CtsCaseListActivity.kt, app/src/main/java/dev/halcamera/cts/suite/CtsChecklistActivity.kt, app/src/main/java/dev/halcamera/cts/suite/CtsSuiteRunActivity.kt, app/src/main/java/dev/halcamera/cts/suite/SuitePlan.kt, app/src/main/java/dev/halcamera/cts/suite/SuiteReport.kt, app/src/main/java/dev/halcamera/cts/CtsCatalog.kt, app/src/main/java/dev/halcamera/cts/CtsRunner.kt, app/src/main/java/dev/halcamera/cts/CameraCaseRunner.kt, app/src/main/java/dev/halcamera/cts/Camera2Ops.kt, app/src/main/java/dev/halcamera/cts/onoff/FastOnOffRules.kt, app/src/main/java/dev/halcamera/cts/switching/SwitchingRules.kt, app/src/main/java/dev/halcamera/cts/sizes/AllSizeOnOffRules.kt, app/src/main/java/dev/halcamera/cts/combination/StillPreviewCombinationRules.kt, app/src/main/java/dev/halcamera/cts/snapshot/VideoSnapshotRules.kt, app/src/main/java/dev/halcamera/CameraProbeActivity.kt, app/src/main/java/dev/halcamera/camera/CameraProbe.kt, app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt
    • +
    • 근거 파일: app/src/main/java/dev/halcamera/MainAppBokehBridge.kt, app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt, app/src/main/java/dev/halcamera/camera/RelativeDepth.kt, app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt, app/src/main/java/dev/halcamera/camera/MultiSession.kt, app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt, app/src/main/java/dev/halcamera/camera/MultiRecording.kt, app/src/main/java/dev/halcamera/camera/SplitDragGesture.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, app/src/main/java/dev/halcamera/cli/LiveController.kt, app/src/main/java/dev/halcamera/cli/BenchmarkController.kt, app/src/main/java/dev/halcamera/cli/CliLibrary.kt, app/src/main/java/dev/halcamera/MainCliBridge.kt, app/src/main/java/dev/halcamera/DualCliBridge.kt, app/src/main/java/dev/halcamera/camera/CameraEngine.kt, app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt, app/src/main/java/dev/halcamera/benchmark/platform/EnvironmentProbe.kt, app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt, app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunIndex.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkCsv.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt, app/src/main/java/dev/halcamera/MainActivity.kt, app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt, app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt, app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt, app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt, app/src/main/java/dev/halcamera/ui/RecentMediaButton.kt, app/src/main/java/dev/halcamera/ui/Look.kt, app/src/main/java/dev/halcamera/cts/recording/BasicRecordingRules.kt, app/src/main/java/dev/halcamera/cts/CtsEntryActivity.kt, app/src/main/java/dev/halcamera/cts/vendored/VendoredCaseActivity.kt, app/src/main/java/dev/halcamera/cts/vendored/VendoredCtsListActivity.kt, app/src/main/java/dev/halcamera/cts/CtsCaseActivity.kt, app/src/main/java/dev/halcamera/cts/CtsCaseListActivity.kt, app/src/main/java/dev/halcamera/cts/suite/CtsChecklistActivity.kt, app/src/main/java/dev/halcamera/cts/suite/CtsSuiteRunActivity.kt, app/src/main/java/dev/halcamera/cts/suite/SuitePlan.kt, app/src/main/java/dev/halcamera/cts/suite/SuiteReport.kt, app/src/main/java/dev/halcamera/cts/CtsCatalog.kt, app/src/main/java/dev/halcamera/cts/CtsRunner.kt, app/src/main/java/dev/halcamera/cts/CameraCaseRunner.kt, app/src/main/java/dev/halcamera/cts/Camera2Ops.kt, app/src/main/java/dev/halcamera/cts/onoff/FastOnOffRules.kt, app/src/main/java/dev/halcamera/cts/switching/SwitchingRules.kt, app/src/main/java/dev/halcamera/cts/sizes/AllSizeOnOffRules.kt, app/src/main/java/dev/halcamera/cts/combination/StillPreviewCombinationRules.kt, app/src/main/java/dev/halcamera/cts/snapshot/VideoSnapshotRules.kt, app/src/main/java/dev/halcamera/CameraProbeActivity.kt, app/src/main/java/dev/halcamera/camera/CameraProbe.kt, app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt
    • 근거 수준: 코드 확인
    • -
    • 검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review
    • +
    • 검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review

    CLI 책임을 따라가세요

    CliCommands는 명령별 필수 인자·허용 옵션·기본 제한·실행 대상과 Live에서 실행되는 방식(flow)·녹화 여부를 한곳에서 정의하고 hello.command_specs로 제공합니다.

    @@ -313,6 +315,7 @@

    주요 실행 흐름

    Live 셔터 조작은 MainActivity에서 선택한 엔진의 촬영·녹화 동작으로 이어집니다.

    Live의 Depth 보기는 DepthCameraEngine → ImageReader → DepthFrame → DepthMapView로 연결합니다. 컬러 촬영과 분리된 보기이며, Telemetry에는 버퍼의 시각·크기·형식과 capture 콜백만 기록합니다. 맵 픽셀을 벤치마크나 incident ZIP의 입력으로 전달하지 않습니다. 사용 순서는 Depth 맵 보기에 있습니다.

    같은 Depth 메뉴의 Portrait는 PortraitCameraEngine → CameraExtensionSession → JPEG ImageReader → MediaLibrary로 연결합니다. 제조사 Bokeh가 적용된 컬러 사진을 저장하며 별도의 센서 Depth 맵을 받지 않습니다. 확장 처리 이벤트는 일반 Camera2 프레임 지표와 구분하고, 사진 픽셀은 측정 파일에 넣지 않습니다.

    +

    App Bokeh는 일반 Camera2의 Preview+JPEG로 촬영한 원본을 저장한 뒤 MainAppBokehBridge → AppBokehEditor → AppBokehProcessor로 전달합니다. JPEG를 RGB로 디코딩하고 MiDaS 모델이 256×256 Float 상대 깊이를 추론합니다. 사용자가 선택한 깊이보다 먼 영역을 앱에서 흐린 뒤 보케 JPEG·깊이 PNG·처리 메타데이터를 별도 저장합니다. 원본은 그대로 유지하며 분석용 YUV·HAL Depth·제조사 보케 버퍼는 사용하지 않습니다.

    벤치마크는 별도 화면인 BenchmarkActivity가 준비를 마친 뒤 BenchmarkRunner.start()를 호출하여 시작합니다.

    Telemetry.callback(...)이 반환한 Camera2 콜백의 onCaptureStarted는 프레임워크 신호를 기록하며, Live 셔터와 벤치마크 시작을 연결하는 메서드가 아닙니다.

    실행에서 저장까지

    @@ -439,9 +442,9 @@

    CLI 요청과 결과 수집

    이전 CameraX 그래프가 닫히기 전에 MainActivity가 다시 카메라를 열지 않도록 합니다.

    근거와 검토 정보
      -
    • 근거 파일: app/src/main/java/dev/halcamera/cli/CliProvider.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, tools/halcam/halcam/cli.py, tools/halcam/halcam/download.py, app/src/main/java/dev/halcamera/MainActivity.kt, app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt, app/src/main/java/dev/halcamera/GalleryActivity.kt, app/src/main/java/dev/halcamera/camera/MediaLibrary.kt, app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt, app/src/main/java/dev/halcamera/telemetry/Telemetry.kt, app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt, app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunRetention.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt, app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt, app/src/main/java/dev/halcamera/benchmark/platform/LaunchDiagnostics.kt, app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt
    • +
    • 근거 파일: app/src/main/java/dev/halcamera/MainAppBokehBridge.kt, app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt, app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt, app/src/main/java/dev/halcamera/cli/CliProvider.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, tools/halcam/halcam/cli.py, tools/halcam/halcam/download.py, app/src/main/java/dev/halcamera/MainActivity.kt, app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt, app/src/main/java/dev/halcamera/GalleryActivity.kt, app/src/main/java/dev/halcamera/camera/MediaLibrary.kt, app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt, app/src/main/java/dev/halcamera/telemetry/Telemetry.kt, app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt, app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunRetention.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt, app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt, app/src/main/java/dev/halcamera/benchmark/platform/LaunchDiagnostics.kt, app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt
    • 근거 수준: 코드 확인
    • -
    • 검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review
    • +
    • 검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review

    이벤트와 결과가 전달되는 경로

    graph TB
    @@ -636,6 +639,7 @@ 

    실행과 측정의 제약

    Depth 보기는 컬러와 Depth를 같은 요청에 포함하지 않는 별도 Camera2 세션입니다. HAL의 동시 요청 허용 선언은 특정 출력 조합의 성공을 보장하지 않습니다. DepthCameraEngine은 촬영·녹화·수동 제어·줌·터치 측광을 제공하지 않으며 CLI 제어 변경도 거절합니다. Photo·Video 또는 CLI 촬영 준비로 전환하면 Depth를 해제합니다.

    제조사 Portrait도 일반 촬영과 별도의 확장 세션을 사용합니다. Bokeh 확장 전용 해상도로 컬러 미리보기와 JPEG만 요청하며, 세션이 거절되면 다른 크기로 자동 대체하지 않습니다. 단일 촬영의 JPEG를 받기 전에 실패하거나 종료되면 세션을 해제하여 늦은 이미지를 다음 촬영에 연결하지 않습니다. 이미 받은 JPEG의 저장 작업은 종료 후에도 마칠 수 있습니다.

    +

    App Bokeh는 제조사 Portrait와 달리 일반 JPEG 저장 이후의 앱 처리입니다. 상대 깊이는 센서 거리나 DEPTH16으로 해석하지 않으며, 편집·저장 해상도는 긴 변 최대 1600픽셀로 제한합니다. 원본은 수정하지 않습니다. 추론·렌더·추가 저장 실패는 원본을 삭제하지 않으며, Activity 중단 후에는 편집기의 늦은 결과를 화면에 적용하지 않습니다.

    Multi 모드의 MultiSession은 CameraEngine과 별도입니다. Camera2는 선택한 독립 장치를 연 뒤 세션을 구성하며, CameraX는 광고된 동시 조합을 바인딩합니다.

    모든 장치의 종료 통지를 확인해야 출력을 해제하고 다음 동시 세션을 열 수 있습니다. 한 명령의 여러 사진을 센서 동기 촬영으로 해석하지 않습니다.

    Multi V의 MicrophoneCapture는 AudioRecord의 MONOTONIC 하드웨어 시각과 PCM 프레임 위치로 오디오 원점을 구합니다.

    @@ -674,9 +678,9 @@

    CLI의 접근과 완료 경계

    Live 카메라 종료 후 벤치마크 화면으로 이동하고 기존 측정·판정 규칙을 유지합니다.

    근거와 검토 정보
      -
    • 근거 파일: app/src/main/java/dev/halcamera/camera/MultiSession.kt, app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt, app/src/main/java/dev/halcamera/camera/MultiRecording.kt, app/src/main/java/dev/halcamera/camera/MicrophoneCapture.kt, app/src/main/java/dev/halcamera/camera/TimestampedVideoRecording.kt, app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt, app/src/main/java/dev/halcamera/cli/CliProvider.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, app/src/main/java/dev/halcamera/cli/CommandStore.kt, app/src/main/java/dev/halcamera/camera/CameraEngine.kt, app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt, app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt, app/src/main/java/dev/halcamera/camera/CameraEndpointResolver.kt, app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RegressionRules.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkStore.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkIndex.kt, app/src/main/java/dev/halcamera/benchmark/domain/AtomicFiles.kt
    • +
    • 근거 파일: app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt, app/src/main/java/dev/halcamera/camera/RelativeDepth.kt, app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt, app/src/main/java/dev/halcamera/camera/MultiSession.kt, app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt, app/src/main/java/dev/halcamera/camera/MultiRecording.kt, app/src/main/java/dev/halcamera/camera/MicrophoneCapture.kt, app/src/main/java/dev/halcamera/camera/TimestampedVideoRecording.kt, app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt, app/src/main/java/dev/halcamera/cli/CliProvider.kt, app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt, app/src/main/java/dev/halcamera/cli/CommandStore.kt, app/src/main/java/dev/halcamera/camera/CameraEngine.kt, app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt, app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt, app/src/main/java/dev/halcamera/camera/CameraEndpointResolver.kt, app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt, app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt, app/src/main/java/dev/halcamera/benchmark/domain/RegressionRules.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt, app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkStore.kt, app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkIndex.kt, app/src/main/java/dev/halcamera/benchmark/domain/AtomicFiles.kt
    • 근거 수준: 코드 확인
    • -
    • 검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review
    • +
    • 검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review

    변경 후 검증

    Android 의존성이 없는 러너와 평가 로직은 JVM 단위 테스트로 확인할 수 있습니다. 저장소 루트에서 실행하세요.

    @@ -716,17 +720,17 @@

    미완성 기능과 추가 검증

    구조 원본 data-flow 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review 구조 원본 overall-architecture 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review 구조 원본 state-transitions 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review 구조 원본 ui-camera-label @@ -736,32 +740,32 @@

    미완성 기능과 추가 검증

    구조 원본 ui-tool-handoff 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review 구조 원본 ui-zoom 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review 원고 overview 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review 원고 module-roles 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review 원고 runtime-flow 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review 원고 constraints 최신 -검토 2026-10-19 @ 2fb9f2aa · codex-user-authorized-portrait-review +검토 2026-10-24 @ e4de2762 · codex-app-bokeh-code-review

    표의 최신은 기록된 근거와 원고가 검토 이후 바뀌지 않았다는 뜻입니다. 모든 기기에서 동작을 검증했다는 뜻은 아닙니다.

    diff --git a/docs/live.html b/docs/live.html index 58769dcf..9342ac17 100644 --- a/docs/live.html +++ b/docs/live.html @@ -34,6 +34,7 @@

    Set. Capture. Inspect.

    같은 장면도, 촬영 조건을

    Photo로 한 장을 찍고, Video로 움직임을 담아 보세요. 처음에는 기본값이면 충분합니다.

    • 촬영 · 사진·연사·AEB·동영상을 촬영합니다.
    • +
    • App Bokeh · 인물이나 사물의 깊이를 추정하고 촬영 후 배경 흐림을 조절합니다.
    • Portrait · Depth 메뉴에서 제조사 Bokeh 사진을 선택합니다.
    • 수동 설정 · ISO·노출 시간·초점을 고정합니다.
    • 스트림 설정 · 출력 크기·FPS·포맷을 바꿉니다.
    • @@ -49,6 +50,17 @@

      Live에서 촬영하세요

      기본 사진은 카메라가 만든 JPEG 한 장입니다. DCIM/HALCamera에 저장하며, 촬영 메타데이터 JSON도 남깁니다.

      JPEG 목록에서 YUV를 고르면 앱이 변환한 JPEG를 저장합니다. Camera2에서 RAW를 켜면 DNG도 저장합니다.

      모드나 엔진을 바꾸면 새 촬영으로 시작합니다. PIP·스트림·줌·촬영 제어는 초기화됩니다. Callback의 고정 시간은 사용자 설정으로 유지합니다.

      +

      앱에서 배경을 흐리세요

      +
        +
      1. Depth → App Bokeh를 선택합니다. 제조사 Bokeh나 센서 Depth 지원 여부와 관계없이 일반 JPEG 촬영을 사용합니다.
      2. +
      3. 셔터를 누릅니다. 원본을 저장한 뒤 기기 안에서 깊이를 추정합니다. LIVE 미리보기에는 이 효과가 적용되지 않습니다.
      4. +
      5. 편집 화면에서 선명하게 유지할 인물이나 사물을 누릅니다. 선택한 지점보다 멀다고 추정된 영역을 흐리게 처리합니다.
      6. +
      7. 슬라이더로 강도를 조절하고 Bokeh, Original, Depth로 결과를 비교합니다. 강도 0은 처리용 원본 픽셀을 유지합니다.
      8. +
      9. Save copy를 누릅니다. 보케 JPEG와 추정 깊이 PNG는 DCIM/HALCamera, 모델·원본 URI·선택 지점·강도 기록은 Download/HALCamera에 저장합니다. 촬영 원본은 변경하지 않습니다.
      10. +
      +

      App Bokeh는 RGB 사진 한 장으로 상대적인 깊이를 추정합니다. MiDaS v2.1 모바일 모델을 LiteRT CPU에서 실행하고 앱이 배경 흐림과 JPEG 인코딩을 수행합니다. 모델은 앱에 포함되며 사진을 서버로 보내지 않습니다. 깊이 맵의 흰색은 가까운 영역, 검은색은 먼 영역을 뜻하고 미터 단위 거리나 센서 DEPTH16이 아닙니다.

      +

      편집·저장 결과는 메모리 사용을 제한하기 위해 긴 변이 최대 1600픽셀입니다. 원본 해상도는 별도로 유지하며, 깊이 추론 해상도는 256×256입니다. 얇은 물체, 반사·투명 물체, 머리카락 경계나 무늬가 적은 장면에서는 깊이와 경계가 부정확할 수 있습니다. 깊이를 구분하지 못하면 실패 이유를 표시하고 원본을 유지합니다. 선택한 지점과 같은 깊이 또는 더 가까운 물체는 선명하게 남습니다.

      +

      App Bokeh는 사진 한 장씩 처리합니다. 연사·AEB·PIP·수동 제어는 제공하지 않으며, Photo나 Video를 누르면 일반 촬영으로 돌아갑니다. 화면을 다시 열면 원본에서 편집을 다시 시작하므로 저장 전 선택 지점과 강도는 초기화됩니다. 모델 출처와 빌드 절차를 참고하세요.

      제조사 Portrait로 배경을 흐리세요

      1. Lab → Probe에서 카메라별 Portrait · Manufacturer Bokeh 지원 여부와 전용 미리보기·JPEG 해상도를 확인합니다.
      2. diff --git a/guide/_reference/engine.md b/guide/_reference/engine.md index 41137a14..eece2191 100644 --- a/guide/_reference/engine.md +++ b/guide/_reference/engine.md @@ -23,6 +23,7 @@ nav_order: 5 | 항목 | Camera2 | CameraX | | --- | --- | --- | +| App Bokeh | 일반 Preview+JPEG로 촬영하고 앱에서 상대 깊이를 추정해 배경 흐림을 적용합니다. 인물·사물 모두 선택할 수 있으며 긴 변 최대 1600픽셀로 저장합니다. | App Bokeh를 선택하면 Camera2로 전환합니다. | | Depth 보기 | DEPTH_OUTPUT과 DEPTH16 해상도를 공개한 카메라에서 별도 Depth 전용 세션을 엽니다. 맵 표시만 제공합니다. | Depth 해상도를 선택하면 Camera2로 전환합니다. | | 제조사 Portrait | API 31 이상의 BOKEH 확장 세션으로 전용 미리보기와 JPEG를 받습니다. Depth 메뉴에서 선택하며 수동 제어·RAW·PIP·영상은 제공하지 않습니다. | Portrait를 선택하면 Camera2로 전환합니다. | | 수동 촬영 | ISO·노출 시간·초점과 WB를 프리뷰·사진·녹화에 적용합니다. 지원 여부와 실제 적용값을 표시합니다. | Manual 버튼을 미지원 상태로 비활성화합니다. | @@ -59,9 +60,9 @@ PIP와 Multi는 Android 11 이상에서 제공합니다. 저장물과 설정 방
        근거와 검토 정보 -- 근거 파일: `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2StillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2LiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeterRequests.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXPipSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/LivePipSession.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt` +- 근거 파일: `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2StillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2LiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeterRequests.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXPipSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/LivePipSession.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review
        @@ -179,7 +180,9 @@ classDiagram 종료 통지와 다음 엔진의 대기 순서는 [엔진 전환 그림](architecture.md#카메라-열기와-닫기)을 확인하세요. -Photo·Video·Depth·Multi · P·Multi · V 사이에서 모드를 바꾸면 기존 세션을 닫고 스트림을 다시 구성합니다. 같은 모드를 다시 누르면 유지하며 Depth 버튼은 해상도 선택창을 엽니다. 일반 단일 카메라는 새 프리뷰의 첫 화면 갱신 뒤, Multi는 모든 장치의 합성 프리뷰가 준비된 뒤에 촬영 버튼을 활성화합니다. Depth는 첫 맵 수신 후에도 촬영 버튼을 비활성화합니다. CLI 호환용 Dual도 두 프리뷰가 모두 갱신된 뒤에 촬영할 수 있습니다. 모드 선택 자체로 사진이나 녹화를 시작하지는 않습니다. +Photo·Video·Depth·Multi · P·Multi · V 사이에서 모드를 바꾸면 기존 세션을 닫고 스트림을 다시 구성합니다. 같은 모드를 다시 누르면 유지하며 Depth 버튼은 App Bokeh·제조사 Portrait·센서 Depth 선택창을 엽니다. 일반 단일 카메라는 새 프리뷰의 첫 화면 갱신 뒤, Multi는 모든 장치의 합성 프리뷰가 준비된 뒤에 촬영 버튼을 활성화합니다. 센서 Depth는 첫 맵 수신 후에도 촬영 버튼을 비활성화합니다. CLI 호환용 Dual도 두 프리뷰가 모두 갱신된 뒤에 촬영할 수 있습니다. 모드 선택 자체로 사진이나 녹화를 시작하지는 않습니다. + +App Bokeh는 Camera2의 일반 Preview+JPEG 세션에서 촬영한 원본을 편집기로 전달합니다. 분석 YUV와 센서 Depth를 사용하지 않으며, 모델로 추정한 상대 깊이에 따라 저장할 복사본에 배경 흐림을 적용합니다. 카메라·엔진 변경이나 일반 스트림 설정 적용 시 App Bokeh 모드를 종료합니다. ### Benchmark와 CLI가 쓰는 엔진 @@ -220,9 +223,9 @@ Live 제어와 터치 측광은 다음 이벤트를 추가로 남깁니다.
        근거와 검토 정보 -- 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/ui/LiveControlBar.kt`, `app/src/main/java/dev/halcamera/ui/FocusRing.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/StartCardPresenter.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt` +- 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/ui/LiveControlBar.kt`, `app/src/main/java/dev/halcamera/ui/FocusRing.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/StartCardPresenter.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review
        @@ -320,6 +323,8 @@ Live의 기본 세션은 프리뷰, YUV_420_888, JPEG 세 스트림으로 구성 | YUV (`analysis_acquire_latest`) | 640×480 이하에서 가장 큰 크기 | 프레임 도착 기록, 앱 변환을 선택한 사진 입력 | | JPEG (`still`) | 1920×1080 이하에서 가장 큰 크기 | 카메라 JPEG 사진, 벤치마크 still | +App Bokeh에서는 이 기본 크기의 Preview와 JPEG만 구성하고 분석 YUV는 끕니다. 저장한 JPEG를 앱의 깊이 추정 모델에 전달하므로 촬영 세션에 DEPTH16 출력을 추가하지 않습니다. + 벤치마크 세션은 profile의 `StreamSpec` 크기를 그대로 사용합니다. 지원하지 않는 크기이면 작은 크기로 대체하지 않고 구성 단계에서 실패합니다. 같은 profile ID로 다른 크기를 측정하면 비교가 무의미해지기 때문입니다. 명시한 Live 설정도 작은 크기로 대체하지 않습니다. @@ -606,9 +611,9 @@ Camera2에서 짧게 터치한 경우입니다. 긴 누르기는 별도의 AE
        근거와 검토 정보 -- 근거 파일: `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/LivePipSession.kt`, `app/src/main/java/dev/halcamera/camera/PipSessionQuery.kt`, `app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt`, `app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt`, `app/src/main/java/dev/halcamera/camera/LiveStreamSettings.kt`, `app/src/main/java/dev/halcamera/camera/LiveStabilization.kt`, `app/src/main/java/dev/halcamera/camera/LiveStreamCapabilities.kt`, `app/src/main/java/dev/halcamera/camera/LiveSessionCheck.kt`, `app/src/main/java/dev/halcamera/camera/Camera2StillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2LiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/Camera2VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt`, `app/src/main/java/dev/halcamera/camera/PreviewBufferRelay.kt`, `app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt`, `app/src/main/java/dev/halcamera/camera/StreamConfiguration.kt`, `app/src/main/java/dev/halcamera/camera/StillPair.kt`, `app/src/main/java/dev/halcamera/camera/YuvPacking.kt`, `app/src/main/java/dev/halcamera/camera/RawFrame.kt`, `app/src/main/java/dev/halcamera/camera/DngOutput.kt`, `app/src/main/java/dev/halcamera/camera/StillEncoding.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/camera/LiveControlRequests.kt`, `app/src/main/java/dev/halcamera/camera/ManualControls.kt`, `app/src/main/java/dev/halcamera/camera/ManualControlRequests.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeter.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeterRequests.kt` +- 근거 파일: `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/LivePipSession.kt`, `app/src/main/java/dev/halcamera/camera/PipSessionQuery.kt`, `app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt`, `app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt`, `app/src/main/java/dev/halcamera/camera/LiveStreamSettings.kt`, `app/src/main/java/dev/halcamera/camera/LiveStabilization.kt`, `app/src/main/java/dev/halcamera/camera/LiveStreamCapabilities.kt`, `app/src/main/java/dev/halcamera/camera/LiveSessionCheck.kt`, `app/src/main/java/dev/halcamera/camera/Camera2StillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2LiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/Camera2VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt`, `app/src/main/java/dev/halcamera/camera/PreviewBufferRelay.kt`, `app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt`, `app/src/main/java/dev/halcamera/camera/StreamConfiguration.kt`, `app/src/main/java/dev/halcamera/camera/StillPair.kt`, `app/src/main/java/dev/halcamera/camera/YuvPacking.kt`, `app/src/main/java/dev/halcamera/camera/RawFrame.kt`, `app/src/main/java/dev/halcamera/camera/DngOutput.kt`, `app/src/main/java/dev/halcamera/camera/StillEncoding.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/camera/LiveControlRequests.kt`, `app/src/main/java/dev/halcamera/camera/ManualControls.kt`, `app/src/main/java/dev/halcamera/camera/ManualControlRequests.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeter.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeterRequests.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review
        @@ -827,7 +832,7 @@ AE 재잠금은 Camera2와 같은 [AeRelock 상태도](#노출은-언제-다시- - 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXPipSession.kt`, `app/src/main/java/dev/halcamera/camera/PipSessionQuery.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/CameraXVideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/camera/StillEncoding.kt`, `app/src/main/java/dev/halcamera/camera/YuvPacking.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/StreamConfiguration.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeter.kt`, `app/build.gradle.kts` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review @@ -847,11 +852,11 @@ AE 재잠금은 Camera2와 같은 [AeRelock 상태도](#노출은-언제-다시- | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `contract` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `camera2` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `camerax` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `comparison` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `contract` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `camera2` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `camerax` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `comparison` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | diff --git a/guide/_reference/evidence.md b/guide/_reference/evidence.md index 4ee9868d..284161ee 100644 --- a/guide/_reference/evidence.md +++ b/guide/_reference/evidence.md @@ -80,7 +80,7 @@ Galaxy S25+·Android 16에서 2026년 9월 17~19일, versionCode 106~108로 수 - 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt` - 기기 검증: `V-002` - 근거 수준: 기기 검증 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review @@ -98,8 +98,8 @@ Galaxy S25+·Android 16에서 2026년 9월 17~19일, versionCode 106~108로 수 | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `device-notes` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `device-notes` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | diff --git a/guide/_reference/troubleshooting.md b/guide/_reference/troubleshooting.md index c5ffd7f7..15922219 100644 --- a/guide/_reference/troubleshooting.md +++ b/guide/_reference/troubleshooting.md @@ -140,7 +140,7 @@ PC의 대기 시간이 끝나도 앱 작업은 계속될 수 있습니다. 요 - 근거 파일: `app/src/main/java/dev/halcamera/cli/CommandStore.kt`, `tools/halcam/halcam/cli.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/ManualControlRequests.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review @@ -172,9 +172,9 @@ PC의 대기 시간이 끝나도 앱 작업은 계속될 수 있습니다. 요 | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `data-flow` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `layer-isolation` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | +| 구조 원본 `data-flow` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `layer-isolation` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | diff --git a/guide/architecture.md b/guide/architecture.md index 02e190ae..29942fbb 100644 --- a/guide/architecture.md +++ b/guide/architecture.md @@ -32,7 +32,7 @@ nav_order: 4 | 화면 | 할 수 있는 일 | | --- | --- | -| Live · `MainActivity` | 앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영하며, 지원 카메라의 제조사 Portrait와 Depth 맵을 선택합니다. | +| Live · `MainActivity` | 앱을 열면 나오는 화면입니다. 프리뷰를 보고 사진·동영상을 촬영합니다. App Bokeh로 촬영 후 배경 흐림을 조절하며, 지원 카메라의 제조사 Portrait와 Depth 맵도 선택합니다. | | Multi · `ConcurrentCameraActivity` | 독립 카메라 장치별 사진·영상을 제공합니다. Live의 Multi · P와 Multi · V에서 엽니다. | | Lab · `WorkbenchActivity` | 기기 정보, 검사 도구, 저장된 결과와 설정을 엽니다. | | Benchmark · `BenchmarkActivity` | 정해진 조건(profile)으로 측정하고 결과를 저장합니다. | @@ -57,7 +57,7 @@ PC에서 같은 기능을 실행하려면 [CLI](cli.md)를 사용합니다. `Cli - 근거 파일: `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `tools/halcam/halcam/cli.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt`, `app/src/main/java/dev/halcamera/WorkbenchActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review @@ -138,6 +138,7 @@ flowchart TB - 엔진 계약, Camera2·CameraX 구현, 엔드포인트 열거를 제공합니다. `DepthCameraEngine`은 컬러 출력 없이 DEPTH16 맵만 받으며 `DepthFrame`이 순수 Kotlin으로 표시용 표본을 만듭니다. - `PortraitCameraEngine`은 LIVE의 제조사 BOKEH 전용 세션입니다. MainPortraitBridge가 JPEG 크기를 선택하고 MainDepthBridge가 Depth Map과 Portrait의 진입점을 구분합니다. 이 경로는 벤치마크를 실행하거나 센서 Depth 맵을 저장하지 않습니다. - `close(done)` 완료 전에 다음 카메라를 열지 않습니다. +- `AppBokehProcessor`는 일반 JPEG를 RGB로 읽고 기기 안에서 상대 깊이를 추정합니다. `RelativeDepth`와 `DepthBokeh`는 깊이 정규화·선택 지점·배경 흐림을 순수 Kotlin으로 계산합니다. `MainAppBokehBridge`와 `AppBokehEditor`가 촬영 후 편집을 연결하며 원본과 보케 결과를 별도로 보관합니다. - 엔진 계약(`MediaCapture`, `LiveTuning`, `TouchMetering`)과 두 엔진의 구현, 두 엔진의 차이는 [Engine Comparison](engine.md)에 있습니다. - 벤치마크 RECORD 단계의 recorder 상태 기계는 `BenchmarkRecorder`가 담당하고 `Camera2Engine`은 그 호출을 위임합니다. - 카메라를 화면에 적는 이름(`Camera · 0 (Wide · Rear)`)도 이 패키지의 `CameraLabel` 하나가 만들며, Live·Benchmark·Probe·CTS가 모두 그것을 부릅니다. @@ -251,9 +252,9 @@ Camera2의 RAW (DNG)는 `RawFrame`이 RAW 샘플을 복사하고 `DngOutput`이
        근거와 검토 정보 -- 근거 파일: `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/SplitDragGesture.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/cli/BenchmarkController.kt`, `app/src/main/java/dev/halcamera/cli/CliLibrary.kt`, `app/src/main/java/dev/halcamera/MainCliBridge.kt`, `app/src/main/java/dev/halcamera/DualCliBridge.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/EnvironmentProbe.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunIndex.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkCsv.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt`, `app/src/main/java/dev/halcamera/ui/RecentMediaButton.kt`, `app/src/main/java/dev/halcamera/ui/Look.kt`, `app/src/main/java/dev/halcamera/cts/recording/BasicRecordingRules.kt`, `app/src/main/java/dev/halcamera/cts/CtsEntryActivity.kt`, `app/src/main/java/dev/halcamera/cts/vendored/VendoredCaseActivity.kt`, `app/src/main/java/dev/halcamera/cts/vendored/VendoredCtsListActivity.kt`, `app/src/main/java/dev/halcamera/cts/CtsCaseActivity.kt`, `app/src/main/java/dev/halcamera/cts/CtsCaseListActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/CtsChecklistActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/CtsSuiteRunActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/SuitePlan.kt`, `app/src/main/java/dev/halcamera/cts/suite/SuiteReport.kt`, `app/src/main/java/dev/halcamera/cts/CtsCatalog.kt`, `app/src/main/java/dev/halcamera/cts/CtsRunner.kt`, `app/src/main/java/dev/halcamera/cts/CameraCaseRunner.kt`, `app/src/main/java/dev/halcamera/cts/Camera2Ops.kt`, `app/src/main/java/dev/halcamera/cts/onoff/FastOnOffRules.kt`, `app/src/main/java/dev/halcamera/cts/switching/SwitchingRules.kt`, `app/src/main/java/dev/halcamera/cts/sizes/AllSizeOnOffRules.kt`, `app/src/main/java/dev/halcamera/cts/combination/StillPreviewCombinationRules.kt`, `app/src/main/java/dev/halcamera/cts/snapshot/VideoSnapshotRules.kt`, `app/src/main/java/dev/halcamera/CameraProbeActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraProbe.kt`, `app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt` +- 근거 파일: `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt`, `app/src/main/java/dev/halcamera/camera/RelativeDepth.kt`, `app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt`, `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/SplitDragGesture.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/cli/BenchmarkController.kt`, `app/src/main/java/dev/halcamera/cli/CliLibrary.kt`, `app/src/main/java/dev/halcamera/MainCliBridge.kt`, `app/src/main/java/dev/halcamera/DualCliBridge.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/EnvironmentProbe.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunIndex.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkCsv.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt`, `app/src/main/java/dev/halcamera/ui/RecentMediaButton.kt`, `app/src/main/java/dev/halcamera/ui/Look.kt`, `app/src/main/java/dev/halcamera/cts/recording/BasicRecordingRules.kt`, `app/src/main/java/dev/halcamera/cts/CtsEntryActivity.kt`, `app/src/main/java/dev/halcamera/cts/vendored/VendoredCaseActivity.kt`, `app/src/main/java/dev/halcamera/cts/vendored/VendoredCtsListActivity.kt`, `app/src/main/java/dev/halcamera/cts/CtsCaseActivity.kt`, `app/src/main/java/dev/halcamera/cts/CtsCaseListActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/CtsChecklistActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/CtsSuiteRunActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/SuitePlan.kt`, `app/src/main/java/dev/halcamera/cts/suite/SuiteReport.kt`, `app/src/main/java/dev/halcamera/cts/CtsCatalog.kt`, `app/src/main/java/dev/halcamera/cts/CtsRunner.kt`, `app/src/main/java/dev/halcamera/cts/CameraCaseRunner.kt`, `app/src/main/java/dev/halcamera/cts/Camera2Ops.kt`, `app/src/main/java/dev/halcamera/cts/onoff/FastOnOffRules.kt`, `app/src/main/java/dev/halcamera/cts/switching/SwitchingRules.kt`, `app/src/main/java/dev/halcamera/cts/sizes/AllSizeOnOffRules.kt`, `app/src/main/java/dev/halcamera/cts/combination/StillPreviewCombinationRules.kt`, `app/src/main/java/dev/halcamera/cts/snapshot/VideoSnapshotRules.kt`, `app/src/main/java/dev/halcamera/CameraProbeActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraProbe.kt`, `app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review
        @@ -295,6 +296,8 @@ Live의 Depth 보기는 `DepthCameraEngine → ImageReader → DepthFrame → De 같은 Depth 메뉴의 Portrait는 `PortraitCameraEngine → CameraExtensionSession → JPEG ImageReader → MediaLibrary`로 연결합니다. 제조사 Bokeh가 적용된 컬러 사진을 저장하며 별도의 센서 Depth 맵을 받지 않습니다. 확장 처리 이벤트는 일반 Camera2 프레임 지표와 구분하고, 사진 픽셀은 측정 파일에 넣지 않습니다. +App Bokeh는 일반 Camera2의 Preview+JPEG로 촬영한 원본을 저장한 뒤 `MainAppBokehBridge → AppBokehEditor → AppBokehProcessor`로 전달합니다. JPEG를 RGB로 디코딩하고 MiDaS 모델이 256×256 Float 상대 깊이를 추론합니다. 사용자가 선택한 깊이보다 먼 영역을 앱에서 흐린 뒤 보케 JPEG·깊이 PNG·처리 메타데이터를 별도 저장합니다. 원본은 그대로 유지하며 분석용 YUV·HAL Depth·제조사 보케 버퍼는 사용하지 않습니다. + 벤치마크는 별도 화면인 `BenchmarkActivity`가 준비를 마친 뒤 `BenchmarkRunner.start()`를 호출하여 시작합니다. `Telemetry.callback(...)`이 반환한 Camera2 콜백의 `onCaptureStarted`는 프레임워크 신호를 기록하며, Live 셔터와 벤치마크 시작을 연결하는 메서드가 아닙니다. @@ -474,9 +477,9 @@ CliLaunchActivity는 Dual·Multi에서 Live로 돌아갈 때 CliNavigationHost
        근거와 검토 정보 -- 근거 파일: `app/src/main/java/dev/halcamera/cli/CliProvider.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `tools/halcam/halcam/cli.py`, `tools/halcam/halcam/download.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/GalleryActivity.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunRetention.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/LaunchDiagnostics.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt` +- 근거 파일: `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt`, `app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt`, `app/src/main/java/dev/halcamera/cli/CliProvider.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `tools/halcam/halcam/cli.py`, `tools/halcam/halcam/download.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/GalleryActivity.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunRetention.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/LaunchDiagnostics.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review
        @@ -692,6 +695,8 @@ Depth 보기는 컬러와 Depth를 같은 요청에 포함하지 않는 별도 C 제조사 Portrait도 일반 촬영과 별도의 확장 세션을 사용합니다. Bokeh 확장 전용 해상도로 컬러 미리보기와 JPEG만 요청하며, 세션이 거절되면 다른 크기로 자동 대체하지 않습니다. 단일 촬영의 JPEG를 받기 전에 실패하거나 종료되면 세션을 해제하여 늦은 이미지를 다음 촬영에 연결하지 않습니다. 이미 받은 JPEG의 저장 작업은 종료 후에도 마칠 수 있습니다. +App Bokeh는 제조사 Portrait와 달리 일반 JPEG 저장 이후의 앱 처리입니다. 상대 깊이는 센서 거리나 DEPTH16으로 해석하지 않으며, 편집·저장 해상도는 긴 변 최대 1600픽셀로 제한합니다. 원본은 수정하지 않습니다. 추론·렌더·추가 저장 실패는 원본을 삭제하지 않으며, Activity 중단 후에는 편집기의 늦은 결과를 화면에 적용하지 않습니다. + Multi 모드의 `MultiSession`은 `CameraEngine`과 별도입니다. Camera2는 선택한 독립 장치를 연 뒤 세션을 구성하며, CameraX는 광고된 동시 조합을 바인딩합니다. 모든 장치의 종료 통지를 확인해야 출력을 해제하고 다음 동시 세션을 열 수 있습니다. 한 명령의 여러 사진을 센서 동기 촬영으로 해석하지 않습니다. @@ -749,9 +754,9 @@ Live 카메라 종료 후 벤치마크 화면으로 이동하고 기존 측정·
        근거와 검토 정보 -- 근거 파일: `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/MicrophoneCapture.kt`, `app/src/main/java/dev/halcamera/camera/TimestampedVideoRecording.kt`, `app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt`, `app/src/main/java/dev/halcamera/cli/CliProvider.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `app/src/main/java/dev/halcamera/cli/CommandStore.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraEndpointResolver.kt`, `app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionRules.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkStore.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkIndex.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/AtomicFiles.kt` +- 근거 파일: `app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt`, `app/src/main/java/dev/halcamera/camera/RelativeDepth.kt`, `app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt`, `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/MicrophoneCapture.kt`, `app/src/main/java/dev/halcamera/camera/TimestampedVideoRecording.kt`, `app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt`, `app/src/main/java/dev/halcamera/cli/CliProvider.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `app/src/main/java/dev/halcamera/cli/CommandStore.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraEndpointResolver.kt`, `app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionRules.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkStore.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkIndex.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/AtomicFiles.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review +- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review
        @@ -803,16 +808,16 @@ Android 의존성이 없는 러너와 평가 로직은 JVM 단위 테스트로 | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `data-flow` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | +| 구조 원본 `data-flow` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | | 구조 원본 `ui-camera-label` | 최신 | 검토 2026-10-10 @ `93385443` · Codex (mode reset, labels and numeric ID selection review) | -| 구조 원본 `ui-tool-handoff` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 구조 원본 `ui-zoom` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `overview` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `module-roles` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `runtime-flow` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | -| 원고 `constraints` | 최신 | 검토 2026-10-19 @ `2fb9f2aa` · codex-user-authorized-portrait-review | +| 구조 원본 `ui-tool-handoff` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 구조 원본 `ui-zoom` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `overview` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `module-roles` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `runtime-flow` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `constraints` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | diff --git a/tools/docgen/state/evidence.json b/tools/docgen/state/evidence.json index 8833354e..48ccc0e8 100644 --- a/tools/docgen/state/evidence.json +++ b/tools/docgen/state/evidence.json @@ -3,16 +3,16 @@ "entries": { "omm:overall-architecture": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "057f692f5c7ba25d", - "modelHash": "a911638bf6e3cbc2", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "256cd50e4dae5e7f", + "modelHash": "cce80d4534048485", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -20,10 +20,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -64,6 +65,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -162,6 +164,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -245,6 +248,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -293,17 +297,18 @@ }, "omm:data-flow": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "b415ef0c3c1996d6", - "modelHash": "7c9aaf4a651758f8", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "abdeffda7f395e31", + "modelHash": "a3e8b80c9d1ac34b", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", "app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt": "fc28cf52692a271225524bfa045dc2e232d0ba45", "app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt": "3ce60c16472f2ed3e100034c62feee32f13236d3", @@ -319,12 +324,14 @@ "app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt": "df32b7ff015c81cb44407884ea02deda965752df", "app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt": "cb8a23b1587af5faf4409d01b075f2a615987c62", "app/src/main/java/dev/halcamera/benchmark/platform/DeviceInstance.kt": "ba39a6256eeb87193f9a6560762dee274c4f776f", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/Camera2Engine.kt": "a07b50648bd1812a3ae55e1b31494e963feb060c", "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", "app/src/main/java/dev/halcamera/camera/MediaLibrary.kt": "2fbb56c9e81761cea93f64109ec12497309cffe8", "app/src/main/java/dev/halcamera/camera/MediaTransaction.kt": "34f0121f9c418c8e49fb0326a99882ae6f447c94", "app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt": "c8ca57c719ca0df2bc002493a1568c3de1196390", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/cli/BenchmarkController.kt": "1789489c62871f8bb25a4274e76c58c80414182f", "app/src/main/java/dev/halcamera/cli/CliArtifactRegistry.kt": "e01a37879d2b3638a95bcb2a54adf75ab4643ac8", "app/src/main/java/dev/halcamera/cli/CliLaunchActivity.kt": "117b88d8cb5458aaa05dabe4153e74c5dc832e45", @@ -344,17 +351,18 @@ }, "omm:state-transitions": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "f98fe1d594f993e7", - "modelHash": "d4ee6d79890b29f2", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "6c26499ca15e0c52", + "modelHash": "29c82f37e7694476", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", "app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt": "fc28cf52692a271225524bfa045dc2e232d0ba45", @@ -384,6 +392,7 @@ "app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt": "2dc23b55b00407ea5126dd19e3871fcb8f9fc5d4", "app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt": "f5148663814d0aec3ed45a44478bae4b0323e01d", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "tools/halcam/halcam/cli.py": "4161d167e0c041fdca0c185505083b57907bc789", "tools/halcam/halcam/submission.py": "34a8444dcbaec0b783fc71ac1cffb844d9fc2147" } @@ -391,16 +400,16 @@ }, "content:architecture.md/overview": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "37a6ba8e14f48c93", - "modelHash": "c04a5640113b0575", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "7b3a5428bb6445ea", + "modelHash": "c6d557a56b5f62f7", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -408,10 +417,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -452,6 +462,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -550,6 +561,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -633,6 +645,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -684,17 +697,18 @@ }, "content:architecture.md/runtime-flow": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "56197667ece1fbbb", - "modelHash": "4cdc084d8ed49bb6", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "48aa19e183edff3d", + "modelHash": "3291cd6bfd0a9310", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", "app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt": "fc28cf52692a271225524bfa045dc2e232d0ba45", "app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt": "3ce60c16472f2ed3e100034c62feee32f13236d3", @@ -711,6 +725,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt": "cb8a23b1587af5faf4409d01b075f2a615987c62", "app/src/main/java/dev/halcamera/benchmark/platform/DeviceInstance.kt": "ba39a6256eeb87193f9a6560762dee274c4f776f", "app/src/main/java/dev/halcamera/benchmark/platform/LaunchDiagnostics.kt": "c406e4887c0617d8b57eb4b3c91263f427953372", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/Camera2Engine.kt": "a07b50648bd1812a3ae55e1b31494e963feb060c", "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -718,6 +733,7 @@ "app/src/main/java/dev/halcamera/camera/MediaTransaction.kt": "34f0121f9c418c8e49fb0326a99882ae6f447c94", "app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt": "487943447e62e1f101687a0ebc3967abb78a54a3", "app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt": "c8ca57c719ca0df2bc002493a1568c3de1196390", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/cli/BenchmarkController.kt": "1789489c62871f8bb25a4274e76c58c80414182f", "app/src/main/java/dev/halcamera/cli/CliArtifactRegistry.kt": "e01a37879d2b3638a95bcb2a54adf75ab4643ac8", "app/src/main/java/dev/halcamera/cli/CliLaunchActivity.kt": "117b88d8cb5458aaa05dabe4153e74c5dc832e45", @@ -729,6 +745,7 @@ "app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt": "2dc23b55b00407ea5126dd19e3871fcb8f9fc5d4", "app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt": "f5148663814d0aec3ed45a44478bae4b0323e01d", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "tools/halcam/halcam/cli.py": "4161d167e0c041fdca0c185505083b57907bc789", "tools/halcam/halcam/download.py": "6cb9875bdae0257330e0942386f7ecd64a34d690", "tools/halcam/halcam/submission.py": "34a8444dcbaec0b783fc71ac1cffb844d9fc2147" @@ -737,13 +754,13 @@ }, "content:architecture.md/module-roles": { "accepted": { - "codeHash": "057f692f5c7ba25d", - "modelHash": "9f3d1f8b2ae8dba5", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "256cd50e4dae5e7f", + "modelHash": "357181d41ec7931b", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -751,10 +768,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -795,6 +813,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -893,6 +912,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -976,6 +996,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -1022,18 +1043,18 @@ } }, "observed": { - "state": "stale" + "state": "fresh" } }, "content:architecture.md/constraints": { "accepted": { - "codeHash": "057f692f5c7ba25d", - "modelHash": "bdd9eb84b2f3c403", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "256cd50e4dae5e7f", + "modelHash": "2b7fa475e3b6d941", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -1041,10 +1062,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -1085,6 +1107,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -1183,6 +1206,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -1266,6 +1290,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -1312,7 +1337,7 @@ } }, "observed": { - "state": "stale" + "state": "fresh" } }, "omm:ui-camera-label": { @@ -1333,33 +1358,33 @@ }, "omm:ui-tool-handoff": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "af533493626cd705", - "modelHash": "a27ff87e34cf0c58", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "7ddbb49d92345b28", + "modelHash": "b733a71299fad849", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2" + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c" } } }, "omm:ui-zoom": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "9142002fc98a5ef3", + "codeHash": "c3fde4d1df61aded", "modelHash": "b0bccaebbd551910", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", "app/src/main/java/dev/halcamera/ui/ExpandingZoomControl.kt": "e93a1cc7727b9200fa6aaf31d8210050a0b6acf9", "app/src/main/java/dev/halcamera/ui/FocusRing.kt": "e129ce223edde62c329a15b5662fa34f9f6a6918" } @@ -1367,16 +1392,16 @@ }, "content:_reference/engine.md/contract": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "057f692f5c7ba25d", - "modelHash": "f6d90c9bafc4d997", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "256cd50e4dae5e7f", + "modelHash": "66c6d69f9fc52416", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -1384,10 +1409,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -1428,6 +1454,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -1526,6 +1553,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -1609,6 +1637,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -1657,16 +1686,16 @@ }, "content:_reference/engine.md/camera2": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "057f692f5c7ba25d", - "modelHash": "75f06da9844aed35", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "256cd50e4dae5e7f", + "modelHash": "7af1d65d67f75fb3", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -1674,10 +1703,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -1718,6 +1748,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -1816,6 +1847,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -1899,6 +1931,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -1947,16 +1980,16 @@ }, "content:_reference/engine.md/camerax": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "057f692f5c7ba25d", - "modelHash": "ccd050f4375de1c0", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "256cd50e4dae5e7f", + "modelHash": "da611927ad8a984f", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -1964,10 +1997,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -2008,6 +2042,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -2106,6 +2141,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -2189,6 +2225,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -2237,16 +2274,16 @@ }, "content:_reference/engine.md/comparison": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "057f692f5c7ba25d", - "modelHash": "7b83fe41e96866ee", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "256cd50e4dae5e7f", + "modelHash": "c6e5f3e19d40ec82", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -2254,10 +2291,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -2298,6 +2336,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -2396,6 +2435,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -2479,6 +2519,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -2527,18 +2568,19 @@ }, "content:_reference/troubleshooting.md/layer-isolation": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "c7377f64a346f1ea", - "modelHash": "cbaa131e4b682933", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "bce4991cbed5d674", + "modelHash": "1c146246635748ea", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt": "2c93630441feea87cc093dd8aed8384cf45956f1", "app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt": "fc28cf52692a271225524bfa045dc2e232d0ba45", @@ -2555,6 +2597,7 @@ "app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt": "df32b7ff015c81cb44407884ea02deda965752df", "app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt": "cb8a23b1587af5faf4409d01b075f2a615987c62", "app/src/main/java/dev/halcamera/benchmark/platform/DeviceInstance.kt": "ba39a6256eeb87193f9a6560762dee274c4f776f", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/Camera2Engine.kt": "a07b50648bd1812a3ae55e1b31494e963feb060c", "app/src/main/java/dev/halcamera/camera/CameraEngine.kt": "7441e69d8ef8394db42448a69abd9fa688167c70", "app/src/main/java/dev/halcamera/camera/CameraXEngine.kt": "c885e0bb38e481d45fda923dbea1d03e60a1a2b7", @@ -2564,6 +2607,7 @@ "app/src/main/java/dev/halcamera/camera/MediaTransaction.kt": "34f0121f9c418c8e49fb0326a99882ae6f447c94", "app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt": "487943447e62e1f101687a0ebc3967abb78a54a3", "app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt": "c8ca57c719ca0df2bc002493a1568c3de1196390", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/cli/BenchmarkController.kt": "1789489c62871f8bb25a4274e76c58c80414182f", "app/src/main/java/dev/halcamera/cli/CliArtifactRegistry.kt": "e01a37879d2b3638a95bcb2a54adf75ab4643ac8", "app/src/main/java/dev/halcamera/cli/CliCommand.kt": "590a408fec3e4c8fb08a9b0a3ca50efb2b10e609", @@ -2584,6 +2628,7 @@ "app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt": "2dc23b55b00407ea5126dd19e3871fcb8f9fc5d4", "app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt": "f5148663814d0aec3ed45a44478bae4b0323e01d", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "tools/halcam/halcam/cli.py": "4161d167e0c041fdca0c185505083b57907bc789", "tools/halcam/halcam/download.py": "6cb9875bdae0257330e0942386f7ecd64a34d690", "tools/halcam/halcam/submission.py": "34a8444dcbaec0b783fc71ac1cffb844d9fc2147" @@ -2592,16 +2637,16 @@ }, "content:_reference/evidence.md/device-notes": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "057f692f5c7ba25d", - "modelHash": "5ce9361a6565a237", - "at": "2026-10-19", - "commit": "2fb9f2aa", - "reviewer": "codex-user-authorized-portrait-review", + "codeHash": "256cd50e4dae5e7f", + "modelHash": "20cdcdfca8bd78ef", + "at": "2026-10-24", + "commit": "e4de2762", + "reviewer": "codex-app-bokeh-code-review", "files": { - "app/build.gradle.kts": "d66d0773b7c3040674518d8a9db04d40fd67ee57", + "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", "app/src/main/java/dev/halcamera/CameraProbeActivity.kt": "e73e84d36c6184544b8ef50d3cd2fec6f6af972e", "app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt": "b49983bd24b055ea611104ef9571ef6cb6e68049", @@ -2609,10 +2654,11 @@ "app/src/main/java/dev/halcamera/DualPreviewActivity.kt": "93bf6e4bb3e10601631adcf12824164a06dc990e", "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/LiveStreamsActivity.kt": "71377b456162e55482645967b3d5b22260d91223", - "app/src/main/java/dev/halcamera/MainActivity.kt": "4e4d7b9b7383b8b866db5942ff1e9de6b5dd0f8e", - "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "875268d3943a2ede5659580905ba787a934fb1dc", - "app/src/main/java/dev/halcamera/MainCliBridge.kt": "f50d07599ff67f5718f285d7903197516ab0e5a2", - "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "1ce3bb1f2264e8a7d8e920c9aea6b6f67db14825", + "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", + "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", + "app/src/main/java/dev/halcamera/MainCameraPicker.kt": "1ea55a5d91811b785338ba1b2eee6d8eff988594", + "app/src/main/java/dev/halcamera/MainCliBridge.kt": "af771f768c23b4a5d4f066b9660ed2b36ff1323c", + "app/src/main/java/dev/halcamera/MainDepthBridge.kt": "367e293bc496f724205bb28eab2fab25e6035464", "app/src/main/java/dev/halcamera/MainPortraitBridge.kt": "d5a840effb2748fd3c1f027396eed92078886159", "app/src/main/java/dev/halcamera/MultiCliBridge.kt": "52a5825ca1ec9d495a6ab76f5738bb5e1d7a1004", "app/src/main/java/dev/halcamera/WorkbenchActivity.kt": "f9c6c4c5e045c737e5375f8c3c2ec283b6381891", @@ -2653,6 +2699,7 @@ "app/src/main/java/dev/halcamera/benchmark/platform/StreamSpecs.kt": "a3dc42ba6008f47189702c40a9ee45186f6daafb", "app/src/main/java/dev/halcamera/benchmark/platform/SubjectPrefs.kt": "1e9d5597619ea4b4e107cd08702c4ccba2cb41b7", "app/src/main/java/dev/halcamera/benchmark/platform/ThermalTracker.kt": "5572c02cd6ed7ad1f8fcd9285c02b03cb3f60ee1", + "app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt": "ff7b9e4699d391cabb01c714dd83b56ab2359681", "app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt": "f15c520fa871ff94c5c1069ad962d36cf485b85b", "app/src/main/java/dev/halcamera/camera/BracketCapture.kt": "ed6366635f5e466394c416a2b9efb1cff767dd4d", "app/src/main/java/dev/halcamera/camera/BracketFiles.kt": "36c1b51465b9e503d6e4632fad6119079f55f687", @@ -2751,6 +2798,7 @@ "app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt": "d40ba1bb593c5f55c4f12ebf963f0a2b259de1b0", "app/src/main/java/dev/halcamera/camera/RecordingTimebase.kt": "9c2c60e3fa71dea4deeb8cdc973f443e8c329300", "app/src/main/java/dev/halcamera/camera/RecordingTimeline.kt": "5f18000163c7e4568b3e4ba1581a3a1856d0597c", + "app/src/main/java/dev/halcamera/camera/RelativeDepth.kt": "65570fd109f49490b091cb8e9d2b2ded990a0c39", "app/src/main/java/dev/halcamera/camera/RequestStreamRegistry.kt": "5673124cf2f38d9ff29bcb6e3907be44ae82e3c6", "app/src/main/java/dev/halcamera/camera/SampleClock.kt": "64397572af6bc204dce7c802c4077cbc4769a334", "app/src/main/java/dev/halcamera/camera/SnapshotRequest.kt": "86ea336a62641229a89f1e05963a2e42e63db030", @@ -2834,6 +2882,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", From 9e71f282664d7d39bd69c3f2cb68838bd2a1db57 Mon Sep 17 00:00:00 2001 From: TTolsun <112919054+TTolsun@users.noreply.github.com> Date: Sat, 24 Oct 2026 14:41:17 +0900 Subject: [PATCH 3/4] Align Bokeh editor controls with Live and Gallery --- .../camera-engines/app-bokeh/description.md | 2 + .../dev/halcamera/camera/AppBokehChecks.kt | 52 ++++++++++++++++++ .../halcamera/cli/CliStoreInstrumentation.kt | 5 +- .../java/dev/halcamera/ui/AppBokehEditor.kt | 55 +++++++++++++++---- docs/validation/app-bokeh.md | 4 ++ guide/live.md | 4 +- tools/docgen/state/evidence.json | 24 ++++---- 7 files changed, 119 insertions(+), 27 deletions(-) diff --git a/.omm/overall-architecture/camera-engines/app-bokeh/description.md b/.omm/overall-architecture/camera-engines/app-bokeh/description.md index 74b831c5..8f352c62 100644 --- a/.omm/overall-architecture/camera-engines/app-bokeh/description.md +++ b/.omm/overall-architecture/camera-engines/app-bokeh/description.md @@ -3,3 +3,5 @@ App Bokeh는 일반 Camera2의 Preview와 JPEG 두 출력을 사용한다. MainA AppBokehProcessor는 원본을 EXIF 방향에 맞추고 긴 변을 최대 1600픽셀로 제한한다. MiDaS v2.1 모델은 RGB 사진 한 장에서 256×256 Float 상대 역깊이를 추론한다. RelativeDepth와 DepthBokeh는 Android 의존성 없이 깊이 정규화·선택 위치 표본·배경 혼합과 blur를 계산한다. 결과는 JPEG로 재인코딩하며 별도 깊이 PNG와 모델·원본·선택 위치·강도 메타데이터를 저장한다. 원본은 수정하지 않는다. 편집기의 worker가 추론·렌더·저장을 처리한다. 변경 세대가 지난 렌더는 표시하지 않고 화면 종료 후에는 UI를 갱신하지 않는다. 화면 복귀는 보관한 원본 URI로 편집을 다시 시작하며 이전 강도·선택 위치는 초기화한다. 상대 깊이는 실제 거리나 센서 DEPTH16이 아니다. Benchmark와 incident ZIP에는 이미지 픽셀을 넣지 않는다. + +편집기는 Live 선택창과 같은 제목·닫기 아이콘, 흰색 선택 테두리와 접근성 선택 상태를 사용한다. 이미지와 저장 버튼은 Gallery의 Look 토큰을 재사용한다. 강도 조절은 Bokeh 결과로 돌아가며 Save Bokeh는 비교 화면과 관계없이 보케 복사본을 저장한다. 슬라이더는 최소 48dp 높이를 제공한다. diff --git a/app/src/androidTest/java/dev/halcamera/camera/AppBokehChecks.kt b/app/src/androidTest/java/dev/halcamera/camera/AppBokehChecks.kt index a7dc50a6..473ea2bb 100644 --- a/app/src/androidTest/java/dev/halcamera/camera/AppBokehChecks.kt +++ b/app/src/androidTest/java/dev/halcamera/camera/AppBokehChecks.kt @@ -7,6 +7,58 @@ import android.net.Uri import java.io.File object AppBokehChecks { + /** Reuse a local photo so editor checks never need another live capture. */ + fun ui(test: android.app.Instrumentation, path: String): String { + fun main(block: () -> T): T { + val result = java.util.concurrent.atomic.AtomicReference>() + test.runOnMainSync { result.set(runCatching(block)) } + return result.get().getOrThrow() + } + fun nodes(view: android.view.View): Sequence = sequence { + yield(view) + if (view is android.view.ViewGroup) for (i in 0 until view.childCount) yieldAll(nodes(view.getChildAt(i))) + } + fun root() = android.view.inspector.WindowInspector.getGlobalWindowViews().last { it.isShown } + fun button(label: String) = nodes(root()).filterIsInstance().first { it.text.toString() == label } + val activity = test.startActivitySync(android.content.Intent(test.targetContext, dev.halcamera.MainActivity::class.java) + .addFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK)) + val editor = main { dev.halcamera.ui.AppBokehEditor(activity, Uri.fromFile(File(path))) {}.also { it.show() } } + try { + val deadline = android.os.SystemClock.uptimeMillis() + 25_000 + while (!main { button("Save Bokeh").isEnabled }) { + check(android.os.SystemClock.uptimeMillis() < deadline) + android.os.SystemClock.sleep(100) + } + main { + check(button("Bokeh").isSelected) + button("Original").performClick() + check(button("Original").isSelected && !button("Bokeh").isSelected) + button("Depth").performClick() + check(button("Depth").isSelected) + val slider = nodes(root()).filterIsInstance().first() + check(slider.height >= (48 * activity.resources.displayMetrics.density).toInt()) + check(slider.performAccessibilityAction(android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS.id, + android.os.Bundle().apply { putFloat(android.view.accessibility.AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE, 50f) })) + check(button("Bokeh").isSelected && !button("Depth").isSelected) + } + while (!main { button("Save Bokeh").isEnabled }) { + check(android.os.SystemClock.uptimeMillis() < deadline) + android.os.SystemClock.sleep(100) + } + main { + val view = root() + val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) + view.draw(android.graphics.Canvas(bitmap)) + File(test.targetContext.getExternalFilesDir(null), "app-bokeh-ux.png").outputStream().use { + check(bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)) + } + bitmap.recycle() + check(nodes(view).first { it.contentDescription == "Close App Bokeh" }.performClick()) + } + return "APP_BOKEH_UI_OK selected comparison, accessible strength, result after adjustment, shared close control" + } finally { main { editor.dismiss(); activity.finish() } } + } + fun run(context: Context, path: String, focusX: Float = .5f, focusY: Float = .5f): String { val processor = AppBokehProcessor(context) val source = Uri.fromFile(File(path)) diff --git a/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt b/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt index 1f6070d8..c710f42a 100644 --- a/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt +++ b/app/src/androidTest/java/dev/halcamera/cli/CliStoreInstrumentation.kt @@ -10,6 +10,7 @@ import java.util.UUID /** Device tests use real JSONObject and AtomicFile, with a deterministic clock and isolated files. */ class CliStoreInstrumentation : Instrumentation() { private var appBokehSource: String? = null + private var appBokehUi = false private var appBokehFocusX = .5f private var appBokehFocusY = .5f private var exportReports = false @@ -28,6 +29,7 @@ class CliStoreInstrumentation : Instrumentation() { override fun onCreate(arguments: Bundle?) { super.onCreate(arguments) appBokehSource = arguments?.getString("app_bokeh_source") + appBokehUi = arguments?.getString("app_bokeh_ui") == "true" appBokehFocusX = arguments?.getString("app_bokeh_focus_x")?.toFloat() ?: .5f appBokehFocusY = arguments?.getString("app_bokeh_focus_y")?.toFloat() ?: .5f exportReports = arguments?.getString("export_reports") == "true" @@ -49,7 +51,8 @@ class CliStoreInstrumentation : Instrumentation() { override fun onStart() { appBokehSource?.let { path -> try { - val result = dev.halcamera.camera.AppBokehChecks.run(targetContext, path, appBokehFocusX, appBokehFocusY) + val result = if (appBokehUi) dev.halcamera.camera.AppBokehChecks.ui(this, path) + else dev.halcamera.camera.AppBokehChecks.run(targetContext, path, appBokehFocusX, appBokehFocusY) finish(Activity.RESULT_OK, Bundle().apply { putString("stream", "$result\n") }) } catch (error: Throwable) { finish(Activity.RESULT_CANCELED, Bundle().apply { putString("stream", "APP_BOKEH_FAILED: ${error.stackTraceToString()}\n") }) diff --git a/app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt b/app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt index d06a67f2..3f8dc072 100644 --- a/app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt +++ b/app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt @@ -2,10 +2,13 @@ package dev.halcamera.ui import android.app.Activity import android.app.Dialog +import android.content.res.ColorStateList import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Paint import android.graphics.RectF +import android.graphics.Typeface +import android.graphics.drawable.RippleDrawable import android.net.Uri import android.os.Handler import android.os.Looper @@ -13,7 +16,10 @@ import android.view.MotionEvent import android.view.View import android.view.WindowManager import android.widget.LinearLayout +import android.widget.Button import android.widget.SeekBar +import androidx.core.view.ViewCompat +import dev.halcamera.R import dev.halcamera.camera.AppBokehProcessor import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicInteger @@ -34,42 +40,57 @@ class AppBokehEditor(private val activity: Activity, private val source: Uri, pr private var strength = 65 private var display = 0 private val dialog = Dialog(activity) - private val info = Look.text(activity, "Estimating depth on this device…", 13, Look.onDarkMuted) + private val info = Look.text(activity, "Estimating depth on this device…", 13, Look.onDarkMuted).apply { minLines = 2 } private val picture = Picture(activity) { x, y -> focusX = x; focusY = y; render() } - private val save = Look.galleryButton(activity, "Save copy", true) { saveCopy() } - private val slider = SeekBar(activity).apply { max = 100; progress = strength; contentDescription = "Background blur strength" } + private val save = Look.galleryButton(activity, "Save Bokeh", true) { saveCopy() } + private val close = IconButton(activity, R.drawable.ic_action_close, "Close App Bokeh", dark = true) { if (!saving) dismiss() } + private val viewButtons = mutableListOf

      App Bokeh는 RGB 사진 한 장으로 상대적인 깊이를 추정합니다. MiDaS v2.1 모바일 모델을 LiteRT CPU에서 실행하고 앱이 배경 흐림과 JPEG 인코딩을 수행합니다. 모델은 앱에 포함되며 사진을 서버로 보내지 않습니다. 깊이 맵의 흰색은 가까운 영역, 검은색은 먼 영역을 뜻하고 미터 단위 거리나 센서 DEPTH16이 아닙니다.

      편집·저장 결과는 메모리 사용을 제한하기 위해 긴 변이 최대 1600픽셀입니다. 원본 해상도는 별도로 유지하며, 깊이 추론 해상도는 256×256입니다. 얇은 물체, 반사·투명 물체, 머리카락 경계나 무늬가 적은 장면에서는 깊이와 경계가 부정확할 수 있습니다. 깊이를 구분하지 못하면 실패 이유를 표시하고 원본을 유지합니다. 선택한 지점과 같은 깊이 또는 더 가까운 물체는 선명하게 남습니다.

      diff --git a/guide/_reference/engine.md b/guide/_reference/engine.md index eece2191..075beabc 100644 --- a/guide/_reference/engine.md +++ b/guide/_reference/engine.md @@ -62,7 +62,7 @@ PIP와 Multi는 Android 11 이상에서 제공합니다. 저장물과 설정 방 - 근거 파일: `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2StillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2LiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeterRequests.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXPipSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/LivePipSession.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -225,7 +225,7 @@ Live 제어와 터치 측광은 다음 이벤트를 추가로 남깁니다. - 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/DepthCameraEngine.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/ui/LiveControlBar.kt`, `app/src/main/java/dev/halcamera/ui/FocusRing.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/StartCardPresenter.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -613,7 +613,7 @@ Camera2에서 짧게 터치한 경우입니다. 긴 누르기는 별도의 AE - 근거 파일: `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/LivePipSession.kt`, `app/src/main/java/dev/halcamera/camera/PipSessionQuery.kt`, `app/src/main/java/dev/halcamera/camera/CameraOpenRetry.kt`, `app/src/main/java/dev/halcamera/camera/CameraReleaseWait.kt`, `app/src/main/java/dev/halcamera/camera/LiveStreamSettings.kt`, `app/src/main/java/dev/halcamera/camera/LiveStabilization.kt`, `app/src/main/java/dev/halcamera/camera/LiveStreamCapabilities.kt`, `app/src/main/java/dev/halcamera/camera/LiveSessionCheck.kt`, `app/src/main/java/dev/halcamera/camera/Camera2StillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2LiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/Camera2VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt`, `app/src/main/java/dev/halcamera/camera/PreviewBufferRelay.kt`, `app/src/main/java/dev/halcamera/camera/RecordingBufferRelay.kt`, `app/src/main/java/dev/halcamera/camera/StreamConfiguration.kt`, `app/src/main/java/dev/halcamera/camera/StillPair.kt`, `app/src/main/java/dev/halcamera/camera/YuvPacking.kt`, `app/src/main/java/dev/halcamera/camera/RawFrame.kt`, `app/src/main/java/dev/halcamera/camera/DngOutput.kt`, `app/src/main/java/dev/halcamera/camera/StillEncoding.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/camera/LiveControlRequests.kt`, `app/src/main/java/dev/halcamera/camera/ManualControls.kt`, `app/src/main/java/dev/halcamera/camera/ManualControlRequests.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeter.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeterRequests.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -832,7 +832,7 @@ AE 재잠금은 Camera2와 같은 [AeRelock 상태도](#노출은-언제-다시- - 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraXEngine.kt`, `app/src/main/java/dev/halcamera/camera/CameraXPipSession.kt`, `app/src/main/java/dev/halcamera/camera/PipSessionQuery.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt`, `app/src/main/java/dev/halcamera/camera/CameraXVideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/VideoSnapshot.kt`, `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/camera/StillEncoding.kt`, `app/src/main/java/dev/halcamera/camera/YuvPacking.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/StreamConfiguration.kt`, `app/src/main/java/dev/halcamera/camera/LiveControls.kt`, `app/src/main/java/dev/halcamera/camera/TouchMeter.kt`, `app/build.gradle.kts` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -852,11 +852,11 @@ AE 재잠금은 Camera2와 같은 [AeRelock 상태도](#노출은-언제-다시- | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `contract` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `camera2` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `camerax` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `comparison` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `contract` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `camera2` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `camerax` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `comparison` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | diff --git a/guide/_reference/evidence.md b/guide/_reference/evidence.md index 284161ee..133c3d10 100644 --- a/guide/_reference/evidence.md +++ b/guide/_reference/evidence.md @@ -80,7 +80,7 @@ Galaxy S25+·Android 16에서 2026년 9월 17~19일, versionCode 106~108로 수 - 근거 파일: `app/src/main/java/dev/halcamera/camera/CameraXControls.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/CameraXLiveRecorder.kt` - 기기 검증: `V-002` - 근거 수준: 기기 검증 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -98,8 +98,8 @@ Galaxy S25+·Android 16에서 2026년 9월 17~19일, versionCode 106~108로 수 | 항목 | 최신성 | 검토 | | --- | --- | --- | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `device-notes` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `device-notes` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | diff --git a/guide/_reference/troubleshooting.md b/guide/_reference/troubleshooting.md index 15922219..4169b40d 100644 --- a/guide/_reference/troubleshooting.md +++ b/guide/_reference/troubleshooting.md @@ -140,7 +140,7 @@ PC의 대기 시간이 끝나도 앱 작업은 계속될 수 있습니다. 요 - 근거 파일: `app/src/main/java/dev/halcamera/cli/CommandStore.kt`, `tools/halcam/halcam/cli.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraXStillCapture.kt`, `app/src/main/java/dev/halcamera/camera/Camera2Engine.kt`, `app/src/main/java/dev/halcamera/camera/ManualControlRequests.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -173,8 +173,8 @@ PC의 대기 시간이 끝나도 앱 작업은 계속될 수 있습니다. 요 | 항목 | 최신성 | 검토 | | --- | --- | --- | | 구조 원본 `data-flow` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `layer-isolation` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `layer-isolation` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | diff --git a/guide/architecture.md b/guide/architecture.md index 29942fbb..e19dc34c 100644 --- a/guide/architecture.md +++ b/guide/architecture.md @@ -57,7 +57,7 @@ PC에서 같은 기능을 실행하려면 [CLI](cli.md)를 사용합니다. `Cli - 근거 파일: `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `tools/halcam/halcam/cli.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt`, `app/src/main/java/dev/halcamera/WorkbenchActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -254,7 +254,7 @@ Camera2의 RAW (DNG)는 `RawFrame`이 RAW 샘플을 복사하고 `DngOutput`이 - 근거 파일: `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt`, `app/src/main/java/dev/halcamera/camera/RelativeDepth.kt`, `app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt`, `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/SplitDragGesture.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `app/src/main/java/dev/halcamera/cli/LiveController.kt`, `app/src/main/java/dev/halcamera/cli/BenchmarkController.kt`, `app/src/main/java/dev/halcamera/cli/CliLibrary.kt`, `app/src/main/java/dev/halcamera/MainCliBridge.kt`, `app/src/main/java/dev/halcamera/DualCliBridge.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/BenchmarkRecorder.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/EnvironmentProbe.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/HistoryActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunIndex.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkCsv.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/ConcurrentCameraActivity.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt`, `app/src/main/java/dev/halcamera/ui/RecentMediaButton.kt`, `app/src/main/java/dev/halcamera/ui/Look.kt`, `app/src/main/java/dev/halcamera/cts/recording/BasicRecordingRules.kt`, `app/src/main/java/dev/halcamera/cts/CtsEntryActivity.kt`, `app/src/main/java/dev/halcamera/cts/vendored/VendoredCaseActivity.kt`, `app/src/main/java/dev/halcamera/cts/vendored/VendoredCtsListActivity.kt`, `app/src/main/java/dev/halcamera/cts/CtsCaseActivity.kt`, `app/src/main/java/dev/halcamera/cts/CtsCaseListActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/CtsChecklistActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/CtsSuiteRunActivity.kt`, `app/src/main/java/dev/halcamera/cts/suite/SuitePlan.kt`, `app/src/main/java/dev/halcamera/cts/suite/SuiteReport.kt`, `app/src/main/java/dev/halcamera/cts/CtsCatalog.kt`, `app/src/main/java/dev/halcamera/cts/CtsRunner.kt`, `app/src/main/java/dev/halcamera/cts/CameraCaseRunner.kt`, `app/src/main/java/dev/halcamera/cts/Camera2Ops.kt`, `app/src/main/java/dev/halcamera/cts/onoff/FastOnOffRules.kt`, `app/src/main/java/dev/halcamera/cts/switching/SwitchingRules.kt`, `app/src/main/java/dev/halcamera/cts/sizes/AllSizeOnOffRules.kt`, `app/src/main/java/dev/halcamera/cts/combination/StillPreviewCombinationRules.kt`, `app/src/main/java/dev/halcamera/cts/snapshot/VideoSnapshotRules.kt`, `app/src/main/java/dev/halcamera/CameraProbeActivity.kt`, `app/src/main/java/dev/halcamera/camera/CameraProbe.kt`, `app/src/main/java/dev/halcamera/camera/CameraProbeReader.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -479,7 +479,7 @@ CliLaunchActivity는 Dual·Multi에서 Live로 돌아갈 때 CliNavigationHost - 근거 파일: `app/src/main/java/dev/halcamera/MainAppBokehBridge.kt`, `app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt`, `app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt`, `app/src/main/java/dev/halcamera/cli/CliProvider.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `tools/halcam/halcam/cli.py`, `tools/halcam/halcam/download.py`, `app/src/main/java/dev/halcamera/MainActivity.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/GalleryActivity.kt`, `app/src/main/java/dev/halcamera/camera/MediaLibrary.kt`, `app/src/main/java/dev/halcamera/camera/RecentMediaThumbnail.kt`, `app/src/main/java/dev/halcamera/telemetry/Telemetry.kt`, `app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt`, `app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunAssembler.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunRetention.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkEvaluator.kt`, `app/src/main/java/dev/halcamera/benchmark/BenchmarkActivity.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/LaunchDiagnostics.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionDetector.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -756,7 +756,7 @@ Live 카메라 종료 후 벤치마크 화면으로 이동하고 기존 측정· - 근거 파일: `app/src/main/java/dev/halcamera/camera/AppBokehProcessor.kt`, `app/src/main/java/dev/halcamera/camera/RelativeDepth.kt`, `app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt`, `app/src/main/java/dev/halcamera/camera/MultiSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraXMultiSession.kt`, `app/src/main/java/dev/halcamera/camera/MultiRecording.kt`, `app/src/main/java/dev/halcamera/camera/MicrophoneCapture.kt`, `app/src/main/java/dev/halcamera/camera/TimestampedVideoRecording.kt`, `app/src/main/java/dev/halcamera/camera/DeviceCompositor.kt`, `app/src/main/java/dev/halcamera/cli/CliProvider.kt`, `app/src/main/java/dev/halcamera/cli/CommandCoordinator.kt`, `app/src/main/java/dev/halcamera/cli/CommandStore.kt`, `app/src/main/java/dev/halcamera/camera/CameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/PortraitCameraEngine.kt`, `app/src/main/java/dev/halcamera/camera/ConcurrentSession.kt`, `app/src/main/java/dev/halcamera/camera/CameraEndpointResolver.kt`, `app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkRunner.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RunValidity.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/RegressionRules.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkReport.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkReportCodec.kt`, `app/src/main/java/dev/halcamera/benchmark/platform/BenchmarkStore.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/BenchmarkIndex.kt`, `app/src/main/java/dev/halcamera/benchmark/domain/AtomicFiles.kt` - 근거 수준: 코드 확인 -- 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review +- 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review @@ -809,15 +809,15 @@ Android 의존성이 없는 러너와 평가 로직은 JVM 단위 테스트로 | 항목 | 최신성 | 검토 | | --- | --- | --- | | 구조 원본 `data-flow` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 구조 원본 `overall-architecture` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 구조 원본 `state-transitions` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | | 구조 원본 `ui-camera-label` | 최신 | 검토 2026-10-10 @ `93385443` · Codex (mode reset, labels and numeric ID selection review) | | 구조 원본 `ui-tool-handoff` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | | 구조 원본 `ui-zoom` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `overview` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `module-roles` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `runtime-flow` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | -| 원고 `constraints` | 최신 | 검토 2026-10-24 @ `e4de2762` · codex-app-bokeh-code-review | +| 원고 `overview` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `module-roles` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `runtime-flow` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | +| 원고 `constraints` | 최신 | 검토 2026-10-24 @ `9e71f282` · codex-app-bokeh-ui-review | diff --git a/tools/docgen/state/evidence.json b/tools/docgen/state/evidence.json index 00ee165b..3213d160 100644 --- a/tools/docgen/state/evidence.json +++ b/tools/docgen/state/evidence.json @@ -3,14 +3,14 @@ "entries": { "omm:overall-architecture": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "256cd50e4dae5e7f", - "modelHash": "cce80d4534048485", + "codeHash": "e5526928cc6bb406", + "modelHash": "d7c4aa2c4c6325a1", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -248,7 +248,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -351,14 +351,14 @@ }, "omm:state-transitions": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "6c26499ca15e0c52", + "codeHash": "bf2492172cb06636", "modelHash": "29c82f37e7694476", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", "app/src/main/java/dev/halcamera/MainAppBokehBridge.kt": "964638cb0bef8555c7495f56beb992faf1223583", @@ -392,7 +392,7 @@ "app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt": "2dc23b55b00407ea5126dd19e3871fcb8f9fc5d4", "app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt": "f5148663814d0aec3ed45a44478bae4b0323e01d", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "tools/halcam/halcam/cli.py": "4161d167e0c041fdca0c185505083b57907bc789", "tools/halcam/halcam/submission.py": "34a8444dcbaec0b783fc71ac1cffb844d9fc2147" } @@ -400,14 +400,14 @@ }, "content:architecture.md/overview": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "7b3a5428bb6445ea", - "modelHash": "c6d557a56b5f62f7", + "codeHash": "4bd046519edd291b", + "modelHash": "61c84735e9b9b510", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -645,7 +645,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -697,14 +697,14 @@ }, "content:architecture.md/runtime-flow": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "48aa19e183edff3d", + "codeHash": "167e256bf008a70d", "modelHash": "3291cd6bfd0a9310", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", @@ -745,7 +745,7 @@ "app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt": "2dc23b55b00407ea5126dd19e3871fcb8f9fc5d4", "app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt": "f5148663814d0aec3ed45a44478bae4b0323e01d", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "tools/halcam/halcam/cli.py": "4161d167e0c041fdca0c185505083b57907bc789", "tools/halcam/halcam/download.py": "6cb9875bdae0257330e0942386f7ecd64a34d690", "tools/halcam/halcam/submission.py": "34a8444dcbaec0b783fc71ac1cffb844d9fc2147" @@ -754,11 +754,11 @@ }, "content:architecture.md/module-roles": { "accepted": { - "codeHash": "256cd50e4dae5e7f", - "modelHash": "357181d41ec7931b", + "codeHash": "e5526928cc6bb406", + "modelHash": "21de31d2606fef6b", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -996,7 +996,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -1043,16 +1043,16 @@ } }, "observed": { - "state": "stale" + "state": "fresh" } }, "content:architecture.md/constraints": { "accepted": { - "codeHash": "256cd50e4dae5e7f", - "modelHash": "2b7fa475e3b6d941", + "codeHash": "e5526928cc6bb406", + "modelHash": "36d19ef7ee11f149", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -1290,7 +1290,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -1337,7 +1337,7 @@ } }, "observed": { - "state": "stale" + "state": "fresh" } }, "omm:ui-camera-label": { @@ -1392,14 +1392,14 @@ }, "content:_reference/engine.md/contract": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "256cd50e4dae5e7f", - "modelHash": "66c6d69f9fc52416", + "codeHash": "e5526928cc6bb406", + "modelHash": "4ef7c17af745df2f", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -1637,7 +1637,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -1686,14 +1686,14 @@ }, "content:_reference/engine.md/camera2": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "256cd50e4dae5e7f", - "modelHash": "7af1d65d67f75fb3", + "codeHash": "e5526928cc6bb406", + "modelHash": "e53b130506a00ed6", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -1931,7 +1931,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -1980,14 +1980,14 @@ }, "content:_reference/engine.md/camerax": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "256cd50e4dae5e7f", - "modelHash": "da611927ad8a984f", + "codeHash": "e5526928cc6bb406", + "modelHash": "9db4e087c89fd6e3", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -2225,7 +2225,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -2274,14 +2274,14 @@ }, "content:_reference/engine.md/comparison": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "256cd50e4dae5e7f", - "modelHash": "c6e5f3e19d40ec82", + "codeHash": "e5526928cc6bb406", + "modelHash": "159d4eedf4472bea", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -2519,7 +2519,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c", @@ -2568,14 +2568,14 @@ }, "content:_reference/troubleshooting.md/layer-isolation": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "bce4991cbed5d674", + "codeHash": "844a3e7c556ad4ae", "modelHash": "1c146246635748ea", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/src/main/java/dev/halcamera/GalleryActivity.kt": "d220302a8e9dd392cf1560e5b28dc5aa6378202f", "app/src/main/java/dev/halcamera/MainActivity.kt": "b29ca903388af63b0218a32b5bcc2d8075479cef", @@ -2628,7 +2628,7 @@ "app/src/main/java/dev/halcamera/metrics/MetricExtractor.kt": "2dc23b55b00407ea5126dd19e3871fcb8f9fc5d4", "app/src/main/java/dev/halcamera/telemetry/FlightRecorder.kt": "f5148663814d0aec3ed45a44478bae4b0323e01d", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "tools/halcam/halcam/cli.py": "4161d167e0c041fdca0c185505083b57907bc789", "tools/halcam/halcam/download.py": "6cb9875bdae0257330e0942386f7ecd64a34d690", "tools/halcam/halcam/submission.py": "34a8444dcbaec0b783fc71ac1cffb844d9fc2147" @@ -2637,14 +2637,14 @@ }, "content:_reference/evidence.md/device-notes": { "observed": { - "state": "stale" + "state": "fresh" }, "accepted": { - "codeHash": "256cd50e4dae5e7f", - "modelHash": "20cdcdfca8bd78ef", + "codeHash": "e5526928cc6bb406", + "modelHash": "b9db0c76a101865b", "at": "2026-10-24", - "commit": "e4de2762", - "reviewer": "codex-app-bokeh-code-review", + "commit": "9e71f282", + "reviewer": "codex-app-bokeh-ui-review", "files": { "app/build.gradle.kts": "f253a1187163f8d464f7eb2ae00426c64806ad75", "app/src/main/AndroidManifest.xml": "394f1ceb67558a1400f691c3eb3157f7be783960", @@ -2882,7 +2882,7 @@ "app/src/main/java/dev/halcamera/telemetry/IncidentExporter.kt": "b3bbdcb22853d8b517dd1b3232ad3e3f12c1e0e8", "app/src/main/java/dev/halcamera/telemetry/Telemetry.kt": "3b5449d9b65774b05888a61cf7a3c856749035db", "app/src/main/java/dev/halcamera/ui/AboutSheet.kt": "daa585260738255daaa1bca7a784b3f19d0a02bf", - "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "d06a67f29686176bfa65f72d87a422da48482514", + "app/src/main/java/dev/halcamera/ui/AppBokehEditor.kt": "3f8dc072f1b1efe267d4f1a52121ea2efaaad902", "app/src/main/java/dev/halcamera/ui/BenchmarkResultCards.kt": "743c60bd7877908b05b50f2dceaa309ea4b55344", "app/src/main/java/dev/halcamera/ui/CameraWidgets.kt": "5ff28a3023ab38034e6322214e29ef066e671435", "app/src/main/java/dev/halcamera/ui/CaptureFeedback.kt": "d7c54ea31ebe7827ca434f2d1c9ab2eb21114c5c",