mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fix(studio): the timeline takes m4v video and flac audio (#4649)
* fix(studio): the timeline takes m4v video and flac audio * fix(studio-server): check and label flac uploads as audio
This commit is contained in:
@@ -111,6 +111,15 @@ describe("probeMediaMetadata", () => {
|
||||
expect(audioFile).not.toHaveProperty("hasAudio");
|
||||
});
|
||||
|
||||
it("reads a flac file as audio", async () => {
|
||||
const flac = await probeMediaMetadata("/tmp/music.flac", () => ({
|
||||
status: 0,
|
||||
stdout: "{}",
|
||||
stderr: "",
|
||||
}));
|
||||
expect(flac.kind).toBe("audio");
|
||||
});
|
||||
|
||||
it("ignores attached cover art and reads the real video stream", async () => {
|
||||
const metadata = await probeMediaMetadata("/tmp/clip.mp4", () => ({
|
||||
status: 0,
|
||||
|
||||
@@ -96,7 +96,7 @@ const VIDEO_EXT = new Set([
|
||||
".ts",
|
||||
]);
|
||||
const IMAGE_EXT = new Set([".jpg", ".jpeg", ".png", ".webp", ".avif"]);
|
||||
const AUDIO_EXT = new Set([".mp3", ".wav", ".ogg", ".m4a", ".aac"]);
|
||||
const AUDIO_EXT = new Set([".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac"]);
|
||||
|
||||
function lower(value: string | undefined): string {
|
||||
return value?.toLowerCase() ?? "";
|
||||
|
||||
@@ -38,6 +38,19 @@ describe("validateUploadedMedia", () => {
|
||||
).toEqual({ ok: false, reason: "no supported video stream found" });
|
||||
});
|
||||
|
||||
it.each(["/tmp/test.flac", "/tmp/test.m4v"])(
|
||||
"probes %s instead of passing it through",
|
||||
(path) => {
|
||||
expect(
|
||||
validateUploadedMedia(path, () => ({
|
||||
status: 0,
|
||||
stdout: JSON.stringify({ streams: [] }),
|
||||
stderr: "",
|
||||
})),
|
||||
).toMatchObject({ ok: false });
|
||||
},
|
||||
);
|
||||
|
||||
it("accepts audio files with an audio stream", () => {
|
||||
expect(
|
||||
validateUploadedMedia("/tmp/test.wav", () => ({
|
||||
|
||||
@@ -5,7 +5,7 @@ import { basename, join } from "node:path";
|
||||
import { FFPROBE_PATH_ENV, findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
||||
|
||||
const VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi|m4v|mxf|mts|m2ts|ts)$/i;
|
||||
const AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
||||
const AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac|flac)$/i;
|
||||
|
||||
type FfprobeRunner = (
|
||||
command: string,
|
||||
|
||||
@@ -16,6 +16,7 @@ import { buildProjectApiPath } from "../../utils/projectRouting";
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
import type { FeedbackContext } from "./feedbackTrigger";
|
||||
import { isMediaFile } from "../../utils/mediaTypes";
|
||||
|
||||
/** Written by `hyperframes init`; absent in a hand-made or copied project. */
|
||||
const CONFIG_FILE = "hyperframes.json";
|
||||
@@ -23,12 +24,10 @@ const CONFIG_FILE = "hyperframes.json";
|
||||
/** Matches the CLI's own slug gate, so a hand-edited value cannot leak text. */
|
||||
const SKILL_SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
||||
|
||||
const MEDIA_EXTENSIONS = /\.(mp4|mov|webm|m4v|mp3|wav|m4a|aac|ogg|png|jpe?g|gif|webp|svg|avif)$/i;
|
||||
|
||||
let snapshot: FeedbackContext = {};
|
||||
|
||||
function countMedia(files: string[]): number {
|
||||
return files.filter((f) => MEDIA_EXTENSIONS.test(f)).length;
|
||||
return files.filter(isMediaFile).length;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { AUDIO_EXT, IMAGE_EXT, isMediaFile, MEDIA_EXT, VIDEO_EXT } from "./mediaTypes";
|
||||
import { getTimelineAssetKind } from "./timelineAssetDrop";
|
||||
|
||||
describe("media types", () => {
|
||||
it.each([
|
||||
["clip.m4v", "video"],
|
||||
["assets/Take 2.M4V", "video"],
|
||||
["song.flac", "audio"],
|
||||
["voice.FLAC", "audio"],
|
||||
["clip.mp4", "video"],
|
||||
["music.mp3", "audio"],
|
||||
["still.png", "image"],
|
||||
] as const)("the timeline takes %s as %s", (path, kind) => {
|
||||
expect(getTimelineAssetKind(path)).toBe(kind);
|
||||
expect(isMediaFile(path)).toBe(true);
|
||||
});
|
||||
|
||||
it("still refuses files that are not media", () => {
|
||||
for (const path of ["notes.pdf", "font.woff2", "grade.cube", "clip.m4v.txt", "flac"]) {
|
||||
expect(getTimelineAssetKind(path)).toBeNull();
|
||||
expect(isMediaFile(path)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("counts a file as media exactly when it is an image, video or audio file", () => {
|
||||
const paths = ["a.mp4", "a.m4v", "a.flac", "a.png", "a.ico", "a.woff", "a.pdf", "a.cube"];
|
||||
for (const path of paths) {
|
||||
const kinds = [IMAGE_EXT, VIDEO_EXT, AUDIO_EXT].filter((re) => re.test(path)).length;
|
||||
expect(MEDIA_EXT.test(path)).toBe(kinds > 0);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,17 @@
|
||||
export const IMAGE_EXT = /\.(jpg|jpeg|png|gif|webp|avif|svg|ico)$/i;
|
||||
export const VIDEO_EXT = /\.(mp4|webm|mov)$/i;
|
||||
export const AUDIO_EXT = /\.(mp3|wav|ogg|m4a|aac)$/i;
|
||||
const IMAGE_EXTS = ["jpg", "jpeg", "png", "gif", "webp", "avif", "svg", "ico"];
|
||||
const VIDEO_EXTS = ["mp4", "webm", "mov", "m4v"];
|
||||
const AUDIO_EXTS = ["mp3", "wav", "ogg", "m4a", "aac", "flac"];
|
||||
|
||||
function extensionPattern(extensions: readonly string[]): RegExp {
|
||||
return new RegExp(`\\.(${extensions.join("|")})$`, "i");
|
||||
}
|
||||
|
||||
export const IMAGE_EXT = extensionPattern(IMAGE_EXTS);
|
||||
export const VIDEO_EXT = extensionPattern(VIDEO_EXTS);
|
||||
export const AUDIO_EXT = extensionPattern(AUDIO_EXTS);
|
||||
export const FONT_EXT = /\.(woff|woff2|ttf|ttc|otf|eot)$/i;
|
||||
export const LUT_EXT = /\.cube$/i;
|
||||
export const MEDIA_EXT =
|
||||
/\.(mp4|webm|mov|mp3|wav|ogg|m4a|aac|jpg|jpeg|png|gif|webp|avif|svg|ico)$/i;
|
||||
export const MEDIA_EXT = extensionPattern([...VIDEO_EXTS, ...AUDIO_EXTS, ...IMAGE_EXTS]);
|
||||
|
||||
export function isMediaFile(path: string): boolean {
|
||||
return MEDIA_EXT.test(path);
|
||||
|
||||
Reference in New Issue
Block a user