atris 3.23.0 → 3.24.0
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/atris.md +1 -1
- package/bin/atris.js +7 -1
- package/commands/reel.js +128 -0
- package/lib/reel.js +52 -0
- package/package.json +1 -1
package/atris.md
CHANGED
|
@@ -67,7 +67,7 @@ What you ship should not read as generated. The test: if someone said "an AI mad
|
|
|
67
67
|
- **Refuse the wells** (named so you can): purple/indigo gradients, gradient-filled text, glassmorphism, Inter/Roboto defaults, claude-beige, neon-on-dark, hero-metric rows, identical card grids, eyebrow/tracked-caps labels, pulsing live-dots, em dashes.
|
|
68
68
|
- **Commit to constraints.** One distinctive font, one accent hue, a small spacing scale. Taste is subtraction, not addition.
|
|
69
69
|
- **Generate it right.** `atris deck` (slides), `atris deck from <doc.md> --html` (a web page from a plain doc, in the web app's design tokens), `atris site <dir>` (a whole markdown folder into a navigable site), and `atris recap --html` (a memory-updates page) all apply the system by default: own backgrounds and fonts, never the tool's stock template. Output as an AppBlock with `--block` to drop into a web app.
|
|
70
|
-
- **Compound it.** A new tell becomes a project rule in `.atris/slop.rules.json` (`atris slop rules --add`), and a project's brand lives in `.atris/theme.json` (`atris theme create` builds your own by feel, or `atris theme init` scaffolds one) so every deck, page, and site is on-brand by default, and one line of text becomes an on-brand image with `atris card`. The gate and the look grow per project instead of leaning on memory. Taste lives in code, not vibes.
|
|
70
|
+
- **Compound it.** A new tell becomes a project rule in `.atris/slop.rules.json` (`atris slop rules --add`), and a project's brand lives in `.atris/theme.json` (`atris theme create` builds your own by feel, or `atris theme init` scaffolds one) so every deck, page, and site is on-brand by default, and one line of text becomes an on-brand image with `atris card` or a short video with `atris reel`. The gate and the look grow per project instead of leaning on memory. Taste lives in code, not vibes.
|
|
71
71
|
|
|
72
72
|
## voice
|
|
73
73
|
|
package/bin/atris.js
CHANGED
|
@@ -366,6 +366,7 @@ function showHelp() {
|
|
|
366
366
|
console.log(' site - Build a beautiful static site from a folder of markdown');
|
|
367
367
|
console.log(' theme - Brand themes: "theme create" builds your own by feel (deck/html/site)');
|
|
368
368
|
console.log(' card - One line of text into an on-brand image (uses your theme)');
|
|
369
|
+
console.log(' reel - One line of text into a short on-brand video (animated card)');
|
|
369
370
|
console.log(' run - Auto-chain plan→do→review (autonomous loop, auto-pushes)');
|
|
370
371
|
console.log(' run logs - Browse glass run logs (phase reasoning persisted to disk)');
|
|
371
372
|
console.log(' run search - Search phase reasoning across all run logs');
|
|
@@ -831,7 +832,7 @@ if (command === '2' && ['fast', 'pro'].includes(String(firstCommandArg || '').to
|
|
|
831
832
|
const knownCommands = ['init', 'log', 'now', 'radar', 'ctop', 'status', 'analytics', 'visualize', 'brain', 'brainstorm', 'autopilot', 'run', 'plan', 'do', 'review', 'release',
|
|
832
833
|
'activate', '_activate', 'agent', 'chat', 'fast', 'ax', 'console', 'serve', 'login', 'logout', 'whoami', 'switch', 'use', 'accounts', '_resolve', '_profile-email', '_switch-session', 'shell-init', 'update', 'upgrade', 'version', 'help', 'next', 'atris',
|
|
833
834
|
'clean', 'verify', 'search', 'skill', 'member', 'codex-goal', 'app', 'apps', 'learn', 'lesson', 'plugin', 'experiments', 'receipt', 'proof', 'openclaw', 'pull', 'push', 'live', 'align', 'terminal', 'computer', 'diff', 'business', 'sync', 'youtube',
|
|
834
|
-
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'site', 'theme', 'card', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
|
|
835
|
+
'ingest', 'query', 'lint', 'loop', 'pulse', 'task', 'mission', 'probe', 'worktree', 'aeo', 'slop', 'deck', 'site', 'theme', 'card', 'reel', 'improve', 'xp', 'play', 'gm', 'x', 'recap',
|
|
835
836
|
'gmail', 'calendar', 'twitter', 'slack', 'imessage', 'integrations', 'setup', 'clean-workspace', 'cw',
|
|
836
837
|
'fork', 'browse', 'publish', 'sleep', 'wake', 'feedback', 'errors', 'wiki', 'code-review', 'cr', 'soul', 'fleet', 'compile', 'spaceship'];
|
|
837
838
|
|
|
@@ -1370,6 +1371,11 @@ if (command === 'init') {
|
|
|
1370
1371
|
Promise.resolve(require('../commands/card').run(process.argv.slice(3)))
|
|
1371
1372
|
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
1372
1373
|
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1374
|
+
} else if (command === 'reel') {
|
|
1375
|
+
// Reel: one line of text into a short on-brand video (an animated card; frames via Chrome + ffmpeg).
|
|
1376
|
+
Promise.resolve(require('../commands/reel').run(process.argv.slice(3)))
|
|
1377
|
+
.then((code) => process.exit(typeof code === 'number' ? code : 0))
|
|
1378
|
+
.catch((err) => { console.error(`\n✗ Error: ${err.message || err}`); process.exit(1); });
|
|
1373
1379
|
} else if (command === 'aeo') {
|
|
1374
1380
|
// AEO: AI Engine Optimization — credit-metered citation drafting against the customer workspace.
|
|
1375
1381
|
Promise.resolve(require('../commands/aeo').run(process.argv.slice(3)))
|
package/commands/reel.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// atris reel — one line of text into a short, on-brand video (an animated card).
|
|
2
|
+
//
|
|
3
|
+
// atris reel "Ship faster" --theme brand --size square
|
|
4
|
+
// atris reel "It just works." --kind quote --by "a founder" --seconds 3
|
|
5
|
+
//
|
|
6
|
+
// Renders frames with headless Chrome (the same one `atris card` uses) and encodes
|
|
7
|
+
// with ffmpeg. No new dependency. Falls back to a card image if either is missing.
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { execFile, spawnSync } = require('child_process');
|
|
13
|
+
const { promisify } = require('util');
|
|
14
|
+
const pexec = promisify(execFile);
|
|
15
|
+
const { buildReelFrame, reelFrames } = require('../lib/reel');
|
|
16
|
+
const { SIZES, KINDS } = require('../lib/card');
|
|
17
|
+
|
|
18
|
+
function parseFlags(argv) {
|
|
19
|
+
const flags = {}; const pos = [];
|
|
20
|
+
for (let i = 0; i < argv.length; i++) {
|
|
21
|
+
const a = argv[i];
|
|
22
|
+
if (a.startsWith('--')) {
|
|
23
|
+
const key = a.slice(2); const next = argv[i + 1];
|
|
24
|
+
if (next != null && !next.startsWith('--')) { flags[key] = next; i++; } else flags[key] = true;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
pos.push(a);
|
|
28
|
+
}
|
|
29
|
+
return { flags, pos };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function findBin(macApps, names) {
|
|
33
|
+
for (const p of macApps) if (fs.existsSync(p)) return p;
|
|
34
|
+
for (const name of names) {
|
|
35
|
+
const r = spawnSync('command', ['-v', name], { shell: true, encoding: 'utf8' });
|
|
36
|
+
if (r.status === 0 && r.stdout.trim()) return r.stdout.trim();
|
|
37
|
+
}
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const findChrome = () => (process.env.CHROME_PATH && fs.existsSync(process.env.CHROME_PATH))
|
|
41
|
+
? process.env.CHROME_PATH
|
|
42
|
+
: findBin([
|
|
43
|
+
'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
|
|
44
|
+
'/Applications/Chromium.app/Contents/MacOS/Chromium',
|
|
45
|
+
'/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge',
|
|
46
|
+
], ['google-chrome', 'google-chrome-stable', 'chromium', 'chromium-browser', 'chrome']);
|
|
47
|
+
const findFfmpeg = () => findBin(['/opt/homebrew/bin/ffmpeg', '/usr/local/bin/ffmpeg', '/usr/bin/ffmpeg'], ['ffmpeg']);
|
|
48
|
+
|
|
49
|
+
async function renderFrames(chrome, ts, spec, dir, w, h, onDone) {
|
|
50
|
+
const pad = (i) => String(i).padStart(4, '0');
|
|
51
|
+
let idx = 0; let done = 0;
|
|
52
|
+
async function worker() {
|
|
53
|
+
while (idx < ts.length) {
|
|
54
|
+
const i = idx++;
|
|
55
|
+
const { html } = buildReelFrame(spec, ts[i]);
|
|
56
|
+
const hf = path.join(dir, `f-${pad(i)}.html`);
|
|
57
|
+
const pf = path.join(dir, `f-${pad(i)}.png`);
|
|
58
|
+
fs.writeFileSync(hf, html);
|
|
59
|
+
await pexec(chrome, [
|
|
60
|
+
'--headless=new', '--disable-gpu', '--hide-scrollbars', '--force-device-scale-factor=1',
|
|
61
|
+
`--window-size=${w},${h}`, '--virtual-time-budget=2200',
|
|
62
|
+
`--user-data-dir=${path.join(dir, 'ud-' + i)}`,
|
|
63
|
+
`--screenshot=${pf}`, `file://${hf}`,
|
|
64
|
+
], { timeout: 60000 });
|
|
65
|
+
onDone && onDone(++done, ts.length);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const conc = Math.min(5, ts.length);
|
|
69
|
+
await Promise.all(Array.from({ length: conc }, worker));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function run(argv) {
|
|
73
|
+
const { flags, pos } = parseFlags(argv);
|
|
74
|
+
if (pos[0] === 'help' || flags.help) {
|
|
75
|
+
console.log(`\n atris reel — one line of text into a short on-brand video\n
|
|
76
|
+
atris reel "Your headline" [--kind statement|quote|stat] [--theme <name>] [--size square|og|wide|story] [--seconds 2.6]
|
|
77
|
+
flags: --sub --kicker --by --number --label --brand --version --out <file.mp4>\n`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const text = pos.join(' ').trim();
|
|
82
|
+
const kind = flags.kind || 'statement';
|
|
83
|
+
if (!KINDS.includes(kind)) { console.error(` unknown kind "${kind}". try: ${KINDS.join(', ')}`); return 2; }
|
|
84
|
+
const size = flags.size || 'square';
|
|
85
|
+
if (!SIZES[size]) { console.error(` unknown size "${size}". try: ${Object.keys(SIZES).join(', ')}`); return 2; }
|
|
86
|
+
if (kind === 'stat' && !flags.number && !text) { console.error(' stat reels need --number'); return 2; }
|
|
87
|
+
if (kind !== 'stat' && !text) { console.error(' give the reel some text: atris reel "Your headline"'); return 2; }
|
|
88
|
+
|
|
89
|
+
const seconds = Math.max(1, Math.min(8, parseFloat(flags.seconds) || 2.6));
|
|
90
|
+
const fps = 20;
|
|
91
|
+
const spec = {
|
|
92
|
+
kind, text, headline: text, theme: flags.theme, size,
|
|
93
|
+
sub: flags.sub, kicker: flags.kicker, by: flags.by,
|
|
94
|
+
number: flags.number, label: flags.label, brand: flags.brand, version: flags.version,
|
|
95
|
+
};
|
|
96
|
+
const { w, h } = SIZES[size];
|
|
97
|
+
const base = flags.out ? String(flags.out).replace(/\.mp4$/i, '') : `reel-${kind}-${flags.theme || 'atris'}-${size}`;
|
|
98
|
+
const outMp4 = path.resolve(`${base}.mp4`);
|
|
99
|
+
|
|
100
|
+
const chrome = findChrome();
|
|
101
|
+
if (!chrome) { console.error('\n no Chrome found to render frames. install Chrome or set CHROME_PATH.\n'); return 1; }
|
|
102
|
+
const ffmpeg = findFfmpeg();
|
|
103
|
+
if (!ffmpeg) { console.error('\n no ffmpeg found to encode the video. install ffmpeg (brew install ffmpeg).\n'); return 1; }
|
|
104
|
+
|
|
105
|
+
const ts = reelFrames(seconds, fps);
|
|
106
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'atris-reel-'));
|
|
107
|
+
console.log(`\n rendering ${ts.length} frames (${seconds}s, ${size})...`);
|
|
108
|
+
try {
|
|
109
|
+
await renderFrames(chrome, ts, spec, dir, w, h, (d, n) => {
|
|
110
|
+
if (d === n || d % 10 === 0) process.stdout.write(`\r frames ${d}/${n} `);
|
|
111
|
+
});
|
|
112
|
+
process.stdout.write('\n encoding...\n');
|
|
113
|
+
await pexec(ffmpeg, [
|
|
114
|
+
'-y', '-framerate', String(fps), '-i', path.join(dir, 'f-%04d.png'),
|
|
115
|
+
'-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-movflags', '+faststart', outMp4,
|
|
116
|
+
]);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
console.error(`\n render failed: ${String(e.message).split('\n')[0]}`); return 1;
|
|
119
|
+
} finally {
|
|
120
|
+
try { fs.rmSync(dir, { recursive: true, force: true }); } catch {}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(outMp4)) { console.error(' no video produced'); return 1; }
|
|
124
|
+
console.log(`\n ✓ ${kind} reel, ${w}x${h}, ${seconds}s, theme ${flags.theme || 'atris'}\n ${outMp4}\n`);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = { run, findChrome, findFfmpeg };
|
package/lib/reel.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// atris reel — a card, animated. A reel is the card from lib/card.js rendered at
|
|
2
|
+
// progress t in [0,1]: each element fades and rises in on a staggered schedule.
|
|
3
|
+
// Pure: (spec, t) -> HTML for that single frame. commands/reel.js screenshots the
|
|
4
|
+
// frames (same Chrome as card) and ffmpeg-encodes them. No new dependency.
|
|
5
|
+
|
|
6
|
+
const { buildCard } = require('./card');
|
|
7
|
+
|
|
8
|
+
const clamp01 = (x) => Math.max(0, Math.min(1, x));
|
|
9
|
+
const easeOutCubic = (p) => 1 - Math.pow(1 - clamp01(p), 3);
|
|
10
|
+
// reveal progress of an element whose window is [a,b], at global time t
|
|
11
|
+
const win = (t, a, b) => easeOutCubic((t - a) / (b - a));
|
|
12
|
+
|
|
13
|
+
// staggered reveal windows by element class. A kind only has some of these;
|
|
14
|
+
// overriding a class that isn't present is harmless.
|
|
15
|
+
const STAGGER = [
|
|
16
|
+
['.rule', 0.00, 0.16],
|
|
17
|
+
['.kicker', 0.10, 0.30],
|
|
18
|
+
['.qmark', 0.10, 0.30],
|
|
19
|
+
['.headline', 0.18, 0.46],
|
|
20
|
+
['.qtext', 0.20, 0.50],
|
|
21
|
+
['.big', 0.16, 0.46],
|
|
22
|
+
['.statlabel', 0.34, 0.58],
|
|
23
|
+
['.sub', 0.40, 0.62],
|
|
24
|
+
['.by', 0.46, 0.66],
|
|
25
|
+
['.foot', 0.58, 0.80],
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
// CSS that places every element at its reveal state for time t (one-shot, no loops)
|
|
29
|
+
function revealCss(t, dist = 16) {
|
|
30
|
+
const tt = clamp01(t);
|
|
31
|
+
let css = '';
|
|
32
|
+
for (const [sel, a, b] of STAGGER) {
|
|
33
|
+
const p = win(tt, a, b);
|
|
34
|
+
css += `${sel}{opacity:${p.toFixed(3)};transform:translateY(${((1 - p) * dist).toFixed(2)}px)}`;
|
|
35
|
+
}
|
|
36
|
+
return css;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// HTML for a single reel frame at time t. Reuses the card, appends the reveal styles.
|
|
40
|
+
function buildReelFrame(spec, t, opts = {}) {
|
|
41
|
+
const card = buildCard(spec, opts);
|
|
42
|
+
const overrides = `<style id="reel">${revealCss(t)}</style>`;
|
|
43
|
+
return { html: card.html.replace('</head>', `${overrides}</head>`), width: card.width, height: card.height };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// the list of t values for a reel of `seconds` at `fps` (>=2 frames)
|
|
47
|
+
function reelFrames(seconds = 2.6, fps = 20) {
|
|
48
|
+
const n = Math.max(2, Math.round(seconds * fps));
|
|
49
|
+
return Array.from({ length: n }, (_, i) => i / (n - 1));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = { buildReelFrame, revealCss, reelFrames, win, STAGGER };
|