mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
* feat(media-use): core infrastructure — manifest, cache, adopt, probe Foundation for media-use — the media resolution layer for HyperFrames. - manifest.mjs: JSONL read/write/find for .media/manifest.jsonl - index-gen.mjs: regenerate agent-readable index.md from manifest - cache.mjs: content-addressed global cache at ~/.media/ (SHA-256, sentinel) - freeze.mjs: download URL or copy local file to .media/ - probe.mjs: extract duration/dimensions via ffprobe - adopt.mjs: scan assets/ directory, register existing files with metadata - 19 passing tests (manifest round-trip, cache, promote, index generation) * fix(media-use): oxfmt formatting + cap freeze download size Format adopt/cache/probe/manifest.test (CI oxfmt --check gate). Cap freezeUrl downloads at 256MB so a hostile/runaway URL can't fill the disk (addresses CodeQL #670: network data written to file). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
92 lines
2.4 KiB
JavaScript
92 lines
2.4 KiB
JavaScript
import { readFileSync, appendFileSync, mkdirSync, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
const MANIFEST_FILE = "manifest.jsonl";
|
|
const INDEX_FILE = "index.md";
|
|
|
|
const TYPE_DIRS = {
|
|
bgm: "audio/bgm",
|
|
sfx: "audio/sfx",
|
|
voice: "audio/voice",
|
|
image: "images",
|
|
icon: "images",
|
|
brand: "images",
|
|
video: "video",
|
|
};
|
|
|
|
export function mediaDir(projectDir) {
|
|
return join(projectDir, ".media");
|
|
}
|
|
|
|
export function manifestPath(projectDir) {
|
|
return join(mediaDir(projectDir), MANIFEST_FILE);
|
|
}
|
|
|
|
export function indexPath(projectDir) {
|
|
return join(mediaDir(projectDir), INDEX_FILE);
|
|
}
|
|
|
|
export function typeSubdir(type) {
|
|
const sub = TYPE_DIRS[type];
|
|
if (!sub) throw new Error(`unknown media type: ${type}`);
|
|
return sub;
|
|
}
|
|
|
|
export function typeDirPath(projectDir, type) {
|
|
return join(mediaDir(projectDir), typeSubdir(type));
|
|
}
|
|
|
|
export function readManifest(projectDir) {
|
|
const p = manifestPath(projectDir);
|
|
if (!existsSync(p)) return [];
|
|
const raw = readFileSync(p, "utf8");
|
|
const records = [];
|
|
for (const line of raw.split(/\r?\n/)) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
try {
|
|
records.push(JSON.parse(trimmed));
|
|
} catch {
|
|
// ponytail: skip malformed lines, don't crash
|
|
}
|
|
}
|
|
return records;
|
|
}
|
|
|
|
export function appendRecord(projectDir, record) {
|
|
const dir = mediaDir(projectDir);
|
|
mkdirSync(dir, { recursive: true });
|
|
const typeDir = typeDirPath(projectDir, record.type);
|
|
mkdirSync(typeDir, { recursive: true });
|
|
|
|
const p = manifestPath(projectDir);
|
|
const line = JSON.stringify(record) + "\n";
|
|
appendFileSync(p, line);
|
|
}
|
|
|
|
export function findByPrompt(projectDir, prompt, type) {
|
|
const records = readManifest(projectDir);
|
|
return (
|
|
records.find((r) => r.provenance?.prompt === prompt && (type == null || r.type === type)) ||
|
|
null
|
|
);
|
|
}
|
|
|
|
export function findByEntity(projectDir, entity) {
|
|
const lower = entity.toLowerCase();
|
|
const records = readManifest(projectDir);
|
|
return records.find((r) => r.entity && r.entity.toLowerCase() === lower) || null;
|
|
}
|
|
|
|
export function nextId(projectDir, type) {
|
|
const records = readManifest(projectDir);
|
|
const prefix = type;
|
|
let max = 0;
|
|
for (const r of records) {
|
|
if (r.type !== type) continue;
|
|
const m = r.id?.match(new RegExp(`^${prefix}_(\\d+)$`));
|
|
if (m) max = Math.max(max, parseInt(m[1], 10));
|
|
}
|
|
return `${prefix}_${String(max + 1).padStart(3, "0")}`;
|
|
}
|