diff --git a/README.md b/README.md index 727972b..6a45a0b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,10 @@ `geoptimize` is a deterministic content-readiness lint for static websites and documentation. It checks reproducible properties such as document structure, sourced quantitative claims, structured-data hygiene, indexing controls, metadata quality, and repetitive wording — locally, in CI, or pre-commit. +https://github.com/user-attachments/assets/5dafd888-92c9-428b-b372-62864c58059a + +30-second introduction · [Download the MP4](https://github.com/cucuwang/geoptimize/raw/refs/heads/main/marketing/geoptimize-preview-20260916/output/geoptimize-preview-v2.mp4) · [Video source and credits](https://github.com/cucuwang/geoptimize/tree/main/marketing/geoptimize-preview-20260916) + [![geoptimize visual report with five readiness scores and site health charts](docs/assets/report-demo.png)](docs/assets/report-demo.html) [Open the offline example](docs/assets/report-demo.html) · [After improvements](docs/assets/report-demo-after.html) diff --git a/marketing/geoptimize-preview-20260916/.gitignore b/marketing/geoptimize-preview-20260916/.gitignore new file mode 100644 index 0000000..0633bf4 --- /dev/null +++ b/marketing/geoptimize-preview-20260916/.gitignore @@ -0,0 +1,7 @@ +work/ +assets/soundtrack.wav +output/*.wav +output/silent.mp4 +output/stills/ +output/contact-sheet.jpg +output/transitions-v2.jpg diff --git a/marketing/geoptimize-preview-20260916/ATTRIBUTION.md b/marketing/geoptimize-preview-20260916/ATTRIBUTION.md new file mode 100644 index 0000000..23c0867 --- /dev/null +++ b/marketing/geoptimize-preview-20260916/ATTRIBUTION.md @@ -0,0 +1,29 @@ +# Attribution and third-party licenses + +This geoptimize preview uses the motion-design workflow in +[motion-web](https://github.com/feitangyuan/motion-web), by **feitangyuan**, +at commit `5f4e40f1253e11e28850d08dce28b9b7e4320115`. + +The reference skill is licensed under +[CC BY-NC 4.0](https://creativecommons.org/licenses/by-nc/4.0/). +Its [repository license](https://github.com/feitangyuan/motion-web/blob/5f4e40f1253e11e28850d08dce28b9b7e4320115/LICENSE) +applies to its prompts, cases, scripts and assets. This preview adapts the workflow +to a fixed 30-second Canvas timeline with analytical springs, explicit seek controls, +headless frame rendering and an MP4 export. It does not copy a demonstration case, +its artwork, or its soundtrack. No endorsement is implied. + +This preview is shared as a demonstration of the free, open-source geoptimize +project. The referenced motion-web license permits public open-source demonstration +projects that do not generate commercial revenue. Commercial reuse must follow +the applicable motion-web license conditions. The parent project's MIT license +does not relicense motion-web material. + +Space Grotesk and Space Mono are supplied from the Google Fonts repository. +The original SIL Open Font Licenses are included beside the font files. + +The soundtrack is original procedural synthesis. It uses no third-party samples, +recordings or commercial songs. Its complete reproducible source is `soundtrack.py`. + +The product facts and demonstration counts come from geoptimize itself. The pinned +source URLs and commit are in `source-data.json`. Onscreen scan results describe a +synthetic three-page fixture, not a measured customer outcome. diff --git a/marketing/geoptimize-preview-20260916/README.md b/marketing/geoptimize-preview-20260916/README.md new file mode 100644 index 0000000..5aee21b --- /dev/null +++ b/marketing/geoptimize-preview-20260916/README.md @@ -0,0 +1,79 @@ +# geoptimize video preview + +30-second English product film, 1920 × 1080, 30 fps. Playful typography, restrained +spring motion, a moving document scan and an original instrumental soundtrack. +The design retains geoptimize's dark ground and mint-green accent. + +https://github.com/user-attachments/assets/5dafd888-92c9-428b-b372-62864c58059a + +[Watch or download the current MP4](output/geoptimize-preview-v2.mp4) · +[First preview](output/geoptimize-preview-v1.mp4) · +[Attribution and licenses](ATTRIBUTION.md) + +The intended viewer is a developer maintaining a static website or documentation. +The film introduces a first local scan and ends with the npm installation command. +It uses verified public demonstration data and makes no search-outcome claims. + +## Files + +- `output/geoptimize-preview-v2.mp4`: current review copy with sound and varied transitions. +- `output/geoptimize-preview-v1.mp4`: preserved first preview. +- `index.html`, `scene.js`: editable Canvas source and accessible local player. +- `source-data.json`, `source-notes.md`: pinned public product evidence. +- `soundtrack.py`: original deterministic 128 BPM music. +- `ATTRIBUTION.md`: motion-web credit, font licenses and preview usage scope. +- `output/verification.json`: dimensions, duration, stream properties and hashes. +- `output/verification-v2.json`, `output/layout-audit-v2.json`: current media and text-layout readback. + +## Reproduce + +Requires Node, Puppeteer Core, Chrome, FFmpeg and Python with NumPy. The parent +checkout already has Puppeteer Core. `GEO_CHROME` can select a Chrome executable. +No global dependency installation is needed in the producing workspace. + +```sh +python3 soundtrack.py +node render.mjs +ffmpeg -y -i output/silent.mp4 -i assets/soundtrack.wav -map 0:v -map 1:a -c:v copy -c:a aac -b:a 192k -t 30 -movflags +faststart output/geoptimize-preview-v2.mp4 +``` + +`node render.mjs --stills` writes representative frames only. Rendering runs through +an ephemeral loopback HTTP server which is closed on completion. Every frame is +evaluated at its exact timestamp; no mouse operation or live webpage is recorded. + +The optional HTML player starts paused, including under reduced motion. Serve this +folder over a local HTTP server to use its play, seek and sound controls. The MP4 is +the primary review artifact and requires no server. + +## Editorial structure + +| Seconds | Content | +| --- | --- | +| 0–4.7 | A page looks ready; a closer scan reveals a reason to check. | +| 4.7–9.4 | geoptimize introduction and its local scan command. | +| 9.4–15 | The actual synthetic fixture's five readiness dimensions. | +| 15–21 | Editing the fixture and comparing two concrete finding counts. | +| 21–25.3 | Local scans, offline reports and the existing CI capability. | +| 25.3–30 | Brand, installation command and repository URL. | + +The overall score is not shown increasing, since the fixture's later score reflects +several content changes beyond the two findings highlighted in the video. + +Each boundary now has its own motion: circular reveal, terminal opening, +horizontal push, diagonal sweep and a small zoom into a beat cut. Incoming scene +clocks continue through the boundary rather than restarting. Heading travel is +bounded to its own area so it cannot cross a fixed caption. + +## Validation scope + +The video is a stylized product explanation, not a literal screen recording. +Validation checks the rendered frames, playback duration, codec, sound levels, +source-data consistency and deterministic frame output. It does not claim new +runtime validation of geoptimize's CLI or a released marketing campaign. + +The v2 layout pass checks transformed font-glyph boxes in all 900 frames, along +with settled text against the safe area and its own clipping region. Text pairs +are checked within each scene; the opaque transition masks intentionally replace +one scene with the next. The tiny HTML labels in overlapping paper illustrations +are decorative and excluded from the glyph-pair audit. Representative mid-transition +frames are also reviewed visually. diff --git a/marketing/geoptimize-preview-20260916/assets/SpaceGrotesk-OFL.txt b/marketing/geoptimize-preview-20260916/assets/SpaceGrotesk-OFL.txt new file mode 100644 index 0000000..d5666d7 --- /dev/null +++ b/marketing/geoptimize-preview-20260916/assets/SpaceGrotesk-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2020 The Space Grotesk Project Authors (https://github.com/floriankarsten/space-grotesk) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/marketing/geoptimize-preview-20260916/assets/SpaceGrotesk.ttf b/marketing/geoptimize-preview-20260916/assets/SpaceGrotesk.ttf new file mode 100644 index 0000000..a1b2e6c Binary files /dev/null and b/marketing/geoptimize-preview-20260916/assets/SpaceGrotesk.ttf differ diff --git a/marketing/geoptimize-preview-20260916/assets/SpaceMono-OFL.txt b/marketing/geoptimize-preview-20260916/assets/SpaceMono-OFL.txt new file mode 100644 index 0000000..389d65c --- /dev/null +++ b/marketing/geoptimize-preview-20260916/assets/SpaceMono-OFL.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Space Mono Project Authors (https://github.com/googlefonts/spacemono) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/marketing/geoptimize-preview-20260916/assets/SpaceMono.ttf b/marketing/geoptimize-preview-20260916/assets/SpaceMono.ttf new file mode 100644 index 0000000..1cfa365 Binary files /dev/null and b/marketing/geoptimize-preview-20260916/assets/SpaceMono.ttf differ diff --git a/marketing/geoptimize-preview-20260916/index.html b/marketing/geoptimize-preview-20260916/index.html new file mode 100644 index 0000000..4f6838d --- /dev/null +++ b/marketing/geoptimize-preview-20260916/index.html @@ -0,0 +1,39 @@ + + + + + + + geoptimize · Let's check. + + + +
+ +
0:00
+
+
+

geoptimize. Let's check.

+

Looks ready. Let's check. Scan a built website with npx geoptimize scan ./dist --dir. See five content-readiness dimensions, inspect findings and compare the same demonstration site after editing. Scan locally and save offline reports.

+

The scan shown uses a synthetic three-page site. Content-readiness scores are heuristic checks; search visibility and AI citations require separate measurement.

+

Get geoptimize on GitHub

+
+ + + + diff --git a/marketing/geoptimize-preview-20260916/output/geoptimize-preview-v1.mp4 b/marketing/geoptimize-preview-20260916/output/geoptimize-preview-v1.mp4 new file mode 100644 index 0000000..bbbc6ee Binary files /dev/null and b/marketing/geoptimize-preview-20260916/output/geoptimize-preview-v1.mp4 differ diff --git a/marketing/geoptimize-preview-20260916/output/geoptimize-preview-v2.mp4 b/marketing/geoptimize-preview-20260916/output/geoptimize-preview-v2.mp4 new file mode 100644 index 0000000..d2bef78 Binary files /dev/null and b/marketing/geoptimize-preview-20260916/output/geoptimize-preview-v2.mp4 differ diff --git a/marketing/geoptimize-preview-20260916/output/layout-audit-v2.json b/marketing/geoptimize-preview-20260916/output/layout-audit-v2.json new file mode 100644 index 0000000..5a82cfd --- /dev/null +++ b/marketing/geoptimize-preview-20260916/output/layout-audit-v2.json @@ -0,0 +1,14 @@ +{ + "frames": 900, + "textBoxes": 10354, + "transitionNames": [ + "circle-reveal", + "terminal-open", + "horizontal-push", + "diagonal-sweep", + "beat-cut" + ], + "collisions": [], + "unsafe": [], + "scope": "Actual font glyph bounds transformed into canvas coordinates, with authored entrance clips. Text pairs are checked within their own scene; transition masks intentionally replace scenes. Decorative labels on overlapping paper illustrations excluded." +} diff --git a/marketing/geoptimize-preview-20260916/output/poster.jpg b/marketing/geoptimize-preview-20260916/output/poster.jpg new file mode 100644 index 0000000..f2b22f6 Binary files /dev/null and b/marketing/geoptimize-preview-20260916/output/poster.jpg differ diff --git a/marketing/geoptimize-preview-20260916/output/verification-v2.json b/marketing/geoptimize-preview-20260916/output/verification-v2.json new file mode 100644 index 0000000..54774fb --- /dev/null +++ b/marketing/geoptimize-preview-20260916/output/verification-v2.json @@ -0,0 +1,50 @@ +{ + "artifact": "geoptimize-preview-v2.mp4", + "media": { + "programs": [], + "stream_groups": [], + "streams": [ + { + "codec_name": "h264", + "codec_type": "video", + "width": 1920, + "height": 1080, + "avg_frame_rate": "30/1", + "nb_read_frames": "900" + }, + { + "codec_name": "aac", + "codec_type": "audio", + "sample_rate": "48000", + "channels": 2, + "avg_frame_rate": "0/0", + "nb_read_frames": "1407" + } + ], + "format": { + "duration": "30.000000", + "size": "1926673" + } + }, + "sha256": "80a2f2ce50e850cdf31a5abfcb33b5fdbb0f7bcc75b55e6b1b22343123a74ab1", + "audioIdenticalToV1": true, + "v1Preserved": true, + "layout": { + "frames": 900, + "textBoxes": 10354, + "collisions": 0, + "unsafe": 0, + "transitions": [ + "circle-reveal", + "terminal-open", + "horizontal-push", + "diagonal-sweep", + "beat-cut" + ], + "report": "layout-audit-v2.json" + }, + "browserPageErrors": [], + "deterministicFrameReadback": true, + "visualReview": "Representative frames across all five transitions, title entrances and settled text; fixed the initial y descender clipping.", + "validationScope": "Actual font glyph bounds transformed into canvas coordinates, with authored entrance clips. Text pairs are checked within their own scene; transition masks intentionally replace scenes. Decorative labels on overlapping paper illustrations excluded." +} diff --git a/marketing/geoptimize-preview-20260916/output/verification.json b/marketing/geoptimize-preview-20260916/output/verification.json new file mode 100644 index 0000000..30ac534 --- /dev/null +++ b/marketing/geoptimize-preview-20260916/output/verification.json @@ -0,0 +1,41 @@ +{ + "artifact": "geoptimize-preview-v1.mp4", + "media": { + "programs": [], + "stream_groups": [], + "streams": [ + { + "codec_name": "h264", + "codec_type": "video", + "width": 1920, + "height": 1080, + "avg_frame_rate": "30/1", + "nb_read_frames": "900" + }, + { + "codec_name": "aac", + "codec_type": "audio", + "sample_rate": "48000", + "channels": 2, + "avg_frame_rate": "0/0", + "nb_read_frames": "1407" + } + ], + "format": { + "duration": "30.000000", + "size": "2179953" + } + }, + "integratedLufs": -18.1, + "audioPeakDbfs": -3.9, + "sha256": "3fd281a12b4878146ae536c41e18e5ecec3c982647335e599858f5ffa2eb31db", + "frameRender": { + "frames": 900, + "deterministicReadback": true, + "browserPageErrors": [], + "representativeFramesReviewed": 14 + }, + "sourceCommit": "8872148f444a51177af75cb03ee861b8326f1787", + "validationScope": "Encoded video decoded successfully; fixture data checked against pinned public source. CLI was not executed in this task.", + "designDetector": "Degraded regex-only mode; no findings. Canvas visual review performed separately." +} diff --git a/marketing/geoptimize-preview-20260916/render.mjs b/marketing/geoptimize-preview-20260916/render.mjs new file mode 100644 index 0000000..86e781f --- /dev/null +++ b/marketing/geoptimize-preview-20260916/render.mjs @@ -0,0 +1,66 @@ +import http from 'node:http'; +import fs from 'node:fs'; +import path from 'node:path'; +import {fileURLToPath} from 'node:url'; +import {spawn} from 'node:child_process'; +import {once} from 'node:events'; +import puppeteer from 'puppeteer-core'; + +const root=path.dirname(fileURLToPath(import.meta.url)); +const output=path.join(root,'output');fs.mkdirSync(output,{recursive:true}); +const mime={'.html':'text/html','.js':'text/javascript','.json':'application/json','.ttf':'font/ttf','.wav':'audio/wav'}; +const server=http.createServer((req,res)=>{ + const u=new URL(req.url,'http://localhost');const rel=decodeURIComponent(u.pathname)==='/'?'index.html':decodeURIComponent(u.pathname).slice(1); + const filename=path.resolve(root,rel);if(!filename.startsWith(root+path.sep)){res.writeHead(403).end();return} + fs.readFile(filename,(err,b)=>{if(err){res.writeHead(404).end();return}res.setHeader('Content-Type',mime[path.extname(filename)]||'application/octet-stream');res.setHeader('Cache-Control','no-store');res.end(b)}); +}); +await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve)); +const port=server.address().port; +const browser=await puppeteer.launch({executablePath:process.env.GEO_CHROME||'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',headless:true,args:['--disable-background-timer-throttling','--disable-renderer-backgrounding','--hide-scrollbars']}); +try{ + const page=await browser.newPage();const errors=[];page.on('pageerror',e=>errors.push(e.message)); + await page.setViewport({width:1920,height:1080,deviceScaleFactor:1}); + await page.goto(`http://127.0.0.1:${port}/?render`,{waitUntil:'networkidle0'});await page.evaluate(()=>window.__ready); + const stills=path.join(output,'stills');fs.mkdirSync(stills,{recursive:true}); + const sampleTimes=[0,1.2,3.3,4.45,4.65,5.2,6.1,8.8,9.08,9.3,10.7,13.2,14.55,14.85,15.25,16.5,18.8,20.3,20.7,20.9,21.2,22.8,24.85,25.04,25.18,25.5,26.8,29.6]; + for(const t of sampleTimes){await page.evaluate(t=>window.__seek(t),t);await page.screenshot({path:path.join(stills,`${String(t).replace('.','_')}.png`)})} + // Exact same frame after seeking away must be identical: no hidden ambient clocks. + await page.evaluate(()=>window.__seek(13.2));const first=await page.screenshot(); + await page.evaluate(()=>window.__seek(22.8));await page.evaluate(()=>window.__seek(13.2));const second=await page.screenshot(); + if(!Buffer.from(first).equals(Buffer.from(second)))throw new Error('Non-deterministic render'); + if(errors.length)throw new Error(errors.join('\n')); + const audit=await page.evaluate(()=>{ + const collisions=[],unsafe=[],transitionNames=new Set();let textBoxes=0; + for(let n=0;n<900;n++){ + const t=n/30;window.__seek(t);const {bounds,transition,scene}=window.__probe(); + if(transition)transitionNames.add(transition);textBoxes+=bounds.length; + for(let a=0;a1.8){ + if(x.raw.x<64||x.raw.y<44||x.raw.right>1856||x.raw.bottom>1036)unsafe.push({t,text:x.s,box:x.raw,reason:'frame-safe-area'}); + if(x.authoredClip&&(x.raw.xx.region.right+1||x.raw.bottom>x.region.bottom+1))unsafe.push({t,text:x.s,box:x.raw,region:x.region,reason:'settled-text-clipped'}); + } + for(let b=a+1;b1&&height>1)collisions.push({t,scene:x.scene,first:x.s,second:y.s,width,height}); + } + } + } + return{frames:900,textBoxes,transitionNames:[...transitionNames],collisions,unsafe,scope:'Actual font glyph bounds transformed into canvas coordinates, with authored entrance clips. Text pairs are checked within their own scene; transition masks intentionally replace scenes. Decorative labels on overlapping paper illustrations excluded.'}; + }); + fs.writeFileSync(path.join(output,'layout-audit-v2.json'),JSON.stringify(audit,null,2)+'\n'); + console.log(JSON.stringify({stills:sampleTimes.length,deterministic:true,pageErrors:errors,layout:{frames:audit.frames,textBoxes:audit.textBoxes,transitions:audit.transitionNames,collisions:audit.collisions.length,unsafe:audit.unsafe.length}})); + if(audit.collisions.length||audit.unsafe.length){console.log(JSON.stringify({collisions:audit.collisions.slice(0,12),unsafe:audit.unsafe.slice(0,12)},null,2));throw new Error('Text layout audit failed')} + if(!process.argv.includes('--stills')){ + const ff=spawn('ffmpeg',['-hide_banner','-loglevel','error','-y','-f','image2pipe','-framerate','30','-i','pipe:0','-an','-c:v','libx264','-preset','fast','-crf','18','-pix_fmt','yuv420p','-movflags','+faststart',path.join(output,'silent.mp4')],{stdio:['pipe','inherit','inherit']}); + const done=once(ff,'close'); + for(let n=0;n<900;n++){ + const frame=await page.evaluate(t=>{window.__seek(t);return document.querySelector('canvas').toDataURL('image/png').split(',')[1]},n/30); + if(!ff.stdin.write(Buffer.from(frame,'base64')))await once(ff.stdin,'drain'); + if(n%150===0)console.log(`Rendered ${n}/900 frames`); + } + ff.stdin.end();const [code]=await done;if(code!==0)throw new Error(`FFmpeg ${code}`); + console.log('Rendered 900 frames at 1920x1080, 30fps.'); + } +}finally{await browser.close();await new Promise(resolve=>server.close(resolve))} diff --git a/marketing/geoptimize-preview-20260916/scene.js b/marketing/geoptimize-preview-20260916/scene.js new file mode 100644 index 0000000..b9d072a --- /dev/null +++ b/marketing/geoptimize-preview-20260916/scene.js @@ -0,0 +1,157 @@ +/* Original geoptimize film. Motion study informed by feitangyuan/motion-web. + * See ATTRIBUTION.md. Fixed-time evaluation makes every frame reproducible. */ +const canvas = document.querySelector('#film'); +const ctx = canvas.getContext('2d', {alpha:false}); +const C = {bg:'#101318',ink:'#f7f5ee',mint:'#79edb3',muted:'#a9b1bd',rail:'#303842',pink:'#ff9eac',paper:'#f3f0e7',dark:'#1a2029',line:'#3e4854'}; +const starts=[0,4.7,9.4,15,21,25.3]; +const transitions=[ + {name:'circle-reveal',duration:.55,preroll:.55}, + {name:'terminal-open',duration:.6,preroll:.6}, + {name:'horizontal-push',duration:.65,preroll:.65}, + {name:'diagonal-sweep',duration:.5,preroll:.5}, + {name:'beat-cut',duration:.6,preroll:.3}, +]; +const clamp=(v,a=0,b=1)=>Math.min(b,Math.max(a,v)); +const lerp=(a,b,t)=>a+(b-a)*t; +const ease=t=>1-Math.pow(1-clamp(t),4); +const smooth=t=>{t=clamp(t);return t*t*(3-2*t)}; +// Analytic response of an underdamped spring, stiffness=200, damping=14, mass=1. +function spring(t,k=200,c=14){if(t<=0)return 0;const w=Math.sqrt(k-c*c/4);return 1-Math.exp(-c*t/2)*(Math.cos(w*t)+c/(2*w)*Math.sin(w*t));} +let data, playing=false, held=0, origin=0, soundOn=false, bounds=[],clips=[],activeScene=0; +function rect(x,y,w,h,fill,r=0,stroke=null){ctx.beginPath();ctx.roundRect(x,y,w,h,r);if(fill){ctx.fillStyle=fill;ctx.fill()}if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=2;ctx.stroke()}} +function circle(x,y,r,fill,stroke=null,lw=3){ctx.beginPath();ctx.arc(x,y,Math.max(0,r),0,Math.PI*2);if(fill){ctx.fillStyle=fill;ctx.fill()}if(stroke){ctx.strokeStyle=stroke;ctx.lineWidth=lw;ctx.stroke()}} +function line(x1,y1,x2,y2,color,width=3){ctx.beginPath();ctx.moveTo(x1,y1);ctx.lineTo(x2,y2);ctx.strokeStyle=color;ctx.lineWidth=width;ctx.lineCap='round';ctx.stroke()} +function boxInWorld(x,y,w,h){const m=ctx.getTransform();const ps=[[x,y],[x+w,y],[x+w,y+h],[x,y+h]].map(([a,b])=>m.transformPoint({x:a,y:b}));return{x:Math.min(...ps.map(p=>p.x)),y:Math.min(...ps.map(p=>p.y)),right:Math.max(...ps.map(p=>p.x)),bottom:Math.max(...ps.map(p=>p.y))}} +function intersection(a,b){return{x:Math.max(a.x,b.x),y:Math.max(a.y,b.y),right:Math.min(a.right,b.right),bottom:Math.min(a.bottom,b.bottom)}} +function text(s,x,y,size=48,color=C.ink,weight=600,align='left',font='Space',track=true){ + ctx.font=`${weight} ${size}px ${font}`;ctx.fillStyle=color;ctx.textAlign=align;ctx.textBaseline='alphabetic';ctx.fillText(s,x,y); + if(track&&s){const m=ctx.measureText(s);const raw=boxInWorld(x-m.actualBoundingBoxLeft,y-m.actualBoundingBoxAscent,m.actualBoundingBoxLeft+m.actualBoundingBoxRight,m.actualBoundingBoxAscent+m.actualBoundingBoxDescent);let region={x:0,y:0,right:1920,bottom:1080};for(const clip of clips)region=intersection(region,clip);const visible=intersection(raw,region);if(visible.right-visible.x>1&&visible.bottom-visible.y>1)bounds.push({s,scene:activeScene,raw,...visible,region,authoredClip:clips.length>0})} +} +function transform(x,y,angle,scale,fn){ctx.save();ctx.translate(x,y);ctx.rotate(angle);ctx.scale(scale,scale);fn();ctx.restore()} +function clipRegion(x,y,w,h,fn){ctx.save();ctx.beginPath();ctx.rect(x,y,w,h);ctx.clip();clips.push(boxInWorld(x,y,w,h));fn();clips.pop();ctx.restore()} +function revealText(s,x,y,size,color,p){clipRegion(x-12,y-size-16,1100,size+60,()=>text(s,x,y+(1-p)*(size+32),size,color,700))} +function logo(x,y,size=38,color=C.ink){text('>_',x,y,size,C.mint,600,'left','Code');text('geoptimize',x+size*1.8,y,size,color,700)} +function check(x,y,s,color=C.bg,p=1){ctx.save();ctx.translate(x,y);ctx.beginPath();ctx.moveTo(-s*.35,0);if(p<.4){ctx.lineTo(lerp(-s*.35,-s*.08,p/.4),s*.26*p/.4)}else{ctx.lineTo(-s*.08,s*.26);ctx.lineTo(lerp(-s*.08,s*.48,(p-.4)/.6),lerp(s*.26,-s*.4,(p-.4)/.6))}ctx.strokeStyle=color;ctx.lineWidth=s*.11;ctx.lineCap='round';ctx.lineJoin='round';ctx.stroke();ctx.restore()} +function label(s,x,y,w,fill,color=C.bg,angle=0){transform(x,y,angle,1,()=>{rect(-w/2,-30,w,60,fill,30);text(s,0,11,28,color,600,'center')})} +function pageShape(x,y,t,rotation,variant){transform(x,y,rotation,1,()=>{rect(-170,-215,340,430,C.paper,18);text(variant===0?'':'<html>',-130,-147,30,C.bg,400,'left','Code',false);for(let i=0;i<5;i++)line(-130,-88+i*47,i%2?92:128,-88+i*47,i===2?C.pink:'#c6c9c4',i===2?14:9);rect(-130,165,150,12,C.mint,6);})} +function bg(color){rect(0,0,1920,1080,color)} +function scene0(t){ + bg(C.bg);logo(116,115); + const a=spring(t-.18),b=spring(t-1.18); + revealText('Looks ready.',120,400,142,C.ink,a);revealText("Let's check.",120,575,142,C.mint,b); + text('A closer look at your website content.',128,695,34,C.muted,400); + const p=ease((t-.32)/.8);const yy=lerp(1450,564,p); + pageShape(1460,yy,t,.12+Math.sin(t*1.6)*.025,0); + pageShape(1340,yy+10,t,-.09+Math.sin(t*1.4)*.025,1); + if(t>.92){const s=spring(t-.92);transform(1548,386,-.12,s,()=>{circle(0,0,63,C.pink);text('?',0,28,80,C.bg,700,'center')})} + if(t>1.7){const s=spring(t-1.7);transform(1300,824,.075,s,()=>{rect(-181,-40,362,80,C.mint,40);text('One quick scan.',0,13,33,C.bg,600,'center')})} + // The scanning focus belongs to the document, rather than a decorative pointer trail. + if(t>2.5){const p=smooth((t-2.5)/1.2);const y=lerp(384,732,p);line(1108,y,1577,y,C.mint,5);circle(1108,y,9,C.mint)} +} +function scene1(t){ + bg(C.mint); + const p=spring(t-.1,200,18);clipRegion(104,180,1690,398,()=>{text('Meet',119,330-(1-p)*36,106,C.bg,500);text('geoptimize.',110,510-(1-p)*36,186,C.bg,700)}); + const q=spring(t-.5,200,18); + if(t>=.5)clipRegion(100,592,1718,235,()=>transform(960,704,0,.9+.1*q,()=>{rect(-844,-102,1688,204,C.bg,18);text('$',-785,25,54,C.mint,400,'left','Code');const cmd='npx geoptimize scan ./dist --dir';const n=Math.floor(clamp((t-.7)/1.45)*cmd.length);text(cmd.slice(0,n),-704,25,51,C.ink,400,'left','Code');if(t<2.4){const w=ctx.measureText(cmd.slice(0,n)).width;rect(-697+w,-24,24,55,C.mint,2)}})); + text('Your website. A little more legible.',124,934,43,C.bg,500); + if(t>2.4){const s=spring(t-2.4);transform(1744,323,.08,s,()=>{circle(0,0,75,C.bg);check(0,0,70,C.mint,clamp((t-2.4)/.3))})} +} +function scene2(t){ + bg(C.bg);logo(116,105);text('See where things stand.',116,249,88,C.ink,700); + const q=ease(t/.8);clipRegion(96,306,1700,616,()=>{ctx.save();ctx.translate(0,(1-q)*40); + text('Content readiness',122,360,37,C.muted,500); + text(String(data.before.total),106,654,270,C.ink,700);text('/ 100',456,642,67,C.muted,500); + text('3 pages checked',126,734,37,C.ink,500); + text('Synthetic demo site',126,790,31,C.muted,400); + const ds=data.before.dimensions; + ds.forEach((d,i)=>{const y=390+i*108;const v=ease((t-.3-i*.13)/1);text(d.name,735,y,34,C.ink,500);text(`${d.value} / ${d.max}`,1772,y,31,C.muted,400,'right','Code');rect(735,y+23,1038,16,C.rail,8);rect(735,y+23,1038*d.value/d.max*v,16,i===2?C.mint:(i===0?C.pink:C.mint),8)}); + ctx.restore();}); + text('Findings you can inspect. Evidence you can open.',120,987,36,C.mint,500); +} +function metricRow(y,title,before,after,t,delay){ + line(880,y-49,1765,y-49,'#c7cbc4',2);text(title,897,y+5,32,C.bg,500); + text(String(before),1428,y+10,56,'#737b75',500,'center'); + const p=spring(t-delay);line(1486,y-9,1550,y-9,C.bg,3);line(1540,y-18,1550,y-9,C.bg,3);line(1540,y,1550,y-9,C.bg,3); + transform(1670,y-13,0,Math.max(.001,p),()=>{circle(0,0,45,C.mint);text(String(after),0,18,53,C.bg,700,'center')}); +} +function scene3(t){ + bg(C.paper);text('>_',116,105,38,'#188453',600,'left','Code');text('geoptimize',184,105,38,C.bg,700); + const p=spring(t-.1);clipRegion(104,206,700,420,()=>{text('Find it.',119,395-(1-p)*28,161,C.bg,700);text('Fix it.',118,568-(1-p)*28,161,C.bg,700)}); + text('Then check again.',126,680,49,C.bg,500); + clipRegion(865,195,935,730,()=>{ctx.save(); + rect(880,213,899,349,C.bg,16);text('Demo page · before / after',916,270,28,C.muted,500); + text('<title>',919,346,35,C.muted,400,'left','Code'); + text(t<1.55?'Demo':'Demo /',919,398,34,t<1.55?C.pink:C.mint,400,'left','Code'); + text('',919,450,35,C.muted,400,'left','Code'); + if(t>1.6){rect(906,479,820,55,'#1f3b2e',6);text('canonical = https://demo.example/',922,517,28,C.mint,400,'left','Code')} + text('BEFORE',1390,635,24,'#667168',600);text('AFTER',1620,635,24,'#667168',600); + metricRow(716,'Missing canonical',data.before.missingCanonical,data.after.missingCanonical,t,2.1); + metricRow(854,'Duplicate title groups',data.before.duplicateTitleGroups,data.after.duplicateTitleGroups,t,2.38); + ctx.restore();}); + text('Same synthetic site, with revised content.',126,989,31,'#556057',400); +} +function scene4(t){ + bg(C.bg);logo(116,105); + const p=spring(t-.1,200,18);clipRegion(104,218,1700,350,()=>{text('Fits the way',118,358-(1-p)*28,123,C.ink,700);text('you already work.',118,503-(1-p)*28,123,C.mint,700)}); + const x0=145,x1=978;line(x0,709,1773,709,C.line,5); + const progress=ease((t-.5)/1.6);line(x0,709,lerp(x0,1773,progress),709,C.mint,5); + circle(x0,709,13,C.mint);circle(x1,709,13,t>1.3?C.mint:C.line);circle(1773,709,13,t>2?C.mint:C.line); + text('SCAN LOCALLY',123,644,28,C.muted,500); + text('SAVE A REPORT',978,644,28,C.muted,500); + text('geo scan ./dist --dir',124,818,43,C.ink,400,'left','Code'); + text('report.html',979,818,52,C.ink,500); + const q=spring(t-1.6);transform(1695,826,0,Math.max(.001,q),()=>{circle(0,0,46,C.mint);check(0,0,50,C.bg)}); + text('Free and open source. Ready for CI.',123,972,38,C.muted,500); +} +function scene5(t){ + bg(C.mint); + const p=spring(t-.1,200,18);transform(960,249,0,Math.max(.001,p),()=>{text('>_',-115,0,121,C.bg,600,'left','Code')}); + clipRegion(105,302,1710,240,()=>text('geoptimize',960,494-(1-p)*42,181,C.bg,700,'center')); + text('Lint your docs like code.',960,601,66,C.bg,500,'center'); + const q=spring(t-.6,200,18);if(t>=.6)clipRegion(245,680,1430,175,()=>transform(960,761,0,.9+.1*q,()=>{rect(-686,-67,1372,134,C.bg,16);text('npm install --save-dev geoptimize',0,18,46,C.ink,400,'center','Code')})); + text('github.com/cucuwang/geoptimize',960,965,42,C.bg,500,'center'); + // Two punctuation marks arrive like the tool has found its place. + if(t>1.6){const s=spring(t-1.6);transform(1745,235,.12,s,()=>{circle(0,0,61,C.bg);check(0,0,63,C.mint)})} +} +const scenes=[scene0,scene1,scene2,scene3,scene4,scene5]; +function renderScene(i,t){activeScene=i;scenes[i](t)} +function sceneTime(i,t){return t-starts[i]+(i?transitions[i-1].preroll:0)} +function renderTransition(i,t,p){ + const next=i+1,spec=transitions[i],incoming=t-(starts[next]-spec.duration),outgoing=sceneTime(i,t); + if(spec.name==='horizontal-push'){ + bg(C.paper);ctx.save();ctx.translate(-1920*p,0);renderScene(i,outgoing);ctx.restore();ctx.save();ctx.translate(1920*(1-p),0);renderScene(next,incoming);ctx.restore();return; + } + if(spec.name==='beat-cut'){ + const local=incoming/spec.duration; + if(local<.5){bg(C.bg);transform(960,540,0,1+.025*smooth(local*2),()=>{ctx.translate(-960,-540);renderScene(i,outgoing)})} + else{bg(C.mint);transform(960,540,0,lerp(.96,1,ease((local-.5)*2)),()=>{ctx.translate(-960,-540);renderScene(next,incoming-spec.duration/2)})} + return; + } + renderScene(i,outgoing);ctx.save();ctx.beginPath(); + if(spec.name==='circle-reveal')ctx.arc(1548,386,Math.max(0,p*2040),0,Math.PI*2); + if(spec.name==='terminal-open')ctx.rect(0,704-704*p,1920,1408*p); + if(spec.name==='diagonal-sweep'){const x=2400*p;ctx.moveTo(0,0);ctx.lineTo(x,0);ctx.lineTo(x-350,1080);ctx.lineTo(0,1080);ctx.closePath()} + ctx.clip();renderScene(next,incoming);ctx.restore(); +} +function draw(t){ + t=clamp(t,0,30);bounds=[];clips=[];const i=starts.findLastIndex(s=>t>=s);let transition=null; + if(i<5&&t>starts[i+1]-transitions[i].duration){transition=transitions[i].name;const p=smooth((t-(starts[i+1]-transitions[i].duration))/transitions[i].duration);renderTransition(i,t,p)} + else renderScene(i,sceneTime(i,t)); + window.__state={t,scene:i,transition,bounds}; +} +window.__seek=t=>{playing=false;held=t;draw(t);return {t,scene:window.__state.scene}}; +window.__hold=window.__seek; +window.__probe=()=>window.__state; +const play=document.querySelector('#play'),seek=document.querySelector('#seek'),clock=document.querySelector('#time'),music=document.querySelector('#music'); +function syncUI(t){seek.value=t;clock.value=`0:${String(Math.floor(t)).padStart(2,'0')}`;play.textContent=playing?'Pause':'Play'} +function pause(){playing=false;music.pause();syncUI(held)} +function tick(now){if(playing){held=clamp((now-origin)/1000,0,30);draw(held);syncUI(held);if(held>=30)pause()}requestAnimationFrame(tick)} +play.addEventListener('click',()=>{if(playing){pause();return}if(held>=30)held=0;origin=performance.now()-held*1000;playing=true;if(soundOn){music.currentTime=held;music.play().catch(()=>{})}syncUI(held)}); +seek.addEventListener('input',()=>{pause();held=Number(seek.value);draw(held);music.currentTime=held;syncUI(held)}); +document.querySelector('#sound').addEventListener('click',e=>{soundOn=!soundOn;e.target.textContent=soundOn?'Sound on':'Sound off';e.target.setAttribute('aria-pressed',String(soundOn));if(soundOn&&playing){music.currentTime=held;music.play().catch(()=>{})}else music.pause()}); +window.__ready=(async()=>{ + data=await (await fetch('./source-data.json')).json(); + await Promise.all([document.fonts.load('700 160px Space'),document.fonts.load('400 46px Code')]); + if(new URLSearchParams(location.search).has('render'))document.body.classList.add('render'); + draw(0);requestAnimationFrame(tick);return true; +})(); diff --git a/marketing/geoptimize-preview-20260916/soundtrack.py b/marketing/geoptimize-preview-20260916/soundtrack.py new file mode 100644 index 0000000..ff1344f --- /dev/null +++ b/marketing/geoptimize-preview-20260916/soundtrack.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +"""Render the original GeoOptimize preview soundtrack. + +Reproducibility: run this file with +`python3 soundtrack.py` with NumPy installed +from this directory. The fixed seed, 48 kHz sample rate, 30.0-second length, +and all synthesis parameters are defined below; no downloaded samples or +external sound libraries are used. It writes `assets/soundtrack.wav` as +16-bit stereo PCM and prints concise readback measurements. +""" + +from __future__ import annotations + +import math +import re +import subprocess +import wave +from pathlib import Path + +import numpy as np + + +SEED = 20260916 +SAMPLE_RATE = 48_000 +DURATION_SECONDS = 30.0 +SAMPLES = int(SAMPLE_RATE * DURATION_SECONDS) +BPM = 128 +BEAT = 60.0 / BPM +OUTFILE = Path(__file__).parent / "assets" / "soundtrack.wav" +RNG = np.random.default_rng(SEED) + +NOTE = { + "D2": 38, "E2": 40, "F#2": 42, "G2": 43, "A2": 45, "B2": 47, + "D3": 50, "E3": 52, "F#3": 54, "G3": 55, "A3": 57, "B3": 59, + "C#4": 61, "D4": 62, "E4": 64, "F#4": 66, "G4": 67, "A4": 69, + "B4": 71, "C#5": 73, "D5": 74, "E5": 76, "F#5": 78, "A5": 81, +} + + +def hz(name: str) -> float: + return 440.0 * 2.0 ** ((NOTE[name] - 69) / 12.0) + + +mix = np.zeros((SAMPLES, 2), dtype=np.float64) + + +def section_gain(time: float) -> float: + """Phrase boundaries follow the supplied picture timing, with soft edges.""" + if time < 4.7: + return 0.56 + if time < 12.0: + return 0.80 + if time < 21.0: + return 1.00 + if time < 25.0: + return 1.10 + if time < 29.0: + return 0.92 + return 0.0 + + +def add_stereo(start: float, mono: np.ndarray, gain: float = 1.0, + pan: float = 0.0, width_delay: int = 0) -> None: + """Place a finite mono voice into the stereo timeline with equal-power pan.""" + index = int(round(start * SAMPLE_RATE)) + if index >= SAMPLES or index + len(mono) <= 0: + return + source_lo = max(0, -index) + dest_lo = max(0, index) + dest_hi = min(SAMPLES, index + len(mono)) + source_hi = source_lo + (dest_hi - dest_lo) + signal = mono[source_lo:source_hi] * gain + left = math.cos((pan + 1.0) * math.pi / 4.0) + right = math.sin((pan + 1.0) * math.pi / 4.0) + mix[dest_lo:dest_hi, 0] += signal * left + mix[dest_lo:dest_hi, 1] += signal * right + if width_delay: + delayed_lo = dest_lo + width_delay + delayed_hi = dest_hi + width_delay + if delayed_lo < SAMPLES: + clipped_hi = min(SAMPLES, delayed_hi) + delay_signal = signal[:clipped_hi - delayed_lo] + mix[delayed_lo:clipped_hi, 0] += delay_signal * right * 0.18 + mix[delayed_lo:clipped_hi, 1] += delay_signal * left * 0.18 + + +def exp_env(length: int, attack: float, decay: float, release: float = 0.0) -> np.ndarray: + t = np.arange(length) / SAMPLE_RATE + envelope = np.minimum(1.0, t / max(attack, 1.0 / SAMPLE_RATE)) + envelope *= np.exp(-t / decay) + if release: + envelope *= np.minimum(1.0, (length / SAMPLE_RATE - t) / release) + return np.maximum(envelope, 0.0) + + +def pluck(note: str, start: float, length: float = 0.52, gain: float = 0.20, + pan: float = 0.0) -> None: + count = max(1, int(length * SAMPLE_RATE)) + t = np.arange(count) / SAMPLE_RATE + frequency = hz(note) + # Rounded, harmonic-rich pluck with a quiet octave body and no sharp edge. + voice = ( + np.sin(2 * np.pi * frequency * t) + + 0.33 * np.sin(2 * np.pi * frequency * 2.0 * t + 0.22) + + 0.12 * np.sin(2 * np.pi * frequency * 3.0 * t + 0.54) + + 0.08 * np.sin(2 * np.pi * frequency * 0.5 * t) + ) + voice *= exp_env(count, 0.009, 0.26, 0.03) + add_stereo(start, voice, gain, pan, width_delay=11) + + +def pad(notes: tuple[str, ...], start: float, length: float, gain: float) -> None: + count = max(1, int(length * SAMPLE_RATE)) + t = np.arange(count) / SAMPLE_RATE + envelope = np.minimum(1.0, t / 0.18) * np.minimum(1.0, (length - t) / 0.34) + envelope = np.maximum(envelope, 0.0) * (0.86 + 0.14 * np.sin(2 * np.pi * 0.22 * t)) + for offset, name in enumerate(notes): + frequency = hz(name) + vibrato = 0.003 * np.sin(2 * np.pi * (0.18 + offset * 0.03) * t) + voice = ( + 0.74 * np.sin(2 * np.pi * frequency * (1.0 + vibrato) * t + offset * 0.3) + + 0.18 * np.sin(2 * np.pi * frequency * 2 * t + 0.4) + ) * envelope + add_stereo(start, voice, gain / len(notes), (-0.42 + offset * 0.30), width_delay=17 + offset * 4) + + +def bass(note: str, start: float, length: float = 0.39, gain: float = 0.27) -> None: + count = max(1, int(length * SAMPLE_RATE)) + t = np.arange(count) / SAMPLE_RATE + frequency = hz(note) + # Sine plus a gentle odd harmonic stays audible on small speakers. + voice = np.sin(2 * np.pi * frequency * t) + 0.22 * np.sin(2 * np.pi * frequency * 3 * t) + voice *= exp_env(count, 0.014, 0.23, 0.04) + add_stereo(start, voice, gain, -0.03) + + +def kick(start: float, gain: float) -> None: + count = int(0.27 * SAMPLE_RATE) + t = np.arange(count) / SAMPLE_RATE + frequency = 47.0 + 58.0 * np.exp(-t / 0.045) + phase = 2 * np.pi * np.cumsum(frequency) / SAMPLE_RATE + voice = (np.sin(phase) + 0.10 * np.sin(2 * phase)) * exp_env(count, 0.003, 0.105, 0.035) + add_stereo(start, voice, gain) + + +def snare(start: float, gain: float) -> None: + count = int(0.17 * SAMPLE_RATE) + t = np.arange(count) / SAMPLE_RATE + noise = RNG.standard_normal(count) + # A brief, smoothed pop replaces abrasive white-noise snare fizz. + smooth = np.convolve(noise, np.ones(17) / 17.0, mode="same") + voice = 0.36 * smooth + 0.26 * np.sin(2 * np.pi * 188 * t) + voice *= exp_env(count, 0.004, 0.050, 0.028) + add_stereo(start, voice, gain, 0.04, width_delay=7) + + +def digital_pop(start: float, gain: float, pan: float) -> None: + count = int(0.095 * SAMPLE_RATE) + t = np.arange(count) / SAMPLE_RATE + frequency = 900.0 + 520.0 * np.exp(-t / 0.020) + phase = 2 * np.pi * np.cumsum(frequency) / SAMPLE_RATE + voice = (np.sin(phase) + 0.22 * np.sin(2 * phase)) * exp_env(count, 0.002, 0.026, 0.015) + add_stereo(start, voice, gain, pan, width_delay=5) + + +def warm_riser(start: float, length: float, gain: float) -> None: + count = int(length * SAMPLE_RATE) + t = np.arange(count) / SAMPLE_RATE + frequency = 180.0 + 520.0 * (t / length) ** 1.45 + phase = 2 * np.pi * np.cumsum(frequency) / SAMPLE_RATE + envelope = np.sin(np.pi * t / length) ** 1.35 + voice = (np.sin(phase) + 0.13 * np.sin(0.5 * phase)) * envelope + add_stereo(start, voice, gain, -0.10, width_delay=23) + + +def add_transition_markers() -> None: + """Give the picture-cut times a musical cue even where they sit between bars.""" + # 4.70s: groove pickup arrives before the next bar's full drum pattern. + bass("B2", 4.70, 0.38, 0.17) + kick(4.70, 0.17) + kick(4.70 + BEAT, 0.13) + snare(4.70 + BEAT, 0.085) + pluck("F#4", 4.70 + BEAT * 0.5, gain=0.090, pan=0.20) + + # 12.00s: a small bright response signals the denser product-demo section. + digital_pop(12.00, 0.038, -0.56) + pluck("D5", 12.00, gain=0.145, pan=0.22) + pluck("F#4", 12.00 + BEAT * 0.5, gain=0.092, pan=-0.24) + bass("D2", 12.00, 0.36, 0.18) + + # 21.00s: gentle lift, deliberately rounded instead of a harsh sweep. + warm_riser(21.00, 0.98, 0.042) + digital_pop(21.00 + BEAT * 0.65, 0.035, 0.52) + pluck("A4", 21.00 + BEAT * 0.25, gain=0.105, pan=-0.18) + + +def render_cta() -> None: + """A short D-major landing for the call to action, ending before the tail.""" + pad(("D3", "F#3", "A3", "C#4"), 25.00, 4.35, 0.072) + cta_hook = ("D4", "A4", "F#4", "C#5", "A4", "F#4", "E4", "D4") + for step, name in enumerate(cta_hook): + start = 25.00 + step * BEAT / 2 + pluck(name, start, length=0.48, gain=0.112 + 0.010 * (step % 2), + pan=-0.24 if step % 2 == 0 else 0.24) + for step, name in enumerate(("F#4", "A4", "D5", "A4", "F#4")): + start = 27.05 + step * BEAT / 2 + pluck(name, start, length=0.42, gain=0.088, pan=0.20 if step % 2 else -0.20) + for start, accent in ((25.00, 1.0), (25.00 + BEAT, 0.68), + (25.00 + BEAT * 2, 0.88), (25.00 + BEAT * 3, 0.56), + (27.05, 0.76), (27.05 + BEAT * 2, 0.66)): + if start < 29.0: + bass("D2", start, 0.35, 0.20 * accent) + kick(start, 0.19 * accent) + snare(25.00 + BEAT, 0.095) + snare(25.00 + BEAT * 3, 0.080) + digital_pop(26.15, 0.026, 0.52) + pluck("D5", 28.12, length=0.72, gain=0.115, pan=0.10) + pluck("A4", 28.37, length=0.60, gain=0.078, pan=-0.20) + pad(("D3", "F#3", "A3", "C#4"), 28.40, 0.92, 0.052) + + +# Four-chord identity: Dmaj7, Aadd9, Bm7, Gmaj7. Each chord lasts one bar. +CHORDS = ( + (("D3", "F#3", "A3", "C#4"), "D2", ("D4", "A4", "F#4", "C#5", "A4", "F#4", "E4", "A4")), + (("A2", "C#4", "E4", "B4"), "A2", ("E4", "B4", "C#5", "E5", "B4", "A4", "E4", "C#5")), + (("B2", "D3", "F#3", "A3"), "B2", ("F#4", "A4", "B4", "D5", "A4", "F#4", "D4", "A4")), + (("G2", "B2", "D3", "F#3"), "G2", ("D4", "G4", "B4", "D5", "B4", "A4", "G4", "D4")), +) + + +def render_arrangement() -> None: + bar = BEAT * 4 + # The looping section stops at the CTA cut. Its last tails can cross 25.0s. + bar_count = math.ceil(25.0 / bar) + for bar_index in range(bar_count): + start = bar_index * bar + chord, root, hook = CHORDS[bar_index % len(CHORDS)] + intensity = section_gain(start) + if intensity <= 0: + continue + remaining = min(bar, 25.0 - start) + pad(chord, start, remaining + 0.30, 0.075 * intensity) + + # Opening arrives as a sparse invitation; the hook then becomes the motif. + if start < 4.7: + pluck(hook[0], start + 0.00, gain=0.13, pan=-0.20) + pluck(hook[3], start + BEAT * 1.5, gain=0.11, pan=0.17) + pluck(hook[5], start + BEAT * 2.75, gain=0.10, pan=-0.10) + else: + note_count = 8 if start < 21.0 else 10 + for step in range(note_count): + note_time = start + step * BEAT / 2 + if note_time >= 25.0: + break + hook_note = hook[step % len(hook)] + swing = 0.018 if step % 2 else 0.0 + pluck(hook_note, note_time + swing, gain=(0.13 + 0.018 * (step % 3)) * intensity, + pan=(-0.25 if step % 2 == 0 else 0.25)) + if start >= 12.0 and step in (2, 6): + pluck(hook_note, note_time + 0.102, length=0.30, gain=0.055 * intensity, + pan=(0.48 if step % 2 else -0.48)) + + if start >= 4.7: + # Bouncy bass uses two accented beats and one anticipatory eighth. + for position, length, accent in ((0.0, 0.39, 1.0), (1.0, 0.32, 0.72), + (2.0, 0.39, 0.92), (2.75, 0.24, 0.56)): + bass_time = start + position * BEAT + if bass_time < 25.0: + bass(root, bass_time, length, 0.23 * intensity * accent) + for beat_index in range(4): + kick_time = start + beat_index * BEAT + if kick_time < 25.0: + kick(kick_time, 0.24 * intensity * (1.05 if beat_index in (0, 2) else 0.78)) + for beat_index in (1, 3): + snare_time = start + beat_index * BEAT + if snare_time < 25.0: + snare(snare_time, 0.13 * intensity) + + # Digital details are intentionally intermittent, leaving visual text breathing room. + if start >= 12.0: + for step in (1, 5): + pop_time = start + step * BEAT / 2 + 0.032 + if pop_time < 25.0: + digital_pop(pop_time, 0.030 * intensity, -0.58 if step == 1 else 0.58) + if 21.0 <= start < 25.0: + warm_riser(start + BEAT * 2.5, min(BEAT * 1.15, 25.0 - (start + BEAT * 2.5)), 0.035) + + add_transition_markers() + render_cta() + + +def finalize_and_write() -> tuple[float, float]: + # The final second is a fully quiet tail. A cosine fade starts at 29.0 exactly. + fade_start = int(29.0 * SAMPLE_RATE) + fade = 0.5 + 0.5 * np.cos(np.linspace(0.0, np.pi, SAMPLES - fade_start, endpoint=True)) + mix[fade_start:] *= fade[:, None] + if not np.isfinite(mix).all(): + raise RuntimeError("Synthesis produced a non-finite sample") + + # Gentle tanh limiting avoids transient clipping while keeping the text-friendly mix restrained. + limited = np.tanh(mix * 1.16) / math.tanh(1.16) + rms = float(np.sqrt(np.mean(limited ** 2))) + # Empirically calibrated for this arrangement to land near -18 LUFS by EBU R128. + target_rms = 10.0 ** (-20.1 / 20.0) + if rms > 0: + limited *= target_rms / rms + peak_limit = 10.0 ** (-1.65 / 20.0) + peak = float(np.max(np.abs(limited))) + if peak > peak_limit: + limited *= peak_limit / peak + if not np.isfinite(limited).all() or np.max(np.abs(limited)) > 1.0: + raise RuntimeError("Invalid output level") + + pcm = np.round(np.clip(limited, -1.0, 1.0) * 32767.0).astype(" str: + try: + report = subprocess.run( + ["ffmpeg", "-hide_banner", "-nostats", "-i", str(path), "-filter_complex", "ebur128=peak=true", "-f", "null", "-"], + check=True, capture_output=True, text=True, + ).stderr + values = re.findall(r"I:\s*(-?\d+(?:\.\d+)?)\s*LUFS", report) + return values[-1] if values else "unavailable" + except (OSError, subprocess.CalledProcessError): + return "unavailable" + + +def main() -> None: + render_arrangement() + peak, rms = finalize_and_write() + with wave.open(str(OUTFILE), "rb") as wav: + frames = wav.getnframes() + rate = wav.getframerate() + channels = wav.getnchannels() + width = wav.getsampwidth() * 8 + print( + f"wrote={OUTFILE} duration={frames / rate:.6f}s samples={frames} " + f"format={rate}Hz/{channels}ch/{width}-bit peak={20 * math.log10(peak):.2f}dBFS " + f"rms={20 * math.log10(rms):.2f}dBFS integrated={measured_lufs(OUTFILE)}LUFS bytes={OUTFILE.stat().st_size}" + ) + + +if __name__ == "__main__": + main() diff --git a/marketing/geoptimize-preview-20260916/source-data.json b/marketing/geoptimize-preview-20260916/source-data.json new file mode 100644 index 0000000..1068cc3 --- /dev/null +++ b/marketing/geoptimize-preview-20260916/source-data.json @@ -0,0 +1,51 @@ +{ + "sourceCommit": "8872148f444a51177af75cb03ee861b8326f1787", + "sourceUrls": { + "mainCommit": "https://api.github.com/repos/cucuwang/geoptimize/commits/main", + "commitRecord": "https://api.github.com/repos/cucuwang/geoptimize/commits/8872148f444a51177af75cb03ee861b8326f1787", + "readme": "https://raw.githubusercontent.com/cucuwang/geoptimize/8872148f444a51177af75cb03ee861b8326f1787/README.md", + "beforeReport": "https://raw.githubusercontent.com/cucuwang/geoptimize/8872148f444a51177af75cb03ee861b8326f1787/docs/assets/report-demo.html", + "afterReport": "https://raw.githubusercontent.com/cucuwang/geoptimize/8872148f444a51177af75cb03ee861b8326f1787/docs/assets/report-demo-after.html", + "fixtureScript": "https://raw.githubusercontent.com/cucuwang/geoptimize/8872148f444a51177af75cb03ee861b8326f1787/scripts/prepare-metrics-demo.mjs" + }, + "synthetic": true, + "exampleSite": { + "origin": "https://demo.example", + "pages": [ + "https://demo.example/", + "https://demo.example/guide", + "https://demo.example/contact" + ], + "readinessPagesAssessed": 3, + "siteAudit": { + "crawledPages": 3, + "maxPages": 20, + "source": "response-html", + "queueCompleted": true + } + }, + "before": { + "total": 53, + "dimensions": [ + { "name": "Structure", "value": 9, "max": 25 }, + { "name": "Citability", "value": 11, "max": 25 }, + { "name": "Schema", "value": 20, "max": 20 }, + { "name": "AI Metadata", "value": 5, "max": 15 }, + { "name": "Content Density", "value": 8, "max": 15 } + ], + "missingCanonical": 3, + "duplicateTitleGroups": 1 + }, + "after": { + "total": 97, + "dimensions": [ + { "name": "Structure", "value": 25, "max": 25 }, + { "name": "Citability", "value": 25, "max": 25 }, + { "name": "Schema", "value": 20, "max": 20 }, + { "name": "AI Metadata", "value": 15, "max": 15 }, + { "name": "Content Density", "value": 12, "max": 15 } + ], + "missingCanonical": 0, + "duplicateTitleGroups": 0 + } +} diff --git a/marketing/geoptimize-preview-20260916/source-notes.md b/marketing/geoptimize-preview-20260916/source-notes.md new file mode 100644 index 0000000..9e45ade --- /dev/null +++ b/marketing/geoptimize-preview-20260916/source-notes.md @@ -0,0 +1,19 @@ +# geoptimize 預覽影片來源備註 + +資料來源固定為 GitHub `main` 在 2026-09-16 讀取到的 commit `8872148f444a51177af75cb03ee861b8326f1787`。提交 API 與每個固定 commit 的 raw 連結都列在 `source-data.json`。 + +兩份 HTML 報表與 `scripts/prepare-metrics-demo.mjs` 都明示這是合成示範。腳本只模擬 `https://demo.example` 的三個頁面:`/`、`/guide`、`/contact`,攔截 fetch,沒有對外網路請求或外部量測。site audit 的範圍是 response HTML、三個已爬頁面、上限 20,佇列完成。畫面應標示「合成三頁示範」。 + +可安全上鏡的文案: + +- 「檢查五個內容就緒面向:結構、可引用性、結構化資料、AI Metadata、內容密度。」 +- 「合成三頁示範的內容就緒分數:53 分到 97 分。」 +- 「這次合成掃描顯示的 canonical 遺漏:3 到 0;重複 title 群組:1 到 0。」 +- 「掃描可輸出 JSON,README 將 `--json` 列為穩定自動化介面。」 +- 「可在本機、CI 或 pre-commit 使用。」 + +示範中可具體點出的掃描發現:首頁少 H1、部分頁面沒有 headings 或 meta description、contact 頁有 `noindex`、三個頁面缺 canonical 且共用 `Demo` title。修正版報表的 scoring findings 為 0。 + +README 將分數定義為同一專案內捕捉回歸的版本化 heuristic。它不預測搜尋排名、索引、rich results、流量、轉換或任何 AI 系統的引用。影片避免宣稱排名提升、被 AI 引用或實際網站成效。`audit-site` 也只覆蓋受限爬取,不能代表整個已索引網站。 + +README 已確認的功能:`scan`、單頁 `audit`、受限 `audit-site`、離線 HTML `report`、`metrics` 比較、`audit-build`;常用穩定自動化命令為 `npx geoptimize scan ./dist --dir --json`。README 要求 Node.js 22.12 以上。