diff --git a/apple/whistlegraph/README.md b/apple/whistlegraph/README.md index d9efc408dd..5316e1343c 100644 --- a/apple/whistlegraph/README.md +++ b/apple/whistlegraph/README.md @@ -63,7 +63,9 @@ Each successful version retains its combined request: transcript, ordered normal The model receives a PNG of the submitted chalk alongside vector coordinates and measured sound cues in one request. The PNG keeps the preview's aspect ratio and mark positions, with a longest side of 768 pixels. It contains only marks on white; it does not capture the app or the piece. History and recovery retain vectors and regenerate the image, including on a checked-edit repair. Old images leave the model conversation at the next turn; selected-branch text still supplies history. -Chalk is an instruction channel: the prompt asks the model to infer targeting, motion, emphasis, additions, or depicted content from words, timing, placement, and existing source. It favors focused additive edits and preserves the scene rather than automatically literalizing each mark. This is a prompting policy, not a trained gesture classifier or a measured recognition guarantee. The model receives source context, not a captured preview frame, so precise targeting of animated objects remains unresolved. Drawing bypasses text-only local shortcuts. Sampling caps the prompt at 320 points across at most 32 strokes (the native draft retains at most 1,200 points). Timings and endpoints survive sampling; fine detail can be lost. +Chalk follows **understand, reify, evolve**: interpret the idea or intended change in context, give it a concrete working form, and let later input develop it. Marks can propose concepts, relationships, targets or transformations. Tracing, literal fidelity and animation are not defaults. Existing work supplies continuity while allowing coherent transformation; captions describe only implemented behavior. This is a prompting policy, not a trained gesture classifier or a measured recognition guarantee. + +Held taps are rendered as dots even when their samples repeat the same position. Stored strokes remain independently normalized to 0..1000 on each axis. Provider evidence converts them to one aspect-correct plane with explicit width and height, then sends each stroke’s full bounds and at most six timed samples. This keeps spatial and temporal cues without inviting a verbatim point-array transcription. The PNG carries the complete sampled form; the compact observations cannot reconstruct every bend or pause. Both describe the same positions, so a single fit scale covers the full drawing without stretching or clipping. The model receives source context, not a captured preview frame, so precise targeting of animated objects remains unresolved. Drawing bypasses text-only local shortcuts. Sampling caps the prompt at 320 points across at most 32 strokes (the native draft retains at most 1,200 points). Timings and endpoints survive sampling; fine detail can be lost. Deploy the bounded PNG support in `easel-policy.mjs`, `easel-input-images.mjs`, and `easel-paid-credits.mjs` before installing this client for paid inference. The selected model must support image input. The server accepts only static inline PNGs up to 768×768 and 700,000 base64 characters, at most 16 per request. Paid holds conservatively include one token per pixel plus image overhead and serialized bytes; this can require substantially more available credit than the final cost. Actual provider usage settles the charge and refunds the unused hold. Free and paid requests use the same image validation. diff --git a/apple/whistlegraph/Resources/Web/drawing-input.mjs b/apple/whistlegraph/Resources/Web/drawing-input.mjs index d4b46175de..6cf91189ad 100644 --- a/apple/whistlegraph/Resources/Web/drawing-input.mjs +++ b/apple/whistlegraph/Resources/Web/drawing-input.mjs @@ -50,7 +50,9 @@ export function drawingImage(value,canvas=document.createElement('canvas')) { const point=p=>[p[0]/1000*canvas.width,p[1]/1000*canvas.height]; for(const stroke of drawing.strokes){ ctx.beginPath(); - if(stroke.length===1){const [x,y]=point(stroke[0]);ctx.arc(x,y,1.5,0,Math.PI*2);ctx.fill();} + // A held tap has several timestamped samples at the same position. Canvas + // does not reliably paint a zero-length stroked path, even with round caps. + if(stroke.every(p=>p[0]===stroke[0][0]&&p[1]===stroke[0][1])){const [x,y]=point(stroke[0]);ctx.arc(x,y,1.5,0,Math.PI*2);ctx.fill();} else {stroke.forEach((p,i)=>ctx[i?'lineTo':'moveTo'](...point(p)));ctx.stroke();} } const url=canvas.toDataURL('image/png'); @@ -60,12 +62,29 @@ export function drawingImage(value,canvas=document.createElement('canvas')) { export function drawingContent(text,image) { return image?[{type:'text',text:text+'\nThe attached image renders only the chalk marks on white, with their original placement and aspect ratio. It is not a frame of the piece. Read the whole shape alongside the timed strokes; do not copy the white background into the piece.'},image]:text; } +export function drawingGeometry(value) { + const {id,revision,schema,aspect,...drawing}=normalizeDrawing(value); + const round=n=>Math.round(n*1000)/1000; + // Storage normalizes each axis independently. Provider evidence instead uses + // one isotropic plane, so fitting width/height cannot stretch or crop marks. + return {...drawing,width:1000,height:round(1000/aspect), + strokes:drawing.strokes.map(stroke=>stroke.map(([x,y,...rest])=>[x,round(y/aspect),...rest]))}; +} export function drawingEvidence(value) { - const {id,revision,schema,...drawing}=normalizeDrawing(value); + const geometry=drawingGeometry(value); + // The image carries form. Supply compact spatial/temporal observations so + // source generation is not invited to transcribe a full coordinate table. + const drawing={...geometry,strokes:geometry.strokes.map(stroke=>{ + const xs=stroke.map(p=>p[0]),ys=stroke.map(p=>p[1]); + const count=Math.min(6,stroke.length); + return {bounds:[Math.min(...xs),Math.min(...ys),Math.max(...xs),Math.max(...ys)], + samples:Array.from({length:count},(_,i)=>stroke[Math.round(i*(stroke.length-1)/Math.max(1,count-1))])}; + })}; return ` DRAWING REFERENCE — CHALK GESTURES: ordered marks over the current preview, supplied as an instruction channel alongside speech, sound and the existing piece source. These are ambiguous gestures, not certain labels or commands. Infer each mark's role in context: pointing or enclosing can identify a target; a sweep can suggest movement or a transformation; repeated marks can emphasize or multiply; marks in open space can propose an addition; a drawn form can also be content when the request supports that reading. These are possibilities, not fixed gesture bindings. A circle does not automatically mean "add a circle" or "select this". Read related strokes as a phrase, not unrelated objects. -Follow explicit words first. Use the current scene and the mark's location and timing to resolve "this", "here" and "like that". Preserve existing objects, behavior and composition; prefer a focused additive or local change. Do not replace the scene, erase objects or invent an unrelated subject merely because a gesture is ambiguous. Without words, use the strongest contextual evidence and make the smallest coherent change. In an empty scene, let placement, form and motion guide a simple starting composition without assuming every stroke depicts a literal object. -Each point is [x,y,elapsedMs,optionalPencilPressure]; x/y span 0..1000, origin top-left, y downward. Preserve aspect ratio. Time starts with the first mark; pen lifts separate strokes. Pressure (0..1000) exists only when measured with a Pencil; finger pressure is unknown. speechStartMs, when present, locates sound/word time zero on this same timeline. Use direction, pauses, speed changes and nearby sound/word emphasis as evidence, not proof of intent. The source is context, not a captured frame: do not claim a precise object hit or visual recognition you cannot establish, especially in a moving scene. Do not display the coordinate data, add annotation UI, or leave the chalk overlay in the piece unless requested. +Understand, reify, evolve: infer the idea or intended change from the whole mark phrase, words, current piece and prior requests; give that idea a concrete working form in the piece; let subsequent input develop or transform it. A recognizable sketch supplies a concept and relationships, not a requirement to copy its outline or animate it. Choose a representation and behavior that make the inferred idea tangible. Use a fresh representation of the inferred concept: choose its meaningful parts, relationships and possible behavior, and express those in code. A sketch of a thing asks you to realize that thing, not produce a "chalk-outline" copy by default. Use simple geometry or meaningful state and interactions as appropriate, rather than transcribing point arrays. Tracing, animation, and literal fidelity are appropriate only when the intent calls for them. Do not add arbitrary features just to make the result seem richer. +Follow explicit words first. Use location and timing to resolve "this", "here" and "like that". Existing subjects and behavior give continuity, not a frozen template: evolve them coherently when the new input supports a change. Ambiguity alone does not authorize erasing unrelated work. Without words, make a modest concrete interpretation supported by the strongest contextual evidence, leaving room for the next gesture to steer it. In an empty scene, use the inferred concept as a starting point. Describe only behavior actually implemented in the exported caption; a painted subject need not move or react. +The image carries the complete sampled form; the following compact stroke observations support placement and timing. Each stroke supplies its full bounds [minX,minY,maxX,maxY] and at most six ordered samples (first and last always included). Samples omit fine trajectory and timing detail; do not reconstruct the drawing from them. Each sample is [x,y,elapsedMs,optionalPencilPressure] in the aspect-correct plane of the supplied width and height, origin top-left, y downward. Both axes now use the same spatial unit; do not normalize or apply aspect a second time. When spatial placement matters, fit this entire plane with one scale min(availableWidth/width,availableHeight/height), center it, and leave room for the full subject and any motion. Do not reinterpret height as 1000 or discard coordinates outside a guessed 4:3 box. Timed stationary points are intentional dots and can be meaningful features. Time starts with the first mark; pen lifts separate strokes. Pressure (0..1000) exists only when measured with a Pencil; finger pressure is unknown. speechStartMs, when present, locates sound/word time zero on this same timeline. Use direction, pauses, speed changes and nearby sound/word emphasis as evidence, not proof of intent. The source is context, not a captured frame: do not claim a precise object hit you cannot establish, especially in a moving scene. Do not display coordinate data, add annotation UI, or leave the chalk overlay in the piece unless requested. `+JSON.stringify(drawing); } diff --git a/apple/whistlegraph/Resources/Web/generation-policy.mjs b/apple/whistlegraph/Resources/Web/generation-policy.mjs index 77f4676b9c..2c621cea18 100644 --- a/apple/whistlegraph/Resources/Web/generation-policy.mjs +++ b/apple/whistlegraph/Resources/Web/generation-policy.mjs @@ -1,2 +1,2 @@ export const DEFAULT_MODEL = 'deepseek/deepseek-v4.1-flash'; -export const GENERATION_INSTRUCTIONS = 'This surface is Whistlegraph. The person watches the piece being made. A local starter may already be visible: its source is the real current piece; refine it instead of discarding it. For an existing piece, make small revision-checked edit_piece calls instead of resending unchanged code. Use write_piece to create the initial piece or for a necessary rewrite. No prose preamble. Each ask becomes one internal version; complete edit checkpoints are preview layers within it. Make the first visible working drawing in 3–6 lines whenever possible. The UI compiles runnable module prefixes during streaming: put required declarations and a complete exported paint function first; avoid long data tables or helper code before that first drawing. Then refine in small complete write_piece checkpoints only as needed to satisfy the request. Do not add unrequested animation or features. For animation use Number(paintCount) from the paint API, or explicit state advanced in sim; there is no numeric frame API. A request for a spoken narrator needs audible speak(), not just written notes; use the speech guide. Use AC ink alpha on the 0–255 scale and size geometry relative to the screen. Do not repeat the prompt or add interaction instructions, help text, usage hints, or captions inside or outside the picture unless explicitly requested. Use the default write font for other in-piece text unless the person requests another. Use ac_preview for runtime observations; this phone has no desktop frame-capture service. Once the requested change is painted and checked, finish the turn. Never fake progress or tests. Keep one line `export const caption = "…"` in the piece: a plain sentence under 120 characters naming the subject, its mood, what moves and what the person can do; update it whenever an edit changes any of that. It is the piece\'s memory of itself and comes back to you with every follow-up. Preserve the current piece and the original intent of the selected branch on revisions. Short follow-ups modify that existing world; do not replace its subject, characters, message, interactions, or sound without a request to do so. Treat uncertain speech transcriptions conservatively in context instead of inventing a new subject from an ambiguous word. Adjectives of culture, nationality or place (Latina, Japanese, Nordic, tropical) describe how things look: palette, patterns, dress, flora, light. They never change the language of any text or speech unless the request mentions words, text, language or voice. Do not turn ordinary spoken requests into text posters, transcript timelines, waveform charts, or word-timed animation unless explicitly requested. No publishing: these are private local previews.'; +export const GENERATION_INSTRUCTIONS = 'This surface is Whistlegraph. The person watches the piece being made. A local starter may already be visible: its source is the real current piece; refine it instead of discarding it. For an existing piece, make small revision-checked edit_piece calls instead of resending unchanged code. Use write_piece to create the initial piece or for a necessary rewrite. No prose preamble. Each ask becomes one internal version; complete edit checkpoints are preview layers within it. Make the first visible working drawing in 3–6 lines whenever possible. The UI compiles runnable module prefixes during streaming: put required declarations and a complete exported paint function first; avoid long data tables or helper code before that first drawing. Then refine in small complete write_piece checkpoints only as needed to satisfy the request. Do not add unrequested animation or features. For animation use Number(paintCount) from the paint API, or explicit state advanced in sim; there is no numeric frame API. A request for a spoken narrator needs audible speak(), not just written notes; use the speech guide. Use numeric RGB ink(r,g,b) or six-digit hex colors like "#ffffff"; three-digit CSS hex colors are unsupported. Use AC ink alpha on the 0–255 scale and size geometry relative to the screen. Every saved piece must have working helpers and a caption; never finish with placeholder or empty drawing helpers. line(x1,y1,x2,y2) draws ONE segment; its fifth argument is thickness, never another coordinate. For a path, loop over adjacent point pairs with separate four-argument line calls; never spread three or more point pairs into line. Use the native oval(x,y,radiusX,radiusY,filled) for ellipses instead of approximating them with overlapping circles. Fit the entire composition, including radii and stroke thickness, inside the screen with margins; use at most 90% of the fitting scale and center it. Do not repeat the prompt or add interaction instructions, help text, usage hints, or captions inside or outside the picture unless explicitly requested. Use the default write font for other in-piece text unless the person requests another. Use ac_preview for runtime observations; this phone has no desktop frame-capture service. Once the requested change is painted and checked, finish the turn. Never fake progress or tests. Keep one line `export const caption = "…"` in the piece: a plain sentence under 120 characters naming the subject, its mood, what actually moves and what the person can actually do, if anything; never invent motion or interactions in the caption. Update it whenever an edit changes any of that. It is the piece\'s memory of itself and comes back to you with every follow-up. Preserve the current piece and the original intent of the selected branch on revisions. Short follow-ups modify that existing world; do not replace its subject, characters, message, interactions, or sound without a request to do so. Treat uncertain speech transcriptions conservatively in context instead of inventing a new subject from an ambiguous word. Adjectives of culture, nationality or place (Latina, Japanese, Nordic, tropical) describe how things look: palette, patterns, dress, flora, light. They never change the language of any text or speech unless the request mentions words, text, language or voice. Do not turn ordinary spoken requests into text posters, transcript timelines, waveform charts, or word-timed animation unless explicitly requested. No publishing: these are private local previews.'; diff --git a/apple/whistlegraph/Sources/WhistlegraphDrawing.swift b/apple/whistlegraph/Sources/WhistlegraphDrawing.swift index 102f05e51e..4b4c2a0478 100644 --- a/apple/whistlegraph/Sources/WhistlegraphDrawing.swift +++ b/apple/whistlegraph/Sources/WhistlegraphDrawing.swift @@ -85,7 +85,10 @@ final class GestureInkView: UIView { let origin = CGPoint(x: first[0] / 1000 * bounds.width, y: first[1] / 1000 * bounds.height) path.move(to: origin) for p in stroke.dropFirst() { path.addLine(to: CGPoint(x: p[0] / 1000 * bounds.width, y: p[1] / 1000 * bounds.height)) } - if stroke.count == 1 { path.addLine(to: CGPoint(x: origin.x + 0.1, y: origin.y)) } + // Touch-up adds a timed sample even when a held tap never moved. + if stroke.allSatisfy({ $0[0] == first[0] && $0[1] == first[1] }) { + path.addLine(to: CGPoint(x: origin.x + 0.1, y: origin.y)) + } UIColor.black.withAlphaComponent(0.8).setStroke(); path.lineWidth = 7; path.stroke() UIColor(red: 1, green: 0.14, blue: 1, alpha: 1).setStroke(); path.lineWidth = 4; path.stroke() } diff --git a/apple/whistlegraph/Tests/drawing-input.test.mjs b/apple/whistlegraph/Tests/drawing-input.test.mjs index 785dc13a0d..f610fa2c76 100644 --- a/apple/whistlegraph/Tests/drawing-input.test.mjs +++ b/apple/whistlegraph/Tests/drawing-input.test.mjs @@ -1,6 +1,6 @@ import test from 'node:test'; import assert from 'node:assert/strict'; -import {normalizeDrawing,withDrawing,inputData,drawingEvidence} from '../Resources/Web/drawing-input.mjs'; +import {normalizeDrawing,withDrawing,inputData,drawingEvidence,drawingGeometry} from '../Resources/Web/drawing-input.mjs'; import {inferenceRequest} from '../Resources/Web/inference-input.mjs'; import {musicalPrompt} from '../Resources/Web/musical-input.mjs'; import {selectedBranch} from '../Resources/Web/branch-context.mjs'; @@ -36,3 +36,30 @@ test('selected branch descriptions retain the presence of drawings without raw c const context=selectedBranch({head:1,versions:[{id:0,parent:null,source:''},{id:1,parent:0,request:withDrawing('a bird',sketch),source:''}]}); assert.equal(context.history[0].request,'a bird [with drawing]'); }); +test('provider geometry fits every edge with one scale across portrait, square and landscape',()=>{ + for(const aspect of [.25,.5,1,1.333,4]){ + const original={...sketch,aspect,strokes:[[[0,0,0],[1000,1000,200,900]]]}; + const evidence=drawingGeometry(original); + assert.equal(evidence.width,1000);assert.ok(Math.abs(evidence.width/evidence.height-aspect)<.00001); + assert.deepEqual(evidence.strokes[0].at(-1),[evidence.width,evidence.height,200,900]); + for(const [w,h] of [[600,450],[300,600]]){ + const scale=Math.min(w/evidence.width,h/evidence.height); + for(const [x,y] of evidence.strokes.flat()){ + assert.ok(x*scale<=w+.001);assert.ok(y*scale<=h+.001); + } + } + assert.deepEqual(original.strokes[0].at(-1),[1000,1000,200,900],'stored coordinates stay normalized'); + assert.equal(evidence.speechStartMs,sketch.speechStartMs); + } +}); +test('compact model observations retain full bounds, endpoints, dot timing and alignment',()=>{ + const points=Array.from({length:30},(_,i)=>[i*10,i===7?1000:200,i*100]); + const input={...sketch,aspect:2,strokes:[points,[[390,580,4000],[390,580,4300]]]}; + const evidence=JSON.parse(drawingEvidence(input).split('\n').at(-1)); + assert.equal(evidence.strokes[0].samples.length,6); + assert.deepEqual(evidence.strokes[0].bounds,[0,100,290,500],'bounds include extrema omitted from sample trajectory'); + assert.deepEqual(evidence.strokes[0].samples[0],[0,100,0]); + assert.deepEqual(evidence.strokes[0].samples.at(-1),[290,100,2900]); + assert.deepEqual(evidence.strokes[1].samples,[[390,290,4000],[390,290,4300]]); + assert.equal(evidence.speechStartMs,-500); +}); diff --git a/apple/whistlegraph/Tests/fixtures/chalk-rabbit.json b/apple/whistlegraph/Tests/fixtures/chalk-rabbit.json new file mode 100644 index 0000000000..37725f7a52 --- /dev/null +++ b/apple/whistlegraph/Tests/fixtures/chalk-rabbit.json @@ -0,0 +1 @@ +{"schema": "whistlegraph-drawing/v1", "id": "11111111-1111-4111-8111-111111111111", "revision": 1, "aspect": 1.333, "strokes": [[[314, 576, 0], [292, 552, 98], [288, 538, 114], [276, 515, 131], [243, 462, 164], [229, 431, 181], [214, 403, 198], [202, 373, 214], [190, 342, 231], [180, 310, 248], [172, 281, 264], [167, 250, 281], [163, 198, 314], [163, 176, 331], [163, 156, 348], [168, 142, 364], [173, 131, 381], [181, 122, 398], [189, 114, 414], [203, 102, 448], [210, 99, 464], [218, 94, 481], [228, 88, 498], [238, 84, 515], [250, 81, 531], [260, 80, 548], [281, 80, 581], [291, 87, 598], [300, 97, 615], [310, 108, 631], [318, 122, 648], [327, 135, 665], [336, 152, 681], [344, 174, 698], [359, 222, 731], [366, 244, 748], [372, 267, 765], [379, 291, 782], [385, 310, 798], [390, 330, 815], [394, 348, 831], [401, 381, 865], [402, 395, 882], [404, 409, 898], [406, 420, 915], [407, 430, 932], [408, 440, 948], [408, 448, 965], [408, 457, 982], [409, 467, 1015], [410, 470, 1032], [410, 469, 1082], [410, 463, 1098], [410, 450, 1115], [410, 433, 1132], [410, 409, 1148], [410, 329, 1182], [410, 279, 1199], [412, 229, 1215], [415, 185, 1232], [420, 142, 1248], [424, 109, 1265], [427, 91, 1282], [430, 79, 1299], [437, 52, 1332], [442, 37, 1349], [447, 22, 1365], [451, 13, 1382], [454, 8, 1399], [457, 6, 1415], [462, 6, 1432], [471, 7, 1465], [476, 10, 1482], [483, 19, 1499], [492, 30, 1515], [501, 48, 1532], [512, 73, 1549], [522, 102, 1565], [540, 165, 1599], [548, 201, 1616], [554, 236, 1632], [558, 272, 1649], [562, 307, 1665], [564, 337, 1682], [565, 365, 1699], [565, 391, 1715], [565, 426, 1749], [565, 437, 1765], [564, 449, 1782], [562, 462, 1799], [558, 472, 1816], [555, 480, 1832], [553, 487, 1849], [549, 497, 1882], [545, 501, 1916], [543, 506, 1949], [541, 509, 2016], [539, 512, 2066], [537, 515, 2116], [534, 519, 2166], [532, 522, 2216], [531, 524, 2450], [536, 524, 2483], [543, 524, 2500], [550, 524, 2516], [557, 524, 2533], [565, 528, 2550], [571, 535, 2566], [581, 550, 2599], [586, 558, 2616], [592, 567, 2633], [595, 577, 2650], [599, 585, 2666], [603, 593, 2683], [606, 602, 2700], [609, 610, 2716], [610, 634, 2750], [610, 650, 2766], [610, 666, 2783], [606, 686, 2800], [597, 708, 2816], [586, 731, 2833], [576, 753, 2850], [555, 783, 2883], [545, 791, 2900], [532, 793, 2916], [518, 794, 2933], [506, 794, 2950], [494, 794, 2966], [483, 794, 2983], [458, 794, 3016], [447, 794, 3033], [435, 794, 3050], [424, 793, 3067], [414, 792, 3083], [403, 790, 3100], [392, 787, 3117], [380, 785, 3133], [357, 770, 3167], [346, 758, 3183], [334, 744, 3200], [323, 730, 3217], [312, 715, 3233], [302, 699, 3250], [293, 685, 3267], [285, 650, 3300], [285, 634, 3317], [285, 620, 3333], [285, 608, 3350], [286, 600, 3367], [291, 593, 3383], [299, 586, 3400], [306, 580, 3417], [316, 573, 3450], [319, 572, 3467], [322, 571, 3484], [325, 569, 3484]], [[447, 666, 3787], [455, 645, 3984], [455, 648, 4001], [458, 648, 4034], [445, 649, 4101], [435, 649, 4117], [429, 648, 4134], [425, 642, 4151], [422, 638, 4167], [422, 634, 4184], [422, 630, 4201], [430, 622, 4234], [435, 621, 4251], [439, 621, 4267], [442, 621, 4284], [445, 621, 4301], [446, 623, 4351], [446, 626, 4368], [446, 633, 4434], [446, 638, 4468], [446, 644, 4484], [446, 651, 4501], [446, 658, 4518], [446, 665, 4534], [446, 672, 4551], [446, 678, 4568], [439, 686, 4601], [434, 688, 4618], [428, 691, 4634], [422, 692, 4651], [416, 693, 4668], [410, 693, 4684], [404, 693, 4701], [392, 695, 4734], [387, 697, 4751], [382, 697, 4768], [377, 705, 4802]], [[453, 651, 5172], [454, 692, 5285], [462, 698, 5302], [472, 708, 5319], [493, 722, 5352], [501, 727, 5368], [510, 728, 5385], [518, 728, 5402], [527, 728, 5418], [533, 723, 5435], [537, 717, 5452], [539, 713, 5468], [540, 700, 5502], [540, 697, 5518], [540, 694, 5535], [540, 690, 5568], [539, 683, 5585]], [[390, 580, 5805], [390, 580, 6119]], [[472, 553, 6439], [472, 553, 6653]], [[410, 687, 6890], [420, 728, 7037], [422, 733, 7053], [429, 741, 7086], [433, 742, 7103], [441, 742, 7136], [447, 742, 7153], [453, 737, 7170], [461, 729, 7186], [473, 716, 7220], [476, 714, 7236], [480, 712, 7270], [485, 706, 7270]], [[637, 666, 7573], [640, 664, 7654], [646, 660, 7670], [663, 650, 7671], [725, 624, 7704], [754, 622, 7720], [778, 622, 7737], [798, 630, 7754], [812, 652, 7770], [818, 676, 7787], [823, 731, 7820], [821, 764, 7837], [810, 793, 7854], [796, 815, 7870], [779, 831, 7887], [763, 843, 7904], [743, 848, 7920], [709, 848, 7954], [695, 848, 7970], [683, 848, 7987], [671, 848, 8004], [659, 848, 8020], [648, 848, 8037], [627, 848, 8070], [617, 848, 8087], [606, 848, 8104], [593, 848, 8120], [579, 848, 8137], [565, 847, 8154], [548, 845, 8171], [514, 841, 8204], [499, 838, 8221], [486, 836, 8237], [474, 834, 8254], [465, 830, 8271], [458, 828, 8287], [448, 827, 8321], [444, 827, 8337], [444, 824, 8371], [444, 815, 8387]], [[802, 603, 8707], [790, 592, 8821], [778, 562, 8838], [764, 513, 8871], [764, 499, 8888], [765, 490, 8904], [776, 478, 8921], [793, 464, 8938], [813, 450, 8954], [844, 445, 8988], [854, 445, 9005], [860, 455, 9021], [866, 474, 9038], [870, 498, 9055], [872, 522, 9071], [870, 584, 9105], [860, 607, 9121], [849, 619, 9138], [837, 626, 9155], [822, 627, 9171], [809, 627, 9188], [791, 627, 9221], [787, 626, 9238], [784, 624, 9255], [778, 617, 9255]], [[576, 851, 9509], [560, 885, 9605], [554, 901, 9622], [528, 940, 9655], [517, 955, 9672], [511, 965, 9688], [508, 971, 9705], [519, 958, 9772], [529, 950, 9789], [534, 943, 9789]], [[764, 814, 10225], [764, 822, 10339], [763, 843, 10356], [764, 863, 10389], [768, 872, 10406], [771, 880, 10422], [775, 886, 10439], [777, 891, 10456], [777, 895, 10472], [777, 903, 10506], [778, 906, 10522], [781, 909, 10539], [787, 917, 10556]]]} diff --git a/apple/whistlegraph/Tests/native-bridge.test.cjs b/apple/whistlegraph/Tests/native-bridge.test.cjs index f39f3ed9cb..3bb1eb1c30 100644 --- a/apple/whistlegraph/Tests/native-bridge.test.cjs +++ b/apple/whistlegraph/Tests/native-bridge.test.cjs @@ -124,6 +124,19 @@ const server = http.createServer(async (req, res) => { const {imageInputBound}=await import('../../../system/backend/easel-input-images.mjs'); assert.ok(imageInputBound({messages:[{role:'user',content:[rendered.block]}]})>768*576,'real browser PNG passes hosted bounds'); if(process.env.CHALK_IMAGE_OUT)await require('node:fs/promises').writeFile(process.env.CHALK_IMAGE_OUT,Buffer.from(rendered.block.source.data,'base64')); + const rabbit=JSON.parse(await readFile(resolve(__dirname,'fixtures/chalk-rabbit.json'),'utf8')); + const dots=await page.evaluate(async drawing=>{ + const {drawingImage}=await import('./drawing-input.mjs'); + const canvas=document.createElement('canvas'),block=drawingImage(drawing,canvas); + const ctx=canvas.getContext('2d'); + const dark=(x,y)=>{const p=ctx.getImageData(Math.floor(x/1000*canvas.width)-2,Math.floor(y/1000*canvas.height)-2,5,5).data;return [...p].some((v,i)=>i%4===0&&v<128);}; + const eyes=[dark(390,580),dark(472,553)],empty=dark(50,950); + drawingImage({...drawing,strokes:[[[500,500,0]]]},canvas); + return {block,eyes,empty,single:dark(500,500)}; + },rabbit); + assert.deepEqual(dots.eyes,[true,true],'the rabbit\'s timed stationary strokes remain visible eyes'); + assert.ok(dots.single&&!dots.empty,'single-point taps paint without fabricating marks elsewhere'); + if(process.env.CHALK_RABBIT_OUT)await require('node:fs/promises').writeFile(process.env.CHALK_RABBIT_OUT,Buffer.from(dots.block.source.data,'base64')); assert.deepEqual(errors,[]);console.log('PASS: typed and spoken gesture input, aligned sound, per-version attachment, and failed-draft retention');return; } if(process.argv.includes('--checked-edits')) { diff --git a/apple/whistlegraph/project.yml b/apple/whistlegraph/project.yml index 7021c922f2..b65ca3ee01 100644 --- a/apple/whistlegraph/project.yml +++ b/apple/whistlegraph/project.yml @@ -9,7 +9,7 @@ settings: CODE_SIGN_STYLE: Automatic SWIFT_VERSION: "5.0" MARKETING_VERSION: "0.1.0" - CURRENT_PROJECT_VERSION: "90" + CURRENT_PROJECT_VERSION: "91" targets: Whistlegraph: type: application