Why the phone does not translate
An iPhone can already read text in a photo, and Apple ships an on-device translator. On the day Spark was designed, those platform pieces were probed one by one, and Dad ruled on the result before a line of code existed:
온디바이스 모드는 제약이 너무 많을 거야. 아빠는 진짜 피파랑 얘기하고 싶으니까.
An on-device mode would have too many limits; Dad wants to talk with the real Pippa.
The probes backed him. Some of the languages he would meet had no on-device translation at all, and VisionKit's Live Text does not hand in-image translation to apps. The architecture records it plainly: Vision supplies the boxes, and Spark draws the replacement text. On-device translation stayed a page of research and never became a product path.
So Vision on the phone keeps exactly one job. It finds where the text is. It does not decide what the text says, which language it is in, or what it means. I do all three.
What Vision is asked for
VisionReader.lines runs a RecognizeTextRequest at the accurate level, with language correction and automatic language detection switched on. Those settings make the glyph reading better; none of their output is treated as a decision. For each observation it keeps the top candidate's string, its confidence, and its bounding box.
One line of the excerpt below is easy to skip and costly to get wrong. Vision's normalized boxes put the origin at the lower left; images and SwiftUI put it at the upper left. The reader flips the y axis once, at the boundary, with 1 - y - height, so every box Spark stores and sends is already in the photo's own frame. Nothing downstream has to remember which system a box came from.
Reading order, then a number
ordered turns a bag of boxes into lines a person would read. It sorts by vertical centre, then groups into rows: a line joins the current row when its centre sits within half the smaller line's height of the previous one. Each row is read left to right, and every line gets a number starting from 1.
That number is the most important thing Vision produces. It is the join key between three places:
- The message I receive lists the lines as
1. text,2. text, each with its share of the photo (next lesson). - Each
lineresult I return carriesn, the number of the Vision line it matches. - The phone draws my translation for
ninside boxn.
Text Vision missed still gets read. I return it without n. It appears in the list under the photo, and nothing is drawn over the picture for it, because there is no box to draw in.
I correct the reading
The instructions for a practical turn say it directly: the numbered lines only locate text, so read the photo yourself, correct anything they misread, and add what they missed. The engine's test for a photo turn uses exactly that case. Vision reports line 1 as SAL1DA, with a digit where a letter belongs. The message I receive contains 1. SAL1DA, and my result for n 1 reads Salida. The box stays Vision's; the words become mine.
On the engine side, the practical route stores the lines it is given as their own media record on the moment, a small JSON file with the role vision-lines, before the turn starts. The record is part of the corpus like everything else, so a later turn and a later reader see the same numbered lines I saw.
Vision runs on the copy I will see, never on the full camera bitmap. PreparedPhoto.prepare makes the 2048 px copy first, off the main actor, and gives that image to Vision. Because the boxes are normalized, they fit the original just as well: same picture, same proportions.
Drawing the answer back
TranslatedPhoto lays a GeometryReader over the photo. Each Vision line starts as an outline in Spark's ember colour, so Dad sees at once what will be read. When my translation for that number arrives, the outline becomes white text on a softened material patch inside the same box. OverlayGeometry sizes the type at 72 percent of the box height, never below 8 points or above 34, and keeps every box at least 8 points wide and tall so a tiny line still gets a patch. A tap on the photo toggles back to the original.
It is an approximation on purpose: low resolution, the right words in roughly the right place, available the second each line exists. A redrawn picture with the text set properly comes later through Ember, and Track 8 covers it. The web has no Vision step and no overlay. A web photo goes up with no numbered lines, and my reading arrives as a list.