diff --git a/.gitignore b/.gitignore index bc305d9..800c375 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ scans/ *.mp4 # the one exception: a 3-second synthetic test pattern (ffmpeg testsrc) the demo page tests play; no home in it !apps/web/e2e/fixtures/test-clip.mp4 +# the one screen recording the author chose to publish; every other recording stays out +!apps/web/public/demo-video/watch.mp4 # the sample room's photo colours stay local (the viewer hides "Photo" when the file is missing) # your own scan for /viewer (make viewer); it never leaves your computer diff --git a/README.md b/README.md index cbaf427..ba968e0 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,8 @@ come from the planner the phone uses, on the scan's floor. ## Screenshots **Real output, from the author's own living room:** one walk with an iPhone, rebuilt on a Mac. The room map shows -floor, furniture and walls as cells, and the things as dots: no photos of the home are in this repo. +floor, furniture and walls as cells, and the things as dots. The scan holds no photos of the home; the only footage in this repo is one short screen +recording and one frame of it, which the author reviewed and chose to publish (`apps/web/public/demo-video/`). @@ -509,7 +510,8 @@ Every route except `/health` needs a key: the owner's device key, or a robot's o - **The published demo scan:** the author's own living room is published for `/homes/flat/viewer`, reviewed and approved by the author: - points and surfaces tinted by height, the room model, the drawings, labels and robot files; - - no photos, video, depth images, keys or paths; guard tests fail if a photo file or photo colours ever appear. + - no photos, video, depth images, keys or paths in the scan files; guard tests fail if a photo file or photo colours ever appear; + - separately, one reviewed screen recording of the AR view and one frame of it in `apps/web/public/demo-video/`, chosen by the author. - the no-keys demo (`samples/replay/flat/`) holds the same scan's room model and labels, its point cloud thinned to one point per 5 cm (positions only, no colours) and the path the phone walked (camera poses only, no times or compass readings), with the recorded answers; a test checks there is nothing else. diff --git a/apps/web/components/viewer/PhonePanel.tsx b/apps/web/components/viewer/PhonePanel.tsx index 13afd8f..f1278c8 100644 --- a/apps/web/components/viewer/PhonePanel.tsx +++ b/apps/web/components/viewer/PhonePanel.tsx @@ -63,54 +63,52 @@ function Frame({ children, label }: { children: ReactNode; label: string }) { } /** - * The AR camera view, drawn: no photo of the home goes on the public page, so the room is a floor grid. The chevrons - * lean the way the clock-face step says; the thing's tag grows as the walk nears it, as the labels do in the camera. + * The AR camera view, drawn as a plain room: no photo of the home is needed for it. The floor chevrons lean the way the + * clock-face step says; the thing's tag grows as the walk nears it; on arrival the app's blue arrow points down at it. */ function ArScene({ thing, hour, progress, before, there }: { thing: string; hour: number; progress: number; before: boolean; there: boolean }) { const lean = ((hour % 12 > 6 ? hour % 12 - 12 : hour % 12) * 18).toFixed(0); const name = thing.toUpperCase(); - const scale = there ? 1.2 : 0.65 + progress * 0.4; + const scale = there ? 1.15 : 0.65 + progress * 0.35; const width = name.length * 8.6 + 26; return ( - + - - + + - - + + - - - - {[-200, -110, -40, 40, 110, 200, 320, -320].map((x) => ( - - ))} - {[166, 190, 226, 276].map((y) => ( - + + + + + {[-260, -170, -90, -30, 30, 90, 170, 260].map((x) => ( + ))} {!before && !there && ( - + {[ - { y: 286, s: 1, o: 1 }, - { y: 244, s: 0.74, o: 0.8 }, - { y: 212, s: 0.54, o: 0.6 }, - { y: 190, s: 0.38, o: 0.4 }, + { y: 310, s: 1, o: 0.95 }, + { y: 268, s: 0.74, o: 0.8 }, + { y: 236, s: 0.54, o: 0.65 }, + { y: 212, s: 0.38, o: 0.5 }, ].map((c) => ( - + ))} )} - - - {there && } - - - + + + {there && } + + + {name} @@ -120,16 +118,25 @@ function ArScene({ thing, hour, progress, before, there }: { thing: string; hour ); } +/** The app's direction dial: a white face with 12, 3, 6, 9, and a blue hand on the hour, or a green tick on arrival. */ function ClockFace({ hour, done }: { hour: number; done?: boolean }) { return ( - - + + + {[12, 3, 6, 9].map((h) => { + const a = (h % 12) * (Math.PI / 6); + return ( + + {h} + + ); + })} {done ? ( - + ) : ( <> - - + + )} @@ -137,9 +144,9 @@ function ClockFace({ hour, done }: { hour: number; done?: boolean }) { } /** - * The guidance screen as the app lays it out (the arrow, then a card with the clock face, the thing and the step), - * drawn in HTML for the walks no one recorded. `step` is the step being spoken: -1 before the walk, steps.length - * once there. + * The guidance screen as the app lays it out (the camera with its labels, the card with the dial and the step, then + * Repeat and Stop or Done), drawn in HTML for the walks no one recorded. `step` is the step being spoken: -1 before the + * walk, steps.length once there. */ export function SimulatedPhone({ thing, heard, steps, arrive, step }: { thing: string; heard?: string; steps: { clock: number; say: string }[]; arrive: string; step: number }) { const before = step < 0; @@ -148,35 +155,60 @@ export function SimulatedPhone({ thing, heard, steps, arrive, step }: { thing: s const progress = there ? 1 : Math.max(step + 1, 0) / Math.max(steps.length, 1); return (
+
-
- {format(t.walkingTo, { thing })} - {t.done} +
+ 9:41 + + + {[3, 5, 7, 9].map((h) => ( + + ))} + +
+
+ {format(t.walkingTo, { thing })} + {t.done}
-
+
{t.camera} {t.edges}
- {before &&

{t.startHere}

} + {before &&

{t.startHere}

} {heard && ( -

“{heard}”

+

“{heard}”

)}
-
-
+
+
-

{thing.charAt(0).toUpperCase() + thing.slice(1)}

-

{before ? t.takeMeThere : there ? arrive : now.say}

+

{thing.charAt(0).toUpperCase() + thing.slice(1)}

+

{before ? t.takeMeThere : there ? arrive : now.say}

-
- {t.repeat} - {there ? t.done : t.stop} +
+ + + + + + {t.repeat} + + + {there ? ( + + + + ) : ( + + )} + {there ? t.done : t.stop} +
+
); diff --git a/apps/web/e2e/demo.spec.ts b/apps/web/e2e/demo.spec.ts index 381234b..8d13063 100644 --- a/apps/web/e2e/demo.spec.ts +++ b/apps/web/e2e/demo.spec.ts @@ -204,3 +204,16 @@ test("the viewer shows what was measured in real use, says what it isn't, and re await expect(page.locator("[data-ar-mode]")).toBeVisible(); await expect(page.locator("[data-hear-room]")).toHaveText(en.app.hearRoom.hear); }); + +test("the published recording plays on the hosted page, and the scan recording is never published", async ({ page }) => { + await page.unrouteAll(); + await page.route("**/local-video/**", (route) => route.fulfill({ status: 404, body: "" })); // as on the hosted site + await open(page, PAGE); + await expect(page.locator("[data-recorded-phone='watch']")).toBeVisible(); + await expect(page.locator("[data-recorded-chip]")).toHaveCount(1); + await expect(page.getByText(t.phone.localOnly)).toHaveCount(0); + const published = JSON.parse(fs.readFileSync(path.join(__dirname, "../public/demo-video/clips.json"), "utf8")); + expect(published.source).toBe("published"); + expect(published.clips.map((c: { id: string }) => c.id).sort()).toEqual(["ar-labels", "watch"]); + expect(fs.readdirSync(path.join(__dirname, "../public/demo-video")).sort()).toEqual(["ar-labels.jpg", "clips.json", "watch.mp4"]); +}); diff --git a/apps/web/i18n/dictionaries/en.ts b/apps/web/i18n/dictionaries/en.ts index 2200314..6651f2f 100644 --- a/apps/web/i18n/dictionaries/en.ts +++ b/apps/web/i18n/dictionaries/en.ts @@ -427,7 +427,7 @@ export const en = { }, { q: "Is there a real scan I can see?", - a: "Yes. The author's own living room, scanned with an iPhone and rebuilt by Meta's models, is on this site in 3D: geometry only, no photos. The memory feed higher up this page shows the kind of answers Recall gives, and is labelled as an illustration.", + a: "Yes. The author's own living room, scanned with an iPhone and rebuilt by Meta's models, is on this site in 3D: the scan is geometry only, with no photos, and the demo page adds one short screen recording the author chose to publish. The memory feed higher up this page shows the kind of answers Recall gives, and is labelled as an illustration.", links: [ { text: "See the actual scan", href: "/homes/flat/viewer" }, { text: "The code on GitHub", href: "https://github.com/coder-RT/recall" }, @@ -533,7 +533,7 @@ export const en = { { title: "People in view", body: "Scan when nobody else is in the room. Anyone the camera sees is in the scan photos that go to SAM 3.1 with the rest; Recall never looks for people or faces, and robots never get photos." }, ], siteTitle: "This website", - site: "No cookies and no analytics. It keeps two small settings in your browser: Reduce motion, and that you saw the storage note. Vercel Speed Insights measures page speed without cookies or anything personal. “Ask by voice” sends your recording through our server to Muse Voice Transcribe and doesn't store it. The contact form sends your name, email and message through our server to Rachit's inbox, using the email service Resend, and Vercel BotID checks invisibly that a browser sent it; the site keeps none of it. The demo shows recorded output of a real run, not your data.", + site: "No cookies and no analytics. It keeps two small settings in your browser: Reduce motion, and that you saw the storage note. Vercel Speed Insights measures page speed without cookies or anything personal. “Ask by voice” sends your recording through our server to Muse Voice Transcribe and doesn't store it. The contact form sends your name, email and message through our server to Rachit's inbox, using the email service Resend, and Vercel BotID checks invisibly that a browser sent it; the site keeps none of it. The demo shows recorded output of a real run, not your data, and one screen recording of the author's own room that the author chose to publish.", source: "The same note is in the iPhone app, under Settings → Privacy, where your Mac also shows what it sends right now.", }, @@ -672,7 +672,7 @@ export const en = { badge: "Real scan · not a simulation", tryIt: "Actual scan · try it", tryHint: "Drag to turn the room, pick a view, or choose a thing in the list. Dots and Architect read best.", - body: "The author's own living room, walked once with an iPhone 16 Pro on {date}, then rebuilt and searched by Meta models on a Mac. Geometry and labels only: no photo of the room is published, and every colour you see comes from height.", + body: "The author's own living room, walked once with an iPhone 16 Pro on {date}, then rebuilt and searched by Meta models on a Mac. This scan is geometry and labels only, with every colour coming from height. The demo page adds one real screen recording, which the author chose to publish.", }, story: { models: { @@ -784,10 +784,10 @@ export const en = { lead: "One guide, step by step, for you or for your AI coding agent: the website in 5 minutes, the no-keys demo in 10, your own room with your own keys in about 45.", download: "Download INSTALL.md", }, - privacy: "Published from {src}: points, surfaces, drawings, labels and robot files. No photos, no video, nothing anyone said.", + privacy: "Published from {src}: points, surfaces, drawings, labels and robot files. No scan photos, no video, nothing anyone said.", privacyTitle: "Public data only", privacyItems: ["3D points (colour from height)", "Surfaces and the room model", "Drawings", "Labels of furniture and things", "Robot files"], - privacyNever: "Never published: photos, video, depth images, anything anyone said, keys or paths.", + privacyNever: "Never in these files: scan photos, video, depth images, anything anyone said, keys or paths. The demo page's one screen recording is separate, and the author chose to publish it.", }, }, arMode: { @@ -848,7 +848,7 @@ export const en = { recordedNote: "Recorded on {date} with a newer scan of this room, so places can differ a little from this page's scan. The route on the map is planned from where this page's scan began.", localOnly: "On this computer only (make demo-clips): not on the hosted site.", recordingBadge: "Real recording", - recordingHint: "A real recording of a walk is on this computer:", + recordingHint: "Watch a real recording of a walk:", recordingAsk: "play “{q}”", showRecording: "Real recording", showSimulated: "Simulated screen", diff --git a/apps/web/public/demo-video/ar-labels.jpg b/apps/web/public/demo-video/ar-labels.jpg new file mode 100644 index 0000000..a80098f Binary files /dev/null and b/apps/web/public/demo-video/ar-labels.jpg differ diff --git a/apps/web/public/demo-video/clips.json b/apps/web/public/demo-video/clips.json new file mode 100644 index 0000000..2c0fae8 --- /dev/null +++ b/apps/web/public/demo-video/clips.json @@ -0,0 +1,53 @@ +{ + "source": "published", + "clips": [ + { + "id": "watch", + "kind": "find", + "thing": "watch", + "title": "Finding the watch by voice, in AR", + "recorded": "2026-11-04", + "walk": [ + 18, + 25 + ], + "captions": [ + [ + 0, + "The labels float over the room. Tap Speak and ask out loud." + ], + [ + 2, + "You ask: “Hey Recall, where's my watch?”" + ], + [ + 11, + "Recall: it's on the cabinet in the living room, on shelf 1, at waist height, seen 37 minutes ago." + ], + [ + 18, + "Straight ahead, 1 metre; at 1 o'clock, 90 centimetres…" + ], + [ + 25, + "Your watch should be in front of you. The camera checks it's really there." + ], + [ + 30, + "Found it: at 12 o'clock, 40 centimetres away." + ] + ], + "file": "watch.mp4", + "seconds": 35 + }, + { + "id": "ar-labels", + "kind": "image", + "title": "The room through the iPhone, with a label on everything Recall found", + "recorded": "2026-11-04", + "file": "ar-labels.jpg", + "captions": [], + "seconds": 0 + } + ] +} \ No newline at end of file diff --git a/apps/web/public/demo-video/watch.mp4 b/apps/web/public/demo-video/watch.mp4 new file mode 100644 index 0000000..33ba657 Binary files /dev/null and b/apps/web/public/demo-video/watch.mp4 differ diff --git a/docs/privacy.md b/docs/privacy.md index 994fd56..ea5fb7b 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -57,4 +57,4 @@ note). Vercel Speed Insights measures page speed without cookies or anything per recording through our server to Muse Voice Transcribe and doesn't store it. The contact form sends your name, email and message through our server to Rachit's inbox, using the email service Resend, and Vercel BotID checks invisibly that a browser sent it; the site keeps none of it. The demo shows **recorded output of a real -run**, not your data. +run**, not your data, and one screen recording of the author's own room that the author chose to publish.