Skip to content

fix: 펫 신원 인증 영상 720p 재인코딩 + 전송 전 용량 차단 (pmdb#136 ①) - #311

Open
seizeh wants to merge 1 commit into
mainfrom
fix/pet-identity-video-compress
Open

seizeh wants to merge 1 commit into
mainfrom
fix/pet-identity-video-compress

Conversation

@seizeh

@seizeh seizeh commented Aug 25, 2026

Copy link
Copy Markdown
Owner

실사용 첫날 3회 중 2회video_too_large 로 막혔습니다(2026-08-23 야간). 7초짜리가 17MB(≈19Mbps)였고 서버 한도는 19M base64 chars(원본 약 13.6MB, 영상+프레임 합산)입니다.

분석 전문은 seizeh/pmdb#136 코멘트에 있습니다.

원인 — 이 경로만 압축을 안 거쳤습니다

첨부 영상은 pickVideo 가 720p 로 다시 굽는데(1cb0085), 신원 인증 경로만 촬영 원본을 그대로 넘겼습니다.

// 첨부 — 압축한다
Future<XFile?> pickVideo(...) async { ...; return _normalizeVideo(f, onCompress); }

// 신원 인증 — 원본 그대로  ← 여기
Future<XFile?> capturePetVideo() => _picker.pickVideo(
  source: ImageSource.camera, maxDuration: const Duration(seconds: 11),
);

길이가 아니라 화질이 원인이라 짧게 다시 찍어도 또 걸립니다. 실제로 11초를 허용해 놓고 6초분만 받고 있었습니다(19Mbps 환산).

야간이 이를 밀어냈습니다

같은 설정(maxWidth: 1024, quality: 85)으로 뽑은 프레임 실측:

촬영 프레임 4장
주간 07-03 0.20 · 0.21 · 0.21 · 0.23 MB
야간 08-23 0.43 · 0.44 · 0.47 · 0.47 MB

2배입니다. 어두우면 ISO가 올라가고 센서 노이즈가 생기는데, 노이즈는 프레임마다 무작위라 인코더가 압축하지 못합니다. 7초는 원래 경계였고 노이즈가 세 번 중 두 번 넘겼습니다.

고친 것

capturePetVideo_normalizeVideo(720p)를 거칩니다. 11초를 꽉 채워도 3MB 안팎이라 이 축이 통째로 사라집니다.

⚠️ 압축 위치가 중요합니다. 화면은 여기서 받은 파일 하나만 보고 거기서 프레임을 뽑으므로, 반환한 파일이 곧 전송될 파일입니다 — 프레임과 영상의 출처가 갈릴 수 없습니다. 전송 직전에 압축하면 원본에서 뽑은 프레임이 압축본과 함께 나가고, 서버의 frames_from_video 판정이 "영상에 없는 장면"으로 오탐할 수 있습니다(pmdb#135).

압축본이 따로 생기면 촬영 임시본을 지웁니다. 안 지우면 검증만 하고 버릴 원본 17MB가 기기에 남습니다(화면 주석의 '잔존 방지' 의도). 갤러리 경로가 원본을 남기는 것과는 이유가 다릅니다 — 저기는 사용자가 다시 고를 수 있는 파일입니다.

② 전송 전에 용량을 잽니다. 서버와 같은 사유 코드를 돌려주므로 화면 문구는 그대로입니다. 방어가 아니라 낭비 방지입니다 — 종전에는 서버 판정을 받으려고 25MB를 다 올려야 했고, 실패해도 aienroll 레이트리밋(10회)은 소모됐습니다. base64 길이는 인코딩하지 않고 셉니다.

③ 재인코딩 진행률을 띄웁니다. 촬영 직후 몇 초가 걸리는데 진행률이 없으면 멈춘 것처럼 보입니다. 콜백은 압축이 실제로 일어날 때만 옵니다.

④ 안내 문구를 실행 가능한 지시로 바꿨습니다.

문구
종전 영상 용량이 커서 처리하지 못했어요. 잠시 후 다시 시도하거나 고객센터로 문의해주세요
지금 영상 용량이 커서 처리하지 못했어요. 조금 더 짧게, 밝은 곳에서 다시 촬영해주세요

종전 문구의 근거("원인이 화질이라 사용자가 할 수 있는 게 없다")는 그때는 맞았습니다. 이제는 앱이 720p로 굽고 나서 재므로 남은 변수가 길이와 밝기뿐이고 둘 다 사용자가 바꿀 수 있습니다. 이제야 이 안내가 성립합니다.

왜 지금인가 — pmdb#135와의 순서

#135는 측정 중 프레임 추출 경로 변경을 금지합니다("측정 오염"). 압축은 프레임이 나오는 파일을 바꾸므로 거기 걸립니다.

지금은 유효 표본이 1건이라 버릴 게 없습니다. 표본이 쌓인 뒤에 넣으면 전부 버려야 합니다. 그리고 실패율 66.7%로는 #135의 백업 경로(자가 표본 n=30)도 불가능합니다 — 30건에 약 90회 촬영이고 레이트리밋에 계속 걸립니다.

#136 ① → 자가 표본 30건 → #135 오탐률 판정 → enforce 전환

검증

flutter test        339 passed (신규 11 포함)
커버리지            20.6%  (하한 18%)
catch (_)           104곳  (상한 104, 증가 없음)
포맷 · analyze · dart:io 게이트 · 웹 빌드   전부 통과

신규 테스트가 못 박은 것:

  • b64Len 이 실제 base64Encode 길이와 일치(패딩 포함 경계 14종)
  • 클라 상수가 서버 MAX_INLINE_B64_CHARS 와 같을 것
  • 한도와 같으면 통과 · 1바이트 초과면 거절(서버가 > 로 거절하므로)
  • 프레임도 예산을 함께 쓸 것 — 영상만 재면 서버가 거절할 조합을 통과시킵니다
  • 실측 시나리오: 야간 프레임 4장 + 17MB → 차단 / 압축본 2~5MB → 통과

클라 상수가 서버보다 빡빡하면 서버가 받아 줄 영상을 앱이 먼저 막고 서버 로그에 흔적도 안 남습니다. 그쪽이 더 나쁜 실패라 경계를 정확히 고정했습니다.

남는 것

  • 720p가 개체 식별 정확도(무늬·질감)에 주는 영향은 실측이 필요합니다 — #135의 자가 표본 30건이 그 검증을 겸합니다
  • 프레임 quality(85) 조정은 frames_from_video 판정 입력을 바꾸므로 fix: 펫 한 마리일 때 히어로 왼쪽 치우침 #135 판정 전에는 손대지 않습니다
  • 앱 릴리스가 필요합니다(현재 2.2.3+17). 기존 설치본은 업데이트 전까지 종전 동작입니다

🤖 Generated with Claude Code

실사용 첫날 3회 중 2회가 `video_too_large` 로 막혔다(2026-08-23 야간). 7초짜리가
17MB(≈19Mbps)였고 서버 한도는 19M base64 chars(원본 약 13.6MB, 영상+프레임 합산)다.

## 원인 — 이 경로만 압축을 안 거쳤다

첨부 영상은 pickVideo 가 720p 로 다시 굽는데(1cb0085) **신원 인증 경로만 촬영
원본을 그대로 넘겼다.** capturePetVideo 는 maxDuration 11초만 걸었다.

길이가 아니라 화질이 원인이라 사용자가 짧게 다시 찍어도 또 걸린다. 실제로
11초를 허용해 놓고 6초분만 받고 있었다(19Mbps 환산).

야간이 이를 밀어냈다. 어두우면 ISO 가 올라가고 센서 노이즈가 생기는데 노이즈는
프레임마다 무작위라 인코더가 압축하지 못한다. 같은 설정으로 뽑은 프레임 실측:

  주간(07-03) 0.20 · 0.21 · 0.21 · 0.23 MB
  야간(08-23) 0.43 · 0.44 · 0.47 · 0.47 MB   ← 2배

7초는 원래 경계였고 노이즈가 세 번 중 두 번 넘겼다.

## 고친 것

① capturePetVideo 가 _normalizeVideo(720p)를 거친다. 11초를 꽉 채워도 3MB 안팎이라
   이 축이 통째로 사라진다.

   ⚠️ **압축 위치가 중요하다.** 화면은 여기서 받은 파일 하나만 보고 거기서 프레임을
   뽑으므로, 반환한 파일이 곧 전송될 파일이다 — 프레임과 영상의 출처가 갈릴 수 없다.
   전송 직전에 압축하면 원본에서 뽑은 프레임이 압축본과 함께 나가고 서버의
   frames_from_video 판정이 "영상에 없는 장면" 으로 오탐할 수 있다(pmdb#135).

   압축본이 따로 생기면 촬영 임시본을 지운다. 안 지우면 검증만 하고 버릴 원본
   17MB 가 기기에 남는다(화면 주석의 '잔존 방지' 의도). 갤러리 경로가 원본을
   남기는 것과 이유가 다르다 — 저기는 사용자가 다시 고를 수 있는 파일이다.

② 전송 전에 용량을 잰다(PetEnrollRepository). 서버와 같은 사유 코드를 돌려주므로
   화면 문구는 그대로다. 방어가 아니라 낭비 방지다 — 종전에는 서버 판정을 받으려고
   25MB 를 다 올려야 했고, 실패해도 aienroll 레이트리밋(10회)은 소모됐다.
   base64 길이는 인코딩하지 않고 센다(재려는 낭비를 그대로 치를 이유가 없다).

③ 재인코딩 진행률을 화면에 띄운다. 촬영 직후 몇 초가 걸리는데 진행률이 없으면
   멈춘 것처럼 보인다. 콜백은 압축이 실제로 일어날 때만 온다.

④ video_too_large 안내를 실행 가능한 지시로 바꿨다.
   종전: "잠시 후 다시 시도하거나 고객센터로 문의해주세요"
   지금: "조금 더 짧게, 밝은 곳에서 다시 촬영해주세요"

   종전 문구의 근거는 "원인이 화질이라 사용자가 할 수 있는 게 없다" 였고 그때는
   맞았다. 이제는 앱이 720p 로 굽고 나서 재므로 남은 변수가 길이와 밝기뿐이고
   둘 다 사용자가 바꿀 수 있다. 그래서 이제야 이 안내가 성립한다.

## 왜 지금인가 (pmdb#135 와의 순서)

#135 는 측정 중 프레임 추출 경로 변경을 금지한다("측정 오염"). 압축은 프레임이
나오는 파일을 바꾸므로 거기 걸린다.

**지금은 유효 표본이 1건이라 버릴 게 없다.** 표본이 쌓인 뒤에 넣으면 전부 버려야
한다. 그리고 실패율 66.7% 로는 #135 의 백업 경로(자가 표본 n=30)도 불가능하다 —
30건에 약 90회 촬영이고 레이트리밋에 계속 걸린다. 순서가 이렇게 정해진다:

  #136 ① → 자가 표본 30건 → #135 오탐률 판정 → enforce 전환

## 검증

  flutter test        339 passed (신규 11 포함)
  커버리지            20.6% (하한 18%)
  catch (_)           104곳 (상한 104, 증가 없음)
  포맷·analyze·dart:io 게이트·웹 빌드  전부 통과

신규 테스트가 못 박은 것: b64Len 이 실제 base64Encode 길이와 일치할 것(패딩 포함
경계 14종), 클라 상수가 서버 MAX_INLINE_B64_CHARS 와 같을 것, 한도와 같으면 통과·
1바이트 초과면 거절(서버가 > 로 거절하므로), 프레임도 예산을 함께 쓸 것,
그리고 실측 시나리오(야간 프레임 4장 + 17MB 는 차단 / 압축본 2~5MB 는 통과).

클라 상수가 서버보다 빡빡하면 **서버가 받아 줄 영상을 앱이 먼저 막고 서버 로그에
흔적도 안 남는다.** 그쪽이 더 나쁜 실패라 경계를 정확히 고정했다.

## 남는 것

720p 가 개체 식별 정확도(무늬·질감)에 주는 영향은 실측이 필요하다 — #135 의 자가
표본 30건이 그 검증을 겸한다. 프레임 quality(85) 조정은 frames_from_video 판정
입력을 바꾸므로 #135 판정 전에는 손대지 않는다.

앱 릴리스가 필요하다(현재 2.2.3+17). 기존 설치본은 업데이트 전까지 종전 동작이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

전체 라인 커버리지: 20.6% (4315/20991)

이 PR 의 변경 파일:

파일 커버리지
lib/screen/pet_identity_enroll_screen.dart 🔴 0% (0/120)
lib/services/pet_enroll_repository.dart 🟡 46% (17/37)
lib/services/storage_service.dart 🔴 17% (31/181)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant