From 38b1f8267d445e94d3f7783b957256e013d92e7e Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Fri, 2 Oct 2026 14:15:50 +0200 Subject: [PATCH 1/4] feat(website): record the published demo loops, and check them in CI The loops live on R2, so nothing in the repository said which files were online, and nothing in CI would notice a page naming a loop nobody uploaded: the page would just show an empty box. - media/loops.json: every published file of the current cut, by size and SHA-256, and each loop's duration. - scripts/media/publish-loops.mjs: uploads a cut with the cf CLI and writes that record, plus the generated block in src/lib/demo-loop.ts (base URL, durations, publication date), so the code and the record cannot disagree. - scripts/media/encode-loops.sh: the encoder settings, out of a scratchpad. - scripts/check-loops.mjs, run by the docs workflow with --live: the names the pages use are all published, and every recorded file answers on the CDN at its recorded size. Paced to six requests per 0.75 s, under the zone's per-IP rate limit (the first, unpaced version tripped it). - media/README.md: how a new cut is made, and the Cloudflare set-up. --- .github/workflows/docs.yml | 8 + website/media/README.md | 35 +++ website/media/loops.json | 387 ++++++++++++++++++++++++ website/package.json | 3 +- website/scripts/check-loops.mjs | 85 ++++++ website/scripts/media/encode-loops.sh | 31 ++ website/scripts/media/publish-loops.mjs | 144 +++++++++ website/src/lib/demo-loop.ts | 30 +- 8 files changed, 721 insertions(+), 2 deletions(-) create mode 100644 website/media/README.md create mode 100644 website/media/loops.json create mode 100755 website/scripts/check-loops.mjs create mode 100755 website/scripts/media/encode-loops.sh create mode 100755 website/scripts/media/publish-loops.mjs diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index e8a87f80f..ff31102d3 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -76,6 +76,14 @@ jobs: - name: Check media budget working-directory: website run: npm run check:media + # The demo loops are served from R2 (media.getopenscreen.com), not git, so + # nothing above would notice a loop a page names that was never uploaded, + # or a cut folder holding the wrong files. media/loops.json records what + # scripts/media/publish-loops.mjs uploaded; this holds the code to it and + # asks the CDN for every recorded file, paced under the zone's rate limit. + - name: Check demo loops + working-directory: website + run: npm run check:loops -- --live # The recreation's data is generated from the app's own source and a # vendored project document. It is committed, so it can go stale in either # direction — a locale key moves, a timeline constant changes — and the diff --git a/website/media/README.md b/website/media/README.md new file mode 100644 index 000000000..cb864848d --- /dev/null +++ b/website/media/README.md @@ -0,0 +1,35 @@ +# Demo loops + +The silent loops on the homepage and the feature pages are served from +Cloudflare R2 at `media.getopenscreen.com/loops//`, not from git: +`static/` is committed for good, and a cut of eighteen loops in two codecs and +two sizes is about 75 MB. This folder holds the record of them instead. + +`loops.json` lists every published file with its size and SHA-256, and each +loop's duration. CI (`npm run check:loops -- --live`) holds the code and the +CDN to it. + +## Publishing a new cut + +1. **Masters.** One 1920x1080, 60 fps, silent `.mp4` per loop, named after + `LOOP_NAMES` in `src/lib/demo-loop.ts`. They are built, and archived to R2 + under `masters/` (off the public domain), by the openscreen-demo-production + repository: `v2-launch/README.md`. +2. **Encode.** `FFMPEG= scripts/media/encode-loops.sh ` +3. **Publish, under a new folder name.** `node scripts/media/publish-loops.mjs --cut --dir ` + uploads, rewrites `loops.json` and the generated block in `demo-loop.ts` + (base URL, durations, publication date). Never reuse a folder: the edge + caches each file for a year. +4. `npx biome format --write src/lib/demo-loop.ts && npm run check:loops -- --live`, then commit both records. + +Upload before anything (a page, a preview) requests the new folder: the zone +caches a 404 for an hour. If it happens, purge those URLs with +`cf cache purge -z --body '{"files":[...]}' --force`. + +## On the Cloudflare side + +Bucket `openscreen-media`, Standard storage class (the free tier's), `r2.dev` +off. On the `getopenscreen.com` zone: a cache rule keeping the loops at the +edge for a year with query strings out of the cache key, a WAF rule that serves +only GET/HEAD under `/loops/` with no query string, a 120 requests / 10 s per-IP +rate limit, and billing alerts at any spend and at half of the free tier. diff --git a/website/media/loops.json b/website/media/loops.json new file mode 100644 index 000000000..91ae8dd8a --- /dev/null +++ b/website/media/loops.json @@ -0,0 +1,387 @@ +{ + "cut": "2026-10b", + "base": "https://media.getopenscreen.com/loops/2026-10b", + "published": "2026-10-02", + "durations": { + "3d-camera": 5.4, + "3d-cursors": 6.97, + "animated-backgrounds": 7.5, + "ask-the-agent": 7.6, + "automatic-subtitles": 4.8, + "background-picker": 11.2, + "beautiful-by-default": 11, + "camera-blur": 6.6, + "caption-styles": 6.2, + "classic-zoom": 5.2, + "device-frames": 4.85, + "edit-by-transcript": 4.5, + "edit-like-a-doc": 4.8, + "every-format": 5.4, + "every-layout": 8.2, + "sensitive-data-mask": 8.7, + "simpler-editor": 4.4, + "tighten-pauses": 7.4 + }, + "files": { + "3d-camera-1080-h264.mp4": { + "bytes": 1767703, + "sha256": "5b330faf534a153c468bb70bd1f6b54ec677d786b38dd132e376819d2717b3ad" + }, + "3d-camera-1080-hevc.mp4": { + "bytes": 1754416, + "sha256": "999de07044dd15b42b74c76f8ff2639b5d1e3fa93c79d7e0d1716c54ee27af5c" + }, + "3d-camera-720-h264.mp4": { + "bytes": 967086, + "sha256": "f6c4c83c62d3920bc22368f0e16930a7654a0313ec547ac378db4e30fd63e936" + }, + "3d-camera-720-hevc.mp4": { + "bytes": 907870, + "sha256": "7a29212976865f35b1e468b86fe05b84d6d238cc90a183144c687a5ff55ff48f" + }, + "3d-camera-poster.webp": { + "bytes": 32508, + "sha256": "97ef00e0fda6d558d7e45df58056d5b266ee028e9c446061be491646e2c073be" + }, + "3d-cursors-1080-h264.mp4": { + "bytes": 2202461, + "sha256": "e17d0a645adbe0a7d692bc4928612757733b95b3ccfd41da9aaa5705e914c9f2" + }, + "3d-cursors-1080-hevc.mp4": { + "bytes": 2139119, + "sha256": "4e31209e74af472bb3fb45368ee9fc25ec772b9230034dfa402f7c2eb5de147b" + }, + "3d-cursors-720-h264.mp4": { + "bytes": 1253111, + "sha256": "c02cbd1992a9a5851e44e2afc602d0be217570c81dc4ff6809362d19ead81c20" + }, + "3d-cursors-720-hevc.mp4": { + "bytes": 1149727, + "sha256": "d0cdc4ed29fe2919cb4d3adcdd09bf401bf6d51112751d5327233b4e9825abe0" + }, + "3d-cursors-poster.webp": { + "bytes": 24904, + "sha256": "c65aa539a60a60dc168612644d1afca1da01e207e74393269d3e6f79290d6951" + }, + "animated-backgrounds-1080-h264.mp4": { + "bytes": 956441, + "sha256": "b387a149759885a6f0f2239adff98afdad877a15de0059d2a58b9af602d98c84" + }, + "animated-backgrounds-1080-hevc.mp4": { + "bytes": 705583, + "sha256": "60dc39ae5763b7e4961b186bee9de3ac6e7c606015aa1f8ef5c649b8916cc406" + }, + "animated-backgrounds-720-h264.mp4": { + "bytes": 463199, + "sha256": "8ab450bc878d681510a3576fac8625d76c2c8bd6b98f8273e15bff7fbdcdac4a" + }, + "animated-backgrounds-720-hevc.mp4": { + "bytes": 374995, + "sha256": "ccce45896a10ecbbafb16526ced818d3c1c9725980d40aa49bab53d00ba5a8c1" + }, + "animated-backgrounds-poster.webp": { + "bytes": 34426, + "sha256": "03d8764707d2201e5653c1c8ecbd360d90fe8ac3a8247eff887924c416993ca9" + }, + "ask-the-agent-1080-h264.mp4": { + "bytes": 1714369, + "sha256": "701727630a942c0a6c681bb539016f7b70320b5108b8a8acf6950279d6a9bdb5" + }, + "ask-the-agent-1080-hevc.mp4": { + "bytes": 1692213, + "sha256": "54c0656dd6288b4f08b9159eb6059606b9065cc2d5e3be29155700088ab95f1f" + }, + "ask-the-agent-720-h264.mp4": { + "bytes": 949454, + "sha256": "e40e61e8cb4f5bc87c509deec4eef3e33670839923c5aefee9e577bd75da378e" + }, + "ask-the-agent-720-hevc.mp4": { + "bytes": 897844, + "sha256": "6710d49a91c39cd9c4d01accc4dc5338ea8562e72b0734b3b807bcbf266f7076" + }, + "ask-the-agent-poster.webp": { + "bytes": 30032, + "sha256": "28d99ad7569633275382f0d761568963c1fca3fef1f9425daeb9fbe570fa9e86" + }, + "automatic-subtitles-1080-h264.mp4": { + "bytes": 2142748, + "sha256": "40ceebe6be7718656e35074a47dc51234087087018e47fbecb08b994095fcc52" + }, + "automatic-subtitles-1080-hevc.mp4": { + "bytes": 1989676, + "sha256": "bf91c8e250c20ab24237cca41fcaf9fa5e6f94834e44c2ed8185dd81fd1f381e" + }, + "automatic-subtitles-720-h264.mp4": { + "bytes": 1134794, + "sha256": "faa91a7d894abab0faf5318adcacd24d3ec8a7d123e5e3cf2ab3e3d80c71992a" + }, + "automatic-subtitles-720-hevc.mp4": { + "bytes": 1015296, + "sha256": "62ec0d8454b905a341c969bb4e1eac6333987f2d5ed2d29a34cd7b5c6f2e1b96" + }, + "automatic-subtitles-poster.webp": { + "bytes": 34214, + "sha256": "1e1e11a2204e97eb81f2d1b533407b9873cb0361e1e4a4a463d6b3951c0eba41" + }, + "background-picker-1080-h264.mp4": { + "bytes": 1376180, + "sha256": "f38171d2e28e4819dba896d748c8c72bb205b55978b8e51d822761383f0621ef" + }, + "background-picker-1080-hevc.mp4": { + "bytes": 1277370, + "sha256": "2c82d89a0b4cdf78eb43d51694bc41fdaa136bd935ace0dd0d6b9982304dd397" + }, + "background-picker-720-h264.mp4": { + "bytes": 766557, + "sha256": "dfa056a12ba806916e6ade94a5bc75b192b3cd65a96e07a54fdb698c0ef32b77" + }, + "background-picker-720-hevc.mp4": { + "bytes": 729874, + "sha256": "4d71f9c2d69e935c204598b73a8969362e341291830289f822e954c881567599" + }, + "background-picker-poster.webp": { + "bytes": 33340, + "sha256": "89f150803c565ad40a4044701332266ae668b3fbb0e545aaf4407f3fe6a0744c" + }, + "beautiful-by-default-1080-h264.mp4": { + "bytes": 3518766, + "sha256": "44e7561faf6b7b9f3f191a09f98038dfc631e87aec4c44ba7087a1230cf7e228" + }, + "beautiful-by-default-1080-hevc.mp4": { + "bytes": 2892107, + "sha256": "b2ead32e9f85e2c5fb38677928ca5abcaf90df7adc5946f03a947b3d05c818b0" + }, + "beautiful-by-default-720-h264.mp4": { + "bytes": 1755077, + "sha256": "c2ca072e93ed9ff252da47579324543f227ddaf45a88e4920d93c1741b2976e4" + }, + "beautiful-by-default-720-hevc.mp4": { + "bytes": 1431358, + "sha256": "97a8806161ca65aa21470edbd8cc3c5fa889c2bc50b559ec295e1f11b194c1bd" + }, + "beautiful-by-default-poster.webp": { + "bytes": 34328, + "sha256": "4a4eed4542e6baa58ad6f214b55e727f818de9d95a91618e6bb25043fb906da2" + }, + "camera-blur-1080-h264.mp4": { + "bytes": 1511697, + "sha256": "4cd0ddb92ce7a67a865f86a0d8b7744e5a39fd2742ead91dfd6c677dd6a5fe61" + }, + "camera-blur-1080-hevc.mp4": { + "bytes": 1551604, + "sha256": "eb3d2c2ae3907eeef0cac12f1b02f23763c5d46407adbe39ef2b534f5035131d" + }, + "camera-blur-720-h264.mp4": { + "bytes": 910763, + "sha256": "57905e7f82ab752ebcbb72d9a98e1e4d46c0a5c5106cc458777174881cef4df7" + }, + "camera-blur-720-hevc.mp4": { + "bytes": 880967, + "sha256": "0a36d59d58ef5b570ada3549083418afdca40f708b5f208fd3592f598f556b6a" + }, + "camera-blur-poster.webp": { + "bytes": 35672, + "sha256": "687f695e20912ef481203ae1addbf172dcb762e1386e48259dda620545db9c95" + }, + "caption-styles-1080-h264.mp4": { + "bytes": 795327, + "sha256": "4431849fdeb50d8569a1e608c8e6042bd340e8ffa0ea875f97ee2693e1f710da" + }, + "caption-styles-1080-hevc.mp4": { + "bytes": 818827, + "sha256": "a3712ec4851970e2671c670e6c00e8dc851bcf4f44d41cc477b99729d59c0c80" + }, + "caption-styles-720-h264.mp4": { + "bytes": 457326, + "sha256": "7343a48b9630e3de66d81f9852016d7f3fc6554674ad103efadb0be40c662166" + }, + "caption-styles-720-hevc.mp4": { + "bytes": 432580, + "sha256": "94a4012e850d0abda99cfdae48d32bd895c4fe17a3ed054bfea4e1c443bb8075" + }, + "caption-styles-poster.webp": { + "bytes": 36770, + "sha256": "36457cf6cdd52272be16ca6f28c639182eb34c68f77d305a2305138d4222bf3d" + }, + "classic-zoom-1080-h264.mp4": { + "bytes": 1310822, + "sha256": "806772c665724757e5db9ca9bc3e0b6bd227da468a606eea3df2ad055b6ba7b8" + }, + "classic-zoom-1080-hevc.mp4": { + "bytes": 1286250, + "sha256": "0d5802e65e1ea8c7e45227dc6875cfd03863e45db34b4894fab331bd98b4c16b" + }, + "classic-zoom-720-h264.mp4": { + "bytes": 757148, + "sha256": "1e1bfee7e2a429fc73e32b43545d75e0855d2a25230676190016fcd864980458" + }, + "classic-zoom-720-hevc.mp4": { + "bytes": 729250, + "sha256": "0dd337e10a3b37a6c0d2c7650e6b68e506321c04fb07eab0f211a082bedecd71" + }, + "classic-zoom-poster.webp": { + "bytes": 22270, + "sha256": "f5b211c8c964758cc4c86fb9f8f3804da100dd1565a556cc4c198d7fa0c5dbb2" + }, + "device-frames-1080-h264.mp4": { + "bytes": 397773, + "sha256": "c94e3555e51d24f2936ad33709903ecd06b354531cc02bb6412863fed2991929" + }, + "device-frames-1080-hevc.mp4": { + "bytes": 351049, + "sha256": "1da8182a417d2a908ab2a63489ba9344a375af7b013f6c55dc72d48a57ef9388" + }, + "device-frames-720-h264.mp4": { + "bytes": 220357, + "sha256": "b08c2695828b113d4f49e7dc7d7993a34114db49a2a7df133cbd11ef791ccdb1" + }, + "device-frames-720-hevc.mp4": { + "bytes": 200673, + "sha256": "1c54a95d2df6bc41f72a429ec8242d0eb8a6096e8e63825e6a4be9b172444c9b" + }, + "device-frames-poster.webp": { + "bytes": 24592, + "sha256": "fa8382ac896f32c285280ef815869d5a74e04a2b487001438d3ca65baa0c661b" + }, + "edit-by-transcript-1080-h264.mp4": { + "bytes": 1109652, + "sha256": "55af4a20bd30ad09dfda4570c7a3463731b71d6ba93e398c83b3e8b6f3531804" + }, + "edit-by-transcript-1080-hevc.mp4": { + "bytes": 1029463, + "sha256": "6fd744574a198736f161af631739c07e5de9ee5258c63aec722c4e608cbe68f8" + }, + "edit-by-transcript-720-h264.mp4": { + "bytes": 622225, + "sha256": "5f99b259a7abb33581f9f0c7a0345b29dd17cf8c5349887292eec2dc0f8f95ea" + }, + "edit-by-transcript-720-hevc.mp4": { + "bytes": 594773, + "sha256": "bb95dbbd3ff2d7e2ab12fc04e59ac30d2e0fd809e08cf3d77401338e57aff7e1" + }, + "edit-by-transcript-poster.webp": { + "bytes": 42966, + "sha256": "e1deb953b3df1e86e350f6008db65ac750c25f828f6b7189b23a8a6f97afc57a" + }, + "edit-like-a-doc-1080-h264.mp4": { + "bytes": 792475, + "sha256": "bce980471c41df9037323cf5f46438a5b4d416fbc5314a1bb7e6296e9143f457" + }, + "edit-like-a-doc-1080-hevc.mp4": { + "bytes": 758618, + "sha256": "c820ed034efbb7231045d64c61ff6f7b1b6cf9bd9a7aa39a0ef8c2f813166594" + }, + "edit-like-a-doc-720-h264.mp4": { + "bytes": 468874, + "sha256": "330ed51a036e23fb036efddb27fd7ad93e59f271a1be3853b7d7318c4390d7f7" + }, + "edit-like-a-doc-720-hevc.mp4": { + "bytes": 440162, + "sha256": "ce20e8d0e1ac47bde8807f89796aec4d763dd609b91aa0812917bc6dca1f1e00" + }, + "edit-like-a-doc-poster.webp": { + "bytes": 42652, + "sha256": "7010e2ec1c2ff529b168e49766620148cabd5c33cdd232f609314284eae81131" + }, + "every-format-1080-h264.mp4": { + "bytes": 1072179, + "sha256": "127b71417245b666e55004e6db34e50452487b800c738ac5e09d1bf1c0746d6e" + }, + "every-format-1080-hevc.mp4": { + "bytes": 1036212, + "sha256": "cf6439e6162cf3002d74a945a93478b5c0dd1bd3c752f2e4177bd7aca7e05f3b" + }, + "every-format-720-h264.mp4": { + "bytes": 595493, + "sha256": "ea9e90bd673f10a9755fb7f763727488a92506e46f0a2a0d44c828acd3b57864" + }, + "every-format-720-hevc.mp4": { + "bytes": 560985, + "sha256": "be57b4181ad59b5c928a73baddd58bc3e8857b72ef05bb5edff7518d3d494595" + }, + "every-format-poster.webp": { + "bytes": 31018, + "sha256": "105f33c2818c8b1b0be29b546deff0c9198050b94153dbfa62ba209a7332a599" + }, + "every-layout-1080-h264.mp4": { + "bytes": 1898864, + "sha256": "f36a458911840a1bd54dc782e28c74c377bbacbdf893a619f2a3a001632abfcf" + }, + "every-layout-1080-hevc.mp4": { + "bytes": 1365570, + "sha256": "a67c0ddce767dc5c9b621072c67d3444f3f98700a485287b24ec41de8072ef0a" + }, + "every-layout-720-h264.mp4": { + "bytes": 892156, + "sha256": "1ab810408a7961f0eb9a69b5ea295b6d0ccf0847e7b7089ba7f9c1b679d4c380" + }, + "every-layout-720-hevc.mp4": { + "bytes": 758734, + "sha256": "6d1c9afa46f8885fc93a9061137f343b53d1e3a126c7be9b44d85072e75fd0bf" + }, + "every-layout-poster.webp": { + "bytes": 36074, + "sha256": "138377a17fb081a1d1935ed6020a20b18a5d8b582312bbab2c0d8b7ff1040657" + }, + "sensitive-data-mask-1080-h264.mp4": { + "bytes": 793371, + "sha256": "262449a5d7005c42044567ccd29bd2b03ee956abd45084e239b68f7f300d5b1d" + }, + "sensitive-data-mask-1080-hevc.mp4": { + "bytes": 655548, + "sha256": "b7ebffa743288a6ac671b37e58b8ac2a5f74dd69545c04e5980f516598d3002a" + }, + "sensitive-data-mask-720-h264.mp4": { + "bytes": 399485, + "sha256": "fcebf6008447b171e580e338c95ca6eadfe414561257597a85c9e5f9d4796932" + }, + "sensitive-data-mask-720-hevc.mp4": { + "bytes": 372682, + "sha256": "d67a4624964a83377ce3e97d088fb5bf6bbf553719549f8c1ebd6b593b551893" + }, + "sensitive-data-mask-poster.webp": { + "bytes": 13346, + "sha256": "008774a638caf72190d7f42e2a4b5ab3b6a998578753329dd2cf3db65559e228" + }, + "simpler-editor-1080-h264.mp4": { + "bytes": 1049742, + "sha256": "3d090acbc6cc0714c8bab0c17cd18ac6f4cbcea0a944bfa4222becade0f91a6e" + }, + "simpler-editor-1080-hevc.mp4": { + "bytes": 997740, + "sha256": "5e08d3024d039794147052e4778905a956883459eb8745709e4d6f2f054b03d7" + }, + "simpler-editor-720-h264.mp4": { + "bytes": 566323, + "sha256": "19fb16c3502e107fb7ef09ef0767bd808ae04e0a3bb67217d684a899ba907f46" + }, + "simpler-editor-720-hevc.mp4": { + "bytes": 547540, + "sha256": "ad8696239a3c8d2284f64265c095ced77b42fcafe21891549ac79036cd89032b" + }, + "simpler-editor-poster.webp": { + "bytes": 39050, + "sha256": "9e7a49608e705c6816b0e65066f28e562b0836260e05d1009ac0652182bd3582" + }, + "tighten-pauses-1080-h264.mp4": { + "bytes": 1394762, + "sha256": "45edb46e285d1d444e03544f6245d1f9bfc04aa52764bd0a67097df42501630a" + }, + "tighten-pauses-1080-hevc.mp4": { + "bytes": 1297946, + "sha256": "080256b109744a38881e4afc6d614d657bd03fb26fe41b0700ab5ce5ce6d472e" + }, + "tighten-pauses-720-h264.mp4": { + "bytes": 748982, + "sha256": "4eeda67ec2248589f557fc2a59c0a817deea17685923b6b810604882b6e7f349" + }, + "tighten-pauses-720-hevc.mp4": { + "bytes": 726958, + "sha256": "51a408b94fb2096fc2700433185707b46971701fca6bb3b85363c0fecdc82200" + }, + "tighten-pauses-poster.webp": { + "bytes": 36764, + "sha256": "6b05a3a618d32cc4923dac91c37c77f33b634a6042d0332407fcc9bf1ecdbe00" + } + } +} diff --git a/website/package.json b/website/package.json index 7126b06f8..8e0a4fccb 100644 --- a/website/package.json +++ b/website/package.json @@ -13,7 +13,8 @@ "check:recreation": "node scripts/gen-recreation.mjs --check", "i18n:check": "node scripts/i18n.mjs check", "i18n:sync": "node scripts/i18n.mjs sync", - "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test \"src/**/*.test.ts\"" + "test": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON --test \"src/**/*.test.ts\"", + "check:loops": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON scripts/check-loops.mjs" }, "dependencies": { "@docusaurus/core": "~3.10.1", diff --git a/website/scripts/check-loops.mjs b/website/scripts/check-loops.mjs new file mode 100755 index 000000000..57e7d0bae --- /dev/null +++ b/website/scripts/check-loops.mjs @@ -0,0 +1,85 @@ +#!/usr/bin/env node +/** + * Holds the demo loops the site uses to what is actually published. + * + * node scripts/check-loops.mjs the record agrees with the code + * node scripts/check-loops.mjs --live ... and every file answers, at its size + * + * The loops are served from R2, not from git, so nothing else in CI would + * notice a loop the pages name but nobody uploaded, or a cut folder the code + * points at that holds the wrong files: the page would just show an empty box. + * + * Offline it compares src/lib/demo-loop.ts (the names the pages use, the base + * URL, the durations the VideoObject markup declares) with media/loops.json + * (what scripts/media/publish-loops.mjs uploaded). --live then asks the CDN + * for every file in the record, a HEAD each, and compares the length. + */ + +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { LOOP_BASE, LOOP_DURATIONS, LOOP_NAMES } from "../src/lib/demo-loop.ts"; + +const ROOT = resolve(import.meta.dirname, ".."); +const live = process.argv.includes("--live"); +const manifest = JSON.parse(readFileSync(resolve(ROOT, "media/loops.json"), "utf8")); + +const problems = []; +const VARIANTS = ["1080-hevc.mp4", "1080-h264.mp4", "720-hevc.mp4", "720-h264.mp4", "poster.webp"]; + +if (manifest.base !== LOOP_BASE) { + problems.push(`LOOP_BASE is ${LOOP_BASE}, media/loops.json records ${manifest.base}`); +} +for (const name of LOOP_NAMES) { + for (const v of VARIANTS) { + if (!manifest.files[`${name}-${v}`]) + problems.push(`${name}-${v}: used by the site, not published`); + } + if (LOOP_DURATIONS[name] !== manifest.durations[name]) { + problems.push( + `${name}: duration ${LOOP_DURATIONS[name]} in demo-loop.ts, ${manifest.durations[name]} in media/loops.json`, + ); + } +} +const used = new Set(LOOP_NAMES); +const unused = Object.keys(manifest.durations).filter((n) => !used.has(n)); + +if (live && problems.length === 0) { + const entries = Object.entries(manifest.files); + // Paced, not just batched: the zone blocks one address for 10 s past 120 + // requests in 10 s, and a CI runner is one address. Six requests every + // 0.75 s keeps under 80 per window however many loops there are. + for (let i = 0; i < entries.length; i += 6) { + if (i > 0) await new Promise((r) => setTimeout(r, 750)); + await Promise.all( + entries.slice(i, i + 6).map(async ([name, { bytes }]) => { + const url = `${manifest.base}/${name}`; + let res; + for (let attempt = 0; attempt < 2 && !res?.ok; attempt++) { + try { + res = await fetch(url, { method: "HEAD" }); + } catch (err) { + if (attempt === 1) problems.push(`${url}: ${err.message}`); + } + } + if (!res) return; + const length = Number(res.headers.get("content-length")); + if (!res.ok) problems.push(`${url}: HTTP ${res.status}`); + else if (length !== bytes) problems.push(`${url}: ${length} B served, ${bytes} B recorded`); + }), + ); + } +} + +if (problems.length) { + console.error(`demo loops: ${problems.length} problem(s)\n ${problems.join("\n ")}`); + console.error( + "Publish with scripts/media/publish-loops.mjs (it writes both records), or fix the name in the page.", + ); + process.exit(1); +} +const files = Object.keys(manifest.files).length; +console.log( + `demo loops ok — ${LOOP_NAMES.length} loops, ${files} files in ${manifest.cut}${live ? ", all live at their recorded size" : ""}` + + (unused.length ? ` (published but unused: ${unused.join(", ")})` : ""), +); diff --git a/website/scripts/media/encode-loops.sh b/website/scripts/media/encode-loops.sh new file mode 100755 index 000000000..49b86dc98 --- /dev/null +++ b/website/scripts/media/encode-loops.sh @@ -0,0 +1,31 @@ +#!/bin/zsh +# Web encodes of the demo loops, for scripts/media/publish-loops.mjs. +# +# scripts/media/encode-loops.sh +# +# For every .mp4 master (1920x1080, 60 fps, silent): HEVC (tagged hvc1, so +# Safari plays it) and H.264, each at 1080p60 and 720p60, no audio track +# (WebKit only autoplays without a gesture when there is none), faststart, and a +# WebP poster of frame 0, which is the frame the loop starts on. +# +# Settings measured with VMAF against the masters: x265 crf 25 slow lands +# around 94-98, 23% lighter than x264 at equal quality. Needs an ffmpeg built +# with libx264, libx265 and libwebp (FFMPEG=..., e.g. `npm i ffmpeg-static`); +# the LGPL build under crates/thirdparty has neither encoder. +# A master whose poster already exists in is skipped. +set -e +F=${FFMPEG:-ffmpeg} +S=${1:?masters dir}; O=${2:?out dir}; mkdir -p $O +for src in $S/*.mp4; do + slug=${src:t:r} + [[ -f $O/$slug-poster.webp ]] && continue + for h in 1080 720; do + vf="scale=-2:${h}:flags=lanczos" + $F -v error -y -i $src -an -vf $vf -c:v libx265 -crf 25 -preset slow -tag:v hvc1 \ + -x265-params log-level=error:keyint=120:min-keyint=120 -pix_fmt yuv420p -movflags +faststart $O/$slug-$h-hevc.mp4 + $F -v error -y -i $src -an -vf $vf -c:v libx264 -crf 23 -preset veryslow -g 120 -profile:v high -level 4.2 \ + -pix_fmt yuv420p -movflags +faststart $O/$slug-$h-h264.mp4 + done + $F -v error -y -i $src -frames:v 1 -vf "scale=1280:-2:flags=lanczos" -c:v libwebp -quality 80 $O/$slug-poster.webp + echo "$slug done" +done diff --git a/website/scripts/media/publish-loops.mjs b/website/scripts/media/publish-loops.mjs new file mode 100755 index 000000000..ea031c3df --- /dev/null +++ b/website/scripts/media/publish-loops.mjs @@ -0,0 +1,144 @@ +#!/usr/bin/env node +/** + * Publishes a cut of the demo loops to R2 and records it. + * + * node scripts/media/publish-loops.mjs --cut 2026-10b --dir [--no-upload] + * + * holds what scripts/media/encode-loops.sh writes: for every loop, + * -{1080,720}-{hevc,h264}.mp4 and -poster.webp. + * + * The bytes never enter git (scripts/check-media-budget.mjs says why). What + * does is the record of them, written here: + * + * media/loops.json every published file's size and SHA-256, and each + * loop's duration: scripts/check-loops.mjs holds the + * live files to it in CI. + * src/lib/demo-loop.ts the block between the @generated markers: the cut's + * base URL and the durations the VideoObject markup + * declares. + * + * A cut is a folder and is never rewritten: the edge caches it for a year, and + * an earlier commit's pages still point at the earlier folder. A new encode is + * a new --cut. Upload BEFORE anything points at the folder: the zone caches a + * 404 for an hour. + * + * Needs the `cf` CLI logged in to the account that owns getopenscreen.com, and + * ffprobe (FFPROBE, or on PATH) for the durations. + */ + +import { execFileSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { join, resolve } from "node:path"; +import { parseArgs } from "node:util"; + +const ROOT = resolve(import.meta.dirname, "../.."); +const BUCKET = "openscreen-media"; +const ORIGIN = "https://media.getopenscreen.com"; + +const { values: opts } = parseArgs({ + options: { + cut: { type: "string" }, + dir: { type: "string" }, + "no-upload": { type: "boolean", default: false }, + }, +}); +if (!opts.cut || !opts.dir) { + console.error("usage: publish-loops.mjs --cut --dir [--no-upload]"); + process.exit(2); +} +if (!/^[0-9a-z-]+$/.test(opts.cut)) + throw new Error(`--cut must be a plain folder name, got ${opts.cut}`); + +const dir = resolve(opts.dir); +const names = readdirSync(dir) + .filter((f) => /\.(mp4|webp)$/.test(f)) + .sort(); +const FILE = /^(.+)-(?:(?:1080|720)-(?:hevc|h264)\.mp4|poster\.webp)$/; + +const files = {}; +const durations = {}; +for (const name of names) { + const m = FILE.exec(name); + if (!m) + throw new Error( + `${name}: not a loop file (-<1080|720>-.mp4 or -poster.webp)`, + ); + const path = join(dir, name); + files[name] = { + bytes: statSync(path).size, + sha256: createHash("sha256").update(readFileSync(path)).digest("hex"), + }; + if (name.endsWith("-1080-h264.mp4")) { + const out = execFileSync( + process.env.FFPROBE ?? "ffprobe", + ["-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", path], + { encoding: "utf8" }, + ); + durations[m[1]] = Math.round(Number.parseFloat(out) * 100) / 100; + } +} + +if (!opts["no-upload"]) { + for (const name of names) { + execFileSync( + "cf", + [ + "r2", + "objects", + "put", + `loops/${opts.cut}/${name}`, + "--bucket-name", + BUCKET, + "--file", + join(dir, name), + "--content-type", + name.endsWith(".webp") ? "image/webp" : "video/mp4", + "-q", + ], + { stdio: ["ignore", "ignore", "inherit"] }, + ); + console.log("uploaded", name); + } +} + +const base = `${ORIGIN}/loops/${opts.cut}`; +// The day the cut went up; re-running on the same cut keeps it. +const manifestPath = join(ROOT, "media/loops.json"); +const previous = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : null; +const published = + previous?.cut === opts.cut ? previous.published : new Date().toISOString().slice(0, 10); +writeFileSync( + manifestPath, + `${JSON.stringify({ cut: opts.cut, base, published, durations, files }, null, "\t")}\n`, +); + +// The generated block in demo-loop.ts: everything between the two markers is +// rewritten, nothing outside them is touched. +const libPath = join(ROOT, "src/lib/demo-loop.ts"); +const lib = readFileSync(libPath, "utf8"); +const START = "// @generated by scripts/media/publish-loops.mjs: start"; +const END = "// @generated by scripts/media/publish-loops.mjs: end"; +const a = lib.indexOf(START); +const b = lib.indexOf(END); +if (a < 0 || b < a) throw new Error(`${libPath}: the @generated markers are missing`); +const block = [ + START, + `export const LOOP_BASE = "${base}";`, + "", + "/** Seconds, from the 1080p H.264 file of each loop. */", + "export const LOOP_DURATIONS: Record = {", + ...Object.entries(durations).map(([k, v]) => `\t"${k}": ${v},`), + "};", + "", + "/** When this cut was published: the VideoObject uploadDate. */", + `export const LOOP_PUBLISHED = "${published}";`, +].join("\n"); +writeFileSync(libPath, lib.slice(0, a) + block + "\n" + lib.slice(b)); + +console.log( + `${names.length} files, ${Object.keys(durations).length} loops -> media/loops.json, src/lib/demo-loop.ts`, +); +console.log( + "Run npx biome format --write src/lib/demo-loop.ts, then npm run check:loops -- --live.", +); diff --git a/website/src/lib/demo-loop.ts b/website/src/lib/demo-loop.ts index 834c91477..f5e01b43c 100644 --- a/website/src/lib/demo-loop.ts +++ b/website/src/lib/demo-loop.ts @@ -6,11 +6,39 @@ * says why), and eighteen 1080p60 clips in two codecs and two sizes are about * 75 MB per cut. The bucket holds them outside the repository; the folder name * is the cut, so a re-encode goes to a new folder and the old files can keep - * their `immutable` cache header. + * their year-long cache. media/loops.json records every published file by size + * and hash, and the block below is written by the same publish script. */ +// @generated by scripts/media/publish-loops.mjs: start export const LOOP_BASE = "https://media.getopenscreen.com/loops/2026-10b"; +/** Seconds, from the 1080p H.264 file of each loop. */ +export const LOOP_DURATIONS: Record = { + "3d-camera": 5.4, + "3d-cursors": 6.97, + "animated-backgrounds": 7.5, + "ask-the-agent": 7.6, + "automatic-subtitles": 4.8, + "background-picker": 11.2, + "beautiful-by-default": 11, + "camera-blur": 6.6, + "caption-styles": 6.2, + "classic-zoom": 5.2, + "device-frames": 4.85, + "edit-by-transcript": 4.5, + "edit-like-a-doc": 4.8, + "every-format": 5.4, + "every-layout": 8.2, + "sensitive-data-mask": 8.7, + "simpler-editor": 4.4, + "tighten-pauses": 7.4, +}; + +/** When this cut was published: the VideoObject uploadDate. */ +export const LOOP_PUBLISHED = "2026-10-02"; +// @generated by scripts/media/publish-loops.mjs: end + export const LOOP_NAMES = [ "classic-zoom", "3d-camera", From 2c70d720b28192dc8ae9c907b4ad800b1366d70c Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Fri, 2 Oct 2026 14:16:00 +0200 Subject: [PATCH 2/4] feat(website): declare each demo loop as a VideoObject where it is shown Every page that presents a loop now carries its schema.org VideoObject in the server HTML: a name and a description in the page's language (a new short title per loop, translated, and the existing screen-reader label), the poster as thumbnail, the 1080p H.264 file as content, the duration and the cut's publication date from the publish record. A homepage stage declares all of its tabs' loops, since each is one click away; nothing is declared on a page that does not show the loop, which is the line Google's structured-data policy draws. The comment that ruled VideoObject out was about the recreation's webcam fragment, which is still not declared; it now says so. --- website/i18n/code.source.json | 112 ++++++++++++--------- website/i18n/de/code.json | 72 +++++++++++++ website/i18n/es/code.json | 72 +++++++++++++ website/i18n/fr/code.json | 72 +++++++++++++ website/i18n/ja/code.json | 72 +++++++++++++ website/i18n/pt-BR/code.json | 72 +++++++++++++ website/i18n/zh-CN/code.json | 72 +++++++++++++ website/i18n/zh-TW/code.json | 72 +++++++++++++ website/src/components/DemoLoop/index.tsx | 30 +++++- website/src/components/DemoLoop/labels.ts | 117 ++++++++++++++++++++++ website/src/components/Films/index.tsx | 5 +- website/src/lib/structured-data.ts | 44 +++++++- 12 files changed, 758 insertions(+), 54 deletions(-) diff --git a/website/i18n/code.source.json b/website/i18n/code.source.json index 160006266..5d3e9b4fd 100644 --- a/website/i18n/code.source.json +++ b/website/i18n/code.source.json @@ -72,53 +72,6 @@ "home.install.note.windows": "Windows", "home.install.note.mac": "Mac", "home.install.note.linux": "Linux", - "editor.skipLink": "Skip the editor — go to downloads", - "editor.title": "Five things you will actually do", - "demoLoop.pause": "Pause video", - "demoLoop.play": "Play video", - "demoLoop.classic-zoom": "A task board recorded in OpenScreen. The view zooms in on a checklist item and a Ship button as they are clicked, then pulls back to the whole board.", - "demoLoop.3d-camera": "The same recording seen through the 3D camera, which tilts the screen and orbits around it while it follows the cursor.", - "demoLoop.3d-cursors": "One recorded pointer, redrawn in five cursor styles one after another while the recording keeps playing.", - "demoLoop.automatic-subtitles": "A narrated recording with captions under it, switching from English to French, Spanish and Korean.", - "demoLoop.caption-styles": "One caption shown in five styles: a plain caption, a bold yellow one, a handwritten one, a monospaced one, and one showing a single word at a time.", - "demoLoop.edit-by-transcript": "In the editor's transcript, a sentence is selected and deleted, and the matching cut appears on the timeline.", - "demoLoop.tighten-pauses": "Two clicks on the silence markers in the transcript, and both pauses are cut from the timeline.", - "demoLoop.background-picker": "The editor's background picker going through wallpapers, a gradient and a solid color, then turning on the animated Aurora background.", - "demoLoop.every-format": "The same recording exported in 16:9, then square, then vertical, with the framing following the cursor.", - "demoLoop.sensitive-data-mask": "A sign-in form being filled in. The email and password fields stay blurred while they are typed.", - "demoLoop.camera-blur": "A square webcam vignette over a screen recording. The view moves in on it while the room behind the speaker blurs, then pulls back.", - "demoLoop.every-layout": "One recording with a webcam, cycling through the layouts: picture in picture, side by side, stacked, full camera, then screen only.", - "demoLoop.device-frames": "The same playing recording framed in turn as a browser window, a phone, a laptop and a desktop monitor.", - "demoLoop.animated-backgrounds": "A recording on a plain background, then on a blurred photo background that moves slowly behind it.", - "demoLoop.beautiful-by-default": "A raw screen capture, then the same take as OpenScreen opens it, already zoomed, framed and with a restyled cursor, then in several looks.", - "demoLoop.simpler-editor": "The OpenScreen editor. One click on a background thumbnail restyles the whole video, then the side panel switches to the transcript.", - "demoLoop.edit-like-a-doc": "Text selected in the transcript is deleted like text in a document, and the video is cut to match.", - "demoLoop.ask-the-agent": "A request typed into the editor's chat panel, and the edits the agent makes appearing on the timeline.", - "showcase.record.kicker": "record", - "showcase.record.claim": "It records with the operating system, not around it.", - "showcase.record.body": "Pick a window or a display. macOS goes through ScreenCaptureKit, Windows through Windows Graphics Capture, Linux through PipeWire and the ScreenCast portal — on each, the capture path the system itself provides. The pointer is recorded as data rather than burned into the pixels, which is the only reason you could restyle it further up this page.", - "showcase.record.fact": "ScreenCaptureKit · Windows Graphics Capture · PipeWire · system audio without an extra driver", - "showcase.record.link.docs": "Screen recording docs", - "showcase.record.label": "A drawing of the recorder: two capture targets side by side, Display 1 selected and a window titled Terminal beside it, then the settings for the take — ScreenCaptureKit, system audio, 1920 × 1080 at 60 fps — a microphone and a system-audio toggle, and a Start recording button.", - "showcase.export.kicker": "export", - "showcase.export.claim": "Then it writes the file.", - "showcase.export.body": "MP4 from 720p up to source, at 24, 30 or 60, in H.264 — or a GIF. The encode runs on your machine and counts frames while it does. No queue, no account, no watermark, and the file is on disk when the bar fills.", - "showcase.export.fact": "H.264 · 24, 30, 60 fps · no watermark", - "showcase.export.link.docs": "Video export docs", - "showcase.export.label": "A drawing of the export panel: recording-1783066227227.mp4 going out as MP4 at 1080p and 60 fps, with the quality and frame rate rows above a progress bar 62 percent along reading frame 1 488 of 2 400, writing to the Movies folder.", - "showcase.captions.kicker": "captions", - "showcase.captions.claim": "Transcription runs on your machine.", - "showcase.captions.body": "whisper.cpp ships with the app, and the model downloads once on first use — after that it works with the network off. The audio never leaves the laptop, and what comes back is editable text: set the typeface, the size, the color and the position, then burn it into the render.", - "showcase.captions.fact": "whisper.cpp · 100 languages · offline after first run", - "showcase.captions.link.docs": "Captions and transcript docs", - "showcase.captions.link.feature": "How local captions compare", - "showcase.agent.kicker": "agent", - "showcase.agent.claim": "Describe the edit. The agent makes it.", - "showcase.agent.body": "The wizard further up this page places zooms by watching where your cursor went. The agent goes further: it reads the actual transcript and the actual timeline, so it answers with timecodes you can go and check — which spans it will cut, and how much that saves. Every edit it makes is an ordinary undoable one, and it needs a provider key you supply. Nothing runs until you connect one.", - "showcase.agent.fact": "bring your own key · off by default · every edit is undoable", - "showcase.agent.link.docs": "AI editing docs", - "showcase.agent.link.feature": "How automatic zooms work", - "showcase.title": "Recorder, captions, agent, encoder.", "films.default.kicker": "Out of the box", "films.default.title": "Good-looking from the first take.", "films.default.lead": "Every new recording opens {zoomed}, on a wallpaper, with padding, a shadow and rounded corners. Change any of it, or export it as it is.", @@ -168,6 +121,46 @@ "films.ship.blur": "{t} Smooth or mosaic, as a rectangle, an oval or a freehand shape, over an email, a key or a password.", "films.ship.blur.t": "Blur regions.", "films.label": "More of the app, in motion", + "editor.skipLink": "Skip the editor — go to downloads", + "editor.title": "Five things you will actually do", + "demoLoop.pause": "Pause video", + "demoLoop.play": "Play video", + "demoLoop.classic-zoom": "A task board recorded in OpenScreen. The view zooms in on a checklist item and a Ship button as they are clicked, then pulls back to the whole board.", + "demoLoop.3d-camera": "The same recording seen through the 3D camera, which tilts the screen and orbits around it while it follows the cursor.", + "demoLoop.3d-cursors": "One recorded pointer, redrawn in five cursor styles one after another while the recording keeps playing.", + "demoLoop.automatic-subtitles": "A narrated recording with captions under it, switching from English to French, Spanish and Korean.", + "demoLoop.caption-styles": "One caption shown in five styles: a plain caption, a bold yellow one, a handwritten one, a monospaced one, and one showing a single word at a time.", + "demoLoop.edit-by-transcript": "In the editor's transcript, a sentence is selected and deleted, and the matching cut appears on the timeline.", + "demoLoop.tighten-pauses": "Two clicks on the silence markers in the transcript, and both pauses are cut from the timeline.", + "demoLoop.background-picker": "The editor's background picker going through wallpapers, a gradient and a solid color, then turning on the animated Aurora background.", + "demoLoop.every-format": "The same recording exported in 16:9, then square, then vertical, with the framing following the cursor.", + "demoLoop.sensitive-data-mask": "A sign-in form being filled in. The email and password fields stay blurred while they are typed.", + "demoLoop.camera-blur": "A square webcam vignette over a screen recording. The view moves in on it while the room behind the speaker blurs, then pulls back.", + "demoLoop.every-layout": "One recording with a webcam, cycling through the layouts: picture in picture, side by side, stacked, full camera, then screen only.", + "demoLoop.device-frames": "The same playing recording framed in turn as a browser window, a phone, a laptop and a desktop monitor.", + "demoLoop.animated-backgrounds": "A recording on a plain background, then on a blurred photo background that moves slowly behind it.", + "demoLoop.beautiful-by-default": "A raw screen capture, then the same take as OpenScreen opens it, already zoomed, framed and with a restyled cursor, then in several looks.", + "demoLoop.simpler-editor": "The OpenScreen editor. One click on a background thumbnail restyles the whole video, then the side panel switches to the transcript.", + "demoLoop.edit-like-a-doc": "Text selected in the transcript is deleted like text in a document, and the video is cut to match.", + "demoLoop.ask-the-agent": "A request typed into the editor's chat panel, and the edits the agent makes appearing on the timeline.", + "demoLoop.title.classic-zoom": "Automatic zoom on clicks", + "demoLoop.title.3d-camera": "A 3D camera orbiting the screen", + "demoLoop.title.3d-cursors": "Cursor packs, drawn in 3D", + "demoLoop.title.automatic-subtitles": "Captions translated into other languages", + "demoLoop.title.caption-styles": "Five caption styles", + "demoLoop.title.edit-by-transcript": "Cutting video by deleting text in the transcript", + "demoLoop.title.tighten-pauses": "Cutting silences from the transcript", + "demoLoop.title.background-picker": "Changing the background behind a recording", + "demoLoop.title.every-format": "One recording in 16:9, 1:1 and 9:16", + "demoLoop.title.sensitive-data-mask": "Blurring an email address and a password", + "demoLoop.title.camera-blur": "Webcam background blur", + "demoLoop.title.every-layout": "Webcam layouts", + "demoLoop.title.device-frames": "Device frames", + "demoLoop.title.animated-backgrounds": "Animated backgrounds", + "demoLoop.title.beautiful-by-default": "From a raw screen capture to a finished demo", + "demoLoop.title.simpler-editor": "The OpenScreen editor", + "demoLoop.title.edit-like-a-doc": "Editing a video like a document", + "demoLoop.title.ask-the-agent": "Asking the editor's agent for an edit", "recreation.style.kicker": "Style", "recreation.style.title": "Swap the background", "recreation.style.sub": "Image, color or gradient behind your recording — no re-shoot.", @@ -183,6 +176,31 @@ "recreation.transcript.kicker": "Transcript", "recreation.transcript.title": "Edit video like text", "recreation.transcript.sub": "Delete a word or a silence; the cut lands on the timeline. Nothing destructive.", + "showcase.record.kicker": "record", + "showcase.record.claim": "It records with the operating system, not around it.", + "showcase.record.body": "Pick a window or a display. macOS goes through ScreenCaptureKit, Windows through Windows Graphics Capture, Linux through PipeWire and the ScreenCast portal — on each, the capture path the system itself provides. The pointer is recorded as data rather than burned into the pixels, which is the only reason you could restyle it further up this page.", + "showcase.record.fact": "ScreenCaptureKit · Windows Graphics Capture · PipeWire · system audio without an extra driver", + "showcase.record.link.docs": "Screen recording docs", + "showcase.record.label": "A drawing of the recorder: two capture targets side by side, Display 1 selected and a window titled Terminal beside it, then the settings for the take — ScreenCaptureKit, system audio, 1920 × 1080 at 60 fps — a microphone and a system-audio toggle, and a Start recording button.", + "showcase.export.kicker": "export", + "showcase.export.claim": "Then it writes the file.", + "showcase.export.body": "MP4 from 720p up to source, at 24, 30 or 60, in H.264 — or a GIF. The encode runs on your machine and counts frames while it does. No queue, no account, no watermark, and the file is on disk when the bar fills.", + "showcase.export.fact": "H.264 · 24, 30, 60 fps · no watermark", + "showcase.export.link.docs": "Video export docs", + "showcase.export.label": "A drawing of the export panel: recording-1783066227227.mp4 going out as MP4 at 1080p and 60 fps, with the quality and frame rate rows above a progress bar 62 percent along reading frame 1 488 of 2 400, writing to the Movies folder.", + "showcase.captions.kicker": "captions", + "showcase.captions.claim": "Transcription runs on your machine.", + "showcase.captions.body": "whisper.cpp ships with the app, and the model downloads once on first use — after that it works with the network off. The audio never leaves the laptop, and what comes back is editable text: set the typeface, the size, the color and the position, then burn it into the render.", + "showcase.captions.fact": "whisper.cpp · 100 languages · offline after first run", + "showcase.captions.link.docs": "Captions and transcript docs", + "showcase.captions.link.feature": "How local captions compare", + "showcase.agent.kicker": "agent", + "showcase.agent.claim": "Describe the edit. The agent makes it.", + "showcase.agent.body": "The wizard further up this page places zooms by watching where your cursor went. The agent goes further: it reads the actual transcript and the actual timeline, so it answers with timecodes you can go and check — which spans it will cut, and how much that saves. Every edit it makes is an ordinary undoable one, and it needs a provider key you supply. Nothing runs until you connect one.", + "showcase.agent.fact": "bring your own key · off by default · every edit is undoable", + "showcase.agent.link.docs": "AI editing docs", + "showcase.agent.link.feature": "How automatic zooms work", + "showcase.title": "Recorder, captions, agent, encoder.", "footer.brand.description": "A free, open-source screen recorder and editor. Community-maintained continuation, MIT licensed.", "footer.product.title": "Product", "footer.product.download": "Download", diff --git a/website/i18n/de/code.json b/website/i18n/de/code.json index d1a9c1e5b..b27e742e0 100644 --- a/website/i18n/de/code.json +++ b/website/i18n/de/code.json @@ -988,5 +988,77 @@ }, "films.camera.tab.blur": { "message": "Hintergrundunschärfe" + }, + "demoLoop.title.classic-zoom": { + "message": "Automatischer Zoom auf Klicks", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-camera": { + "message": "Eine 3D-Kamera, die den Bildschirm umkreist", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-cursors": { + "message": "Cursor-Pakete, in 3D gezeichnet", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.automatic-subtitles": { + "message": "Untertitel, in andere Sprachen übersetzt", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.caption-styles": { + "message": "Fünf Untertitelstile", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-by-transcript": { + "message": "Video schneiden, indem man Text im Transkript löscht", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.tighten-pauses": { + "message": "Stille aus dem Transkript schneiden", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.background-picker": { + "message": "Den Hintergrund einer Aufnahme wechseln", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-format": { + "message": "Eine Aufnahme in 16:9, 1:1 und 9:16", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.sensitive-data-mask": { + "message": "Eine E-Mail-Adresse und ein Passwort unscharf machen", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.camera-blur": { + "message": "Webcam-Hintergrundunschärfe", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-layout": { + "message": "Webcam-Layouts", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.device-frames": { + "message": "Geräterahmen", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.animated-backgrounds": { + "message": "Animierte Hintergründe", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.beautiful-by-default": { + "message": "Von der Rohaufnahme zur fertigen Demo", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.simpler-editor": { + "message": "Der OpenScreen-Editor", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-like-a-doc": { + "message": "Ein Video wie ein Dokument bearbeiten", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.ask-the-agent": { + "message": "Den Agenten des Editors um eine Änderung bitten", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." } } diff --git a/website/i18n/es/code.json b/website/i18n/es/code.json index 5e780ab24..14b2bbdc4 100644 --- a/website/i18n/es/code.json +++ b/website/i18n/es/code.json @@ -988,5 +988,77 @@ }, "films.camera.tab.blur": { "message": "Desenfoque de fondo" + }, + "demoLoop.title.classic-zoom": { + "message": "Zoom automático en los clics", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-camera": { + "message": "Una cámara 3D que orbita la pantalla", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-cursors": { + "message": "Paquetes de cursores, dibujados en 3D", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.automatic-subtitles": { + "message": "Subtítulos traducidos a otros idiomas", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.caption-styles": { + "message": "Cinco estilos de subtítulos", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-by-transcript": { + "message": "Cortar vídeo borrando texto en la transcripción", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.tighten-pauses": { + "message": "Cortar silencios desde la transcripción", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.background-picker": { + "message": "Cambiar el fondo de una grabación", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-format": { + "message": "Una grabación en 16:9, 1:1 y 9:16", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.sensitive-data-mask": { + "message": "Difuminar un correo y una contraseña", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.camera-blur": { + "message": "Desenfoque de fondo de la webcam", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-layout": { + "message": "Diseños de webcam", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.device-frames": { + "message": "Marcos de dispositivo", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.animated-backgrounds": { + "message": "Fondos animados", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.beautiful-by-default": { + "message": "De una captura sin editar a una demo terminada", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.simpler-editor": { + "message": "El editor de OpenScreen", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-like-a-doc": { + "message": "Editar un vídeo como un documento", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.ask-the-agent": { + "message": "Pedir una edición al agente del editor", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." } } diff --git a/website/i18n/fr/code.json b/website/i18n/fr/code.json index ba51dc7dd..10edea176 100644 --- a/website/i18n/fr/code.json +++ b/website/i18n/fr/code.json @@ -988,5 +988,77 @@ "demoLoop.camera-blur": { "message": "Une vignette de webcam carrée sur un enregistrement d'écran. La vue s'en rapproche pendant que la pièce derrière la personne se floute, puis recule.", "description": "Describes a short silent video loop of the app for screen readers." + }, + "demoLoop.title.classic-zoom": { + "message": "Zoom automatique sur les clics", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-camera": { + "message": "Une caméra 3D qui tourne autour de l'écran", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-cursors": { + "message": "Jeux de curseurs, dessinés en 3D", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.automatic-subtitles": { + "message": "Sous-titres traduits dans d'autres langues", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.caption-styles": { + "message": "Cinq styles de sous-titres", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-by-transcript": { + "message": "Couper la vidéo en supprimant du texte dans la transcription", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.tighten-pauses": { + "message": "Couper les silences depuis la transcription", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.background-picker": { + "message": "Changer l'arrière-plan d'un enregistrement", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-format": { + "message": "Un enregistrement en 16:9, 1:1 et 9:16", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.sensitive-data-mask": { + "message": "Flouter une adresse e-mail et un mot de passe", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.camera-blur": { + "message": "Flou d'arrière-plan de la webcam", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-layout": { + "message": "Dispositions de la webcam", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.device-frames": { + "message": "Cadres d'appareil", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.animated-backgrounds": { + "message": "Arrière-plans animés", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.beautiful-by-default": { + "message": "D'une capture brute à une démo finie", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.simpler-editor": { + "message": "L'éditeur OpenScreen", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-like-a-doc": { + "message": "Monter une vidéo comme un document", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.ask-the-agent": { + "message": "Demander une modification à l'agent de l'éditeur", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." } } diff --git a/website/i18n/ja/code.json b/website/i18n/ja/code.json index 9161ff413..04e120608 100644 --- a/website/i18n/ja/code.json +++ b/website/i18n/ja/code.json @@ -988,5 +988,77 @@ }, "films.camera.tab.blur": { "message": "背景ぼかし" + }, + "demoLoop.title.classic-zoom": { + "message": "クリックへの自動ズーム", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-camera": { + "message": "画面の周りを回る 3D カメラ", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-cursors": { + "message": "3D で描かれるカーソルパック", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.automatic-subtitles": { + "message": "他の言語に翻訳された字幕", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.caption-styles": { + "message": "5 つの字幕スタイル", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-by-transcript": { + "message": "文字起こしのテキストを消して動画をカット", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.tighten-pauses": { + "message": "文字起こしから無音をカット", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.background-picker": { + "message": "録画の背景を変更", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-format": { + "message": "1 つの録画を 16:9、1:1、9:16 で", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.sensitive-data-mask": { + "message": "メールアドレスとパスワードをぼかす", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.camera-blur": { + "message": "ウェブカメラの背景ぼかし", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-layout": { + "message": "ウェブカメラのレイアウト", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.device-frames": { + "message": "デバイスフレーム", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.animated-backgrounds": { + "message": "アニメーション背景", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.beautiful-by-default": { + "message": "加工前の画面キャプチャーから完成したデモへ", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.simpler-editor": { + "message": "OpenScreen エディター", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-like-a-doc": { + "message": "ドキュメントのように動画を編集", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.ask-the-agent": { + "message": "エディターのエージェントに編集を依頼", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." } } diff --git a/website/i18n/pt-BR/code.json b/website/i18n/pt-BR/code.json index db43af098..794350748 100644 --- a/website/i18n/pt-BR/code.json +++ b/website/i18n/pt-BR/code.json @@ -988,5 +988,77 @@ "demoLoop.camera-blur": { "message": "Uma vinheta quadrada de webcam sobre uma gravação de tela. A visualização se aproxima enquanto o ambiente atrás da pessoa fica desfocado, e depois se afasta.", "description": "Describes a short silent video loop of the app for screen readers." + }, + "demoLoop.title.classic-zoom": { + "message": "Zoom automático nos cliques", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-camera": { + "message": "Uma câmera 3D que orbita a tela", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-cursors": { + "message": "Pacotes de cursores, desenhados em 3D", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.automatic-subtitles": { + "message": "Legendas traduzidas para outros idiomas", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.caption-styles": { + "message": "Cinco estilos de legenda", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-by-transcript": { + "message": "Cortar vídeo apagando texto na transcrição", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.tighten-pauses": { + "message": "Cortar silêncios pela transcrição", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.background-picker": { + "message": "Trocar o fundo de uma gravação", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-format": { + "message": "Uma gravação em 16:9, 1:1 e 9:16", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.sensitive-data-mask": { + "message": "Desfocar um e-mail e uma senha", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.camera-blur": { + "message": "Desfoque de fundo da webcam", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-layout": { + "message": "Layouts de webcam", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.device-frames": { + "message": "Molduras de dispositivo", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.animated-backgrounds": { + "message": "Fundos animados", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.beautiful-by-default": { + "message": "De uma captura bruta a uma demo pronta", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.simpler-editor": { + "message": "O editor do OpenScreen", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-like-a-doc": { + "message": "Editar um vídeo como um documento", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.ask-the-agent": { + "message": "Pedir uma edição ao agente do editor", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." } } diff --git a/website/i18n/zh-CN/code.json b/website/i18n/zh-CN/code.json index 96fc0c01b..c10117b85 100644 --- a/website/i18n/zh-CN/code.json +++ b/website/i18n/zh-CN/code.json @@ -988,5 +988,77 @@ }, "films.camera.tab.blur": { "message": "背景虚化" + }, + "demoLoop.title.classic-zoom": { + "message": "自动放大点击处", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-camera": { + "message": "环绕屏幕的 3D 摄像机", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-cursors": { + "message": "以 3D 绘制的光标包", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.automatic-subtitles": { + "message": "翻译成其他语言的字幕", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.caption-styles": { + "message": "五种字幕样式", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-by-transcript": { + "message": "在转录文本中删除文字来剪辑视频", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.tighten-pauses": { + "message": "在转录文本中剪掉静音", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.background-picker": { + "message": "更换录制内容的背景", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-format": { + "message": "同一段录制的 16:9、1:1 和 9:16 版本", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.sensitive-data-mask": { + "message": "模糊邮箱地址和密码", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.camera-blur": { + "message": "摄像头背景虚化", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-layout": { + "message": "摄像头布局", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.device-frames": { + "message": "设备边框", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.animated-backgrounds": { + "message": "动态背景", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.beautiful-by-default": { + "message": "从原始屏幕录制到成品演示", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.simpler-editor": { + "message": "OpenScreen 编辑器", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-like-a-doc": { + "message": "像编辑文档一样编辑视频", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.ask-the-agent": { + "message": "请编辑器的代理完成修改", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." } } diff --git a/website/i18n/zh-TW/code.json b/website/i18n/zh-TW/code.json index c96f8c816..d5e508073 100644 --- a/website/i18n/zh-TW/code.json +++ b/website/i18n/zh-TW/code.json @@ -988,5 +988,77 @@ }, "films.camera.tab.blur": { "message": "背景模糊" + }, + "demoLoop.title.classic-zoom": { + "message": "自動放大點擊處", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-camera": { + "message": "環繞螢幕的 3D 攝影機", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.3d-cursors": { + "message": "以 3D 繪製的游標套件", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.automatic-subtitles": { + "message": "翻譯成其他語言的字幕", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.caption-styles": { + "message": "五種字幕樣式", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-by-transcript": { + "message": "在逐字稿中刪除文字來剪輯影片", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.tighten-pauses": { + "message": "在逐字稿中剪掉靜音", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.background-picker": { + "message": "更換錄製內容的背景", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-format": { + "message": "同一段錄製的 16:9、1:1 和 9:16 版本", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.sensitive-data-mask": { + "message": "模糊電子郵件地址和密碼", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.camera-blur": { + "message": "網路攝影機背景模糊", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.every-layout": { + "message": "網路攝影機版面配置", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.device-frames": { + "message": "裝置外框", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.animated-backgrounds": { + "message": "動態背景", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.beautiful-by-default": { + "message": "從原始螢幕錄製到成品示範", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.simpler-editor": { + "message": "OpenScreen 編輯器", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.edit-like-a-doc": { + "message": "像編輯文件一樣編輯影片", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." + }, + "demoLoop.title.ask-the-agent": { + "message": "請編輯器的代理完成修改", + "description": "The name of a short silent video of the app, given to search engines (schema.org VideoObject)." } } diff --git a/website/src/components/DemoLoop/index.tsx b/website/src/components/DemoLoop/index.tsx index 9258c7272..2d3c53b9d 100644 --- a/website/src/components/DemoLoop/index.tsx +++ b/website/src/components/DemoLoop/index.tsx @@ -24,6 +24,7 @@ * there is none, `muted` alone is not enough on iOS. */ +import Head from "@docusaurus/Head"; import { translate } from "@docusaurus/Translate"; import { Pause, Play } from "lucide-react"; import { useEffect, useRef, useState } from "react"; @@ -35,7 +36,8 @@ import { loopSources, pickHeight, } from "../../lib/demo-loop"; -import { loopLabel } from "./labels"; +import { jsonLd, videoObjectLd } from "../../lib/structured-data"; +import { loopLabel, loopTitle } from "./labels"; import styles from "./styles.module.css"; type Props = { @@ -48,8 +50,24 @@ type Props = { onProgress?: (fraction: number) => void; /** Called once the first frame is decoded, so a stage can fade to it. */ onReady?: () => void; + /** Off on a stage, which declares all of its tabs' loops itself. */ + schema?: boolean; }; +/** + * The schema.org VideoObject for loops the page presents, in the server HTML. + * One + + ); +} + function motionAllowed(): boolean { try { const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches; @@ -61,7 +79,14 @@ function motionAllowed(): boolean { } } -export default function DemoLoop({ name, loop = true, onEnded, onProgress, onReady }: Props) { +export default function DemoLoop({ + name, + loop = true, + onEnded, + onProgress, + onReady, + schema = true, +}: Props) { const box = useRef(null); const video = useRef(null); const [height, setHeight] = useState(null); @@ -158,6 +183,7 @@ export default function DemoLoop({ name, loop = true, onEnded, onProgress, onRea return (
+ {schema && }