Repository navigation
Releases
FrameCritic 2.0 Offline · build 74
FrameCritic 2.4.0
A subject the app names correctly, and a three-line summary on top of it. The detector now reads
two people standing together as two, does not call a portrait a cow, and the result screen opens
with 한 줄 총평 · 가장 돋보이는 점 · 원포인트 조언 instead of ten paragraphs.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall
the previous FrameCritic before installing (adb install -r will fail with a signature mismatch).
After installing, the header must read a build number that matches this release tag (the value after
offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK. The file name keeps the 2.0 lineage that the
release job has used since build 53; the app inside reportsv2.4.0.FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files,
licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs.
What changed in this build
Two people are two people. The count used to come from boxes overlapping less than a third;
two people standing side by side overlap more than that, so the second one was deleted by
deduplication (IoU 0.32). Deduplication now runs at 0.52 for people, and the count is taken from
distinct box centres — the face, torso and full-body boxes of one person share a centre within 15% of
the width, two people do not. The badge reads 인물 2명 (신뢰도 83%), three or more read
인물 여러 명, and the opening paragraph of the critique has a two-person sentence of its own:
"화면에는 두 사람이 나란히 자리해 있고, 사진의 관심은 두 사람 사이의 거리와 시선에 놓여 있습니다."
A portrait is not a cow. Cattle and wildlife names (cow, sheep, horse, bear, elephant, zebra,
giraffe) were reaching portraits through coats, dark clothing and crouched poses at 0.38–0.45, and a
large box scored higher than a tight person box. Now:
- a person at 0.30 or more outranks a cattle/wildlife box unless that box is 0.85+ and 0.15
clear of the person; - cattle/wildlife need 0.85 indoors, in the city or against the sky, and 0.60 outdoors;
- dogs, cats and birds need 0.40 — they are ordinary companions, not misfires;
- the subject is chosen by
score^1.5 × √area, so confidence leads and area only breaks ties; - detections start at a 0.30 class score (Android
setScoreThreshold(0.30f),setMaxResults(10)),
enough to keep a second person or a small product in frame without naming noise.
Box geometry, measured instead of assumed. The browser decoder's anchor order and box-encoding
order were traced on six photos with ground-truth boxes from an independent YOLOX detector. Octave
step outer, ratios 2/1/0.5, decoded as [dy, dx, dh, dw] scored best (mean IoU 0.644 against 0.604
for the previous reading). The gain is real but modest — it is the best-scoring arrangement of the
candidates tested, not a proof of the model's internal layout — and it is documented that way in
docs/WEB_VISION.md.
한눈에 보는 핵심 요약. Both the APK and the web app now open the result with a three-line card,
above the ten sections:
- 📌 한 줄 총평 — the subject named from the detection plus the completeness band
("두 사람이 나란히 선 장면이 화면의 중심을 잡고, 균형이 좋은 한 장입니다.") - ✨ 가장 돋보이는 점 — the strongest axis, or the subject's own praise sentence when that axis
is only middling - 💡 원포인트 조언 — one actionable shooting or editing tip for that subject
A 보기 toggle (웹: 핵심 요약 / 상세 비평, 앱: chips) switches between the card and the full ten
sections, and the card is the default. The brief is built by quickSummary() in
web/src/lib/critic-engine.ts and CriticEngine.kt, so the same photo gives the same three lines on
both, and it obeys the same contract: no digits, no measurement vocabulary, under 320 characters per
line. Copy and report-image sharing include the summary.
Tests. Kotlin: 36 unit tests, 13 of them subject tests — person beats a larger cattle box
indoors, two people count as two and three as several, an indoor cow at 0.62 is not named, an outdoor
cow is, a dog photo keeps its dog over a weak person, and the quick summary has three short lines
without numbers. Web: tsc clean, npm run build clean, the sentence contract replays 49,140
generated sections and summaries with zero failures, and scripts/subject-check.cjs replays 16
detection scenarios (person priority, cattle guard, two-person count, water misfire, empty
detection) with zero failures. Both were also driven in headless Chromium with real photographs:
zidane.jpg (two people) reads 인물 2명 (신뢰도 83%) and the two-person summary, a portrait reads
인물, two cats read 고양이 여러 마리, and a still life falls back to 장면 추정.
Limits
- The detector is COCO's 80 classes; a still life whose object is not confidently recognised stays
"장면 추정" rather than guessing a name. - Box geometry is approximate on this model (mean IoU against an independent detector is around
0.6), which is enough for naming, counting and the overlay, not for measuring subject size. - No physical Galaxy S25 Ultra test was performed for this build. Kotlin unit tests, the APK
signature/alignment/content check and host inference checks run in CI; they do not establish
Android runtime behaviour.
Carried over
- Ten critique sections written at length (Quick 4 / Standard 6 / Expert 9 sentences per section),
deterministic on-device, no language model, no network, no child process. - Subject-aware vocabulary for person, companion animal, vehicle, water, nature, urban and still
object; scene reading from pixel bands; the neutral fallback when nothing is clear. - No manual
recycle()of a bitmap the screen draws; every analysis stage can fail alone and hand a
neutral, labelled result to the next; crash and stage records; 1280 px analysis cap; report copy
and image sharing. - Score curve
1 + 9·x^γwith γ = 0.40 / 0.55 / 0.85 — unchanged.
Version. versionCode 26, versionName 2.4.0. The header reads v2.4.0 (Build N), N being the
CI run number and therefore this release tag.
FrameCritic 2.0 Offline · build 73
FrameCritic 2.3.0
The critique now names what is in the photo. Instead of writing about "피사체", the engine says
인물, 반려견, 고양이, 자동차, 바다, 물가, 산, 숲, 들판, 도심 or the object it actually found (그릇, 컵,
노트북 …), and every section is written in that subject's own vocabulary.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall
the previous FrameCritic before installing (adb install -r will fail with a signature mismatch).
After installing, the header must read a build number that matches this release tag (the value after
offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK. The file name keeps the 2.0 lineage that the
release job has used since build 53; the app inside reportsv2.3.0.FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files,
licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs.
What changed in this build
A subject reading, shared by the app and the web app. SubjectCategory (person, animal,
vehicle, landscape-water, landscape-nature, urban, still object, general) and SceneContext
(indoor, outdoor, urban, nature, water, sky, night) now live in Domain.kt and domain.ts.
Subject.kt / subject.ts turn the detector's boxes and the scene cues into one SubjectScene:
the subject's name, how many of them were found, the detector's confidence, the scene label, and
whether the subject separates from its background by brightness.
Ten sections that speak about that subject. The opening paragraph of 종합 크리틱 names it, and
빛과 노출, 색채, 구도, 선명도와 질감, 분위기와 순간, 잘 살아난 점, 더 다듬을 점, 촬영 제안, 보정 제안
each carry a sentence written for that category — rim light on fur, chrome highlights along a car's
shoulder, an eye-line at a low angle, a horizon dividing sky and water, a mountain ridge fading
with distance, a pot's shadow on a windowsill. Both sentence variants of every slot mention the
subject, so the word appears in at least six of the ten sections whatever the seed. The no-numbers,
no-measurement-vocabulary contract is unchanged, and the raw values stay in the appendix.
The scene is read from pixels, and said honestly. Three bands of the frame (mean colour,
texture, green/blue/bright shares) plus saturation and horizon candidates classify water, nature,
outdoor, urban, sky, night or indoor. Water needs a bright warm top over a smooth dark band, or a
blue bottom under a bright top; a textured dark bottom reads as a mountain, not a sea. When nothing
clearly fits, the subject stays the neutral 장면 and the appendix says the reading rests on pixels
only. Detector names that contradict an outdoor scene (waves called a bed) are dropped rather than
printed.
Web app: real object detection in the tab. web/src/lib/vision.ts runs the same
EfficientDet-Lite0 COCO model as the Android app through LiteRT.js, decodes its raw heads against
EfficientDet's anchor grid, and feeds the boxes into the same critique engine; the new subject row
sits above the critique and the appendix shows "피사체 · 신뢰도 · 장면 · 피사체·배경 분리 · 객체 탐지
시간". A first visit now downloads about 17.5 MB of models (two NIMA nets plus the 4.6 MB detector).
Tests. Kotlin: 23 unit tests, including six new subject-scene tests (every category named in at
least six sections, no digits or measurement words in any depth, scene reading, misfire guard,
empty fallback). Web: tsc clean, npm run build clean, and the sentence contract replays 37,800
generated sections with zero failures.
Carried over from build 66
Ten critique sections, written at length. Each section is now built from a pool of
measurement-driven sentences instead of one or two. Depth sets how many: Quick four, Standard six
(the app's default), Expert nine.
Ten critique sections, written at length. Each section is now built from a pool of
measurement-driven sentences instead of one or two. Depth sets how many: Quick four, Standard six
(the app's default), Expert nine. Because most sentences carry two clauses, a Standard section
runs about eight to twelve sentences and 350–450 Korean characters, an Expert section fourteen to
nineteen sentences and 500–640 characters — against roughly 100–150 characters in build 65. The
sections cover what the brief asked for: the whole frame's breath and the meeting of light and
colour in 종합 크리틱 (now three paragraphs); tone, the closing of both ends of the scale, highlight
roll-off, shadow detail, how light models form and whether the light reads soft or hard in 빛과 노출;
colour temperature, dominant and supporting colour, saturation restraint, the emotional air of the
palette, whether colour follows form or bleeds past it, and where the colour weight sits in 색채;
placement, horizon candidates, negative space and crowding, left-right balance, the path the eye
takes, and how the frame holds its subject in 구도; focus, surface texture, separation from the
background, edge character, evenness of detail, and whether the blur is deliberate in 선명도와 질감;
tone of the moment, human presence, stillness against a caught instant, the story the frame holds,
and what stays after you close it in 분위기와 순간; then strengths, refinements, shooting advice
(angle, distance, metering, shutter, aperture in words rather than numbers) and editing advice
(tone curve, white balance, highlight and shadow balance, noise order, crop).
The contract still holds. No section body contains a digit or one of the machine terms
(관찰되었, 관측, Laplacian, COCO, 휘도, 255, 검출, 측정, NIMA, 히스토그램, 클리핑, 분산); the Kotlin
unit test critiqueBodyCarriesNoNumbersOrMachineWords runs on every persona · level · tone · depth
combination and fails the build otherwise. Every raw value still appears in
상세 측정 데이터 및 기술적 근거. The score curve is untouched: 1 + 9·x^γ with γ = 0.40 / 0.55 /
0.85 — build 65's numbers stay valid.
New: browser app in web/. Vite + React + TypeScript + Tailwind, 100% client-side. The photo
never leaves the tab: pixels are read through Canvas and the TypeScript port of the same engine
writes the same Korean prose. web/src/lib/critic-engine.ts was verified against 4,050 Kotlin
outputs (five measurement sets × three subject sets × two NIMA results × every persona, level, tone
and depth) with zero differences in section text or score. Measurement in the browser covers the
256-bin luminance histogram, mean luminance, clipping, global and local contrast, Laplacian variance,
Sobel edge density, mean RGB, channel spread, the 12-bin hue histogram, mean saturation, horizontal
line candidates and a salience region. The browser has no NIMA or detector weights, so the aesthetic
score and the "주요 영역" box are heuristics from those measurements; the UI labels them 추정 and the
README/doc say so. npm run build runs tsc --noEmit plus the production build, and
node scripts/contract-check.cjs replays 37,800 generated sections against the same contract.
Deployment. Both vercel.json files are in the repository; importing plenuen-cpu/FrameCritic
with the root directory set to web builds with no further configuration. See
docs/VERCEL_DEPLOYMENT.md.
Version. versionCode 25, versionName 2.3.0. The header reads v2.3.0 (Build N), N being the
CI run number and therefore this release tag.
Carried over unchanged
- No manual
recycle()of any bitmap the screen draws; the image the screen renders is never handed
to MediaPipe or released by the analysis. - Every analysis stage can fail alone and hand a neutral, clearly labelled result to the next; the
report is always filled with all ten critique sections. - Crash record with the stack trace and a stage record naming the step that was running.
- Images capped at 1280 px on the long edge, GGUF weights not packaged, no child process and no
network. Report copy and report-image sharing.
No physical Galaxy S25 Ultra test was performed. Kotlin unit tests, the APK
signature/alignment/content check and host inference checks run in CI, but they do not establish
Android runtime behaviour. On the web side, the build is type-checked, the engine is compared against
the Kotlin output, and the app was driven in a headless Chromium session with a real photograph
(upload → analysis → ten sections → measurement appendix) with no console error; that is not a test
on the user's own browser or device.
FrameCritic 2.0 Offline · build 72
FrameCritic 2.3.0 · build 72
The critique now names what is in the photo. Instead of writing about "피사체", the engine says
인물, 반려견, 고양이, 자동차, 바다, 물가, 산, 숲, 들판, 도심 or the object it actually found (그릇, 컵,
노트북 …), and every section is written in that subject's own vocabulary.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall
the previous FrameCritic before installing (adb install -r will fail with a signature mismatch).
After installing, the header must read a build number that matches this release tag (the value after
offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK. The file name keeps the 2.0 lineage that the
release job has used since build 53; the app inside reportsv2.3.0.FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files,
licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs.
What changed in this build
A subject reading, shared by the app and the web app. SubjectCategory (person, animal,
vehicle, landscape-water, landscape-nature, urban, still object, general) and SceneContext
(indoor, outdoor, urban, nature, water, sky, night) now live in Domain.kt and domain.ts.
Subject.kt / subject.ts turn the detector's boxes and the scene cues into one SubjectScene:
the subject's name, how many of them were found, the detector's confidence, the scene label, and
whether the subject separates from its background by brightness.
Ten sections that speak about that subject. The opening paragraph of 종합 크리틱 names it, and
빛과 노출, 색채, 구도, 선명도와 질감, 분위기와 순간, 잘 살아난 점, 더 다듬을 점, 촬영 제안, 보정 제안
each carry a sentence written for that category — rim light on fur, chrome highlights along a car's
shoulder, an eye-line at a low angle, a horizon dividing sky and water, a mountain ridge fading
with distance, a pot's shadow on a windowsill. Both sentence variants of every slot mention the
subject, so the word appears in at least six of the ten sections whatever the seed. The no-numbers,
no-measurement-vocabulary contract is unchanged, and the raw values stay in the appendix.
The scene is read from pixels, and said honestly. Three bands of the frame (mean colour,
texture, green/blue/bright shares) plus saturation and horizon candidates classify water, nature,
outdoor, urban, sky, night or indoor. Water needs a bright warm top over a smooth dark band, or a
blue bottom under a bright top; a textured dark bottom reads as a mountain, not a sea. When nothing
clearly fits, the subject stays the neutral 장면 and the appendix says the reading rests on pixels
only. Detector names that contradict an outdoor scene (waves called a bed) are dropped rather than
printed.
Web app: real object detection in the tab. web/src/lib/vision.ts runs the same
EfficientDet-Lite0 COCO model as the Android app through LiteRT.js, decodes its raw heads against
EfficientDet's anchor grid, and feeds the boxes into the same critique engine; the new subject row
sits above the critique and the appendix shows "피사체 · 신뢰도 · 장면 · 피사체·배경 분리 · 객체 탐지
시간". A first visit now downloads about 17.5 MB of models (two NIMA nets plus the 4.6 MB detector).
Tests. Kotlin: 23 unit tests, including six new subject-scene tests (every category named in at
least six sections, no digits or measurement words in any depth, scene reading, misfire guard,
empty fallback). Web: tsc clean, npm run build clean, and the sentence contract replays 37,800
generated sections with zero failures.
Carried over from build 66
Ten critique sections, written at length. Each section is now built from a pool of
measurement-driven sentences instead of one or two. Depth sets how many: Quick four, Standard six
(the app's default), Expert nine.
Ten critique sections, written at length. Each section is now built from a pool of
measurement-driven sentences instead of one or two. Depth sets how many: Quick four, Standard six
(the app's default), Expert nine. Because most sentences carry two clauses, a Standard section
runs about eight to twelve sentences and 350–450 Korean characters, an Expert section fourteen to
nineteen sentences and 500–640 characters — against roughly 100–150 characters in build 65. The
sections cover what the brief asked for: the whole frame's breath and the meeting of light and
colour in 종합 크리틱 (now three paragraphs); tone, the closing of both ends of the scale, highlight
roll-off, shadow detail, how light models form and whether the light reads soft or hard in 빛과 노출;
colour temperature, dominant and supporting colour, saturation restraint, the emotional air of the
palette, whether colour follows form or bleeds past it, and where the colour weight sits in 색채;
placement, horizon candidates, negative space and crowding, left-right balance, the path the eye
takes, and how the frame holds its subject in 구도; focus, surface texture, separation from the
background, edge character, evenness of detail, and whether the blur is deliberate in 선명도와 질감;
tone of the moment, human presence, stillness against a caught instant, the story the frame holds,
and what stays after you close it in 분위기와 순간; then strengths, refinements, shooting advice
(angle, distance, metering, shutter, aperture in words rather than numbers) and editing advice
(tone curve, white balance, highlight and shadow balance, noise order, crop).
The contract still holds. No section body contains a digit or one of the machine terms
(관찰되었, 관측, Laplacian, COCO, 휘도, 255, 검출, 측정, NIMA, 히스토그램, 클리핑, 분산); the Kotlin
unit test critiqueBodyCarriesNoNumbersOrMachineWords runs on every persona · level · tone · depth
combination and fails the build otherwise. Every raw value still appears in
상세 측정 데이터 및 기술적 근거. The score curve is untouched: 1 + 9·x^γ with γ = 0.40 / 0.55 /
0.85 — build 65's numbers stay valid.
New: browser app in web/. Vite + React + TypeScript + Tailwind, 100% client-side. The photo
never leaves the tab: pixels are read through Canvas and the TypeScript port of the same engine
writes the same Korean prose. web/src/lib/critic-engine.ts was verified against 4,050 Kotlin
outputs (five measurement sets × three subject sets × two NIMA results × every persona, level, tone
and depth) with zero differences in section text or score. Measurement in the browser covers the
256-bin luminance histogram, mean luminance, clipping, global and local contrast, Laplacian variance,
Sobel edge density, mean RGB, channel spread, the 12-bin hue histogram, mean saturation, horizontal
line candidates and a salience region. The browser has no NIMA or detector weights, so the aesthetic
score and the "주요 영역" box are heuristics from those measurements; the UI labels them 추정 and the
README/doc say so. npm run build runs tsc --noEmit plus the production build, and
node scripts/contract-check.cjs replays 37,800 generated sections against the same contract.
Deployment. Both vercel.json files are in the repository; importing plenuen-cpu/FrameCritic
with the root directory set to web builds with no further configuration. See
docs/VERCEL_DEPLOYMENT.md.
Version. versionCode 25, versionName 2.3.0. The header reads v2.3.0 (Build N), N being the
CI run number and therefore this release tag.
Carried over unchanged
- No manual
recycle()of any bitmap the screen draws; the image the screen renders is never handed
to MediaPipe or released by the analysis. - Every analysis stage can fail alone and hand a neutral, clearly labelled result to the next; the
report is always filled with all ten critique sections. - Crash record with the stack trace and a stage record naming the step that was running.
- Images capped at 1280 px on the long edge, GGUF weights not packaged, no child process and no
network. Report copy and report-image sharing.
No physical Galaxy S25 Ultra test was performed. Kotlin unit tests, the APK
signature/alignment/content check and host inference checks run in CI, but they do not establish
Android runtime behaviour. On the web side, the build is type-checked, the engine is compared against
the Kotlin output, and the app was driven in a headless Chromium session with a real photograph
(upload → analysis → ten sections → measurement appendix) with no console error; that is not a test
on the user's own browser or device.
FrameCritic 2.0 Offline · build 69
FrameCritic 2.2.0 · build 66
Build 66 makes the critique read like a photo essayist rather than a caption, and ships the same
engine as a browser app that needs no install.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall
the previous FrameCritic before installing (adb install -r will fail with a signature mismatch).
After installing, the header must read a build number that matches this release tag (the value after
offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK (~65 MB). The file name keeps the 2.0 lineage that
the release job has used since build 53; the app inside reportsv2.2.0.FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files,
licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs for the
pinned NIMA/MediaPipe weights.
What changed in this build
Ten critique sections, written at length. Each section is now built from a pool of
measurement-driven sentences instead of one or two. Depth sets how many: Quick four, Standard six
(the app's default), Expert nine. Because most sentences carry two clauses, a Standard section
runs about eight to twelve sentences and 350–450 Korean characters, an Expert section fourteen to
nineteen sentences and 500–640 characters — against roughly 100–150 characters in build 65. The
sections cover what the brief asked for: the whole frame's breath and the meeting of light and
colour in 종합 크리틱 (now three paragraphs); tone, the closing of both ends of the scale, highlight
roll-off, shadow detail, how light models form and whether the light reads soft or hard in 빛과 노출;
colour temperature, dominant and supporting colour, saturation restraint, the emotional air of the
palette, whether colour follows form or bleeds past it, and where the colour weight sits in 색채;
placement, horizon candidates, negative space and crowding, left-right balance, the path the eye
takes, and how the frame holds its subject in 구도; focus, surface texture, separation from the
background, edge character, evenness of detail, and whether the blur is deliberate in 선명도와 질감;
tone of the moment, human presence, stillness against a caught instant, the story the frame holds,
and what stays after you close it in 분위기와 순간; then strengths, refinements, shooting advice
(angle, distance, metering, shutter, aperture in words rather than numbers) and editing advice
(tone curve, white balance, highlight and shadow balance, noise order, crop).
The contract still holds. No section body contains a digit or one of the machine terms
(관찰되었, 관측, Laplacian, COCO, 휘도, 255, 검출, 측정, NIMA, 히스토그램, 클리핑, 분산); the Kotlin
unit test critiqueBodyCarriesNoNumbersOrMachineWords runs on every persona · level · tone · depth
combination and fails the build otherwise. Every raw value still appears in
상세 측정 데이터 및 기술적 근거. The score curve is untouched: 1 + 9·x^γ with γ = 0.40 / 0.55 /
0.85 — build 65's numbers stay valid.
New: browser app in web/. Vite + React + TypeScript + Tailwind, 100% client-side. The photo
never leaves the tab: pixels are read through Canvas and the TypeScript port of the same engine
writes the same Korean prose. web/src/lib/critic-engine.ts was verified against 4,050 Kotlin
outputs (five measurement sets × three subject sets × two NIMA results × every persona, level, tone
and depth) with zero differences in section text or score. Measurement in the browser covers the
256-bin luminance histogram, mean luminance, clipping, global and local contrast, Laplacian variance,
Sobel edge density, mean RGB, channel spread, the 12-bin hue histogram, mean saturation, horizontal
line candidates and a salience region. The browser has no NIMA or detector weights, so the aesthetic
score and the "주요 영역" box are heuristics from those measurements; the UI labels them 추정 and the
README/doc say so. npm run build runs tsc --noEmit plus the production build, and
node scripts/contract-check.cjs replays 5,400 generated sections against the same contract.
Deployment. Both vercel.json files are in the repository; importing plenuen-cpu/FrameCritic
with the root directory set to web builds with no further configuration. See
docs/VERCEL_DEPLOYMENT.md.
Version. versionCode 24, versionName 2.2.0. The header reads v2.2.0 (Build N), N being the
CI run number and therefore this release tag.
Carried over unchanged
- No manual
recycle()of any bitmap the screen draws; the image the screen renders is never handed
to MediaPipe or released by the analysis. - Every analysis stage can fail alone and hand a neutral, clearly labelled result to the next; the
report is always filled with all ten critique sections. - Crash record with the stack trace and a stage record naming the step that was running.
- Images capped at 1280 px on the long edge, GGUF weights not packaged, no child process and no
network. Report copy and report-image sharing.
No physical Galaxy S25 Ultra test was performed. Kotlin unit tests, the APK
signature/alignment/content check and host inference checks run in CI, but they do not establish
Android runtime behaviour. On the web side, the build is type-checked, the engine is compared against
the Kotlin output, and the app was driven in a headless Chromium session with a real photograph
(upload → analysis → ten sections → measurement appendix) with no console error; that is not a test
on the user's own browser or device.
FrameCritic 2.0 Offline · build 68
FrameCritic 2.2.0 · build 66
Build 66 makes the critique read like a photo essayist rather than a caption, and ships the same
engine as a browser app that needs no install.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall
the previous FrameCritic before installing (adb install -r will fail with a signature mismatch).
After installing, the header must read a build number that matches this release tag (the value after
offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK (~65 MB). The file name keeps the 2.0 lineage that
the release job has used since build 53; the app inside reportsv2.2.0.FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files,
licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs for the
pinned NIMA/MediaPipe weights.
What changed in this build
Ten critique sections, written at length. Each section is now built from a pool of
measurement-driven sentences instead of one or two. Depth sets how many: Quick four, Standard six
(the app's default), Expert nine. Because most sentences carry two clauses, a Standard section
runs about eight to twelve sentences and 350–450 Korean characters, an Expert section fourteen to
nineteen sentences and 500–640 characters — against roughly 100–150 characters in build 65. The
sections cover what the brief asked for: the whole frame's breath and the meeting of light and
colour in 종합 크리틱 (now three paragraphs); tone, the closing of both ends of the scale, highlight
roll-off, shadow detail, how light models form and whether the light reads soft or hard in 빛과 노출;
colour temperature, dominant and supporting colour, saturation restraint, the emotional air of the
palette, whether colour follows form or bleeds past it, and where the colour weight sits in 색채;
placement, horizon candidates, negative space and crowding, left-right balance, the path the eye
takes, and how the frame holds its subject in 구도; focus, surface texture, separation from the
background, edge character, evenness of detail, and whether the blur is deliberate in 선명도와 질감;
tone of the moment, human presence, stillness against a caught instant, the story the frame holds,
and what stays after you close it in 분위기와 순간; then strengths, refinements, shooting advice
(angle, distance, metering, shutter, aperture in words rather than numbers) and editing advice
(tone curve, white balance, highlight and shadow balance, noise order, crop).
The contract still holds. No section body contains a digit or one of the machine terms
(관찰되었, 관측, Laplacian, COCO, 휘도, 255, 검출, 측정, NIMA, 히스토그램, 클리핑, 분산); the Kotlin
unit test critiqueBodyCarriesNoNumbersOrMachineWords runs on every persona · level · tone · depth
combination and fails the build otherwise. Every raw value still appears in
상세 측정 데이터 및 기술적 근거. The score curve is untouched: 1 + 9·x^γ with γ = 0.40 / 0.55 /
0.85 — build 65's numbers stay valid.
New: browser app in web/. Vite + React + TypeScript + Tailwind, 100% client-side. The photo
never leaves the tab: pixels are read through Canvas and the TypeScript port of the same engine
writes the same Korean prose. web/src/lib/critic-engine.ts was verified against 4,050 Kotlin
outputs (five measurement sets × three subject sets × two NIMA results × every persona, level, tone
and depth) with zero differences in section text or score. Measurement in the browser covers the
256-bin luminance histogram, mean luminance, clipping, global and local contrast, Laplacian variance,
Sobel edge density, mean RGB, channel spread, the 12-bin hue histogram, mean saturation, horizontal
line candidates and a salience region. The browser has no NIMA or detector weights, so the aesthetic
score and the "주요 영역" box are heuristics from those measurements; the UI labels them 추정 and the
README/doc say so. npm run build runs tsc --noEmit plus the production build, and
node scripts/contract-check.cjs replays 5,400 generated sections against the same contract.
Deployment. Both vercel.json files are in the repository; importing plenuen-cpu/FrameCritic
with the root directory set to web builds with no further configuration. See
docs/VERCEL_DEPLOYMENT.md.
Version. versionCode 24, versionName 2.2.0. The header reads v2.2.0 (Build N), N being the
CI run number and therefore this release tag.
Carried over unchanged
- No manual
recycle()of any bitmap the screen draws; the image the screen renders is never handed
to MediaPipe or released by the analysis. - Every analysis stage can fail alone and hand a neutral, clearly labelled result to the next; the
report is always filled with all ten critique sections. - Crash record with the stack trace and a stage record naming the step that was running.
- Images capped at 1280 px on the long edge, GGUF weights not packaged, no child process and no
network. Report copy and report-image sharing.
No physical Galaxy S25 Ultra test was performed. Kotlin unit tests, the APK
signature/alignment/content check and host inference checks run in CI, but they do not establish
Android runtime behaviour. On the web side, the build is type-checked, the engine is compared against
the Kotlin output, and the app was driven in a headless Chromium session with a real photograph
(upload → analysis → ten sections → measurement appendix) with no console error; that is not a test
on the user's own browser or device.
FrameCritic 2.0 Offline · build 66
FrameCritic 2.2.0 · build 66
Build 66 makes the critique read like a photo essayist rather than a caption, and ships the same
engine as a browser app that needs no install.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall
the previous FrameCritic before installing (adb install -r will fail with a signature mismatch).
After installing, the header must read a build number that matches this release tag (the value after
offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK (~65 MB). The file name keeps the 2.0 lineage that
the release job has used since build 53; the app inside reportsv2.2.0.FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files,
licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs for the
pinned NIMA/MediaPipe weights.
What changed in this build
Ten critique sections, written at length. Each section is now built from a pool of
measurement-driven sentences instead of one or two. Depth sets how many: Quick four, Standard six
(the app's default), Expert nine. Because most sentences carry two clauses, a Standard section
runs about eight to twelve sentences and 350–450 Korean characters, an Expert section fourteen to
nineteen sentences and 500–640 characters — against roughly 100–150 characters in build 65. The
sections cover what the brief asked for: the whole frame's breath and the meeting of light and
colour in 종합 크리틱 (now three paragraphs); tone, the closing of both ends of the scale, highlight
roll-off, shadow detail, how light models form and whether the light reads soft or hard in 빛과 노출;
colour temperature, dominant and supporting colour, saturation restraint, the emotional air of the
palette, whether colour follows form or bleeds past it, and where the colour weight sits in 색채;
placement, horizon candidates, negative space and crowding, left-right balance, the path the eye
takes, and how the frame holds its subject in 구도; focus, surface texture, separation from the
background, edge character, evenness of detail, and whether the blur is deliberate in 선명도와 질감;
tone of the moment, human presence, stillness against a caught instant, the story the frame holds,
and what stays after you close it in 분위기와 순간; then strengths, refinements, shooting advice
(angle, distance, metering, shutter, aperture in words rather than numbers) and editing advice
(tone curve, white balance, highlight and shadow balance, noise order, crop).
The contract still holds. No section body contains a digit or one of the machine terms
(관찰되었, 관측, Laplacian, COCO, 휘도, 255, 검출, 측정, NIMA, 히스토그램, 클리핑, 분산); the Kotlin
unit test critiqueBodyCarriesNoNumbersOrMachineWords runs on every persona · level · tone · depth
combination and fails the build otherwise. Every raw value still appears in
상세 측정 데이터 및 기술적 근거. The score curve is untouched: 1 + 9·x^γ with γ = 0.40 / 0.55 /
0.85 — build 65's numbers stay valid.
New: browser app in web/. Vite + React + TypeScript + Tailwind, 100% client-side. The photo
never leaves the tab: pixels are read through Canvas and the TypeScript port of the same engine
writes the same Korean prose. web/src/lib/critic-engine.ts was verified against 4,050 Kotlin
outputs (five measurement sets × three subject sets × two NIMA results × every persona, level, tone
and depth) with zero differences in section text or score. Measurement in the browser covers the
256-bin luminance histogram, mean luminance, clipping, global and local contrast, Laplacian variance,
Sobel edge density, mean RGB, channel spread, the 12-bin hue histogram, mean saturation, horizontal
line candidates and a salience region. The browser has no NIMA or detector weights, so the aesthetic
score and the "주요 영역" box are heuristics from those measurements; the UI labels them 추정 and the
README/doc say so. npm run build runs tsc --noEmit plus the production build, and
node scripts/contract-check.cjs replays 5,400 generated sections against the same contract.
Deployment. Both vercel.json files are in the repository; importing plenuen-cpu/FrameCritic
with the root directory set to web builds with no further configuration. See
docs/VERCEL_DEPLOYMENT.md.
Version. versionCode 24, versionName 2.2.0. The header reads v2.2.0 (Build N), N being the
CI run number and therefore this release tag.
Carried over unchanged
- No manual
recycle()of any bitmap the screen draws; the image the screen renders is never handed
to MediaPipe or released by the analysis. - Every analysis stage can fail alone and hand a neutral, clearly labelled result to the next; the
report is always filled with all ten critique sections. - Crash record with the stack trace and a stage record naming the step that was running.
- Images capped at 1280 px on the long edge, GGUF weights not packaged, no child process and no
network. Report copy and report-image sharing.
No physical Galaxy S25 Ultra test was performed. Kotlin unit tests, the APK
signature/alignment/content check and host inference checks run in CI, but they do not establish
Android runtime behaviour. On the web side, the build is type-checked, the engine is compared against
the Kotlin output, and the app was driven in a headless Chromium session with a real photograph
(upload → analysis → ten sections → measurement appendix) with no console error; that is not a test
on the user's own browser or device.
FrameCritic 2.0 Offline · build 65
FrameCritic 2.1.0 · build 65
Build 65 turns the app into a one-touch reader. Pick a photo, press 사진 분석하기, and the score and a written critique appear; nothing else has to be chosen first. The critique is now written the way a photographer talks about a photograph, and every raw measurement has moved into a fold-out block at the bottom.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall the previous FrameCritic before installing (adb install -r will fail with a signature mismatch). After installing, the header must read a build number that matches this release tag (the value after offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK (~62 MB). The file name keeps the 2.0 lineage that the release job has used since build 53; the app inside reportsv2.1.0.FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files, licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs for the pinned NIMA/MediaPipe weights.
What changed in this build
One-touch screen. The photographer cards and the strictness / tone / depth chip rows are gone from the main screen. The app runs with a balanced point of view (equal weight on composition, light, colour and detail) and a mentor tone, so a first-time user has exactly one decision: which photo. After a result appears, the single button reads 다시 분석하기. The overlay chips under the photo (삼분할 · 피사체 · 수평선 후보 · 클리핑) remain, because they inspect a photo already on screen rather than setting up an analysis.
Critique written as prose. The seven report sections were rewritten around what the eye sees: the weight of a frame, whether light explains form, whether colour supports itself, whether texture invites a closer look. Sentences such as "휘도 평균 118.0/255의 중간 톤 사진입니다", "Laplacian 분산은 180.0으로…" or "COCO 범주 피사체는 검출되지 않았습니다" no longer appear in the report body; a unit test (critiqueBodyCarriesNoNumbersOrMachineWords) fails the build if any body section contains a digit or one of the machine terms (관찰되었, Laplacian, COCO, 휘도, 255, 검출, 측정, NIMA …). Sentence variants are selected from a seed computed from the measurements, so the same photo still produces the same text every time.
Measurements kept, not discarded. Everything the engine measured is still visible, one tap down, under 상세 측정 데이터 및 기술적 근거: NIMA aesthetic/technical raw scores and the ten-bin distribution, luminance average, median and percentiles, highlight and shadow clipping, global and local contrast, mean RGB, channel spread, saturation, hue spread, dominant hue share, Laplacian variance, edge density, detection count and boxes, three-way placement distance, per-axis evidence and confidence, and the scoring formula with its exponents. The same block is appended to the copied/shared report text, so the numbers travel with the prose instead of replacing it.
Pleasant score curve. The curve 1 + 9·x^γ now uses γ = 0.40 / 0.55 / 0.85 (Generous / Balanced / Strict) instead of 0.75 / 1 / 1.5. The exponent below 1 lifts the middle of the scale and leaves 1.0 and 10.0 where they were. Measured on the Balanced curve: a pre-boost 5.5 becomes 7.15, a pre-boost 6.5 becomes 7.86, a pre-boost 8.5 becomes 9.2. In practice an ordinary phone photograph with something visually right about it lands in the low-to-mid sevens, a well-made frame in the high sevens or low eighties, and the rare strong frame above nine. Strictness still orders the three levels (7.8 / 7.15 / 6.0 for the same evidence), and no score leaves 1.0–10.0.
Version. versionCode 23, versionName 2.1.0. The header reads FRAMECRITIC with v2.1.0 (Build N) · 사진을 보고, 근거를 읽다, where N is the CI run number and therefore the same N as this release tag; the model dialog repeats it.
Carried over unchanged
- No manual
recycle()of any bitmap the screen draws;SafeBitmapImage/GuardedBitmapPainterstill re-checkisRecycledimmediately before each draw, and the image the screen renders is never handed to MediaPipe or released by the analysis. - Every analysis stage (NIMA, OpenCV, MediaPipe, axis synthesis) can fail alone and hand a neutral, clearly labelled result to the next; the report is always filled with all ten critique sections.
- Crash record with the stack trace and a stage record naming the step that was running when the process died, both copyable.
- Images capped at 1280 px on the long edge, GGUF weights not packaged, no child process and no network.
- Report copy and report-image sharing.
No physical Galaxy S25 Ultra test was performed. Unit tests (ScoringTest, CriticEngineTest, PipelineFallbackTest), the APK signature/alignment/content check and host inference checks all run in CI, but they do not establish Android runtime behaviour. The scoring change is verified numerically on the JVM and the prose change is verified by the no-digits / no-machine-words contract; neither claims that the new text reads like a particular photographer.
FrameCritic 2.0 Offline · build 64
FrameCritic 2.0.2 · build 64
Build 64 removes the recycled-bitmap crash that ended the app while a photo was on screen. The stack trace from build 63 was java.lang.RuntimeException: Canvas: trying to use a recycled bitmap raised from Compose's BitmapPainter.onDraw, which means the picture the screen was drawing had already been released by the analysis code.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall the previous FrameCritic before installing (adb install -r will fail with a signature mismatch). After installing, the header must read a build number that matches this release tag (the value after offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK (65 MB / 62 MiB).FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files, licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs for the pinned NIMA/MediaPipe weights.
What changed in this build:
- MediaPipe no longer receives the displayed bitmap.
Vision.subjectsused to hand the original photo toBitmapImageBuilderand then close theMPImage. Closing an MPImage can release the AndroidBitmapit was built from, so the photo the screen was rendering was freed in the middle of an analysis. Subject detection now always works on anARGB_8888copy; only that copy is released, and the photo is untouched. - No manual
recycle()anywhere on an analysis bitmap.Pipeline.analyze(work.recycle()),Vision.metrics(source.recycle()) andNima.analyze(scaled.recycle()) no longer release bitmaps. Each of those calls could hit the very bitmap Compose was drawing —downscaledreturns the same instance when the photo is already ≤1280 px, andcreateScaledBitmapreturns the source when the size is unchanged. Bitmap memory is left to the garbage collector, which never frees an object the UI still references. OpenCVMatobjects are still released infinally; they are native and not drawn. - The screen checks before it draws.
PhotoAnalysisnow renders the photo and the clipping mask through a painter that re-readsBitmap.isRecycledimmediately before each draw and skips the frame instead of lettingandroid.graphics.Canvasthrow. The clipping-mask tap handler, the share-image renderer and the mask overlay all test the bitmap state first. - Version.
versionCode22,versionName2.0.2. The header readsFRAMECRITICwithv2.0.2 (Build N) · 사진을 보고, 근거를 읽다, where N is the CI run number and therefore the same N as this release tag; the model dialog repeats it. - Regression guard. The instrumentation test now asserts that the original bitmap is still usable after
Nima.analyze,Vision.metrics,Vision.subjectsandPipeline.analyze.
Carried over from build 63 (unchanged in behaviour):
- Every one of the four analysis stages can fail on its own and hand a neutral, clearly labelled result to the next, so a measurement error cannot end the app.
- Crash record and stage record: a previous crash shows its stack trace with a 클립보드 복사 button, and a silent process death shows the stage that was running.
- Subject detection passes the model as a direct
ByteBuffer, empty category lists are skipped, non-finite and inverted boxes are dropped. - The report always has the eleven sections; missing or non-Korean items are filled from measured values with a notice.
- Images are capped at 1280 px on the long edge, and the unused ARM64
llama-serverbinary is not packaged.
No physical Galaxy S25 Ultra test was performed. Unit tests (ScoringTest, CriticEngineTest, PipelineFallbackTest), the APK signature/alignment/content check and host inference checks all run in CI, but they do not establish Android runtime behaviour, and the reopened crash came from a path only a device exercised. If the app still exits, the start-up dialog now carries either the stack trace or the stage that was running, and both can be copied.
FrameCritic 2.0 Offline · build 63
FrameCritic 2.0.1 · build 63
Build 63 makes the analysis pipeline survive every stage. Each of the four stages can now fail on its own and hand a neutral, clearly labelled result to the next one, so a device-specific measurement error can no longer end the app. The screen also shows exactly which build is installed.
Install note: this APK is signed with the CI debug key, which differs from run to run. Uninstall the previous FrameCritic before installing (adb install -r will fail with a signature mismatch). After installing, the header must read a build number that matches this release tag (the value after offline-v2-); if it does not, the old build is still the one running.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable APK (~62 MB; the unused native server binary is no longer packaged, down from ~114 MB).FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files, licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs for the pinned NIMA/MediaPipe weights.
What changed in this build:
- Version is visible.
versionCode21,versionName2.0.1. The header readsFRAMECRITICwithv2.0.1 (Build N) · 사진을 보고, 근거를 읽다, where N is the CI run number and therefore the same N as this release tag; the model dialog repeats it. - Crash record can be copied. The start-up dialog for a previous crash now has a 클립보드 복사 button beside 닫기, so the stack trace can be pasted straight into a message.
- Stage record for silent exits. While an analysis runs, the current stage is written to
filesDir/stage.txtand deleted when the analysis finishes or fails normally. If the process is killed without a Java exception (native fault, low-memory killer), the next launch shows "직전 분석이 도중에 끊겼습니다" with the exact stage and time. - 1/4 NIMA defence.
Nima.analyzereturnsnullinstead of throwing when a TFLite interpreter cannot be built or run, and it still returns a result when only one of the two models runs.Pipelinereplaces a missing result with the neutral score (aesthetic 5.5, technical 5.0, deviation 1.0, ten 0.1 probability bins) and records the cause inwarnings. - 2/4 OpenCV defence.
Vision.metricsvalidates the bitmap size, converts hardware bitmaps, checks everyMat, treats a shortHoughLinesProw as “no line”, allocates the clip map insiderunCatching, and releases everyMatinfinally. On failurePipelinesubstitutes neutral metrics (luminance 128, zero contrast/sharpness/saturation) with a warning. - 3/4 MediaPipe defence. The detector now receives the model as a direct
ByteBuffer(setModelAssetBuffer) instead of an absolute asset path, which the asset loader cannot open — so subject detection no longer fails silently on every photo.categories().maxByOrNull { it.score() }removes theNoSuchElementExceptionon an empty category list, and non-finite or inverted boxes are skipped. - 4/4 critic defence.
safeAssessstill isolates each axis, andPipelinewraps the whole assessment in its own fallback so a failure there cannot escape. - Report always has 11 sections.
Pipeline.reportcompares the engine output with the fixed 11-section list and fills any missing, blank or non-Korean item from the measured values, with a notice that the item was simplified. An empty section map can no longer reach the screen. - Analysis resolution cap. Images larger than 1280 px on the long edge are scaled down before NIMA/OpenCV/MediaPipe, bounding the size of the pixel arrays and the clip map.
- Smaller APK. The ARM64
llama-serverbinary (~90 MB) was still packaged although no code loads it since build 58; it is no longer built into the APK.
No physical Galaxy S25 Ultra test was performed. Unit tests (ScoringTest, CriticEngineTest, PipelineFallbackTest), the APK signature/alignment/content check and host inference checks all run in CI, but they do not establish Android runtime behaviour. If the app still exits, the start-up dialog now carries either the stack trace or the stage that was running, and both can be copied.
FrameCritic 2.0 Offline · build 60
FrameCritic 2.0 · build 60
Build 60 makes the stage‑4 result screen unable to end the process. The report path was already off the native inference process, but two things could still take the app down on a real device: a rendering value that the Compose UI could not lay out (NaN or a zero/infinite size in the histogram and NIMA charts), and a half‑written state where analysis was rendered before report existed. Both paths are closed, and an uncaught exit is now recorded so a remaining crash reports its own stack trace instead of disappearing.
Downloads:
FrameCritic-2.0.0-arm64.apk: installable debug APK (~17 MB of embedded TFLite models). A previously installed debug build may need uninstalling because CI debug keys differ.FrameCritic-2.0.0-source-with-models.zip: complete Android Studio project, pinned weight files, licenses and Gradle wrapper.FrameCritic-verification.zip: Android build reports and Linux CPU inference outputs for the pinned NIMA/MediaPipe weights.
What changed in this build:
- Crash logger.
MainActivity.onCreateinstalls aThread.UncaughtExceptionHandlerthat writes the full stack trace, thread name, device, app version and memory limit tofilesDir/crash.txt. The next launch shows the record in a dialog and deletes the file when it is closed. - Rendering guards.
HistogramcomputesstrokeW = (size.width / 256f).coerceAtLeast(1f)and returns early when the canvas has no finite positive size; NIMA bar heights are precomputed withmaxPfinite/non‑zero fallback and clamped to 1–44 dp; subject boxes, line overlays and tap coordinates are skipped when any component is not finite; everyCanvasdraw block runs insiderunCatching; number formatting falls back to “—” instead of throwing; percentage rounding never callsroundToInt()on NaN. - Atomic state update.
CriticViewModel.analyzeno longer publishesanalysisbeforereportexists. Both are written in onemutable.update, so the composition never renders a half state. - Throwable coverage.
analyze()andload()catchThrowable(afterCancellationExceptionandOutOfMemoryError), andPipelinesanitises NIMA probabilities, metrics, subject boxes and line candidates to finite values before any UI or engine code sees them. - Engine fallbacks.
CriticEngine.assessisolates each axis withrunCatchingand neutralises only that axis,safeAssessalways returns anAssessment, andsectionsreturns a full 11‑section map even if prose generation fails. - NIMA normalisation. TFLite output is normalised into a 10‑bin probability distribution before
Scoring.distribution, so a malformed output can no longer throw insideNima.analyze.
No physical Galaxy S25 Ultra test was performed. The unit tests (CriticEngineTest, ScoringTest), APK signature/alignment check and host inference checks all run in CI, but they do not establish Android runtime behaviour. If the app still exits, the crash dialog after restart holds the exact stack trace to fix it from.
Note: the workflow run numbered 59 stopped at a Kotlin compile error in the build itself (a >= token split). The same commit, corrected, is this build 60; no behaviour from run 59 was ever published.