diff --git a/brand/generated-assets.json b/brand/generated-assets.json index 450753c..b792bcc 100644 --- a/brand/generated-assets.json +++ b/brand/generated-assets.json @@ -4,12 +4,15 @@ "brand/source/video2ctx-logo-192.png": "44472cbe3e7ef39bc557278efc808ebc038effe90b96c8089387f5f5a6bf35ac", "brand/source/video2ctx-favicon-32.png": "77ae28a2e39a6020442a3ba136145f52bb1155b195d453595cb8d597c083f38a", "brand/source/video2ctx-favicon.ico": "2ce82549c6b86fa66fa8c1510b0c85a404c14aa9aadead05b02d02a74b6201e5", + "brand/source/video2ctx-og-background-hybrid-icons.png": "089fb9fe62cf1dac7c243b46462d547def9c89da9f3c747284bb8520ed19b783", "web/public/brand/logo-120.png": "d44f423ec5b91a584ed87ef41ffcb0a73a6ec69b13bc5d852f9d589723e03829", "web/public/android-chrome-192x192.png": "44472cbe3e7ef39bc557278efc808ebc038effe90b96c8089387f5f5a6bf35ac", "web/public/android-chrome-512x512.png": "4b3126c170ddeaded8d18d63e65be8a80ee2f58763cfd78a9d04f35afb2991af", "web/app/icon.png": "4b3126c170ddeaded8d18d63e65be8a80ee2f58763cfd78a9d04f35afb2991af", "web/app/apple-icon.png": "0fe814ed4ab3d78fea8bdb723b67218d89ccb2fa0e0f9a5d3d386d04cff99133", "web/app/favicon.ico": "2ce82549c6b86fa66fa8c1510b0c85a404c14aa9aadead05b02d02a74b6201e5", + "web/app/opengraph-image.jpg": "12b0a4c3335a37c1ad3154b2db4753e9afb9e68b35cb8a6cb911ebe33e72311a", + "web/app/twitter-image.jpg": "12b0a4c3335a37c1ad3154b2db4753e9afb9e68b35cb8a6cb911ebe33e72311a", "docs/favicon.png": "77ae28a2e39a6020442a3ba136145f52bb1155b195d453595cb8d597c083f38a", "docs/logo/light.svg": "5744539b3df809e3ba7a20d4a1faae4e0f59aad5b9e31f9b5b2973c3f1c438d4", "docs/logo/dark.svg": "9e72ae6153580322d7a42c2690cb341aa14fcdc7953bc69e5573a37c2f5bf0e5", diff --git a/web/public/scene/fold-scene-poster.webp b/brand/generated/fold-scene-poster.webp similarity index 100% rename from web/public/scene/fold-scene-poster.webp rename to brand/generated/fold-scene-poster.webp diff --git a/web/public/scene/fold-scene.mp4 b/brand/generated/fold-scene.mp4 similarity index 100% rename from web/public/scene/fold-scene.mp4 rename to brand/generated/fold-scene.mp4 diff --git a/web/public/scene/fold-scene.webm b/brand/generated/fold-scene.webm similarity index 100% rename from web/public/scene/fold-scene.webm rename to brand/generated/fold-scene.webm diff --git a/brand/prompts/voxel-horizon-fold-loop-seedance.txt b/brand/prompts/voxel-horizon-fold-loop-seedance.txt new file mode 100644 index 0000000..dc5fc07 --- /dev/null +++ b/brand/prompts/voxel-horizon-fold-loop-seedance.txt @@ -0,0 +1,76 @@ +Fold loop: animate the voxel horizon still. +2026-08-12. Higgsfield, seedance_2_0. + +SUPERSEDED. Nothing this produced still ships. Kept as the record of how the +scene was originally animated, and because its four failures still apply to any +future render of this landscape. + + Its output is brand/generated/fold-scene.{webm,mp4,-poster.webp}, replaced on + desktop by fold-scene-symbols.* and, since 2026-08-22, in portrait too — the + portrait loop is a crop of whichever landscape loop is current: + + crop=416:720:432:0,scale=440:762 + + (a native 9:16 render was tried and rejected — the same prompt invented + fires across the terraces and drifted the camera, loop gap 6.20 vs 1.63) + +Input + start image brand/source/voxel-horizon.webp, decoded to PNG for upload: + dwebp voxel-horizon.webp -o voxel-horizon-source.png + end image deliberately NOT set — see below + +Command + higgsfield generate create seedance_2_0 \ + --prompt "$(cat prompt.txt)" \ + --start-image voxel-horizon-source.png \ + --duration 8 --resolution 720p --mode std --aspect_ratio 16:9 \ + --generate_audio false --wait --wait-timeout 25m --json + +---- prompt ---- + +Thick pale mist lies over the valley floor and between the stone terraces of this dark isometric voxel landscape. The mist rises upward, swells, and sinks back down again in a slow continuous cycle, clearly visible throughout the whole shot — always moving, never still. It is already present and thick in the very first frame. + +Embers rise from many separate places at once: from the ruined terraces at the far left edge, from the foreground ruins along the bottom centre, from the valley floor in the middle, and from the dark slopes at the far right edge. They lift slowly, glowing warm orange, and fade out into the black air. The right side of the frame has just as many embers as the left side. They are spread evenly across the entire width of the frame and never pour out of a single spot. + +New mist and new embers appear continuously, so the whole landscape is alive with slow movement at every moment, from the very first frame to the very last. + +The camera is completely locked on a tripod. It does not move, pan, tilt, drift or zoom at any point. The voxel terrain is solid rock and stays perfectly still. + +Night, deep charcoal shadows, volumetric light, cinematic. + +No text, no logos, no people. + +---- end prompt ---- + +Four failures this prompt is shaped around. Full write-up in +reference/design/HIGGSFIELD_ASSET_GUIDE.md §4. + + Never pin the end frame equal to the start frame. Two identical endpoints + make "change nothing" a valid and cheap solution; the first attempt came back + completely static. + + Do not qualify the motion. An earlier version asked for movement that was + "extremely slow", "weightless", "almost imperceptible", and got exactly that. + + Anchor motion to named places. "Embers rise from all across the landscape" + put them all in one spot left of centre, 6x more motion in one quarter than + another. Naming four anchors and stating that the right side has as many as + the left is what fixed it. + + Give the mist a cycle, not a state. Trying to stop it flooding the valley by + saying it "stays the same thickness throughout" froze it completely, valley + motion 0.062. Rise / swell / sink moves without transforming, and a particle + system in equilibrium loops by itself — raw loop gap 1.5 against 16.6 for the + flooding version. + +Post, all local. Trim, crossfade and encode in ONE ffmpeg pass straight from the +download; an accidental intermediate re-encode cost 60% of the bitrate before +the browser ever saw it. + + TRIM=1.0 X=0.8 tail crossfaded back over the head + AV1 libsvtav1 crf 36 preset 4 -g 48 + H264 libx264 crf 21 preset veryslow -g 48 + poster from the shipped encode's own first frame + +Cost: 5th of 6 renders, ~166 credits for the session. Two of six shipped +anything. Budget for that ratio. diff --git a/brand/prompts/voxel-horizon-symbols-videoedit.txt b/brand/prompts/voxel-horizon-symbols-videoedit.txt new file mode 100644 index 0000000..95e55c0 --- /dev/null +++ b/brand/prompts/voxel-horizon-symbols-videoedit.txt @@ -0,0 +1,59 @@ +Video edit: add the carved symbols to the existing fold loop. +2026-08-22. Higgsfield job hf_20260822_143125_5e972ca2-931e-40c5-8f2f-27ff5469eaa5. + +The raw render is deliberately not kept in the repo — only the graded loop that +ships, web/public/scene/fold-scene-symbols.{webm,mp4}. Re-run this prompt if the +grade, trim, crossfade or codec ever needs changing. + +Inputs + video web/public/scene/fold-scene.mp4 (the previous fold loop, no symbols) + reference brand/source/voxel-horizon-video-icons-gpt.webp + (the scene still with five symbols carved in, made with GPT Image 2 — + prompt in voxel-horizon-video-icons-gpt-image.txt) + +Four lines below are scar tissue, not decoration. Earlier renders in this scene +duplicated a symbol, rendered the glow bright pink, and scattered torch flames +across the terraces unprompted. Keep the prohibitions that name those. + +---- prompt ---- + +The reference image is this exact scene with stone symbols already carved into it. +The video is the same landscape, same camera, same lighting. Add those carved +symbols to the video and change nothing else. + +Carve five symbols into the terrain, exactly one of each, matching the reference +image's placement, scale, perspective and weathering: +- a play triangle inside a rectangular relief panel, on the upper-left terrace +- a horizontal timeline with a round scrubber, on the long wall below it +- a speaker with three sound arcs, on the lower-left terrace face +- four crop-frame corner brackets, on the upper-right slope +- a row of waveform bars, on the right slope below the crop marks + +Every symbol is solid stone in the same voxel masonry as the terrain around it: +weathered, partially eroded, physically part of the wall. They are lit only by +the scene's existing warm light, exactly as in the reference image. They hold +perfectly still and keep identical shape and position in every frame. + +Preserve the rest of the shot completely: the locked tripod camera, the terrain +geometry, the pale fog drifting through the central valley, the rising amber +embers, the deep charcoal palette, the exposure, and the dark empty upper centre. +The fog and embers must keep moving exactly as they already do. + +The symbols never illuminate, pulse, glow, flicker, duplicate, morph, drift or +detach. No new light sources, no torches, no fire, no pink or magenta anywhere. +No text, letters, numbers, logos, screens, interface panels, people or watermarks. + +---- end prompt ---- + +What came back, measured against the previous loop + + all five symbols present, one each, correct placement, stable across the clip + camera locked — tested for both translation and zoom, 0px / 1.000 scale win + black point lifted 4.0 -> 9.1 graded back + saturation dropped 8.9 -> 7.5 graded back + an AAC audio track dropped with -an + fog motion 0.263 -> 0.177 NOT recovered; see below + +The fog is the one real regression and it is not an encoding artefact. AV1 at +crf 30 only reaches 0.194 for an extra 231 KB, so it was not worth the bytes. +The delivered loop is calmer in the valley than its predecessor. diff --git a/brand/prompts/voxel-horizon-video-icons-gpt-image.txt b/brand/prompts/voxel-horizon-video-icons-gpt-image.txt new file mode 100644 index 0000000..ce42dad --- /dev/null +++ b/brand/prompts/voxel-horizon-video-icons-gpt-image.txt @@ -0,0 +1,11 @@ +Use case: precise-object-edit +Asset type: cinematic website hero background and source frame for later video generation +Input image: voxel-horizon.webp is the edit target. + +Primary request: Make one surgical edit to the supplied image. Embed exactly five unique video-application motifs into existing stone terrace faces as ancient architectural carvings made from the same voxel masonry. Add exactly one of each: (1) a triangular play symbol inside a simple rectangular frame on an upper-left terrace face, (2) a horizontal video timeline with one circular scrubber on a middle-left terrace face, (3) a speaker with two sound-wave arcs on a lower-left terrace face, (4) four crop-corner brackets on an upper-right terrace face, and (5) a simple audio waveform on a lower-right terrace face. + +Integration: Each motif must be physically carved or built into a different existing stone wall, following that wall's perspective, voxel grid, depth, erosion, scale, occlusion, amber rim lighting, and charcoal shadows. They should feel like subtle archaeological reliefs discovered in the terrain, not modern UI overlays. Use restrained dim amber illumination only inside a few recessed grooves. No bright neon. + +Composition invariants: Preserve the original image's exact camera, centered composition, horizon, terrain silhouette, central valley, fog quality and density, black upper negative space, lighting direction, warm amber and charcoal palette, voxel scale, materials, texture, and all existing structures. Keep the central valley and upper-center copy area clear. Change only the local stone surfaces required to embed the five motifs. + +Constraints: Exactly five motifs total, exactly one instance of each specified motif. Every motif must remain clearly distinguishable. No duplicates, no extra symbols, no floating icons, no panels, no screens, no labels, no text, no letters, no numbers, no logo, no people, no torches, no visible flames, no watermark. Do not redesign, crop, reframe, brighten, or restyle the original image. diff --git a/brand/source/video2ctx-og-background-hybrid-icons.png b/brand/source/video2ctx-og-background-hybrid-icons.png new file mode 100644 index 0000000..7c2d37a Binary files /dev/null and b/brand/source/video2ctx-og-background-hybrid-icons.png differ diff --git a/web/public/scene/voxel-horizon-900.webp b/brand/source/voxel-horizon-900.webp similarity index 100% rename from web/public/scene/voxel-horizon-900.webp rename to brand/source/voxel-horizon-900.webp diff --git a/web/public/scene/voxel-horizon-portrait.webp b/brand/source/voxel-horizon-portrait.webp similarity index 100% rename from web/public/scene/voxel-horizon-portrait.webp rename to brand/source/voxel-horizon-portrait.webp diff --git a/brand/source/voxel-horizon-video-icons-gpt.png b/brand/source/voxel-horizon-video-icons-gpt.png new file mode 100644 index 0000000..ab8ae13 Binary files /dev/null and b/brand/source/voxel-horizon-video-icons-gpt.png differ diff --git a/brand/source/voxel-horizon-video-icons-gpt.webp b/brand/source/voxel-horizon-video-icons-gpt.webp new file mode 100644 index 0000000..94fbf04 Binary files /dev/null and b/brand/source/voxel-horizon-video-icons-gpt.webp differ diff --git a/web/public/scene/voxel-horizon.webp b/brand/source/voxel-horizon.webp similarity index 100% rename from web/public/scene/voxel-horizon.webp rename to brand/source/voxel-horizon.webp diff --git a/scripts/generate-brand-assets.mjs b/scripts/generate-brand-assets.mjs index 8d4f278..15ba8f0 100644 --- a/scripts/generate-brand-assets.mjs +++ b/scripts/generate-brand-assets.mjs @@ -12,6 +12,11 @@ const sourcePaths = [ 'brand/source/video2ctx-logo-192.png', 'brand/source/video2ctx-favicon-32.png', 'brand/source/video2ctx-favicon.ico', + // Purpose-built for the card: mass on the right, near-black on the left, and + // the same carved symbols as the landing fold. Not the hero still — that one + // fills the frame, so text over it needs a scrim, and a scrim over a scene + // this dark lifts the blacks rather than deepening them. + 'brand/source/video2ctx-og-background-hybrid-icons.png', ]; const outputPaths = [ 'web/public/brand/logo-120.png', @@ -20,6 +25,8 @@ const outputPaths = [ 'web/app/icon.png', 'web/app/apple-icon.png', 'web/app/favicon.ico', + 'web/app/opengraph-image.jpg', + 'web/app/twitter-image.jpg', 'docs/favicon.png', 'docs/logo/light.svg', 'docs/logo/dark.svg', @@ -57,6 +64,33 @@ function wordmark(logoDataUrl, color) { `; } +/* Type here tracks `.craft-nav-wordmark` in craft.css: Geist at weight 670 with + * -0.035em tracking. Geist ships static TTFs at 600 and 700, and 670 sits + * between them, so the card uses Bold as the nearest neighbour rather than a + * variable axis — librsvg does not reliably honour `font-variation-settings`. + * + * The navbar renders its `ctx` suffix in Geist Pixel Grid. That cannot be + * reproduced here: geist ships the pixel family as woff2 only, and librsvg + * silently ignores woff2 @font-face and falls back. So the card's wordmark is + * Geist throughout. Converting the woff2 to TTF would fix it. */ +function openGraphOverlay(logoDataUrl, fontSemiBold, fontBold) { + return ` + + + + + video2ctx + Video context + for agents + Structured, source-linked context from video. + video2ctx.dev +`; +} + async function generate() { const sourceLogoPath = absolute(sourcePaths[0]); await sharp(sourceLogoPath) @@ -76,6 +110,35 @@ async function generate() { .png({ palette: true, colours: 256, compressionLevel: 9 }) .toFile(absolute('web/app/apple-icon.png')); + const ogLogoDataUrl = `data:image/png;base64,${readFileSync(sourceLogoPath).toString('base64')}`; + const ogFontDataUrl = `data:font/ttf;base64,${readFileSync( + absolute('web/node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf'), + ).toString('base64')}`; + const ogFontBoldDataUrl = `data:font/ttf;base64,${readFileSync( + absolute('web/node_modules/geist/dist/fonts/geist-sans/Geist-Bold.ttf'), + ).toString('base64')}`; + /* Centre-cropped and ungraded, both deliberate. This source is already + * composed and exposed for a card, unlike the hero still, which is graded to + * sit under a 64% dimmer and needs a brightness lift to survive a feed. + * + * There is also no scrim. The left of this frame is darker than #171513, so + * any scrim over it *raises* the black point instead of lowering it — + * measured, a 55% wipe took the text plate from 17.9 up to 23.8. */ + await sharp(absolute(sourcePaths[4])) + .resize(1200, 630, { fit: 'cover', position: 'centre' }) + .composite([{ input: Buffer.from(openGraphOverlay(ogLogoDataUrl, ogFontDataUrl, ogFontBoldDataUrl)) }]) + /* JPEG, not PNG. The card is a dark photographic gradient with no flat + * colour and no transparency, which is PNG's worst case — it was 690 KB as + * a PNG against 152 KB here. Banding was the thing to check, since the + * frame is mostly near-black, and it does not appear: peak error against + * the lossless original measured 1/255 across the gradient. + * + * 4:4:4 is deliberate. The logo and the URL are saturated red, and chroma + * subsampling is exactly where those edges smear. */ + .jpeg({ quality: 92, mozjpeg: true, chromaSubsampling: '4:4:4' }) + .toFile(absolute('web/app/opengraph-image.jpg')); + copy('web/app/opengraph-image.jpg', 'web/app/twitter-image.jpg'); + const navLogoDataUrl = `data:image/png;base64,${readFileSync(absolute('web/public/brand/logo-120.png')).toString('base64')}`; write('docs/logo/light.svg', wordmark(navLogoDataUrl, '#191715')); write('docs/logo/dark.svg', wordmark(navLogoDataUrl, '#f5f1eb')); diff --git a/web/DESIGN.md b/web/DESIGN.md index fdd0a85..35ace97 100644 --- a/web/DESIGN.md +++ b/web/DESIGN.md @@ -1,6 +1,6 @@ # Product design system -Covers the live marketing surface (`/` and `/explore`) and the product workspace +Covers the live marketing surface (`/`) and the product workspace (`/dashboard`). The dashboard keeps dedicated `--color-dashboard-*` roles in `tokens.css` because dense application surfaces need their own contrast tuning, but those roles now resolve to Craft's warm dark paper, off-white ink, and @@ -216,8 +216,8 @@ reduced motion **0 bytes of video**. - **No invented metrics.** No adoption counts, no speed multiples, no testimonials that were not said. Every number on the page is one the platform actually produces. -- **Name the cost, not just the benefit.** `/explore` lists each direction's bet - *and* what it gives up. +- **Name the cost, not just the benefit.** Each access path should say what it + gives up as well as what it provides. - Plain language over jargon: "What was said" beats "Transcript"; header names and response shapes belong in the API reference, not the landing page. @@ -247,11 +247,8 @@ Recorded because it is real, not aspirational. Verified 2026-08-12. 8-state component discipline is not met. 3. **`--exit` is declared and never used.** The "exits faster than entrances" rule is documented but enforced nowhere. Either apply it to the lens fade-out - and the beat crossfade, or drop the token. -4. **`--color-stage-*` in `tokens.css` is consumed only by `lens-home.css` and - `cinematic.css`** — both Decomposition-only. If that direction is ever - deleted, those tokens go with it. -5. **The fold scene is 720p** upscaled ~1.16x at common desktop widths. Sharpness + or drop the token. +4. **The fold scene is 720p** upscaled ~1.16x at common desktop widths. Sharpness is currently fine because contrast, not resolution, was the real constraint — but a wider display will expose it. @@ -265,8 +262,6 @@ Resolved 2026-08-12: the stale `tokens.css` stamp, the missing - Keep the resting-scene dimming and the `Flashlight` mount conditions **in step**. The scene is only darkened where a light exists to recover it; dim it anywhere the component does not mount and the fold is simply dark. -- Promoting a different landing direction is one line: `DEFAULT_DIRECTION` in - `_directions/registry.tsx`. - Regenerating the fold scene: prompts, settings and encode commands are in `reference/design/CINEMATIC_ASSETS.md` and `web/public/scene/README.md`. The reusable method — model choice, prompting failures, the measurement harness — is in diff --git a/web/app/_components/direction-switcher.tsx b/web/app/_components/direction-switcher.tsx deleted file mode 100644 index eb2f85b..0000000 --- a/web/app/_components/direction-switcher.tsx +++ /dev/null @@ -1,58 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { DIRECTIONS } from '../_directions/registry'; - -/* A fixed pill for moving between contenders without going back to the index. - * Arrow keys work too, so directions can be flicked through and compared from - * the same scroll position rather than judged one at a time. - * - * Only ever rendered under /explore, so it cannot leak onto the live homepage. - */ - -export function DirectionSwitcher({ current }: { current: string }) { - const router = useRouter(); - const index = DIRECTIONS.findIndex((direction) => direction.slug === current); - - useEffect(() => { - const move = (step: number) => { - const next = DIRECTIONS[(index + step + DIRECTIONS.length) % DIRECTIONS.length]; - router.push(`/explore/${next.slug}`, { scroll: false }); - }; - - const onKey = (event: KeyboardEvent) => { - const target = event.target as HTMLElement | null; - if (target?.matches('input, textarea, [contenteditable="true"]')) return; - if (event.key === 'ArrowRight') move(1); - if (event.key === 'ArrowLeft') move(-1); - }; - - window.addEventListener('keydown', onKey); - return () => window.removeEventListener('keydown', onKey); - }, [index, router]); - - if (index < 0) return null; - const previous = DIRECTIONS[(index - 1 + DIRECTIONS.length) % DIRECTIONS.length]; - const next = DIRECTIONS[(index + 1) % DIRECTIONS.length]; - - return ( - - ); -} diff --git a/web/app/_directions/concepts.tsx b/web/app/_directions/concepts.tsx deleted file mode 100644 index 01142db..0000000 --- a/web/app/_directions/concepts.tsx +++ /dev/null @@ -1,559 +0,0 @@ -'use client'; - -/* The three original landing concepts, kept as reference directions. - * - * Preserved verbatim in composition and copy — they are the record of what was - * tried before Craft, and the reason several of its decisions went the way they - * did. Only the plumbing changed: each concept is now its own entry in the - * directions registry rather than a `?variant=` switch on a separate /demo - * route, so there is one place to compare landing work instead of two. - */ -import { FormEvent, useState } from 'react'; - -type DemoStatus = 'idle' | 'loading' | 'ready' | 'unavailable'; - -interface Thumbnail { - url: string; - width?: number; - height?: number; -} - -interface DemoVideo { - id: string; - title: string; - channel: { id: string; name: string; url: string }; - thumbnails: Thumbnail[]; - durationText?: string; - viewCountText?: string; - publishedTimeText?: string; - url: string; -} - -interface TranscriptSegment { - startMs: number; - durationMs: number; - endMs: number; - text: string; -} - -interface DemoComment { - id: string; - author: { name: string }; - text: string; - publishedTimeText?: string; - likeCountText?: string; - isPinned: boolean; - isHearted: boolean; -} - -interface DemoResponse { - video: DemoVideo; - transcript: - | { - status: 'ready'; - track: { name: string; languageCode: string }; - segmentCount: number; - segments: TranscriptSegment[]; - } - | { status: 'unavailable' }; - comments: - | { status: 'ready'; totalCount?: number; comments: DemoComment[] } - | { status: 'unavailable' }; - quota: { limit: number; remaining: number; resetAt: string; repeated: boolean }; - partial: boolean; -} - -const demoApiBase = - process.env.NEXT_PUBLIC_PLATFORM_API_BASE_URL ?? - (process.env.NODE_ENV === 'production' - ? 'https://api.video2ctx.dev' - : 'http://localhost:8787'); - -function ConceptShell({ hero }: { hero: 'orbit' | 'field' | 'engine' }) { - const [url, setUrl] = useState(''); - const [state, setState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); - const [result, setResult] = useState(null); - const [error, setError] = useState(''); - - const inspectVideo = async (event: FormEvent) => { - event.preventDefault(); - setState('loading'); - setResult(null); - setError(''); - - try { - const response = await fetch(`${demoApiBase}/v1/demo/youtube/inspect`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url }), - }); - const payload = (await response.json()) as DemoResponse & { - error?: { message?: string; details?: { resetAt?: string } }; - }; - - if (!response.ok) { - const reset = payload.error?.details?.resetAt; - const retry = reset - ? ` You can inspect another video after ${formatReset(reset)}.` - : ''; - throw new Error(`${payload.error?.message ?? 'The video could not be inspected.'}${retry}`); - } - - setResult(payload); - setState('success'); - window.setTimeout(() => { - document.getElementById('inspection-result')?.scrollIntoView({ - behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches - ? 'auto' - : 'smooth', - block: 'start', - }); - }, 50); - } catch (cause) { - setError( - cause instanceof TypeError - ? 'The inspection service could not be reached. Please try again.' - : cause instanceof Error - ? cause.message - : 'The video could not be inspected.', - ); - setState('error'); - } - }; - - const mapStatus = (section: 'video' | 'transcript' | 'comments'): DemoStatus => { - if (state === 'loading') return 'loading'; - if (!result) return 'idle'; - if (section === 'video') return 'ready'; - return result[section].status; - }; - - const heroProps = { - url, - state, - error, - mapStatus, - onUrlChange: setUrl, - onSubmit: inspectVideo, - }; - - return ( -
-
- - - video2ctx - -

Landing concept

- - Back to live site - -
- - {hero === 'orbit' ? : null} - {hero === 'field' ? : null} - {hero === 'engine' ? : null} - - {state === 'loading' ? : null} - {result ? : null} -
- ); -} - -export function OrbitConcept() { - return ; -} - -export function SignalFieldConcept() { - return ; -} - -export function ContextEngineConcept() { - return ; -} - -interface HeroProps { - url: string; - state: 'idle' | 'loading' | 'success' | 'error'; - error: string; - mapStatus: (section: 'video' | 'transcript' | 'comments') => DemoStatus; - onUrlChange: (value: string) => void; - onSubmit: (event: FormEvent) => Promise; -} - -function OrbitHero(props: HeroProps) { - return ( -
-
-

video2ctx

-

Connect agents to YouTube context.

-

- Paste a public video URL for metadata, timestamped transcripts, and audience comments in one request. -

- -
- -
- ); -} - -function SignalFieldHero(props: HeroProps) { - return ( -
- -
-

video2ctx

-

YouTube context for agents.

-

- Inspect a public video, then carry its exact moments and audience response into your agent. -

-
-
- -
-
- ); -} - -function ContextEngineHero(props: HeroProps) { - return ( -
-
- YouTube Agent API / Context engine -

video2ctx

-

A video in. Context out.

-

- Turn one public video into timestamped speech, audience response, and source identity. -

- -
- -
- ); -} - -function ContextEngineStage({ - state, - mapStatus, -}: { - state: HeroProps['state']; - mapStatus: HeroProps['mapStatus']; -}) { - return ( -