Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/packages/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,9 @@ npx hyperframes beats [dir]
npx hyperframes beats [dir] --json # { ok, file, count, bpm }
```

`beats` takes the project folder, not a media file. For the beats of one audio
or video file, run `npx hyperframes audio <file> --only beats --json`.

`beats` finds the music track — an `<audio>` element with
`data-timeline-role="music"`, or an id like `music`, `bgm`, or `soundtrack` —
decodes it with ffmpeg, runs the same beat tracker Studio uses, and writes
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/commands/beats.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ describe.skipIf(!canDecode)("hyperframes beats", () => {
expect(Math.abs(report.bpm - 120)).toBeLessThanOrEqual(1);
});

it("points a media file argument to hyperframes audio instead of reading it as a project", async () => {
const song = join(project("file-arg", 120), "assets", "song.wav");
const { exitCode, out } = await runBeats(song);
expect(exitCode).toBe(1);
expect(out).toContain(`hyperframes audio ${JSON.stringify(song)} --only beats --json`);
});

it("tells a composition file argument to pass its folder", async () => {
const dir = project("html-arg", 120);
const { exitCode, out } = await runBeats(join(dir, "index.html"));
expect(exitCode).toBe(1);
expect(out).toContain(`hyperframes beats ${JSON.stringify(dir)}`);
expect(out).not.toContain("hyperframes audio");
});

it("writes nothing for a track with no steady beat", async () => {
const dir = project("silent", null);
const { exitCode, out } = await runBeats(dir);
Expand Down
14 changes: 12 additions & 2 deletions packages/cli/src/commands/beats.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { failCommand } from "../utils/commandResult.js";
import { defineCommand } from "citty";
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { existsSync, readFileSync, mkdirSync, statSync, writeFileSync } from "node:fs";
import { join, dirname, resolve } from "node:path";
import {
analyzeMusicFromSignal,
audioRelPathForSrc,
Expand All @@ -21,6 +21,7 @@ import { c } from "../ui/colors.js";
export const examples: Example[] = [
["Generate the beat file for the current project", "hyperframes beats"],
["Generate for a specific directory", "hyperframes beats ./my-video"],
["Beats of a single audio file", "hyperframes audio music.mp3 --only beats --json"],
];

function fail(message: string): never {
Expand Down Expand Up @@ -101,6 +102,15 @@ export default defineCommand({
json: { type: "boolean", description: "Output result as JSON", default: false },
},
async run({ args }) {
if (args.dir && existsSync(resolve(args.dir)) && statSync(resolve(args.dir)).isFile()) {
Comment thread
heygengenesis[bot] marked this conversation as resolved.
const file = JSON.stringify(args.dir);
fail(
/\.html?$/i.test(args.dir)
? `${args.dir} is a file; beats takes the project folder: hyperframes beats ${JSON.stringify(dirname(args.dir))}`
: `${args.dir} is a file; beats takes a project folder. ` +
`For one audio or video file run: hyperframes audio ${file} --only beats --json`,
);
}
const project = resolveProject(args.dir);
const { rel, audioPath } = resolveMusicTarget(project);
if (!args.json) console.log(c.dim(`Analyzing ${rel}…`));
Expand Down
2 changes: 1 addition & 1 deletion skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"files": 77
},
"hyperframes-cli": {
"hash": "49fa62fa12913672",
"hash": "433959bc422463f4",
"files": 13
},
"hyperframes-registry": {
Expand Down
2 changes: 2 additions & 0 deletions skills/hyperframes-cli/references/beats.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ npx hyperframes beats ./my-video
npx hyperframes beats ./my-video --json
```

`beats` takes the project folder, never a media file. For the beats of one audio or video file, run `npx hyperframes audio <file> --only beats --json`.
Comment thread
heygengenesis[bot] marked this conversation as resolved.

The project must contain a local music `<audio>` source. Mark it with `data-timeline-role="music"`; an id containing `music`, `bgm`, or `soundtrack` is also recognized. The command decodes that file with ffmpeg, tracks its beats, and writes `beats/<audio-relative-path>.json`.

If the track has no steady beat, the command fails and writes nothing. If ffmpeg is missing, run `npx hyperframes doctor`.
Expand Down
Loading