initial code (#2)

* feat: initial code port from hyperframes-internal

Port all OSS-ready packages from the internal monorepo:
- @hyperframes/core — shared types, HTML generation, GSAP utilities, runtime
- @hyperframes/cli — CLI for creating, previewing, and rendering compositions
- @hyperframes/engine — framework-agnostic rendering engine (BeginFrame + FFmpeg)
- @hyperframes/producer — video rendering pipeline (Puppeteer + FFmpeg)
- @hyperframes/ui-player — browser-based video player component
- @hyperframes/studio — composition editor (React frontend + Hono backend)

Includes regression test suite with Docker-based test harness.

All HeyGen-internal references, deployment infrastructure, and
proprietary assets have been removed. Package names migrated
from @app/* to @hyperframes/*.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scrub internal codenames and stale references from OSS port

- Replace static.heygen.ai runtime URLs in test fixtures
- Remove internal CDN publish script (publish-hyperframe-runtime.ts)
- Replace sandbox-studio, sandbox-interceptor, __magicEditRuntime
  with neutral names (studio, hyperframe-runtime, __hyperframeRuntime)
- Fix stale Vault API / localhost references in docs
- Remove broken deprecated_studio link

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove remaining internal codenames and stale references

- Delete stale producer README.md and PIPELINE.md (referenced nonexistent files)
- Replace "Cerberus" codename with "HyperFrames" in test design reviews
- Replace magic-edit postMessage identifiers with hf-preview/hf-parent
- Rename debug-magic-edit-timeline.ts to debug-timeline.ts
- Replace "Motion Cut" with "HyperFrames" in Timeline comments
- Fix studio/CLI references to nonexistent archive package
  (use local data/projects/ dir, stub render proxy)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-03-21 22:43:56 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 10621e7903
commit 9f8e5ba5a1
401 changed files with 54545 additions and 2 deletions
@@ -0,0 +1,57 @@
import { ArrowLeft, CaretRight } from "@phosphor-icons/react";
export interface CompositionLevel {
/** Unique id — "master" or composition file path */
id: string;
/** Display label — "Master" or filename without extension */
label: string;
/** Preview URL for this composition level */
previewUrl: string;
}
interface CompositionBreadcrumbProps {
stack: CompositionLevel[];
onNavigate: (index: number) => void;
}
export function CompositionBreadcrumb({ stack, onNavigate }: CompositionBreadcrumbProps) {
if (stack.length <= 1) return null;
return (
<nav
aria-label="Composition navigation"
className="flex items-center gap-1 px-2 h-8 border-b border-neutral-800/50 bg-neutral-900/50 flex-shrink-0"
>
{/* Back button — always goes to parent */}
<button
type="button"
onClick={() => onNavigate(stack.length - 2)}
className="flex items-center gap-1 px-1.5 py-0.5 rounded text-xs text-neutral-400 hover:text-white hover:bg-neutral-800 transition-colors"
title="Back (Esc)"
>
<ArrowLeft size={12} weight="bold" />
</button>
{/* Breadcrumb path */}
{stack.map((level, i) => {
const isLast = i === stack.length - 1;
return (
<span key={level.id} className="flex items-center gap-1">
{i > 0 && <CaretRight size={10} className="text-neutral-600 flex-shrink-0" />}
{isLast ? (
<span className="text-xs text-neutral-200 font-medium">{level.label}</span>
) : (
<button
type="button"
onClick={() => onNavigate(i)}
className="text-xs text-neutral-500 hover:text-neutral-200 transition-colors"
>
{level.label}
</button>
)}
</span>
);
})}
</nav>
);
}
@@ -0,0 +1,252 @@
import { useState, useEffect, useCallback, useRef, memo, type ReactNode } from "react";
import { useTimelinePlayer, PlayerControls, Timeline, usePlayerStore } from "../../player";
import type { TimelineElement } from "../../player";
import { NLEPreview } from "./NLEPreview";
import { CompositionBreadcrumb, type CompositionLevel } from "./CompositionBreadcrumb";
interface NLELayoutProps {
projectId: string;
portrait?: boolean;
/** Slot for overlays rendered on top of the preview (cursors, highlights, etc.) */
previewOverlay?: ReactNode;
/** Slot rendered below the timeline tracks (e.g., agent activity swim lanes) */
timelineFooter?: ReactNode;
/** Increment to force the preview to reload (e.g., after file writes) */
refreshKey?: number;
/** Navigate to a specific composition path (e.g., "compositions/intro.html") */
activeCompositionPath?: string | null;
/** Callback to expose the iframe ref (for element picker, etc.) */
onIframeRef?: (iframe: HTMLIFrameElement | null) => void;
}
const MIN_TIMELINE_H = 100;
const DEFAULT_TIMELINE_H = 220;
const MIN_PREVIEW_H = 120;
export const NLELayout = memo(function NLELayout({
projectId,
portrait,
previewOverlay,
timelineFooter,
refreshKey,
activeCompositionPath,
onIframeRef,
}: NLELayoutProps) {
const { iframeRef, togglePlay, seek, onIframeLoad: baseOnIframeLoad, saveSeekPosition } = useTimelinePlayer();
// Preserve seek position when refreshKey changes (iframe will remount via key prop).
const prevRefreshKeyRef = useRef(refreshKey);
if (refreshKey !== prevRefreshKeyRef.current) {
prevRefreshKeyRef.current = refreshKey;
saveSeekPosition();
}
// Wrap onIframeLoad to also notify parent of iframe ref
const onIframeLoad = useCallback(() => {
baseOnIframeLoad();
onIframeRef?.(iframeRef.current);
}, [baseOnIframeLoad, iframeRef, onIframeRef]);
// Composition ID → actual file path mapping, built from the raw index.html
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
useEffect(() => {
fetch(`/api/projects/${projectId}/files/index.html`)
.then((r) => r.json())
.then((data: { content?: string }) => {
const html = data.content || "";
const map = new Map<string, string>();
const re = /data-composition-id=["']([^"']+)["'][^>]*data-composition-src=["']([^"']+)["']|data-composition-src=["']([^"']+)["'][^>]*data-composition-id=["']([^"']+)["']/g;
let match;
while ((match = re.exec(html)) !== null) {
const id = match[1] || match[4];
const src = match[2] || match[3];
if (id && src) map.set(id, src);
}
setCompIdToSrc(map);
})
.catch(() => {});
}, [projectId]);
// Composition drill-down stack
const [compositionStack, setCompositionStack] = useState<CompositionLevel[]>([
{ id: "master", label: "Master", previewUrl: `/api/projects/${projectId}/preview` },
]);
// Resizable timeline height
const [timelineH, setTimelineH] = useState(DEFAULT_TIMELINE_H);
const isDragging = useRef(false);
const containerRef = useRef<HTMLDivElement>(null);
// Current preview URL — derived from composition stack
const currentLevel = compositionStack[compositionStack.length - 1];
const directUrl = compositionStack.length > 1 ? currentLevel.previewUrl : undefined;
// Drill-down: push a sub-composition onto the stack
const iframeRef_ = iframeRef; // stable ref for the callback
const handleDrillDown = useCallback(
(element: TimelineElement) => {
if (!element.compositionSrc) return;
// compositionSrc may be a full URL (from runtime manifest) or a relative path
// Extract the element's composition ID from its timeline ID
const compId = element.id;
// 1. Check compIdToSrc map (from index.html)
// 2. Scan the current iframe DOM for data-composition-src attribute
// 3. Fall back to stripping the compositionSrc to a relative path
let resolvedPath = compIdToSrc.get(compId);
if (!resolvedPath) {
try {
const doc = iframeRef_.current?.contentDocument;
if (doc) {
const host = doc.querySelector(`[data-composition-id="${compId}"][data-composition-src]`);
if (host) {
resolvedPath = host.getAttribute("data-composition-src") || undefined;
}
}
} catch { /* cross-origin */ }
}
if (!resolvedPath) {
// Strip full URL to relative path if needed
const src = element.compositionSrc;
const compMatch = src.match(/compositions\/.*\.html/);
resolvedPath = compMatch ? compMatch[0] : src;
}
usePlayerStore.getState().setElements([]);
// Toggle: if already viewing this composition, go back to parent (like Premiere)
setCompositionStack((prev) => {
const currentId = prev[prev.length - 1].id;
if (currentId === resolvedPath && prev.length > 1) {
return prev.slice(0, -1);
}
// Extract a clean label from the path (strip directories and extension)
const label = resolvedPath.split("/").pop()?.replace(/\.html$/, "") || resolvedPath;
const previewUrl = `/api/projects/${projectId}/preview/comp/${resolvedPath}`;
return [...prev, { id: resolvedPath, label, previewUrl }];
});
},
[projectId, compIdToSrc],
);
// Navigate back to a specific breadcrumb level
const handleNavigateComposition = useCallback(
(index: number) => {
usePlayerStore.getState().setElements([]);
setCompositionStack((prev) => prev.slice(0, index + 1));
},
[],
);
// Navigate to a composition when activeCompositionPath changes
const prevActiveCompRef = useRef<string | null>(null);
if (activeCompositionPath && activeCompositionPath !== prevActiveCompRef.current) {
prevActiveCompRef.current = activeCompositionPath;
queueMicrotask(() => usePlayerStore.getState().setElements([]));
if (activeCompositionPath === "index.html") {
setCompositionStack((prev) => prev.length > 1 ? [prev[0]] : prev);
} else if (activeCompositionPath.startsWith("compositions/")) {
const label = activeCompositionPath.replace(/^compositions\//, "").replace(/\.html$/, "");
const previewUrl = `/api/projects/${projectId}/preview/comp/${activeCompositionPath}`;
setCompositionStack((prev) => {
if (prev[prev.length - 1].id === activeCompositionPath) return prev;
return [
{ id: "master", label: "Master", previewUrl: `/api/projects/${projectId}/preview` },
{ id: activeCompositionPath, label, previewUrl },
];
});
}
} else if (!activeCompositionPath && prevActiveCompRef.current) {
prevActiveCompRef.current = null;
queueMicrotask(() => usePlayerStore.getState().setElements([]));
}
// Resize divider handlers
const handleDividerPointerDown = useCallback((e: React.PointerEvent) => {
e.preventDefault();
isDragging.current = true;
(e.target as HTMLElement).setPointerCapture(e.pointerId);
}, []);
const handleDividerPointerMove = useCallback((e: React.PointerEvent) => {
if (!isDragging.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const mouseY = e.clientY - rect.top;
const containerH = rect.height;
const newTimelineH = Math.max(
MIN_TIMELINE_H,
Math.min(containerH - MIN_PREVIEW_H, containerH - mouseY),
);
setTimelineH(newTimelineH);
}, []);
const handleDividerPointerUp = useCallback(() => {
isDragging.current = false;
}, []);
// Keyboard: Escape to pop composition level
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === "Escape" && compositionStack.length > 1) {
setCompositionStack((prev) => prev.slice(0, -1));
}
},
[compositionStack.length],
);
return (
<div
ref={containerRef}
className="flex flex-col h-full min-h-0 bg-neutral-950"
onKeyDown={handleKeyDown}
tabIndex={-1}
>
{/* Preview — takes remaining space above timeline */}
<div className="flex-1 min-h-0 relative">
<NLEPreview
projectId={projectId}
iframeRef={iframeRef}
onIframeLoad={onIframeLoad}
portrait={portrait}
directUrl={directUrl}
refreshKey={refreshKey}
/>
{previewOverlay}
</div>
{/* Resize divider */}
<div
className="h-1 flex-shrink-0 bg-neutral-800 hover:bg-blue-500 cursor-row-resize transition-colors active:bg-blue-400 z-10"
style={{ touchAction: "none" }}
onPointerDown={handleDividerPointerDown}
onPointerMove={handleDividerPointerMove}
onPointerUp={handleDividerPointerUp}
/>
{/* Timeline section — fixed height, resizable */}
<div className="flex flex-col flex-shrink-0" style={{ height: timelineH }}>
{/* Breadcrumb + Player controls */}
<div className="bg-neutral-950 border-t border-neutral-800/50 flex-shrink-0">
{compositionStack.length > 1 && (
<CompositionBreadcrumb stack={compositionStack} onNavigate={handleNavigateComposition} />
)}
<PlayerControls onTogglePlay={togglePlay} onSeek={seek} />
</div>
{/* Timeline tracks */}
<div
className="flex-1 min-h-0 overflow-y-auto bg-neutral-950"
onDoubleClick={(e) => {
if ((e.target as HTMLElement).closest("[data-clip]")) return;
if (compositionStack.length > 1) {
setCompositionStack((prev) => prev.slice(0, -1));
}
}}
>
<Timeline onSeek={seek} onDrillDown={handleDrillDown} />
{timelineFooter}
</div>
</div>
</div>
);
});
@@ -0,0 +1,37 @@
import { memo, type Ref } from "react";
import { Player } from "../../player";
interface NLEPreviewProps {
projectId: string;
iframeRef: Ref<HTMLIFrameElement>;
onIframeLoad: () => void;
portrait?: boolean;
directUrl?: string;
refreshKey?: number;
}
export const NLEPreview = memo(function NLEPreview({
projectId,
iframeRef,
onIframeLoad,
portrait,
directUrl,
refreshKey,
}: NLEPreviewProps) {
const playerKey = `${directUrl ?? projectId}_${refreshKey ?? 0}`;
return (
<div className="flex flex-col h-full min-h-0">
<div className="flex-1 flex items-center justify-center p-2 overflow-hidden min-h-0">
<Player
key={playerKey}
ref={iframeRef}
projectId={directUrl ? undefined : projectId}
directUrl={directUrl}
onLoad={onIframeLoad}
portrait={portrait}
/>
</div>
</div>
);
});