moeru-ai/airi · error · Error
The first motion sample must start at 0 ms.
Error message
The first motion sample must start at 0 ms.
What it means
Motion interpolation assumes the recording begins at t=0 so the first sample anchors the timeline. parseLive2DMotionRecording checks samples[0].atMs === 0 after schema validation and throws this error if the first sample starts at a later time. Recordings from the built-in recorder always start at 0, so this usually means the file was edited or trimmed incorrectly.
Source
Thrown at packages/stage-ui/src/features/devtools/motion/live2d/composables/recording.ts:96
* parseLive2DMotionRecording('{"format":"airi-live2d-motion/v6", ...}')
* // => a validated recording
*/
export function parseLive2DMotionRecording(raw: string): Live2DMotionRecording {
let input: unknown
try {
input = JSON.parse(raw)
}
catch {
throw new Error('The file does not contain valid JSON.')
}
const result = safeParse(live2dMotionRecordingSchema, input)
if (!result.success)
throw new Error('The file is not an AIRI Live2D motion recording.')
const { durationMs, samples } = result.output
if (samples[0].atMs !== 0)
throw new Error('The first motion sample must start at 0 ms.')
for (let index = 1; index < samples.length; index++) {
if (samples[index].atMs < samples[index - 1].atMs)
throw new Error('The motion samples must be in time order.')
}
if (samples.at(-1)!.atMs > durationMs)
throw new Error('A motion sample occurs after the recording duration.')
return result.output
}
/**
* Serializes a Live2D joystick recording as a readable JSON file.
*
* @example
* stringifyLive2DMotionRecording({ format: 'airi-live2d-motion/v6', ... })
* // => readable JSON ending with a newlineView on GitHub (pinned to 9c213115f8)
Solutions
- Rebase all sample timestamps so the first sample is at 0 ms (subtract samples[0].atMs from every atMs, and durationMs accordingly)
- Re-record the motion with the AIRI recorder
- Trim samples via a tool that rebases timestamps automatically
Example fix
// before
parseLive2DMotionRecording(raw)
// after
const data = JSON.parse(raw)
const offset = data.samples[0].atMs
data.samples.forEach(s => { s.atMs -= offset })
data.durationMs -= offset
parseLive2DMotionRecording(JSON.stringify(data)) Defensive patterns
Strategy: validation
Validate before calling
const data = JSON.parse(raw)
if (data.samples?.length && data.samples[0].atMs !== 0) {
const offset = data.samples[0].atMs
data.samples.forEach(s => { s.atMs -= offset })
data.durationMs -= offset
raw = JSON.stringify(data)
} Type guard
const startsAtZero = (v) => Array.isArray(v.samples) && v.samples.length > 0 && v.samples[0].atMs === 0
Try / catch
try {
const recording = parseLive2DMotionRecording(raw)
}
catch (error) {
if (error.message === 'The first motion sample must start at 0 ms.')
showImportError('Recording must start at 0 ms. Trim with timestamp rebasing or re-record.')
else throw error
} Prevention
- Rebase timestamps when trimming the head of a recording
- Never delete leading samples without adjusting atMs values
- Test trimming utilities against the samples[0].atMs === 0 invariant
When it happens
Trigger: Calling parseLive2DMotionRecording where the first sample's atMs is non-zero, e.g. after trimming the head of a recording without rebasing timestamps, or importing a fragment recorded starting at an offset.
Common situations: Manually deleting the first samples from a JSON export; programmatic trimming that removes samples but keeps original atMs values; concatenating recordings where the second clip retains absolute timestamps.
Related errors
- A motion sample occurs after the recording duration.
- The crop range is outside the motion timeline.
- The motion project source timeline is invalid.
- The motion project source samples are not in time order.
- The motion project contains an invalid overlay span.
AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-09-02).
Data as JSON: /api/errors/08647f1e2c2285f8.
Report an issue: GitHub.