Skip to content

Commit a197f65

Browse files
Use a shorter data window for recording and inference (#431)
We now record data for one second (990ms) and infer using time window while maintaining backwards compatibility with existing data samples that use the legacy window of 1800 ms. This makes the model feel much more responsive to user gesture/action changes. - Look at the user's existing gestures to determine whether we should use the legacy data window - Default to the new shorter data window where possible (new session, user deletes all recordings, user deletes all actions) - Maintain existing data window for dataset.json and hex file loads Other fixes and upgrades: - Upgrade pxt-microbit-ml extension to include ZCR fix - Fix prop casing for AlertIcon - Remove circular reference in tour files - Pull up listener for MakeCode project load so it affects all pages
1 parent 94b3048 commit a197f65

20 files changed

+326
-173
lines changed

src/App.tsx

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
2-
import { ChakraProvider } from "@chakra-ui/react";
2+
import { ChakraProvider, useToast } from "@chakra-ui/react";
33
import { MakeCodeFrameDriver } from "@microbit/makecode-embed/react";
44
import React, { ReactNode, useEffect, useMemo, useRef } from "react";
5+
import { useIntl } from "react-intl";
56
import {
67
Outlet,
78
RouterProvider,
89
ScrollRestoration,
910
createBrowserRouter,
11+
useNavigate,
1012
} from "react-router-dom";
1113
import { BufferedDataProvider } from "./buffered-data-hooks";
1214
import EditCodeDialog from "./components/EditCodeDialog";
@@ -20,20 +22,21 @@ import { deployment, useDeployment } from "./deployment";
2022
import { ProjectProvider } from "./hooks/project-hooks";
2123
import { LoggingProvider } from "./logging/logging-hooks";
2224
import TranslationProvider from "./messages/TranslationProvider";
25+
import CodePage from "./pages/CodePage";
2326
import DataSamplesPage from "./pages/DataSamplesPage";
2427
import HomePage from "./pages/HomePage";
2528
import ImportPage from "./pages/ImportPage";
2629
import NewPage from "./pages/NewPage";
2730
import TestingModelPage from "./pages/TestingModelPage";
31+
import { useStore } from "./store";
2832
import {
29-
createDataSamplesPageUrl,
3033
createCodePageUrl,
34+
createDataSamplesPageUrl,
3135
createHomePageUrl,
3236
createImportPageUrl,
3337
createNewPageUrl,
3438
createTestingModelPageUrl,
3539
} from "./urls";
36-
import CodePage from "./pages/CodePage";
3740

3841
export interface ProviderLayoutProps {
3942
children: ReactNode;
@@ -69,6 +72,30 @@ const Providers = ({ children }: ProviderLayoutProps) => {
6972

7073
const Layout = () => {
7174
const driverRef = useRef<MakeCodeFrameDriver>(null);
75+
const navigate = useNavigate();
76+
const toast = useToast();
77+
const intl = useIntl();
78+
79+
useEffect(() => {
80+
return useStore.subscribe(
81+
(
82+
{ projectLoadTimestamp },
83+
{ projectLoadTimestamp: prevProjectLoadTimestamp }
84+
) => {
85+
if (projectLoadTimestamp > prevProjectLoadTimestamp) {
86+
// Side effects of loading a project, which MakeCode notifies us of.
87+
navigate(createDataSamplesPageUrl());
88+
toast({
89+
position: "top",
90+
duration: 5_000,
91+
title: intl.formatMessage({ id: "project-loaded" }),
92+
status: "info",
93+
});
94+
}
95+
}
96+
);
97+
}, [intl, navigate, toast]);
98+
7299
return (
73100
// We use this even though we have errorElement as this does logging.
74101
<ErrorBoundary>

src/buffered-data-hooks.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
11
import { AccelerometerDataEvent } from "@microbit/microbit-connection";
2-
import { ReactNode, createContext, useContext, useEffect, useRef } from "react";
2+
import {
3+
ReactNode,
4+
createContext,
5+
useCallback,
6+
useContext,
7+
useEffect,
8+
useRef,
9+
} from "react";
310
import { BufferedData } from "./buffered-data";
411
import { useConnectActions } from "./connect-actions-hooks";
512
import { ConnectionStatus, useConnectStatus } from "./connect-status-hooks";
6-
import { mlSettings } from "./mlConfig";
13+
import { useStore } from "./store";
714

815
const BufferedDataContext = createContext<BufferedData | null>(null);
916

@@ -31,14 +38,15 @@ export const useBufferedData = (): BufferedData => {
3138
const useBufferedDataInternal = (): BufferedData => {
3239
const [connectStatus] = useConnectStatus();
3340
const connection = useConnectActions();
41+
const dataWindow = useStore((s) => s.dataWindow);
3442
const bufferRef = useRef<BufferedData>();
35-
const getBuffer = () => {
43+
const getBuffer = useCallback(() => {
3644
if (bufferRef.current) {
3745
return bufferRef.current;
3846
}
39-
bufferRef.current = new BufferedData(mlSettings.numSamples * 2);
47+
bufferRef.current = new BufferedData(dataWindow.minSamples * 2);
4048
return bufferRef.current;
41-
};
49+
}, [dataWindow.minSamples]);
4250
useEffect(() => {
4351
if (connectStatus !== ConnectionStatus.Connected) {
4452
return;
@@ -56,6 +64,6 @@ const useBufferedDataInternal = (): BufferedData => {
5664
return () => {
5765
connection.removeAccelerometerListener(listener);
5866
};
59-
}, [connection, connectStatus]);
67+
}, [connection, connectStatus, getBuffer]);
6068
return getBuffer();
6169
};

src/components/AlertIcon.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ interface AlertIconProps extends Omit<IconProps, "viewBox"> {}
55
// Modified version of RiAlertFill from react-icons (MIT licensed icon set used elsewhere)
66
const AlertIcon = ({ ...props }: AlertIconProps) => {
77
return (
8-
<Icon stroke-width="0" viewBox="0 0 24 24" {...props}>
8+
<Icon strokeWidth="0" viewBox="0 0 24 24" {...props}>
99
<path
1010
d="M12.8659 3.00017L22.3922 19.5002C22.6684 19.9785 22.5045 20.5901 22.0262 20.8662C21.8742 20.954 21.7017 21.0002 21.5262 21.0002H2.47363C1.92135 21.0002 1.47363 20.5525 1.47363 20.0002C1.47363 19.8246 1.51984 19.6522 1.60761 19.5002L11.1339 3.00017C11.41 2.52187 12.0216 2.358 12.4999 2.63414C12.6519 2.72191 12.7782 2.84815 12.8659 3.00017ZM10.9999 16.0002V18.0002H12.9999V16.0002H10.9999ZM10.9999 9.00017V14.0002H12.9999V9.00017H10.9999Z"
1111
id="path1"

src/components/CodeViewGridItem.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,11 @@ interface CodeViewGridItemProps {
1616
const CodeViewGridItem = ({ gesture }: CodeViewGridItemProps) => {
1717
const model = useStore((s) => s.model);
1818
const gestures = useStore((s) => s.gestures);
19+
const dataWindow = useStore((s) => s.dataWindow);
1920
const project = useMemo(
2021
// Project name is left empty as it is not used or displayed.
21-
() => generateProject("", { data: gestures }, model, gesture),
22-
[gesture, gestures, model]
22+
() => generateProject("", { data: gestures }, model, dataWindow, gesture),
23+
[dataWindow, gesture, gestures, model]
2324
);
2425
const width = useMemo(
2526
() => `${120 + gesture.name.length * 5}px`,

src/components/DefaultPageLayout.tsx

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import {
77
IconButton,
88
MenuDivider,
99
MenuItem,
10-
useToast,
1110
VStack,
1211
} from "@chakra-ui/react";
1312
import { ReactNode, useCallback, useEffect } from "react";
@@ -24,7 +23,7 @@ import { useProject } from "../hooks/project-hooks";
2423
import { SaveStep, TrainModelDialogStage } from "../model";
2524
import Tour from "../pages/Tour";
2625
import { useStore } from "../store";
27-
import { createDataSamplesPageUrl, createHomePageUrl } from "../urls";
26+
import { createHomePageUrl } from "../urls";
2827
import ActionBar from "./ActionBar";
2928
import AppLogo from "./AppLogo";
3029
import ConnectionDialogs from "./ConnectionFlowDialogs";
@@ -54,7 +53,6 @@ const DefaultPageLayout = ({
5453
showPageTitle = false,
5554
}: DefaultPageLayoutProps) => {
5655
const intl = useIntl();
57-
const navigate = useNavigate();
5856
const isEditorOpen = useStore((s) => s.isEditorOpen);
5957
const isTourClosed = useStore((s) => s.tourState === undefined);
6058
const isTrainDialogClosed = useStore(
@@ -63,8 +61,6 @@ const DefaultPageLayout = ({
6361
const { stage } = useConnectionStage();
6462
const isConnectionDialogClosed = stage.flowStep === ConnectionFlowStep.None;
6563
const isSaveDialogClosed = useStore((s) => s.save.step === SaveStep.None);
66-
67-
const toast = useToast();
6864
const { appNameFull } = useDeployment();
6965

7066
useEffect(() => {
@@ -73,26 +69,6 @@ const DefaultPageLayout = ({
7369
: appNameFull;
7470
}, [appNameFull, intl, titleId]);
7571

76-
useEffect(() => {
77-
return useStore.subscribe(
78-
(
79-
{ projectLoadTimestamp },
80-
{ projectLoadTimestamp: prevProjectLoadTimestamp }
81-
) => {
82-
if (projectLoadTimestamp > prevProjectLoadTimestamp) {
83-
// Side effects of loading a project, which MakeCode notifies us of.
84-
navigate(createDataSamplesPageUrl());
85-
toast({
86-
position: "top",
87-
duration: 5_000,
88-
title: intl.formatMessage({ id: "project-loaded" }),
89-
status: "info",
90-
});
91-
}
92-
}
93-
);
94-
}, [intl, navigate, toast]);
95-
9672
return (
9773
<>
9874
{/* Suppress dialogs to prevent overlapping dialogs */}

src/components/LiveGraph.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { RiArrowDropLeftFill } from "react-icons/ri";
1010
import React from "react";
1111
import { LabelConfig, getUpdatedLabelConfig } from "../live-graph-label-config";
1212
import { useStore } from "../store";
13-
import { mlSettings } from "../mlConfig";
1413

1514
const initialLabelConfigs: LabelConfig[] = [
1615
{ label: "x", arrowHeight: 0, labelHeight: 0, color: "#f9808e", id: 0 },
@@ -89,6 +88,7 @@ const LiveGraph = () => {
8988
// Draw on graph to display that users are recording
9089
// Ideally we'd do this without timing the recording again!
9190
const [isTimingRecording, setIsTimingRecording] = useState<boolean>(false);
91+
const dataWindow = useStore((s) => s.dataWindow);
9292
const isRecording = useStore((s) => s.isRecording);
9393
useEffect(() => {
9494
if (isRecording && !isTimingRecording) {
@@ -106,9 +106,9 @@ const LiveGraph = () => {
106106
recordLines.append(now - 1, 2.3, false);
107107
recordLines.append(now, -2, false);
108108
setIsTimingRecording(false);
109-
}, mlSettings.duration);
109+
}, dataWindow.duration);
110110
}
111-
}, [isTimingRecording, recordLines, isRecording]);
111+
}, [isTimingRecording, recordLines, isRecording, dataWindow.duration]);
112112

113113
const [labelConfigs, setLabelConfigs] =
114114
useState<LabelConfig[]>(initialLabelConfigs);

src/components/RecordingDialog.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { TimedXYZ } from "../buffered-data";
1818
import { useBufferedData } from "../buffered-data-hooks";
1919
import { GestureData, XYZData } from "../model";
2020
import { useStore } from "../store";
21-
import { mlSettings } from "../mlConfig";
2221

2322
interface CountdownStage {
2423
value: string | number;
@@ -231,13 +230,14 @@ interface RecordingDataSource {
231230

232231
const useRecordingDataSource = (): RecordingDataSource => {
233232
const ref = useRef<InProgressRecording | undefined>();
233+
const dataWindow = useStore((s) => s.dataWindow);
234234
const bufferedData = useBufferedData();
235235
useEffect(() => {
236236
const listener = (sample: TimedXYZ) => {
237237
if (ref.current) {
238238
const percentage =
239239
((sample.timestamp - ref.current.startTimeMillis) /
240-
mlSettings.duration) *
240+
dataWindow.duration) *
241241
100;
242242
ref.current.onProgress(percentage);
243243
}
@@ -246,7 +246,7 @@ const useRecordingDataSource = (): RecordingDataSource => {
246246
return () => {
247247
bufferedData.removeListener(listener);
248248
};
249-
}, [bufferedData]);
249+
}, [bufferedData, dataWindow.duration]);
250250

251251
return useMemo(
252252
() => ({
@@ -257,10 +257,10 @@ const useRecordingDataSource = (): RecordingDataSource => {
257257
if (ref.current) {
258258
const data = bufferedData.getSamples(
259259
ref.current.startTimeMillis,
260-
ref.current.startTimeMillis + mlSettings.duration
260+
ref.current.startTimeMillis + dataWindow.duration
261261
);
262262
const sampleCount = data.x.length;
263-
if (sampleCount < mlSettings.minSamples) {
263+
if (sampleCount < dataWindow.minSamples) {
264264
ref.current.onError();
265265
ref.current = undefined;
266266
} else {
@@ -269,7 +269,7 @@ const useRecordingDataSource = (): RecordingDataSource => {
269269
ref.current = undefined;
270270
}
271271
}
272-
}, mlSettings.duration);
272+
}, dataWindow.duration);
273273

274274
ref.current = {
275275
startTimeMillis: Date.now(),
@@ -281,7 +281,7 @@ const useRecordingDataSource = (): RecordingDataSource => {
281281
ref.current = undefined;
282282
},
283283
}),
284-
[bufferedData]
284+
[bufferedData, dataWindow.duration, dataWindow.minSamples]
285285
);
286286
};
287287

src/components/RecordingFingerprint.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { applyFilters } from "../ml";
44
import { XYZData } from "../model";
55
import { calculateGradientColor } from "../utils/gradient-calculator";
66
import ClickableTooltip from "./ClickableTooltip";
7+
import { useStore } from "../store";
78

89
interface RecordingFingerprintProps extends BoxProps {
910
data: XYZData;
@@ -15,7 +16,8 @@ const RecordingFingerprint = ({
1516
gestureName,
1617
...rest
1718
}: RecordingFingerprintProps) => {
18-
const dataFeatures = applyFilters(data, { normalize: true });
19+
const dataWindow = useStore((s) => s.dataWindow);
20+
const dataFeatures = applyFilters(data, dataWindow, { normalize: true });
1921

2022
return (
2123
<Grid

src/hooks/ml-hooks.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export const usePrediction = () => {
2121
const [prediction, setPrediction] = useState<PredictionResult | undefined>();
2222
const gestureData = useStore((s) => s.gestures);
2323
const model = useStore((s) => s.model);
24+
const dataWindow = useStore((s) => s.dataWindow);
2425

2526
// Use a ref to prevent restarting the effect every time thesholds change.
2627
// We only use the ref's value during the setInterval callback not render.
@@ -32,14 +33,14 @@ export const usePrediction = () => {
3233
return;
3334
}
3435
const runPrediction = async () => {
35-
const startTime = Date.now() - mlSettings.duration;
36+
const startTime = Date.now() - dataWindow.duration;
3637
const input = {
3738
model,
3839
data: buffer.getSamples(startTime),
3940
classificationIds: gestureDataRef.current.map((g) => g.ID),
4041
};
41-
if (input.data.x.length > mlSettings.minSamples) {
42-
const result = await predict(input);
42+
if (input.data.x.length > dataWindow.minSamples) {
43+
const result = await predict(input, dataWindow);
4344
if (result.error) {
4445
logging.error(result.detail);
4546
} else {
@@ -63,7 +64,7 @@ export const usePrediction = () => {
6364
setPrediction(undefined);
6465
clearInterval(interval);
6566
};
66-
}, [connection, logging, connectStatus, buffer, model]);
67+
}, [connection, logging, connectStatus, buffer, model, dataWindow]);
6768

6869
return prediction;
6970
};

0 commit comments

Comments
 (0)