From e3b216150b25282b1edce0095f1b6b14ce851cf3 Mon Sep 17 00:00:00 2001 From: Rachit Date: Mon, 2 Nov 2026 10:26:08 +0100 Subject: [PATCH 01/30] fix(viewer): furniture boxes follow the room's walls and stay inside them The real scan's walls run at about 42 degrees, but each furniture box was a world-axis min/max box, so anything against a wall cut through it (up to 29 cm). Boxes are now fitted in the room's own frame from points inside the room, clipped to it, and drawn turned (centre, size, yaw). A wall that RANSAC found twice (its face and a plane in front of it) becomes one wall on its outer line: the published scan goes from 6 wall sheets to 3. Re-exported real-scan/flat from the same episode; the point and mesh files are byte-identical. Co-Authored-By: Claude Opus 5.5 --- apps/web/components/viewer/Layers.tsx | 32 +- apps/web/components/viewer/RoomViews.tsx | 36 +- apps/web/public/real-scan/flat/architect.json | 2 +- apps/web/public/real-scan/flat/meta.json | 607 +++++++++++++----- apps/web/public/real-scan/flat/sheet-a1-1.svg | 354 +++++----- apps/web/public/real-scan/flat/sheet-a2-1.svg | 122 ++-- .../pipeline/scripts/export_room_views.py | 18 +- .../pipeline/scripts/export_viewer_scan.py | 13 +- .../recall_pipeline/domain/architecture.py | 22 + .../src/recall_pipeline/domain/room_model.py | 59 +- services/pipeline/tests/test_architecture.py | 20 + .../pipeline/tests/unit/test_lift_and_room.py | 18 + 12 files changed, 843 insertions(+), 460 deletions(-) diff --git a/apps/web/components/viewer/Layers.tsx b/apps/web/components/viewer/Layers.tsx index 2503ef8..d649837 100644 --- a/apps/web/components/viewer/Layers.tsx +++ b/apps/web/components/viewer/Layers.tsx @@ -100,8 +100,30 @@ export interface RoomShape { export interface FurnitureBox { label: string; + /** world bounds (of the turned box, when it is turned) */ min: number[]; max: number[]; + /** newer exports: the box fitted in the room's own frame, so it sits square to the walls and inside them */ + centre?: number[]; + size?: number[]; + /** radians about z */ + yaw?: number; +} + +export interface Placement { + centre: number[]; + size: number[]; + yaw: number; +} + +/** Where a furniture box goes: turned with the walls when the export says so, else axis-aligned from min/max. */ +export function placement(f: FurnitureBox): Placement { + if (f.centre && f.size) return { centre: f.centre, size: f.size, yaw: f.yaw ?? 0 }; + return { + centre: [0, 1, 2].map((k) => (f.min[k] + f.max[k]) / 2), + size: [0, 1, 2].map((k) => f.max[k] - f.min[k]), + yaw: 0, + }; } export interface Thing { @@ -164,17 +186,17 @@ export function Furniture({ items, tokens }: { items: FurnitureBox[]; tokens: To return ( <> {items.map((f, i) => ( - + ))} ); } -function BoxOutline({ min, max, colour }: { min: number[]; max: number[]; colour: string }) { - const size = [max[0] - min[0], max[1] - min[1], max[2] - min[2]]; - const edges = useMemo(() => new EdgesGeometry(new BoxGeometry(size[0], size[1], size[2])), [size[0], size[1], size[2]]); // eslint-disable-line react-hooks/exhaustive-deps +function BoxOutline({ at, colour }: { at: Placement; colour: string }) { + const [sx, sy, sz] = at.size; + const edges = useMemo(() => new EdgesGeometry(new BoxGeometry(sx, sy, sz)), [sx, sy, sz]); return ( - + ); diff --git a/apps/web/components/viewer/RoomViews.tsx b/apps/web/components/viewer/RoomViews.tsx index 6dbc3ab..eb7fcfa 100644 --- a/apps/web/components/viewer/RoomViews.tsx +++ b/apps/web/components/viewer/RoomViews.tsx @@ -2,7 +2,7 @@ import { useMemo } from "react"; import { BoxGeometry, BufferAttribute, BufferGeometry, Color, EdgesGeometry, PlaneGeometry, Shape, ShapeGeometry, SRGBColorSpace, TextureLoader, type Texture } from "three"; -import { NorthArrow, type FurnitureBox, type RoomShape, type Thing, type Tokens } from "./Layers"; +import { NorthArrow, placement, type FurnitureBox, type Placement, type RoomShape, type Thing, type Tokens } from "./Layers"; /** One colour per kind of furniture, like a semantic map (a legend in the controls names them). */ export const KIND_COLOURS: Record = { @@ -73,29 +73,18 @@ export function RoomModel({ {showFurniture && furniture.map((f, i) => ( ))} {things.map((t, i) => { + // a thing's size is measured along the world axes (min/max), so its box stays square to them const s = (t.size ?? [0.12, 0.12, 0.12]).map((v) => Math.max(v, 0.05)); - const min = [ - t.at[0] - s[0] / 2, - t.at[1] - s[1] / 2, - t.at[2] - s[2] / 2, - ]; - const max = [ - t.at[0] + s[0] / 2, - t.at[1] + s[1] / 2, - t.at[2] + s[2] / 2, - ]; return ( new BoxGeometry(sx, sy, sz), [sx, sy, sz]); const edges = useMemo(() => new EdgesGeometry(box), [box]); return ( - + diff --git a/apps/web/public/real-scan/flat/architect.json b/apps/web/public/real-scan/flat/architect.json index 4733e09..3ab72ac 100644 --- a/apps/web/public/real-scan/flat/architect.json +++ b/apps/web/public/real-scan/flat/architect.json @@ -1 +1 @@ -{"theta": 0.71763, "height": 2.56, "plan": [[2.516, 3.512], [2.516, -1.201], [1.486, -1.201], [1.486, -2.789], [0.201, -2.789], [0.201, -3.292], [-0.409, -3.292], [-0.409, -3.866], [-0.947, -3.866], [-0.947, 3.512]], "walls": [{"a": [-0.9469787076763554, 3.5122991567577926], "b": [-0.9469787076763554, -3.8657099844335736], "scanned": true, "openings": []}, {"a": [-0.9469787076763554, -3.8657099844335736], "b": [-0.4094579917333656, -3.8657099844335736], "scanned": false, "openings": []}, {"a": [-0.4094579917333656, -3.8657099844335736], "b": [-0.4094579917333656, -3.292468363517795], "scanned": false, "openings": []}, {"a": [-0.4094579917333656, -3.292468363517795], "b": [0.20062604188007974, -3.292468363517795], "scanned": false, "openings": []}, {"a": [0.20062604188007974, -3.292468363517795], "b": [0.20062604188007974, -2.7890317382380223], "scanned": false, "openings": []}, {"a": [0.20062604188007974, -2.7890317382380223], "b": [1.4858472187939866, -2.7890317382380223], "scanned": false, "openings": []}, {"a": [1.4858472187939866, -2.7890317382380223], "b": [1.4858472187939866, -1.2011460494682078], "scanned": false, "openings": []}, {"a": [1.4858472187939866, -1.2011460494682078], "b": [2.5156848654094923, -1.2011460494682078], "scanned": false, "openings": []}, {"a": [2.5156848654094923, -1.2011460494682078], "b": [2.5156848654094923, 3.5122991567577926], "scanned": true, "openings": []}, {"a": [2.5156848654094923, 3.5122991567577926], "b": [-0.9469787076763554, 3.5122991567577926], "scanned": true, "openings": []}], "wall_t": 0.12, "floor_colour": "#dfe3ee", "wall_colour": "#8697b6", "furniture": [{"label": "table", "colour": "#97a6c1", "lo": [0.313, -2.04, 0.757], "hi": [1.048, -1.319, 0.856], "parts": [{"kind": "top", "c": [0.68, -1.679, 0.841], "s": [0.734, 0.722, 0.03]}, {"kind": "leg", "c": [0.368, -1.985, 0.413], "s": [0.05, 0.05, 0.826]}, {"kind": "leg", "c": [0.368, -1.374, 0.413], "s": [0.05, 0.05, 0.826]}, {"kind": "leg", "c": [0.993, -1.985, 0.413], "s": [0.05, 0.05, 0.826]}, {"kind": "leg", "c": [0.993, -1.374, 0.413], "s": [0.05, 0.05, 0.826]}]}, {"label": "desk", "colour": "#b0bbd1", "lo": [1.833, 0.279, 0.0], "hi": [2.421, 1.228, 0.876], "parts": [{"kind": "top", "c": [2.127, 0.753, 0.861], "s": [0.588, 0.949, 0.03]}, {"kind": "leg", "c": [1.888, 0.334, 0.423], "s": [0.05, 0.05, 0.846]}, {"kind": "leg", "c": [1.888, 1.173, 0.423], "s": [0.05, 0.05, 0.846]}, {"kind": "leg", "c": [2.366, 0.334, 0.423], "s": [0.05, 0.05, 0.846]}, {"kind": "leg", "c": [2.366, 1.173, 0.423], "s": [0.05, 0.05, 0.846]}]}, {"label": "desk", "colour": "#92a1be", "lo": [-0.926, -1.233, 0.547], "hi": [-0.372, -0.28, 1.307], "parts": [{"kind": "top", "c": [-0.649, -0.756, 1.292], "s": [0.554, 0.953, 0.03]}, {"kind": "leg", "c": [-0.871, -1.178, 0.638], "s": [0.05, 0.05, 1.277]}, {"kind": "leg", "c": [-0.871, -0.335, 0.638], "s": [0.05, 0.05, 1.277]}, {"kind": "leg", "c": [-0.427, -1.178, 0.638], "s": [0.05, 0.05, 1.277]}, {"kind": "leg", "c": [-0.427, -0.335, 0.638], "s": [0.05, 0.05, 1.277]}]}, {"label": "desk", "colour": "#a3b0c9", "lo": [1.922, 1.344, 0.577], "hi": [2.26, 1.675, 0.825], "parts": [{"kind": "top", "c": [2.091, 1.51, 0.81], "s": [0.339, 0.332, 0.03]}, {"kind": "leg", "c": [1.977, 1.399, 0.397], "s": [0.05, 0.05, 0.795]}, {"kind": "leg", "c": [1.977, 1.62, 0.397], "s": [0.05, 0.05, 0.795]}, {"kind": "leg", "c": [2.205, 1.399, 0.397], "s": [0.05, 0.05, 0.795]}, {"kind": "leg", "c": [2.205, 1.62, 0.397], "s": [0.05, 0.05, 0.795]}]}, {"label": "table", "colour": "#d8ddea", "lo": [2.26, 2.115, 0.021], "hi": [2.36, 2.244, 0.136], "parts": [{"kind": "top", "c": [2.31, 2.179, 0.121], "s": [0.1, 0.129, 0.03]}, {"kind": "leg", "c": [2.315, 2.17, 0.053], "s": [0.05, 0.05, 0.106]}, {"kind": "leg", "c": [2.315, 2.189, 0.053], "s": [0.05, 0.05, 0.106]}, {"kind": "leg", "c": [2.305, 2.17, 0.053], "s": [0.05, 0.05, 0.106]}, {"kind": "leg", "c": [2.305, 2.189, 0.053], "s": [0.05, 0.05, 0.106]}]}, {"label": "desk", "colour": "#9caac4", "lo": [1.661, 2.536, 0.457], "hi": [2.112, 2.916, 0.894], "parts": [{"kind": "top", "c": [1.886, 2.726, 0.879], "s": [0.452, 0.38, 0.03]}, {"kind": "leg", "c": [1.716, 2.591, 0.432], "s": [0.05, 0.05, 0.864]}, {"kind": "leg", "c": [1.716, 2.861, 0.432], "s": [0.05, 0.05, 0.864]}, {"kind": "leg", "c": [2.057, 2.591, 0.432], "s": [0.05, 0.05, 0.864]}, {"kind": "leg", "c": [2.057, 2.861, 0.432], "s": [0.05, 0.05, 0.864]}]}, {"label": "desk", "colour": "#91a1be", "lo": [-0.349, 2.293, 0.849], "hi": [0.274, 2.791, 0.889], "parts": [{"kind": "top", "c": [-0.038, 2.542, 0.874], "s": [0.623, 0.499, 0.03]}, {"kind": "leg", "c": [-0.294, 2.348, 0.43], "s": [0.05, 0.05, 0.859]}, {"kind": "leg", "c": [-0.294, 2.736, 0.43], "s": [0.05, 0.05, 0.859]}, {"kind": "leg", "c": [0.219, 2.348, 0.43], "s": [0.05, 0.05, 0.859]}, {"kind": "leg", "c": [0.219, 2.736, 0.43], "s": [0.05, 0.05, 0.859]}]}, {"label": "tv", "colour": "#7286aa", "lo": [2.206, 0.852, 0.995], "hi": [2.376, 1.403, 1.422], "parts": [{"kind": "back", "c": [2.351, 1.128, 1.208], "s": [0.05, 0.551, 0.427]}]}, {"label": "tv", "colour": "#7c8fb0", "lo": [-0.872, -0.772, 0.923], "hi": [-0.494, -0.367, 1.264], "parts": [{"kind": "back", "c": [-0.847, -0.57, 1.094], "s": [0.05, 0.405, 0.341]}]}, {"label": "tv", "colour": "#95a4c0", "lo": [-0.59, -0.335, 0.761], "hi": [-0.491, -0.242, 0.836], "parts": [{"kind": "back", "c": [-0.565, -0.288, 0.798], "s": [0.05, 0.093, 0.075]}]}, {"label": "bed", "colour": "#d9deea", "lo": [-0.17, -0.337, 0.0], "hi": [1.059, 0.455, 0.73], "parts": [{"kind": "base", "c": [0.445, 0.059, 0.15], "s": [1.229, 0.792, 0.3]}, {"kind": "mattress", "c": [0.445, 0.059, 0.4], "s": [1.169, 0.732, 0.2]}, {"kind": "headboard", "c": [0.445, -0.307, 0.45], "s": [1.229, 0.06, 0.9]}]}, {"label": "bed", "colour": "#7c8fb0", "lo": [1.097, 3.014, 0.637], "hi": [1.927, 3.436, 1.653], "parts": [{"kind": "base", "c": [1.512, 3.225, 0.15], "s": [0.83, 0.422, 0.3]}, {"kind": "mattress", "c": [1.512, 3.225, 0.4], "s": [0.77, 0.362, 0.2]}, {"kind": "headboard", "c": [1.512, 3.406, 0.826], "s": [0.83, 0.06, 1.653]}]}, {"label": "chair", "colour": "#b1bcd2", "lo": [1.625, -0.612, 0.027], "hi": [2.34, 0.055, 0.937], "parts": [{"kind": "seat", "c": [1.983, -0.278, 0.43], "s": [0.715, 0.666, 0.04]}, {"kind": "back", "c": [2.315, -0.278, 0.468], "s": [0.05, 0.666, 0.937]}, {"kind": "leg", "c": [1.675, -0.562, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.675, 0.005, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.29, -0.562, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.29, 0.005, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#98a7c2", "lo": [0.987, -2.26, 0.744], "hi": [1.309, -1.917, 0.85], "parts": [{"kind": "seat", "c": [1.148, -2.088, 0.43], "s": [0.322, 0.343, 0.04]}, {"kind": "back", "c": [1.148, -2.235, 0.425], "s": [0.322, 0.05, 0.85]}, {"kind": "leg", "c": [1.037, -2.21, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.037, -1.967, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.259, -2.21, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.259, -1.967, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#b9a58a", "lo": [1.452, 0.944, -0.118], "hi": [1.857, 1.367, -0.088], "parts": [{"kind": "seat", "c": [1.654, 1.156, 0.07], "s": [0.404, 0.423, 0.04]}, {"kind": "back", "c": [1.832, 1.156, -0.044], "s": [0.05, 0.423, 0.01]}, {"kind": "leg", "c": [1.502, 0.994, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [1.502, 1.317, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [1.807, 0.994, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [1.807, 1.317, 0.025], "s": [0.04, 0.04, 0.05]}]}, {"label": "chair", "colour": "#b4bed3", "lo": [-0.865, -0.153, 0.416], "hi": [-0.419, 0.284, 0.796], "parts": [{"kind": "seat", "c": [-0.642, 0.065, 0.43], "s": [0.446, 0.437, 0.04]}, {"kind": "back", "c": [-0.84, 0.065, 0.398], "s": [0.05, 0.437, 0.796]}, {"kind": "leg", "c": [-0.815, -0.103, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.815, 0.234, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.469, -0.103, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.469, 0.234, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#a4b1c9", "lo": [2.327, 0.035, 0.418], "hi": [2.457, 0.219, 0.908], "parts": [{"kind": "seat", "c": [2.392, 0.127, 0.43], "s": [0.13, 0.185, 0.04]}, {"kind": "back", "c": [2.432, 0.127, 0.454], "s": [0.05, 0.185, 0.908]}, {"kind": "leg", "c": [2.377, 0.085, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.377, 0.169, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.407, 0.085, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.407, 0.169, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#9eacc6", "lo": [1.604, 2.847, 0.524], "hi": [1.739, 2.961, 0.873], "parts": [{"kind": "seat", "c": [1.671, 2.904, 0.43], "s": [0.134, 0.114, 0.04]}, {"kind": "back", "c": [1.671, 2.936, 0.436], "s": [0.134, 0.05, 0.873]}, {"kind": "leg", "c": [1.654, 2.897, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.654, 2.911, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.689, 2.897, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.689, 2.911, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#9caac5", "lo": [1.638, 3.101, 0.595], "hi": [1.792, 3.232, 0.795], "parts": [{"kind": "seat", "c": [1.715, 3.166, 0.43], "s": [0.154, 0.131, 0.04]}, {"kind": "back", "c": [1.715, 3.207, 0.398], "s": [0.154, 0.05, 0.795]}, {"kind": "leg", "c": [1.688, 3.151, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.688, 3.182, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.742, 3.151, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.742, 3.182, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#90a0bd", "lo": [0.324, 2.936, 0.751], "hi": [0.616, 3.14, 1.001], "parts": [{"kind": "seat", "c": [0.47, 3.038, 0.43], "s": [0.292, 0.203, 0.04]}, {"kind": "back", "c": [0.47, 3.115, 0.5], "s": [0.292, 0.05, 1.001]}, {"kind": "leg", "c": [0.374, 2.986, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [0.374, 3.09, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [0.566, 2.986, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [0.566, 3.09, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#97a6c1", "lo": [-0.76, 2.786, 0.501], "hi": [-0.01, 3.403, 1.015], "parts": [{"kind": "seat", "c": [-0.385, 3.095, 0.43], "s": [0.749, 0.618, 0.04]}, {"kind": "back", "c": [-0.385, 3.378, 0.507], "s": [0.749, 0.05, 1.015]}, {"kind": "leg", "c": [-0.71, 2.836, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.71, 3.353, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.06, 2.836, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.06, 3.353, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#dfe3ee", "lo": [-0.907, 2.14, 0.0], "hi": [-0.713, 2.224, 0.003], "parts": [{"kind": "seat", "c": [-0.81, 2.182, 0.07], "s": [0.194, 0.084, 0.04]}, {"kind": "back", "c": [-0.81, 2.199, 0.002], "s": [0.194, 0.05, 0.01]}, {"kind": "leg", "c": [-0.857, 2.19, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [-0.857, 2.174, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [-0.763, 2.19, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [-0.763, 2.174, 0.025], "s": [0.04, 0.04, 0.05]}]}, {"label": "chair", "colour": "#aab5cd", "lo": [1.935, -1.158, 0.39], "hi": [2.356, -0.853, 0.755], "parts": [{"kind": "seat", "c": [2.146, -1.006, 0.43], "s": [0.421, 0.305, 0.04]}, {"kind": "back", "c": [2.331, -1.006, 0.378], "s": [0.05, 0.305, 0.755]}, {"kind": "leg", "c": [1.985, -1.108, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.985, -0.903, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.306, -1.108, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.306, -0.903, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "cabinet", "colour": "#b6c1d5", "lo": [1.838, 0.296, 0.005], "hi": [2.386, 1.097, 0.764], "parts": [{"kind": "box", "c": [2.112, 0.696, 0.382], "s": [0.548, 0.802, 0.764]}]}, {"label": "sofa", "colour": "#92a2be", "lo": [-0.888, 0.339, 0.536], "hi": [-0.724, 0.723, 1.075], "parts": [{"kind": "base", "c": [-0.806, 0.531, 0.21], "s": [0.164, 0.384, 0.42]}, {"kind": "back", "c": [-0.788, 0.531, 0.538], "s": [0.2, 0.384, 1.075]}, {"kind": "arm", "c": [-0.806, 0.414, 0.31], "s": [0.164, 0.15, 0.62]}, {"kind": "arm", "c": [-0.806, 0.648, 0.31], "s": [0.164, 0.15, 0.62]}]}, {"label": "sofa", "colour": "#7f91b3", "lo": [1.474, -1.131, 0.627], "hi": [2.236, -0.951, 1.595], "parts": [{"kind": "base", "c": [1.855, -1.041, 0.21], "s": [0.762, 0.18, 0.42]}, {"kind": "back", "c": [1.855, -1.031, 0.798], "s": [0.762, 0.2, 1.595]}, {"kind": "arm", "c": [1.549, -1.041, 0.31], "s": [0.15, 0.18, 0.62]}, {"kind": "arm", "c": [2.161, -1.041, 0.31], "s": [0.15, 0.18, 0.62]}]}, {"label": "shelf", "colour": "#667ca3", "lo": [2.051, 2.778, 1.173], "hi": [2.282, 3.163, 1.469], "parts": [{"kind": "side", "c": [2.166, 2.788, 1.321], "s": [0.231, 0.02, 0.296]}, {"kind": "side", "c": [2.166, 3.153, 1.321], "s": [0.231, 0.02, 0.296]}, {"kind": "board", "c": [2.166, 2.97, 1.183], "s": [0.231, 0.384, 0.02]}, {"kind": "board", "c": [2.166, 2.97, 1.459], "s": [0.231, 0.384, 0.02]}]}, {"label": "shelf", "colour": "#93a3bf", "lo": [1.669, 2.655, 0.709], "hi": [2.089, 3.034, 0.992], "parts": [{"kind": "side", "c": [1.679, 2.845, 0.85], "s": [0.02, 0.379, 0.283]}, {"kind": "side", "c": [2.079, 2.845, 0.85], "s": [0.02, 0.379, 0.283]}, {"kind": "board", "c": [1.879, 2.845, 0.719], "s": [0.42, 0.379, 0.02]}, {"kind": "board", "c": [1.879, 2.845, 0.982], "s": [0.42, 0.379, 0.02]}]}], "things": [{"n": 1, "label": "laptop", "color": "grey", "at": [0.64, -1.952, 0.794], "size": [0.22, 0.219, 0.024]}, {"n": 2, "label": "watch", "color": "grey", "at": [2.078, 1.048, 0.763], "size": [0.064, 0.071, 0.016]}, {"n": 3, "label": "remote control", "color": "grey", "at": [0.551, -1.556, 0.782], "size": [0.105, 0.318, 0.063]}, {"n": 4, "label": "bottle", "color": "grey", "at": [2.378, 2.071, 0.341], "size": [0.025, 0.174, 0.09]}, {"n": 5, "label": "mug", "color": "blue", "at": [0.677, -1.67, 0.851], "size": [0.075, 0.155, 0.09]}, {"n": 6, "label": "bottle", "color": "grey", "at": [2.017, 1.762, 0.724], "size": [0.055, 0.095, 0.11]}, {"n": 7, "label": "remote control", "color": "white", "at": [0.086, 2.466, 0.857], "size": [0.074, 0.062, 0.016]}, {"n": 8, "label": "book", "color": "grey", "at": [-0.494, 0.08, 0.443], "size": [0.187, 0.136, 0.011]}, {"n": 9, "label": "book", "color": "white", "at": [-0.287, 2.672, 0.875], "size": [0.25, 0.273, 0.03]}, {"n": 10, "label": "bottle", "color": "grey", "at": [2.045, 0.563, 0.736], "size": [0.04, 0.092, 0.112]}, {"n": 11, "label": "laptop", "color": "black", "at": [-0.631, -0.966, 0.774], "size": [0.262, 0.289, 0.14]}, {"n": 12, "label": "wallet", "color": "grey", "at": [1.966, 1.274, 0.664], "size": [0.101, 0.055, 0.022]}, {"n": 13, "label": "book", "color": "grey", "at": [0.045, 2.657, 0.878], "size": [0.113, 0.145, 0.04]}, {"n": 14, "label": "bag", "color": "grey", "at": [0.302, 3.157, 0.975], "size": [0.457, 0.252, 0.212]}, {"n": 15, "label": "glasses", "color": "red", "at": [2.098, 1.571, 0.651], "size": [0.111, 0.156, 0.087]}, {"n": 16, "label": "book", "color": "beige", "at": [-0.85, 0.748, 0.94], "size": [0.094, 0.144, 0.047]}, {"n": 17, "label": "charger", "color": "beige", "at": [-0.75, -1.504, 0.916], "size": [0.058, 0.071, 0.038]}, {"n": 18, "label": "bottle", "color": "grey", "at": [1.919, 2.668, 0.812], "size": [0.1, 0.09, 0.069]}, {"n": 19, "label": "book", "color": "red", "at": [1.997, 1.472, 0.656], "size": [0.155, 0.162, 0.045]}, {"n": 20, "label": "book", "color": "grey", "at": [0.851, -1.433, 0.797], "size": [0.084, 0.059, 0.008]}, {"n": 21, "label": "passport", "color": "grey", "at": [1.966, 1.274, 0.664], "size": [0.101, 0.055, 0.022]}, {"n": 22, "label": "bottle", "color": "grey", "at": [-0.736, -1.231, 0.766], "size": [0.155, 0.165, 0.096]}, {"n": 23, "label": "keys", "color": "brown", "at": [2.171, 0.541, 0.658], "size": [0.048, 0.035, 0.01]}, {"n": 24, "label": "mug", "color": "brown", "at": [2.041, -0.411, 0.499], "size": [0.1, 0.185, 0.128]}]} +{"theta": 0.73435, "height": 2.56, "plan": [[2.521, 3.518], [2.521, -1.258], [1.393, -1.258], [1.393, -2.781], [0.033, -2.781], [0.033, -3.664], [-1.009, -3.664], [-1.009, 3.518]], "walls": [{"a": [-1.0094424804328892, 3.5184772775817925], "b": [-1.0094424804328892, -3.663947303793406], "scanned": true, "openings": []}, {"a": [-1.0094424804328892, -3.663947303793406], "b": [0.03276191884012894, -3.663947303793406], "scanned": false, "openings": []}, {"a": [0.03276191884012894, -3.663947303793406], "b": [0.03276191884012894, -2.7814009230552963], "scanned": false, "openings": []}, {"a": [0.03276191884012894, -2.7814009230552963], "b": [1.3929259379069256, -2.7814009230552963], "scanned": false, "openings": []}, {"a": [1.3929259379069256, -2.7814009230552963], "b": [1.3929259379069256, -1.2580852698787488], "scanned": false, "openings": []}, {"a": [1.3929259379069256, -1.2580852698787488], "b": [2.520819228804687, -1.2580852698787488], "scanned": false, "openings": []}, {"a": [2.520819228804687, -1.2580852698787488], "b": [2.520819228804687, 3.5184772775817925], "scanned": true, "openings": []}, {"a": [2.520819228804687, 3.5184772775817925], "b": [-1.0094424804328892, 3.5184772775817925], "scanned": true, "openings": []}], "wall_t": 0.12, "floor_colour": "#dfe3ee", "wall_colour": "#8697b6", "furniture": [{"label": "table", "colour": "#97a6c1", "lo": [0.284, -2.05, 0.757], "hi": [1.019, -1.33, 0.856], "parts": [{"kind": "top", "c": [0.652, -1.69, 0.841], "s": [0.735, 0.721, 0.03]}, {"kind": "leg", "c": [0.339, -1.995, 0.413], "s": [0.05, 0.05, 0.826]}, {"kind": "leg", "c": [0.339, -1.385, 0.413], "s": [0.05, 0.05, 0.826]}, {"kind": "leg", "c": [0.964, -1.995, 0.413], "s": [0.05, 0.05, 0.826]}, {"kind": "leg", "c": [0.964, -1.385, 0.413], "s": [0.05, 0.05, 0.826]}]}, {"label": "desk", "colour": "#b0bbd1", "lo": [1.843, 0.244, 0.0], "hi": [2.429, 1.194, 0.876], "parts": [{"kind": "top", "c": [2.136, 0.719, 0.861], "s": [0.586, 0.95, 0.03]}, {"kind": "leg", "c": [1.898, 0.299, 0.423], "s": [0.05, 0.05, 0.846]}, {"kind": "leg", "c": [1.898, 1.139, 0.423], "s": [0.05, 0.05, 0.846]}, {"kind": "leg", "c": [2.374, 0.299, 0.423], "s": [0.05, 0.05, 0.846]}, {"kind": "leg", "c": [2.374, 1.139, 0.423], "s": [0.05, 0.05, 0.846]}]}, {"label": "desk", "colour": "#92a1be", "lo": [-0.943, -1.218, 0.547], "hi": [-0.381, -0.273, 1.307], "parts": [{"kind": "top", "c": [-0.662, -0.746, 1.292], "s": [0.562, 0.945, 0.03]}, {"kind": "leg", "c": [-0.888, -1.163, 0.638], "s": [0.05, 0.05, 1.277]}, {"kind": "leg", "c": [-0.888, -0.328, 0.638], "s": [0.05, 0.05, 1.277]}, {"kind": "leg", "c": [-0.436, -1.163, 0.638], "s": [0.05, 0.05, 1.277]}, {"kind": "leg", "c": [-0.436, -0.328, 0.638], "s": [0.05, 0.05, 1.277]}]}, {"label": "desk", "colour": "#a3b0c9", "lo": [1.948, 1.307, 0.577], "hi": [2.284, 1.642, 0.825], "parts": [{"kind": "top", "c": [2.116, 1.475, 0.81], "s": [0.335, 0.335, 0.03]}, {"kind": "leg", "c": [2.003, 1.362, 0.397], "s": [0.05, 0.05, 0.795]}, {"kind": "leg", "c": [2.003, 1.587, 0.397], "s": [0.05, 0.05, 0.795]}, {"kind": "leg", "c": [2.229, 1.362, 0.397], "s": [0.05, 0.05, 0.795]}, {"kind": "leg", "c": [2.229, 1.587, 0.397], "s": [0.05, 0.05, 0.795]}]}, {"label": "table", "colour": "#d8ddea", "lo": [2.297, 2.075, 0.021], "hi": [2.396, 2.205, 0.136], "parts": [{"kind": "top", "c": [2.346, 2.14, 0.121], "s": [0.098, 0.131, 0.03]}, {"kind": "leg", "c": [2.352, 2.13, 0.053], "s": [0.05, 0.05, 0.106]}, {"kind": "leg", "c": [2.352, 2.15, 0.053], "s": [0.05, 0.05, 0.106]}, {"kind": "leg", "c": [2.341, 2.13, 0.053], "s": [0.05, 0.05, 0.106]}, {"kind": "leg", "c": [2.341, 2.15, 0.053], "s": [0.05, 0.05, 0.106]}]}, {"label": "desk", "colour": "#9caac4", "lo": [1.708, 2.502, 0.457], "hi": [2.156, 2.887, 0.894], "parts": [{"kind": "top", "c": [1.932, 2.694, 0.879], "s": [0.447, 0.385, 0.03]}, {"kind": "leg", "c": [1.763, 2.557, 0.432], "s": [0.05, 0.05, 0.864]}, {"kind": "leg", "c": [1.763, 2.832, 0.432], "s": [0.05, 0.05, 0.864]}, {"kind": "leg", "c": [2.101, 2.557, 0.432], "s": [0.05, 0.05, 0.864]}, {"kind": "leg", "c": [2.101, 2.832, 0.432], "s": [0.05, 0.05, 0.864]}]}, {"label": "desk", "colour": "#91a1be", "lo": [-0.306, 2.293, 0.849], "hi": [0.32, 2.788, 0.889], "parts": [{"kind": "top", "c": [0.007, 2.54, 0.874], "s": [0.626, 0.496, 0.03]}, {"kind": "leg", "c": [-0.251, 2.348, 0.43], "s": [0.05, 0.05, 0.859]}, {"kind": "leg", "c": [-0.251, 2.733, 0.43], "s": [0.05, 0.05, 0.859]}, {"kind": "leg", "c": [0.265, 2.348, 0.43], "s": [0.05, 0.05, 0.859]}, {"kind": "leg", "c": [0.265, 2.733, 0.43], "s": [0.05, 0.05, 0.859]}]}, {"label": "tv", "colour": "#7286aa", "lo": [2.225, 0.814, 0.995], "hi": [2.393, 1.365, 1.422], "parts": [{"kind": "back", "c": [2.368, 1.089, 1.208], "s": [0.05, 0.552, 0.427]}]}, {"label": "tv", "colour": "#7c8fb0", "lo": [-0.885, -0.758, 0.923], "hi": [-0.5, -0.359, 1.264], "parts": [{"kind": "back", "c": [-0.86, -0.558, 1.094], "s": [0.05, 0.399, 0.341]}]}, {"label": "tv", "colour": "#95a4c0", "lo": [-0.594, -0.326, 0.761], "hi": [-0.496, -0.232, 0.836], "parts": [{"kind": "back", "c": [-0.569, -0.279, 0.798], "s": [0.05, 0.094, 0.075]}]}, {"label": "bed", "colour": "#d9deea", "lo": [-0.168, -0.343, 0.0], "hi": [1.064, 0.449, 0.73], "parts": [{"kind": "base", "c": [0.448, 0.053, 0.15], "s": [1.232, 0.793, 0.3]}, {"kind": "mattress", "c": [0.448, 0.053, 0.4], "s": [1.172, 0.733, 0.2]}, {"kind": "headboard", "c": [0.448, -0.313, 0.45], "s": [1.232, 0.06, 0.9]}]}, {"label": "bed", "colour": "#7c8fb0", "lo": [1.152, 2.988, 0.637], "hi": [1.983, 3.405, 1.653], "parts": [{"kind": "base", "c": [1.567, 3.196, 0.15], "s": [0.832, 0.417, 0.3]}, {"kind": "mattress", "c": [1.567, 3.196, 0.4], "s": [0.772, 0.357, 0.2]}, {"kind": "headboard", "c": [1.567, 3.375, 0.826], "s": [0.832, 0.06, 1.653]}]}, {"label": "chair", "colour": "#b1bcd2", "lo": [1.62, -0.645, 0.027], "hi": [2.337, 0.022, 0.937], "parts": [{"kind": "seat", "c": [1.978, -0.311, 0.43], "s": [0.716, 0.667, 0.04]}, {"kind": "back", "c": [2.312, -0.311, 0.468], "s": [0.05, 0.667, 0.937]}, {"kind": "leg", "c": [1.67, -0.595, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.67, -0.028, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.287, -0.595, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.287, -0.028, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#98a7c2", "lo": [0.952, -2.278, 0.744], "hi": [1.275, -1.936, 0.85], "parts": [{"kind": "seat", "c": [1.113, -2.107, 0.43], "s": [0.323, 0.342, 0.04]}, {"kind": "back", "c": [1.113, -2.253, 0.425], "s": [0.323, 0.05, 0.85]}, {"kind": "leg", "c": [1.002, -2.228, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.002, -1.986, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.225, -2.228, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.225, -1.986, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#b9a58a", "lo": [1.47, 0.917, -0.118], "hi": [1.877, 1.338, -0.088], "parts": [{"kind": "seat", "c": [1.674, 1.128, 0.07], "s": [0.407, 0.421, 0.04]}, {"kind": "back", "c": [1.852, 1.128, -0.044], "s": [0.05, 0.421, 0.01]}, {"kind": "leg", "c": [1.52, 0.967, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [1.52, 1.288, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [1.827, 0.967, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [1.827, 1.288, 0.025], "s": [0.04, 0.04, 0.05]}]}, {"label": "chair", "colour": "#b4bed3", "lo": [-0.863, -0.144, 0.416], "hi": [-0.418, 0.296, 0.796], "parts": [{"kind": "seat", "c": [-0.64, 0.076, 0.43], "s": [0.445, 0.439, 0.04]}, {"kind": "back", "c": [-0.838, 0.076, 0.398], "s": [0.05, 0.439, 0.796]}, {"kind": "leg", "c": [-0.813, -0.094, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.813, 0.246, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.468, -0.094, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.468, 0.246, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#a4b1c9", "lo": [2.328, -0.005, 0.418], "hi": [2.459, 0.179, 0.908], "parts": [{"kind": "seat", "c": [2.394, 0.087, 0.43], "s": [0.131, 0.185, 0.04]}, {"kind": "back", "c": [2.434, 0.087, 0.454], "s": [0.05, 0.185, 0.908]}, {"kind": "leg", "c": [2.378, 0.045, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.378, 0.129, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.409, 0.045, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.409, 0.129, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#9eacc6", "lo": [1.653, 2.818, 0.524], "hi": [1.787, 2.933, 0.873], "parts": [{"kind": "seat", "c": [1.72, 2.876, 0.43], "s": [0.134, 0.115, 0.04]}, {"kind": "back", "c": [1.72, 2.908, 0.436], "s": [0.134, 0.05, 0.873]}, {"kind": "leg", "c": [1.703, 2.868, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.703, 2.883, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.737, 2.868, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.737, 2.883, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#9caac5", "lo": [1.691, 3.071, 0.595], "hi": [1.845, 3.203, 0.795], "parts": [{"kind": "seat", "c": [1.768, 3.137, 0.43], "s": [0.154, 0.131, 0.04]}, {"kind": "back", "c": [1.768, 3.178, 0.398], "s": [0.154, 0.05, 0.795]}, {"kind": "leg", "c": [1.741, 3.121, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.741, 3.153, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.795, 3.121, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.795, 3.153, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#90a0bd", "lo": [0.374, 2.929, 0.751], "hi": [0.668, 3.129, 1.001], "parts": [{"kind": "seat", "c": [0.521, 3.029, 0.43], "s": [0.294, 0.2, 0.04]}, {"kind": "back", "c": [0.521, 3.104, 0.5], "s": [0.294, 0.05, 1.001]}, {"kind": "leg", "c": [0.424, 2.979, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [0.424, 3.079, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [0.618, 2.979, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [0.618, 3.079, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#97a6c1", "lo": [-0.709, 2.794, 0.501], "hi": [0.045, 3.409, 1.015], "parts": [{"kind": "seat", "c": [-0.332, 3.101, 0.43], "s": [0.754, 0.615, 0.04]}, {"kind": "back", "c": [-0.332, 3.384, 0.507], "s": [0.754, 0.05, 1.015]}, {"kind": "leg", "c": [-0.659, 2.844, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.659, 3.359, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.005, 2.844, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [-0.005, 3.359, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "chair", "colour": "#dfe3ee", "lo": [-0.87, 2.153, 0.0], "hi": [-0.676, 2.236, 0.003], "parts": [{"kind": "seat", "c": [-0.773, 2.195, 0.07], "s": [0.194, 0.083, 0.04]}, {"kind": "back", "c": [-0.773, 2.211, 0.002], "s": [0.194, 0.05, 0.01]}, {"kind": "leg", "c": [-0.82, 2.203, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [-0.82, 2.186, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [-0.726, 2.203, 0.025], "s": [0.04, 0.04, 0.05]}, {"kind": "leg", "c": [-0.726, 2.186, 0.025], "s": [0.04, 0.04, 0.05]}]}, {"label": "chair", "colour": "#aab5cd", "lo": [1.917, -1.194, 0.39], "hi": [2.34, -0.891, 0.755], "parts": [{"kind": "seat", "c": [2.128, -1.042, 0.43], "s": [0.423, 0.304, 0.04]}, {"kind": "back", "c": [2.315, -1.042, 0.378], "s": [0.05, 0.304, 0.755]}, {"kind": "leg", "c": [1.967, -1.144, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [1.967, -0.941, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.29, -1.144, 0.205], "s": [0.04, 0.04, 0.41]}, {"kind": "leg", "c": [2.29, -0.941, 0.205], "s": [0.04, 0.04, 0.41]}]}, {"label": "cabinet", "colour": "#b6c1d5", "lo": [1.847, 0.259, 0.005], "hi": [2.395, 1.064, 0.764], "parts": [{"kind": "box", "c": [2.121, 0.662, 0.382], "s": [0.548, 0.805, 0.764]}]}, {"label": "sofa", "colour": "#92a2be", "lo": [-0.879, 0.353, 0.536], "hi": [-0.718, 0.738, 1.075], "parts": [{"kind": "base", "c": [-0.798, 0.546, 0.21], "s": [0.161, 0.384, 0.42]}, {"kind": "back", "c": [-0.779, 0.546, 0.538], "s": [0.2, 0.384, 1.075]}, {"kind": "arm", "c": [-0.798, 0.428, 0.31], "s": [0.161, 0.15, 0.62]}, {"kind": "arm", "c": [-0.798, 0.663, 0.31], "s": [0.161, 0.15, 0.62]}]}, {"label": "sofa", "colour": "#7f91b3", "lo": [1.456, -1.164, 0.627], "hi": [2.218, -0.981, 1.595], "parts": [{"kind": "base", "c": [1.837, -1.073, 0.21], "s": [0.762, 0.183, 0.42]}, {"kind": "back", "c": [1.837, -1.064, 0.798], "s": [0.762, 0.2, 1.595]}, {"kind": "arm", "c": [1.531, -1.073, 0.31], "s": [0.15, 0.183, 0.62]}, {"kind": "arm", "c": [2.143, -1.073, 0.31], "s": [0.15, 0.183, 0.62]}]}, {"label": "shelf", "colour": "#667ca3", "lo": [2.102, 2.742, 1.173], "hi": [2.332, 3.126, 1.469], "parts": [{"kind": "side", "c": [2.217, 2.752, 1.321], "s": [0.23, 0.02, 0.296]}, {"kind": "side", "c": [2.217, 3.116, 1.321], "s": [0.23, 0.02, 0.296]}, {"kind": "board", "c": [2.217, 2.934, 1.183], "s": [0.23, 0.384, 0.02]}, {"kind": "board", "c": [2.217, 2.934, 1.459], "s": [0.23, 0.384, 0.02]}]}, {"label": "shelf", "colour": "#93a3bf", "lo": [1.717, 2.622, 0.709], "hi": [2.135, 3.004, 0.992], "parts": [{"kind": "side", "c": [1.727, 2.813, 0.85], "s": [0.02, 0.382, 0.283]}, {"kind": "side", "c": [2.125, 2.813, 0.85], "s": [0.02, 0.382, 0.283]}, {"kind": "board", "c": [1.926, 2.813, 0.719], "s": [0.418, 0.382, 0.02]}, {"kind": "board", "c": [1.926, 2.813, 0.982], "s": [0.418, 0.382, 0.02]}]}], "things": [{"n": 1, "label": "laptop", "color": "grey", "at": [0.607, -1.963, 0.794], "size": [0.22, 0.219, 0.024]}, {"n": 2, "label": "watch", "color": "grey", "at": [2.095, 1.013, 0.763], "size": [0.064, 0.071, 0.016]}, {"n": 3, "label": "remote control", "color": "grey", "at": [0.525, -1.565, 0.782], "size": [0.105, 0.318, 0.063]}, {"n": 4, "label": "bottle", "color": "grey", "at": [2.412, 2.031, 0.341], "size": [0.025, 0.174, 0.09]}, {"n": 5, "label": "mug", "color": "blue", "at": [0.649, -1.681, 0.851], "size": [0.075, 0.155, 0.09]}, {"n": 6, "label": "bottle", "color": "grey", "at": [2.046, 1.728, 0.724], "size": [0.055, 0.095, 0.11]}, {"n": 7, "label": "remote control", "color": "white", "at": [0.127, 2.464, 0.857], "size": [0.074, 0.062, 0.016]}, {"n": 8, "label": "book", "color": "grey", "at": [-0.493, 0.088, 0.443], "size": [0.187, 0.136, 0.011]}, {"n": 9, "label": "book", "color": "white", "at": [-0.242, 2.676, 0.875], "size": [0.25, 0.273, 0.03]}, {"n": 10, "label": "bottle", "color": "grey", "at": [2.055, 0.528, 0.736], "size": [0.04, 0.092, 0.112]}, {"n": 11, "label": "laptop", "color": "black", "at": [-0.647, -0.956, 0.774], "size": [0.262, 0.289, 0.14]}, {"n": 12, "label": "wallet", "color": "grey", "at": [1.987, 1.241, 0.664], "size": [0.101, 0.055, 0.022]}, {"n": 13, "label": "book", "color": "grey", "at": [0.089, 2.655, 0.878], "size": [0.113, 0.145, 0.04]}, {"n": 14, "label": "bag", "color": "grey", "at": [0.355, 3.151, 0.975], "size": [0.457, 0.252, 0.212]}, {"n": 15, "label": "glasses", "color": "red", "at": [2.124, 1.535, 0.651], "size": [0.111, 0.156, 0.087]}, {"n": 16, "label": "book", "color": "beige", "at": [-0.837, 0.762, 0.94], "size": [0.094, 0.144, 0.047]}, {"n": 17, "label": "charger", "color": "beige", "at": [-0.775, -1.491, 0.916], "size": [0.058, 0.071, 0.038]}, {"n": 18, "label": "bottle", "color": "grey", "at": [1.963, 2.636, 0.812], "size": [0.1, 0.09, 0.069]}, {"n": 19, "label": "book", "color": "red", "at": [2.021, 1.439, 0.656], "size": [0.155, 0.162, 0.045]}, {"n": 20, "label": "book", "color": "grey", "at": [0.827, -1.447, 0.797], "size": [0.084, 0.059, 0.008]}, {"n": 21, "label": "passport", "color": "grey", "at": [1.987, 1.241, 0.664], "size": [0.101, 0.055, 0.022]}, {"n": 22, "label": "bottle", "color": "grey", "at": [-0.756, -1.218, 0.766], "size": [0.155, 0.165, 0.096]}, {"n": 23, "label": "keys", "color": "brown", "at": [2.18, 0.504, 0.658], "size": [0.048, 0.035, 0.01]}, {"n": 24, "label": "mug", "color": "brown", "at": [2.034, -0.446, 0.499], "size": [0.1, 0.185, 0.128]}]} diff --git a/apps/web/public/real-scan/flat/meta.json b/apps/web/public/real-scan/flat/meta.json index 69cb828..0245fe0 100644 --- a/apps/web/public/real-scan/flat/meta.json +++ b/apps/web/public/real-scan/flat/meta.json @@ -230,47 +230,25 @@ "walls": [ { "start": [ - 2.291, - 0.807 + 2.354, + -4.055 ], "end": [ - -0.636, - 3.833 - ], - "height_m": 2.48 - }, - { - "start": [ - 2.244, - -4.151 - ], - "end": [ - -2.616, - 1.823 + -2.701, + 1.75 ], "height_m": 2.56 }, { "start": [ - 2.288, - 0.964 - ], - "end": [ - -0.422, - 3.669 - ], - "height_m": 2.45 - }, - { - "start": [ - 2.295, - -3.987 + 2.46, + 0.97 ], "end": [ - -2.434, - 1.444 + -0.41, + 4.046 ], - "height_m": 2.52 + "height_m": 2.5 }, { "start": [ @@ -282,17 +260,6 @@ 2.026 ], "height_m": 2.18 - }, - { - "start": [ - 2.378, - 1.058 - ], - "end": [ - -0.101, - 3.715 - ], - "height_m": 2.5 } ], "height_m": 2.56, @@ -302,128 +269,227 @@ { "label": "table", "min": [ - 1.253, - -1.138, + 1.102, + -1.331, 0.757 ], "max": [ - 1.923, - -0.45, + 2.131, + -0.304, 0.856 ], - "confidence": 0.936 + "confidence": 0.936, + "centre": [ + 1.616, + -0.817, + 0.806 + ], + "size": [ + 0.735, + 0.721, + 0.099 + ], + "yaw": 0.73435 }, { "label": "desk", "min": [ - 0.656, - 1.523, - -0.002 + 0.568, + 1.416, + 0.0 ], "max": [ - 1.539, - 2.325, + 1.64, + 2.514, + 0.876 + ], + "confidence": 0.63, + "centre": [ + 1.104, + 1.965, + 0.438 + ], + "size": [ + 0.586, + 0.95, 0.876 ], - "confidence": 0.63 + "yaw": 0.73435 }, { "label": "desk", "min": [ - -0.239, - -1.546, + -0.517, + -1.536, 0.547 ], "max": [ - 0.278, - -0.46, + 0.534, + -0.458, 1.307 ], - "confidence": 0.647 + "confidence": 0.647, + "centre": [ + 0.009, + -0.997, + 0.927 + ], + "size": [ + 0.562, + 0.945, + 0.76 + ], + "yaw": 0.73435 }, { "label": "desk", "min": [ - 0.358, - 2.426, + 0.346, + 2.276, 0.577 ], "max": [ - 0.783, - 2.585, + 0.819, + 2.749, 0.825 ], - "confidence": 0.727 + "confidence": 0.727, + "centre": [ + 0.583, + 2.512, + 0.701 + ], + "size": [ + 0.335, + 0.335, + 0.248 + ], + "yaw": 0.73435 }, { "label": "table", "min": [ - 0.226, - 3.134, + 0.227, + 3.079, 0.021 ], "max": [ - 0.392, - 3.194, + 0.388, + 3.242, 0.136 ], - "confidence": 0.515 + "confidence": 0.515, + "centre": [ + 0.308, + 3.161, + 0.078 + ], + "size": [ + 0.098, + 0.131, + 0.115 + ], + "yaw": 0.73435 }, { "label": "desk", "min": [ - -0.636, - 3.16, + -0.667, + 3.002, 0.457 ], "max": [ - -0.114, - 3.408, + -0.077, + 3.587, 0.894 ], - "confidence": 0.665 + "confidence": 0.665, + "centre": [ + -0.372, + 3.294, + 0.676 + ], + "size": [ + 0.447, + 0.385, + 0.437 + ], + "yaw": 0.73435 }, { "label": "desk", "min": [ - -1.963, - 1.522, + -2.096, + 1.496, 0.849 ], "max": [ - -1.48, - 2.304, + -1.299, + 2.284, 0.889 ], - "confidence": 0.928 + "confidence": 0.928, + "centre": [ + -1.697, + 1.89, + 0.869 + ], + "size": [ + 0.626, + 0.496, + 0.04 + ], + "yaw": 0.73435 }, { "label": "tv", "min": [ - 0.751, - 2.12, + 0.736, + 2.095, 0.995 ], "max": [ - 1.196, - 2.58, + 1.231, + 2.617, 1.422 ], - "confidence": 0.894 + "confidence": 0.894, + "centre": [ + 0.984, + 2.356, + 1.208 + ], + "size": [ + 0.169, + 0.552, + 0.427 + ], + "yaw": 0.73435 }, { "label": "tv", "min": [ - -0.168, - -1.16, + -0.417, + -1.156, 0.923 ], "max": [ - -0.111, - -0.586, + 0.137, + -0.601, 1.264 ], - "confidence": 0.847 + "confidence": 0.847, + "centre": [ + -0.14, + -0.878, + 1.094 + ], + "size": [ + 0.386, + 0.399, + 0.341 + ], + "yaw": 0.73435 }, { "label": "tv", @@ -442,58 +508,102 @@ { "label": "bed", "min": [ - -0.309, - -0.117, - -0.062 + -0.426, + -0.368, + 0.0 ], "max": [ - 0.703, - 0.984, + 1.02, + 1.046, + 0.73 + ], + "confidence": 0.728, + "centre": [ + 0.297, + 0.339, + 0.365 + ], + "size": [ + 1.232, + 0.793, 0.73 ], - "confidence": 0.728 + "yaw": 0.73435 }, { "label": "bed", "min": [ - -1.37, - 3.161, + -1.427, + 2.989, 0.637 ], "max": [ - -0.746, - 3.853, + -0.53, + 3.856, 1.653 ], - "confidence": 0.752 + "confidence": 0.752, + "centre": [ + -0.978, + 3.423, + 1.145 + ], + "size": [ + 0.832, + 0.417, + 1.016 + ], + "yaw": 0.73435 }, { "label": "chair", "min": [ - 1.302, - 0.768, + 1.188, + 0.607, 0.027 ], "max": [ - 1.965, - 1.449, + 2.166, + 1.582, 0.937 ], - "confidence": 0.777 + "confidence": 0.777, + "centre": [ + 1.677, + 1.095, + 0.482 + ], + "size": [ + 0.716, + 0.667, + 0.91 + ], + "yaw": 0.73435 }, { "label": "chair", "min": [ - 2.119, - -0.985, + 2.004, + -1.053, 0.744 ], "max": [ - 2.351, - -0.631, + 2.473, + -0.583, 0.85 ], - "confidence": 0.818 + "confidence": 0.818, + "centre": [ + 2.238, + -0.818, + 0.797 + ], + "size": [ + 0.323, + 0.342, + 0.106 + ], + "yaw": 0.73435 }, { "label": "chair", @@ -512,184 +622,327 @@ { "label": "chair", "min": [ - -0.769, - -0.572, + -0.839, + -0.685, 0.416 ], "max": [ - -0.328, - -0.2, + -0.214, + -0.061, 0.796 ], - "confidence": 0.82 + "confidence": 0.82, + "centre": [ + -0.526, + -0.373, + 0.606 + ], + "size": [ + 0.445, + 0.439, + 0.38 + ], + "yaw": 0.73435 }, { "label": "chair", "min": [ - 1.653, - 1.582, + 1.608, + 1.556, 0.418 ], "max": [ - 1.788, - 1.751, + 1.829, + 1.781, 0.908 ], - "confidence": 0.621 + "confidence": 0.621, + "centre": [ + 1.719, + 1.669, + 0.663 + ], + "size": [ + 0.131, + 0.185, + 0.49 + ], + "yaw": 0.73435 }, { "label": "chair", "min": [ - -0.722, - 3.248, + -0.739, + 3.2, 0.524 ], "max": [ - -0.58, - 3.338, + -0.562, + 3.375, 0.873 ], - "confidence": 0.66 + "confidence": 0.66, + "centre": [ + -0.65, + 3.287, + 0.698 + ], + "size": [ + 0.134, + 0.115, + 0.349 + ], + "yaw": 0.73435 }, { "label": "chair", "min": [ - -0.855, - 3.45, + -0.891, + 3.413, 0.595 ], "max": [ - -0.72, - 3.573, + -0.689, + 3.614, 0.795 ], - "confidence": 0.508 + "confidence": 0.508, + "centre": [ + -0.79, + 3.513, + 0.695 + ], + "size": [ + 0.154, + 0.131, + 0.2 + ], + "yaw": 0.73435 }, { "label": "chair", "min": [ - -1.743, - 2.456, + -1.819, + 2.425, 0.751 ], "max": [ - -1.564, - 2.771, + -1.467, + 2.77, 1.001 ], - "confidence": 0.543 + "confidence": 0.543, + "centre": [ + -1.643, + 2.597, + 0.876 + ], + "size": [ + 0.294, + 0.2, + 0.25 + ], + "yaw": 0.73435 }, { "label": "chair", "min": [ - -2.613, - 1.67, + -2.811, + 1.599, 0.501 ], "max": [ - -2.11, - 2.513, + -1.839, + 2.56, 1.015 ], - "confidence": 0.686 + "confidence": 0.686, + "centre": [ + -2.325, + 2.079, + 0.758 + ], + "size": [ + 0.754, + 0.615, + 0.514 + ], + "yaw": 0.73435 }, { "label": "chair", "min": [ - -2.14, - 1.012, - -0.033 + -2.144, + 1.015, + 0.0 ], "max": [ - -1.951, - 1.211, + -1.944, + 1.207, + 0.003 + ], + "confidence": 0.546, + "centre": [ + -2.044, + 1.111, + 0.002 + ], + "size": [ + 0.194, + 0.083, 0.003 ], - "confidence": 0.546 + "yaw": 0.73435 }, { "label": "chair", "min": [ - 2.16, - 0.419, + 2.02, + 0.398, 0.39 ], "max": [ - 2.425, - 0.876, + 2.537, + 0.907, 0.755 ], - "confidence": 0.739 + "confidence": 0.739, + "centre": [ + 2.278, + 0.652, + 0.572 + ], + "size": [ + 0.423, + 0.304, + 0.365 + ], + "yaw": 0.73435 }, { "label": "cabinet", "min": [ - 0.682, - 1.589, + 0.658, + 1.43, 0.005 ], "max": [ - 1.494, - 2.144, + 1.604, + 2.395, 0.764 ], - "confidence": 0.638 + "confidence": 0.638, + "centre": [ + 1.131, + 1.913, + 0.384 + ], + "size": [ + 0.548, + 0.805, + 0.759 + ], + "yaw": 0.73435 }, { "label": "sofa", "min": [ - -1.143, - -0.322, + -1.147, + -0.327, 0.536 ], "max": [ - -0.765, - 0.014, + -0.77, + 0.066, 1.075 ], - "confidence": 0.515 + "confidence": 0.515, + "centre": [ + -0.958, + -0.13, + 0.806 + ], + "size": [ + 0.161, + 0.384, + 0.539 + ], + "yaw": 0.73435 }, { "label": "sofa", "min": [ - 1.788, - 0.122, + 1.738, + 0.112, 0.627 ], "max": [ - 2.398, - 0.706, + 2.427, + 0.758, 1.595 ], - "confidence": 0.883 + "confidence": 0.883, + "centre": [ + 2.083, + 0.435, + 1.111 + ], + "size": [ + 0.762, + 0.183, + 0.968 + ], + "yaw": 0.73435 }, { "label": "shelf", "min": [ - -0.485, - 3.476, + -0.535, + 3.443, 1.173 ], "max": [ - -0.181, - 3.818, + -0.107, + 3.883, 1.469 ], - "confidence": 0.831 + "confidence": 0.831, + "centre": [ + -0.321, + 3.663, + 1.321 + ], + "size": [ + 0.23, + 0.384, + 0.296 + ], + "yaw": 0.73435 }, { "label": "shelf", "min": [ - -0.692, - 3.254, + -0.738, + 3.097, 0.709 ], "max": [ - -0.232, - 3.518, + -0.172, + 3.66, 0.992 ], - "confidence": 0.621 + "confidence": 0.621, + "centre": [ + -0.455, + 3.378, + 0.85 + ], + "size": [ + 0.418, + 0.382, + 0.283 + ], + "yaw": 0.73435 } ], "objects": [ @@ -1116,7 +1369,7 @@ "triangles": 119999 }, "architect": { - "walls": 10, + "walls": 8, "scanned_walls": 3 }, "drawings": [ @@ -1124,7 +1377,7 @@ "id": "A1.1", "title": "Floor plan", "file": "sheet-a1-1.svg", - "summary": "Floor plan of the Living room: 3.46 by 7.38 metres, 21.1 square metres, 28 pieces of furniture and 24 things marked." + "summary": "Floor plan of the Living room: 3.53 by 7.18 metres, 21.4 square metres, 28 pieces of furniture and 24 things marked." }, { "id": "A2.1", diff --git a/apps/web/public/real-scan/flat/sheet-a1-1.svg b/apps/web/public/real-scan/flat/sheet-a1-1.svg index 913aac4..8b95b85 100644 --- a/apps/web/public/real-scan/flat/sheet-a1-1.svg +++ b/apps/web/public/real-scan/flat/sheet-a1-1.svg @@ -1,188 +1,174 @@ -A1.1 Floor plan: Living roomFloor plan of the Living room: 3.46 by 7.38 metres, 21.1 square metres, 28 pieces of furniture and 24 things marked. +A1.1 Floor plan: Living roomFloor plan of the Living room: 3.53 by 7.18 metres, 21.4 square metres, 28 pieces of furniture and 24 things marked. - - - - - - - -7.38 - - - - - - -0.54 - - - - - - -0.57 - - - - - - -0.61 - - - - - - -0.50 - - - - - - -1.29 - - - - - - -1.59 - - - - - - -1.03 - - - - - - -4.71 - - - - - - -3.46 - -3.46 - -7.38 - -table - -desk - -desk - -desk - -table - -desk - -desk - -tv - -tv - -tv - -bed - -bed - -chair - -chair - -chair - -chair - -chair - -chair - -chair - -chair - -chair - -chair - -chair - -cabinet - -sofa - -sofa - -shelf - -shelf - -1 - -2 - -3 - -4 - -5 - -6 - -7 - -8 - -9 - -10 - -11 - -12 - -13 - -14 - -15 - -16 - -17 - -18 - -19 - -20 - -21 - -22 - -23 - -24 + + + + + + + +7.18 + + + + + + +1.04 + + + + + + +0.88 + + + + + + +1.36 + + + + + + +1.52 + + + + + + +1.13 + + + + + + +4.78 + + + + + + +3.53 + +3.53 + +7.18 + +table + +desk + +desk + +desk + +table + +desk + +desk + +tv + +tv + +tv + +bed + +bed + +chair + +chair + +chair + +chair + +chair + +chair + +chair + +chair + +chair + +chair + +chair + +cabinet + +sofa + +sofa + +shelf + +shelf + +1 + +2 + +3 + +4 + +5 + +6 + +7 + +8 + +9 + +10 + +11 + +12 + +13 + +14 + +15 + +16 + +17 + +18 + +19 + +20 + +21 + +22 + +23 + +24 @@ -190,9 +176,9 @@ 0 2 m Scale 1:50 (on A3) - + -N +N magnetic north Things 1 grey laptop, on the table, waist height @@ -228,10 +214,10 @@ RECALL Living room Scanned 2026-11-01 -Floor 21.1 m², perimeter 21.7 m, +Floor 21.4 m², perimeter 21.4 m, height 2.56 m. Walls drawn dashed were not seen -by the scan (6.1 m of 21.7 m). +by the scan (5.9 m of 21.4 m). Squared to the room's main direction; edges under 50 cm smoothed away. Dimensions in diff --git a/apps/web/public/real-scan/flat/sheet-a2-1.svg b/apps/web/public/real-scan/flat/sheet-a2-1.svg index 11dee5e..891adf3 100644 --- a/apps/web/public/real-scan/flat/sheet-a2-1.svg +++ b/apps/web/public/real-scan/flat/sheet-a2-1.svg @@ -3,73 +3,73 @@ - - - - - - - - - - - - -floor 0.00 - -0.75 + + + + + + + + + + + + +floor 0.00 + +0.75 ceiling 2.56 - -7.38 -Elevation 1 - - - - - - - - - - - - - - - - - - - -floor 0.00 - -0.75 + +7.18 +Elevation 1 + + + + + + + + + + + + + + + + + + + +floor 0.00 + +0.75 ceiling 2.56 - -4.71 -Elevation 2 - - - - - - - - - - - - -floor 0.00 - -0.75 + +4.78 +Elevation 2 + + + + + + + + + + + + +floor 0.00 + +0.75 ceiling 2.56 - -3.46 -Elevation 3 + +3.53 +Elevation 3 diff --git a/services/pipeline/scripts/export_room_views.py b/services/pipeline/scripts/export_room_views.py index 51d1c63..2789d3b 100644 --- a/services/pipeline/scripts/export_room_views.py +++ b/services/pipeline/scripts/export_room_views.py @@ -98,6 +98,20 @@ def _place(at: np.ndarray, furniture: list[drawings.Furnishing]) -> str: return f"{'on' if on else 'near'} the {near.label}, {band}" +def oriented_box(lo: np.ndarray, hi: np.ndarray, theta: float) -> dict: + """A plan-frame box as the viewer draws it in the home frame: centre, size and yaw (about z), plus the world bounds + of the turned box (min/max) for framing the camera.""" + corners = arch.rotate(np.array([[lo[0], lo[1]], [hi[0], lo[1]], [hi[0], hi[1]], [lo[0], hi[1]]]), theta) + centre = np.r_[arch.rotate(np.array([(lo[0] + hi[0]) / 2, (lo[1] + hi[1]) / 2]), theta), (lo[2] + hi[2]) / 2] + return { + "centre": np.round(centre, 3).tolist(), + "size": np.round(hi - lo, 3).tolist(), + "yaw": round(theta, 5), + "min": np.round(np.r_[corners.min(0), lo[2]], 3).tolist(), + "max": np.round(np.r_[corners.max(0), hi[2]], 3).tolist(), + } + + def architect_view( ep: Path, out: Path, result: dict, meta: dict, points: np.ndarray, colors: np.ndarray, room_name: str ) -> tuple[dict, list[dict]]: @@ -124,8 +138,10 @@ def architect_view( arch.place_door(walls, np.r_[corners.min(0), lo_h[2]], np.r_[corners.max(0), hi_h[2]]) continue inside = np.all((points >= lo_h) & (points <= hi_h), axis=1) - fit = arch.fit_box(pts_plan[inside], lo_h[2], hi_h[2]) + fit = arch.furniture_box(pts_plan[inside], lo_h[2], hi_h[2], plan) lo, hi = fit if fit else (np.r_[corners.min(0), lo_h[2]], np.r_[corners.max(0), hi_h[2]]) + if fit: # the viewer's boxes: the same fit, turned with the room, so they stand inside its walls + f.update(oriented_box(lo, hi, theta)) parts = arch.furniture_parts(f["label"], lo, hi, centre) colour = arch.hex_colour(colors[inside], "#b9a58a") furniture_json.append( diff --git a/services/pipeline/scripts/export_viewer_scan.py b/services/pipeline/scripts/export_viewer_scan.py index 433a168..edeba20 100644 --- a/services/pipeline/scripts/export_viewer_scan.py +++ b/services/pipeline/scripts/export_viewer_scan.py @@ -4,6 +4,9 @@ manifest.json). Output, in the home frame (floor z = 0, z up, metres) shifted so the room is centred on x/y: points.bin (float32 xyz), colors.bin (uint8 rgb), conf.bin (uint8, rank within the room), and meta.json with bounds, horizontal surfaces, the room model (floor outline, walls, furniture), the objects and the run's stats. +A wall RANSAC found twice (its face and a parallel plane in front of it) is exported once. With the Architect view on, +each furniture box is fitted in the room's own frame and clipped to the room: `centre`, `size` and `yaw` (about z), so +the viewer draws it square to the walls and inside them; `min`/`max` stay as its world bounds. With --mesh (default on) it also writes the room as a coloured surface: Poisson reconstruction over the measured points (Open3D, classic geometry; nothing is generated), trimmed to where the scan had points: mesh_pos.bin (float32 xyz), mesh_col.bin (uint8 rgb), mesh_idx.bin (uint32 triangles). @@ -24,6 +27,8 @@ from export_room_views import architect_view, photo_view from export_viewer_points import find_surfaces, voxel_downsample +from recall_pipeline.domain.room_model import merge_walls + def height_colours(z: np.ndarray) -> np.ndarray: """The site's blues by height (pale at the floor, deep at 2 m): a colour for every point that owes nothing to a @@ -127,9 +132,13 @@ def main() -> None: "room": { "name": room_name, "floor": [_xy(p, shift) for p in room["floor_polygon"]], + # merged again here so scans processed before merge_walls existed export one sheet per wall too "walls": [ - {"start": _xy(w["start"], shift), "end": _xy(w["end"], shift), "height_m": round(w["height_m"], 2)} - for w in room["walls"] + {"start": _xy(a, shift), "end": _xy(b, shift), "height_m": round(h, 2)} + for a, b, h in merge_walls( + [(w["start"], w["end"], w["height_m"]) for w in room["walls"]], + tuple(np.mean(room["floor_polygon"], axis=0)), + ) ], "height_m": round(room["height_m"], 2), "north_deg": room.get("north_deg"), diff --git a/services/pipeline/src/recall_pipeline/domain/architecture.py b/services/pipeline/src/recall_pipeline/domain/architecture.py index 074b549..bf94ac2 100644 --- a/services/pipeline/src/recall_pipeline/domain/architecture.py +++ b/services/pipeline/src/recall_pipeline/domain/architecture.py @@ -215,6 +215,28 @@ def fit_box(points_plan: np.ndarray, z_min: float, z_max: float) -> tuple[np.nda return lo, hi +def furniture_box( + points_plan: np.ndarray, z_min: float, z_max: float, plan: Polygon, margin: float = 0.03 +) -> tuple[np.ndarray, np.ndarray] | None: + """`fit_box` for a piece of furniture that may stand against a wall: only points inside the room (the plan shrunk by + `margin`) count, so the wall behind it can't stretch the box, and the footprint is then clipped to the room.""" + room = plan.buffer(-margin) + if room.is_empty: + return None + inside = contains_xy(room, points_plan[:, 0], points_plan[:, 1]) + fit = fit_box(points_plan[inside], z_min, z_max) + if fit is None: + return None + lo, hi = fit + clipped = box(lo[0], lo[1], hi[0], hi[1]).intersection(room) + if clipped.is_empty: + return None + x0, y0, x1, y1 = clipped.bounds + if x1 - x0 < 0.05 or y1 - y0 < 0.05: + return None + return np.array([x0, y0, lo[2]]), np.array([x1, y1, hi[2]]) + + def front_axis(lo: np.ndarray, hi: np.ndarray, toward: np.ndarray) -> tuple[int, int]: """(axis, sign) of the box side that faces `toward` (the room's centre): where a desk is used from, a TV seen.""" d = toward - (lo[:2] + hi[:2]) / 2 diff --git a/services/pipeline/src/recall_pipeline/domain/room_model.py b/services/pipeline/src/recall_pipeline/domain/room_model.py index a0836bb..f136dae 100644 --- a/services/pipeline/src/recall_pipeline/domain/room_model.py +++ b/services/pipeline/src/recall_pipeline/domain/room_model.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math from dataclasses import dataclass import numpy as np @@ -46,8 +47,64 @@ def footprint(points: np.ndarray) -> tuple[list[tuple[float, float]], float]: return [(float(x), float(y)) for x, y in coords], float(np.percentile(points[:, 2], 98)) +Segment = tuple[tuple[float, float], tuple[float, float], float] + +WALL_MERGE_ANGLE_DEG = 10.0 # two planes this close in direction … +WALL_MERGE_GAP_M = 0.35 # … and this close together are one wall seen twice (its face and a cabinet front, a curtain) + + +def merge_walls( + segs: list[Segment], + centre: tuple[float, float], + max_angle_deg: float = WALL_MERGE_ANGLE_DEG, + max_gap: float = WALL_MERGE_GAP_M, +) -> list[Segment]: + """One segment per real wall. RANSAC often finds a wall twice: its face and a parallel plane a few centimetres in + front of it (furniture backs, a curtain, a radiator). Near-parallel segments whose lines are within `max_gap` of + each other and whose spans overlap become one wall: it keeps the line of the outer segment (farther from `centre`, + the room's middle, since the room is inside its walls), spans both, and takes the larger height.""" + c = np.asarray(centre, dtype=float) + out = [(np.asarray(a, float), np.asarray(b, float), h) for a, b, h in segs] + out.sort(key=lambda s: -float(np.linalg.norm(s[1] - s[0]))) + cos_max = math.cos(math.radians(max_angle_deg)) + + def line(s): # unit direction and the signed distance of the centre from the line + u = (s[1] - s[0]) / (np.linalg.norm(s[1] - s[0]) or 1.0) + n = np.array([-u[1], u[0]]) + return u, n, float((c - s[0]) @ n) + + merged = True + while merged: + merged = False + for i in range(len(out)): + for j in range(i + 1, len(out)): + ui, ni, _ = line(out[i]) + uj, _, _ = line(out[j]) + if abs(float(ui @ uj)) < cos_max: + continue + mid_j = (out[j][0] + out[j][1]) / 2 + if abs(float((mid_j - out[i][0]) @ ni)) > max_gap: + continue + si = sorted(float((p - out[i][0]) @ ui) for p in out[i][:2]) + sj = sorted(float((p - out[i][0]) @ ui) for p in out[j][:2]) + if min(si[1], sj[1]) - max(si[0], sj[0]) < 0: + continue # side by side along one line (two walls of a corridor), not the same wall twice + outer, inner = sorted((out[i], out[j]), key=lambda s: -abs(line(s)[2])) + u = line(outer)[0] + ts = [float((p - outer[0]) @ u) for p in (*outer[:2], *inner[:2])] + a, b = outer[0] + u * min(ts), outer[0] + u * max(ts) + out[i] = (a, b, max(outer[2], inner[2])) + del out[j] + merged = True + break + if merged: + break + return [((float(a[0]), float(a[1])), (float(b[0]), float(b[1])), h) for a, b, h in out] + + def room_json(floor: Polygon, walls: list[WallPlane], furniture: list[tuple[str, np.ndarray]]) -> dict: - segs = [wall_segment(w) for w in walls] + middle = floor.centroid + segs = merge_walls([wall_segment(w) for w in walls], (middle.x, middle.y)) outline = [(float(x), float(y)) for x, y in floor.exterior.coords][:-1] if Polygon(outline).exterior.is_ccw is False: outline.reverse() diff --git a/services/pipeline/tests/test_architecture.py b/services/pipeline/tests/test_architecture.py index 0adcefd..921f6d6 100644 --- a/services/pipeline/tests/test_architecture.py +++ b/services/pipeline/tests/test_architecture.py @@ -146,3 +146,23 @@ def test_furniture_measured_above_the_floor_stands_on_it_and_a_tv_stays_on_the_w assert min(p.centre[2] - p.size[2] / 2 for p in shelf) >= 1.39 low = arch.furniture_parts("chair", np.array([0.0, 0.0, 0.0]), np.array([0.5, 0.5, 0.08]), np.array([0.2, 2.0])) assert min(p.centre[2] - p.size[2] / 2 for p in low) >= -1e-3 + + +def test_a_desk_against_a_wall_in_a_rotated_room_stays_inside_the_room(): + # the room is turned 35°; the desk (1.2 x 0.6 m) stands against the y = 4 wall, whose points sit right behind it + rng = np.random.default_rng(3) + theta = math.radians(35) + plan = Polygon([(0, 0), (5, 0), (5, 4), (0, 4)]) + desk = np.c_[rng.uniform(1.0, 2.2, 800), rng.uniform(3.4, 3.98, 800), rng.uniform(0.0, 0.76, 800)] + wall = np.c_[rng.uniform(0.0, 5.0, 3000), rng.normal(4.0, 0.02, 3000), rng.uniform(0.0, 2.5, 3000)] + pts_home = np.vstack([desk, wall]) + pts_home[:, :2] = arch.rotate(pts_home[:, :2], theta) + # what the exporter feeds it: every point inside the desk's world-axis box (which cuts deep into the wall) + desk_home = np.c_[arch.rotate(desk[:, :2], theta), desk[:, 2]] + lo_w, hi_w = desk_home.min(0), desk_home.max(0) + near = pts_home[np.all((pts_home >= lo_w) & (pts_home <= hi_w), axis=1)] + pts_plan = np.c_[arch.rotate(near[:, :2], -theta), near[:, 2]] + assert pts_plan[:, 1].max() > 4.0 # the world box really did pull in wall points + lo, hi = arch.furniture_box(pts_plan, 0.0, 0.76, plan) + assert hi[1] <= 4.0 - 0.03 + 1e-6 and lo[1] > 3.3 # inside the wall, and not stretched by it + assert 1.0 <= lo[0] < 1.1 and 2.1 < hi[0] <= 2.2 # no wider than the desk's own points diff --git a/services/pipeline/tests/unit/test_lift_and_room.py b/services/pipeline/tests/unit/test_lift_and_room.py index fe534b0..e098d96 100644 --- a/services/pipeline/tests/unit/test_lift_and_room.py +++ b/services/pipeline/tests/unit/test_lift_and_room.py @@ -43,3 +43,21 @@ def test_room_json_from_a_synthetic_rectangular_room(): (ax, ay), (bx, by) = room["walls"][0]["start"], room["walls"][0]["end"] assert abs(ay - 3.0) < 0.05 and abs(by - 3.0) < 0.05 and abs(abs(bx - ax) - 4.0) < 0.2 assert abs(room["furniture"][0]["top_z"] - 0.76) < 0.03 + + +def test_a_wall_found_twice_becomes_one_wall_on_its_outer_face(): + # the north wall's face at y = 3.0, and a parallel plane 15 cm in front of it (cabinet fronts), shorter + face = ((0.0, 3.0), (4.0, 3.02), 2.5) + front = ((0.5, 2.85), (3.0, 2.86), 2.1) + east = ((4.0, 0.0), (4.0, 3.0), 2.4) # perpendicular: left alone + merged = room_model.merge_walls([front, face, east], centre=(2.0, 1.5)) + assert len(merged) == 2 + north = next(w for w in merged if abs(w[0][1] - w[1][1]) < 0.1) + assert min(north[0][1], north[1][1]) > 2.95 and north[2] == 2.5 # the outer line and the taller height + assert abs(abs(north[1][0] - north[0][0]) - 4.0) < 0.05 # spans both + + +def test_two_walls_in_line_but_apart_are_not_merged(): + left = ((0.0, 3.0), (1.5, 3.0), 2.5) + right = ((2.5, 3.1), (4.0, 3.1), 2.5) # a doorway between them + assert len(room_model.merge_walls([left, right], centre=(2.0, 1.5))) == 2 From 9c912fa3d61d91279efbfa387b57596bd81ac559 Mon Sep 17 00:00:00 2001 From: Rachit Date: Mon, 2 Nov 2026 10:35:58 +0100 Subject: [PATCH 02/30] docs(readme): live site links, Vercel first and recall.onica.ai alongside Co-Authored-By: Claude Opus 5.5 --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 8b51e9a..4cbf5aa 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,12 @@ Indie track · Theme: Build for Your World

Baseten: serves Muse Spark Open

+

+ Live site: recall-ten-khaki.vercel.app + (also at recall.onica.ai) · + Explore a real room +

+

Try it (no keys) · Architecture · From 0bfd90b53199d789845862fdaef54ff83bd0ef44 Mon Sep 17 00:00:00 2001 From: Rachit Date: Mon, 2 Nov 2026 11:44:59 +0100 Subject: [PATCH 03/30] feat(api): where am I? match one photo against the room scans (ORB + PnP, no model) The pipeline now writes a place index per scan (ORB features of every keyframe lifted to 3D with the phone's LiDAR depth and ARKit pose), and POST /homes/{id}/locate matches one camera photo against it: the room it was taken in, the camera's pose in the room map's world, and what to point at. It runs beside ARKit's relocalisation, never instead of it. On the real living-room scans: held-out keyframes 1.2 cm median error; photos from other sessions fix 59% of the time and 89% of fixes agree (the phone applies a pose only when two fixes agree); 22-35 ms per match. Co-Authored-By: Claude Opus 5.5 --- Makefile | 6 +- services/api/pyproject.toml | 3 +- services/api/resources/openapi.yaml | 2 + services/api/resources/paths/homes.yaml | 35 ++++ .../api/resources/schemas/LocateRequest.yaml | 12 ++ services/api/resources/schemas/PlaceFix.yaml | 27 +++ .../recall_api/adapters/inbound/http/homes.py | 40 +++- .../adapters/inbound/http/models.py | 45 +++++ .../outbound/vision/orb_place_matcher.py | 126 +++++++++++++ .../recall_api/application/locate_by_photo.py | 128 +++++++++++++ .../api/src/recall_api/application/ports.py | 31 ++++ .../api/src/recall_api/infra/container.py | 7 + services/api/src/recall_api/infra/guard.py | 5 +- .../api/tests/unit/test_locate_by_photo.py | 173 ++++++++++++++++++ services/api/uv.lock | 21 +++ .../recall_pipeline/adapters/place_index.py | 102 +++++++++++ .../src/recall_pipeline/adapters/run.py | 7 + .../src/recall_pipeline/domain/place_index.py | 52 ++++++ .../pipeline/tests/unit/test_place_index.py | 24 +++ 19 files changed, 841 insertions(+), 5 deletions(-) create mode 100644 services/api/resources/schemas/LocateRequest.yaml create mode 100644 services/api/resources/schemas/PlaceFix.yaml create mode 100644 services/api/src/recall_api/adapters/outbound/vision/orb_place_matcher.py create mode 100644 services/api/src/recall_api/application/locate_by_photo.py create mode 100644 services/api/tests/unit/test_locate_by_photo.py create mode 100644 services/pipeline/src/recall_pipeline/adapters/place_index.py create mode 100644 services/pipeline/src/recall_pipeline/domain/place_index.py create mode 100644 services/pipeline/tests/unit/test_place_index.py diff --git a/Makefile b/Makefile index 8d09d48..0d77d43 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ BIND ?= 0.0.0.0 WEB := apps/web REDOCLY := npx -y @redocly/cli@2.72.1 -.PHONY: help setup dev worker robot-export regression viewer public-scan live demo record probes diagrams pair rerun test test-all lint lint-imports migrate bundle-spec gen-models gen-swift-client eval +.PHONY: help setup dev worker robot-export place-index regression viewer public-scan live demo record probes diagrams pair rerun test test-all lint lint-imports migrate bundle-spec gen-models gen-swift-client eval help: @grep -E '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) | awk -F':.*?## ' '{printf " %-18s %s\n", $$1, $$2}' @@ -30,6 +30,10 @@ ROBOT_HOME = $(or $(HOME_ID),$(if $(filter command line,$(origin HOME)),$(HOME), robot-export: ## One room's memory for a robot (Nav2 map, things, furniture): make robot-export [HOME=flat] [ROOM=...] -> exports/robot// cd $(API) && uv run python -m recall_api.infra.robot_export --home "$(ROBOT_HOME)" $(if $(ROOM),--room "$(ROOM)",) --out "$(CURDIR)/exports/robot/$(ROBOT_HOME)" +place-index: ## Build the photo matcher's index (ORB + LiDAR depth, no model) for every processed scan on this Mac + cd $(PIPE) && for e in $(CURDIR)/data/blobs/scans/*/episode; do \ + [ -f "$$e/result.json" ] && uv run python -m recall_pipeline.adapters.place_index "$$e"; done; true + regression: ## Re-run every processed scan now (no posting; cached SAM answers) and compare with the last run cd $(API) && uv run python -c "from recall_api.infra import jobs, regression; print('\n'.join(regression.run(jobs.settings, jobs.run_pipeline)))" diff --git a/services/api/pyproject.toml b/services/api/pyproject.toml index c4ef66c..c421a65 100644 --- a/services/api/pyproject.toml +++ b/services/api/pyproject.toml @@ -23,6 +23,7 @@ dependencies = [ "huey>=2.5", "a2a-sdk[http-server]>=1.3.0", "pillow>=13.0.0", + "opencv-python-headless>=4.10,<5", ] [project.scripts] @@ -81,4 +82,4 @@ layers = [ name = "Domain and application stay free of frameworks and I/O" type = "forbidden" source_modules = ["recall_api.domain", "recall_api.application", "recall_api.util"] -forbidden_modules = ["fastapi", "sqlalchemy", "httpx", "openai", "torch", "boto3", "starlette", "uvicorn"] +forbidden_modules = ["fastapi", "sqlalchemy", "httpx", "openai", "torch", "boto3", "starlette", "uvicorn", "cv2"] diff --git a/services/api/resources/openapi.yaml b/services/api/resources/openapi.yaml index cc1544c..fe1f864 100644 --- a/services/api/resources/openapi.yaml +++ b/services/api/resources/openapi.yaml @@ -76,6 +76,8 @@ paths: $ref: paths/homes.yaml#/room_map /homes/{home_id}/ar-labels: $ref: paths/homes.yaml#/ar_labels + /homes/{home_id}/locate: + $ref: paths/homes.yaml#/locate /homes/{home_id}/robot-package: $ref: paths/homes.yaml#/robot_package /homes/{home_id}/robot-export: diff --git a/services/api/resources/paths/homes.yaml b/services/api/resources/paths/homes.yaml index 0df4a15..2730a9b 100644 --- a/services/api/resources/paths/homes.yaml +++ b/services/api/resources/paths/homes.yaml @@ -143,3 +143,38 @@ ar_labels: schema: {$ref: ../schemas/ArLabels.yaml} "404": $ref: ../schemas/Responses.yaml#/NotFound +locate: + post: + tags: [homes] + operationId: locateByPhoto + summary: Where am I? One camera photo matched against the room scans (classic features + PnP, no model) + description: > + Asked by the phone while ARKit hasn't yet recognised the saved room map, alongside it (never instead of it). + Answers the room the photo was taken in when one room clearly wins, the camera's pose in the requested map's + world when that room matched well enough, and what to point the phone at. The photo is matched in memory and not + stored. + parameters: + - $ref: ../schemas/Params.yaml#/home_id + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: ../schemas/LocateRequest.yaml + responses: + "413": + $ref: ../schemas/Responses.yaml#/TooLarge + "429": + $ref: ../schemas/Responses.yaml#/RateLimited + "401": + $ref: ../schemas/Responses.yaml#/Unauthorized + "422": + $ref: ../schemas/Responses.yaml#/Invalid + "200": + description: What the photo matched + content: + application/json: + schema: + $ref: ../schemas/PlaceFix.yaml + "404": + $ref: ../schemas/Responses.yaml#/NotFound diff --git a/services/api/resources/schemas/LocateRequest.yaml b/services/api/resources/schemas/LocateRequest.yaml new file mode 100644 index 0000000..2df5ff0 --- /dev/null +++ b/services/api/resources/schemas/LocateRequest.yaml @@ -0,0 +1,12 @@ +type: object +required: [image, pose, scan_id] +properties: + image: {type: string, format: binary, description: "JPEG of the camera now (any size; intrinsics are for this image)"} + pose: + type: string + description: > + JSON: {"pose": [16 numbers, ARKit camera-to-world, column-major, in the phone's session (not yet the map's)], + "intrinsics": {"fx", "fy", "cx", "cy"} for the JPEG, "image_size": [w, h]}. Only the intrinsics are used on + the Mac; the phone keeps the session pose to turn the answer into its own correction. + scan_id: {type: string, maxLength: 64, description: "The scan whose room map the phone runs: the pose is answered in its world"} + room: {type: string, maxLength: 60, description: "The room the phone is trying to recognise (for the hint)"} diff --git a/services/api/resources/schemas/PlaceFix.yaml b/services/api/resources/schemas/PlaceFix.yaml new file mode 100644 index 0000000..176e71f --- /dev/null +++ b/services/api/resources/schemas/PlaceFix.yaml @@ -0,0 +1,27 @@ +type: object +required: [room, scan_id, score, runner_up_room, runner_up_score, pose, inliers, reproj_px, hint, ms] +properties: + room: + type: [string, "null"] + description: The room the photo was taken in, when one room clearly matched (it may not be the requested map's room) + scan_id: {type: [string, "null"], description: "The scan whose world `pose` is in"} + score: {type: integer, minimum: 0, description: "RANSAC inliers of the best room"} + runner_up_room: {type: [string, "null"]} + runner_up_score: {type: integer, minimum: 0} + pose: + type: [array, "null"] + items: {type: number} + minItems: 16 + maxItems: 16 + description: > + The camera's pose (ARKit camera-to-world, column-major) in the requested map's world; null unless that room + matched with at least 30 inliers and at most 4 px mean reprojection error. The phone applies it only when two + fixes in a row agree. + inliers: {type: integer, minimum: 0, description: "RANSAC inliers in the requested map's world"} + reproj_px: {type: [number, "null"], description: "Mean reprojection error of those inliers, pixels"} + hint: + type: array + items: {type: string} + maxItems: 3 + description: What to point the phone at in that room, one-of-a-kind furniture first + ms: {type: integer, minimum: 0, description: "Time the Mac took"} diff --git a/services/api/src/recall_api/adapters/inbound/http/homes.py b/services/api/src/recall_api/adapters/inbound/http/homes.py index b5342a2..45c0b57 100644 --- a/services/api/src/recall_api/adapters/inbound/http/homes.py +++ b/services/api/src/recall_api/adapters/inbound/http/homes.py @@ -2,17 +2,32 @@ from __future__ import annotations -from fastapi import APIRouter, Depends, HTTPException, Response +import json +from typing import Annotated + +from fastapi import APIRouter, Depends, Form, HTTPException, Response, UploadFile +from fastapi.concurrency import run_in_threadpool from recall_api.application.ar_labels import NoRoomScan from recall_api.application.correct_object import CorrectionError, CorrectionInput from recall_api.application.export_for_robot import RobotExportError from recall_api.application.list_things import NoSuchHome, ThingView +from recall_api.application.locate_by_photo import LocateError from recall_api.application.plan_compass_route import CompassRouteError +from recall_api.application.ports import PlaceQuery from recall_api.domain.memory import Home, Trust from .deps import container, require_device_key -from .models import ArLabels, CompassRoute, ObjectCorrection, ObjectLocation, RobotExport, RoomModel, TrustUpdate +from .models import ( + ArLabels, + CompassRoute, + ObjectCorrection, + ObjectLocation, + PlaceFix, + RobotExport, + RoomModel, + TrustUpdate, +) from .models import Trust as TrustLevel router = APIRouter(tags=["homes"], dependencies=[Depends(require_device_key)]) @@ -137,3 +152,24 @@ def get_ar_labels(home_id: str, room: str, c=Depends(container)) -> ArLabels: raise HTTPException(404, {"error": "not_found", "detail": f"No processed scan of {room} yet."}) from e return ArLabels(scan_id=r.scan_id, room=r.room, floor_y_arkit=r.floor_y_arkit, labels=[vars(x) | {"position_arkit": list(x.position_arkit)} for x in r.labels]) # fmt: skip + + +@router.post("/homes/{home_id}/locate", response_model=PlaceFix) +async def locate( + home_id: str, + image: UploadFile, + pose: Annotated[str, Form()], + scan_id: Annotated[str, Form(max_length=64)], + room: Annotated[str | None, Form(max_length=60)] = None, + c=Depends(container), +) -> PlaceFix: + try: + k = json.loads(pose)["intrinsics"] + query = PlaceQuery(await image.read(), float(k["fx"]), float(k["fy"]), float(k["cx"]), float(k["cy"])) + except (KeyError, ValueError, TypeError) as e: + raise HTTPException(422, {"error": "invalid", "detail": "Bad photo: pose must carry the intrinsics."}) from e + try: + f = await run_in_threadpool(c.locate_by_photo, home_id, scan_id, query, room) # CPU work off the event loop + except LocateError as e: + raise HTTPException(e.status, {"error": e.code, "detail": str(e)}) from e + return PlaceFix.model_validate(vars(f) | {"pose": list(f.pose) if f.pose else None}) diff --git a/services/api/src/recall_api/adapters/inbound/http/models.py b/services/api/src/recall_api/adapters/inbound/http/models.py index 9ccd763..b9c2016 100644 --- a/services/api/src/recall_api/adapters/inbound/http/models.py +++ b/services/api/src/recall_api/adapters/inbound/http/models.py @@ -636,6 +636,51 @@ class ArLabels(BaseModel): labels: list[Label] +class LocateRequest(BaseModel): + image: Annotated[bytes, Field(description="JPEG of the camera now (any size; intrinsics are for this image)")] + pose: Annotated[ + str, + Field( + description='JSON: {"pose": [16 numbers, ARKit camera-to-world, column-major, in the phone\'s session (not yet the map\'s)], "intrinsics": {"fx", "fy", "cx", "cy"} for the JPEG, "image_size": [w, h]}. Only the intrinsics are used on the Mac; the phone keeps the session pose to turn the answer into its own correction.\n' + ), + ] + scan_id: Annotated[ + str, + Field(description="The scan whose room map the phone runs: the pose is answered in its world", max_length=64), + ] + room: Annotated[ + str | None, Field(description="The room the phone is trying to recognise (for the hint)", max_length=60) + ] = None + + +class PlaceFix(BaseModel): + room: Annotated[ + str | None, + Field( + description="The room the photo was taken in, when one room clearly matched (it may not be the requested map's room)" + ), + ] + scan_id: Annotated[str | None, Field(description="The scan whose world `pose` is in")] + score: Annotated[int, Field(description="RANSAC inliers of the best room", ge=0)] + runner_up_room: str | None + runner_up_score: Annotated[int, Field(ge=0)] + pose: Annotated[ + list[float] | None, + Field( + description="The camera's pose (ARKit camera-to-world, column-major) in the requested map's world; null unless that room matched with at least 30 inliers and at most 4 px mean reprojection error. The phone applies it only when two fixes in a row agree.\n", + max_length=16, + min_length=16, + ), + ] + inliers: Annotated[int, Field(description="RANSAC inliers in the requested map's world", ge=0)] + reproj_px: Annotated[float | None, Field(description="Mean reprojection error of those inliers, pixels")] + hint: Annotated[ + list[str], + Field(description="What to point the phone at in that room, one-of-a-kind furniture first", max_length=3), + ] + ms: Annotated[int, Field(description="Time the Mac took", ge=0)] + + class Leg(BaseModel): bearing_deg: Annotated[float, Field(description="Magnetic bearing, clockwise from north", ge=0.0, le=360.0)] metres: Annotated[float, Field(ge=0.0)] diff --git a/services/api/src/recall_api/adapters/outbound/vision/orb_place_matcher.py b/services/api/src/recall_api/adapters/outbound/vision/orb_place_matcher.py new file mode 100644 index 0000000..ad56b3c --- /dev/null +++ b/services/api/src/recall_api/adapters/outbound/vision/orb_place_matcher.py @@ -0,0 +1,126 @@ +"""PlaceMatcher on classic OpenCV: one photo from the phone against a scan's place index (place_index.npz, written by +the pipeline: ORB features of every keyframe lifted to 3D with LiDAR depth and the ARKit pose). + +1. ORB features of the photo (1000, on a 640-pixel copy: the same settings the index was built with). +2. Retrieval: every descriptor's nearest neighbour in the whole index votes for its keyframe; the 3 keyframes with the + most close matches are kept. +3. Matching against those keyframes with Lowe's ratio test gives 2D–3D pairs; `solvePnPRansac` + LM refinement gives + the camera's pose in the scan's ARKit world, and the RANSAC inliers are the score. +No model anywhere; the photo is decoded in memory and never stored. +""" + +from __future__ import annotations + +import os +import time +from dataclasses import dataclass +from functools import lru_cache + +import cv2 +import numpy as np + +from recall_api.application.ports import BlobStore, PlaceMatch, PlaceQuery + +FEATURES = 1000 +LONG_EDGE = 640 +CLOSE = 48 # Hamming distance that counts as a vote in retrieval (of 256 bits) +RATIO = 0.8 +TOP_FRAMES = 3 +REPROJ_PX = 4.0 +_FLIP = np.diag([1.0, -1.0, -1.0, 1.0]) # OpenCV camera (+y down, +z forward) <-> ARKit camera (+y up, −z forward) + + +@dataclass(frozen=True) +class _Index: + desc: np.ndarray # (N, 32) uint8 + xyz: np.ndarray # (N, 3) float32, the scan's ARKit world + frame: np.ndarray # (N,) keyframe row of each point + starts: np.ndarray # first point of each keyframe (points are stored keyframe by keyframe) + frames: int + + +@lru_cache(maxsize=16) +def _load(path: str, mtime: float) -> _Index: # mtime in the key: a rebuilt index is read again + with np.load(path) as z: + frame = z["frame"].astype(np.int64) + n = int(len(z["frame_ids"])) + return _Index(z["desc"], z["xyz"].astype(np.float64), frame, np.searchsorted(frame, np.arange(n + 1)), n) + + +class OrbPlaceMatcher: + def __init__(self, blobs: BlobStore) -> None: + self.blobs = blobs + self.orb = cv2.ORB_create(nfeatures=FEATURES, scaleFactor=1.2, nlevels=8, fastThreshold=12) + self.bf = cv2.BFMatcher(cv2.NORM_HAMMING) + + def _index(self, key: str) -> _Index | None: + path = self.blobs.local_path(key) + if not path or not os.path.exists(path): + return None + return _load(path, os.path.getmtime(path)) + + def _features(self, jpeg: bytes) -> tuple[np.ndarray, np.ndarray]: + gray = cv2.imdecode(np.frombuffer(jpeg, np.uint8), cv2.IMREAD_GRAYSCALE) + if gray is None: + return np.zeros((0, 2)), np.zeros((0, 32), np.uint8) + h, w = gray.shape + s = min(LONG_EDGE / max(h, w), 1.0) + small = cv2.resize(gray, (round(w * s), round(h * s)), interpolation=cv2.INTER_AREA) if s < 1 else gray + kps, desc = self.orb.detectAndCompute(small, None) + if desc is None or not kps: + return np.zeros((0, 2)), np.zeros((0, 32), np.uint8) + return np.array([k.pt for k in kps], dtype=np.float64) / s, desc + + def match(self, query: PlaceQuery, candidates: dict[str, str], budget_s: float = 0.4) -> list[PlaceMatch]: + """One result per candidate scan (scan id -> index key) that has an index, best first; scans not reached + within the time budget are left out.""" + t0 = time.perf_counter() + uv, desc = self._features(query.jpeg) + if len(desc) < 30: + return [] + K = np.array([[query.fx, 0, query.cx], [0, query.fy, query.cy], [0, 0, 1.0]]) + out = [] + for scan_id, key in candidates.items(): + if time.perf_counter() - t0 > budget_s: + break + idx = self._index(key) + if idx is None: + continue + out.append(self._one(scan_id, idx, uv, desc, K)) + return sorted(out, key=lambda m: -m.inliers) + + def _one(self, scan_id: str, idx: _Index, uv: np.ndarray, desc: np.ndarray, K: np.ndarray) -> PlaceMatch: + nearest = self.bf.match(desc, idx.desc) + votes = np.bincount([idx.frame[m.trainIdx] for m in nearest if m.distance <= CLOSE], minlength=idx.frames) + top = [int(f) for f in np.argsort(-votes)[:TOP_FRAMES] if votes[f] > 0] + pairs: dict[int, tuple[float, int]] = {} # query keypoint -> (distance, index point) + for f in top: + a, b = int(idx.starts[f]), int(idx.starts[f + 1]) + if b - a < 2: + continue + for m in self.bf.knnMatch(desc, idx.desc[a:b], k=2): + if len(m) == 2 and m[0].distance < RATIO * m[1].distance: + q, d = m[0].queryIdx, m[0].distance + if q not in pairs or d < pairs[q][0]: + pairs[q] = (d, a + m[0].trainIdx) + keyframe = top[0] if top else None + if len(pairs) < 12: + return PlaceMatch(scan_id, len(pairs) // 4, None, None, keyframe) + q_idx = np.array(list(pairs)) + obj = idx.xyz[[pairs[q][1] for q in q_idx]] + img = uv[q_idx] + ok, rvec, tvec, inl = cv2.solvePnPRansac( + obj, img, K, None, iterationsCount=300, reprojectionError=REPROJ_PX, confidence=0.995, + flags=cv2.SOLVEPNP_EPNP, + ) # fmt: skip + if not ok or inl is None or len(inl) < 6: + return PlaceMatch(scan_id, 0, None, None, keyframe) + inl = inl[:, 0] + rvec, tvec = cv2.solvePnPRefineLM(obj[inl], img[inl], K, None, rvec, tvec) + proj, _ = cv2.projectPoints(obj[inl], rvec, tvec, K, None) + err = float(np.mean(np.linalg.norm(proj[:, 0] - img[inl], axis=1))) + R, _ = cv2.Rodrigues(rvec) + world_to_cam = np.eye(4) + world_to_cam[:3, :3], world_to_cam[:3, 3] = R, tvec[:, 0] + cam2world = np.linalg.inv(world_to_cam) @ _FLIP # OpenCV camera -> ARKit camera, same world + return PlaceMatch(scan_id, int(len(inl)), tuple(float(v) for v in cam2world.T.reshape(-1)), err, keyframe) diff --git a/services/api/src/recall_api/application/locate_by_photo.py b/services/api/src/recall_api/application/locate_by_photo.py new file mode 100644 index 0000000..3d768ae --- /dev/null +++ b/services/api/src/recall_api/application/locate_by_photo.py @@ -0,0 +1,128 @@ +"""Where am I? One photo from the phone, matched against the scans' place indexes (classic features + PnP, no model). + +It runs alongside ARKit's own relocalisation, never instead of it: the phone asks while ARKit hasn't recognised the +room yet and uses whichever answers first. It answers: +- the room the photo was taken in, when one room clearly wins (it may not be the room the thing is in); +- the camera's pose in the world of the room map the phone runs, when that room matched well enough; +- what to point at: the room's biggest one-of-a-kind furniture, for the spoken coaching. +""" + +from __future__ import annotations + +import time +from collections import Counter +from dataclasses import dataclass + +from recall_api.application.ports import PlaceMatch, PlaceMatcher, PlaceQuery +from recall_api.domain.memory import Scan, ScanStatus +from recall_api.domain.ports import HomeRepository, ScanRepository + +ROOM_MIN_INLIERS = 30 # a room is named from this many RANSAC inliers… +ROOM_MARGIN = 1.5 # …and this many times the next room's +POSE_MIN_INLIERS = 30 # on real scans: 59% of photos from another session fix, 89% of those right (evals/results.md) +POSE_MAX_REPROJ_PX = 4.0 +HINTS = 3 + + +class LocateError(Exception): + def __init__(self, message: str, code: str = "not_found", status: int = 404) -> None: + super().__init__(message) + self.code, self.status = code, status + + +@dataclass(frozen=True) +class PlaceFix: + room: str | None # None: no room clearly matched + scan_id: str | None # the scan whose world `pose` is in (the requested map's) + score: int + runner_up_room: str | None + runner_up_score: int + pose: tuple[float, ...] | None # ARKit camera-to-world, column-major, in the requested map's world + inliers: int + reproj_px: float | None + hint: list[str] + ms: int + + +def index_key(scan: Scan) -> str: + return f"{scan.folder}/episode/place_index.npz" + + +class LocateByPhoto: + def __init__(self, homes: HomeRepository, scans: ScanRepository, matcher: PlaceMatcher) -> None: + self.homes, self.scans, self.matcher = homes, scans, matcher + + def __call__(self, home_id: str, scan_id: str, query: PlaceQuery, room: str | None = None) -> PlaceFix: + t0 = time.perf_counter() + home = self.homes.get(home_id) + current = self.scans.get(scan_id) + if home is None or current is None or current.home_id != home_id: + raise LocateError("No such home or scan.") + processed = [s for s in self.scans.list_for_home(home_id) if s.status == ScanStatus.PROCESSED] + # the map's own world: its scan, the scan it was recognised in, and every rescan recognised in either + root = current.aligned_to or current.id + family = {s.id: s for s in processed if root in (s.id, s.aligned_to)} | {current.id: current} + others: dict[str, Scan] = {} + for s in sorted(processed, key=lambda s: s.created_at, reverse=True): # each other room's newest scan + if s.room != current.room and s.room not in {o.room for o in others.values()}: + others[s.id] = s + candidates = {sid: index_key(s) for sid, s in (family | others).items()} + matches = self.matcher.match(query, candidates) + rooms = {**{sid: s.room for sid, s in family.items()}, **{sid: s.room for sid, s in others.items()}} + + best_by_room: dict[str, PlaceMatch] = {} + for m in matches: + r = rooms[m.scan_id] + if r not in best_by_room or m.inliers > best_by_room[r].inliers: + best_by_room[r] = m + ranked = sorted(best_by_room.items(), key=lambda kv: -kv[1].inliers) + top_room, top = ranked[0] if ranked else (None, None) + second_room, second = ranked[1] if len(ranked) > 1 else (None, None) + score = top.inliers if top else 0 + clear = score >= ROOM_MIN_INLIERS and score >= ROOM_MARGIN * (second.inliers if second else 0) + named = top_room if clear else None + + # a pose only in the map's own world, and only when its room clearly won (two rooms alike: no pose) + mine = max((m for m in matches if m.scan_id in family), key=lambda m: m.inliers, default=None) + pose = None + if ( + mine is not None + and mine.pose is not None + and mine.inliers >= POSE_MIN_INLIERS + and (mine.reproj_px or 99) <= POSE_MAX_REPROJ_PX + and named == current.room + ): + pose = mine.pose + hint_room = named or room or current.room + return PlaceFix( + room=named, + scan_id=current.id, + score=score, + runner_up_room=second_room, + runner_up_score=second.inliers if second else 0, + pose=pose, + inliers=mine.inliers if mine else 0, + reproj_px=mine.reproj_px if mine and pose else None, + hint=hints([lm for lm in home.landmarks if lm.room in (hint_room, None)]), + ms=round((time.perf_counter() - t0) * 1000), + ) + + +def hints(landmarks: list) -> list[str]: + """What to point the phone at: one-of-a-kind furniture first (a TV, not one of six chairs), biggest first.""" + counts = Counter(lm.label for lm in landmarks) + seen: set[str] = set() + out = [] + + def size(lm) -> float: + lo, hi = lm.bbox.lo, lm.bbox.hi + return (hi.x - lo.x) * (hi.y - lo.y) * max(hi.z - lo.z, 0.05) + + for lm in sorted(landmarks, key=lambda lm: (counts[lm.label] > 1, -size(lm))): + if lm.label in seen or lm.label == "door": + continue + seen.add(lm.label) + out.append(lm.label) + if len(out) == HINTS: + break + return out diff --git a/services/api/src/recall_api/application/ports.py b/services/api/src/recall_api/application/ports.py index 7c9dfc0..8da04d3 100644 --- a/services/api/src/recall_api/application/ports.py +++ b/services/api/src/recall_api/application/ports.py @@ -191,3 +191,34 @@ class PromptScreener(Protocol): """A Meta model (Muse Spark Open or Glimmer) checks an object name from outside before any tool runs.""" def screen(self, text: str) -> Screening: ... + + +@dataclass(frozen=True) +class PlaceQuery: + """One photo from the phone, for the photo matcher: the JPEG and its camera intrinsics (pixels of that JPEG).""" + + jpeg: bytes + fx: float + fy: float + cx: float + cy: float + + +@dataclass(frozen=True) +class PlaceMatch: + """How one scan matched the photo: RANSAC inliers (the score) and, when the pose solved, the camera's pose in + that scan's ARKit world (camera-to-world, column-major) with its mean reprojection error in pixels.""" + + scan_id: str + inliers: int + pose: tuple[float, ...] | None + reproj_px: float | None + keyframe: int | None + + +class PlaceMatcher(Protocol): + """Classic photo matching (features + PnP, no model) against the scans' place indexes.""" + + def match(self, query: PlaceQuery, candidates: dict[str, str], budget_s: float = 0.4) -> list[PlaceMatch]: + """`candidates`: scan id -> blob key of its place index. Best first; scans without an index are left out.""" + ... diff --git a/services/api/src/recall_api/infra/container.py b/services/api/src/recall_api/infra/container.py index b8319c7..6a1993f 100644 --- a/services/api/src/recall_api/infra/container.py +++ b/services/api/src/recall_api/infra/container.py @@ -40,6 +40,7 @@ from recall_api.application.get_scan import GetScan from recall_api.application.ingest_scan import IngestScan from recall_api.application.list_things import ListThings +from recall_api.application.locate_by_photo import LocateByPhoto from recall_api.application.mark_scan_failed import MarkScanFailed from recall_api.application.plan_compass_route import PlanCompassRoute from recall_api.application.plan_guidance import PlanGuidance @@ -150,6 +151,12 @@ def correct_object(self) -> CorrectObject: def guidance(self) -> PlanGuidance: return PlanGuidance(self.homes, self.scans, self.objects, self.blobs, new_id, trust=self.trust) + @cached_property + def locate_by_photo(self) -> LocateByPhoto: + from recall_api.adapters.outbound.vision.orb_place_matcher import OrbPlaceMatcher + + return LocateByPhoto(self.homes, self.scans, OrbPlaceMatcher(self.blobs)) + @cached_property def ar_labels(self) -> ArLabelsInRoom: return ArLabelsInRoom(self.list_things, self.scans) diff --git a/services/api/src/recall_api/infra/guard.py b/services/api/src/recall_api/infra/guard.py index 6411e69..9505ff4 100644 --- a/services/api/src/recall_api/infra/guard.py +++ b/services/api/src/recall_api/infra/guard.py @@ -18,15 +18,18 @@ ("POST", re.compile(r"^/scans/[^/]+/results$"), 50 * MB), # the pipeline's result ("POST", re.compile(r"^/guidance/sessions/[^/]+/verify$"), 25 * MB), # one photo and its depth ("POST", re.compile(r"^/transcribe$"), 10 * MB), # a spoken question + ("POST", re.compile(r"^/homes/[^/]+/locate$"), 4 * MB), # one photo (and its depth) to match ("*", re.compile(r".*"), 1 * MB), ] -# (method, path pattern, calls allowed per minute per key): the routes that reach a model or start the pipeline +# (method, path pattern, calls allowed per minute per key): the routes that reach a model, start the pipeline or +# match photos RATE_LIMITS: list[tuple[str, re.Pattern[str], int]] = [ ("POST", re.compile(r"^/ask$"), 30), ("POST", re.compile(r"^/transcribe$"), 30), ("POST", re.compile(r"^/guidance/sessions/[^/]+/verify$"), 20), ("POST", re.compile(r"^/scans/[^/]+/complete$"), 10), + ("POST", re.compile(r"^/homes/[^/]+/locate$"), 90), # no model, but CPU: the phone asks about once a second ] diff --git a/services/api/tests/unit/test_locate_by_photo.py b/services/api/tests/unit/test_locate_by_photo.py new file mode 100644 index 0000000..d3c2ab8 --- /dev/null +++ b/services/api/tests/unit/test_locate_by_photo.py @@ -0,0 +1,173 @@ +"""Where am I? A photo matched against the scans' place indexes: the room, the pose in the map's world, the hint.""" + +import json + +import cv2 +import numpy as np +import pytest + +from recall_api.adapters.outbound.vision.orb_place_matcher import OrbPlaceMatcher +from recall_api.application.locate_by_photo import LocateByPhoto, LocateError, hints, index_key +from recall_api.application.ports import PlaceMatch, PlaceQuery +from recall_api.domain.memory import BBox, Landmark, Position, ScanStatus +from tests.unit.test_matching_and_ingest import Repos + +POSE = tuple(float(v) for v in np.eye(4).reshape(-1)) +Q = PlaceQuery(b"jpeg", 700.0, 700.0, 512.0, 384.0) + + +class FakeMatcher: + def __init__(self, inliers: dict[str, int]): + self.inliers, self.asked = inliers, {} + + def match(self, query, candidates, budget_s=0.4): + self.asked = candidates + out = [PlaceMatch(s, n, POSE if n >= 6 else None, 1.5, 0) for s, n in self.inliers.items() if s in candidates] + return sorted(out, key=lambda m: -m.inliers) + + +def _home(inliers): + r = Repos() + for sid, hour, room, aligned in [ + ("o1", 9, "Office", None), + ("o2", 11, "Office", "o1"), + ("b1", 10, "Bedroom", None), + ]: + s = r.scan(sid, hour, room=room, aligned_to=aligned) + s.status = ScanStatus.PROCESSED + r.scans.save(s) + home = r.homes.get("flat") + box = lambda lo, hi: BBox(Position(*lo), Position(*hi)) # noqa: E731 + home.landmarks = [ + Landmark("l1", "chair", box((0, 0, 0), (0.5, 0.5, 0.9)), Position(0.2, 0.2, 0.4), room="Office"), + Landmark("l2", "chair", box((1, 0, 0), (1.5, 0.5, 0.9)), Position(1.2, 0.2, 0.4), room="Office"), + Landmark("l3", "tv", box((2, 0, 1), (3, 0.1, 1.6)), Position(2.5, 0.05, 1.3), room="Office"), + Landmark("l4", "bed", box((0, 0, 0), (2, 1.6, 0.5)), Position(1, 0.8, 0.25), room="Bedroom"), + ] + r.homes.save(home) + m = FakeMatcher(inliers) + return LocateByPhoto(r.homes, r.scans, m), m, r + + +def test_in_the_maps_room_the_pose_comes_back_and_the_whole_family_is_tried(): + use, m, r = _home({"o1": 20, "o2": 64, "b1": 9}) + fix = use("flat", "o2", Q, room="Office") + assert (fix.room, fix.pose, fix.inliers, fix.scan_id) == ("Office", POSE, 64, "o2") + assert set(m.asked) == {"o1", "o2", "b1"} # the map's family (same world) and the other room's newest scan + assert m.asked["b1"] == index_key(r.scans.get("b1")) and m.asked["b1"].endswith("/episode/place_index.npz") + assert fix.hint[0] == "tv" and "chair" in fix.hint # one of a kind first + + +def test_in_another_room_it_says_so_and_gives_no_pose(): + fix, *_ = _home({"o1": 12, "o2": 10, "b1": 80}) + f = fix("flat", "o1", Q, room="Office") + assert (f.room, f.pose, f.runner_up_room) == ("Bedroom", None, "Office") + assert f.hint == ["bed"] # what to point at where the phone is + + +def test_two_rooms_alike_name_neither_and_give_no_pose(): + use, *_ = _home({"o1": 40, "b1": 35}) + f = use("flat", "o1", Q) + assert f.room is None and f.pose is None + + +def test_too_few_inliers_give_no_pose(): + use, *_ = _home({"o1": 25}) + assert use("flat", "o1", Q).pose is None + + +def test_a_scan_of_another_home_is_refused(): + use, *_ = _home({}) + with pytest.raises(LocateError): + use("elsewhere", "o1", Q) + + +def test_hints_skip_doors_and_repeat_kinds(): + box = BBox(Position(0, 0, 0), Position(1, 1, 1)) + lms = [Landmark(str(i), lab, box, Position(0, 0, 0)) for i, lab in enumerate(["door", "chair", "chair", "sofa"])] + assert hints(lms) == ["sofa", "chair"] + + +# The real matcher on a synthetic scene: a textured wall 2 m in front of the camera. +FX, CX, CY, W, H, Z = 700.0, 512.0, 384.0, 1024, 768, 2.0 + + +def _wall(seed: int) -> np.ndarray: + rng = np.random.default_rng(seed) + img = np.full((H, W), 128, np.uint8) + for _ in range(400): + x, y = int(rng.integers(0, W)), int(rng.integers(0, H)) + if rng.random() < 0.5: + cv2.circle(img, (x, y), int(rng.integers(4, 30)), int(rng.integers(0, 255)), -1) + else: + cv2.rectangle( + img, (x, y), (x + int(rng.integers(5, 40)), y + int(rng.integers(5, 40))), int(rng.integers(0, 255)), -1 + ) + return img + + +def _index(tmp_path, img) -> str: + """The pipeline's index for one keyframe: ORB (same settings) lifted onto the wall, camera at the ARKit origin.""" + orb = cv2.ORB_create(nfeatures=1000, scaleFactor=1.2, nlevels=8, fastThreshold=12) + s = 640 / W + kps, desc = orb.detectAndCompute(cv2.resize(img, (640, 480), interpolation=cv2.INTER_AREA), None) + uv = np.array([k.pt for k in kps]) / s + cam = np.c_[(uv[:, 0] - CX) / FX * Z, (uv[:, 1] - CY) / FX * Z, np.full(len(uv), Z)] + world = cam * [1, -1, -1] # OpenCV camera -> ARKit world (identity ARKit pose) + path = tmp_path / "place_index.npz" + np.savez(path, format=np.array("recall-place-index/1"), desc=desc, xyz=world.astype(np.float32), + frame=np.zeros(len(desc), np.int32), frame_ids=np.array([0], np.int32), + poses=np.eye(4, dtype=np.float32).reshape(1, 16), image_size=np.array([W, H], np.int32)) # fmt: skip + return str(path) + + +class Disk: + def local_path(self, key): + return key + + +def _jpeg(img) -> bytes: + return cv2.imencode(".jpg", img, [cv2.IMWRITE_JPEG_QUALITY, 90])[1].tobytes() + + +def test_the_matcher_finds_how_far_the_camera_moved(tmp_path): + wall = _wall(1) + key = _index(tmp_path, wall) + # the camera 10 cm to its right: the wall moves 35 px to the left in the photo + dx = FX * 0.10 / Z + moved = cv2.warpAffine(wall, np.float32([[1, 0, -dx], [0, 1, 0]]), (W, H), borderValue=128) + m = OrbPlaceMatcher(Disk()).match(PlaceQuery(_jpeg(moved), FX, FX, CX, CY), {"s": key})[0] + assert m.inliers >= 30 and m.reproj_px < 2 + T = np.array(m.pose).reshape(4, 4).T # column-major -> matrix; ARKit camera-to-world + assert np.allclose(T[:3, 3], [0.10, 0, 0], atol=0.02) # ARKit +x is the camera's right + assert np.allclose(T[:3, :3], np.eye(3), atol=0.02) + + +def test_a_photo_of_somewhere_else_gives_no_pose(tmp_path): + key = _index(tmp_path, _wall(1)) + m = OrbPlaceMatcher(Disk()).match(PlaceQuery(_jpeg(_wall(2)), FX, FX, CX, CY), {"s": key}) + assert not m or m[0].pose is None or m[0].inliers < 30 + + +def test_the_route_maps_the_form_and_refuses_a_bad_pose(tmp_path): + from fastapi.testclient import TestClient + + from recall_api.config.settings import Settings + from recall_api.infra.app import create_app + + use, m, _ = _home({"o1": 50}) + app = create_app(Settings(RECALL_DATA_DIR=tmp_path, RECALL_DEVICE_KEY="k", RECALL_TRACES="off")) + app.state.container.__dict__["locate_by_photo"] = use + client = TestClient(app) + pose = {"pose": list(POSE), "intrinsics": {"fx": 700, "fy": 700, "cx": 512, "cy": 384}, "image_size": [1024, 768]} + files = {"image": ("a.jpg", b"jpeg", "image/jpeg")} + auth = {"Authorization": "Bearer k"} + r = client.post("/homes/flat/locate", files=files, data={"pose": json.dumps(pose), "scan_id": "o1"}, headers=auth) + assert r.status_code == 200, r.text + assert r.json()["room"] == "Office" and len(r.json()["pose"]) == 16 and r.json()["hint"][0] == "tv" + bad = client.post("/homes/flat/locate", files=files, data={"pose": "{}", "scan_id": "o1"}, headers=auth) + assert bad.status_code == 422 + gone = client.post( + "/homes/flat/locate", files=files, data={"pose": json.dumps(pose), "scan_id": "zz"}, headers=auth + ) + assert gone.status_code == 404 diff --git a/services/api/uv.lock b/services/api/uv.lock index 49191cb..38ff684 100644 --- a/services/api/uv.lock +++ b/services/api/uv.lock @@ -760,6 +760,25 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1a/07/161270b0c2eec56e4c905f6d6d22e1b836887b2cb189d3f5820aa588e9dd/numpy-2.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:aa1cce2ff3f8d953de38b76bf44602caeb69f101430208f64a10067f7cb4b1d3", size = 10521630, upload-time = "2026-10-10T20:03:06.767Z" }, ] +[[package]] +name = "opencv-python-headless" +version = "4.14.0.94" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/8a/51ff1c6287be0b6cd3070cf095875da4ff7d6eda38507e6ada5608786a78/opencv_python_headless-4.14.0.94.tar.gz", hash = "sha256:4afa2ea1214453648be88259f035712454faa9039b686de7753569ba8eec1577", size = 96405664, upload-time = "2026-07-28T19:23:48.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/56/12f9b05628cf4ab6aad54479b9124f7c1f2f92b6a4e47c071beb63d926a2/opencv_python_headless-4.14.0.94-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:bc7db37dc234f7bb3190a158fd9dd750357246fc7d8adb23698843ef721a993b", size = 46491231, upload-time = "2026-07-29T03:52:11.902Z" }, + { url = "https://files.pythonhosted.org/packages/23/3b/2f9c42466f62aa27e97aaf6c9d914008895ecde28013ba4beed7f541f593/opencv_python_headless-4.14.0.94-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1777f43c9fa064f54b916ad70d944b4fde0a644a17e49f04a966bb24a4b5f1e1", size = 33084176, upload-time = "2026-07-28T20:05:59.902Z" }, + { url = "https://files.pythonhosted.org/packages/7d/91/83c51e3fe000b5dfce4f3be5c5d320e07ebab5c1742abc4e45674503f90f/opencv_python_headless-4.14.0.94-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:29714d7716dbfddf9fec20ffb878765e94ccaecd7d3ebd6750877420296e2dc5", size = 35405458, upload-time = "2026-07-28T19:19:29.997Z" }, + { url = "https://files.pythonhosted.org/packages/de/06/14c8c5d331bc72738683a9b227966aceaa4c6d08752f33d64578aebcb6f8/opencv_python_headless-4.14.0.94-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5e02669eac0ba67b2a22d7245af1e8ee1a2ef1185ee526a063d8f0555224bd52", size = 57534165, upload-time = "2026-07-28T19:19:52.742Z" }, + { url = "https://files.pythonhosted.org/packages/3f/0e/0b98bd582582253f61c4506f9b465cf5a8aa7fa4f7237dd99e1b5151a159/opencv_python_headless-4.14.0.94-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:97c6e818c6f71c0cfa214e12293b1d0266d679c38f4e086de08357c8ac6ece0d", size = 38104309, upload-time = "2026-07-28T19:20:08.818Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/7f538891722a4f06921419d55bac6f41729258d54442861edb2de0dbdf5e/opencv_python_headless-4.14.0.94-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:211e581f5a4670acbbe08fff36a35e9946039d2eea28b80394632d036d1be527", size = 61960165, upload-time = "2026-07-28T19:20:33.074Z" }, + { url = "https://files.pythonhosted.org/packages/e9/91/ded03abad74d7264fa6bc7c923d7cade6f94a9023eeb372cfad54c2d0334/opencv_python_headless-4.14.0.94-cp37-abi3-win32.whl", hash = "sha256:f70296aa7ac9d7ade0d925c43fdc006c83e20a23f30e2f40f1b82c74a7a54460", size = 33030271, upload-time = "2026-07-28T18:32:39.447Z" }, + { url = "https://files.pythonhosted.org/packages/ad/8d/db8673846ee53cbb5de4c2b4decc11cf733e203eb7d5146297869f69bd48/opencv_python_headless-4.14.0.94-cp37-abi3-win_amd64.whl", hash = "sha256:cbed65415b8f6a9541c705afe3e64795840524d0ff3bc58f507826284a1dc64b", size = 41028030, upload-time = "2026-07-28T18:32:36.724Z" }, +] + [[package]] name = "opentelemetry-api" version = "1.45.2" @@ -1214,6 +1233,7 @@ dependencies = [ { name = "httpx" }, { name = "huey" }, { name = "numpy" }, + { name = "opencv-python-headless" }, { name = "opentelemetry-exporter-otlp-proto-http" }, { name = "opentelemetry-instrumentation-fastapi" }, { name = "opentelemetry-instrumentation-httpx" }, @@ -1245,6 +1265,7 @@ requires-dist = [ { name = "httpx", specifier = ">=0.28" }, { name = "huey", specifier = ">=2.5" }, { name = "numpy", specifier = ">=2.1" }, + { name = "opencv-python-headless", specifier = ">=4.10,<5" }, { name = "opentelemetry-exporter-otlp-proto-http", specifier = ">=1.30" }, { name = "opentelemetry-instrumentation-fastapi", specifier = ">=0.51b0" }, { name = "opentelemetry-instrumentation-httpx", specifier = ">=0.51b0" }, diff --git a/services/pipeline/src/recall_pipeline/adapters/place_index.py b/services/pipeline/src/recall_pipeline/adapters/place_index.py new file mode 100644 index 0000000..3ac1de6 --- /dev/null +++ b/services/pipeline/src/recall_pipeline/adapters/place_index.py @@ -0,0 +1,102 @@ +"""Builds a scan's place index (place_index.npz in the episode folder): ORB features of every keyframe, lifted to 3D +with the frame's own LiDAR depth and ARKit pose. Classic OpenCV features, no model. The API's photo matcher reads it. + + python -m recall_pipeline.adapters.place_index EPISODE_DIR [EPISODE_DIR ...] +""" + +from __future__ import annotations + +import json +import sys +import time +from pathlib import Path + +import cv2 +import numpy as np + +from recall_pipeline.domain.geometry import column_major_to_matrix +from recall_pipeline.domain.place_index import lift_keypoints + +FORMAT = "recall-place-index/1" +FEATURES = 1000 +LONG_EDGE = 640 # ORB runs on a 640-pixel frame (the API matches the phone's photo at the same size) + + +def orb() -> cv2.ORB: + return cv2.ORB_create(nfeatures=FEATURES, scaleFactor=1.2, nlevels=8, fastThreshold=12) + + +def features(gray: np.ndarray, detector: cv2.ORB | None = None) -> tuple[np.ndarray, np.ndarray]: + """Keypoints (N, 2) in the given image's pixels and their ORB descriptors (N, 32), on a 640-pixel copy.""" + h, w = gray.shape + s = LONG_EDGE / max(h, w) + small = cv2.resize(gray, (round(w * s), round(h * s)), interpolation=cv2.INTER_AREA) if s < 1 else gray + kps, desc = (detector or orb()).detectAndCompute(small, None) + if desc is None or not kps: + return np.zeros((0, 2)), np.zeros((0, 32), np.uint8) + uv = np.array([k.pt for k in kps], dtype=np.float64) / min(s, 1.0) + return uv, desc + + +def build(episode_dir: str | Path) -> Path | None: + """Writes place_index.npz next to the episode's frames; None when the episode has no frames or depth.""" + ep = Path(episode_dir) + manifest = json.loads((ep / "manifest.json").read_text()) + k = manifest["intrinsics"] + K = np.array([[k["fx"], 0, k["cx"]], [0, k["fy"], k["cy"]], [0, 0, 1.0]]) + size = (manifest["image"]["width"], manifest["image"]["height"]) + poses = {} + for line in (ep / "poses.jsonl").read_text().splitlines(): + p = json.loads(line) + if p.get("tracking", "normal") == "normal": # a pose ARKit wasn't sure of would place the points wrongly + poses[p["index"]] = column_major_to_matrix(p["pose"]) + detector = orb() + descs, points, frames, frame_ids, frame_poses = [], [], [], [], [] + for index in sorted(poses): + img, dep = ep / "frames" / f"{index:06d}.jpg", ep / "depth" / f"{index:06d}.png" + if not img.exists() or not dep.exists(): + continue + gray = cv2.imread(str(img), cv2.IMREAD_GRAYSCALE) + depth = cv2.imread(str(dep), cv2.IMREAD_UNCHANGED) + if gray is None or depth is None: + continue + uv, desc = features(gray, detector) + xyz, keep = lift_keypoints(uv, depth, K, size, poses[index]) + if len(keep) < 20: + continue + descs.append(desc[keep]) + points.append(xyz.astype(np.float32)) + frames.append(np.full(len(keep), len(frame_ids), dtype=np.int32)) + frame_ids.append(index) + frame_poses.append(poses[index].T.reshape(-1)) # column-major, as the phone writes it + if not frame_ids: + return None + out = ep / "place_index.npz" + np.savez_compressed( + out, + format=np.array(FORMAT), + desc=np.concatenate(descs), + xyz=np.concatenate(points), + frame=np.concatenate(frames), + frame_ids=np.array(frame_ids, dtype=np.int32), + poses=np.array(frame_poses, dtype=np.float32), + image_size=np.array(size, dtype=np.int32), + ) + return out + + +def main(argv: list[str]) -> int: + for d in argv: + t0 = time.perf_counter() + out = build(d) + if out is None: + print(f"{d}: no frames with depth, no index", file=sys.stderr) + continue + with np.load(out) as z: + n, f = len(z["desc"]), len(z["frame_ids"]) + print(f"{out}: {f} keyframes, {n} points, {time.perf_counter() - t0:.1f} s") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/services/pipeline/src/recall_pipeline/adapters/run.py b/services/pipeline/src/recall_pipeline/adapters/run.py index 482eb1e..62080b9 100644 --- a/services/pipeline/src/recall_pipeline/adapters/run.py +++ b/services/pipeline/src/recall_pipeline/adapters/run.py @@ -89,4 +89,11 @@ def process(episode_dir: str, scan_id: str, post: bool = True, out: str | None = target.write_text(json.dumps(result, indent=1)) print(json.dumps(result["stats"])) print(f"{len(result['landmarks'])} landmarks, {len(result['objects'])} objects -> {target}") + try: # the photo matcher's index: a failure here never fails the scan (ARKit's room map still works) + from recall_pipeline.adapters.place_index import build + + index = build(episode_dir) + print(f"place index -> {index}" if index else "place index: no frames with depth") + except Exception as e: # noqa: BLE001 + print(f"place index failed: {e}", file=sys.stderr) return 0 diff --git a/services/pipeline/src/recall_pipeline/domain/place_index.py b/services/pipeline/src/recall_pipeline/domain/place_index.py new file mode 100644 index 0000000..20bee7d --- /dev/null +++ b/services/pipeline/src/recall_pipeline/domain/place_index.py @@ -0,0 +1,52 @@ +"""A scan's place index: the keyframes' feature points lifted to 3D with the phone's own LiDAR depth and ARKit pose. + +The API matches one photo from the phone against it to say which room the phone is in and where it stands there +(classic features + PnP, no model), alongside ARKit's own relocalisation. Everything is in the scan's ARKit world +(metres, y up), the frame the phone's saved room map uses. +""" + +from __future__ import annotations + +import numpy as np + +from recall_pipeline.domain.geometry import arkit_to_opencv + +DEPTH_MIN_M, DEPTH_MAX_M = 0.2, 6.0 # LiDAR is reliable in this range + + +def depth_at(depth_mm: np.ndarray, uv_depth: np.ndarray, reach: int = 2) -> np.ndarray: + """Depth in metres at (fractional) depth-map pixels: the nearest pixel, or else the median of the valid pixels + within `reach` (an edge pixel often has no LiDAR return); NaN where there is none or it is out of range.""" + h, w = depth_mm.shape + out = np.full(len(uv_depth), np.nan) + cols = np.clip(np.floor(uv_depth[:, 0]).astype(int), 0, w - 1) + rows = np.clip(np.floor(uv_depth[:, 1]).astype(int), 0, h - 1) + for i, (r, c) in enumerate(zip(rows, cols, strict=True)): + v = float(depth_mm[r, c]) + if v <= 0: + patch = depth_mm[max(r - reach, 0) : r + reach + 1, max(c - reach, 0) : c + reach + 1] + valid = patch[patch > 0] + v = float(np.median(valid)) if valid.size else 0.0 + m = v / 1000.0 + if DEPTH_MIN_M <= m <= DEPTH_MAX_M: + out[i] = m + return out + + +def lift_keypoints( + uv: np.ndarray, depth_mm: np.ndarray, K: np.ndarray, image_size: tuple[int, int], cam2world_arkit: np.ndarray +) -> tuple[np.ndarray, np.ndarray]: + """Image pixels (N, 2) of a keyframe → (world points (M, 3), indices of the kept pixels). `K` is for `image_size` + (width, height); the depth map may be smaller (LiDAR is 256 x 192) and is sampled at the same relative spot.""" + if len(uv) == 0: + return np.zeros((0, 3)), np.zeros(0, dtype=int) + w_img, h_img = image_size + dh, dw = depth_mm.shape + z = depth_at(depth_mm, uv * np.array([dw / w_img, dh / h_img])) + keep = np.flatnonzero(np.isfinite(z)) + z = z[keep] + x = (uv[keep, 0] - K[0, 2]) / K[0, 0] * z + y = (uv[keep, 1] - K[1, 2]) / K[1, 1] * z + cam = np.stack([x, y, z], axis=1) + T = arkit_to_opencv(cam2world_arkit) + return cam @ T[:3, :3].T + T[:3, 3], keep diff --git a/services/pipeline/tests/unit/test_place_index.py b/services/pipeline/tests/unit/test_place_index.py new file mode 100644 index 0000000..85a8f22 --- /dev/null +++ b/services/pipeline/tests/unit/test_place_index.py @@ -0,0 +1,24 @@ +import numpy as np + +from recall_pipeline.domain.place_index import depth_at, lift_keypoints + + +def test_keypoints_lift_onto_a_wall_in_front_of_an_arkit_camera(): + # ARKit camera at (1, 1.5, 0) looking down −z at a wall 2 m away; the image is 4x the depth map + K = np.array([[700.0, 0, 512], [0, 700.0, 384], [0, 0, 1]]) + depth = np.full((192, 256), 2000, np.uint16) + pose = np.eye(4) + pose[:3, 3] = [1.0, 1.5, 0.0] + uv = np.array([[512.0, 384.0], [862.0, 384.0], [512.0, 34.0]]) + xyz, keep = lift_keypoints(uv, depth, K, (1024, 768), pose) + assert list(keep) == [0, 1, 2] + # straight ahead; 350 px right = 1 m right; 350 px up (smaller v) = 1 m up (ARKit +y) + assert np.allclose(xyz, [[1.0, 1.5, -2.0], [2.0, 1.5, -2.0], [1.0, 2.5, -2.0]]) + + +def test_missing_depth_borrows_a_neighbour_and_out_of_range_is_dropped(): + depth = np.zeros((10, 10), np.uint16) + depth[5, 6] = 1500 # one valid pixel next to the asked one + depth[0, 0] = 9000 # 9 m: past LiDAR's range + z = depth_at(depth, np.array([[5.2, 5.2], [0.1, 0.1], [9.0, 9.0]])) + assert z[0] == 1.5 and np.isnan(z[1]) and np.isnan(z[2]) From bd3ea514d40519193b14a14a442bbde209f08d26 Mon Sep 17 00:00:00 2001 From: Rachit Date: Mon, 2 Nov 2026 12:16:29 +0100 Subject: [PATCH 04/30] feat(ios): recognise the room from anywhere: photo fixes beside ARKit, a growing map, named coaching - Guide warms up and uses the map of the thing's own room (it used the newest scan of any room). - While ARKit hasn't matched, PhotoAssist sends about one photo a second to the Mac's matcher: it says when the phone is in another room, names furniture to point at, and applies a pose only when two fixes agree (25 cm, 10 degrees). ARKit's own match always replaces it. - ARLocator keeps labels, arrows and the route under one map node, so a photo correction moves them together. - Once ARKit recognises the room, the map it grew is saved beside the scan's own (worldmap.grown.arworldmap), so the next visit matches from there too. - See it in AR shows the list at 25 s but keeps looking for 2 minutes and switches the labels on when the room is recognised. Co-Authored-By: Claude Opus 5.5 --- .../RecallApplication/GuideToThing.swift | 15 +- .../RecallApplication/PhotoAssist.swift | 78 +++++++++ .../Sources/RecallCapture/ARLocator.swift | 109 +++++++++++- .../RecallDomain/Locate/PhotoFix.swift | 83 +++++++++ .../Sources/RecallDomain/Ports/Ports.swift | 13 ++ .../Sources/RecallDomain/Setup/Privacy.swift | 3 +- .../Sources/RecallNetworking/RecallAPI.swift | 32 ++++ .../Sources/RecallNetworking/openapi.json | 164 ++++++++++++++++++ .../RecallPersistence/FileStores.swift | 13 +- .../Sources/RecallTestSupport/Fakes.swift | 37 +++- .../PhotoAssistTests.swift | 86 +++++++++ .../RecallDomainTests/PhotoFixTests.swift | 48 +++++ apps/ios/Recall/App/AppContainer.swift | 32 +++- .../Recall/Features/AR/ARLabelsModel.swift | 54 +++++- apps/ios/Recall/Features/Find/FindModel.swift | 6 +- apps/ios/Recall/Models/ScansModel.swift | 19 +- 16 files changed, 749 insertions(+), 43 deletions(-) create mode 100644 apps/ios/Packages/RecallKit/Sources/RecallApplication/PhotoAssist.swift create mode 100644 apps/ios/Packages/RecallKit/Sources/RecallDomain/Locate/PhotoFix.swift create mode 100644 apps/ios/Packages/RecallKit/Tests/RecallApplicationTests/PhotoAssistTests.swift create mode 100644 apps/ios/Packages/RecallKit/Tests/RecallDomainTests/PhotoFixTests.swift diff --git a/apps/ios/Packages/RecallKit/Sources/RecallApplication/GuideToThing.swift b/apps/ios/Packages/RecallKit/Sources/RecallApplication/GuideToThing.swift index 65a3235..aa7cfe2 100644 --- a/apps/ios/Packages/RecallKit/Sources/RecallApplication/GuideToThing.swift +++ b/apps/ios/Packages/RecallKit/Sources/RecallApplication/GuideToThing.swift @@ -45,15 +45,18 @@ public final class GuideToThing { private let haptics: any HapticsOutput private let clock: any Clock private let compass: (any CompassSensor)? + private let photos: (places: any PlaceLocating, scan: String)? private let settings: Settings private var cancelled = false + /// `photos`: the Mac's photo matcher and the scan whose room map this is, to recognise the room alongside ARKit. public init(home: String, thing: Thing, roomMap: Data?, locator: any Locator, guide: any GuideService, speech: any SpeechOutput, haptics: any HapticsOutput, clock: any Clock, - compass: (any CompassSensor)? = nil, settings: Settings = .init()) { + compass: (any CompassSensor)? = nil, photos: (places: any PlaceLocating, scan: String)? = nil, + settings: Settings = .init()) { self.home = home; self.thing = thing; self.roomMap = roomMap; self.locator = locator; self.guide = guide self.speech = speech; self.haptics = haptics; self.clock = clock; self.compass = compass - self.settings = settings + self.photos = photos; self.settings = settings } /// The compass heading when it can be trusted (else directions leave out "north-east"). @@ -122,8 +125,14 @@ public final class GuideToThing { // MARK: steps - /// Waits for tracking to turn normal (ARKit recognised the room) or the timeout. + /// Waits for tracking to turn normal (ARKit recognised the room, or two photo fixes agreed) or the timeout. private func locate() async -> TrackingState { + let assist = photos.map { + PhotoAssist(home: home, scan: $0.scan, room: thing.room, locator: locator, places: $0.places, speech: speech, + clock: clock) + } + let helping = assist.map { a in Task { await a.run() } } + defer { assist?.cancel(); helping?.cancel() } let started = clock.now() while !cancelled, clock.now().timeIntervalSince(started) < settings.locateTimeout { if locator.reading()?.tracking == .normal { return .normal } diff --git a/apps/ios/Packages/RecallKit/Sources/RecallApplication/PhotoAssist.swift b/apps/ios/Packages/RecallKit/Sources/RecallApplication/PhotoAssist.swift new file mode 100644 index 0000000..2d11d25 --- /dev/null +++ b/apps/ios/Packages/RecallKit/Sources/RecallApplication/PhotoAssist.swift @@ -0,0 +1,78 @@ +import Foundation +import RecallDomain + +/// Helps ARKit recognise the room, never replaces it: while ARKit hasn't matched the saved room map, about once a +/// second one camera photo goes to the Mac's photo matcher (classic features + PnP, no model). From its answers: +/// - the room is somewhere else → said once ("You seem to be in Bedroom, not Living room…"); +/// - what to point at → said every few seconds ("Point the phone slowly at the TV."); +/// - two fixes in a row that agree → the locator takes the correction and tracking reads normal at once. +/// The moment ARKit matches by itself it stops asking. No Mac, or an error: nothing is said and ARKit carries on. +@MainActor +public final class PhotoAssist { + public struct Settings: Sendable { + public var firstAfter: Double = 1.0 // ARKit often matches within a second: then nothing is sent + public var every: Double = 1.0 // the Mac answers in about 35 ms; one photo a second is plenty + public var maxAsks = 40 + public var hintEvery: Double = 8 // seconds between "point at …" lines + public init() {} + } + + private let home: String + private let scan: String + private let room: String? + private let locator: any Locator + private let places: any PlaceLocating + private let speech: any SpeechOutput + private let clock: any Clock + private let settings: Settings + private var cancelled = false + + /// Whether a photo fix (not ARKit) located the phone. + public private(set) var fixed = false + + public init(home: String, scan: String, room: String?, locator: any Locator, places: any PlaceLocating, + speech: any SpeechOutput, clock: any Clock, settings: Settings = .init()) { + self.home = home; self.scan = scan; self.room = room; self.locator = locator; self.places = places + self.speech = speech; self.clock = clock; self.settings = settings + } + + public func cancel() { cancelled = true } + + private var located: Bool { locator.reading()?.tracking == .normal } + + /// Until the room is recognised (by ARKit or two agreeing fixes), `cancel()` or the ask limit. + public func run() async { + try? await clock.sleep(seconds: settings.firstAfter) + var agreement = FixAgreement() + var turn = 0 + var lastHint: Date? + var toldElsewhere = false + for _ in 0..= settings.hintEvery }) ?? true, + let line = LocateCoaching.point(at: fix.hint, turn: turn) { + speech.say(line, interrupt: false) + turn += 1 + lastHint = clock.now() + } + if let pose = fix.pose { + let session = Pose(columnMajor: frame.pose) + if let agreed = agreement.add(MapCorrection.between(map: pose, session: session), session: session) { + locator.correct(mapFromSession: Pose(columnMajor: MapCorrection.columnMajor(agreed))) + fixed = true + return + } + } + } + try? await clock.sleep(seconds: settings.every) + } + } +} diff --git a/apps/ios/Packages/RecallKit/Sources/RecallCapture/ARLocator.swift b/apps/ios/Packages/RecallKit/Sources/RecallCapture/ARLocator.swift index 6e2a75e..d7ece00 100644 --- a/apps/ios/Packages/RecallKit/Sources/RecallCapture/ARLocator.swift +++ b/apps/ios/Packages/RecallKit/Sources/RecallCapture/ARLocator.swift @@ -13,13 +13,27 @@ public final class ARLocator: Locator { /// What the camera view draws over the feed: the arrow above the thing. public let scene = SCNScene() private var arrow: SCNNode? + /// Everything placed in the room map's world hangs here: the session's own world when ARKit matched the map, + /// or turned by a photo fix's correction until it does. + private let mapRoot = SCNNode() private let track = SCNNode() private let tags = SCNNode() private var shown: [ArLabel] = [] + /// T(map ← session) from two agreeing photo fixes, until ARKit recognises the room itself. + private var correction: simd_double4x4? + private var watching: Task? + /// ARKit itself matched the saved map in this run (not a photo fix): the map may grow from here. + public private(set) var recognisedByARKit = false + /// Handed a grown room map (ARKit's map of this run, which keeps the saved map's world) once ARKit has matched + /// and mapped what it sees; the caller saves it beside the scan's own so the next visit matches from here too. + public var onMapGrown: ((Data) -> Void)? + private var growing: Task? + public init() { - scene.rootNode.addChildNode(track) - scene.rootNode.addChildNode(tags) + scene.rootNode.addChildNode(mapRoot) + mapRoot.addChildNode(track) + mapRoot.addChildNode(tags) } public static var isSupported: Bool { ARWorldTrackingConfiguration.isSupported } @@ -43,6 +57,7 @@ public final class ARLocator: Locator { } guard let roomMap else { if running, !runningWithMap { return true } + forgetRun() session.run(config, options: [.resetTracking, .removeExistingAnchors]) running = true; runningWithMap = false return true @@ -54,6 +69,7 @@ public final class ARLocator: Locator { // TODO(verify) on the iPhone how quickly tracking returns to normal after a resume. session.run(config, options: []) } else { + forgetRun() config.initialWorldMap = current.map session.run(config, options: [.resetTracking, .removeExistingAnchors]) } @@ -64,6 +80,7 @@ public final class ARLocator: Locator { return false } current = (roomMap, map) + forgetRun() config.initialWorldMap = map session.run(config, options: [.resetTracking, .removeExistingAnchors]) running = true; runningWithMap = true @@ -74,6 +91,79 @@ public final class ARLocator: Locator { public func stop() { session.pause() running = false + growing?.cancel() + } + + /// A fresh ARKit run: no photo correction, nothing recognised yet. + private func forgetRun() { + correct(mapFromSession: nil) + recognisedByARKit = false + growing?.cancel() + growing = nil + } + + // MARK: Photo fixes and ARKit's own match + + public func correct(mapFromSession: Pose?) { + correction = mapFromSession.map(MapCorrection.matrix) + let inverse = correction.map { Self.float($0.inverse) } ?? matrix_identity_float4x4 + mapRoot.simdTransform = inverse // the scene renders in the session's world: map → session + watching?.cancel() + guard correction != nil else { return } + watching = Task { [weak self] in // ARKit's own match replaces the photo fix the moment it comes + while !Task.isCancelled { + try? await Task.sleep(for: .milliseconds(250)) + guard let self, !Task.isCancelled else { return } + _ = self.reading() + if self.correction == nil { return } + } + } + } + + /// The camera in the room map's world, and whether the room is recognised (by ARKit or a photo fix). + private func mapCamera() -> (transform: simd_float4x4, tracking: TrackingState)? { + guard let camera = session.currentFrame?.camera else { return nil } + let raw = CaptureSession.tracking(camera.trackingState).0 + if raw == .normal, runningWithMap { + if correction != nil { correct(mapFromSession: nil) } // the session's world is now the map's + if !recognisedByARKit { recognisedByARKit = true; growMapSoon() } + } + guard let correction else { return (camera.transform, raw) } + return (Self.float(correction * Self.double(camera.transform)), .normal) + } + + /// A few seconds after ARKit matched, once it has mapped what the camera sees, the run's map (saved map plus what + /// this visit added, in the same world) goes to `onMapGrown`. At most once a run; never from a photo fix alone. + private func growMapSoon() { + guard onMapGrown != nil, growing == nil else { return } + growing = Task { [weak self] in + try? await Task.sleep(for: .seconds(6)) + guard let self, !Task.isCancelled, self.running, self.recognisedByARKit, + self.session.currentFrame?.worldMappingStatus == .mapped else { return } + guard let data = await self.currentMapData(), data.count < 50_000_000 else { return } + guard !Task.isCancelled else { return } + self.onMapGrown?(data) + } + } + + static func float(_ m: simd_double4x4) -> simd_float4x4 { + simd_float4x4(columns: (SIMD4(m.columns.0), SIMD4(m.columns.1), SIMD4(m.columns.2), + SIMD4(m.columns.3))) + } + + static func double(_ m: simd_float4x4) -> simd_double4x4 { + simd_double4x4(columns: (SIMD4(m.columns.0), SIMD4(m.columns.1), SIMD4(m.columns.2), + SIMD4(m.columns.3))) + } + + private func currentMapData() async -> Data? { + await withCheckedContinuation { cont in + session.getCurrentWorldMap { map, _ in + cont.resume(returning: map.flatMap { + try? NSKeyedArchiver.archivedData(withRootObject: $0, requiringSecureCoding: true) + }) + } + } } private let encoder = FrameEncoder() @@ -84,7 +174,9 @@ public final class ARLocator: Locator { // ARKit's intrinsics are for the full camera image; the JPEG is scaled to a 1024-pixel long edge let s = Double(size.width) / Double(frame.camera.imageResolution.width) let k = frame.camera.intrinsics - let pose = frame.camera.transform.columnMajor.map(Double.init) + // in the room map's world once a photo fix corrected it (the Mac lifts the arrival photo in that world) + let camera = correction.map { Self.float($0 * Self.double(frame.camera.transform)) } ?? frame.camera.transform + let pose = camera.columnMajor.map(Double.init) guard let (mm, w, h) = Self.millimetres(depth) else { return nil } return ArrivalFrame(jpeg: jpeg, depthMM: mm, depthSize: (w, h), imageSize: (Int(size.width), Int(size.height)), intrinsics: (Double(k.columns.0.x) * s, Double(k.columns.1.y) * s, @@ -123,7 +215,7 @@ public final class ARLocator: Locator { up.timingMode = .easeInEaseOut node.runAction(.repeatForever(.sequence([up, up.reversed()]))) } - scene.rootNode.addChildNode(node) + mapRoot.addChildNode(node) arrow = node } @@ -186,7 +278,7 @@ public final class ARLocator: Locator { /// The label the camera points at most directly (within 25°), for "what's this?". public func labelInView() -> ArLabel? { - guard let cam = session.currentFrame?.camera.transform else { return nil } + guard let cam = mapCamera()?.transform else { return nil } let eye = SIMD3(Double(cam.columns.3.x), Double(cam.columns.3.y), Double(cam.columns.3.z)) let fwd = -SIMD3(Double(cam.columns.2.x), Double(cam.columns.2.y), Double(cam.columns.2.z)) let scored = shown.map { label -> (ArLabel, Double) in @@ -216,7 +308,7 @@ public final class ARLocator: Locator { let chevrons = ChevronLayout.place(path: path) guard !chevrons.isEmpty else { return } // without the floor from the Mac, assume the phone is held about 1.4 m above it - let y = Float(floorY ?? Double((session.currentFrame?.camera.transform.columns.3.y ?? 1.4) - 1.4)) + 0.01 + let y = Float(floorY ?? Double((mapCamera()?.transform.columns.3.y ?? 1.4) - 1.4)) + 0.01 let still = UIAccessibility.isReduceMotionEnabled for (i, c) in chevrons.enumerated() { let node = Self.chevronNode() @@ -296,10 +388,9 @@ public final class ARLocator: Locator { } public func reading() -> (pose: Pose, tracking: TrackingState)? { - guard let camera = session.currentFrame?.camera else { return nil } - let m = camera.transform + guard let (m, tracking) = mapCamera() else { return nil } let pose = Pose(columnMajor: [m.columns.0, m.columns.1, m.columns.2, m.columns.3] .flatMap { [Double($0.x), Double($0.y), Double($0.z), Double($0.w)] }) - return (pose, CaptureSession.tracking(camera.trackingState).0) + return (pose, tracking) } } diff --git a/apps/ios/Packages/RecallKit/Sources/RecallDomain/Locate/PhotoFix.swift b/apps/ios/Packages/RecallKit/Sources/RecallDomain/Locate/PhotoFix.swift new file mode 100644 index 0000000..521d0ec --- /dev/null +++ b/apps/ios/Packages/RecallKit/Sources/RecallDomain/Locate/PhotoFix.swift @@ -0,0 +1,83 @@ +import Foundation +import simd + +/// What the Mac's photo matcher said about one camera photo (classic features + PnP against the scans; no model). +public struct PlaceFix: Sendable, Equatable { + /// The room the photo was taken in, when one room clearly matched (it may not be the room being looked for). + public let room: String? + /// The camera's pose in the room map's world, when that room matched well enough. + public let pose: Pose? + public let inliers: Int + /// What to point the phone at in that room, one-of-a-kind furniture first ("tv", "cabinet"). + public let hint: [String] + + public init(room: String?, pose: Pose?, inliers: Int, hint: [String]) { + self.room = room; self.pose = pose; self.inliers = inliers; self.hint = hint + } +} + +/// From a photo fix to the phone's own poses: T(map ← session) = map pose × session pose⁻¹, for the same photo. +public enum MapCorrection { + public static func between(map: Pose, session: Pose) -> simd_double4x4 { matrix(map) * matrix(session).inverse } + + /// A session pose in the room map's world. + public static func apply(_ T: simd_double4x4, to pose: Pose) -> Pose { Pose(columnMajor: columnMajor(T * matrix(pose))) } + + public static func matrix(_ p: Pose) -> simd_double4x4 { + let c = p.columnMajor + return simd_double4x4(columns: (SIMD4(c[0], c[1], c[2], c[3]), SIMD4(c[4], c[5], c[6], c[7]), + SIMD4(c[8], c[9], c[10], c[11]), SIMD4(c[12], c[13], c[14], c[15]))) + } + + public static func columnMajor(_ m: simd_double4x4) -> [Double] { + [m.columns.0, m.columns.1, m.columns.2, m.columns.3].flatMap { [$0.x, $0.y, $0.z, $0.w] } + } + + /// How far apart two transforms are: metres between their origins, degrees between their rotations. + public static func distance(_ a: simd_double4x4, _ b: simd_double4x4) -> (metres: Double, degrees: Double) { + let ta = SIMD3(a.columns.3.x, a.columns.3.y, a.columns.3.z), tb = SIMD3(b.columns.3.x, b.columns.3.y, b.columns.3.z) + let ra = simd_double3x3(SIMD3(a.columns.0.x, a.columns.0.y, a.columns.0.z), SIMD3(a.columns.1.x, a.columns.1.y, a.columns.1.z), + SIMD3(a.columns.2.x, a.columns.2.y, a.columns.2.z)) + let rb = simd_double3x3(SIMD3(b.columns.0.x, b.columns.0.y, b.columns.0.z), SIMD3(b.columns.1.x, b.columns.1.y, b.columns.1.z), + SIMD3(b.columns.2.x, b.columns.2.y, b.columns.2.z)) + let d = ra.transpose * rb + let cosine = max(-1, min(1, (d.columns.0.x + d.columns.1.y + d.columns.2.z - 1) / 2)) + return (simd_distance(ta, tb), acos(cosine) * 180 / .pi) + } +} + +/// One photo fix can be wrong (on real scans about 1 in 9 from another session), so the phone trusts photo fixes +/// only when two in a row agree on the correction: within 25 cm and 10°. Two wrong fixes rarely agree. +public struct FixAgreement: Sendable { + public var maxMetres = 0.25 + public var maxDegrees = 10.0 + private var last: (correction: simd_double4x4, session: Pose)? + + public init() {} + + /// Adds a fix's correction and the session pose its photo had; the agreed correction once two in a row agree. + public mutating func add(_ correction: simd_double4x4, session: Pose) -> simd_double4x4? { + defer { last = (correction, session) } + // a session pose that never changed means ARKit isn't tracking: a correction from it would hold nothing + guard let last, last.session != session else { return nil } + let d = MapCorrection.distance(last.correction, correction) + return d.metres <= maxMetres && d.degrees <= maxDegrees ? correction : nil + } +} + +/// The spoken lines while the room is being recognised. +public enum LocateCoaching { + /// "Point the phone slowly at the TV." then "Now at the cabinet." (`turn` counts the lines said so far). + public static func point(at hint: [String], turn: Int) -> String? { + guard !hint.isEmpty else { return nil } + let name = spoken(hint[turn % hint.count]) + return turn == 0 ? "Point the phone slowly at the \(name)." : "Now at the \(name)." + } + + public static func elsewhere(found: String, wanted: String) -> String { + "You seem to be in \(found), not \(wanted). Walk to \(wanted) and I'll keep looking." + } + + /// A furniture label as said aloud: short ones are initials ("tv" → "TV"). + public static func spoken(_ label: String) -> String { label.count <= 2 ? label.uppercased() : label } +} diff --git a/apps/ios/Packages/RecallKit/Sources/RecallDomain/Ports/Ports.swift b/apps/ios/Packages/RecallKit/Sources/RecallDomain/Ports/Ports.swift index 055cf17..2ee4cf6 100644 --- a/apps/ios/Packages/RecallKit/Sources/RecallDomain/Ports/Ports.swift +++ b/apps/ios/Packages/RecallKit/Sources/RecallDomain/Ports/Ports.swift @@ -112,7 +112,10 @@ public protocol ScanLibrary: Sendable { public protocol EpisodeFiles: Sendable { func directory(scan: String) -> URL func manifest(scan: String) -> Manifest? + /// The scan's room map: the one ARKit grew from later visits when there is one, else the one the scan saved. func roomMap(scan: String) -> Data? + /// Keeps a grown room map (the scan's own stays as uploaded). + func saveGrownRoomMap(scan: String, _ data: Data) func delete(scan: String) } @@ -169,6 +172,16 @@ public protocol Clock: Sendable { func arrivalFrame() -> ArrivalFrame? /// Lays arrows on the floor along the route (ARKit x, z), at the floor's height when known; empty clears them. func showPath(_ path: [SIMD2], floorY: Double?) + /// A photo fix's correction, T(map ← session) as a column-major 4 x 4: from now on `reading()` and + /// `arrivalFrame()` answer in the room map's world and tracking reads normal. nil drops it; so does ARKit's own + /// match of the room (its world then is the map's). + func correct(mapFromSession: Pose?) +} + +/// The Mac's photo matcher: which room a camera photo shows, and the camera's pose in a room map's world. +public protocol PlaceLocating: Sendable { + /// `scan`: the scan whose room map the phone runs; `room`: the room being looked for. + func locate(home: String, scan: String, room: String?, frame: ArrivalFrame) async throws -> PlaceFix } /// The phone's compass (magnetic north; no location is read). diff --git a/apps/ios/Packages/RecallKit/Sources/RecallDomain/Setup/Privacy.swift b/apps/ios/Packages/RecallKit/Sources/RecallDomain/Setup/Privacy.swift index 9b5dea1..afa3e09 100644 --- a/apps/ios/Packages/RecallKit/Sources/RecallDomain/Setup/Privacy.swift +++ b/apps/ios/Packages/RecallKit/Sources/RecallDomain/Setup/Privacy.swift @@ -43,7 +43,8 @@ public enum PrivacyNote { "On your iPhone: your scans until you delete them, the room map used to find your way, and the Mac's " + "address. The device key is in the Keychain.", "Between them: your home network, or an encrypted tunnel you set up (Tailscale or Cloudflare Tunnel). " - + "Every request needs the device key.", + + "Every request needs the device key. While Recall finds the room, about one camera photo a second goes " + + "to your Mac (only there), to be matched against your scans; it is not stored.", ] public static let never = [ diff --git a/apps/ios/Packages/RecallKit/Sources/RecallNetworking/RecallAPI.swift b/apps/ios/Packages/RecallKit/Sources/RecallNetworking/RecallAPI.swift index c0906be..eb07846 100644 --- a/apps/ios/Packages/RecallKit/Sources/RecallNetworking/RecallAPI.swift +++ b/apps/ios/Packages/RecallKit/Sources/RecallNetworking/RecallAPI.swift @@ -70,6 +70,38 @@ public struct RecallAPI: Sendable { } } +// MARK: PlaceLocating + +extension RecallAPI: PlaceLocating { + /// Where am I? One photo to the Mac's matcher (LocateRequest.yaml). A short timeout: an answer that comes late is + /// worth less than the next photo. + public func locate(home: String, scan: String, room: String?, frame: ArrivalFrame) async throws -> PlaceFix { + let boundary = "recall-\(UUID().uuidString)" + var body = Data() + func part(_ name: String, filename: String? = nil, type: String? = nil, _ data: Data) { + body.append(Data("--\(boundary)\r\nContent-Disposition: form-data; name=\"\(name)\"".utf8)) + if let filename { body.append(Data("; filename=\"\(filename)\"".utf8)) } + body.append(Data("\r\n".utf8)) + if let type { body.append(Data("Content-Type: \(type)\r\n".utf8)) } + body.append(Data("\r\n".utf8)) + body.append(data) + body.append(Data("\r\n".utf8)) + } + part("image", filename: "now.jpg", type: "image/jpeg", frame.jpeg) + part("pose", frame.poseJSON) + part("scan_id", Data(scan.utf8)) + if let room { part("room", Data(room.utf8)) } + body.append(Data("--\(boundary)--\r\n".utf8)) + let path = "homes/\(home.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? home)/locate" + var req = request(path, method: "POST", contentType: "multipart/form-data; boundary=\(boundary)", body: body) + req.timeoutInterval = 4 + let json = try await send(req) + let pose = (json["pose"] as? [Double]).flatMap { $0.count == 16 ? Pose(columnMajor: $0) : nil } + return PlaceFix(room: json["room"] as? String, pose: pose, inliers: json["inliers"] as? Int ?? 0, + hint: json["hint"] as? [String] ?? []) + } +} + // MARK: HomeMemory extension RecallAPI: HomeMemory { diff --git a/apps/ios/Packages/RecallKit/Sources/RecallNetworking/openapi.json b/apps/ios/Packages/RecallKit/Sources/RecallNetworking/openapi.json index 13b9233..4d7cfe7 100644 --- a/apps/ios/Packages/RecallKit/Sources/RecallNetworking/openapi.json +++ b/apps/ios/Packages/RecallKit/Sources/RecallNetworking/openapi.json @@ -846,6 +846,58 @@ } } }, + "/homes/{home_id}/locate": { + "post": { + "tags": [ + "homes" + ], + "operationId": "locateByPhoto", + "summary": "Where am I? One camera photo matched against the room scans (classic features + PnP, no model)", + "description": "Asked by the phone while ARKit hasn't yet recognised the saved room map, alongside it (never instead of it). Answers the room the photo was taken in when one room clearly wins, the camera's pose in the requested map's world when that room matched well enough, and what to point the phone at. The photo is matched in memory and not stored.\n", + "parameters": [ + { + "$ref": "#/components/parameters/home_id" + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "$ref": "#/components/schemas/LocateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "What the photo matched", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaceFix" + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "413": { + "$ref": "#/components/responses/TooLarge" + }, + "422": { + "$ref": "#/components/responses/Invalid" + }, + "429": { + "$ref": "#/components/responses/RateLimited" + } + } + } + }, "/homes/{home_id}/robot-package": { "get": { "tags": [ @@ -3120,6 +3172,118 @@ } } }, + "LocateRequest": { + "type": "object", + "required": [ + "image", + "pose", + "scan_id" + ], + "properties": { + "image": { + "type": "string", + "format": "binary", + "description": "JPEG of the camera now (any size; intrinsics are for this image)" + }, + "pose": { + "type": "string", + "description": "JSON: {\"pose\": [16 numbers, ARKit camera-to-world, column-major, in the phone's session (not yet the map's)], \"intrinsics\": {\"fx\", \"fy\", \"cx\", \"cy\"} for the JPEG, \"image_size\": [w, h]}. Only the intrinsics are used on the Mac; the phone keeps the session pose to turn the answer into its own correction.\n" + }, + "scan_id": { + "type": "string", + "maxLength": 64, + "description": "The scan whose room map the phone runs: the pose is answered in its world" + }, + "room": { + "type": "string", + "maxLength": 60, + "description": "The room the phone is trying to recognise (for the hint)" + } + } + }, + "PlaceFix": { + "type": "object", + "required": [ + "room", + "scan_id", + "score", + "runner_up_room", + "runner_up_score", + "pose", + "inliers", + "reproj_px", + "hint", + "ms" + ], + "properties": { + "room": { + "type": [ + "string", + "null" + ], + "description": "The room the photo was taken in, when one room clearly matched (it may not be the requested map's room)" + }, + "scan_id": { + "type": [ + "string", + "null" + ], + "description": "The scan whose world `pose` is in" + }, + "score": { + "type": "integer", + "minimum": 0, + "description": "RANSAC inliers of the best room" + }, + "runner_up_room": { + "type": [ + "string", + "null" + ] + }, + "runner_up_score": { + "type": "integer", + "minimum": 0 + }, + "pose": { + "type": [ + "array", + "null" + ], + "items": { + "type": "number" + }, + "minItems": 16, + "maxItems": 16, + "description": "The camera's pose (ARKit camera-to-world, column-major) in the requested map's world; null unless that room matched with at least 30 inliers and at most 4 px mean reprojection error. The phone applies it only when two fixes in a row agree.\n" + }, + "inliers": { + "type": "integer", + "minimum": 0, + "description": "RANSAC inliers in the requested map's world" + }, + "reproj_px": { + "type": [ + "number", + "null" + ], + "description": "Mean reprojection error of those inliers, pixels" + }, + "hint": { + "type": "array", + "items": { + "type": "string" + }, + "maxItems": 3, + "description": "What to point the phone at in that room, one-of-a-kind furniture first" + }, + "ms": { + "type": "integer", + "minimum": 0, + "description": "Time the Mac took" + } + } + }, "CompassRoute": { "type": "object", "description": "A walk to a thing from where its room's scan began, as compass legs (for a watch with no camera)", diff --git a/apps/ios/Packages/RecallKit/Sources/RecallPersistence/FileStores.swift b/apps/ios/Packages/RecallKit/Sources/RecallPersistence/FileStores.swift index 24cb59b..0c2e2e0 100644 --- a/apps/ios/Packages/RecallKit/Sources/RecallPersistence/FileStores.swift +++ b/apps/ios/Packages/RecallKit/Sources/RecallPersistence/FileStores.swift @@ -38,7 +38,18 @@ public struct FileEpisodeFiles: EpisodeFiles { } public func roomMap(scan: String) -> Data? { - try? Data(contentsOf: directory(scan: scan).appending(path: "worldmap.arworldmap")) + let dir = directory(scan: scan) + return (try? Data(contentsOf: dir.appending(path: Self.grownMap))) + ?? (try? Data(contentsOf: dir.appending(path: "worldmap.arworldmap"))) + } + + /// ARKit's map grown on later visits sits beside the scan's own (which stays exactly as uploaded and checksummed). + static let grownMap = "worldmap.grown.arworldmap" + + public func saveGrownRoomMap(scan: String, _ data: Data) { + let dir = directory(scan: scan) + guard FileManager.default.fileExists(atPath: dir.path) else { return } + try? data.write(to: dir.appending(path: Self.grownMap), options: [.atomic, .completeFileProtection]) } /// The folder, its zip and the upload's part files. diff --git a/apps/ios/Packages/RecallKit/Sources/RecallTestSupport/Fakes.swift b/apps/ios/Packages/RecallKit/Sources/RecallTestSupport/Fakes.swift index 2424d99..f3e53b9 100644 --- a/apps/ios/Packages/RecallKit/Sources/RecallTestSupport/Fakes.swift +++ b/apps/ios/Packages/RecallKit/Sources/RecallTestSupport/Fakes.swift @@ -153,10 +153,41 @@ public final class FakeClock: Clock, @unchecked Sendable { public func showPath(_ path: [SIMD2], floorY: Double?) { paths.append(path) } /// The frame the arrival check gets (nil: the camera has none). public var frame: ArrivalFrame? = ArrivalFrame.sample - public func arrivalFrame() -> ArrivalFrame? { frame } + /// Each frame 1 cm further along x than the last (a live, tracking session), when set. + public var moves = false + private var step = 0.0 + public func arrivalFrame() -> ArrivalFrame? { + guard moves, let f = frame else { return frame } + step += 0.01 + var pose = f.pose + pose[12] += step + return ArrivalFrame(jpeg: f.jpeg, depthMM: f.depthMM, depthSize: f.depthSize, imageSize: f.imageSize, + intrinsics: f.intrinsics, pose: pose) + } + /// The photo correction in use (nil: none), and every one applied. + public private(set) var correction: Pose? + public private(set) var corrections: [Pose?] = [] + public func correct(mapFromSession: Pose?) { correction = mapFromSession; corrections.append(mapFromSession) } + /// Like ARLocator: with a correction, poses are in the map's world and tracking reads normal, until ARKit's own + /// match (a normal reading) drops it. public func reading() -> (pose: Pose, tracking: TrackingState)? { guard !readings.isEmpty else { return nil } - return readings.count > 1 ? readings.removeFirst() : readings[0] + let r = readings.count > 1 ? readings.removeFirst() : readings[0] + guard let c = correction else { return r } + if r.tracking == .normal { correction = nil; return r } + return (MapCorrection.apply(MapCorrection.matrix(c), to: r.pose), .normal) + } +} + +/// The Mac's photo matcher, answering from a script (the last answer repeats); records what it was asked. +public final class FakePlaces: PlaceLocating, @unchecked Sendable { + public var answers: [Result] + public private(set) var asked: [(scan: String, room: String?)] = [] + public init(_ answers: [Result]) { self.answers = answers } + public func locate(home: String, scan: String, room: String?, frame: ArrivalFrame) async throws -> PlaceFix { + asked.append((scan, room)) + guard !answers.isEmpty else { throw RecallError.server("no answer") } + return try (answers.count > 1 ? answers.removeFirst() : answers[0]).get() } } @@ -200,6 +231,8 @@ public final class FakeEpisodeFiles: EpisodeFiles, @unchecked Sendable { public func directory(scan: String) -> URL { URL(fileURLWithPath: "/tmp/scan-\(scan)") } public func manifest(scan: String) -> Manifest? { manifests[scan] } public func roomMap(scan: String) -> Data? { maps[scan] } + public private(set) var grown: [String: Data] = [:] + public func saveGrownRoomMap(scan: String, _ data: Data) { grown[scan] = data; maps[scan] = data } public func delete(scan: String) { deleted.append(scan) } } diff --git a/apps/ios/Packages/RecallKit/Tests/RecallApplicationTests/PhotoAssistTests.swift b/apps/ios/Packages/RecallKit/Tests/RecallApplicationTests/PhotoAssistTests.swift new file mode 100644 index 0000000..1121e30 --- /dev/null +++ b/apps/ios/Packages/RecallKit/Tests/RecallApplicationTests/PhotoAssistTests.swift @@ -0,0 +1,86 @@ +import Foundation +import RecallDomain +import RecallTestSupport +import Testing +@testable import RecallApplication + +private let mapPose = Pose(columnMajor: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 2, 1.4, -1, 1]) + +/// A locator that hasn't recognised the room, whose camera moves a little between photos (a live session). +@MainActor private func limitedLocator() -> FakeLocator { + let l = FakeLocator(readings: [(.identity, .limited)]) + l.moves = true + return l +} + +@MainActor private func assist(_ locator: FakeLocator, _ places: FakePlaces, _ speech: FakeSpeech, _ clock: FakeClock, + room: String? = "Living room") -> PhotoAssist { + var s = PhotoAssist.Settings() + s.maxAsks = 6 + return PhotoAssist(home: "flat", scan: "s1", room: room, locator: locator, places: places, speech: speech, clock: clock, + settings: s) +} + +@MainActor @Test func whenARKitMatchesFirstNoPhotoIsSent() async { + let locator = FakeLocator(readings: [(.identity, .normal)]) + let places = FakePlaces([.success(PlaceFix(room: nil, pose: nil, inliers: 0, hint: []))]) + let a = assist(locator, places, FakeSpeech(), FakeClock()) + await a.run() + #expect(places.asked.isEmpty && !a.fixed) +} + +@MainActor @Test func twoAgreeingFixesLocateThePhoneAtOnce() async { + let locator = limitedLocator(), clock = FakeClock() + let places = FakePlaces([.success(PlaceFix(room: "Living room", pose: mapPose, inliers: 80, hint: ["tv"]))]) + let speech = FakeSpeech() + let a = assist(locator, places, speech, clock) + await a.run() + #expect(a.fixed && places.asked.count == 2 && places.asked[0].scan == "s1") + #expect(locator.correction != nil) + #expect(locator.reading()?.tracking == .normal) // the guide can plan now + #expect(speech.lines == ["Point the phone slowly at the TV."]) +} + +@MainActor @Test func inAnotherRoomItSaysSoOnceAndGivesNoFix() async { + let locator = limitedLocator(), clock = FakeClock() + let places = FakePlaces([.success(PlaceFix(room: "Bedroom", pose: nil, inliers: 0, hint: ["bed"]))]) + let speech = FakeSpeech() + let a = assist(locator, places, speech, clock) + await a.run() + #expect(!a.fixed && locator.corrections.isEmpty && places.asked.count == 6) + #expect(speech.lines == ["You seem to be in Bedroom, not Living room. Walk to Living room and I'll keep looking."]) +} + +@MainActor @Test func withoutTheMacNothingChanges() async { + let locator = limitedLocator(), clock = FakeClock() + let speech = FakeSpeech() + let a = assist(locator, FakePlaces([.failure(RecallError.status(503))]), speech, clock) + await a.run() + #expect(!a.fixed && locator.corrections.isEmpty && speech.lines.isEmpty) +} + +@MainActor @Test func fixesThatDisagreeAreNotUsed() async { + let locator = limitedLocator(), clock = FakeClock() + let elsewhere = Pose(columnMajor: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -2, 1.4, 3, 1]) + let fix = { (p: Pose) in Result.success(PlaceFix(room: nil, pose: p, inliers: 40, hint: [])) } + let places = FakePlaces([fix(mapPose), fix(elsewhere), fix(mapPose), fix(elsewhere), fix(mapPose), fix(elsewhere)]) + let a = assist(locator, places, FakeSpeech(), clock) + await a.run() + #expect(!a.fixed && locator.corrections.isEmpty) +} + +@MainActor @Test func theGuideIsLocatedByPhotosWhenARKitIsSlow() async { + let locator = limitedLocator(), clock = FakeClock() + let places = FakePlaces([.success(PlaceFix(room: "Office", pose: mapPose, inliers: 80, hint: []))]) + let service = FakeGuide(routes: [Route(sessionId: "gs", landmarksOnly: true, reason: "no clear path", + steps: [.init(clock: 12, metres: 2, say: "Your keys are on the desk.")], + waypoints: [], arriveSay: "")]) + var settings = GuideToThing.Settings() + settings.locateTimeout = 10 + let g = GuideToThing(home: "flat", thing: Thing.sample(), roomMap: Data("map".utf8), locator: locator, guide: service, + speech: FakeSpeech(), haptics: FakeHaptics(), clock: clock, photos: (places: places, scan: "s1"), + settings: settings) + await g.run() + #expect(locator.correction != nil || !locator.corrections.isEmpty) + #expect(service.routeRequests.first?.tracking == .normal) // planned as located, not landmarks-only for weak tracking +} diff --git a/apps/ios/Packages/RecallKit/Tests/RecallDomainTests/PhotoFixTests.swift b/apps/ios/Packages/RecallKit/Tests/RecallDomainTests/PhotoFixTests.swift new file mode 100644 index 0000000..27e5e25 --- /dev/null +++ b/apps/ios/Packages/RecallKit/Tests/RecallDomainTests/PhotoFixTests.swift @@ -0,0 +1,48 @@ +import Foundation +import simd +import Testing +@testable import RecallDomain + +/// A pose at (x, y, z), turned `yaw` degrees about the vertical (ARKit y). +private func pose(_ x: Double, _ y: Double, _ z: Double, yaw: Double = 0) -> Pose { + let a = yaw * .pi / 180 + return Pose(columnMajor: [cos(a), 0, -sin(a), 0, 0, 1, 0, 0, sin(a), 0, cos(a), 0, x, y, z, 1]) +} + +@Test func aCorrectionTurnsSessionPosesIntoTheMapsWorld() { + let T = MapCorrection.matrix(pose(2, 0, -1, yaw: 30)) // the session's world inside the map's + let session = pose(0.5, 1.4, 0.2, yaw: -10) + let map = MapCorrection.apply(T, to: session) + let back = MapCorrection.between(map: map, session: session) + let d = MapCorrection.distance(back, T) + #expect(d.metres < 1e-9 && d.degrees < 1e-6) + // a later session pose lands where the same correction puts it + let later = MapCorrection.apply(back, to: pose(1.5, 1.4, 0.2, yaw: -10)) + #expect(abs(simd_distance(SIMD2(later.columnMajor[12], later.columnMajor[14]), + SIMD2(map.columnMajor[12], map.columnMajor[14])) - 1) < 1e-9) +} + +@Test func twoFixesMustAgreeAndComeFromALiveSession() { + var a = FixAgreement() + let T1 = MapCorrection.matrix(pose(2, 0, -1, yaw: 30)), near = MapCorrection.matrix(pose(2.1, 0, -1, yaw: 33)) + #expect(a.add(T1, session: pose(0, 1.4, 0)) == nil) // one fix proves nothing + #expect(a.add(near, session: pose(0.05, 1.4, 0)) != nil) // 10 cm and 3° apart: agreed + + var far = FixAgreement() + _ = far.add(T1, session: pose(0, 1.4, 0)) + #expect(far.add(MapCorrection.matrix(pose(3, 0, -1, yaw: 30)), session: pose(0.05, 1.4, 0)) == nil) // 1 m apart + #expect(far.add(MapCorrection.matrix(pose(3, 0, -1, yaw: 31)), session: pose(0.1, 1.4, 0)) != nil) // then two agree + + var frozen = FixAgreement() + _ = frozen.add(T1, session: pose(0, 1.4, 0)) + #expect(frozen.add(T1, session: pose(0, 1.4, 0)) == nil) // the same session pose twice: ARKit isn't tracking +} + +@Test func coachingNamesThingsToPointAt() { + #expect(LocateCoaching.point(at: ["tv", "cabinet"], turn: 0) == "Point the phone slowly at the TV.") + #expect(LocateCoaching.point(at: ["tv", "cabinet"], turn: 1) == "Now at the cabinet.") + #expect(LocateCoaching.point(at: ["tv", "cabinet"], turn: 2) == "Now at the TV.") + #expect(LocateCoaching.point(at: [], turn: 0) == nil) + #expect(LocateCoaching.elsewhere(found: "Bedroom", wanted: "Living room") + == "You seem to be in Bedroom, not Living room. Walk to Living room and I'll keep looking.") +} diff --git a/apps/ios/Recall/App/AppContainer.swift b/apps/ios/Recall/App/AppContainer.swift index c26ec85..ee5e568 100644 --- a/apps/ios/Recall/App/AppContainer.swift +++ b/apps/ios/Recall/App/AppContainer.swift @@ -67,7 +67,8 @@ final class AppContainer { idle?.cancel() let scans = scans return ARLabelsModel(room: room, home: settings.connection.homeId, source: arLabelsSource(), locator: locator, - roomMap: { scans.roomMap(scan: $0) }, speech: speech) + roomMap: { scans.roomMap(scan: $0) }, saveGrownMap: { scans.saveGrownMap(scan: $0, $1) }, + places: UITestSamples.enabled ? nil : server, speech: speech, haptics: haptics, clock: clock) } func arLabelsSource() -> any ArLabelsSource { @@ -93,13 +94,14 @@ final class AppContainer { func findModel() -> FindModel { FindModel(ask: AskWhereIs(guide: UITestSamples.enabled ? UITestSamples.Guide() : server), transcriber: server, voice: WavRecorder(), things: things, settings: settings, speech: speech, haptics: haptics, - prepareGuide: { [weak self] in self?.prepareGuide() }) + prepareGuide: { [weak self] thing in self?.prepareGuide(room: thing.room) }) } - /// Starts recognising the saved room in the background; paused again if no guide starts within a minute. - func prepareGuide() { - guard ARLocator.isSupported, !guiding, let map = scans.roomMap() else { return } - locator.prepare(roomMap: map) + /// Starts recognising the thing's room in the background; paused again if no guide starts within a minute. + func prepareGuide(room: String?) { + guard ARLocator.isSupported, !guiding, let map = scans.guideMap(room: room) else { return } + growMap(scan: map.scan) + locator.prepare(roomMap: map.data) idle?.cancel() idle = Task { [weak self] in try? await Task.sleep(for: .seconds(60)) @@ -111,12 +113,21 @@ final class AppContainer { func guideModel(for thing: Thing) -> GuideModel { idle?.cancel() guiding = true - let use = GuideToThing(home: settings.connection.homeId, thing: thing, roomMap: scans.roomMap(), locator: locator, - guide: server, speech: speech, haptics: haptics, clock: clock, compass: compass) + let map = scans.guideMap(room: thing.room) + if let map { growMap(scan: map.scan) } + let use = GuideToThing(home: settings.connection.homeId, thing: thing, roomMap: map?.data, locator: locator, + guide: server, speech: speech, haptics: haptics, clock: clock, compass: compass, + photos: map.map { (places: server, scan: $0.scan) }) return GuideModel(guide: use, session: locator.session, scene: locator.scene, supported: ARLocator.isSupported, ended: { [weak self] in self?.guiding = false }) } + /// When ARKit recognises this scan's room, the map it grew is kept for next time. + private func growMap(scan: String) { + let scans = scans + locator.onMapGrown = { data in scans.saveGrownMap(scan: scan, data) } + } + /// `again`: a processed scan of this room to scan again in the same frame (its room map is recognised first). func scanModel(room: String, again: Scan? = nil) -> ScanModel { idle?.cancel() @@ -135,7 +146,7 @@ struct SystemClock: Clock { /// The Mac, as every Mac-facing port, reached with the current connection settings. Changing Settings swaps the /// client underneath; a call made before setup throws the setup issue, in words. -final class LiveServer: HomeMemory, GuideService, Transcriber, ScanProcessing, ConnectionCheck, ScanServer, RobotSharing, +final class LiveServer: HomeMemory, GuideService, PlaceLocating, Transcriber, ScanProcessing, ConnectionCheck, ScanServer, RobotSharing, RoomMaps, RobotPackages, CompassRoutes, ArLabelsSource, TrustSettings, PrivacySource, EvidenceSource, @unchecked Sendable { private let lock = NSLock() private var api: RecallAPI? @@ -179,6 +190,9 @@ final class LiveServer: HomeMemory, GuideService, Transcriber, ScanProcessing, C func arLabels(home: String, room: String) async throws -> ArLabels { try await current().arLabels(home: home, room: room) } + func locate(home: String, scan: String, room: String?, frame: ArrivalFrame) async throws -> PlaceFix { + try await current().locate(home: home, scan: scan, room: room, frame: frame) + } func compassRoute(home: String, thing: String) async throws -> CompassRoute { try await current().compassRoute(home: home, thing: thing) } diff --git a/apps/ios/Recall/Features/AR/ARLabelsModel.swift b/apps/ios/Recall/Features/AR/ARLabelsModel.swift index da326b2..b243466 100644 --- a/apps/ios/Recall/Features/AR/ARLabelsModel.swift +++ b/apps/ios/Recall/Features/AR/ARLabelsModel.swift @@ -1,6 +1,7 @@ import ARKit import Foundation import Observation +import RecallApplication import RecallCapture import RecallDomain import SwiftUI @@ -20,13 +21,26 @@ final class ARLabelsModel { private let source: any ArLabelsSource private let home: String private let roomMap: (String) -> Data? + private let saveGrownMap: (String, Data) -> Void + private let places: (any PlaceLocating)? private let speech: any SpeechOutput + private let haptics: (any HapticsOutput)? + private let clock: any Clock private var task: Task? + private var assist: PhotoAssist? + /// Seconds before the list is shown instead, and how long the room is still looked for after that. + static let listAfter = 25.0 + static let keepLooking = 120.0 + + /// `places`: the Mac's photo matcher, to recognise the room alongside ARKit; `saveGrownMap`: keeps the map ARKit + /// grew on this visit, so the next one recognises the room from here too. init(room: String, home: String, source: any ArLabelsSource, locator: ARLocator, roomMap: @escaping (String) -> Data?, - speech: any SpeechOutput) { + saveGrownMap: @escaping (String, Data) -> Void = { _, _ in }, places: (any PlaceLocating)? = nil, + speech: any SpeechOutput, haptics: (any HapticsOutput)? = nil, clock: any Clock = SystemClock()) { self.room = room; self.home = home; self.source = source; self.locator = locator; self.roomMap = roomMap - self.speech = speech + self.saveGrownMap = saveGrownMap; self.places = places; self.speech = speech; self.haptics = haptics + self.clock = clock } func start() { @@ -35,6 +49,8 @@ final class ARLabelsModel { func stop() { task?.cancel() + assist?.cancel() + locator.onMapGrown = nil locator.clearLabels() locator.stop() } @@ -57,20 +73,40 @@ final class ARLabelsModel { } phase = .locating speech.say("Point the phone around \(room) so I can recognise it.", interrupt: true) + let scan = found.scanId + locator.onMapGrown = { [saveGrownMap] data in saveGrownMap(scan, data) } locator.start(roomMap: map) - // ARKit says the room is recognised when tracking turns normal in the saved map - for _ in 0..<50 { - try await Task.sleep(for: .milliseconds(500)) - if locator.reading()?.tracking == .normal { break } + // the Mac's photo matcher helps alongside ARKit (named furniture to point at, a fix from a photo) + if let places { + let a = PhotoAssist(home: home, scan: scan, room: room, locator: locator, places: places, speech: speech, + clock: clock) + assist = a + Task { await a.run() } } - guard locator.reading()?.tracking == .normal else { - return phase = .listOnly("I couldn't recognise \(room) from here, so here is the list.") + // recognised when tracking turns normal: ARKit matched the saved map, or two photo fixes agreed + let started = clock.now() + var late = false + while locator.reading()?.tracking != .normal { + let waited = clock.now().timeIntervalSince(started) + if !late, waited >= Self.listAfter { + late = true // the list now, but keep looking: the labels come on if the room is recognised + phase = .listOnly("I couldn't recognise \(room) from here yet, so here is the list. Still looking.") + } + if waited >= Self.listAfter + Self.keepLooking { + assist?.cancel() + return phase = .listOnly("I couldn't recognise \(room) from here, so here is the list.") + } + try await Task.sleep(for: .milliseconds(250)) } + assist?.cancel() + haptics?.tap() let tags = labels.map { (label: $0, tag: Self.tagImage($0)) } locator.showLabels(tags, floorY: found.floorY) phase = .showing let n = labels.count == 1 ? "1 thing" : "\(labels.count) things" - speech.say("\(n) in \(room). Point at a label and double tap with two fingers to hear it.", interrupt: false) + let lead = late ? "Recognised \(room). " : "" + speech.say("\(lead)\(n) in \(room). Point at a label and double tap with two fingers to hear it.", + interrupt: late) } catch is CancellationError { } catch { phase = .failed(UserMessage.explain(error)) diff --git a/apps/ios/Recall/Features/Find/FindModel.swift b/apps/ios/Recall/Features/Find/FindModel.swift index 521a891..7d9eb82 100644 --- a/apps/ios/Recall/Features/Find/FindModel.swift +++ b/apps/ios/Recall/Features/Find/FindModel.swift @@ -24,10 +24,10 @@ final class FindModel { private let speech: any SpeechOutput private let haptics: any HapticsOutput /// Starts recognising the room while the answer is read out, so "Take me there" begins at once. - private let prepareGuide: @MainActor () -> Void + private let prepareGuide: @MainActor (Thing) -> Void init(ask: AskWhereIs, transcriber: any Transcriber, voice: any VoiceInput, things: ThingsModel, settings: SettingsModel, - speech: any SpeechOutput, haptics: any HapticsOutput, prepareGuide: @escaping @MainActor () -> Void = {}) { + speech: any SpeechOutput, haptics: any HapticsOutput, prepareGuide: @escaping @MainActor (Thing) -> Void = { _ in }) { self.ask = ask; self.transcriber = transcriber; self.voice = voice; self.things = things self.settings = settings; self.speech = speech; self.haptics = haptics; self.prepareGuide = prepareGuide } @@ -72,7 +72,7 @@ final class FindModel { about: lastAnswer?.answer.thingId) lastAnswer = result state = .answered(result) - if result.thing != nil { prepareGuide() } + if let thing = result.thing { prepareGuide(thing) } // its room's map starts recognising now speech.say(result.answer.text + (result.thing == nil ? "" : " Tap Take me there for directions."), interrupt: true) } catch { state = .failed(UserMessage.explain(error)) diff --git a/apps/ios/Recall/Models/ScansModel.swift b/apps/ios/Recall/Models/ScansModel.swift index 65d5954..2eeb20d 100644 --- a/apps/ios/Recall/Models/ScansModel.swift +++ b/apps/ios/Recall/Models/ScansModel.swift @@ -106,14 +106,21 @@ final class ScansModel { return await Task.detached { archiver.archive(episode: dir) }.value } - /// The saved room map to relocalise in: the home's first processed scan made on this phone (routes are planned - /// in that scan's world). - /// The room map guidance starts from: the newest processed scan's (a recognised rescan's map holds the first - /// scan's world, extended). - func roomMap() -> Data? { - scans.filter(\.isProcessed).sorted { $0.createdAt > $1.createdAt }.lazy.compactMap { self.files.roomMap(scan: $0.id) }.first + /// The room map to guide to a thing in `room`: that room's newest processed scan with a map on this phone (the + /// Mac plans in that scan's world: a scan that wasn't recognised started the room's memory over). Without a room, + /// or no map of it, the newest processed scan's. + func guideMap(room: String?) -> (scan: String, data: Data)? { + let ready = scans.filter(\.isProcessed).sorted { $0.createdAt > $1.createdAt } + let inRoom = room.map { r in ready.filter { $0.room == r } } ?? [] + for s in inRoom + ready { + if let data = files.roomMap(scan: s.id) { return (s.id, data) } + } + return nil } + /// Keeps the room map ARKit grew on a later visit (the next visit recognises the room from there too). + func saveGrownMap(scan: String, _ data: Data) { files.saveGrownRoomMap(scan: scan, data) } + /// One scan's saved room map (the AR labels are placed in it). func roomMap(scan id: String) -> Data? { files.roomMap(scan: id) } From 5e427c644149898b3b0a6031326b61829b2c5e18 Mon Sep 17 00:00:00 2001 From: Rachit Date: Mon, 2 Nov 2026 12:16:30 +0100 Subject: [PATCH 05/30] docs: how the room is recognised from anywhere, with measured results Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 3 +- README.md | 14 +++- apps/web/i18n/dictionaries/en.ts | 2 +- docs/privacy.md | 3 +- evals/locate_photo.py | 113 +++++++++++++++++++++++++++++++ evals/results.md | 40 +++++++++++ 6 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 evals/locate_photo.py create mode 100644 evals/results.md diff --git a/CLAUDE.md b/CLAUDE.md index 75f96ec..5d2b037 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -106,7 +106,8 @@ domain ← application ← adapters (inbound + outbound) - **API `domain/ports.py`** (one repository per aggregate): `HomeRepository`, `ScanRepository`, `TrackedObjectRepository`. - **API `application/ports.py`** (needs of use cases): `LlmProvider`, `VisionProvider` (Muse Spark 1.1, tie-break only), `Transcriber`, `BlobStore`, `PipelineTrigger`, `Clock`, `Segmenter` (SAM 3.1 over HTTP on the Meta API, for the Verifier), `ArrivalVerifier`, - `PromptScreener` (Muse Spark Open or Glimmer, after schema validation). + `PromptScreener` (Muse Spark Open or Glimmer, after schema validation), `PlaceMatcher` (classic ORB + PnP photo + matching against each scan's `place_index.npz`, no model; `POST /homes/{id}/locate`, ADR-039). - **Pipeline `application/ports.py`**: `Reconstructor`, `Segmenter`, `EpisodeSource` (reads from R2 or disk), `ResultSink` (posts to the API). **torch and local models live only in the pipeline**; the API reaches vision models over HTTP only. diff --git a/README.md b/README.md index 4cbf5aa..0f60997 100644 --- a/README.md +++ b/README.md @@ -429,6 +429,7 @@ routes match it. While `make demo` or `make live` runs, **Swagger UI** is at `ht | `DELETE /scans/{scan_id}` | Erase a scan from your computer, with what only it found | | `GET /homes/{home_id}/objects`, `GET /homes/{home_id}/room`, `GET /homes/{home_id}/room-map` | What Recall found, the room model, and each room's map for Find | | `POST /guidance/sessions`, `POST /guidance/sessions/{session_id}/verify` | Directions, and the arrival check | +| `POST /homes/{home_id}/locate` | Where am I? One camera photo matched against the room scans: the room, the pose, what to point at | | `GET /privacy`, `GET /audit` | What is sent where right now; who did what | | `GET /homes/{home_id}/robot-export`, `/a2a/home-memory` | For robots: the Nav2 map, and the Home Memory agent ([Agent Card](services/api/resources/agents/home-memory.card.json)) | @@ -451,6 +452,15 @@ Every route except `/health` needs a key: the owner's device key, or a robot's o 4. *Not found is a fixed sentence,* never the model's: "I haven't seen that yet." 5. *Tested:* `tests/unit/test_grounding.py` (an invented place or number is caught) and `tests/unit/test_ask_where_is.py` (an invented place is corrected, then replaced by the tool facts; a place with no sighting behind it is never said). - **Every answer is traceable.** Sightings are append-only. Every answer is logged with its model and tool steps and the sightings it rests on, and **Why?** on the phone (or `GET /answers/{answer_id}/evidence`, or asking the Guide "why?") leads back to the camera's view, the scan and the models that made it. An answer that names a place with no sighting behind it is never said. +- **Recognising the room from anywhere, never on one photo's word.** ARKit's own match of the saved room map is + the main signal. While it hasn't matched, the phone sends about one photo a second to the Mac, which matches it + against every room's scan photos with classic ORB features, the scan's own LiDAR depth and PnP (OpenCV, no model; + `adapters/outbound/vision/orb_place_matcher.py`). The phone uses a photo pose only when **two fixes in a row agree** + within 25 cm and 10°, and ARKit's own match replaces it the moment it comes. The Mac also says which room the photo + shows ("You seem to be in Bedroom, not Living room") and what to point at ("the TV"). Each time ARKit recognises + the room, the map it grew is kept on the phone, so the next visit matches from there too. Measured on the real + living-room scans: held-out photos 1.2 cm median error; photos from other sessions fix 59% of the time, 89% of + them right; 22 to 35 ms per match ([evals/results.md](evals/results.md)). - **The Verifier can only downgrade.** The vision model may turn a result into "uncertain"; only SAM plus geometry can confirm. - **Two checks on the door.** A robot or outside app can send only one object name (letters, digits, spaces and hyphens, at most 40 characters, `services/api/resources/agents/find-object.input.schema.json`); free text is refused and never reaches a model or a tool. Then Muse Spark Open (or Glimmer) must answer `{"verdict": "safe", ...}` in strict JSON; anything else, a timeout or "injection" refuses. Measured: 21 ordinary names allowed, 18 instruction-like names refused, none wrong. - **Privacy and security.** See [Security and privacy](#security-and-privacy): what stays on the Mac, what is sent where, and how the home's memory is protected. @@ -553,7 +563,9 @@ erDiagram - **Indexes:** scans, landmarks, objects and rejections by home; observations and set-asides by object; audit events by time and by action; the answer log by home and time. - **The job queue** is a separate SQLite file (`data/jobs.db`, Huey) that holds only queued scan jobs. -- **Not in SQL:** the scan files (`data/blobs` on the Mac, or Cloudflare R2). On the iPhone there is no database: +- **Not in SQL:** the scan files (`data/blobs` on the Mac, or Cloudflare R2), and each scan's `place_index.npz` beside + them (ORB descriptors of the keyframes and their 3D points, for the photo matcher; built by the pipeline, or + `make place-index`). On the iPhone there is no database: scans are folders in Application Support, the device key is in the Keychain, and the Mac's address is in UserDefaults. diff --git a/apps/web/i18n/dictionaries/en.ts b/apps/web/i18n/dictionaries/en.ts index 4348cd5..a6726ea 100644 --- a/apps/web/i18n/dictionaries/en.ts +++ b/apps/web/i18n/dictionaries/en.ts @@ -500,7 +500,7 @@ export const en = { kept: [ "On your Mac: every scan you upload (photos, LiDAR depth, the phone's path through the room), the 3D map, the things and furniture found, your own names and notes for things, and the activity log.", "On your iPhone: your scans until you delete them, the room map used to find your way, and the Mac's address. The device key is in the iPhone's Keychain.", - "Between them: your home network, or an encrypted tunnel you set up yourself (Tailscale or Cloudflare Tunnel). Every request needs the device key.", + "Between them: your home network, or an encrypted tunnel you set up yourself (Tailscale or Cloudflare Tunnel). Every request needs the device key. While Recall finds the room, about one camera photo a second goes to your Mac (only there), to be matched against your scans; it is not stored.", ], sentTitle: "What leaves your Mac, to whom, and when", sentLabels: { to: "To", when: "When" }, diff --git a/docs/privacy.md b/docs/privacy.md index 8960c3c..f954921 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -15,7 +15,8 @@ nobody else holds your scans or your memory. Some steps use **Meta's models onli - **On your iPhone**: your scans until you delete them, the room map used to find your way, and the Mac's address. The device key is in the iPhone's Keychain. - Between them: your home network, or an encrypted tunnel you set up yourself (Tailscale or Cloudflare Tunnel). Every - request needs the device key. + request needs the device key. While Recall finds the room, about one camera photo a second goes to your Mac (only + there), to be matched against your scans; it is not stored. ## What leaves your Mac, to whom, and when diff --git a/evals/locate_photo.py b/evals/locate_photo.py new file mode 100644 index 0000000..5885bb8 --- /dev/null +++ b/evals/locate_photo.py @@ -0,0 +1,113 @@ +"""Replays real scans through the photo matcher (evals/results.md, "Recognising the room from a photo"). + + cd services/api && uv run python ../../evals/locate_photo.py REFERENCE_SCAN_ID [OTHER_SCAN_ID ...] + +Needs the scans on this Mac (data/blobs/scans) with their place indexes (`make place-index`). Prints the held-out +accuracy on the reference scan, then for each other scan the fix rate and the share of fixes that agree with the +consensus scan-to-scan transform. +""" + +from __future__ import annotations + +import json +import sys +import tempfile +import time +from pathlib import Path + +import numpy as np + +from recall_api.adapters.outbound.vision.orb_place_matcher import OrbPlaceMatcher +from recall_api.application.ports import PlaceQuery + +SCANS = Path(__file__).resolve().parents[1] / "data" / "blobs" / "scans" + + +class Disk: + def local_path(self, key): + return key + + +def episode(prefix: str) -> Path: + return next(SCANS.glob(f"*_{prefix}*/episode")) + + +def poses(ep: Path) -> dict[int, np.ndarray]: + out = {} + for line in (ep / "poses.jsonl").read_text().splitlines(): + p = json.loads(line) + out[p["index"]] = np.array(p["pose"], float).reshape(4, 4).T + return out + + +def query(ep: Path, i: int) -> PlaceQuery: + k = json.loads((ep / "manifest.json").read_text())["intrinsics"] + return PlaceQuery((ep / "frames" / f"{i:06d}.jpg").read_bytes(), k["fx"], k["fy"], k["cx"], k["cy"]) + + +def angle(a: np.ndarray, b: np.ndarray) -> float: + d = a[:3, :3].T @ b[:3, :3] + return float(np.degrees(np.arccos(np.clip((np.trace(d) - 1) / 2, -1, 1)))) + + +def fixed(r) -> bool: + return bool(r) and r[0].pose is not None and r[0].inliers >= 30 and r[0].reproj_px <= 4 + + +def held_out(matcher: OrbPlaceMatcher, ep: Path, out: Path) -> None: + z = dict(np.load(ep / "place_index.npz")) + ids = z["frame_ids"] + rows = np.arange(len(ids))[1::2] # the index keeps every other keyframe; the rest are the photos + sel = np.isin(z["frame"], rows) + remap = {r: k for k, r in enumerate(rows)} + sub = {**z, "desc": z["desc"][sel], "xyz": z["xyz"][sel], "frame": np.array([remap[f] for f in z["frame"][sel]]), + "frame_ids": ids[rows], "poses": z["poses"][rows]} # fmt: skip + np.savez(out / "held.npz", **sub) + truth = poses(ep) + errs, angs, times = [], [], [] + photos = [int(ids[r]) for r in range(0, len(ids), 2)] + for i in photos: + t = time.perf_counter() + r = matcher.match(query(ep, i), {"held": str(out / "held.npz")}) + times.append(time.perf_counter() - t) + if fixed(r): + P = np.array(r[0].pose).reshape(4, 4).T + errs.append(np.linalg.norm(P[:3, 3] - truth[i][:3, 3])) + angs.append(angle(P, truth[i])) + print(f"held-out: {len(errs)}/{len(photos)} fixes; {np.median(errs) * 100:.1f} cm median, " + f"{np.percentile(errs, 90) * 100:.1f} cm p90; {np.median(angs):.2f} deg median; " + f"{np.median(times) * 1000:.0f} ms median") # fmt: skip + + +def other_session(matcher: OrbPlaceMatcher, ref: Path, other: Path) -> None: + truth = poses(other) + photos = [i for i in sorted(truth)[::3] if (other / "frames" / f"{i:06d}.jpg").exists()] + Ts = [] + for i in photos: + r = matcher.match(query(other, i), {"ref": str(ref / "place_index.npz")}) + if fixed(r): + Ts.append(np.array(r[0].pose).reshape(4, 4).T @ np.linalg.inv(truth[i])) + + def close(a, b): + return np.linalg.norm(a[:3, 3] - b[:3, 3]) < 0.25 and angle(a, b) < 10 + + if not Ts: + print(f"{other.parent.name[:40]}: 0/{len(photos)} fixes") + return + best = Ts[int(np.argmax([sum(close(a, b) for b in Ts) for a in Ts]))] + right = sum(close(best, t) for t in Ts) + print(f"{other.parent.name[:40]}: {len(Ts)}/{len(photos)} fixes, {right} right") + + +def main(argv: list[str]) -> int: + matcher = OrbPlaceMatcher(Disk()) + ref = episode(argv[0]) + with tempfile.TemporaryDirectory() as d: + held_out(matcher, ref, Path(d)) + for o in argv[1:]: + other_session(matcher, ref, episode(o)) + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/evals/results.md b/evals/results.md new file mode 100644 index 0000000..0db1b90 --- /dev/null +++ b/evals/results.md @@ -0,0 +1,40 @@ +# Evaluation results + +Dated measurements on real scans. Each row names what was run, so it can be run again. + +## Recognising the room from a photo (2026-11-02) + +The photo matcher behind `POST /homes/{home_id}/locate`: ORB features of one camera photo matched against a scan's +place index (every keyframe's ORB features lifted to 3D with the phone's own LiDAR depth and ARKit pose), then PnP +with RANSAC. Classic OpenCV, no model. Data: the author's living room, six scans made Oct 30 to Nov 2 with the +iPhone 16 Pro; the reference is the published scan (`bb449f4c`, 162 keyframes). + +| Test | Photos | Fix (≥ 30 inliers, ≤ 4 px) | Right | Accuracy | Time on the Mac (M5 Pro) | +|---|---|---|---|---|---| +| Held-out keyframes of the same scan (index from the other half) | 81 | 100% | 100% | 1.2 cm median, 4.1 cm p90; 0.55° median | 22 ms median, 25 ms p90 | +| Photos from four other sessions (Nov 1 to Nov 2) | 242 | 62% | 90% | — | 33 to 35 ms median | +| Photos from the oldest session (Oct 30; furniture since moved) | 19 | 26% | 60% | — | 34 ms median | + +"Right" for other sessions: their ARKit worlds differ from the reference, so a fix is right when the scan-to-scan +transform it implies agrees (within 25 cm and 10°) with the one most fixes agree on. + +Threshold sweep over all 261 photos from other sessions (fix rate, then share right): + +| Inliers ≥ | 15 | 20 | 30 | 40 | 50 | 70 | 100 | +|---|---|---|---|---|---|---|---| +| Fix rate | 66% | 63% | 59% | 53% | 49% | 38% | 30% | +| Right | 87% | 89% | 89% | 94% | 95% | 98% | 97% | + +**What the phone does with it:** a photo pose is used only when two fixes in a row agree within 25 cm and 10° +(`FixAgreement`), and ARKit's own match replaces it as soon as it comes. With about 6 in 10 photos giving a fix and +one photo a second, two agreeing fixes typically take 2 to 4 s. + +**Found on the way:** none of the five rescans of this room was recognised by ARKit at the start of the rescan (all +are marked `fresh` in the database), which is the problem this feature addresses. + +To run it again: `make place-index`, then +`cd services/api && uv run python ../../evals/locate_photo.py bb449f4c 926c7d73 56c966eb 63bdddb5 236dc98e 7a736bf2`. + +**Still to measure on the iPhone:** time from opening "See it in AR" to the labels, from five start spots (doorway, +corner, sofa, facing a wall, hallway), before and after; and whether a grown room map matches from a spot that failed +before. From c73238d00db10ff116d61073f797ea45dd635a9f Mon Sep 17 00:00:00 2001 From: Rachit Date: Mon, 2 Nov 2026 12:41:54 +0100 Subject: [PATCH 06/30] docs(readme): recognising the room in how it works, algorithms and results; today's test counts Co-Authored-By: Claude Opus 5.5 --- README.md | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0f60997..16aaf11 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,7 @@ services/ api/ FastAPI: domain/ → application/ → adapters/ + infra/ ; resources/ = the OpenAPI spec (source of truth) pipeline/ the scan pipeline, same layers: MapAnything, SAM 3.1, planes, merging, the room model docs/ architecture, privacy, verification, Meta Muse (docs/muse.md) -evals/ questions, usability plan +evals/ questions, usability plan, results and the replay scripts ``` - **Hexagonal (ports and adapters) in every service**: domain ← application ← adapters and infra. @@ -378,14 +378,18 @@ runs its software or its models; every model Recall itself calls is a Meta model 1. The phone sends a spoken question to the Mac, and Muse Voice Transcribe turns it into text (typed questions skip this). 2. The phone says what it is looking for at once ("Looking for your keys."). 3. The Guide calls `find_object`, which returns each match with its room, landmark, relation ("on the desk"), shelf level, height band and when it was seen; one call usually answers. The answer is logged with the sightings behind it, and **Why?** reads them out. -4. **Take me there**: `localize` from the phone's ARKit pose, `plan_route` over an occupancy grid, clock-face steps spoken as you walk. +4. **Take me there**: the phone recognises the room (ARKit's match of the saved room map, helped by a photo match on + the Mac about once a second, trusted only when two fixes agree, and spoken hints such as "point the phone at the + TV"; landmark directions if neither matches), then `localize` from the phone's pose, `plan_route` over an + occupancy grid, clock-face steps spoken as you walk. 5. On arrival the Verifier checks the camera frame; if the thing is nearby, memory moves it. **OS and browser features (not model calls)** | Feature | Used for | Where | |---|---|---| -| LiDAR depth, ARKit tracking, ARWorldMap | Capture in metres; relocalisation for live guidance | iPhone | +| LiDAR depth, ARKit tracking, ARWorldMap | Capture in metres; relocalisation for live guidance; the map grows on each visit | iPhone | +| OpenCV ORB features + PnP (classic geometry, not a model) | Recognising the room from one camera photo, beside ARKit | Mac (`services/api/.../vision/orb_place_matcher.py`) | | ARKit poses, compass (magnetic north, no location) | Speed and turn rate for scan coaching come from the ARKit poses; the scan learns where north lies in the room, so directions can say "north-east" | iPhone | | Haptics | Turn and arrival cues | iPhone (Apple Watch planned) | | VoiceOver, Dynamic Type | The app is usable without looking | iPhone | @@ -580,6 +584,10 @@ erDiagram - **Spatial relations:** "on" (within 5 cm of a surface and inside its footprint), left or right of a landmark's front, nearest landmark within 1.5 m, and five height bands (floor, knee, waist, chest, head). - **What changed:** a rescan matches each kind of thing with the room's last scan by optimal assignment on 3D distance; moved more than 30 cm, new, or gone is decided by geometry, and things outside the rescan's view are left alone. - **Navigation:** an occupancy grid at 10 cm from the point cloud, obstacles widened by a 0.3 m body radius and paths kept to the middle of free space, A* to a reachable cell near the object, simplified to turns and spoken as clock-face steps. A gap too narrow gives "no safe path" and landmark directions instead. +- **Recognising the room:** after processing, each scan's keyframes become ORB features lifted to 3D with the phone's + LiDAR depth and ARKit pose (`place_index.npz`). One camera photo is matched against them (ratio test, PnP with + RANSAC; at least 30 inliers within 4 px), giving the room and the camera's pose in the room map's world. The phone + trusts two fixes in a row that agree within 25 cm and 10°, and ARKit's own match replaces them. - **Arrival check:** SAM 3.1's box in one camera frame, lifted to 3D with the LiDAR depth; within 30 cm of the remembered place is confirmed. - **Viewer:** voxel downsampling to about 150k points; a vertex shader that gathers every point from a scattered shell to its real position, floor first. @@ -593,14 +601,18 @@ Measured, dated, and recorded in `docs/verification.md`. Nothing below is estima - **Without a model:** LiDAR depth back-projected with the ARKit poses put all 10 objects of the living room within **2 to 7 cm** of the MapAnything run, in **0.2 s**. - **Answer time:** the Guide on Muse Spark Open answers in **2.3 s** median, **2.7 s** p90 (10 real questions, one tool call each); the phone speaks an acknowledgement at once. - **Arrival check:** on the real keys, SAM 3.1 plus the LiDAR depth put them **3 cm** from the remembered place, in **4.9 s**. +- **Recognising the room from a photo** ([evals/results.md](evals/results.md)): held-out photos of the living room + came out **1.2 cm** (median) and **0.55°** from ARKit's own poses; photos from other sessions gave a fix **59 %** of + the time and **89 %** of those fixes were right, so the phone waits for two that agree; **22 to 35 ms** per match on + the M5 Pro. On the way: none of five rescans of that room was recognised by ARKit alone, the problem this fixes. - **Robot screening:** **21** ordinary object names allowed and **18** instruction-like names refused, none wrong. - **Cost, measured:** a room scan is **456** SAM 3.1 calls and **48 MB** on disk; a question is **2** Muse Spark Open calls, about **2,460 + 261 tokens**. - **Flat surfaces:** the geometry-only detector found **12 level patches** in the room; the two largest sit at the heights of the two measured desks. - **Viewer:** about 150k points, the gather animation, photo and confidence colouring, and the floor grid, height bands and surfaces as layers, on a phone and a laptop. -- **Tests (2 November):** API 202 (including Schemathesis against the spec), pipeline 59 (including a synthetic room run through the whole pipeline), website 190 (accessibility with axe, WCAG 2.2 AA, reduced motion, no-JavaScript, keyboard-only and 200 % text usability, security headers); on the iPhone, 182 unit tests and 31 UI tests: accessibility audits and usability tasks done with VoiceOver labels at default and largest text; the Watch app builds. +- **Tests (2 November):** API 213 (including Schemathesis against the spec), pipeline 64 (including a synthetic room run through the whole pipeline), website 194 (accessibility with axe, WCAG 2.2 AA, reduced motion, no-JavaScript, keyboard-only and 200 % text usability, security headers); on the iPhone, 197 unit tests and 34 UI tests (2 skipped): accessibility audits and usability tasks done with VoiceOver labels at default and largest text; the Watch app builds. - **Usability, honestly:** the checks above are automated. In person, the author's screen-off run (VoiceOver Screen Curtain: the screen stays black) is planned, and a session with blind and low-vision testers is planned; its plan is ready in `evals/usability/`. -**Planned evaluation:** median position error against hand-measured positions, questions answered correctly, Verifier precision, and time to find an object by touch versus with Recall, scored without AI; the dated table will live in `evals/results.md`. The session plan for a blind or low-vision tester is in `evals/usability/`. +**Planned evaluation:** median position error against hand-measured positions, questions answered correctly, Verifier precision, and time to find an object by touch versus with Recall, scored without AI. The dated table is [`evals/results.md`](evals/results.md); recognising the room from a photo is its first entry. The session plan for a blind or low-vision tester is in `evals/usability/`. ## Installation @@ -726,6 +738,8 @@ Planned next, not built or not working yet (updated before submission; removed i - A physical robot running the ROS 2 example (the example and the robot package are in the repo). - The tape-measured 20 cm accuracy check with the phone's depth and poses. - Quantitative evaluations and the Verifier precision figure. +- Timing room recognition on the iPhone from five start spots, and whether a grown room map matches from a spot that + failed before (the photo matcher is measured on the Mac only so far). - The usability session with blind and low-vision testers (plan ready in `evals/usability/`), and the author's screen-off run. - Daily spend caps on model calls (the API and the website's voice route are rate-limited per key or visitor; there is no daily cap yet). - The 3-minute demo video. From 832de811bd2fa7ff1899536d2d1e8c3c8ed5ca64 Mon Sep 17 00:00:00 2001 From: Rachit Date: Tue, 3 Nov 2026 02:34:45 +0100 Subject: [PATCH 07/30] docs(examples): ros2 Dockerfile names its real build command Co-Authored-By: Claude Opus 5.5 --- examples/ros2-robot/Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/ros2-robot/Dockerfile b/examples/ros2-robot/Dockerfile index 5a7e5d6..26e05b3 100644 --- a/examples/ros2-robot/Dockerfile +++ b/examples/ros2-robot/Dockerfile @@ -1,5 +1,6 @@ # Recall's robot skill on ROS 2 Jazzy with Nav2 and its loopback simulator (a simulated robot that moves exactly as -# commanded: classic software, no models). Built by `make robot-demo`; nothing here runs on the iPhone or the Mac's API. +# commanded: classic software, no models). An in-progress example, not part of the submission or Recall's install +# (Recall itself runs without Docker). Build: docker build -t recall-fetch examples/ros2-robot FROM ros:jazzy-ros-base RUN apt-get update && apt-get install -y --no-install-recommends \ From 78e2d20d8f3c9a076282cdb9851721410c04b30f Mon Sep 17 00:00:00 2001 From: Rachit Date: Tue, 3 Nov 2026 03:01:43 +0100 Subject: [PATCH 08/30] fix(navigation): say "1 metre", never "1 metres" or "100 centimetres" Co-Authored-By: Claude Opus 5.5 --- services/api/src/recall_api/domain/navigation/route.py | 10 +++++++--- services/api/tests/unit/test_navigation.py | 8 ++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/services/api/src/recall_api/domain/navigation/route.py b/services/api/src/recall_api/domain/navigation/route.py index d8ab6c5..d4d7e44 100644 --- a/services/api/src/recall_api/domain/navigation/route.py +++ b/services/api/src/recall_api/domain/navigation/route.py @@ -108,9 +108,13 @@ def to_clock_steps(waypoints: list[tuple[float, float]], heading_xy: tuple[float def _say_m(m: float) -> str: - if m < 1: - return f"{int(round(m * 10)) * 10} centimetres" - return f"{m:.1f} metres".replace(".0 metres", " metres") + """Spoken to the nearest 10 cm: "70 centimetres", "1 metre" (not "1 metres" or "100 centimetres"), "1.5 metres".""" + tenths = round(m * 10) + if tenths < 10: + return f"{tenths * 10} centimetres" + if tenths == 10: + return "1 metre" + return f"{tenths / 10:.1f} metres".replace(".0 metres", " metres") def bearing(dx: float, dy: float, north_deg: float) -> float: diff --git a/services/api/tests/unit/test_navigation.py b/services/api/tests/unit/test_navigation.py index 6b22755..c13f1de 100644 --- a/services/api/tests/unit/test_navigation.py +++ b/services/api/tests/unit/test_navigation.py @@ -76,3 +76,11 @@ def test_steps_are_spoken_as_turns_and_metres(): steps = to_clock_steps([(0, 0), (2, 0), (2, -1.5)], heading_xy=(1, 0)) assert [s["clock"] for s in steps] == [12, 3] assert steps[0]["say"] == "Straight ahead, then about 2 metres." and steps[1]["metres"] == 1.5 + + +def test_distances_are_said_the_way_a_person_says_them(): + say = lambda metres: to_clock_steps([(0, 0), (metres, 0)], heading_xy=(1, 0))[0]["say"] # noqa: E731 + assert say(0.7) == "Straight ahead, then about 70 centimetres." + assert say(0.96) == "Straight ahead, then about 1 metre." # not "100 centimetres" + assert say(1.02) == "Straight ahead, then about 1 metre." # not "1 metres" + assert say(1.5) == "Straight ahead, then about 1.5 metres." From 5ff417a214c1c1e94d4196a821d99818b3fb2282 Mon Sep 17 00:00:00 2001 From: Rachit Date: Tue, 3 Nov 2026 03:01:43 +0100 Subject: [PATCH 09/30] feat(demo): the no-keys demo on the full living-room scan, with walks to each thing samples/replay/flat is re-recorded from scan bb449f4c (real Muse Spark Open calls) and now holds the floor geometry routes need: the point cloud thinned to 5 cm, positions only, and the walk's camera poses only. make demo-walks runs the replayed Guide and the phone's route planner from where the scan began and writes the website's walks.json; make demo starts the API and the website together. Co-Authored-By: Claude Opus 5.5 --- Makefile | 15 +- apps/web/public/real-scan/flat/walks.json | 293 +++ samples/replay/flat/guide.jsonl | 39 +- samples/replay/flat/home_cloud.npy | Bin 0 -> 406592 bytes samples/replay/flat/meta.json | 34 +- samples/replay/flat/poses.jsonl | 162 ++ samples/replay/flat/scan-result.json | 2188 +++++++++++++++---- services/api/src/recall_api/infra/demo.py | 108 +- services/api/tests/unit/test_demo_replay.py | 50 + 9 files changed, 2380 insertions(+), 509 deletions(-) create mode 100644 apps/web/public/real-scan/flat/walks.json create mode 100644 samples/replay/flat/home_cloud.npy create mode 100644 samples/replay/flat/poses.jsonl diff --git a/Makefile b/Makefile index 0d77d43..f42e605 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ BIND ?= 0.0.0.0 WEB := apps/web REDOCLY := npx -y @redocly/cli@2.72.1 -.PHONY: help setup dev worker robot-export place-index regression viewer public-scan live demo record probes diagrams pair rerun test test-all lint lint-imports migrate bundle-spec gen-models gen-swift-client eval +.PHONY: help setup dev worker robot-export place-index regression viewer public-scan live demo record probes diagrams pair rerun demo-walks test test-all lint lint-imports migrate bundle-spec gen-models gen-swift-client eval help: @grep -E '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) | awk -F':.*?## ' '{printf " %-18s %s\n", $$1, $$2}' @@ -59,11 +59,20 @@ viewer: ## Export a home's newest processed scan for /homes//viewer: make live: ## The API with real Meta model calls (keys from .env), and the scan worker ( cd $(API) && RECALL_BIND_HOST=$(BIND) uv run recall-api ) & $(MAKE) worker ; wait -demo: ## No keys: the API on a memory built from a recorded real scan; the Guide's replies are replayed (labelled recorded) - cd $(API) && uv run python -m recall_api.infra.demo serve $(if $(PORT),--port $(PORT)) +demo: ## Judges, start here. No keys: Recall's API on a recorded real scan and the website's demo page (localhost:3000/demo), labelled recorded + @if command -v pnpm >/dev/null 2>&1 && [ -d $(WEB)/node_modules ]; then \ + ( cd $(API) && uv run python -m recall_api.infra.demo serve $(if $(PORT),--port $(PORT)) ) & ( cd $(WEB) && PORT=3000 pnpm dev ) ; wait; \ + else \ + echo "No website here (run make setup for it): the API only. The same demo page is online at https://recall-ten-khaki.vercel.app/demo"; \ + cd $(API) && uv run python -m recall_api.infra.demo serve $(if $(PORT),--port $(PORT)); \ + fi + +demo-walks: ## Rebuild the demo page's walks (public/real-scan/flat/walks.json) from samples/replay/ with Recall's own code; no keys + cd $(API) && uv run python -m recall_api.infra.demo walks --viewer "$(CURDIR)/$(WEB)/public/real-scan/flat/meta.json" --out "$(CURDIR)/$(WEB)/public/real-scan/flat/walks.json" record: ## Re-record samples/replay/ with real Muse Spark Open calls: make record RESULT= ROOM="Living room" AT= cd $(API) && uv run python -m recall_api.infra.demo record "$(abspath $(RESULT))" --room "$(ROOM)" --captured-at "$(AT)" + $(MAKE) demo-walks diagrams: ## Redraw docs/diagrams/*.svg from the Mermaid sources (mermaid-cli; set PUPPETEER_EXECUTABLE_PATH to use a local Chrome) @for f in docs/diagrams/*.mmd; do \ diff --git a/apps/web/public/real-scan/flat/walks.json b/apps/web/public/real-scan/flat/walks.json new file mode 100644 index 0000000..01ea07e --- /dev/null +++ b/apps/web/public/real-scan/flat/walks.json @@ -0,0 +1,293 @@ +{ + "label": "Recorded output of a real run on 2026-11-03: one living-room scan from an iPhone 16 Pro, processed by MapAnything and SAM 3.1, and Muse Spark Open's replies to the questions below.", + "recorded_at": "2026-11-03T01:47:26+00:00", + "captured_at": "2026-11-01T18:11:32+00:00", + "room": "Living room", + "frame": "the viewer's: home frame (floor z = 0, z up, metres), x/y centred on the room as in meta.json", + "walks": [ + { + "question": "Where are my keys?", + "answer": "Your brown keys are near the desk in the living room at knee height, seen on Sunday at 19:11.", + "grounded": true, + "model": "replay:muse-spark-open (recorded)", + "label": "keys", + "target": [ + 1.28, + 1.835, + 0.658 + ], + "mode": "route", + "steps": [ + { + "clock": 11, + "metres": 1.1, + "say": "Turn to 11 o'clock, then about 1.1 metres." + }, + { + "clock": 1, + "metres": 0.6, + "say": "Turn to 1 o'clock, then about 60 centimetres." + }, + { + "clock": 11, + "metres": 0.9, + "say": "Turn to 11 o'clock, then about 90 centimetres." + }, + { + "clock": 1, + "metres": 0.9, + "say": "Turn to 1 o'clock, then about 90 centimetres." + } + ], + "arrive": "Your keys should be in front of you, near the desk, at knee height.", + "path": [ + [ + 1.261, + -1.467 + ], + [ + 0.861, + -0.467 + ], + [ + 1.061, + 0.133 + ], + [ + 0.961, + 1.033 + ], + [ + 1.261, + 1.833 + ] + ] + }, + { + "question": "Where did I leave my glasses?", + "answer": "Your glasses were near the desk in the Living room at knee height, seen on Sunday at 19:11.", + "grounded": true, + "model": "replay:muse-spark-open (recorded)", + "label": "glasses", + "target": [ + 0.548, + 2.563, + 0.651 + ], + "mode": "route", + "steps": [ + { + "clock": 11, + "metres": 1.1, + "say": "Turn to 11 o'clock, then about 1.1 metres." + }, + { + "clock": 1, + "metres": 0.6, + "say": "Turn to 1 o'clock, then about 60 centimetres." + }, + { + "clock": 11, + "metres": 1.2, + "say": "Turn to 11 o'clock, then about 1.2 metres." + }, + { + "clock": 11, + "metres": 1.1, + "say": "Turn to 11 o'clock, then about 1.1 metres." + } + ], + "arrive": "Your glasses should be in front of you, near the desk, at knee height.", + "path": [ + [ + 1.261, + -1.467 + ], + [ + 0.861, + -0.467 + ], + [ + 1.061, + 0.133 + ], + [ + 0.961, + 1.333 + ], + [ + 0.161, + 2.133 + ] + ] + }, + { + "question": "Where is my laptop?", + "answer": "I found two laptops in the living room on Sunday at 19:11, a grey one near the table at waist height and a black one near the desk at waist height. Which one are you looking for?", + "grounded": true, + "model": "replay:muse-spark-open (recorded)", + "asks_which": true + }, + { + "question": "Where is my wallet?", + "answer": "Your wallet is near the desk in the living room at knee height, seen on Sunday at 19:11.", + "grounded": true, + "model": "replay:muse-spark-open (recorded)", + "label": "wallet", + "target": [ + 0.643, + 2.253, + 0.664 + ], + "mode": "route", + "steps": [ + { + "clock": 11, + "metres": 1.1, + "say": "Turn to 11 o'clock, then about 1.1 metres." + }, + { + "clock": 1, + "metres": 1.2, + "say": "Turn to 1 o'clock, then about 1.2 metres." + }, + { + "clock": 11, + "metres": 0.6, + "say": "Turn to 11 o'clock, then about 60 centimetres." + }, + { + "clock": 11, + "metres": 0.4, + "say": "Turn to 11 o'clock, then about 40 centimetres." + }, + { + "clock": 2, + "metres": 0.4, + "say": "Turn to 2 o'clock, then about 40 centimetres." + } + ], + "arrive": "Your wallet should be in front of you, near the desk, at knee height.", + "path": [ + [ + 1.261, + -1.467 + ], + [ + 0.861, + -0.467 + ], + [ + 1.061, + 0.733 + ], + [ + 0.961, + 1.333 + ], + [ + 0.661, + 1.633 + ], + [ + 0.761, + 2.033 + ] + ] + }, + { + "question": "Where is the remote?", + "answer": "I found two remotes in the Living room seen on Sunday at 19:11, a grey one near the table at waist height and a white one on the desk at waist height, which one did you mean?", + "grounded": true, + "model": "replay:muse-spark-open (recorded)", + "asks_which": true + }, + { + "question": "Where is my bag?", + "answer": "Your grey bag is near the chair in the living room at waist height, seen on Sunday at 19:11.", + "grounded": true, + "model": "replay:muse-spark-open (recorded)", + "label": "bag", + "target": [ + -1.848, + 2.577, + 0.975 + ], + "mode": "route", + "steps": [ + { + "clock": 11, + "metres": 1.1, + "say": "Turn to 11 o'clock, then about 1.1 metres." + }, + { + "clock": 1, + "metres": 0.6, + "say": "Turn to 1 o'clock, then about 60 centimetres." + }, + { + "clock": 11, + "metres": 1.2, + "say": "Turn to 11 o'clock, then about 1.2 metres." + }, + { + "clock": 11, + "metres": 1.6, + "say": "Turn to 11 o'clock, then about 1.6 metres." + }, + { + "clock": 10, + "metres": 1.3, + "say": "Turn to 10 o'clock, then about 1.3 metres." + } + ], + "arrive": "Your bag should be in front of you, near the chair, at waist height.", + "path": [ + [ + 1.261, + -1.467 + ], + [ + 0.861, + -0.467 + ], + [ + 1.061, + 0.133 + ], + [ + 0.961, + 1.333 + ], + [ + -0.139, + 2.433 + ], + [ + -1.339, + 2.033 + ] + ] + }, + { + "question": "Have you seen a bottle?", + "answer": "I saw a grey bottle on the cabinet on shelf 1 at waist height seen on Sunday at 19:11, and two more grey bottles near the desk at waist height seen on Sunday at 19:11. Let me know which one you need and I can guide you to it.", + "grounded": true, + "model": "replay:muse-spark-open (recorded)", + "asks_which": true + }, + { + "question": "Where's my book?", + "answer": "Your book was seen near the table at waist height in the Living room, and two others near the chair at knee height and the sofa at waist height, all seen on Sunday at 19:11 \u2014 can you describe which book you mean by colour or name?", + "grounded": true, + "model": "replay:muse-spark-open (recorded)", + "asks_which": true + }, + { + "question": "Where is my umbrella?", + "answer": "I haven't seen that yet. Scan the room it might be in, and I'll remember it.", + "grounded": true, + "model": "replay:muse-spark-open (recorded)" + } + ] +} diff --git a/samples/replay/flat/guide.jsonl b/samples/replay/flat/guide.jsonl index 8da62d1..affb4ff 100644 --- a/samples/replay/flat/guide.jsonl +++ b/samples/replay/flat/guide.jsonl @@ -1,18 +1,21 @@ -{"question": "Where are my keys?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-17f0f934043c4c39b89f71c1da8168e9", "name": "find_object", "arguments": {"name": "keys"}}]}} -{"question": "Where are my keys?", "step": 1, "reply": {"text": "Your keys are in the Living room near the desk at waist height, seen yesterday at 04:21.", "tool_calls": []}} -{"question": "Where did I leave my glasses?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-7613d07eef0b4830a2869fe18f562ba4", "name": "find_object", "arguments": {"name": "glasses"}}]}} -{"question": "Where did I leave my glasses?", "step": 1, "reply": {"text": "Your grey glasses were near the desk in the living room at waist height, seen yesterday at 04:21.", "tool_calls": []}} -{"question": "Where is my laptop?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-9148a321cb374e91942537b78bd58c3e", "name": "find_object", "arguments": {"name": "laptop"}}]}} -{"question": "Where is my laptop?", "step": 1, "reply": {"text": "Your laptop was near the desk in the living room at waist height, seen yesterday at 04:21.", "tool_calls": []}} -{"question": "Where is my bag?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-caeed23def3443b69bf398bc8e170435", "name": "find_object", "arguments": {"name": "bag"}}]}} -{"question": "Where is my bag?", "step": 1, "reply": {"text": "I remember two bags from yesterday at 04:21, a grey bag near the desk at knee height and a white bag on the cabinet on shelf level 1 at waist height in the Living room, which one did you mean?", "tool_calls": []}} -{"question": "Have you seen a bottle?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-d4b41c061c374ac39ab892b6650dd1b9", "name": "find_object", "arguments": {"name": "bottle"}}]}} -{"question": "Have you seen a bottle?", "step": 1, "reply": {"text": "I saw a grey bottle in the Living room near the desk at waist height, seen yesterday at 04:21. I also saw a grey bottle on the desk in the same area at the same time.", "tool_calls": []}} -{"question": "Where's my book?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-6c38bc73533845ffad9585adac2c042d", "name": "find_object", "arguments": {"name": "book"}}]}} -{"question": "Where's my book?", "step": 1, "reply": {"text": "I last saw two books in the Living room seen yesterday at 04:21 \u2014 a red one on the desk at waist height, and a grey one near the shelf at waist height. Which book did you mean?", "tool_calls": []}} -{"question": "Where is the laptop", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-27b2630358ce45f5bf25e88ae4436044", "name": "find_object", "arguments": {"name": "laptop"}}]}} -{"question": "Where is the laptop", "step": 1, "reply": {"text": "Your laptop was near the desk in the living room at waist height, seen yesterday at 04:21.", "tool_calls": []}} -{"question": "Where did I put my reading glasses?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-be6b68b48de7426e86eaa08bf225375b", "name": "find_object", "arguments": {"name": "reading glasses"}}]}} -{"question": "Where did I put my reading glasses?", "step": 1, "reply": {"text": "Your reading glasses were near the desk in the Living room at waist height, seen yesterday at 04:21.", "tool_calls": []}} -{"question": "Where is my umbrella?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-6d2d35d244144e068698c1efb969a016", "name": "find_object", "arguments": {"name": "umbrella"}}]}} -{"question": "Where is my umbrella?", "step": 1, "reply": {"text": "I haven't seen your umbrella in my recent scans, so I'm not sure where it is right now. Try scanning around the entryway or where you usually leave it and I can check again.", "tool_calls": []}} +{"question": "Where are my keys?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-e18b747ed50147108020c8ce38bd680a", "name": "find_object", "arguments": {"name": "keys"}}]}} +{"question": "Where are my keys?", "step": 1, "reply": {"text": "Your brown keys are near the desk in the living room at knee height, seen on Sunday at 19:11.", "tool_calls": []}} +{"question": "Where did I leave my glasses?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-ac51479c9e744db7baf31c01eda2dbd7", "name": "find_object", "arguments": {"name": "glasses"}}]}} +{"question": "Where did I leave my glasses?", "step": 1, "reply": {"text": "Your glasses were near the desk in the Living room at knee height, seen on Sunday at 19:11.", "tool_calls": []}} +{"question": "Where is my laptop?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-a4ead53696744bd7a6b614de29fbdd2f", "name": "find_object", "arguments": {"name": "laptop"}}]}} +{"question": "Where is my laptop?", "step": 1, "reply": {"text": "I found two laptops in the living room on Sunday at 19:11, a grey one near the table at waist height and a black one near the desk at waist height. Which one are you looking for?", "tool_calls": []}} +{"question": "Where is my wallet?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-90cb2bbec28a4ad8b7fc8cd5fd29a4c0", "name": "find_object", "arguments": {"name": "wallet"}}]}} +{"question": "Where is my wallet?", "step": 1, "reply": {"text": "Your wallet is near the desk in the living room at knee height, seen on Sunday at 19:11.", "tool_calls": []}} +{"question": "Where is the remote?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-25ae49b6121443b0ac7474b32469a341", "name": "find_object", "arguments": {"name": "remote"}}]}} +{"question": "Where is the remote?", "step": 1, "reply": {"text": "I found two remotes in the Living room seen on Sunday at 19:11, a grey one near the table at waist height and a white one on the desk at waist height, which one did you mean?", "tool_calls": []}} +{"question": "Where is my bag?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-664577b31b284219a689859c7e3bfc8c", "name": "find_object", "arguments": {"name": "bag"}}]}} +{"question": "Where is my bag?", "step": 1, "reply": {"text": "Your grey bag is near the chair in the living room at waist height, seen on Sunday at 19:11.", "tool_calls": []}} +{"question": "Have you seen a bottle?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-fa84b39bafa9482884e713c5479db01f", "name": "find_object", "arguments": {"name": "bottle"}}]}} +{"question": "Have you seen a bottle?", "step": 1, "reply": {"text": "Yes, I have seen 3 grey bottles. One was on the cabinet at waist height and two were near the desk at waist height, all seen on Sunday at 19:11. Which one are you looking for?", "tool_calls": []}} +{"question": "Have you seen a bottle?", "step": 2, "reply": {"text": "I saw a grey bottle on the cabinet on shelf 1 at waist height seen on Sunday at 19:11, and two more grey bottles near the desk at waist height seen on Sunday at 19:11. Let me know which one you need and I can guide you to it.", "tool_calls": []}} +{"question": "Where's my book?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-4532fea656c144c2b26737ee44388593", "name": "find_object", "arguments": {"name": "book"}}]}} +{"question": "Where's my book?", "step": 1, "reply": {"text": "I found 3 books in the Living room seen on Sunday at 19:11 \u2014 a grey one near the table at waist height, a grey one near the chair at knee height, and a beige one near the sofa at waist height. Which one are you looking for?", "tool_calls": []}} +{"question": "Where's my book?", "step": 2, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-b1ecc28bf3a140f69140ef1c5448d1dd", "name": "find_object", "arguments": {"name": "book"}}]}} +{"question": "Where's my book?", "step": 3, "reply": {"text": "Your book was seen near the table at waist height in the Living room, and two others near the chair at knee height and the sofa at waist height, all seen on Sunday at 19:11 \u2014 can you describe which book you mean by colour or name?", "tool_calls": []}} +{"question": "Where is my umbrella?", "step": 0, "reply": {"text": null, "tool_calls": [{"id": "chatcmpl-tool-05820fcdd41449538dea322747b3e562", "name": "find_object", "arguments": {"name": "umbrella"}}]}} +{"question": "Where is my umbrella?", "step": 1, "reply": {"text": "I haven't seen an umbrella \u2014 try scanning the room so I can look again.", "tool_calls": []}} diff --git a/samples/replay/flat/home_cloud.npy b/samples/replay/flat/home_cloud.npy new file mode 100644 index 0000000000000000000000000000000000000000..a65486dc5e89bfda627b5b9db9e651b2be9ecfb5 GIT binary patch literal 406592 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

H?iT|`a4&B%%v6j+Ju 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

nhHS;pX^vw142zHZM;$JKu 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 Ny^9(<6j>t4CNZ-( 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 --out + no keys; each recorded answer and the route to its thing, for the website's demo page record: python -m recall_api.infra.demo record --room "Living room" --captured-at real Muse Spark Open calls (keys from .env); writes samples/replay/flat/. Review the files before committing. """ @@ -15,6 +17,8 @@ from datetime import UTC, datetime from pathlib import Path +import numpy as np + from recall_api.adapters.outbound.replay.recorded_llm import RecordingLlm from recall_api.application.ask_where_is import AskWhereIs from recall_api.application.ingest_scan import IngestScan @@ -27,13 +31,16 @@ HOME = "flat" DEMO_KEY = "demo" # the demo's device key: it runs on 127.0.0.1 with recorded data only QUESTIONS = [ - "Where are my keys?", "Where did I leave my glasses?", "Where is my laptop?", "Where is my bag?", - "Have you seen a bottle?", "Where's my book?", "Where is the laptop", "Where did I put my reading glasses?", + "Where are my keys?", "Where did I leave my glasses?", "Where is my laptop?", "Where is my wallet?", + "Where is the remote?", "Where is my bag?", "Have you seen a bottle?", "Where's my book?", "Where is my umbrella?", ] # fmt: skip # what a scan result may carry into the public sample: geometry, labels and colours; never paths, names or notes OBJECT_FIELDS = ("label", "also_seen_as", "centroid", "bbox_min", "bbox_max", "size_m", "confidence", "sightings", "color", "front_dir") # fmt: skip +# the scan's geometry the routes need, beside the result: the point cloud (no colours) and the path the phone walked +GEOMETRY = ("home_cloud.npy", "poses.jsonl") +VOXEL_M = 0.05 # the routes' floor grid has 10 cm cells, so one point per 5 cm loses nothing def sanitise(result: dict) -> dict: @@ -46,6 +53,25 @@ def sanitise(result: dict) -> dict: } +def thin_cloud(points: np.ndarray, voxel: float = VOXEL_M) -> np.ndarray: + """One point per `voxel` cube, positions only (no colours), as float16: small enough to publish.""" + points = np.asarray(points, dtype=np.float64) + _, first = np.unique(np.floor(points / voxel).astype(np.int64), axis=0, return_index=True) + return points[np.sort(first)].astype(np.float16) + + +def walk_poses(text: str) -> str: + """Only the camera poses of the scan's walk (where routes start): no times, compass readings or tracking notes.""" + lines = [] + for line in text.splitlines(): + try: + pose = json.loads(line)["pose"] + except (ValueError, KeyError, TypeError): + continue + lines.append(json.dumps({"pose": [round(float(v), 5) for v in pose]})) + return "\n".join(lines) + "\n" + + def demo_settings(data_dir: Path, **more) -> Settings: return Settings( RECALL_DATA_DIR=data_dir, @@ -64,11 +90,16 @@ def build(settings: Settings, sample: Path) -> Container: c = Container(settings) c.homes.save(Home(id=HOME, name=HOME)) created = datetime.fromisoformat(meta["captured_at"]) - c.scans.save(Scan(meta["scan_id"], HOME, meta["room"], ScanStatus.PROCESSING, 0, "", 0, created)) + scan = Scan(meta["scan_id"], HOME, meta["room"], ScanStatus.PROCESSING, 0, "", 0, created) + c.scans.save(scan) # the same ids every time, so a recorded tool call that names an object finds it again in the replay n = itertools.count(1) ingest = IngestScan(c.scans, c.homes, c.objects, c.clock, lambda prefix: f"{prefix}_demo{next(n)}", audit=c.auditor) ingest(meta["scan_id"], json.loads((sample / "scan-result.json").read_text())) + # the geometry goes where a processed scan keeps it, so routes are planned by the same code as for a live scan + for name in GEOMETRY: + if (sample / name).exists(): + c.blobs.put(f"{scan.folder}/episode/{name}", (sample / name).read_bytes()) # the Guide's replies were recorded when every find counted (both bags were seen in one photo each), so the # replayed memory uses the same level, or the recordings would no longer be what it says c.owner_settings.put("trust", Trust.EVERYTHING.value) @@ -82,6 +113,63 @@ def replay_settings(data_dir: Path, sample: Path) -> Settings: ) +def viewer_shift(objects: list[dict], viewer_objects: list[dict]) -> np.ndarray: + """The viewer export centres the room on x/y; the shift is found from the things both list (same scan), and + checked: every matched thing must land within 2 cm, or the viewer shows a different scan.""" + ours = [o for o in objects if sum(p["label"] == o["label"] for p in objects) == 1] + pairs = [] + for o in ours: + same = [v for v in viewer_objects if v["label"] == o["label"]] + if len(same) == 1: + pairs.append((np.array(o["centroid"][:2]), np.array(same[0]["at"][:2]))) + if len(pairs) < 3: + raise ValueError("The viewer export and the sample share too few things: are they the same scan?") + shift = np.median([a - b for a, b in pairs], axis=0) + worst = max(float(np.linalg.norm(a - b - shift)) for a, b in pairs) + if worst > 0.02: + raise ValueError(f"The viewer export is not this scan (things differ by up to {worst:.2f} m).") + return shift + + +def walks(sample: Path, viewer_meta: Path) -> dict: + """Each recorded question, answered by the replayed Guide, and the route the phone would give from where the scan + began: the same A* on the scan's floor and the same clock-face steps. Positions are in the viewer's frame.""" + meta = json.loads((sample / "meta.json").read_text()) + c = build(replay_settings(Path(tempfile.mkdtemp(prefix="recall-walks-")), sample), sample) + result = json.loads((sample / "scan-result.json").read_text()) + shift = viewer_shift(result["objects"], json.loads(viewer_meta.read_text())["objects"]) + xy = lambda p: [round(float(p[0] - shift[0]), 3), round(float(p[1] - shift[1]), 3)] # noqa: E731 + first = json.loads((sample / "poses.jsonl").read_text().splitlines()[0])["pose"] + items = [] + for rec in meta["answers"]: + a = c.ask(HOME, rec["question"]) + item: dict = {"question": rec["question"], "answer": a.text, "grounded": a.grounded, "model": a.model} + obj = c.objects.get(a.object_id) if a.object_id else None + seen = obj.where(Trust.EVERYTHING) if obj else None + alike = [o for o in c.objects.list_for_home(HOME) if obj and o.label == obj.label and o.where(Trust.EVERYTHING)] + if len(alike) > 1: + item["asks_which"] = True # several alike: the answer asks which one, so no route until the person says + elif seen: + g = c.guidance.start(HOME, obj.id, first, "normal") + item |= { + "label": obj.label, + "target": [*xy((seen.position.x, seen.position.y)), round(seen.position.z, 3)], + "mode": g["mode"], + "steps": [{k: s[k] for k in ("clock", "metres", "say")} for s in g["steps"]], + "arrive": g["arrive_say"], + "path": [xy(p) for p in g["path"]], + } + items.append(item) + return { + "label": meta["label"], + "recorded_at": meta["recorded_at"], + "captured_at": meta["captured_at"], + "room": meta["room"], + "frame": "the viewer's: home frame (floor z = 0, z up, metres), x/y centred on the room as in meta.json", + "walks": items, + } + + def serve(sample: Path, port: int) -> None: import uvicorn @@ -95,7 +183,8 @@ def serve(sample: Path, port: int) -> None: body = json.dumps({"home_id": HOME, "question": q}) auth = f"-H 'Authorization: Bearer {DEMO_KEY}' -H 'Content-Type: application/json'" print(f" curl -s localhost:{port}/ask {auth} -d '{body}'") - print(f"\nAPI docs: http://localhost:{port}/docs (device key: {DEMO_KEY})\n") + print(f"\nAPI docs: http://localhost:{port}/docs (device key: {DEMO_KEY})") + print("Ask, then walk (the website's demo page, started by make demo): http://localhost:3000/demo\n") uvicorn.run(create_app(settings), host="127.0.0.1", port=port) @@ -104,6 +193,11 @@ def record(result_path: Path, room: str, captured_at: str, sample: Path) -> None recorded_at = datetime.now(UTC).replace(microsecond=0) result = sanitise(json.loads(result_path.read_text())) (sample / "scan-result.json").write_text(json.dumps(result, indent=1) + "\n") + episode = result_path.parent + if (episode / "home_cloud.npy").exists(): + np.save(sample / "home_cloud.npy", thin_cloud(np.load(episode / "home_cloud.npy"))) + if (episode / "poses.jsonl").exists(): + (sample / "poses.jsonl").write_text(walk_poses((episode / "poses.jsonl").read_text())) meta = { "label": f"Recorded output of a real run on {recorded_at:%Y-%m-%d}: one living-room scan from an iPhone " "16 Pro, processed by MapAnything and SAM 3.1, and Muse Spark Open's replies to the questions below.", @@ -134,6 +228,9 @@ def main() -> None: sub = p.add_subparsers(dest="cmd", required=True) s = sub.add_parser("serve") s.add_argument("--port", type=int, default=8000) + w = sub.add_parser("walks") + w.add_argument("--viewer", type=Path, required=True, help="the website's meta.json for the same scan") + w.add_argument("--out", type=Path, required=True) r = sub.add_parser("record") r.add_argument("result", type=Path) r.add_argument("--room", required=True) @@ -141,6 +238,9 @@ def main() -> None: a = p.parse_args() if a.cmd == "serve": serve(SAMPLE, a.port) + elif a.cmd == "walks": + a.out.write_text(json.dumps(walks(SAMPLE, a.viewer), indent=1) + "\n") + print(f"Wrote {a.out}") else: record(a.result, a.room, a.captured_at, SAMPLE) diff --git a/services/api/tests/unit/test_demo_replay.py b/services/api/tests/unit/test_demo_replay.py index 39e60c3..df2b81e 100644 --- a/services/api/tests/unit/test_demo_replay.py +++ b/services/api/tests/unit/test_demo_replay.py @@ -2,6 +2,7 @@ import json +import numpy as np import pytest from recall_api.adapters.outbound.replay.recorded_llm import NotRecorded, RecordedLlm @@ -34,3 +35,52 @@ def test_the_sample_holds_geometry_and_labels_only(): raw = {"scan_id": "x", "stats": {"path": "/Users/x"}, "room": {}, "landmarks": [], "transform_to_home": None, "objects": [{"label": "keys", "keyframe_index": 3, "note": "mine", "centroid": [0, 0, 0]}]} # fmt: skip assert sanitise(raw)["objects"] == [{"label": "keys", "centroid": [0, 0, 0]}] + + +def test_the_sample_geometry_is_positions_only(): + from recall_api.infra.demo import thin_cloud, walk_poses + + for line in (SAMPLE / "poses.jsonl").read_text().splitlines(): + assert set(json.loads(line)) == {"pose"} # no times, compass readings or tracking notes + cloud = np.load(SAMPLE / "home_cloud.npy") + assert cloud.ndim == 2 and cloud.shape[1] == 3 # x, y, z only: no colours + assert len(thin_cloud(np.array([[0, 0, 0], [0.01, 0.01, 0.01], [1, 1, 1]]))) == 2 + raw = '{"t": 1, "heading": {"magnetic_deg": 3}, "pose": [1, 0]}\nnot json\n' + assert walk_poses(raw) == '{"pose": [1.0, 0.0]}\n' + + +def test_the_demo_routes_each_answer_from_where_the_scan_began(tmp_path): + from recall_api.infra.demo import walks + + result = json.loads((SAMPLE / "scan-result.json").read_text()) + shift = (1.0, -2.0) # a viewer export centred elsewhere + viewer = [{"label": o["label"], "at": [o["centroid"][0] - shift[0], o["centroid"][1] - shift[1], 0]} + for o in result["objects"]] # fmt: skip + (tmp_path / "meta.json").write_text(json.dumps({"objects": viewer})) + out = walks(SAMPLE, tmp_path / "meta.json") + keys = next(w for w in out["walks"] if w["question"] == "Where are my keys?") + assert keys["mode"] == "route" and keys["steps"] and keys["label"] == "keys" + assert all(set(s) == {"clock", "metres", "say"} for s in keys["steps"]) + # the route ends at the keys, in the viewer's frame + assert np.hypot(keys["path"][-1][0] - keys["target"][0], keys["path"][-1][1] - keys["target"][1]) < 0.8 + unseen = next(w for w in out["walks"] if "umbrella" in w["question"]) + assert "path" not in unseen # nothing to walk to: the answer alone + + +def test_a_viewer_export_of_another_scan_is_refused(): + from recall_api.infra.demo import viewer_shift + + ours = [{"label": l, "centroid": [i, 0, 0]} for i, l in enumerate(("keys", "mug", "book", "bag"))] # noqa: E741 + other = [{"label": l, "at": [i * 2.0, 0, 0]} for i, l in enumerate(("keys", "mug", "book", "bag"))] # noqa: E741 + with pytest.raises(ValueError, match="not this scan"): + viewer_shift(ours, other) + + +def test_an_answer_that_asks_which_one_gets_no_route(tmp_path): + from recall_api.infra.demo import walks + + result = json.loads((SAMPLE / "scan-result.json").read_text()) + viewer = [{"label": o["label"], "at": o["centroid"]} for o in result["objects"]] + (tmp_path / "meta.json").write_text(json.dumps({"objects": viewer})) + laptop = next(w for w in walks(SAMPLE, tmp_path / "meta.json")["walks"] if "laptop" in w["question"]) + assert laptop["asks_which"] and "path" not in laptop From f347b4b8d2593a9879005b90f571a7026f356b80 Mon Sep 17 00:00:00 2001 From: Rachit Date: Tue, 3 Nov 2026 03:01:43 +0100 Subject: [PATCH 10/30] feat(web): /demo, ask then walk: recorded answers and a GSAP walk on the real scan Co-Authored-By: Claude Opus 5.5 --- apps/web/app/(app)/demo/page.tsx | 44 ++++ apps/web/components/viewer/Layers.tsx | 63 ++++- apps/web/components/viewer/LocalScan.tsx | 58 ++-- apps/web/components/viewer/ViewerCanvas.tsx | 43 ++- apps/web/components/viewer/WalkDemo.tsx | 276 ++++++++++++++++++++ apps/web/e2e/demo.spec.ts | 88 +++++++ apps/web/i18n/dictionaries/en.ts | 27 ++ 7 files changed, 556 insertions(+), 43 deletions(-) create mode 100644 apps/web/app/(app)/demo/page.tsx create mode 100644 apps/web/components/viewer/WalkDemo.tsx create mode 100644 apps/web/e2e/demo.spec.ts diff --git a/apps/web/app/(app)/demo/page.tsx b/apps/web/app/(app)/demo/page.tsx new file mode 100644 index 0000000..6639fc5 --- /dev/null +++ b/apps/web/app/(app)/demo/page.tsx @@ -0,0 +1,44 @@ +import type { Metadata } from "next"; +import Link from "next/link"; +import { WalkDemo, type Walks } from "@/components/viewer/WalkDemo"; +import { getDictionary } from "@/i18n"; +// made by Recall's own code from the recorded run (make demo-walks): the replayed answers and the planner's routes +import walks from "@/public/real-scan/flat/walks.json"; + +const t = getDictionary().app.demo; + +export const metadata: Metadata = { + title: `${t.metaTitle} · Recall`, + description: t.metaDescription, +}; + +/** The no-keys demo: the published real scan, the recorded answers and the walks to each thing. Signed out, no API. */ +export default function DemoPage() { + return ( +
+

+ + {t.badge} +

+

{t.title}

+

{t.lead}

+ + + +
+

+ {t.judges.title} +

+

{t.judges.body}

+
+          {t.judges.code}
+        
+

+ + {t.viewer} + +

+
+
+ ); +} diff --git a/apps/web/components/viewer/Layers.tsx b/apps/web/components/viewer/Layers.tsx index d649837..3ba9694 100644 --- a/apps/web/components/viewer/Layers.tsx +++ b/apps/web/components/viewer/Layers.tsx @@ -1,8 +1,8 @@ "use client"; -import { useFrame } from "@react-three/fiber"; -import { useMemo, useRef } from "react"; -import { BoxGeometry, BufferGeometry, CanvasTexture, Color, EdgesGeometry, Float32BufferAttribute, type Group, PlaneGeometry, SRGBColorSpace } from "three"; +import { useFrame, useThree } from "@react-three/fiber"; +import { useEffect, useMemo, useRef } from "react"; +import { BoxGeometry, BufferGeometry, CanvasTexture, Color, CurvePath, EdgesGeometry, Float32BufferAttribute, type Group, LineCurve3, PlaneGeometry, SRGBColorSpace, TubeGeometry, Vector3 } from "three"; import { BAND_EDGES } from "./bands"; export interface Surface { @@ -372,3 +372,60 @@ export function NorthArrow({ floor, northDeg, tokens, label, theta = 0 }: { floo ); } + +/** A walk to show: floor points (x, y) from start to thing, and how much of it is drawn (0 to 1; GSAP tweens it). */ +export interface Walk { + path: [number, number][]; + progress: { value: number }; +} + +const ROUTE_Z = 0.03; // just above the floor, so the points don't cover it + +/** + * The route on the floor as a green band, drawn up to `walk.progress`, with a ring where the walk starts. Like the + * arrows it is drawn over the points (no depth test), so furniture never hides it. + */ +export function Route({ walk, tokens }: { walk: Walk; tokens: Tokens }) { + const { invalidate } = useThree(); + const tube = useMemo(() => { + const curve = new CurvePath(); + walk.path.slice(1).forEach(([x, y], i) => { + const [x0, y0] = walk.path[i]; + curve.add(new LineCurve3(new Vector3(x0, y0, ROUTE_Z), new Vector3(x, y, ROUTE_Z))); + }); + // the tube's triangles run from start to end, so drawing the first n of them draws the first part of the walk + return new TubeGeometry(curve, Math.max(24, Math.round(curve.getLength() * 40)), 0.035, 8, false); + }, [walk.path]); + const drawn = useRef(-1); + const draw = () => { + const total = tube.index?.count ?? 0; + const n = Math.floor((total * Math.min(1, Math.max(0, walk.progress.value))) / 3) * 3; + if (n === drawn.current) return false; + tube.setDrawRange(0, n); + drawn.current = n; + return true; + }; + // the first frame shows the current progress (all of it when motion is reduced); later frames follow the tween + useEffect(() => { + drawn.current = -1; + draw(); + invalidate(); + return () => tube.dispose(); + // eslint-disable-next-line react-hooks/exhaustive-deps -- draw reads the same tube and progress box + }, [tube, walk.progress, invalidate]); + useFrame(() => { + draw(); + }); + const [sx, sy] = walk.path[0]; + return ( + <> + + + + + + + + + ); +} diff --git a/apps/web/components/viewer/LocalScan.tsx b/apps/web/components/viewer/LocalScan.tsx index 567f93b..d477f6d 100644 --- a/apps/web/components/viewer/LocalScan.tsx +++ b/apps/web/components/viewer/LocalScan.tsx @@ -11,6 +11,7 @@ import { Sheets } from "./Sheets"; import { RealScanStory } from "./RealScanStory"; import type { FurnitureBox, RoomShape, Surface, Thing } from "./Layers"; import { ViewerStats } from "./ViewerStats"; +import type { ViewerStrings } from "./ViewerCanvas"; const s = getDictionary().app.viewer.scan; @@ -43,6 +44,36 @@ export interface ScanMeta { drawings?: { id: string; title: string; file: string; summary: string }[]; } +/** The viewer's labels, from the dictionary (shared with the demo page). */ +export function viewerStrings(): ViewerStrings { + return { + label: s.alt, + loading: s.loading, + view: s.view, + views: s.views, + viewNotes: s.viewNotes, + colour: s.colour, + modes: s.modes, + layers: s.layers, + grid: s.grid, + bands: s.bands, + surfaces: s.surfaces, + room: s.room, + things: s.things, + axes: s.axes, + axisLabels: s.axisLabels, + best: s.best, + more: s.more, + camera: s.camera, + presets: s.presets, + wallOpacity: s.wallOpacity, + showFurniture: s.showFurniture, + wireframe: s.wireframe, + flat: s.flat, + cut: s.cut, + }; +} + function bandRange(i: number) { if (i === 0) return format(s.bandBelow, { to: BAND_EDGES[0] }); if (i === BANDS.length - 1) return format(s.bandAbove, { from: BAND_EDGES[BAND_EDGES.length - 1] }); @@ -74,32 +105,7 @@ export function LocalScan({ steps }: { steps: ReactNode }) { { value: String(meta.stats.sam_calls), label: s.stats.sam }, { value: `${Math.round(meta.count / 1000)}k`, label: s.stats.points }, ]; - const viewer = { - label: s.alt, - loading: s.loading, - view: s.view, - views: s.views, - viewNotes: s.viewNotes, - colour: s.colour, - modes: s.modes, - layers: s.layers, - grid: s.grid, - bands: s.bands, - surfaces: s.surfaces, - room: s.room, - things: s.things, - axes: s.axes, - axisLabels: s.axisLabels, - best: s.best, - more: s.more, - camera: s.camera, - presets: s.presets, - wallOpacity: s.wallOpacity, - showFurniture: s.showFurniture, - wireframe: s.wireframe, - flat: s.flat, - cut: s.cut, - }; + const viewer = viewerStrings(); return ( <> {PUBLIC_SCANS.has(homeId) && ( diff --git a/apps/web/components/viewer/ViewerCanvas.tsx b/apps/web/components/viewer/ViewerCanvas.tsx index 3ce2f05..fa760b4 100644 --- a/apps/web/components/viewer/ViewerCanvas.tsx +++ b/apps/web/components/viewer/ViewerCanvas.tsx @@ -8,7 +8,7 @@ import { Color, Vector3 } from "three"; import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; import { gsap, MOTION_OK } from "@/lib/gsap"; import { useMotionPrefs } from "@/lib/motion-prefs"; -import { Axes, FloorGrid, Furniture, HeightBands, Room, Surfaces, Things, type Bounds, type FurnitureBox, type RoomShape, type Surface, type Thing, type Tokens } from "./Layers"; +import { Axes, FloorGrid, Furniture, HeightBands, Room, Route, Surfaces, Things, type Bounds, type FurnitureBox, type RoomShape, type Surface, type Thing, type Tokens, type Walk } from "./Layers"; import { makeUniforms, MODES, RoomCloud, type CloudData, type CloudUniforms, type ColourMode } from "./RoomCloud"; import { ArchitectRoom, CUT, kindColour, loadArchitect, loadMesh, loadPhoto, PhotoRoom, RoomModel, SurfaceMesh, type ArchitectData, type MeshData, type PhotoData } from "./RoomViews"; @@ -25,6 +25,9 @@ const PRESETS: Record = { front: [0, -9.5, 2.4], side: [9.5, 0, 2.4], }; +/** A preset camera position moved `k` times as far from the room's centre. */ +const away = ([x, y, z]: [number, number, number], k: number): [number, number, number] => [x * k, y * k, z * k]; + /** The views that read best: every measured point, and the regenerated room. */ export const BEST_VIEWS: View[] = ["dots", "architect"]; @@ -81,6 +84,11 @@ export interface ViewerProps { hasPhoto?: boolean; /** the export has the regenerated room (architect.json) */ hasArchitect?: boolean; + /** a walk to draw on the floor (the demo page), or none */ + walk?: Walk | null; + /** how the viewer opens (default: every point, from the side, turning slowly); `distance` scales the camera's + * distance so a whole room fits a smaller frame */ + start?: { view: View; preset: Preset; distance?: number; spin?: boolean }; t: ViewerStrings; } @@ -93,10 +101,10 @@ interface Layers { things: boolean; } -type Model = Pick; +type Model = Pick; /** Drag to orbit (sideways on phones, so vertical swipes still scroll the page); a slow turn until the first drag. */ -function Controls({ spin, preset }: { spin: boolean; preset: Preset }) { +function Controls({ spin, preset, distance }: { spin: boolean; preset: Preset; distance: number }) { const { camera, gl, invalidate } = useThree(); const controls = useMemo(() => { camera.up.set(0, 0, 1); @@ -135,11 +143,11 @@ function Controls({ spin, preset }: { spin: boolean; preset: Preset }) { first.current = false; return; } - camera.position.set(...PRESETS[preset]); + camera.position.set(...away(PRESETS[preset], distance)); controls.autoRotate = false; controls.update(); invalidate(); - }, [preset, camera, controls, invalidate]); + }, [preset, distance, camera, controls, invalidate]); useEffect(() => () => controls.dispose(), [controls]); useFrame(() => controls.update()); @@ -169,11 +177,12 @@ function useTokens(): Tokens { return tokens; } -function Scene({ view, mesh, photo, architect, data, uniforms, spin, layers, bounds, surfaces, model, tokens, options, axisLabels }: { view: View; mesh: MeshData | null; photo: PhotoData | null; architect: ArchitectData | null; data: CloudData; uniforms: CloudUniforms; spin: boolean; layers: Layers; bounds: Bounds; surfaces: Surface[]; model: Model; tokens: Tokens; options: Options; axisLabels: ViewerStrings["axisLabels"] }) { +function Scene({ view, mesh, photo, architect, data, uniforms, spin, distance, layers, bounds, surfaces, model, tokens, options, axisLabels }: { view: View; distance: number; mesh: MeshData | null; photo: PhotoData | null; architect: ArchitectData | null; data: CloudData; uniforms: CloudUniforms; spin: boolean; layers: Layers; bounds: Bounds; surfaces: Surface[]; model: Model; tokens: Tokens; options: Options; axisLabels: ViewerStrings["axisLabels"] }) { const { invalidate } = useThree(); - useEffect(() => invalidate(), [model.selected, layers, view, mesh, photo, architect, options, invalidate]); + useEffect(() => invalidate(), [model.selected, model.walk, layers, view, mesh, photo, architect, options, invalidate]); const axes = options.axes && ; - const controls = ; + const controls = ; + const walk = model.walk && ; useEffect(() => { uniforms.uLow.value = new Color(tokens.line); uniforms.uHigh.value = new Color(tokens.blue); @@ -189,6 +198,7 @@ function Scene({ view, mesh, photo, architect, data, uniforms, spin, layers, bou {axes} + {walk} {controls} ); @@ -199,6 +209,7 @@ function Scene({ view, mesh, photo, architect, data, uniforms, spin, layers, bou {axes} + {walk} {controls} ); @@ -207,8 +218,9 @@ function Scene({ view, mesh, photo, architect, data, uniforms, spin, layers, bou return ( <> {/* an architect's model shows north and scale, not x/y/z axes */} - + + {walk} {controls} ); @@ -219,6 +231,7 @@ function Scene({ view, mesh, photo, architect, data, uniforms, spin, layers, bou {axes} + {walk} {controls} ); @@ -233,6 +246,7 @@ function Scene({ view, mesh, photo, architect, data, uniforms, spin, layers, bou {layers.room && } {layers.things && } {axes} + {walk} {controls} ); @@ -250,14 +264,15 @@ async function load(src: string): Promise { return { positions: new Float32Array(positions!), colors: colors ? new Uint8Array(colors) : null, conf: new Uint8Array(conf!) }; } -export default function ViewerCanvas({ src, bounds, surfaces, room, furniture, things, selected, hasMesh = false, hasPhoto = false, hasArchitect = false, t }: ViewerProps) { +export default function ViewerCanvas({ src, bounds, surfaces, room, furniture, things, selected, hasMesh = false, hasPhoto = false, hasArchitect = false, walk = null, start, t }: ViewerProps) { const root = useRef(null); const id = useId(); const { reduced } = useMotionPrefs(); const [data, setData] = useState(null); const [layers, setLayers] = useState({ mode: "height", grid: true, bands: false, surfaces: false, room: true, things: true }); - const [view, setView] = useState("dots"); - const [options, setOptions] = useState({ preset: "iso", axes: true, wallOpacity: 0.45, showFurniture: true, wireframe: false, flat: false, cut: CUT }); + const [view, setView] = useState(start?.view ?? "dots"); + const [options, setOptions] = useState({ preset: start?.preset ?? "iso", axes: true, wallOpacity: 0.45, showFurniture: true, wireframe: false, flat: false, cut: start?.view === "architect" ? 1.5 : CUT }); + const distance = start?.distance ?? 1; const [moreOpen, setMoreOpen] = useState(false); const set = (key: K, value: Options[K]) => setOptions((o) => ({ ...o, [key]: value })); // each view opens with the settings it reads best in (each can be changed after): @@ -368,10 +383,10 @@ export default function ViewerCanvas({ src, bounds, surfaces, room, furniture, t - + ) : (

{t.loading}

diff --git a/apps/web/components/viewer/WalkDemo.tsx b/apps/web/components/viewer/WalkDemo.tsx new file mode 100644 index 0000000..bb37a7f --- /dev/null +++ b/apps/web/components/viewer/WalkDemo.tsx @@ -0,0 +1,276 @@ +"use client"; + +import { useEffect, useId, useMemo, useRef, useState, useSyncExternalStore } from "react"; +import { format, getDictionary } from "@/i18n"; +import { gsap, MOTION_OK, SplitText, useGSAP } from "@/lib/gsap"; +import { useMotionPrefs } from "@/lib/motion-prefs"; +import { canSpeak, sayLine, stopSpeaking } from "@/lib/speech"; +import type { Walk } from "./Layers"; +import { type ScanMeta, viewerStrings } from "./LocalScan"; +import { RoomViewer } from "./RoomViewer"; + +/** One recorded question: the Guide's answer and, when it names one thing, the walk to it (walks.json, `make demo-walks`). */ +export interface WalkItem { + question: string; + answer: string; + grounded: boolean; + model: string; + label?: string; + /** the thing, in the viewer's frame */ + target?: number[]; + mode?: "route" | "landmarks"; + steps?: { clock: number; metres: number; say: string }[]; + arrive?: string; + /** floor points from where the scan began to the thing */ + path?: [number, number][]; + /** several alike were found: the answer asks which one, so there is no walk yet */ + asks_which?: boolean; +} + +export interface Walks { + label: string; + recorded_at: string; + captured_at: string; + room: string; + walks: WalkItem[]; +} + +const t = getDictionary().app.demo; + +/** The thing's arrow in the viewer: the one with this label nearest the walk's target. */ +function thingIndex(meta: ScanMeta | null, item: WalkItem): number | null { + if (!meta || !item.target || !item.label) return null; + const [x, y] = item.target; + let best: number | null = null; + let bestD = Infinity; + for (const [i, o] of meta.objects.entries()) { + const d = Math.hypot(o.at[0] - x, o.at[1] - y); + if (o.label === item.label && d < bestD) { + best = i; + bestD = d; + } + } + return best; +} + +/** A small clock face with its hand on the step's hour (12 is straight ahead). */ +function Clock({ hour }: { hour: number }) { + return ( + + + + + + + ); +} + +/** + * Ask, then walk: pick a recorded question, read (or hear) Recall's answer, and watch the walk: GSAP writes the answer + * in word by word, draws the route on the floor of the 3D room and brings in each spoken step as the route reaches it. + * Everything is in the page's HTML first, so it reads the same without JavaScript or with motion reduced. + */ +export function WalkDemo({ walks, src }: { walks: Walks; src: string }) { + const id = useId(); + const root = useRef(null); + const { reduced } = useMotionPrefs(); + const [chosen, setChosen] = useState(0); + const [replays, setReplays] = useState(0); + const [meta, setMeta] = useState(null); + const [speaking, setSpeaking] = useState(false); + // speech synthesis exists only in the browser: the server (and the first render) draws no Hear button + const speech = useSyncExternalStore(noSubscribe, canSpeak, () => false); + const item = walks.walks[chosen]; + + useEffect(() => { + let cancelled = false; + fetch(`${src}/meta.json`) + .then((r) => (r.ok ? r.json() : null)) + .catch(() => null) + .then((m) => !cancelled && setMeta(m)); + return () => { + cancelled = true; + }; + }, [src]); + + // a new box per question (and per replay): the route redraws from its start; full when motion is reduced + const walk: Walk | null = useMemo( + () => (item.path && item.path.length > 1 ? { path: item.path, progress: { value: 1 } } : null), + // eslint-disable-next-line react-hooks/exhaustive-deps -- a replay asks for a fresh box on purpose + [item, replays], + ); + const selected = thingIndex(meta, item); + + useGSAP( + () => { + const el = root.current; + if (reduced || !window.matchMedia(MOTION_OK).matches) { + if (el) el.dataset.played = "true"; + return; + } + if (el) el.dataset.played = "false"; + const steps = gsap.utils.toArray("[data-walk-step]"); + const words = SplitText.create("[data-answer-text]", { type: "words", aria: "none" }); + const tl = gsap.timeline({ + defaults: { ease: "power2.out" }, + onComplete: () => { + if (el) el.dataset.played = "true"; + }, + }); + tl.from(words.words, { opacity: 0, y: 6, duration: 0.35, stagger: 0.035 }); + if (walk && item.steps) { + // the route draws at walking pace on screen; each step arrives when the route reaches its start + const metres = item.steps.map((s) => Math.max(s.metres, 0.2)); + const total = metres.reduce((a, b) => a + b, 0); + const span = Math.min(4, 0.9 + total * 0.45); + const at = tl.duration(); + tl.fromTo(walk.progress, { value: 0 }, { value: 1, duration: span, ease: "none", immediateRender: true }, at); + let done = 0; + steps.forEach((el, i) => { + const start = at + (span * done) / total; + tl.from(el, { opacity: 0, x: -14, duration: 0.4 }, start); + tl.from(el.querySelector("[data-hand]"), { rotation: 0, svgOrigin: "20 20", duration: 0.5, ease: "back.out(2)" }, start); + done += metres[i] ?? 0; + }); + tl.from("[data-walk-arrive]", { opacity: 0, y: 8, duration: 0.4 }, at + span); + } else { + tl.from("[data-walk-note]", { opacity: 0, y: 8, duration: 0.4 }); + } + return () => { + words.revert(); + if (walk) gsap.set(walk.progress, { value: 1 }); + }; + }, + { scope: root, dependencies: [walk, reduced], revertOnUpdate: true }, + ); + + useEffect(() => () => stopSpeaking(), []); + const hear = async () => { + if (speaking) { + stopSpeaking(); + setSpeaking(false); + return; + } + setSpeaking(true); + const lines = [item.answer, ...(item.steps ? [t.start, ...item.steps.map((s) => s.say), item.arrive ?? ""] : [])].filter(Boolean); + await sayLine(lines.join(" "), "assistant", { long: true }); + setSpeaking(false); + }; + + return ( +
+
+ {meta ? ( + + ) : ( +

{viewerStrings().loading}

+ )} +

{t.legend}

+
+ +
+
+ {t.ask} +

+ {t.ask} +

+
+ {walks.walks.map((w, i) => ( + + ))} +
+
+ +
+

+ {t.youAsked} “{item.question}” +

+

+ {t.answer} +

+ {/* screen readers get the sentence whole; the copy on screen is split into words for the animation */} +

{item.answer}

+

+ {item.answer} +

+
+ {speech && ( + + )} + {walk && ( + + )} +
+
+ +
+

+ {t.walkTitle} +

+ {item.steps && item.path ? ( +
    +
  1. {t.start}
  2. + {item.steps.map((s, i) => ( +
  3. + + + + {s.say} +
  4. + ))} +
  5. + {t.arrive}: + {item.arrive} +
  6. +
+ ) : ( +

+ {item.asks_which ? t.asksWhich : t.notSeen} +

+ )} +
+ +

+ {format(t.recorded, { date: walks.recorded_at.slice(0, 10) })} +

+
+
+ ); +} + +const noSubscribe = () => () => {}; diff --git a/apps/web/e2e/demo.spec.ts b/apps/web/e2e/demo.spec.ts new file mode 100644 index 0000000..e424de9 --- /dev/null +++ b/apps/web/e2e/demo.spec.ts @@ -0,0 +1,88 @@ +import fs from "node:fs"; +import path from "node:path"; +import AxeBuilder from "@axe-core/playwright"; +import { expect, test } from "@playwright/test"; +import { en } from "../i18n/dictionaries/en"; +import { open, prepare } from "./helpers"; + +/** + * /demo: the no-keys demo. Recorded answers from a real run (samples/replay/flat) and the planner's walks, read from + * public/real-scan/flat/walks.json (`make demo-walks`). Nothing here calls a model or the API. + */ + +const PAGE = "/demo"; +const t = en.app.demo; +const walks = JSON.parse(fs.readFileSync(path.join(__dirname, "../public/real-scan/flat/walks.json"), "utf8")); +const recorded = JSON.parse(fs.readFileSync(path.join(__dirname, "../../../samples/replay/flat/meta.json"), "utf8")); +const keys = walks.walks.find((w: { question: string }) => w.question === "Where are my keys?"); + +test.beforeEach(async ({ page }) => prepare(page)); + +test("the walks are the recorded run's answers, labelled as replayed", () => { + expect(walks.recorded_at).toBe(recorded.recorded_at); + const answers = Object.fromEntries(recorded.answers.map((a: { question: string; answer: string }) => [a.question, a.answer])); + for (const w of walks.walks) { + expect(w.answer, w.question).toBe(answers[w.question]); + expect(w.model).toMatch(/^replay:.*\(recorded\)$/); + } + expect(keys.steps.length).toBeGreaterThan(0); +}); + +test("says it is recorded, and shows the first answer and its walk", async ({ page }) => { + await open(page, PAGE); + await expect(page.locator("[data-recorded-badge]")).toHaveText(t.badge); + await expect(page.locator("[data-recorded]")).toContainText("Recorded output, not live"); + await expect(page.locator("[data-answer-text]")).toHaveText(keys.answer); + await expect(page.getByText(keys.answer, { exact: true }).first()).toBeAttached(); // the screen reader's copy + await expect(page.locator("[data-walk-step]")).toHaveCount(keys.steps.length); + await expect(page.locator("[data-walk-arrive]")).toContainText(keys.arrive); +}); + +test("picking a question shows its answer and walks there step by step", async ({ page }) => { + const bag = walks.walks.find((w: { question: string }) => w.question === "Where is my bag?"); + await open(page, PAGE); + await page.getByText("Where is my bag?", { exact: true }).click(); + await expect(page.locator("[data-answer-text]")).toHaveText(bag.answer); + const steps = page.locator("[data-walk-step]"); + await expect(steps).toHaveCount(bag.steps.length); + await expect(steps.first()).toContainText(bag.steps[0].say); + // once the walk has played, every step is fully shown + await expect.poll(() => steps.last().evaluate((el) => getComputedStyle(el).opacity), { timeout: 10_000 }).toBe("1"); +}); + +test("an answer that asks which one, or a thing never seen, has no walk", async ({ page }) => { + await open(page, PAGE); + await page.getByText("Where is my laptop?", { exact: true }).click(); + await expect(page.locator("[data-walk-note]")).toHaveText(t.asksWhich); + await expect(page.locator("[data-walk-step]")).toHaveCount(0); + await page.getByText("Where is my umbrella?", { exact: true }).click(); + await expect(page.locator("[data-walk-note]")).toHaveText(t.notSeen); +}); + +test("with motion reduced, every step is there at once", async ({ browser }) => { + const context = await browser.newContext({ reducedMotion: "reduce" }); + const page = await context.newPage(); + await prepare(page); + await open(page, PAGE); + await page.getByText("Where is my wallet?", { exact: true }).click(); + const steps = page.locator("[data-walk-step]"); + for (const el of await steps.all()) await expect(el).toHaveCSS("opacity", "1"); + await context.close(); +}); + +test("reads without JavaScript: the answer and the steps are in the page", async ({ browser }) => { + const context = await browser.newContext({ javaScriptEnabled: false }); + const page = await context.newPage(); + await page.goto(PAGE); + await expect(page.locator("[data-answer-text]")).toHaveText(keys.answer); + await expect(page.locator("[data-walk-step]")).toHaveCount(keys.steps.length); + await context.close(); +}); + +test("has no WCAG 2.2 AA violations", async ({ page }) => { + await open(page, PAGE); + // measured once the walk has played (mid-fade, words are lighter on purpose) + await page.waitForFunction(() => document.querySelector("[data-walk-demo]")?.dataset.played === "true"); + const results = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"]).analyze(); + expect(results.violations).toEqual([]); +}); diff --git a/apps/web/i18n/dictionaries/en.ts b/apps/web/i18n/dictionaries/en.ts index a6726ea..c1ca3ec 100644 --- a/apps/web/i18n/dictionaries/en.ts +++ b/apps/web/i18n/dictionaries/en.ts @@ -785,5 +785,32 @@ export const en = { privacyNever: "Never published: photos, video, depth images, anything anyone said, keys or paths.", }, }, + demo: { + metaTitle: "Try Recall: ask, then walk", + metaDescription: "Ask where something is in a real scanned living room, hear Recall's recorded answer, and follow the clock-face steps to it. No account, no keys.", + badge: "Recorded demo · real scan, real answers", + title: "Ask, then walk", + lead: "This is the author's living room, scanned once with an iPhone. Pick a question: you get Recall's answer, then the steps the phone would speak, drawn as a route to the thing.", + recorded: "Recorded output, not live: Muse Spark Open's real replies, recorded on {date}, replayed through Recall's own code. The routes come from the same planner the phone uses, on this scan's floor.", + ask: "Ask Recall", + youAsked: "You asked", + answer: "Recall says", + hear: "Hear it", + stop: "Stop", + walkTitle: "The walk", + start: "Stand where the scan began, facing into the room.", + arrive: "Arrive", + asksWhich: "Recall found more than one, so it asks which you mean before guiding you. Pick a question with one answer to see a walk.", + notSeen: "Nothing to walk to: Recall hasn't seen it, so it says so instead of guessing.", + replay: "Replay the walk", + viewerAlt: "The scanned living room in 3D, with a green route on the floor from where the scan began to the thing.", + legend: "Seen from above. Green: the walk, from the ring where the scan began. Large dark blue marker: the thing.", + judges: { + title: "Run this yourself", + body: "Clone the repo and run one command. No keys, no GPU, no account: the same recorded answers, served by Recall's API on your computer, and this page at localhost:3000/demo.", + code: "git clone https://github.com/coder-RT/recall && cd recall && make setup && make demo", + }, + viewer: "Explore the whole scan", + }, }, }; From 8d65b55bfe2d36a35258bc5c99eca4cd2e0a4d7d Mon Sep 17 00:00:00 2001 From: Rachit Date: Tue, 3 Nov 2026 03:01:43 +0100 Subject: [PATCH 11/30] docs: judges start with the demo page and make demo Co-Authored-By: Claude Opus 5.5 --- README.md | 36 +++++++++++++++++++++++++++++------- apps/web/public/INSTALL.md | 37 +++++++++++++++++++++++++++++-------- docs/install/README.md | 37 +++++++++++++++++++++++++++++-------- 3 files changed, 87 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 16aaf11..fdae649 100644 --- a/README.md +++ b/README.md @@ -46,11 +46,12 @@ Indie track · Theme: Build for Your World

- Try it (no keys) · + Try it (no keys) · Architecture · API · Privacy · @@ -77,11 +78,24 @@ Indie track · Theme: Build for Your World

### For judges, in one minute +**Start with the demo.** Nothing to install: open +**[recall-ten-khaki.vercel.app/demo](https://recall-ten-khaki.vercel.app/demo)**, pick "Where are my keys?", and +follow the walk to them in the author's real scanned living room. To run the same thing from this repo, with no keys, +no GPU and no account: + +```bash +git clone https://github.com/coder-RT/recall.git && cd recall && make setup && make demo # then open localhost:3000/demo +``` + +`make demo` starts Recall's API on a memory built from that real scan and the website beside it. The answers are Muse +Spark Open's real replies, recorded once and replayed through Recall's own code, and labelled as recorded; the routes +come from the planner the phone uses, on the scan's floor. + | Criterion | Where to look | |---|---| | **Impact** (30 %) | [Why I built it](#why-i-built-it): a true story · [What it is](#what-it-is): built first for blind and low-vision people, for anyone who misplaces things, voice first · [Privacy](docs/privacy.md): the home stays on your own computer · the [tester session plan](evals/usability/) | | **Use of Meta AI tooling** (25 %) | [Only Meta models, each with one job](#how-it-works-the-meta-models): MapAnything, SAM 3.1, Muse Spark Open (Muse Glimmer on your own Mac), Muse Voice Transcribe, Muse Spark 1.1 · [Recall's three agents](#recalls-three-agents) · [Sharing with robots](#sharing-with-robots) over A2A | -| **Demo** (25 %) | The screenshots below are real output · `make demo` replays a real run with no keys · [How to run it](#how-to-run-it) | +| **Demo** (25 %) | [The demo page](https://recall-ten-khaki.vercel.app/demo) and `make demo`: a real run, no keys · the screenshots below are real output · [How to run it](#how-to-run-it) | | **Technical depth** (20 %) | [Architecture](docs/architecture.md) · [28 decisions](docs/decisions.md) · [Guardrails](#guardrails): geometry decides the facts, the model only phrases them · [Algorithms](#algorithms) · tests on every part ([Results so far](#results-so-far)) | --- @@ -173,7 +187,7 @@ floor, furniture and walls as cells, and the things as dots: no photos of the ho | iPhone app (Swift 6, SwiftUI, ARKit; VoiceOver first) | `apps/ios` (build with XcodeGen) | Works on an iPhone with LiDAR, iOS 26 | | The API on your computer: the Guide, the arrival check, memory, privacy, robots | `services/api` | Works (`make live`) | | The pipeline: 3D reconstruction, furniture and things, the room model | `services/pipeline` | Works (`make worker`) | -| A recorded real run you can try with no keys | `make demo` | Works; the recording is added after a privacy review | +| A recorded real run you can try with no keys: ask, then walk | `make demo`, [`/demo`](https://recall-ten-khaki.vercel.app/demo) | Works | | Website and 3D viewer of your own scan | `apps/web` | Works (`pnpm dev`) | | API spec (OpenAPI 3.1, Swagger UI) and the A2A Agent Card | `services/api/resources/` | See [API](#api) | | Architecture, decisions, privacy note, measurements | [`docs/`](docs) | [architecture](docs/architecture.md) · [decisions](docs/decisions.md) · [privacy](docs/privacy.md) · [verification](docs/verification.md) | @@ -496,6 +510,9 @@ Every route except `/health` needs a key: the owner's device key, or a robot's o 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. + - 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. **Security: how the home's memory is protected** @@ -609,7 +626,7 @@ Measured, dated, and recorded in `docs/verification.md`. Nothing below is estima - **Cost, measured:** a room scan is **456** SAM 3.1 calls and **48 MB** on disk; a question is **2** Muse Spark Open calls, about **2,460 + 261 tokens**. - **Flat surfaces:** the geometry-only detector found **12 level patches** in the room; the two largest sit at the heights of the two measured desks. - **Viewer:** about 150k points, the gather animation, photo and confidence colouring, and the floor grid, height bands and surfaces as layers, on a phone and a laptop. -- **Tests (2 November):** API 213 (including Schemathesis against the spec), pipeline 64 (including a synthetic room run through the whole pipeline), website 194 (accessibility with axe, WCAG 2.2 AA, reduced motion, no-JavaScript, keyboard-only and 200 % text usability, security headers); on the iPhone, 197 unit tests and 34 UI tests (2 skipped): accessibility audits and usability tasks done with VoiceOver labels at default and largest text; the Watch app builds. +- **Tests (3 November):** API 218 (including Schemathesis against the spec), pipeline 64 (including a synthetic room run through the whole pipeline), website 201 (accessibility with axe, WCAG 2.2 AA, reduced motion, no-JavaScript, keyboard-only and 200 % text usability, security headers); on the iPhone, 197 unit tests and 34 UI tests (2 skipped): accessibility audits and usability tasks done with VoiceOver labels at default and largest text; the Watch app builds. - **Usability, honestly:** the checks above are automated. In person, the author's screen-off run (VoiceOver Screen Curtain: the screen stays black) is planned, and a session with blind and low-vision testers is planned; its plan is ready in `evals/usability/`. **Planned evaluation:** median position error against hand-measured positions, questions answered correctly, Verifier precision, and time to find an object by touch versus with Recall, scored without AI. The dated table is [`evals/results.md`](evals/results.md); recognising the room from a photo is its first entry. The session plan for a blind or low-vision tester is in `evals/usability/`. @@ -623,7 +640,7 @@ offers the same guide as a download (`/INSTALL.md`). | Path | You get | Needs | Time | |---|---|---|---| | **A. Website** | The landing page and the real 3D scan at `/homes/flat/viewer` | Node 22 | 5 min | -| **B. Demo, no keys** | `make demo`: Recall answering "where are my keys?" from a recorded real scan, labelled as recorded | uv (Python 3.12 comes with it) | 10 min | +| **B. Demo, no keys** (judges: start here) | `make demo`: the demo page at `localhost:3000/demo` (ask, then walk, in a real scanned room) and the API answering from that scan, labelled as recorded | uv (Python 3.12 comes with it), Node 22 | 10 min | | **C. Live** | Your own room: `make live`, the iPhone app (Xcode + XcodeGen), pairing by QR, the Watch | A Mac with Apple silicon, your own Meta Model API and Baseten keys, an iPhone with LiDAR on iOS 26 | 45 min | The short version: @@ -631,7 +648,7 @@ The short version: ```bash git clone https://github.com/coder-RT/recall.git && cd recall make setup # Python services (uv), the web app (pnpm), generated models, the database -make demo # B: no keys; then: curl -s -X POST localhost:8000/ask -H "Authorization: Bearer demo" ... +make demo # B: no keys; open localhost:3000/demo, or curl -s -X POST localhost:8000/ask -H "Authorization: Bearer demo" ... cd apps/web && pnpm dev # A: http://localhost:3000 and /homes/flat/viewer ``` @@ -684,7 +701,12 @@ Watch app all run. ### 3. Backend, pipeline and demo - `make setup`: Python 3.12 with uv, both services, the web app. -- `make demo`: no keys, no GPU. Builds a memory from a recorded real scan (`samples/replay/`, geometry and labels only, no photos) through the real ingest code, then serves the API on `localhost:8000` with Muse Spark Open's recorded replies; every answer is labelled `replay:muse-spark-open (recorded)`, and the command prints questions to try. `make record` re-records it with real calls. +- `make demo` (**judges: start here**): no keys, no GPU. + - It builds a memory from a recorded real scan through the real ingest code. The scan is `samples/replay/`: the room model, labels, a thinned point cloud and the path walked, with no photos. + - It serves the API on `localhost:8000` with Muse Spark Open's recorded replies. Every answer is labelled `replay:muse-spark-open (recorded)`, and the command prints questions to try. + - It starts the website, with the demo page at `localhost:3000/demo`: pick a question, read or hear the answer, and GSAP plays the walk. Each clock-face step comes in as the route draws on the floor of the 3D room. The same page is online at [recall-ten-khaki.vercel.app/demo](https://recall-ten-khaki.vercel.app/demo). + - `make demo-walks` rebuilds that page's walks (`apps/web/public/real-scan/flat/walks.json`): the replayed Guide's answer, then the phone's own route planner from where the scan began. + - `make record` re-records the answers with real calls, and then the walks. - `make live`: the API and the scan worker (`make worker` alone starts the worker: the warm pipeline server plus the job queue), with real Meta model calls using the keys from `.env`. The minimum is one Meta Model API key from [dev.meta.ai](https://dev.meta.ai) with `LLM_PROVIDER=meta`; the demo video uses Muse Spark Open on Baseten (`LLM_PROVIDER=baseten_spark_open`); the local privacy mode uses Muse Glimmer in LM Studio (`LLM_PROVIDER=lmstudio`). - **Where the API listens:** `make live` and `make dev` listen on your network (`BIND=0.0.0.0`) so the phone can reach the Mac; behind a Cloudflare Tunnel use `make live BIND=127.0.0.1`. Run on its own (`uv run recall-api`), it listens on 127.0.0.1 only. Every route except `/health` and the API docs needs the device key, the calls that reach a model are rate-limited, and bodies are size-checked. - **Home Memory agent (A2A) with `make live`:** its check runs on Muse Spark Open with the Baseten key (`SCREEN_PROVIDER=baseten_spark_open`, the default) or on Glimmer in LM Studio (`SCREEN_PROVIDER=lmstudio`); no other key is needed. diff --git a/apps/web/public/INSTALL.md b/apps/web/public/INSTALL.md index fc2309c..9d54212 100644 --- a/apps/web/public/INSTALL.md +++ b/apps/web/public/INSTALL.md @@ -38,12 +38,16 @@ Built during the Meta Global AI Developer Hackathon, Oct 26 to Nov 4, 2026. Step by step, for a person or for an AI coding agent. Every step has the command, what you should see, and what to do if you don't. Do the steps in order, and stop at the first check that fails. +**Judges: start with Path B, `make demo`.** It needs no keys, no GPU and no account, and opens a page where you ask +"where are my keys?" in a real scanned living room and follow the walk to them. The same page is online, with +nothing to install: [recall-ten-khaki.vercel.app/demo](https://recall-ten-khaki.vercel.app/demo). + **Pick a path.** Each one builds on the one before. | Path | You get | Needs | Time | |---|---|---|---| | **A. Website** | The landing page and the real 3D scan at `/homes/flat/viewer` | macOS, Linux or Windows (WSL) with Node 22 | 5 min | -| **B. Demo, no keys** | The API answering "where are my keys?" from a recorded real scan, with recorded Muse Spark Open replies | macOS (Apple silicon) or Linux | 10 min | +| **B. Demo, no keys** (start here) | The demo page at `localhost:3000/demo`: ask, then walk, in a real scanned room. Behind it, the API answering from that scan with recorded Muse Spark Open replies | macOS (Apple silicon) or Linux, Node 22 | 10 min | | **C. Live** | Real Meta model calls, the scan pipeline, and the iPhone app scanning your own room | A Mac with Apple silicon (24 GB memory or more), your own keys, an iPhone with LiDAR on iOS 26 | 45 min | Nothing asks for a Recall account. Paths A and B need no keys. Path C needs your own Meta Model API key, and a @@ -121,7 +125,7 @@ Path A is done. Stop the server with `Ctrl+C`. --- -## Path B: the demo API, no keys +## Path B: the demo, no keys ### B1. uv, the Python manager @@ -156,10 +160,26 @@ This step: make demo ``` -**Check:** it prints "Recall demo: Recorded output of a real run…", a list of questions to try, and -`Uvicorn running on http://127.0.0.1:8000`. +This starts two things: Recall's API on a memory built from a recorded real scan, and the website. + +**Check:** it prints "Recall demo: Recorded output of a real run…", a list of questions to try, +`Uvicorn running on http://127.0.0.1:8000`, and the website's `Ready`. +**If it says "No website here":** `make setup` didn't install the web app (it needs Node 22 and pnpm). The API still +runs; the same demo page is online at [recall-ten-khaki.vercel.app/demo](https://recall-ten-khaki.vercel.app/demo). + +### B4. Open the demo page + +Open `http://localhost:3000/demo`. + +**Check:** +- the badge says "Recorded demo · real scan, real answers"; +- Recall's answer to "Where are my keys?" is shown, with the walk to them as clock-face steps; +- choosing "Where is my bag?" draws a green route on the floor of the 3D room, one step at a time. + +The answers are Muse Spark Open's real replies, recorded once and replayed through Recall's own code. The routes come +from the same planner the phone uses, on the scan's floor. Nothing on this page calls a model. -### B4. Ask it something +### B5. Ask the API something In a second terminal: @@ -169,12 +189,12 @@ curl -s -X POST http://127.0.0.1:8000/ask \ -d '{"home_id": "flat", "question": "Where are my keys?"}' ``` -**Check:** a JSON answer like `"Your keys are in the Living room near the desk at waist height, seen yesterday at -04:21."`, with `"model": "replay:muse-spark-open (recorded)"`. Every demo answer is labelled as recorded. +**Check:** a JSON answer like `"Your brown keys are near the desk in the living room at knee height, seen on Sunday at +19:11."`, with `"model": "replay:muse-spark-open (recorded)"`. Every demo answer is labelled as recorded. The API documentation (Swagger UI) is at `http://127.0.0.1:8000/docs`. -### B5. Run the tests (optional) +### B6. Run the tests (optional) ```bash make test lint lint-imports @@ -340,6 +360,7 @@ Each run's logs go to `data/test-runs/

Live site: recall-ten-khaki.vercel.app (also at recall.onica.ai) · + Try the demo: ask, then walk · Explore a real room