mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
feat(cli): beats analyses an audio or video file given directly
This commit is contained in:
@@ -716,10 +716,11 @@ them.
|
||||
```bash
|
||||
npx hyperframes beats [dir]
|
||||
npx hyperframes beats [dir] --json # { ok, file, count, bpm }
|
||||
npx hyperframes beats <file> --json # { ok, file, count, bpm, beatTimes }
|
||||
```
|
||||
|
||||
`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`.
|
||||
Given an audio or video file instead of a folder, `beats` analyses that file
|
||||
with the same tracker and prints the result; it writes nothing.
|
||||
|
||||
`beats` finds the music track — an `<audio>` element with
|
||||
`data-timeline-role="music"`, or an id like `music`, `bgm`, or `soundtrack` —
|
||||
|
||||
@@ -90,11 +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("analyses an audio file given directly and writes nothing", async () => {
|
||||
const dir = project("file-arg", 120);
|
||||
const song = join(dir, "assets", "song.wav");
|
||||
const { exitCode, out } = await runBeats(song, true);
|
||||
expect(exitCode).toBe(0);
|
||||
const report = JSON.parse(out) as {
|
||||
ok: boolean;
|
||||
file: string;
|
||||
bpm: number;
|
||||
beatTimes: number[];
|
||||
};
|
||||
expect(report).toMatchObject({ ok: true, file: song });
|
||||
expect(report.beatTimes.length).toBeGreaterThan(18);
|
||||
expect(Math.abs(report.bpm - 120)).toBeLessThanOrEqual(1);
|
||||
expect(existsSync(join(dir, "beats"))).toBe(false);
|
||||
});
|
||||
|
||||
it("tells a composition file argument to pass its folder", async () => {
|
||||
|
||||
@@ -21,7 +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"],
|
||||
["Beats of a single audio or video file", "hyperframes beats music.mp3 --json"],
|
||||
];
|
||||
|
||||
function fail(message: string): never {
|
||||
@@ -96,24 +96,54 @@ function report(file: string, result: MusicBeatAnalysis, json: boolean): void {
|
||||
);
|
||||
}
|
||||
|
||||
async function analyzeFile(audioPath: string, json: boolean): Promise<void> {
|
||||
const result = await analyzeMusicFromSignal(await decode(audioPath));
|
||||
if (result.beatTimes.length === 0) fail(`No beats detected in ${audioPath}.`);
|
||||
if (json) {
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
file: audioPath,
|
||||
count: result.beatTimes.length,
|
||||
bpm: result.bpm,
|
||||
beatTimes: result.beatTimes,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
console.log(
|
||||
c.success(
|
||||
`✓ ${result.beatTimes.length} beats in ${audioPath} (bpm ${result.bpm ?? "?"}, ${result.bpmConfidence})`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: "beats",
|
||||
description: "Detect beats in the music track and write beats/<audio>.json",
|
||||
description:
|
||||
"Detect beats in the project music track (writes beats/<audio>.json) or in one audio file",
|
||||
},
|
||||
args: {
|
||||
dir: { type: "positional", description: "Project directory", required: false },
|
||||
dir: {
|
||||
type: "positional",
|
||||
description: "Project directory, or one audio or video file to analyse",
|
||||
required: false,
|
||||
},
|
||||
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()) {
|
||||
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`,
|
||||
);
|
||||
if (/\.html?$/i.test(args.dir)) {
|
||||
fail(
|
||||
`${args.dir} is a file; beats takes the project folder: hyperframes beats ${JSON.stringify(dirname(args.dir))}`,
|
||||
);
|
||||
}
|
||||
return await analyzeFile(resolve(args.dir), Boolean(args.json));
|
||||
}
|
||||
const project = resolveProject(args.dir);
|
||||
const target = resolveMusicTarget(project);
|
||||
|
||||
Reference in New Issue
Block a user