@appinternalleads/ui 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -6
- package/dist/cjs/motion/player.js +3 -0
- package/dist/cjs/motion/player.js.map +1 -1
- package/dist/esm/motion/player.d.ts.map +1 -1
- package/dist/esm/motion/player.js +3 -0
- package/dist/esm/motion/player.js.map +1 -1
- package/docs/TYPE-SURFACE.md +5 -0
- package/package.json +3 -3
- package/src/motion/player.ts +3 -0
- package/tools/ask/dist/components.mjs +3254 -1008
- package/tools/ask/dist/render.js +589 -0
- package/tools/ask/mcp.mjs +203 -34
- package/tools/ask/render.mjs +248 -0
- package/tools/ask/dist/neon.js +0 -69
- package/tools/ask/neon-render.mjs +0 -143
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* **A neon request as a video file.** `render_neon_sign` calls this.
|
|
3
|
-
*
|
|
4
|
-
* The neon is an SVG with filters and web fonts, so a browser has to draw it — the same reason
|
|
5
|
-
* `ask_user` goes to one (`serve.mjs`). The shape is the same too: a one-shot loopback server, a
|
|
6
|
-
* token, a prebuilt page (`dist/neon.js`, the package's `NeonCanvas`) and the request in the
|
|
7
|
-
* document. The difference is who looks: a headless browser steps the page through every frame,
|
|
8
|
-
* photographs each, and ffmpeg joins them.
|
|
9
|
-
*
|
|
10
|
-
* ⚠ **Playwright and ffmpeg are found, not depended on.** The library takes five peers and no
|
|
11
|
-
* more, so neither can be a dependency of the package. Playwright is resolved from the project
|
|
12
|
-
* the server was launched in, then from the package; ffmpeg from `PATH`. When one is missing the
|
|
13
|
-
* tool says which and how to get it, and every other tool of the server keeps working.
|
|
14
|
-
*
|
|
15
|
-
* No sleeps: the page's `ready` promise resolves when fonts, wall and the first frame are in,
|
|
16
|
-
* and each frame's promise resolves after the browser has painted it.
|
|
17
|
-
*/
|
|
18
|
-
import { createServer } from 'node:http';
|
|
19
|
-
import { createRequire } from 'node:module';
|
|
20
|
-
import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
21
|
-
import { randomUUID } from 'node:crypto';
|
|
22
|
-
import { spawn } from 'node:child_process';
|
|
23
|
-
import { cpus, tmpdir } from 'node:os';
|
|
24
|
-
import { dirname, join, resolve } from 'node:path';
|
|
25
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
26
|
-
|
|
27
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
28
|
-
|
|
29
|
-
/** A failure the caller can act on: reported in the tool's result, not as a protocol error. */
|
|
30
|
-
export class NeonRenderError extends Error {}
|
|
31
|
-
|
|
32
|
-
async function chromiumFrom() {
|
|
33
|
-
const tried = [];
|
|
34
|
-
for (const from of [join(process.cwd(), 'package.json'), import.meta.url]) {
|
|
35
|
-
const require = createRequire(from);
|
|
36
|
-
for (const name of ['playwright', 'playwright-core', '@playwright/test']) {
|
|
37
|
-
try {
|
|
38
|
-
const m = await import(pathToFileURL(require.resolve(name)).href);
|
|
39
|
-
const chromium = m.chromium ?? m.default?.chromium;
|
|
40
|
-
if (chromium) return chromium;
|
|
41
|
-
} catch (e) { tried.push(`${name}: ${e.code ?? e.message}`); }
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
throw new NeonRenderError(
|
|
45
|
-
'Rendering needs Playwright to draw the frames, and it is not installed in this project. '
|
|
46
|
-
+ 'Run `npm install -D playwright && npx playwright install chromium`, then call again.',
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function launch(chromium) {
|
|
51
|
-
try { return await chromium.launch(); } catch (first) {
|
|
52
|
-
/* No downloaded browser: the machine's own Chrome draws the same SVG. */
|
|
53
|
-
try { return await chromium.launch({ channel: 'chrome' }); } catch {
|
|
54
|
-
throw new NeonRenderError(`No browser to render with (${String(first.message).split('\n')[0]}). Run \`npx playwright install chromium\`, then call again.`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const CODECS = {
|
|
60
|
-
/* JPEG frames are full-range; players expect video range, so convert rather than tag. */
|
|
61
|
-
mp4: ['-vf', 'scale=out_range=tv', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-color_range', 'tv', '-crf', '16', '-preset', 'medium', '-movflags', '+faststart'],
|
|
62
|
-
webm: ['-vf', 'scale=out_range=tv', '-c:v', 'libvpx-vp9', '-pix_fmt', 'yuv420p', '-color_range', 'tv', '-crf', '28', '-b:v', '0'],
|
|
63
|
-
gif: ['-filter_complex', '[0:v]split[a][b];[a]palettegen=stats_mode=diff[p];[b][p]paletteuse=dither=bayer'],
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
function encode(dir, output, outputPath) {
|
|
67
|
-
return new Promise((res, rej) => {
|
|
68
|
-
const ff = spawn('ffmpeg', ['-y', '-loglevel', 'error', '-framerate', String(output.fps), '-i', join(dir, 'f-%05d.jpg'), ...CODECS[output.format], outputPath], { stdio: ['ignore', 'ignore', 'pipe'] });
|
|
69
|
-
let err = '';
|
|
70
|
-
ff.stderr.on('data', (d) => { err += d; });
|
|
71
|
-
ff.on('error', (e) => rej(new NeonRenderError(e.code === 'ENOENT'
|
|
72
|
-
? 'Rendering needs ffmpeg to join the frames into a video, and it is not on PATH. Install it (`brew install ffmpeg`, `apt install ffmpeg`), then call again.'
|
|
73
|
-
: `ffmpeg did not start: ${e.message}`)));
|
|
74
|
-
ff.on('exit', (code) => (code === 0 ? res() : rej(new NeonRenderError(`ffmpeg failed (${code}): ${err.trim().split('\n').slice(-3).join(' ')}`))));
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* @param {{content: object, output: {format: string, width: number, height: number, durationSeconds: number, fps: number, frameCount: number},
|
|
80
|
-
* outputPath: string, backgroundColor?: string}} job `content` is the request; `output` is what `buildNeonScene` resolved from it.
|
|
81
|
-
* @returns {Promise<{errors: object[], elements?: object[], fonts?: object[], bytes?: number}>} `errors` are the page's validation errors.
|
|
82
|
-
*/
|
|
83
|
-
export async function renderNeon({ content, output, outputPath, backgroundColor = 'black' }) {
|
|
84
|
-
let js;
|
|
85
|
-
try { js = readFileSync(resolve(here, 'dist/neon.js'), 'utf8'); } catch {
|
|
86
|
-
throw new NeonRenderError('The neon render page is missing (tools/ask/dist/neon.js). In this repository run `npm run ask:build`; as an installed package, reinstall without --ignore-scripts.');
|
|
87
|
-
}
|
|
88
|
-
const chromium = await chromiumFrom();
|
|
89
|
-
|
|
90
|
-
const token = randomUUID();
|
|
91
|
-
const json = JSON.stringify({ content }).replace(/</g, '\\u003c');
|
|
92
|
-
const html = '<!doctype html><html><head><meta charset="utf-8"><title>CompPack neon render</title>'
|
|
93
|
-
+ `<style>html,body,#root{margin:0;width:100%;height:100%;overflow:hidden;background:${backgroundColor}}</style></head>`
|
|
94
|
-
+ `<body><div id="root"></div><script id="comppack-neon" type="application/json">${json}</script>`
|
|
95
|
-
+ '<script type="module" src="/neon.js"></script></body></html>';
|
|
96
|
-
const server = createServer((req, res) => {
|
|
97
|
-
/* A throw in a request listener takes the process down (see serve.mjs). */
|
|
98
|
-
let url;
|
|
99
|
-
try { url = new URL(req.url, 'http://127.0.0.1'); } catch { res.writeHead(400).end('bad request'); return; }
|
|
100
|
-
if (req.method === 'GET' && url.pathname === '/' && url.searchParams.get('t') === token) {
|
|
101
|
-
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(html);
|
|
102
|
-
} else if (req.method === 'GET' && url.pathname === '/neon.js' && req.headers.referer?.includes(token)) {
|
|
103
|
-
res.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' }).end(js);
|
|
104
|
-
} else res.writeHead(404).end();
|
|
105
|
-
});
|
|
106
|
-
server.on('clientError', (_e, socket) => socket.destroy());
|
|
107
|
-
const port = await new Promise((res, rej) => { server.once('error', rej); server.listen(0, '127.0.0.1', () => res(server.address().port)); });
|
|
108
|
-
|
|
109
|
-
const dir = mkdtempSync(join(tmpdir(), 'comppack-neon-'));
|
|
110
|
-
let browser;
|
|
111
|
-
try {
|
|
112
|
-
browser = await launch(chromium);
|
|
113
|
-
const context = await browser.newContext({ viewport: { width: output.width, height: output.height }, deviceScaleFactor: 1 });
|
|
114
|
-
const open = async () => {
|
|
115
|
-
const page = await context.newPage();
|
|
116
|
-
await page.goto(`http://127.0.0.1:${port}/?t=${token}`);
|
|
117
|
-
return { page, ready: await page.evaluate(() => globalThis.__neon.ready) };
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/* The first page decides whether the request can be drawn at all, with real fonts measured. */
|
|
121
|
-
const first = await open();
|
|
122
|
-
if (first.ready.errors.length) return { errors: first.ready.errors };
|
|
123
|
-
|
|
124
|
-
/* Frames are independent (the canvas is pure in time), so a few pages draw them side by side. */
|
|
125
|
-
const workers = Math.max(1, Math.min(4, cpus().length - 1, Math.ceil(output.frameCount / 8)));
|
|
126
|
-
const pages = [first.page, ...(await Promise.all(Array.from({ length: workers - 1 }, open))).map((o) => o.page)];
|
|
127
|
-
let next = 0;
|
|
128
|
-
await Promise.all(pages.map(async (page) => {
|
|
129
|
-
for (let i = next++; i < output.frameCount; i = next++) {
|
|
130
|
-
await page.evaluate((ms) => globalThis.__neon.frame(ms), (i * 1000) / output.fps);
|
|
131
|
-
await page.screenshot({ path: join(dir, `f-${String(i).padStart(5, '0')}.jpg`), type: 'jpeg', quality: 95 });
|
|
132
|
-
}
|
|
133
|
-
}));
|
|
134
|
-
|
|
135
|
-
mkdirSync(dirname(outputPath), { recursive: true });
|
|
136
|
-
await encode(dir, output, outputPath);
|
|
137
|
-
return { errors: [], elements: first.ready.elements, fonts: first.ready.fonts, bytes: statSync(outputPath).size };
|
|
138
|
-
} finally {
|
|
139
|
-
await browser?.close().catch(() => {});
|
|
140
|
-
server.close();
|
|
141
|
-
rmSync(dir, { recursive: true, force: true });
|
|
142
|
-
}
|
|
143
|
-
}
|