storymode-cli 1.2.2 → 1.2.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/package.json +1 -1
- package/src/api.mjs +23 -0
- package/src/cli.mjs +23 -2
- package/src/player.mjs +107 -0
package/package.json
CHANGED
package/src/api.mjs
CHANGED
|
@@ -49,3 +49,26 @@ export async function fetchCharacter(galleryId) {
|
|
|
49
49
|
const { buffer } = await request(`${BASE}/gallery/${galleryId}/character`);
|
|
50
50
|
return JSON.parse(buffer.toString('utf-8'));
|
|
51
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetch raw PNG frames (base64) for a gallery animation.
|
|
55
|
+
* Returns { fps, frame_count, frames: [Buffer, ...] }
|
|
56
|
+
*/
|
|
57
|
+
export async function fetchPngFrames(galleryId, { animId } = {}) {
|
|
58
|
+
let url = `${BASE}/gallery/${galleryId}/png-frames`;
|
|
59
|
+
if (animId) url += `?anim_id=${animId}`;
|
|
60
|
+
const { buffer, contentType } = await request(url);
|
|
61
|
+
let json;
|
|
62
|
+
if (contentType.includes('gzip') || buffer[0] === 0x1f) {
|
|
63
|
+
json = gunzipSync(buffer).toString('utf-8');
|
|
64
|
+
} else {
|
|
65
|
+
json = buffer.toString('utf-8');
|
|
66
|
+
}
|
|
67
|
+
const data = JSON.parse(json);
|
|
68
|
+
// Decode base64 frames to Buffers
|
|
69
|
+
return {
|
|
70
|
+
fps: data.fps,
|
|
71
|
+
frame_count: data.frame_count,
|
|
72
|
+
frames: data.frames.map(b64 => Buffer.from(b64, 'base64')),
|
|
73
|
+
};
|
|
74
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { createInterface } from 'node:readline';
|
|
3
|
-
import { fetchFrames, fetchCharacter } from './api.mjs';
|
|
4
|
-
import { playAnimation, playCompanion, playReactiveCompanion, showFrame } from './player.mjs';
|
|
3
|
+
import { fetchFrames, fetchCharacter, fetchPngFrames } from './api.mjs';
|
|
4
|
+
import { playAnimation, playCompanion, playReactiveCompanion, showFrame, playHdAnimation, detectGraphicsProtocol } from './player.mjs';
|
|
5
5
|
import { browse } from './browse.mjs';
|
|
6
6
|
import { startMcpServer } from './mcp.mjs';
|
|
7
7
|
import { loadAnimations, clearCache } from './cache.mjs';
|
|
@@ -86,6 +86,7 @@ const HELP = `
|
|
|
86
86
|
--tiny Tiny companion pane
|
|
87
87
|
--no-ai Disable AI personality (preset speech only)
|
|
88
88
|
--model=ID Anthropic model for AI personality
|
|
89
|
+
--hd Pixel-perfect rendering (iTerm2/Kitty/Ghostty)
|
|
89
90
|
--classic Full-screen animation viewer (non-reactive)
|
|
90
91
|
--detach Return control immediately
|
|
91
92
|
|
|
@@ -137,6 +138,26 @@ export async function run(args) {
|
|
|
137
138
|
break;
|
|
138
139
|
}
|
|
139
140
|
|
|
141
|
+
// --hd: pixel-perfect rendering via graphics protocol (iTerm2/Kitty/Ghostty)
|
|
142
|
+
if (flags.hd) {
|
|
143
|
+
const protocol = detectGraphicsProtocol();
|
|
144
|
+
if (!protocol) {
|
|
145
|
+
console.error(' --hd requires a terminal with graphics protocol support');
|
|
146
|
+
console.error(' (iTerm2, Kitty, Ghostty, or WezTerm)');
|
|
147
|
+
process.exit(1);
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
process.stderr.write(' Loading HD frames...\n');
|
|
151
|
+
const pngData = await fetchPngFrames(id);
|
|
152
|
+
process.stderr.write(` ${pngData.frame_count} frames @ ${pngData.fps}fps (${protocol} protocol)\n`);
|
|
153
|
+
await playHdAnimation(pngData.frames, { fps: pngData.fps });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
console.error(`Error: ${err.message}`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
|
|
140
161
|
// Determine size: default full, --compact, --tiny, or explicit --size=
|
|
141
162
|
const size = flags.tiny ? 'tiny' : flags.compact ? 'compact' : (flags.size || 'full');
|
|
142
163
|
const detach = !!flags.detach;
|
package/src/player.mjs
CHANGED
|
@@ -661,3 +661,110 @@ export function showFrame(framesData, info) {
|
|
|
661
661
|
}
|
|
662
662
|
console.log('');
|
|
663
663
|
}
|
|
664
|
+
|
|
665
|
+
// --- Graphics Protocol Rendering (HD mode) ---
|
|
666
|
+
|
|
667
|
+
const CHUNK_SIZE = 4096;
|
|
668
|
+
|
|
669
|
+
/** Detect terminal graphics protocol support */
|
|
670
|
+
export function detectGraphicsProtocol() {
|
|
671
|
+
const termProgram = process.env.TERM_PROGRAM || '';
|
|
672
|
+
const term = process.env.TERM || '';
|
|
673
|
+
if (process.env.KITTY_WINDOW_ID) return 'kitty';
|
|
674
|
+
if (term === 'xterm-ghostty' || process.env.GHOSTTY_RESOURCES_DIR) return 'kitty';
|
|
675
|
+
if (termProgram === 'WezTerm') return 'kitty';
|
|
676
|
+
if (termProgram === 'iTerm.app') return 'iterm2';
|
|
677
|
+
return null;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/** Display PNG via iTerm2 inline images protocol */
|
|
681
|
+
function writeIterm2(pngBuf, cols, rows) {
|
|
682
|
+
const b64 = pngBuf.toString('base64');
|
|
683
|
+
let params = `inline=1;size=${pngBuf.length}`;
|
|
684
|
+
if (cols) params += `;width=${cols}`;
|
|
685
|
+
if (rows) params += `;height=${rows}`;
|
|
686
|
+
params += `;preserveAspectRatio=1`;
|
|
687
|
+
stdout.write(`\x1b]1337;File=${params}:${b64}\x07`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Display PNG via Kitty graphics protocol */
|
|
691
|
+
function writeKitty(pngBuf, cols, rows) {
|
|
692
|
+
const b64 = pngBuf.toString('base64');
|
|
693
|
+
if (b64.length <= CHUNK_SIZE) {
|
|
694
|
+
let params = `a=T,f=100`;
|
|
695
|
+
if (cols) params += `,c=${cols}`;
|
|
696
|
+
if (rows) params += `,r=${rows}`;
|
|
697
|
+
stdout.write(`\x1b_G${params};${b64}\x1b\\`);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
for (let i = 0; i < b64.length; i += CHUNK_SIZE) {
|
|
701
|
+
const chunk = b64.slice(i, i + CHUNK_SIZE);
|
|
702
|
+
const isFirst = i === 0;
|
|
703
|
+
const isLast = i + CHUNK_SIZE >= b64.length;
|
|
704
|
+
let params = `m=${isLast ? 0 : 1}`;
|
|
705
|
+
if (isFirst) {
|
|
706
|
+
params = `a=T,f=100`;
|
|
707
|
+
if (cols) params += `,c=${cols}`;
|
|
708
|
+
if (rows) params += `,r=${rows}`;
|
|
709
|
+
params += `,m=${isLast ? 0 : 1}`;
|
|
710
|
+
}
|
|
711
|
+
stdout.write(`\x1b_G${params};${chunk}\x1b\\`);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Play PNG frames using native graphics protocol.
|
|
717
|
+
* @param {Buffer[]} frames - Array of PNG buffers
|
|
718
|
+
* @param {object} opts - { fps, cols, rows }
|
|
719
|
+
*/
|
|
720
|
+
export function playHdAnimation(frames, opts = {}) {
|
|
721
|
+
const { fps = 16, cols, rows } = opts;
|
|
722
|
+
const interval = 1000 / fps;
|
|
723
|
+
const protocol = detectGraphicsProtocol();
|
|
724
|
+
|
|
725
|
+
if (!protocol) {
|
|
726
|
+
console.error(' No graphics protocol supported. Use without --hd.');
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
let frameIdx = 0;
|
|
731
|
+
let quit = false;
|
|
732
|
+
|
|
733
|
+
// Alt screen, hide cursor, clear
|
|
734
|
+
stdout.write('\x1b[?1049h\x1b[?25l\x1b[2J');
|
|
735
|
+
|
|
736
|
+
function drawFrame() {
|
|
737
|
+
stdout.write('\x1b[H');
|
|
738
|
+
if (protocol === 'kitty') {
|
|
739
|
+
stdout.write(`\x1b_Ga=d,d=a\x1b\\`);
|
|
740
|
+
writeKitty(frames[frameIdx], cols, rows);
|
|
741
|
+
} else {
|
|
742
|
+
writeIterm2(frames[frameIdx], cols, rows);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (stdin.isTTY) {
|
|
747
|
+
stdin.setRawMode(true);
|
|
748
|
+
stdin.resume();
|
|
749
|
+
stdin.on('data', (data) => {
|
|
750
|
+
const key = data.toString();
|
|
751
|
+
if (key === 'q' || key === '\x03') quit = true;
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
return new Promise((resolve) => {
|
|
756
|
+
function tick() {
|
|
757
|
+
if (quit) {
|
|
758
|
+
stdout.write('\x1b[?1049l\x1b[?25h');
|
|
759
|
+
if (stdin.isTTY) stdin.setRawMode(false);
|
|
760
|
+
stdin.pause();
|
|
761
|
+
resolve();
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
764
|
+
drawFrame();
|
|
765
|
+
frameIdx = (frameIdx + 1) % frames.length;
|
|
766
|
+
setTimeout(tick, interval);
|
|
767
|
+
}
|
|
768
|
+
tick();
|
|
769
|
+
});
|
|
770
|
+
}
|