fix(cli): beats --json answers with no track when the composition has no music (#6690)

* fix(cli): beats --json answers with no track when the composition has no music

* fix(cli): drop a comment the name already says
This commit is contained in:
Miguel Ángel
2026-10-28 18:57:53 +00:00
committed by GitHub
parent aa4a71fe41
commit ec10aae54f
2 changed files with 35 additions and 12 deletions
+19 -2
View File
@@ -44,7 +44,7 @@ function project(name: string, bpm: number | null, seconds = 12): string {
return dir;
}
async function runBeats(dir: string): Promise<{ exitCode: number; out: string }> {
async function runBeats(dir: string, json = true): Promise<{ exitCode: number; out: string }> {
const command = (await import("./beats.js")).default as unknown as {
run: (context: { args: Record<string, unknown> }) => Promise<void>;
};
@@ -54,7 +54,7 @@ async function runBeats(dir: string): Promise<{ exitCode: number; out: string }>
const error = vi.spyOn(console, "error").mockImplementation(capture);
let exitCode = 0;
try {
await command.run({ args: { dir, json: true } });
await command.run({ args: { dir, json } });
} catch (thrown) {
exitCode = (thrown as { result?: { exitCode?: number } }).result?.exitCode ?? -1;
} finally {
@@ -105,6 +105,23 @@ describe.skipIf(!canDecode)("hyperframes beats", () => {
expect(out).not.toContain("hyperframes audio");
});
it("answers --json with no track, not a failure, when the composition has no music", async () => {
const dir = project("no-track", 120);
writeFileSync(join(dir, "index.html"), `<div data-composition-id="main"></div>`);
const { exitCode, out } = await runBeats(dir);
expect(exitCode).toBe(0);
expect(JSON.parse(out)).toEqual({ ok: true, track: null, file: null, count: 0, bpm: null });
expect(existsSync(join(dir, "beats"))).toBe(false);
});
it("still tells a person with no music track how to mark one", async () => {
const dir = project("no-track-person", 120);
writeFileSync(join(dir, "index.html"), `<div data-composition-id="main"></div>`);
const { exitCode, out } = await runBeats(dir, false);
expect(exitCode).toBe(1);
expect(out).toMatch(/No music track found/);
});
it("writes nothing for a track with no steady beat", async () => {
const dir = project("silent", null);
const { exitCode, out } = await runBeats(dir);
+16 -10
View File
@@ -50,16 +50,15 @@ function findMusicSrc(html: string): string | null {
return findMusicAudioSrc(document.toString());
}
/** Locate the music track + its on-disk audio, or fail with a clear message. */
function resolveMusicTarget(project: ProjectDir): { rel: string; audioPath: string } {
const NO_MUSIC_TRACK =
'No music track found. Add data-timeline-role="music" to the <audio> element, or to a ' +
'<video data-has-audio="true"> whose own sound is the music ' +
"(or give it an id like music/bgm/soundtrack).";
/** Locate the music track + its on-disk audio: null when the composition has none, or fail with a clear message. */
function resolveMusicTarget(project: ProjectDir): { rel: string; audioPath: string } | null {
const src = findMusicSrc(readFileSync(project.indexPath, "utf-8"));
if (!src) {
fail(
'No music track found. Add data-timeline-role="music" to the <audio> element, or to a ' +
'<video data-has-audio="true"> whose own sound is the music ' +
"(or give it an id like music/bgm/soundtrack).",
);
}
if (!src) return null;
const rel = audioRelPathForSrc(src); // same derivation the Studio uses
if (!rel) fail(`Cannot derive a beat-file path for music src: ${src}`);
const audioPath = resolveWithinProject(project.dir, rel);
@@ -68,6 +67,11 @@ function resolveMusicTarget(project: ProjectDir): { rel: string; audioPath: stri
return { rel, audioPath };
}
function answerNoTrack(json: boolean): void {
if (!json) fail(NO_MUSIC_TRACK);
console.log(JSON.stringify({ ok: true, track: null, file: null, count: 0, bpm: null }, null, 2));
}
async function decode(audioPath: string): Promise<AudioSignal> {
try {
const decoded = await decodeAudio(audioPath);
@@ -112,7 +116,9 @@ export default defineCommand({
);
}
const project = resolveProject(args.dir);
const { rel, audioPath } = resolveMusicTarget(project);
const target = resolveMusicTarget(project);
if (!target) return answerNoTrack(Boolean(args.json));
const { rel, audioPath } = target;
if (!args.json) console.log(c.dim(`Analyzing ${rel}…`));
const result = await analyzeMusicFromSignal(await decode(audioPath));