diff --git a/.gitignore b/.gitignore index b441a25..c18ddfb 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,10 @@ node_modules/ *.log dist/ +# Local video sources & encoded outputs — hosted on HF, never committed. +# (Drop raw clips in media/, run `npm run media`; only manifest.json is tracked.) +media/ +media-dist/ + # Claude Code worktrees and local state .claude/ diff --git a/DESIGN.md b/DESIGN.md index 7835c82..1ec6b7f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -63,12 +63,16 @@ white** used sparingly. - **White Line** `rgba(255,255,255,0.22)` (`--accent-line`) — emphasized 1px borders (active nodes, tabs, pills). -### Status — the only permitted non-neutral -- **Success / Live** → **white** (`--green` is `#FFFFFF`). The "LIVE" dot, the - `✓` ticks, a sent-message confirmation are all white. No green. +### Status — the only permitted non-neutrals +- **Live** → a **green blinking dot** (`--live` `#4FB58B`, the live / open-source + signal) paired with the word **"LIVE" set in white**. The pulsing green dot is + a sanctioned status light — it reads as "the system is running." Everything + around it stays monochrome: the **"LIVE" label itself is white** (never green), + as are the `✓` ticks and a sent-message confirmation. - **Error** `#D98A8E` (`--red`) — **form-validation errors only**, paired with - text and an invalid border. This desaturated rose is the *single* sanctioned - chroma in the system, and it must never appear decoratively. + text and an invalid border. This desaturated rose and the green live-dot are + the *only two* sanctioned chromas in the system; neither may ever appear + decoratively. **Banned:** any colored accent (the old clay/orange and cyan are gone for good), purple/blue "AI neon," outer-glow shadows, gradient text, pure `#000000`. @@ -199,8 +203,10 @@ Universal AI tells **and** OpenRAL-specific bans: - **No colored accent of any kind.** No clay/orange, no cyan, no purple/blue AI neon, no gradient text. Emphasis is white + weight + opacity, period. -- **No green/red as decoration.** Success and "live" are white; the lone - desaturated `--red` is reserved for form errors paired with text. +- **No green/red as decoration.** The only sanctioned chromas are the **green + blinking live-dot** (`--live`, always paired with a *white* "LIVE" label) and + the desaturated `--red` for form errors paired with text. Success ticks and + confirmations stay white; never use green for text, fills or borders. - No outer-glow / neon shadows (CTA shadows are dark, not white halos). - No pure black `#000000` — the canvas is Ink `#0E0F13`. - No emojis as UI — SVG icons only (GitHub / Hugging Face / Discord marks, diff --git a/README.md b/README.md index 4d6c02e..a06e0ea 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,40 @@ perception and reasoning into safe, runnable robot behavior. ## Sections -Hero (floating L0–L7 layer stack) → **architecture diagram** (Helix-style dual-system -S1 ⇄ S2 flow) → what it solves → capabilities → **terminal install** (`curl` installer) → -rSkills → team → contact → footer (GitHub · Hugging Face · Discord). +Hero (floating L0–L7 layer stack) → **terminal install** (`curl` installer) → +what it solves → **architecture diagram** (Helix-style dual-system S1 ⇄ S2 flow) → +capabilities → rSkills → **showcase** (autoscrolling benchmark / simulation / +deployment clips) → contact → footer (GitHub · Hugging Face · Discord). + +## Showcase videos + +The **"See it run"** section is an autoscrolling strip of benchmark, simulation +and deployment clips. Videos are **not** committed to this repo — they're hosted +on the public [`OpenRAL/website-media`](https://huggingface.co/datasets/OpenRAL/website-media) +Hugging Face dataset (CDN-backed) and listed in `src/videos/manifest.json`. + +**To make a video show up on the site:** + +```bash +# 1. Drop the raw clip (any size/format) into the gitignored media/ folder. +# The sub-folder picks the tab; the filename fills the on-clip labels: +# __. +media/benchmarks/libero-spatial_pi05_success.mp4 +media/simulation/warehouse-pick_rtdetr-v2_fail.mp4 +media/deployment/franka-stack_qwen35_success.mp4 + +# 2. Encode (poster + square preview + full clip), upload to HF, refresh manifest: +npm run media + +# 3. Commit the updated src/videos/manifest.json — that's the only tracked change. +git add src/videos/manifest.json && git commit -m "Add showcase clip" +``` + +Use hyphens *inside* a name (`libero-spatial`); underscores only separate the +three parts, and the last must be `success` or `fail`. Adding clips needs +`ffmpeg` and the `hf` CLI signed in with write access to the `OpenRAL` org +(`hf auth whoami`); `npm run media -- --no-upload` encodes + updates the +manifest locally without publishing. Full reference: [`src/videos/README.md`](./src/videos/README.md). ## Local preview diff --git a/package-lock.json b/package-lock.json index 0cbe562..1e14799 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@vercel/analytics": "^2.0.1", + "@vercel/speed-insights": "^2.0.0", "framer-motion": "^11.3.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -1196,6 +1197,44 @@ } } }, + "node_modules/@vercel/speed-insights": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vercel/speed-insights/-/speed-insights-2.0.0.tgz", + "integrity": "sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==", + "license": "Apache-2.0", + "peerDependencies": { + "@sveltejs/kit": "^1 || ^2", + "next": ">= 13", + "nuxt": ">= 3", + "react": "^18 || ^19 || ^19.0.0-rc", + "svelte": ">= 4", + "vue": "^3", + "vue-router": "^4" + }, + "peerDependenciesMeta": { + "@sveltejs/kit": { + "optional": true + }, + "next": { + "optional": true + }, + "nuxt": { + "optional": true + }, + "react": { + "optional": true + }, + "svelte": { + "optional": true + }, + "vue": { + "optional": true + }, + "vue-router": { + "optional": true + } + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", diff --git a/package.json b/package.json index 6f1cf1b..cbf8daf 100644 --- a/package.json +++ b/package.json @@ -12,10 +12,12 @@ "scripts": { "dev": "vite", "build": "vite build", - "preview": "vite preview" + "preview": "vite preview", + "media": "node scripts/build-media.mjs" }, "dependencies": { "@vercel/analytics": "^2.0.1", + "@vercel/speed-insights": "^2.0.0", "framer-motion": "^11.3.0", "react": "^18.3.1", "react-dom": "^18.3.1" diff --git a/public/assets/dashboard-navigation.webm b/public/assets/dashboard-navigation.webm new file mode 100644 index 0000000..bffb3fc Binary files /dev/null and b/public/assets/dashboard-navigation.webm differ diff --git a/public/assets/dashboard-view.webp b/public/assets/dashboard-view.webp new file mode 100644 index 0000000..490d2b9 Binary files /dev/null and b/public/assets/dashboard-view.webp differ diff --git a/scripts/build-media.mjs b/scripts/build-media.mjs new file mode 100644 index 0000000..143ee94 --- /dev/null +++ b/scripts/build-media.mjs @@ -0,0 +1,133 @@ +#!/usr/bin/env node +/* ------------------------------------------------------------------ + build-media.mjs — encode + upload showcase clips, refresh manifest. + + Workflow: + 1. Drop raw clips into media//__. + categories: benchmarks | simulation | deployment + 2. Run: npm run media + → ffmpeg encodes each clip into three web assets: + poster.jpg small first-frame thumbnail + preview.mp4 square 640px, muted, compressed — the autoscroll strip + full.mp4 capped 1080p, with audio — the click-to-expand modal + → uploads them to the OpenRAL/website-media HF dataset (public) + → writes src/videos/manifest.json (the only tracked artifact) + + Raw clips and encoded outputs are gitignored; the website loads every + clip from the HF CDN, so nothing heavy ever touches the repo or bundle. + + Flags: + --no-upload encode + write manifest only (skip the HF push) + ------------------------------------------------------------------ */ +import { execFileSync } from "node:child_process"; +import { readdirSync, mkdirSync, writeFileSync, existsSync, statSync } from "node:fs"; +import { join, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const SRC = join(ROOT, "media"); +const OUT = join(ROOT, "media-dist"); +const MANIFEST = join(ROOT, "src", "videos", "manifest.json"); + +const REPO = "OpenRAL/website-media"; +const BASE = `https://huggingface.co/datasets/${REPO}/resolve/main`; +const CATEGORIES = ["benchmarks", "simulation", "deployment"]; +const VIDEO_RE = /\.(mp4|mov|webm|m4v|mkv)$/i; +const noUpload = process.argv.includes("--no-upload"); + +function sh(cmd, args) { + return execFileSync(cmd, args, { stdio: ["ignore", "pipe", "pipe"] }); +} + +// __ — hyphens inside names, underscores separate. +function parseName(file) { + const base = file.replace(/\.[^.]+$/, ""); + const parts = base.split("_"); + const tail = parts[parts.length - 1] || ""; + const status = /^succ/i.test(tail) ? "success" : /^fail/i.test(tail) ? "fail" : null; + const benchmark = parts[0] || base; + const rskill = parts.slice(1, status ? -1 : undefined).join("_") || "—"; + return { base, benchmark, rskill, status }; +} + +function encode(input, dir, category) { + mkdirSync(dir, { recursive: true }); + const preview = join(dir, "preview.mp4"); + const full = join(dir, "full.mp4"); + const poster = join(dir, "poster.jpg"); + + // strip preview: 640px square, muted, web-optimised. + // benchmarks/simulation are center-cropped to a square; deployment clips are + // wide side-by-side (sim | dashboard) composites, so a center crop would only + // show the seam — letterbox the whole frame into the square instead. + const previewVf = category === "deployment" + ? "scale=640:-2:flags=lanczos,pad=640:640:(ow-iw)/2:(oh-ih)/2:color=black,fps=30" + : "crop='min(iw,ih)':'min(iw,ih)',scale=640:640:flags=lanczos,fps=30"; + sh("ffmpeg", ["-y", "-i", input, + "-vf", previewVf, + "-an", "-c:v", "libx264", "-profile:v", "main", "-pix_fmt", "yuv420p", + "-crf", "28", "-preset", "veryfast", "-movflags", "+faststart", preview]); + + // full clip: native aspect, capped at 1080p, keep audio + sh("ffmpeg", ["-y", "-i", input, + "-vf", "scale='min(1920,iw)':'-2':force_original_aspect_ratio=decrease,scale='-2':'min(1080,ih)'", + "-c:v", "libx264", "-profile:v", "high", "-pix_fmt", "yuv420p", + "-crf", "23", "-preset", "medium", "-c:a", "aac", "-b:a", "128k", + "-movflags", "+faststart", full]); + + // poster: one frame from the square preview + sh("ffmpeg", ["-y", "-ss", "0.3", "-i", preview, "-frames:v", "1", "-q:v", "3", poster]); + + const kb = (p) => Math.round(statSync(p).size / 1024); + return { preview, full, poster, sizes: { preview: kb(preview), full: kb(full), poster: kb(poster) } }; +} + +if (!existsSync(SRC)) { + console.error(`No media/ folder. Create media// and drop clips in.`); + process.exit(1); +} + +const clips = []; +let count = 0; +for (const category of CATEGORIES) { + const dir = join(SRC, category); + if (!existsSync(dir)) continue; + for (const file of readdirSync(dir).sort()) { + if (!VIDEO_RE.test(file)) continue; + const { base, benchmark, rskill, status } = parseName(file); + if (!status) { + console.warn(`⚠ skip ${category}/${file} — name must end in _success or _fail`); + continue; + } + const id = `${category}/${base}`; + process.stdout.write(`• ${id} … `); + const { sizes } = encode(join(dir, file), join(OUT, id), category); + console.log(`poster ${sizes.poster}kB · preview ${sizes.preview}kB · full ${sizes.full}kB`); + clips.push({ + id, category, benchmark, rskill, status, + poster: `${id}/poster.jpg`, + preview: `${id}/preview.mp4`, + full: `${id}/full.mp4`, + }); + count++; + } +} + +if (!count) { + console.error("No valid clips found in media/. Nothing to do."); + process.exit(1); +} + +writeFileSync(MANIFEST, JSON.stringify({ base: BASE, clips }, null, 2) + "\n"); +console.log(`\n✓ wrote ${MANIFEST} (${count} clip${count > 1 ? "s" : ""})`); + +if (noUpload) { + console.log("↷ --no-upload: skipped HF push."); + process.exit(0); +} + +console.log(`↑ uploading media-dist → ${REPO} …`); +execFileSync("hf", ["upload", REPO, "media-dist", ".", "--type", "dataset", + "--commit-message", `Add ${count} showcase clip asset(s)`], + { stdio: "inherit", cwd: ROOT }); +console.log(`✓ done — ${count} clip(s) live on the HF CDN.`); diff --git a/src/App.jsx b/src/App.jsx index 503ec13..56b0c6d 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,5 @@ import { Analytics } from "@vercel/analytics/react"; +import { SpeedInsights } from "@vercel/speed-insights/react"; import Nav from "./components/Nav.jsx"; import Hero from "./components/Hero.jsx"; import Terminal from "./components/Terminal.jsx"; @@ -6,6 +7,8 @@ import ArchitectureDiagram from "./components/ArchitectureDiagram.jsx"; import Solve from "./components/Solve.jsx"; import Capabilities from "./components/Capabilities.jsx"; import RSkills from "./components/RSkills.jsx"; +import VideoShowcase from "./components/VideoShowcase.jsx"; +import Dashboard from "./components/Dashboard.jsx"; import Contact from "./components/Contact.jsx"; import Footer from "./components/Footer.jsx"; import Privacy from "./components/Privacy.jsx"; @@ -18,6 +21,7 @@ export default function App() { <> + ); @@ -28,15 +32,18 @@ export default function App() {
- + + +