@oh-my-pi/snapcompact 16.1.6 → 16.1.8
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/CHANGELOG.md +6 -0
- package/dist/types/snapcompact.d.ts +3 -4
- package/package.json +5 -5
- package/src/snapcompact.ts +66 -53
package/CHANGELOG.md
CHANGED
|
@@ -496,7 +496,7 @@ export declare function wrap(text: string, width: number): string[];
|
|
|
496
496
|
export declare function geometry(shape: Shape, size?: number): Geometry;
|
|
497
497
|
/** Render one snapcompact frame from already-normalized text. Doc shapes
|
|
498
498
|
* (`columns === 2`) expect one page of `\n`-joined pre-wrapped lines. */
|
|
499
|
-
export declare function render(text: string, shape: Shape, size?: number): RenderedFrame
|
|
499
|
+
export declare function render(text: string, shape: Shape, size?: number): Promise<RenderedFrame>;
|
|
500
500
|
/** Options for {@link renderMany} and {@link frames}. */
|
|
501
501
|
export interface RenderManyOptions {
|
|
502
502
|
/** Explicit shape; wins over `model`. */
|
|
@@ -510,10 +510,9 @@ export interface RenderManyOptions {
|
|
|
510
510
|
}
|
|
511
511
|
/**
|
|
512
512
|
* Render arbitrary text into snapcompact PNG frames as LLM image blocks
|
|
513
|
-
* (first page first).
|
|
514
|
-
* Empty/whitespace-only input yields no frames.
|
|
513
|
+
* (first page first). Empty/whitespace-only input yields no frames.
|
|
515
514
|
*/
|
|
516
|
-
export declare function renderMany(text: string, options?: RenderManyOptions): ImageContent[]
|
|
515
|
+
export declare function renderMany(text: string, options?: RenderManyOptions): Promise<ImageContent[]>;
|
|
517
516
|
/** Frames needed to hold `text` at the given shape/size, without rendering.
|
|
518
517
|
* For doc shapes this wraps the text once and counts pages of `2 * rows`
|
|
519
518
|
* lines; for grid shapes it divides by the frame capacity. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/snapcompact",
|
|
4
|
-
"version": "16.1.
|
|
4
|
+
"version": "16.1.8",
|
|
5
5
|
"description": "Bitmap-frame context compression for vision-capable LLMs",
|
|
6
6
|
"homepage": "https://omp.sh",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"fmt": "biome format --write ."
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@oh-my-pi/pi-ai": "16.1.
|
|
35
|
-
"@oh-my-pi/pi-natives": "16.1.
|
|
36
|
-
"@oh-my-pi/pi-utils": "16.1.
|
|
37
|
-
"@oh-my-pi/pi-wire": "16.1.
|
|
34
|
+
"@oh-my-pi/pi-ai": "16.1.8",
|
|
35
|
+
"@oh-my-pi/pi-natives": "16.1.8",
|
|
36
|
+
"@oh-my-pi/pi-utils": "16.1.8",
|
|
37
|
+
"@oh-my-pi/pi-wire": "16.1.8"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/bun": "^1.3.14"
|
package/src/snapcompact.ts
CHANGED
|
@@ -1151,15 +1151,8 @@ export function geometry(shape: Shape, size: number = shape.frameSize): Geometry
|
|
|
1151
1151
|
|
|
1152
1152
|
const NEWLINES = /\n/g;
|
|
1153
1153
|
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
export function render(text: string, shape: Shape, size: number = shape.frameSize): RenderedFrame {
|
|
1157
|
-
const { cols, rows, capacity } = geometry(shape, size);
|
|
1158
|
-
let visible = text.length - (text.match(DIM_MARKERS)?.length ?? 0);
|
|
1159
|
-
// Doc line separators consume no cell; in the grid they print as a blank.
|
|
1160
|
-
if (shape.columns === 2) visible -= text.match(NEWLINES)?.length ?? 0;
|
|
1161
|
-
const chars = Math.min(visible, capacity);
|
|
1162
|
-
const data = renderSnapcompactPng(text, {
|
|
1154
|
+
function nativeRenderOptions(shape: Shape, size: number) {
|
|
1155
|
+
return {
|
|
1163
1156
|
size,
|
|
1164
1157
|
font: shape.font,
|
|
1165
1158
|
cellWidth: shape.cellWidth,
|
|
@@ -1168,7 +1161,22 @@ export function render(text: string, shape: Shape, size: number = shape.frameSiz
|
|
|
1168
1161
|
variant: shape.variant,
|
|
1169
1162
|
lineRepeat: shape.lineRepeat,
|
|
1170
1163
|
columns: shape.columns,
|
|
1171
|
-
}
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
function renderedChars(text: string, shape: Shape, capacity: number): number {
|
|
1168
|
+
let visible = text.length - (text.match(DIM_MARKERS)?.length ?? 0);
|
|
1169
|
+
// Doc line separators consume no cell; in the grid they print as a blank.
|
|
1170
|
+
if (shape.columns === 2) visible -= text.match(NEWLINES)?.length ?? 0;
|
|
1171
|
+
return Math.min(visible, capacity);
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/** Render one snapcompact frame from already-normalized text. Doc shapes
|
|
1175
|
+
* (`columns === 2`) expect one page of `\n`-joined pre-wrapped lines. */
|
|
1176
|
+
export async function render(text: string, shape: Shape, size: number = shape.frameSize): Promise<RenderedFrame> {
|
|
1177
|
+
const { cols, rows, capacity } = geometry(shape, size);
|
|
1178
|
+
const chars = renderedChars(text, shape, capacity);
|
|
1179
|
+
const data = await renderSnapcompactPng(text, nativeRenderOptions(shape, size));
|
|
1172
1180
|
return { data, cols, rows, chars };
|
|
1173
1181
|
}
|
|
1174
1182
|
|
|
@@ -1198,38 +1206,39 @@ export interface RenderManyOptions {
|
|
|
1198
1206
|
|
|
1199
1207
|
/**
|
|
1200
1208
|
* Render arbitrary text into snapcompact PNG frames as LLM image blocks
|
|
1201
|
-
* (first page first).
|
|
1202
|
-
* Empty/whitespace-only input yields no frames.
|
|
1209
|
+
* (first page first). Empty/whitespace-only input yields no frames.
|
|
1203
1210
|
*/
|
|
1204
|
-
export function renderMany(text: string, options?: RenderManyOptions): ImageContent[] {
|
|
1211
|
+
export async function renderMany(text: string, options?: RenderManyOptions): Promise<ImageContent[]> {
|
|
1205
1212
|
const shape = options?.shape ?? resolveShape(options?.model);
|
|
1206
1213
|
const frameSize = options?.frameSize ?? shape.frameSize;
|
|
1207
1214
|
const geo = geometry(shape, frameSize);
|
|
1208
1215
|
const normalized = normalize(text);
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
mimeType: "image/png",
|
|
1215
|
-
...(shape.imageDetail ? { detail: shape.imageDetail } : {}),
|
|
1216
|
-
});
|
|
1217
|
-
};
|
|
1216
|
+
const cap = options?.maxFrames;
|
|
1217
|
+
// Build the per-frame texts in order first (cheap, synchronous), then fan
|
|
1218
|
+
// the native PNG renders out concurrently — render() is async/off-thread,
|
|
1219
|
+
// so awaiting each before starting the next leaves throughput on the table.
|
|
1220
|
+
const pageTexts: string[] = [];
|
|
1218
1221
|
if (shape.columns === 2) {
|
|
1219
1222
|
const finish = pageFinisher(shape);
|
|
1220
1223
|
for (const page of docPages(normalized, geo)) {
|
|
1221
|
-
if (
|
|
1222
|
-
push(
|
|
1224
|
+
if (cap !== undefined && pageTexts.length >= cap) break;
|
|
1225
|
+
pageTexts.push(finish(page));
|
|
1226
|
+
}
|
|
1227
|
+
} else {
|
|
1228
|
+
for (let offset = 0; offset < normalized.length; offset += geo.capacity) {
|
|
1229
|
+
if (cap !== undefined && pageTexts.length >= cap) break;
|
|
1230
|
+
let chunk = normalized.slice(offset, offset + geo.capacity);
|
|
1231
|
+
if (shape.stopwordDim) chunk = dimStopwords(chunk);
|
|
1232
|
+
pageTexts.push(chunk);
|
|
1223
1233
|
}
|
|
1224
|
-
return frames;
|
|
1225
|
-
}
|
|
1226
|
-
for (let offset = 0; offset < normalized.length; offset += geo.capacity) {
|
|
1227
|
-
if (options?.maxFrames !== undefined && frames.length >= options.maxFrames) break;
|
|
1228
|
-
let chunk = normalized.slice(offset, offset + geo.capacity);
|
|
1229
|
-
if (shape.stopwordDim) chunk = dimStopwords(chunk);
|
|
1230
|
-
push(render(chunk, shape, frameSize));
|
|
1231
1234
|
}
|
|
1232
|
-
|
|
1235
|
+
const rendered = await Promise.all(pageTexts.map(page => render(page, shape, frameSize)));
|
|
1236
|
+
return rendered.map(frame => ({
|
|
1237
|
+
type: "image",
|
|
1238
|
+
data: frame.data,
|
|
1239
|
+
mimeType: "image/png",
|
|
1240
|
+
...(shape.imageDetail ? { detail: shape.imageDetail } : {}),
|
|
1241
|
+
}));
|
|
1233
1242
|
}
|
|
1234
1243
|
|
|
1235
1244
|
/** Frames needed to hold `text` at the given shape/size, without rendering.
|
|
@@ -1482,7 +1491,13 @@ export async function compact<TMessage = Message>(
|
|
|
1482
1491
|
let archiveText = normalize(serializeConversation(llmMessages, options));
|
|
1483
1492
|
|
|
1484
1493
|
const previousArchive = getPreservedArchive(previousPreserveData);
|
|
1485
|
-
const
|
|
1494
|
+
const previousText =
|
|
1495
|
+
previousArchive?.text ??
|
|
1496
|
+
[previousArchive?.textHead, previousArchive?.textTail]
|
|
1497
|
+
.filter((part): part is string => typeof part === "string" && part.length > 0)
|
|
1498
|
+
.join(NEWLINE_GLYPH);
|
|
1499
|
+
const hasPreviousText = previousText.length > 0;
|
|
1500
|
+
const includedPreviousSummary = !hasPreviousText && !!previousSummary;
|
|
1486
1501
|
if (includedPreviousSummary && previousSummary) {
|
|
1487
1502
|
const head = `[Summary of earlier history] ${normalize(previousSummary)}`;
|
|
1488
1503
|
archiveText = archiveText.length > 0 ? `${head} [Recent conversation] ${archiveText}` : head;
|
|
@@ -1493,8 +1508,7 @@ export async function compact<TMessage = Message>(
|
|
|
1493
1508
|
// Re-compacting a snapcompacted history unfolds the prior archive's source
|
|
1494
1509
|
// text and treats it as one coherent transcript: the previous kept source
|
|
1495
1510
|
// ages in ahead of the new history, then the whole thing is re-rendered.
|
|
1496
|
-
|
|
1497
|
-
if (previousText) {
|
|
1511
|
+
if (hasPreviousText) {
|
|
1498
1512
|
archiveText = archiveText.length > 0 ? `${previousText}${NEWLINE_GLYPH}${archiveText}` : previousText;
|
|
1499
1513
|
}
|
|
1500
1514
|
|
|
@@ -1504,34 +1518,33 @@ export async function compact<TMessage = Message>(
|
|
|
1504
1518
|
// Re-render the planned frames, carrying any open dim span across every
|
|
1505
1519
|
// boundary: textHead → frames → textTail.
|
|
1506
1520
|
let dimOpen = layout.textHead.lastIndexOf(DIM_ON) > layout.textHead.lastIndexOf(DIM_OFF);
|
|
1507
|
-
const newFrames: Frame[] = [];
|
|
1521
|
+
const newFrames: Promise<Frame>[] = [];
|
|
1508
1522
|
for (const planned of layout.frames) {
|
|
1509
1523
|
let pageText: string = dimOpen ? DIM_ON + planned.text : planned.text;
|
|
1510
1524
|
dimOpen = pageText.lastIndexOf(DIM_ON) > pageText.lastIndexOf(DIM_OFF);
|
|
1511
1525
|
if (planned.shape.stopwordDim) pageText = dimStopwords(pageText);
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
await Bun.sleep(0);
|
|
1526
|
+
newFrames.push(
|
|
1527
|
+
render(pageText, planned.shape).then(rendered => ({
|
|
1528
|
+
data: rendered.data,
|
|
1529
|
+
mimeType: "image/png",
|
|
1530
|
+
cols: rendered.cols,
|
|
1531
|
+
rows: rendered.rows,
|
|
1532
|
+
chars: rendered.chars,
|
|
1533
|
+
font: planned.shape.font,
|
|
1534
|
+
variant: planned.shape.variant,
|
|
1535
|
+
lineRepeat: planned.shape.lineRepeat,
|
|
1536
|
+
...(planned.shape.columns === 2 ? { columns: 2 } : {}),
|
|
1537
|
+
...(planned.shape.stopwordDim ? { stopwordDim: true } : {}),
|
|
1538
|
+
...(planned.shape.imageDetail ? { detail: planned.shape.imageDetail } : {}),
|
|
1539
|
+
})),
|
|
1540
|
+
);
|
|
1528
1541
|
}
|
|
1529
1542
|
|
|
1530
1543
|
const textHead = layout.textHead;
|
|
1531
1544
|
const textTail = layout.textTail.length > 0 ? (dimOpen ? DIM_ON : "") + layout.textTail : "";
|
|
1532
1545
|
const textChars = textHead.length + textTail.length;
|
|
1533
1546
|
|
|
1534
|
-
const frames = newFrames;
|
|
1547
|
+
const frames = await Promise.all(newFrames);
|
|
1535
1548
|
const totalChars = frames.reduce((sum, frame) => sum + frame.chars, 0) + textChars;
|
|
1536
1549
|
const mixedShapes = frames.some(
|
|
1537
1550
|
frame =>
|