mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-03 20:37:30 +00:00
fix(cli): cutout retries dedupe on request_id, uploads carry their real type; style-remix is public
The background-removal route dedupes on the body's request_id and ignores the Idempotency-Key header, so a retry after a lost answer started a second job: runJob now also sends the key in the body for such a route. Uploads typed every non-MP4 file as audio, so WebM and MOV inputs were refused: they now take their type from the extension. Fails without the fix: the new cutout test in generate.test.ts sees audio/mpeg for WebM and MOV. style-remix drops metadata.internal and is listed with the other published skills. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0e30f4c78c
commit
83d176e1c3
@@ -8,7 +8,7 @@ This repo ships AI agent skills via [vercel-labs/skills](https://github.com/verc
|
||||
|
||||
```bash
|
||||
npx hyperframes skills update # default: installs/refreshes the core set plus anything already installed
|
||||
npx hyperframes skills # all 8 published skills at once
|
||||
npx hyperframes skills # all 9 published skills at once
|
||||
npx skills add heygen-com/hyperframes # interactive picker (terminal only; repo-internal skills are excluded by default)
|
||||
```
|
||||
|
||||
@@ -22,6 +22,7 @@ Make any video by asking for it; the `/hyperframes` skill carries the contract a
|
||||
- `/slideshow` — a presentation, pitch deck or interactive deck. Output is a navigable deck, not a rendered MP4.
|
||||
- `/remotion-to-hyperframes` — bring existing video code or footage into HyperFrames (Remotion first; also Motion Canvas, Lottie, Three.js, canvas, or renders from other tools) and verify it frame by frame against the original.
|
||||
- `/reverse-any-video` — rebuild any video file as an editable HyperFrames project (motion graphics, talking heads, live action or AI-generated footage) and check the render against the original frame by frame. A HyperFrames Pro feature.
|
||||
- `/style-remix` — make a new episode on your topic in one of five proven styles (Collage Explainer, Finance Explainer, Animated Science Explainer, 3D Brand Explainer, 3D True Crime). A HyperFrames Pro feature.
|
||||
|
||||
## Issue and PR triage
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@ Open-source video rendering framework: write HTML, render video.
|
||||
|
||||
## Skills
|
||||
|
||||
This repo ships 8 AI agent skills via [vercel-labs/skills](https://github.com/vercel-labs/skills). Install them before writing compositions — they encode framework-specific patterns that generic docs don't cover. **Default to the core set** (`/hyperframes`, `/hyperframes-cli`, `/hyperframes-registry`, `/media-use`); `/slideshow`, `/remotion-to-hyperframes`, `/reverse-any-video` and `/figma` install on demand. Install all 8 only when the user explicitly asks for the full set.
|
||||
This repo ships 9 AI agent skills via [vercel-labs/skills](https://github.com/vercel-labs/skills). Install them before writing compositions — they encode framework-specific patterns that generic docs don't cover. **Default to the core set** (`/hyperframes`, `/hyperframes-cli`, `/hyperframes-registry`, `/media-use`); `/slideshow`, `/remotion-to-hyperframes`, `/reverse-any-video` and `/figma` install on demand. Install all 8 only when the user explicitly asks for the full set.
|
||||
|
||||
```bash
|
||||
npx hyperframes skills update # default: installs/refreshes the core set plus anything already installed
|
||||
npx hyperframes skills # all 8 published skills at once — only on explicit request
|
||||
npx hyperframes skills # all 9 published skills at once — only on explicit request
|
||||
npx skills add heygen-com/hyperframes # interactive picker; internal skills require INSTALL_INTERNAL_SKILLS=1 for discovery
|
||||
npx skills add heygen-com/hyperframes --all # excludes internal skills unless INSTALL_INTERNAL_SKILLS=1
|
||||
npx skills add heygen-com/hyperframes --skill <name> # a named match installs regardless of metadata.internal (bare name, no leading slash)
|
||||
@@ -28,6 +28,7 @@ Make any video by asking for it; the `/hyperframes` skill carries the render con
|
||||
- `/slideshow` — a presentation, pitch deck or interactive deck with discrete slides, fragment reveals, branching, hotspot navigation and presenter mode with speaker notes; also converts an existing page into a deck. Output is a navigable deck, not a rendered MP4.
|
||||
- `/remotion-to-hyperframes` — bring existing video code or footage into HyperFrames (Remotion first; also Motion Canvas, Lottie, Three.js, canvas, or renders from other tools) and verify it frame by frame against the original.
|
||||
- `/reverse-any-video` — rebuild any video file as an editable HyperFrames project (motion graphics, talking heads, live action or AI-generated footage) and check the render against the original frame by frame. A HyperFrames Pro feature.
|
||||
- `/style-remix` — make a new episode on your topic in one of five proven styles (Collage Explainer, Finance Explainer, Animated Science Explainer, 3D Brand Explainer, 3D True Crime). A HyperFrames Pro feature.
|
||||
|
||||
## Skill catalog maintenance
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ For standalone skills (including OpenCode), use:
|
||||
npx skills add heygen-com/hyperframes
|
||||
```
|
||||
|
||||
> The picker opens with nothing pre-selected — the **Core Skills** group is all you need; `/slideshow`, `/remotion-to-hyperframes`, `/reverse-any-video` and `/figma` install on demand. Agents and non-interactive runs should use `npx hyperframes skills update` instead — it installs exactly the core set, whereas `skills add --all` installs all 8 published skills. The six repo-internal skills under `.claude/skills` / `.agents/skills` are excluded by default. For the full published set use `npx hyperframes skills`.
|
||||
> The picker opens with nothing pre-selected — the **Core Skills** group is all you need; `/slideshow`, `/remotion-to-hyperframes`, `/reverse-any-video`, `/style-remix` and `/figma` install on demand. Agents and non-interactive runs should use `npx hyperframes skills update` instead — it installs exactly the core set, whereas `skills add --all` installs all 9 published skills. The six repo-internal skills under `.claude/skills` / `.agents/skills` are excluded by default. For the full published set use `npx hyperframes skills`.
|
||||
>
|
||||
> `skills add` resolves the skills.sh registry blob, which can lag `main` by hours. `npx hyperframes skills update` installs from the current `main`, so reach for it when you need the newest copy of a skill.
|
||||
|
||||
@@ -81,7 +81,7 @@ The skills teach agents the HyperFrames production loop: plan the video, write v
|
||||
|
||||
## Skills
|
||||
|
||||
HyperFrames ships 8 skills. Make any video by asking for it; the `/hyperframes` skill carries the contract and the references (film, captions, overlays, motion, look). Read it first.
|
||||
HyperFrames ships 9 skills. Make any video by asking for it; the `/hyperframes` skill carries the contract and the references (film, captions, overlays, motion, look). Read it first.
|
||||
|
||||
For standalone installation, default to the **core set** (`/hyperframes`, `/hyperframes-cli`, `/hyperframes-registry`, `/media-use`). `npx hyperframes skills update` installs exactly that from anywhere; the interactive picker (`npx skills add heygen-com/hyperframes`) lists it as the "Core Skills" group, nothing pre-selected. The picker is interactive-only — a non-interactive or agent run without `--skill` installs all 8. Use `npx skills add heygen-com/hyperframes --all` to install the complete published set deliberately (skips the picker), or `npx skills add heygen-com/hyperframes --skill <name>` for just one (bare name, no leading `/`).
|
||||
|
||||
@@ -120,6 +120,7 @@ This writes `dist/hyperframes-plugin.zip` with a `hyperframes/` root folder and
|
||||
| `/slideshow` | A presentation, pitch deck or interactive deck — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode with speaker notes. Output is a navigable deck, not a rendered MP4. |
|
||||
| `/remotion-to-hyperframes` | Bringing existing video code or footage into HyperFrames (Remotion first; also Motion Canvas, Lottie, Three.js, canvas, other tools' renders), verified frame by frame. |
|
||||
| `/reverse-any-video` | Rebuilding any video file as an editable HyperFrames project (motion graphics, talking heads, live action, AI-generated footage), checked frame by frame against the original. HyperFrames Pro. |
|
||||
| `/style-remix` | Making a new episode on your topic in one of five proven styles: Collage Explainer, Finance Explainer, Animated Science Explainer, 3D Brand Explainer, 3D True Crime. HyperFrames Pro. |
|
||||
|
||||
For visual design handoff workflows, see the [Claude Design guide](https://hyperframes.heygen.com/guides/claude-design) and [Open Design guide](https://hyperframes.heygen.com/guides/open-design).
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ Make any video by asking for it; the `/hyperframes` skill carries the contract a
|
||||
| `/slideshow` | A presentation, pitch deck or interactive deck. Output is a navigable deck, not a rendered MP4. |
|
||||
| `/remotion-to-hyperframes` | Bringing existing video code or footage into HyperFrames (Remotion first; also Motion Canvas, Lottie, Three.js, canvas, other tools' renders), verified frame by frame. |
|
||||
| `/reverse-any-video` | Rebuilding any video file as an editable HyperFrames project (motion graphics, talking heads, live action, AI-generated footage), checked frame by frame against the original. HyperFrames Pro. |
|
||||
| `/style-remix` | Making a new episode on your topic in one of five proven styles: Collage Explainer, Finance Explainer, Animated Science Explainer, 3D Brand Explainer, 3D True Crime. HyperFrames Pro. |
|
||||
|
||||
You can name a skill directly, but you do not need to memorize the names to start.
|
||||
|
||||
|
||||
@@ -531,7 +531,6 @@ interface FilePlan {
|
||||
segments?: PlayedSegment[];
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
async function planFile(
|
||||
ctx: OffloadContext,
|
||||
path: string,
|
||||
@@ -893,7 +892,7 @@ function rewriteCssEntry(absDir: string, entryPath: string, css: string, urlFor:
|
||||
|
||||
const TEMPLATE_SCENE_RE = /(<template\b[^>]*>)([\s\S]*?)(<\/template>)/gi;
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
/** An entry's HTML with each <template> scene rewritten too: the runtime stamps those scenes into the page. */
|
||||
function rewriteHtmlEntry(
|
||||
absDir: string,
|
||||
entryPath: string,
|
||||
@@ -908,7 +907,20 @@ function rewriteHtmlEntry(
|
||||
return next === null ? full : open + next + close;
|
||||
},
|
||||
);
|
||||
return (
|
||||
rewriteHtmlDocument(absDir, entryPath, html, urlFor, fileMap) ?? (html === source ? null : html)
|
||||
);
|
||||
}
|
||||
|
||||
function rewriteHtmlDocument(
|
||||
absDir: string,
|
||||
entryPath: string,
|
||||
html: string,
|
||||
urlFor: UrlFor,
|
||||
fileMap: Map<string, StagedFile>,
|
||||
): string | null {
|
||||
const referrerAbsDir = resolve(absDir, dirname(entryPath));
|
||||
// Sub-composition srcs resolve the way the renderer inlines them.
|
||||
const resolveTarget: HtmlAttributeResolver = (raw) =>
|
||||
localTarget(raw, (clean) => rewriteAssetPath(entryPath, clean, (p) => fileMap.has(p)), urlFor);
|
||||
const { document } = parseHTML(html);
|
||||
@@ -925,8 +937,7 @@ function rewriteHtmlEntry(
|
||||
script.textContent = next;
|
||||
scripts = true;
|
||||
}
|
||||
if (attrs || srcsets || styles || scripts) return document.toString();
|
||||
return html === source ? null : html;
|
||||
return attrs || srcsets || styles || scripts ? document.toString() : null;
|
||||
}
|
||||
|
||||
// A plain string literal: no escapes, interpolation or line breaks.
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
checkRequest,
|
||||
generate,
|
||||
parseMask,
|
||||
pickVoice,
|
||||
voiceFor,
|
||||
type GenerateRequest,
|
||||
type GenerateType,
|
||||
} from "../../heygen/generate.js";
|
||||
@@ -174,8 +174,7 @@ export default defineCommand({
|
||||
bpm: num(args.bpm, "--bpm"),
|
||||
instrumental: !args.vocals,
|
||||
};
|
||||
const speaks = type === "voice" || (type === "avatar" && !request.audio);
|
||||
const voice = speaks ? await pickVoice(request) : undefined;
|
||||
const voice = await voiceFor(request);
|
||||
if (voice) request.voice = voice.voice_id;
|
||||
const price = priceOf(request);
|
||||
const credits = creditsOf(price);
|
||||
|
||||
@@ -13,10 +13,12 @@ vi.mock("../cloud/auth.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../cloud/index.js", () => ({ createCloudClient: async () => ({}) }));
|
||||
const uploads = vi.hoisted(() => [] as { filename: string; contentType: string }[]);
|
||||
vi.mock("../cloud/upload.js", () => ({
|
||||
uploadAssetViaDirectUpload: async ({ filename }: { filename: string }) => ({
|
||||
asset_id: `asset-${filename}`,
|
||||
}),
|
||||
uploadAssetViaDirectUpload: async (u: { filename: string; contentType: string }) => {
|
||||
uploads.push({ filename: u.filename, contentType: u.contentType });
|
||||
return { asset_id: `asset-${u.filename}` };
|
||||
},
|
||||
}));
|
||||
|
||||
vi.stubEnv("HEYGEN_API_KEY", "k");
|
||||
@@ -694,6 +696,50 @@ describe("generate", () => {
|
||||
expect(() => checkRequest({ type: "lipsync", audio, out })).toThrow("--video");
|
||||
});
|
||||
|
||||
it.skipIf(!ffmpeg)("cuts out a WebM or MOV shot as video, with its key in the body", async () => {
|
||||
const posts: Record<string, unknown>[] = [];
|
||||
vi.stubGlobal("fetch", async (url: string, init?: RequestInit) => {
|
||||
if (url === "https://files.test/fg.webm") return new Response(new Uint8Array([1, 2, 3]));
|
||||
if (init?.method === "POST") posts.push(JSON.parse(String(init.body)));
|
||||
return json({
|
||||
data: {
|
||||
id: "bg1",
|
||||
status: "completed",
|
||||
layers: { foreground: "https://files.test/fg.webm" },
|
||||
},
|
||||
});
|
||||
});
|
||||
uploads.length = 0;
|
||||
for (const ext of ["webm", "mov"]) {
|
||||
const video = join(project, `cut-shot.${ext}`);
|
||||
execFileSync(ffmpeg!, [
|
||||
"-v",
|
||||
"error",
|
||||
"-y",
|
||||
"-f",
|
||||
"lavfi",
|
||||
"-i",
|
||||
"color=c=red:s=64x36:d=1",
|
||||
video,
|
||||
]);
|
||||
const out = join(project, `cut-${ext}.webm`);
|
||||
await generate({ type: "cutout", video, out });
|
||||
await generate({ type: "cutout", video, out });
|
||||
}
|
||||
expect(uploads.map((u) => u.contentType)).toEqual([
|
||||
"video/webm",
|
||||
"video/webm",
|
||||
"video/quicktime",
|
||||
"video/quicktime",
|
||||
]);
|
||||
// the route dedupes on the body's request_id alone, so a re-run must send the same one
|
||||
const ids = posts.map((p) => p.request_id);
|
||||
expect(ids[0]).toMatch(/\S/);
|
||||
expect(ids[1]).toBe(ids[0]);
|
||||
expect(ids[2]).toBe(ids[3]);
|
||||
expect(ids[2]).not.toBe(ids[0]);
|
||||
});
|
||||
|
||||
it.skipIf(!ffmpeg)("speaks a finished voice as it is, from a photo or a look", async () => {
|
||||
const photo = join(project, "avatar-photo.png");
|
||||
execFileSync(ffmpeg!, [
|
||||
|
||||
@@ -156,6 +156,8 @@ interface Route {
|
||||
urls: string[];
|
||||
ledger: "image" | "video" | "voice" | "bgm";
|
||||
ext: string[];
|
||||
/** The body field the route dedupes on, when it ignores the Idempotency-Key header. */
|
||||
bodyKey?: string;
|
||||
}
|
||||
|
||||
type Media = (path: string, kind: "image" | "audio" | "video") => Record<string, unknown>;
|
||||
@@ -247,6 +249,7 @@ const ROUTES: Record<GenerateType, Route> = {
|
||||
urls: ["layers.foreground"],
|
||||
ledger: "video",
|
||||
ext: [".webm", ".mov"],
|
||||
bodyKey: "request_id",
|
||||
},
|
||||
avatar: {
|
||||
path: "/v3/videos",
|
||||
@@ -377,13 +380,14 @@ async function uploaded(paths: string[]): Promise<Map<string, string>> {
|
||||
if (!paths.length) return ids;
|
||||
const { createCloudClient } = await import("../cloud/index.js");
|
||||
const { uploadAssetViaDirectUpload } = await import("../cloud/upload.js");
|
||||
const { getMimeType } = await import("@hyperframes/studio-server");
|
||||
const client = await createCloudClient();
|
||||
for (const path of paths) {
|
||||
const asset = await uploadAssetViaDirectUpload({
|
||||
client,
|
||||
bytes: new Uint8Array(readFileSync(path)),
|
||||
filename: basename(path),
|
||||
contentType: extname(path).toLowerCase() === ".mp4" ? "video/mp4" : "audio/mpeg",
|
||||
contentType: getMimeType(path),
|
||||
idempotencyKey: randomUUID(),
|
||||
});
|
||||
ids.set(path, asset.asset_id);
|
||||
@@ -469,7 +473,7 @@ function fillMasks(path: string, masks: Mask[], work: string): string {
|
||||
* HeyGen answers a 16:9 request at 1344x768 (1.75:1). The result is cropped, centred, to the requested ratio
|
||||
* (1344x756) rather than stretched; returns a note on what was done, or nothing when it already fits.
|
||||
*/
|
||||
export function fitAspect(out: string, aspect: string): string | undefined {
|
||||
function fitAspect(out: string, aspect: string): string | undefined {
|
||||
const [aw, ah] = aspect.split(":").map(Number);
|
||||
if (!aw || !ah) return;
|
||||
const { width, height } = probeVideo(out);
|
||||
@@ -547,6 +551,11 @@ function checkGuide(r: Pick<GenerateRequest, "motion" | "allowCuts">): void {
|
||||
* The route's body runs with a stand-in for the media, so its required flags and conflicts are checked as sent. */
|
||||
const VOICE_ID = /^[0-9a-f]{32}$/i;
|
||||
|
||||
/** The voice a request speaks in, picked only when it reads a script: an avatar given --audio keeps that voice. */
|
||||
export async function voiceFor(r: GenerateRequest): Promise<Voice | undefined> {
|
||||
return r.type === "voice" || (r.type === "avatar" && !r.audio) ? pickVoice(r) : undefined;
|
||||
}
|
||||
|
||||
export async function pickVoice(r: GenerateRequest): Promise<Voice> {
|
||||
if (r.voice && VOICE_ID.test(r.voice)) return { voice_id: r.voice, name: r.voice };
|
||||
if (!r.voice) {
|
||||
@@ -632,6 +641,7 @@ export async function generate(
|
||||
onWait: (j) => onWait?.(j.status),
|
||||
timeoutMs: 30 * 60_000,
|
||||
credits,
|
||||
bodyKey: route.bodyKey,
|
||||
});
|
||||
const url = urlOf(job, route.urls);
|
||||
if (!url)
|
||||
|
||||
@@ -152,6 +152,38 @@ describe("runJob", () => {
|
||||
expect(keys).toEqual(["hf-broke", "hf-broke-1"]);
|
||||
});
|
||||
|
||||
it("puts the key in the body for a route that dedupes there, and moves it only past a failed job", async () => {
|
||||
const jobs = new Map<string, string>();
|
||||
const sent: string[] = [];
|
||||
let status = "completed";
|
||||
let lose = false;
|
||||
vi.stubGlobal("fetch", async (_url: string, init: RequestInit) => {
|
||||
if (init.method !== "POST") return reply(200, { data: { id: "j", status } });
|
||||
// like /v3/background-removals: the header is ignored, the body's request_id picks the job
|
||||
const id = String(JSON.parse(String(init.body)).request_id);
|
||||
sent.push(id);
|
||||
if (!jobs.has(id)) jobs.set(id, `job-${jobs.size}`);
|
||||
if (lose) return reply(408, { message: "timeout" });
|
||||
return reply(200, { data: { id: jobs.get(id), status } });
|
||||
});
|
||||
const go = () =>
|
||||
runJob("/v3/background-removals", { layers: ["foreground"] }, "hf-cut", (id) => id, {
|
||||
bodyKey: "request_id",
|
||||
intervalMs: 1,
|
||||
});
|
||||
lose = true;
|
||||
await expect(go()).rejects.toThrow();
|
||||
lose = false;
|
||||
await go();
|
||||
expect(jobs.size).toBe(1);
|
||||
status = "failed";
|
||||
await expect(go()).rejects.toThrow();
|
||||
status = "completed";
|
||||
await go();
|
||||
expect(sent).toEqual(["hf-cut", "hf-cut", "hf-cut", "hf-cut-1"]);
|
||||
expect(jobs.size).toBe(2);
|
||||
});
|
||||
|
||||
it.each([401, 403, 408, 409, 425, 429])(
|
||||
"keeps the key after a %i, which says nothing about the job",
|
||||
async (status) => {
|
||||
|
||||
@@ -212,6 +212,8 @@ export interface RunOptions {
|
||||
log?: JobLog;
|
||||
/** What the call is priced at in credits, named in an out-of-credits refusal that does not say. */
|
||||
credits?: number;
|
||||
/** A body field that also carries the sent key, for a route that dedupes on the body and ignores the header. */
|
||||
bodyKey?: string;
|
||||
}
|
||||
|
||||
/** Starts a job (or picks up the one this key already started) and polls `pollRoute(id)` until it ends. */
|
||||
@@ -227,7 +229,10 @@ export async function runJob(
|
||||
const sent = liveKey(key, scope);
|
||||
let job: Job;
|
||||
try {
|
||||
job = await call("POST", route, body, sent, opts.credits, auth);
|
||||
const payload = opts.bodyKey
|
||||
? { ...(body as Record<string, unknown>), [opts.bodyKey]: sent }
|
||||
: body;
|
||||
job = await call("POST", route, payload, sent, opts.credits, auth);
|
||||
} catch (err) {
|
||||
if (err instanceof Refused && !KEEPS_KEY.has(err.status)) markFailed(sent, scope);
|
||||
throw err;
|
||||
|
||||
@@ -18,6 +18,7 @@ MP4.
|
||||
- `/slideshow` — a presentation or pitch deck. Output is a navigable deck, not a rendered MP4.
|
||||
- `/remotion-to-hyperframes` — bring existing video code or footage into HyperFrames (Remotion first; also Motion Canvas, Lottie, Three.js, canvas, or renders from other tools) and verify it frame by frame against the original.
|
||||
- `/reverse-any-video` — rebuild any video file as an editable HyperFrames project and check it against the original frame by frame (HyperFrames Pro).
|
||||
- `/style-remix` — make a new episode on your topic in one of five proven styles (HyperFrames Pro).
|
||||
|
||||
**Changing how real footage or images look or reveal?** Load `/media-use` and read its `references/media-treatments.md` before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” It governs how footage is treated, never whether media may be used. Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ MP4.
|
||||
- `/slideshow` — a presentation or pitch deck. Output is a navigable deck, not a rendered MP4.
|
||||
- `/remotion-to-hyperframes` — bring existing video code or footage into HyperFrames (Remotion first; also Motion Canvas, Lottie, Three.js, canvas, or renders from other tools) and verify it frame by frame against the original.
|
||||
- `/reverse-any-video` — rebuild any video file as an editable HyperFrames project and check it against the original frame by frame (HyperFrames Pro).
|
||||
- `/style-remix` — make a new episode on your topic in one of five proven styles (HyperFrames Pro).
|
||||
|
||||
**Changing how real footage or images look or reveal?** Load `/media-use` and read its `references/media-treatments.md` before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” It governs how footage is treated, never whether media may be used. Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
|
||||
|
||||
|
||||
@@ -110,7 +110,8 @@ const FETCH_TIMEOUT_MS = 4000;
|
||||
// "call /media-use" preambles). These must be present and current
|
||||
// for any video work, so `init` / `skills update` keep them fresh.
|
||||
// on-demand — everything else: decks, Remotion ports, video rebuilds
|
||||
// (reverse-any-video) and optional integrations (figma).
|
||||
// (reverse-any-video), style episodes (style-remix) and optional
|
||||
// integrations (figma).
|
||||
// They install when named (`hyperframes skills update <name>`),
|
||||
// instead of being sprayed onto every machine that runs `init`.
|
||||
|
||||
|
||||
@@ -254,7 +254,7 @@ fi
|
||||
say "Verifying skill installation..."
|
||||
|
||||
SKILLS=(hyperframes hyperframes-cli hyperframes-registry media-use \
|
||||
figma slideshow remotion-to-hyperframes reverse-any-video)
|
||||
figma slideshow remotion-to-hyperframes reverse-any-video style-remix)
|
||||
|
||||
MISSING=()
|
||||
check_skill() { if [[ -d "$SKILLS_DIR/$1" ]]; then ok "$SKILLS_DIR/$1/"; else MISSING+=("$1"); fi; }
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
"files": 2
|
||||
},
|
||||
"style-remix": {
|
||||
"hash": "1e46aaea1c21d542",
|
||||
"hash": "290a666124a63d37",
|
||||
"files": 2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
name: style-remix
|
||||
description: Make a new episode in a proven video style - Collage Explainer, Finance Explainer, Animated Science Explainer, 3D Brand Explainer, 3D True Crime - on the person's topic. Use for "an episode of", "in the style of", 「用这个风格做一集」. One video to copy is reverse-any-video.
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
**Plugin installs:** Before setup or freshness commands, follow [plugin execution rules](../hyperframes/references/plugin-installation.md) when this skill is inside a HyperFrames plugin.
|
||||
|
||||
Reference in New Issue
Block a user