feat(studio-server): add media processing routes

This commit is contained in:
ukimsanov
2026-07-06 13:25:04 -07:00
parent 2757d32092
commit a3bf7eb995
9 changed files with 938 additions and 4 deletions
+89 -2
View File
@@ -14,6 +14,7 @@ import type { ViteDevServer } from "vite";
import {
type ResolvedProject,
type RenderJobState,
type MediaProcessingJobState,
type StudioApiAdapter,
createProjectSignature,
} from "@hyperframes/studio-server";
@@ -22,6 +23,25 @@ import { createRetryingModuleLoader, ensureProducerDist } from "./vite.producer"
import { createStudioDevRenderBodyScripts } from "./vite.studioMotion";
import { generateThumbnail, findSystemChrome } from "./vite.browser";
type BackgroundRemovalRender = (options: {
inputPath: string;
outputPath: string;
backgroundOutputPath?: string;
device?: "auto" | "cpu" | "coreml" | "cuda";
quality?: "fast" | "balanced" | "best";
onProgress?: (
event:
| { kind: "info"; message: string }
| { kind: "metadata"; width: number; height: number; fps: number; frameCount: number }
| { kind: "frame"; index: number; total: number; avgMsPerFrame: number },
) => void;
}) => Promise<{
provider: string;
framesProcessed: number;
durationSeconds: number;
avgMsPerFrame: number;
}>;
export function isPathWithin(parentDir: string, childPath: string): boolean {
const childRelativePath = relative(resolve(parentDir), resolve(childPath));
return (
@@ -32,7 +52,10 @@ export function isPathWithin(parentDir: string, childPath: string): boolean {
export function createViteAdapter(dataDir: string, server: ViteDevServer): StudioApiAdapter {
let _bundler:
| ((dir: string, options?: { runtime?: "inline" | "placeholder" }) => Promise<string>)
| ((
dir: string,
options?: { runtime?: "inline" | "placeholder"; inlineColorGradingLuts?: boolean },
) => Promise<string>)
| null = null;
let _producerModuleLoader:
| (() => Promise<{
@@ -162,7 +185,7 @@ export function createViteAdapter(dataDir: string, server: ViteDevServer): Studi
async bundle(dir: string) {
const bundler = await getBundler();
if (!bundler) return null;
let html = await bundler(dir, { runtime: "placeholder" });
let html = await bundler(dir, { runtime: "placeholder", inlineColorGradingLuts: false });
html = html.replace(
'data-hyperframes-preview-runtime="1" src=""',
`data-hyperframes-preview-runtime="1" src="${this.runtimeUrl}"`,
@@ -251,6 +274,70 @@ export function createViteAdapter(dataDir: string, server: ViteDevServer): Studi
return state;
},
startBackgroundRemoval(opts): MediaProcessingJobState {
const state: MediaProcessingJobState = {
id: opts.jobId,
status: "processing",
progress: 0,
stage: "Preparing background removal",
inputAssetPath: opts.inputAssetPath,
outputAssetPath: opts.outputAssetPath,
outputPath: opts.outputPath,
...(opts.backgroundOutputPath ? { backgroundOutputPath: opts.backgroundOutputPath } : {}),
...(opts.backgroundOutputAssetPath
? { backgroundOutputAssetPath: opts.backgroundOutputAssetPath }
: {}),
};
(async () => {
try {
const mod = await server.ssrLoadModule(
resolve(__dirname, "../cli/src/background-removal/pipeline.ts"),
);
const render = mod.render as BackgroundRemovalRender;
const result = await render({
inputPath: opts.inputPath,
outputPath: opts.outputPath,
backgroundOutputPath: opts.backgroundOutputPath,
device: opts.device,
quality: opts.quality,
onProgress: (event) => {
if (event.kind === "info") {
state.stage = event.message;
return;
}
if (event.kind === "metadata") {
state.stage = `Source ${event.width}×${event.height}`;
state.progress = 2;
return;
}
state.progress = event.total
? Math.min(99, Math.floor((event.index / event.total) * 100))
: 0;
state.stage = event.total
? `Removing background ${event.index}/${event.total}`
: `Removing background frame ${event.index}`;
state.framesProcessed = event.index;
state.avgMsPerFrame = event.avgMsPerFrame;
},
});
state.status = "complete";
state.progress = 100;
state.stage = "Complete";
state.provider = result.provider;
state.framesProcessed = result.framesProcessed;
state.durationSeconds = result.durationSeconds;
state.avgMsPerFrame = result.avgMsPerFrame;
} catch (err) {
state.status = "failed";
state.error = err instanceof Error ? err.message : String(err);
state.stage = "Failed";
}
})();
return state;
},
async generateThumbnail(opts) {
return generateThumbnail(opts);
},