Repository navigation
Description
Describe the bug
After replacing music at the same project path with audio that has no steady beat, rerunning hyperframes beats exits successfully and reports count: 0, but leaves the old nonempty beat file on disk. Studio loads that saved grid in preference to detected beats, so reopening the project can still show and snap to beats from the previous track.
Reproduced on upstream main 7bf19ab5f9d1690d596334822f251dece3921e1a. This is about regenerating an existing grid after changing the source, rather than a track that has never had a grid.
Minimal reproduction
Use a source checkout at that revision, Bun 1.4.4, Node 22 or newer, Python 3, and FFmpeg/FFprobe on PATH:
git checkout 7bf19ab5f9d1690d596334822f251dece3921e1a
PUPPETEER_SKIP_DOWNLOAD=1 bun install --frozen-lockfile
bun run build
python3 repro.py "$PWD"Save this as repro.py. It creates only synthetic audio in a fresh temporary project:
import json, math, pathlib, struct, subprocess, sys, tempfile, wave, os
repo = pathlib.Path(sys.argv[1]).resolve()
project = pathlib.Path(tempfile.mkdtemp(prefix='hf-beats-replaced-'))
(project / 'index.html').write_text('<div data-composition-id="main"><audio id="music" src="song.wav"></audio></div>\n')
rate = 22050
samples = [0.0] * (12 * rate)
seed = 7
for beat in range(24):
start = round((0.2 + beat * 0.5) * rate)
for i in range(round(0.05 * rate)):
if start + i >= len(samples): break
seed = (seed * 1103515245 + 12345) % 2147483648
samples[start + i] += 0.6 * (seed / 2**30 - 1) * math.exp(-i / rate / 0.015)
def save(values):
with wave.open(str(project / 'song.wav'), 'wb') as f:
f.setparams((1, 2, rate, 0, 'NONE', 'not compressed'))
f.writeframes(b''.join(struct.pack('<h', round(max(-1, min(1, x)) * 32767)) for x in values))
def run():
p = subprocess.run(['bun', str(repo / 'packages/cli/src/cli.ts')] + ['beats', str(project), '--json'], text=True, capture_output=True,
env={**os.environ, 'HYPERFRAMES_NO_TELEMETRY': '1', 'HYPERFRAMES_NO_UPDATE_CHECK': '1'})
return {'exit': p.returncode, 'stdout': p.stdout, 'stderr': p.stderr}
save(samples)
first = run()
grid = project / 'beats/song.wav.json'
initial = len(json.loads(grid.read_text())['beats']) if grid.exists() else None
save([0.0] * len(samples))
second = run()
print(json.dumps({'project': str(project), 'first': first, 'initialSavedBeats': initial, 'second': second,
'savedFileAfterReanalysis': grid.exists(), 'savedBeatsAfterReanalysis': len(json.loads(grid.read_text())['beats']) if grid.exists() else None}, indent=2))Steps to reproduce
The script generates a 12-second 120 BPM click track, runs beats, replaces the same WAV with 12 seconds of silence, then runs beats again and reads the persisted file.
Expected behavior
Successful reanalysis with zero beats leaves no saved grid for that source. An unsuccessful decode preserves the old grid, since it has not established a new result.
Actual behavior
The first run saves 24 beats at 120 BPM. The second run exits 0 and reports { "ok": true, "track": "song.wav", "file": null, "count": 0, "bpm": null }, but savedFileAfterReanalysis is true and savedBeatsAfterReanalysis is still 24.
Environment
Linux arm64, Node v26.13.0, Bun 1.4.4, FFmpeg/FFprobe 7.0.2-static. The command decodes locally; no browser, transcription model, account or private media is needed.
Additional context
The zero-beat return in packages/cli/src/commands/beats.ts happens before the persisted output is updated. packages/studio/src/hooks/useMusicBeatAnalysis.ts gives an existing nonempty saved file precedence. #7173 makes an empty analysis a successful result, but does not cover regeneration after replacing a previously analyzed track. Public issue, PR, review-comment and Discussion searches found no report of this regeneration case.
The CLI/file sequence was executed through the source CLI on the baseline and patch. The built CLI was also exercised with the patch. Studio's saved-grid precedence was traced in source; a live Studio UI was not exercised.
Activity