egaki 0.5.0 → 0.7.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/CHANGELOG.md +188 -0
- package/dist/cli/cache-utils.d.ts +33 -0
- package/dist/cli/cache-utils.d.ts.map +1 -0
- package/dist/cli/cache-utils.js +157 -0
- package/dist/cli/cached-generate.d.ts +105 -0
- package/dist/cli/cached-generate.d.ts.map +1 -0
- package/dist/cli/cached-generate.js +208 -0
- package/dist/cli/cached-generate.test.d.ts +11 -0
- package/dist/cli/cached-generate.test.d.ts.map +1 -0
- package/dist/cli/cached-generate.test.js +431 -0
- package/dist/cli/cartesia-provider.d.ts +16 -0
- package/dist/cli/cartesia-provider.d.ts.map +1 -0
- package/dist/cli/cartesia-provider.js +184 -0
- package/dist/cli/chatgpt-auth.d.ts.map +1 -0
- package/dist/cli/cli.d.ts +4 -0
- package/dist/cli/cli.d.ts.map +1 -0
- package/dist/cli/cli.js +1329 -0
- package/dist/cli/clone-generate.d.ts +26 -0
- package/dist/cli/clone-generate.d.ts.map +1 -0
- package/dist/cli/clone-generate.js +148 -0
- package/dist/{credentials.d.ts → cli/credentials.d.ts} +11 -2
- package/dist/cli/credentials.d.ts.map +1 -0
- package/dist/{credentials.js → cli/credentials.js} +88 -5
- package/dist/cli/elevenlabs-speech-provider.d.ts +26 -0
- package/dist/cli/elevenlabs-speech-provider.d.ts.map +1 -0
- package/dist/cli/elevenlabs-speech-provider.js +199 -0
- package/dist/cli/generate.d.ts +140 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +604 -0
- package/dist/cli/generate.test.d.ts +2 -0
- package/dist/cli/generate.test.d.ts.map +1 -0
- package/dist/cli/generate.test.js +131 -0
- package/dist/cli/login.d.ts.map +1 -0
- package/dist/{login.js → cli/login.js} +45 -4
- package/dist/cli/main.d.ts +3 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +7 -0
- package/dist/cli/model-catalog.d.ts +141 -0
- package/dist/cli/model-catalog.d.ts.map +1 -0
- package/dist/cli/model-catalog.js +2135 -0
- package/dist/{models.d.ts → cli/models.d.ts} +16 -9
- package/dist/cli/models.d.ts.map +1 -0
- package/dist/cli/models.js +313 -0
- package/dist/cli/open-browser.d.ts.map +1 -0
- package/dist/cli/openai-speech-provider.d.ts +3 -0
- package/dist/cli/openai-speech-provider.d.ts.map +1 -0
- package/dist/cli/openai-speech-provider.js +28 -0
- package/dist/cli/separation-generate.d.ts +34 -0
- package/dist/cli/separation-generate.d.ts.map +1 -0
- package/dist/cli/separation-generate.js +117 -0
- package/dist/cli/speech-catalog.d.ts +35 -0
- package/dist/cli/speech-catalog.d.ts.map +1 -0
- package/dist/cli/speech-catalog.js +195 -0
- package/dist/cli/speech-generate.d.ts +86 -0
- package/dist/cli/speech-generate.d.ts.map +1 -0
- package/dist/cli/speech-generate.js +156 -0
- package/dist/cli/speech-models.d.ts +8 -0
- package/dist/cli/speech-models.d.ts.map +1 -0
- package/dist/cli/speech-models.js +109 -0
- package/dist/cli/speech-provider.test.d.ts +2 -0
- package/dist/cli/speech-provider.test.d.ts.map +1 -0
- package/dist/cli/speech-provider.test.js +197 -0
- package/dist/cli/subscription.d.ts.map +1 -0
- package/dist/cli/transcription-catalog.d.ts +35 -0
- package/dist/cli/transcription-catalog.d.ts.map +1 -0
- package/dist/cli/transcription-catalog.js +192 -0
- package/dist/cli/transcription-generate.d.ts +62 -0
- package/dist/cli/transcription-generate.d.ts.map +1 -0
- package/dist/cli/transcription-generate.js +144 -0
- package/dist/cli/transcription-models.d.ts +9 -0
- package/dist/cli/transcription-models.d.ts.map +1 -0
- package/dist/cli/transcription-models.js +100 -0
- package/dist/cli/transcription-providers.d.ts +22 -0
- package/dist/cli/transcription-providers.d.ts.map +1 -0
- package/dist/cli/transcription-providers.js +290 -0
- package/dist/cli/video-model-catalog.d.ts +2 -0
- package/dist/cli/video-model-catalog.d.ts.map +1 -0
- package/dist/cli/video-model-catalog.js +5 -0
- package/dist/cli/xai-auth.d.ts +29 -0
- package/dist/cli/xai-auth.d.ts.map +1 -0
- package/dist/cli/xai-auth.js +325 -0
- package/dist/vite/angled-screen.d.ts +57 -0
- package/dist/vite/angled-screen.d.ts.map +1 -0
- package/dist/vite/angled-screen.js +115 -0
- package/dist/vite/app.d.ts +51 -0
- package/dist/vite/app.d.ts.map +1 -0
- package/dist/vite/app.js +150 -0
- package/dist/vite/bands-shader.d.ts +47 -0
- package/dist/vite/bands-shader.d.ts.map +1 -0
- package/dist/vite/bands-shader.js +267 -0
- package/dist/vite/code-block.d.ts +107 -0
- package/dist/vite/code-block.d.ts.map +1 -0
- package/dist/vite/code-block.js +641 -0
- package/dist/vite/components.d.ts +36 -0
- package/dist/vite/components.d.ts.map +1 -0
- package/dist/vite/components.js +113 -0
- package/dist/vite/dispersion-rings-shader.d.ts +44 -0
- package/dist/vite/dispersion-rings-shader.d.ts.map +1 -0
- package/dist/vite/dispersion-rings-shader.js +282 -0
- package/dist/vite/easing-curves.d.ts +129 -0
- package/dist/vite/easing-curves.d.ts.map +1 -0
- package/dist/vite/easing-curves.js +584 -0
- package/dist/vite/easing-curves.test.d.ts +2 -0
- package/dist/vite/easing-curves.test.d.ts.map +1 -0
- package/dist/vite/easing-curves.test.js +200 -0
- package/dist/vite/generated-media-client.d.ts +35 -0
- package/dist/vite/generated-media-client.d.ts.map +1 -0
- package/dist/vite/generated-media-client.js +76 -0
- package/dist/vite/keyframes.d.ts +129 -0
- package/dist/vite/keyframes.d.ts.map +1 -0
- package/dist/vite/keyframes.js +194 -0
- package/dist/vite/layout-editor.d.ts +45 -0
- package/dist/vite/layout-editor.d.ts.map +1 -0
- package/dist/vite/layout-editor.js +587 -0
- package/dist/vite/layout-transition.d.ts +136 -0
- package/dist/vite/layout-transition.d.ts.map +1 -0
- package/dist/vite/layout-transition.js +581 -0
- package/dist/vite/liquid-gradient-shader.d.ts +62 -0
- package/dist/vite/liquid-gradient-shader.d.ts.map +1 -0
- package/dist/vite/liquid-gradient-shader.js +413 -0
- package/dist/vite/mdx-client.d.ts +15 -0
- package/dist/vite/mdx-client.d.ts.map +1 -0
- package/dist/vite/mdx-client.js +265 -0
- package/dist/vite/mdx-parse.d.ts +102 -0
- package/dist/vite/mdx-parse.d.ts.map +1 -0
- package/dist/vite/mdx-parse.js +251 -0
- package/dist/vite/mdx-provided-components.d.ts +18 -0
- package/dist/vite/mdx-provided-components.d.ts.map +1 -0
- package/dist/vite/mdx-provided-components.js +14 -0
- package/dist/vite/mdx-video.d.ts +327 -0
- package/dist/vite/mdx-video.d.ts.map +1 -0
- package/dist/vite/mdx-video.js +596 -0
- package/dist/vite/mdx-video.test.d.ts +9 -0
- package/dist/vite/mdx-video.test.d.ts.map +1 -0
- package/dist/vite/mdx-video.test.js +1945 -0
- package/dist/vite/media-components.d.ts +45 -0
- package/dist/vite/media-components.d.ts.map +1 -0
- package/dist/vite/media-components.js +355 -0
- package/dist/vite/media-duration-store.d.ts +66 -0
- package/dist/vite/media-duration-store.d.ts.map +1 -0
- package/dist/vite/media-duration-store.js +204 -0
- package/dist/vite/player-page.d.ts +35 -0
- package/dist/vite/player-page.d.ts.map +1 -0
- package/dist/vite/player-page.js +720 -0
- package/dist/vite/render-client.d.ts +18 -0
- package/dist/vite/render-client.d.ts.map +1 -0
- package/dist/vite/render-client.js +67 -0
- package/dist/vite/sdk.d.ts +221 -0
- package/dist/vite/sdk.d.ts.map +1 -0
- package/dist/vite/sdk.js +355 -0
- package/dist/vite/server-components.d.ts +55 -0
- package/dist/vite/server-components.d.ts.map +1 -0
- package/dist/vite/server-components.js +156 -0
- package/dist/vite/server-mdx.d.ts +74 -0
- package/dist/vite/server-mdx.d.ts.map +1 -0
- package/dist/vite/server-mdx.js +259 -0
- package/dist/vite/shader-renderer.d.ts +48 -0
- package/dist/vite/shader-renderer.d.ts.map +1 -0
- package/dist/vite/shader-renderer.js +411 -0
- package/dist/vite/store-hooks.d.ts +12 -0
- package/dist/vite/store-hooks.d.ts.map +1 -0
- package/dist/vite/store-hooks.js +93 -0
- package/dist/vite/store.d.ts +75 -0
- package/dist/vite/store.d.ts.map +1 -0
- package/dist/vite/store.js +52 -0
- package/dist/vite/store.test.d.ts +6 -0
- package/dist/vite/store.test.d.ts.map +1 -0
- package/dist/vite/store.test.js +172 -0
- package/dist/vite/tweakpane-hook.d.ts +94 -0
- package/dist/vite/tweakpane-hook.d.ts.map +1 -0
- package/dist/vite/tweakpane-hook.js +450 -0
- package/dist/vite/vite-plugin.d.ts +28 -0
- package/dist/vite/vite-plugin.d.ts.map +1 -0
- package/dist/vite/vite-plugin.js +538 -0
- package/dist/vite/wave-gradient-shader.d.ts +41 -0
- package/dist/vite/wave-gradient-shader.d.ts.map +1 -0
- package/dist/vite/wave-gradient-shader.js +207 -0
- package/package.json +103 -25
- package/skills/egaki/SKILL.md +162 -1
- package/src/cli/cache-utils.ts +169 -0
- package/src/cli/cached-generate.test.ts +531 -0
- package/src/cli/cached-generate.ts +348 -0
- package/src/cli/cartesia-provider.ts +249 -0
- package/src/cli/cli.ts +1818 -0
- package/src/cli/clone-generate.ts +202 -0
- package/src/{credentials.ts → cli/credentials.ts} +90 -7
- package/src/cli/elevenlabs-speech-provider.ts +243 -0
- package/src/cli/generate.test.ts +166 -0
- package/src/cli/generate.ts +912 -0
- package/src/{login.ts → cli/login.ts} +48 -3
- package/src/cli/main.ts +8 -0
- package/src/cli/model-catalog.ts +2336 -0
- package/src/cli/models.ts +408 -0
- package/src/cli/openai-speech-provider.ts +34 -0
- package/src/cli/separation-generate.ts +187 -0
- package/src/cli/speech-catalog.ts +253 -0
- package/src/cli/speech-generate.ts +275 -0
- package/src/cli/speech-models.ts +146 -0
- package/src/cli/speech-provider.test.ts +226 -0
- package/src/cli/transcription-catalog.ts +250 -0
- package/src/cli/transcription-generate.ts +214 -0
- package/src/cli/transcription-models.ts +136 -0
- package/src/cli/transcription-providers.ts +427 -0
- package/src/cli/video-model-catalog.ts +14 -0
- package/src/cli/xai-auth.ts +396 -0
- package/src/vite/angled-screen.tsx +214 -0
- package/src/vite/app.tsx +197 -0
- package/src/vite/bands-shader.tsx +319 -0
- package/src/vite/code-block.tsx +939 -0
- package/src/vite/components.tsx +278 -0
- package/src/vite/dispersion-rings-shader.tsx +331 -0
- package/src/vite/easing-curves.test.ts +232 -0
- package/src/vite/easing-curves.ts +743 -0
- package/src/vite/generated-media-client.tsx +121 -0
- package/src/vite/keyframes.tsx +347 -0
- package/src/vite/layout-editor.tsx +709 -0
- package/src/vite/layout-transition.tsx +706 -0
- package/src/vite/liquid-gradient-shader.tsx +474 -0
- package/src/vite/mdx-client.tsx +420 -0
- package/src/vite/mdx-parse.ts +343 -0
- package/src/vite/mdx-provided-components.ts +19 -0
- package/src/vite/mdx-video.test.tsx +2219 -0
- package/src/vite/mdx-video.tsx +976 -0
- package/src/vite/media-components.tsx +473 -0
- package/src/vite/media-duration-store.ts +225 -0
- package/src/vite/motion-timing.ts +194 -0
- package/src/vite/player-page.tsx +1107 -0
- package/src/vite/render-client.ts +84 -0
- package/src/vite/sdk.ts +598 -0
- package/src/vite/server-components.tsx +241 -0
- package/src/vite/server-mdx.ts +285 -0
- package/src/vite/shader-renderer.tsx +535 -0
- package/src/vite/store-hooks.ts +117 -0
- package/src/vite/store.test.ts +202 -0
- package/src/vite/store.ts +115 -0
- package/src/vite/styles.css +21 -0
- package/src/vite/tweakpane-hook.tsx +593 -0
- package/src/vite/virtual-modules.d.ts +30 -0
- package/src/vite/vite-plugin.ts +584 -0
- package/src/vite/wave-gradient-shader.tsx +253 -0
- package/README.md +0 -266
- package/dist/antigravity-auth.d.ts +0 -15
- package/dist/antigravity-auth.d.ts.map +0 -1
- package/dist/antigravity-auth.js +0 -544
- package/dist/antigravity-image.d.ts +0 -11
- package/dist/antigravity-image.d.ts.map +0 -1
- package/dist/antigravity-image.js +0 -285
- package/dist/chatgpt-auth.d.ts.map +0 -1
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -968
- package/dist/credentials.d.ts.map +0 -1
- package/dist/gateway-model-candidates.generated.d.ts +0 -11
- package/dist/gateway-model-candidates.generated.d.ts.map +0 -1
- package/dist/gateway-model-candidates.generated.js +0 -64
- package/dist/login.d.ts.map +0 -1
- package/dist/model-catalog.d.ts +0 -42
- package/dist/model-catalog.d.ts.map +0 -1
- package/dist/model-catalog.js +0 -1001
- package/dist/models.d.ts.map +0 -1
- package/dist/models.js +0 -246
- package/dist/open-browser.d.ts.map +0 -1
- package/dist/subscription.d.ts.map +0 -1
- package/dist/video-model-catalog.d.ts +0 -52
- package/dist/video-model-catalog.d.ts.map +0 -1
- package/dist/video-model-catalog.js +0 -306
- package/src/cli.ts +0 -1378
- package/src/model-catalog.ts +0 -1077
- package/src/models.ts +0 -309
- package/src/video-model-catalog.ts +0 -363
- /package/dist/{chatgpt-auth.d.ts → cli/chatgpt-auth.d.ts} +0 -0
- /package/dist/{chatgpt-auth.js → cli/chatgpt-auth.js} +0 -0
- /package/dist/{login.d.ts → cli/login.d.ts} +0 -0
- /package/dist/{open-browser.d.ts → cli/open-browser.d.ts} +0 -0
- /package/dist/{open-browser.js → cli/open-browser.js} +0 -0
- /package/dist/{subscription.d.ts → cli/subscription.d.ts} +0 -0
- /package/dist/{subscription.js → cli/subscription.js} +0 -0
- /package/src/{chatgpt-auth.ts → cli/chatgpt-auth.ts} +0 -0
- /package/src/{open-browser.ts → cli/open-browser.ts} +0 -0
- /package/src/{subscription.ts → cli/subscription.ts} +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// General-purpose persisted memoization for expensive async functions.
|
|
2
|
+
//
|
|
3
|
+
// cachedGenerate() wraps any async function with:
|
|
4
|
+
// - Deterministic filesystem caching (params → stable hash → file)
|
|
5
|
+
// - Concurrent call deduplication (same key → same promise)
|
|
6
|
+
// - Stale fallback management (old results moved to stale/ subfolder)
|
|
7
|
+
// - Progress tracking (namespace-based, drives the player toolbar UI)
|
|
8
|
+
//
|
|
9
|
+
// Binary params (Uint8Array) are auto-hashed in the cache key. The caller
|
|
10
|
+
// only provides a generate function, a serializer, and a namespace.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// const generateImage = cachedGenerate({
|
|
14
|
+
// namespace: 'image',
|
|
15
|
+
// prefixFrom: (p) => p.prompt,
|
|
16
|
+
// generate: (p) => generateImageUncached(p).then(r => r.images[0]!),
|
|
17
|
+
// serialize: (file) => ({ bytes: file.uint8Array, extension: '.png' }),
|
|
18
|
+
// })
|
|
19
|
+
import fs from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { stableJsonKey, hashKey, promptPrefix, generatedDir, findCachedFile, findFallbackFile, moveFileToStale, generationQueue, formatKeyParams, } from './cache-utils.js';
|
|
22
|
+
const generationRegistry = globalThis.__egakiGenerationRegistry ??= new Map();
|
|
23
|
+
const progressListeners = globalThis.__egakiProgressListeners ??= new Set();
|
|
24
|
+
function notifyProgressListeners() {
|
|
25
|
+
for (const listener of progressListeners) {
|
|
26
|
+
try {
|
|
27
|
+
listener();
|
|
28
|
+
}
|
|
29
|
+
catch { /* listener errors are non-fatal */ }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Subscribe to generation progress changes. Returns an unsubscribe function. */
|
|
33
|
+
export function onProgressChange(listener) {
|
|
34
|
+
progressListeners.add(listener);
|
|
35
|
+
return () => { progressListeners.delete(listener); };
|
|
36
|
+
}
|
|
37
|
+
/** Recent generation errors, drained on each getGenerationProgress() call
|
|
38
|
+
* so each SSE event includes errors that happened since the last yield. */
|
|
39
|
+
const recentErrors = globalThis.__egakiRecentErrors ??= [];
|
|
40
|
+
/** Get a snapshot of all active generations with elapsed time computed.
|
|
41
|
+
* Drains the error queue so each call returns only new errors since last read. */
|
|
42
|
+
export function getGenerationProgress() {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const generations = [];
|
|
45
|
+
const counts = {};
|
|
46
|
+
for (const [key, entry] of generationRegistry) {
|
|
47
|
+
generations.push({ ...entry, key, elapsedMs: now - entry.startedAt });
|
|
48
|
+
counts[entry.namespace] = (counts[entry.namespace] ?? 0) + 1;
|
|
49
|
+
}
|
|
50
|
+
const total = generations.length;
|
|
51
|
+
const errors = recentErrors.splice(0);
|
|
52
|
+
return {
|
|
53
|
+
generations,
|
|
54
|
+
errors,
|
|
55
|
+
summary: { counts, total },
|
|
56
|
+
done: total === 0 && errors.length === 0,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function registerGeneration(key, entry) {
|
|
60
|
+
generationRegistry.set(key, entry);
|
|
61
|
+
notifyProgressListeners();
|
|
62
|
+
}
|
|
63
|
+
function unregisterGeneration(key, error) {
|
|
64
|
+
const entry = generationRegistry.get(key);
|
|
65
|
+
if (entry && error) {
|
|
66
|
+
recentErrors.push({
|
|
67
|
+
key,
|
|
68
|
+
namespace: entry.namespace,
|
|
69
|
+
label: entry.label,
|
|
70
|
+
model: entry.model,
|
|
71
|
+
error,
|
|
72
|
+
durationMs: Date.now() - entry.startedAt,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
generationRegistry.delete(key);
|
|
76
|
+
notifyProgressListeners();
|
|
77
|
+
}
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Auto cache key — recursively walks params, hashes Uint8Array values,
|
|
80
|
+
// strips undefined. Handles nested objects and arrays of any depth.
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
function normalizeCacheValue(value) {
|
|
83
|
+
if (value === undefined)
|
|
84
|
+
return undefined;
|
|
85
|
+
if (value instanceof Uint8Array)
|
|
86
|
+
return hashKey(value);
|
|
87
|
+
if (Array.isArray(value))
|
|
88
|
+
return value.map(normalizeCacheValue);
|
|
89
|
+
if (value && typeof value === 'object') {
|
|
90
|
+
return Object.fromEntries(Object.entries(value)
|
|
91
|
+
.filter(([, v]) => v !== undefined)
|
|
92
|
+
.map(([k, v]) => [k, normalizeCacheValue(v)]));
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function autoCacheKey(params) {
|
|
97
|
+
return normalizeCacheValue(params);
|
|
98
|
+
}
|
|
99
|
+
/** Compute cache info for a given namespace and params without starting generation.
|
|
100
|
+
* Used by server components to check cache and find fallback synchronously. */
|
|
101
|
+
export function getCacheInfo(namespace, params, prefixText) {
|
|
102
|
+
const keyParams = autoCacheKey(params);
|
|
103
|
+
// Note: getCacheInfo uses the raw params. If a cachedGenerate wrapper uses
|
|
104
|
+
// cacheKey to exclude fields, the caller should pass the same subset here.
|
|
105
|
+
const key = stableJsonKey(keyParams);
|
|
106
|
+
const hash = hashKey(key);
|
|
107
|
+
const dir = generatedDir(namespace);
|
|
108
|
+
const prefix = promptPrefix(prefixText);
|
|
109
|
+
const cached = findCachedFile(dir, hash);
|
|
110
|
+
const fallback = cached ? undefined : findFallbackFile(dir, prefix, hash);
|
|
111
|
+
const fallbackSrc = fallback
|
|
112
|
+
? `/generated/${namespace}/${fallback.inStale ? 'stale/' : ''}${fallback.filename}`
|
|
113
|
+
: undefined;
|
|
114
|
+
return { hash, prefix, dir, key, cached, fallback, fallbackSrc };
|
|
115
|
+
}
|
|
116
|
+
export function cachedGenerate(config) {
|
|
117
|
+
const { namespace, prefixFrom, cacheKey, generate, serialize, deserialize, modelFrom, postWrite } = config;
|
|
118
|
+
function cachedFn(params) {
|
|
119
|
+
const rawKeyParams = cacheKey ? cacheKey(params) : params;
|
|
120
|
+
const keyParams = autoCacheKey(rawKeyParams);
|
|
121
|
+
const key = stableJsonKey(keyParams);
|
|
122
|
+
const hash = hashKey(key);
|
|
123
|
+
const dir = generatedDir(namespace);
|
|
124
|
+
const prefix = promptPrefix(prefixFrom(params));
|
|
125
|
+
// Queue key includes namespace to prevent cross-namespace collisions.
|
|
126
|
+
// Two wrappers with different namespaces but identical params must not
|
|
127
|
+
// share the same in-flight promise.
|
|
128
|
+
const queueKey = `${namespace}:${key}`;
|
|
129
|
+
// 1. Cache hit — return immediately
|
|
130
|
+
const cached = findCachedFile(dir, hash);
|
|
131
|
+
if (cached) {
|
|
132
|
+
const urlPath = `/generated/${namespace}/${cached}`;
|
|
133
|
+
const filePath = path.join(dir, cached);
|
|
134
|
+
if (deserialize) {
|
|
135
|
+
return Promise.resolve(deserialize({ urlPath, filePath }));
|
|
136
|
+
}
|
|
137
|
+
return Promise.resolve({ src: urlPath });
|
|
138
|
+
}
|
|
139
|
+
// 2. Dedup — return pending promise if same key is in-flight
|
|
140
|
+
const pending = generationQueue.get(queueKey);
|
|
141
|
+
if (pending)
|
|
142
|
+
return pending;
|
|
143
|
+
// 3. Find stale fallback for cleanup after generation
|
|
144
|
+
const fallback = findFallbackFile(dir, prefix, hash);
|
|
145
|
+
// 4. Register progress
|
|
146
|
+
const regKey = `${namespace}:${hash}`;
|
|
147
|
+
const label = prefixFrom(params);
|
|
148
|
+
registerGeneration(regKey, {
|
|
149
|
+
namespace,
|
|
150
|
+
label,
|
|
151
|
+
model: modelFrom?.(params),
|
|
152
|
+
startedAt: Date.now(),
|
|
153
|
+
params: keyParams,
|
|
154
|
+
});
|
|
155
|
+
// 5. Generate, serialize, write to disk
|
|
156
|
+
// The entire body is wrapped in try/catch so that failures in serialize(),
|
|
157
|
+
// fs.writeFileSync(), or deserialize() also unregister progress and return
|
|
158
|
+
// Error instead of throwing through the promise.
|
|
159
|
+
const promise = (async () => {
|
|
160
|
+
try {
|
|
161
|
+
console.log(`[egaki] generating ${namespace}: ${prefix}-${hash} (${formatKeyParams(keyParams)})`);
|
|
162
|
+
const result = await generate(params);
|
|
163
|
+
const serialized = serialize(result, params);
|
|
164
|
+
const finalPrefix = serialized.prefix ? promptPrefix(serialized.prefix) : prefix;
|
|
165
|
+
const filename = `${finalPrefix}-${hash}${serialized.extension}`;
|
|
166
|
+
const filePath = path.join(dir, filename);
|
|
167
|
+
if ('bytes' in serialized) {
|
|
168
|
+
fs.writeFileSync(filePath, serialized.bytes);
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
fs.writeFileSync(filePath, JSON.stringify(serialized.json, null, 2));
|
|
172
|
+
}
|
|
173
|
+
if (postWrite) {
|
|
174
|
+
try {
|
|
175
|
+
postWrite(filePath, result, params);
|
|
176
|
+
}
|
|
177
|
+
catch (e) {
|
|
178
|
+
console.error(`[egaki] postWrite error for ${namespace}:`, e);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
console.log(`[egaki] generated ${namespace}: ${filename}`);
|
|
182
|
+
if (fallback)
|
|
183
|
+
moveFileToStale(dir, fallback);
|
|
184
|
+
unregisterGeneration(regKey);
|
|
185
|
+
const urlPath = `/generated/${namespace}/${filename}`;
|
|
186
|
+
if (deserialize) {
|
|
187
|
+
return deserialize({ urlPath, filePath });
|
|
188
|
+
}
|
|
189
|
+
return { src: urlPath };
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
193
|
+
unregisterGeneration(regKey, error.message);
|
|
194
|
+
return error;
|
|
195
|
+
}
|
|
196
|
+
finally {
|
|
197
|
+
generationQueue.delete(queueKey);
|
|
198
|
+
}
|
|
199
|
+
})();
|
|
200
|
+
generationQueue.set(queueKey, promise);
|
|
201
|
+
return promise;
|
|
202
|
+
}
|
|
203
|
+
cachedFn.getCacheInfo = function (params) {
|
|
204
|
+
const rawKeyParams = cacheKey ? cacheKey(params) : params;
|
|
205
|
+
return getCacheInfo(namespace, rawKeyParams, prefixFrom(params));
|
|
206
|
+
};
|
|
207
|
+
return cachedFn;
|
|
208
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cachedGenerate — the general-purpose persisted memoization HOF.
|
|
3
|
+
*
|
|
4
|
+
* Uses a temp directory as the project root so tests are isolated and don't
|
|
5
|
+
* touch the real public/generated/ folder. Tests verify: cache hit/miss,
|
|
6
|
+
* dedup of concurrent calls, stale fallback management, auto-hashing of
|
|
7
|
+
* Uint8Array params, JSON serialization, progress tracking, error handling,
|
|
8
|
+
* and the getCacheInfo helper.
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=cached-generate.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cached-generate.test.d.ts","sourceRoot":"","sources":["../../src/cli/cached-generate.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for cachedGenerate — the general-purpose persisted memoization HOF.
|
|
3
|
+
*
|
|
4
|
+
* Uses a temp directory as the project root so tests are isolated and don't
|
|
5
|
+
* touch the real public/generated/ folder. Tests verify: cache hit/miss,
|
|
6
|
+
* dedup of concurrent calls, stale fallback management, auto-hashing of
|
|
7
|
+
* Uint8Array params, JSON serialization, progress tracking, error handling,
|
|
8
|
+
* and the getCacheInfo helper.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, test, expect, beforeEach, afterEach } from 'vitest';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { cachedGenerate, getCacheInfo, getGenerationProgress, } from "./cached-generate.js";
|
|
15
|
+
import { setProjectRoot } from "./cache-utils.js";
|
|
16
|
+
let tmpDir;
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'egaki-cached-generate-test-'));
|
|
19
|
+
setProjectRoot(tmpDir);
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
function makeTestConfig(overrides) {
|
|
25
|
+
let callCount = 0;
|
|
26
|
+
const config = {
|
|
27
|
+
namespace: 'test',
|
|
28
|
+
prefixFrom: (p) => p.prompt,
|
|
29
|
+
generate: async (params) => {
|
|
30
|
+
callCount++;
|
|
31
|
+
return { data: `generated-${params.prompt}` };
|
|
32
|
+
},
|
|
33
|
+
serialize: (result) => ({
|
|
34
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
35
|
+
extension: '.txt',
|
|
36
|
+
}),
|
|
37
|
+
...overrides,
|
|
38
|
+
};
|
|
39
|
+
return { config, getCallCount: () => callCount };
|
|
40
|
+
}
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// Cache hit / miss
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
describe('cache hit and miss', () => {
|
|
45
|
+
test('first call generates and writes file', async () => {
|
|
46
|
+
const { config, getCallCount } = makeTestConfig();
|
|
47
|
+
const fn = cachedGenerate(config);
|
|
48
|
+
const result = await fn({ prompt: 'hello world' });
|
|
49
|
+
expect(result).not.toBeInstanceOf(Error);
|
|
50
|
+
const r = result;
|
|
51
|
+
expect(r.src).toMatch(/^\/generated\/test\/hello-world-[a-f0-9]{8}\.txt$/);
|
|
52
|
+
expect(getCallCount()).toBe(1);
|
|
53
|
+
// File exists on disk
|
|
54
|
+
const filePath = path.join(tmpDir, 'public', r.src);
|
|
55
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
56
|
+
expect(fs.readFileSync(filePath, 'utf-8')).toBe('generated-hello world');
|
|
57
|
+
});
|
|
58
|
+
test('second call with same params returns cached (no generation)', async () => {
|
|
59
|
+
const { config, getCallCount } = makeTestConfig();
|
|
60
|
+
const fn = cachedGenerate(config);
|
|
61
|
+
const r1 = await fn({ prompt: 'hello world' });
|
|
62
|
+
const r2 = await fn({ prompt: 'hello world' });
|
|
63
|
+
expect(r1).not.toBeInstanceOf(Error);
|
|
64
|
+
expect(r2).not.toBeInstanceOf(Error);
|
|
65
|
+
expect(r1.src).toBe(r2.src);
|
|
66
|
+
expect(getCallCount()).toBe(1); // only called once
|
|
67
|
+
});
|
|
68
|
+
test('different params generate different files', async () => {
|
|
69
|
+
const { config, getCallCount } = makeTestConfig();
|
|
70
|
+
const fn = cachedGenerate(config);
|
|
71
|
+
const r1 = await fn({ prompt: 'alpha' });
|
|
72
|
+
const r2 = await fn({ prompt: 'beta' });
|
|
73
|
+
expect(r1).not.toBeInstanceOf(Error);
|
|
74
|
+
expect(r2).not.toBeInstanceOf(Error);
|
|
75
|
+
expect(r1.src).not.toBe(r2.src);
|
|
76
|
+
expect(getCallCount()).toBe(2);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Concurrent dedup
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
describe('deduplication', () => {
|
|
83
|
+
test('concurrent calls with same params return same promise', async () => {
|
|
84
|
+
let resolveGenerate;
|
|
85
|
+
const { config } = makeTestConfig({
|
|
86
|
+
generate: () => new Promise((resolve) => { resolveGenerate = resolve; }),
|
|
87
|
+
});
|
|
88
|
+
const fn = cachedGenerate(config);
|
|
89
|
+
const p1 = fn({ prompt: 'dedup test' });
|
|
90
|
+
const p2 = fn({ prompt: 'dedup test' });
|
|
91
|
+
// Resolve the single generation
|
|
92
|
+
resolveGenerate({ data: 'result' });
|
|
93
|
+
const [r1, r2] = await Promise.all([p1, p2]);
|
|
94
|
+
expect(r1).not.toBeInstanceOf(Error);
|
|
95
|
+
expect(r2).not.toBeInstanceOf(Error);
|
|
96
|
+
expect(r1.src).toBe(r2.src);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
// Stale fallback management
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
describe('stale management', () => {
|
|
103
|
+
test('changing params moves old file to stale/', async () => {
|
|
104
|
+
const { config } = makeTestConfig();
|
|
105
|
+
const fn = cachedGenerate(config);
|
|
106
|
+
// Generate first version
|
|
107
|
+
const r1 = await fn({ prompt: 'hello world', seed: 1 });
|
|
108
|
+
expect(r1).not.toBeInstanceOf(Error);
|
|
109
|
+
const firstFile = path.basename(r1.src);
|
|
110
|
+
// Generate with different seed but same prompt prefix
|
|
111
|
+
const r2 = await fn({ prompt: 'hello world', seed: 2 });
|
|
112
|
+
expect(r2).not.toBeInstanceOf(Error);
|
|
113
|
+
// Old file should be in stale/
|
|
114
|
+
const staleDir = path.join(tmpDir, 'public', 'generated', 'test', 'stale');
|
|
115
|
+
const staleFiles = fs.existsSync(staleDir) ? fs.readdirSync(staleDir) : [];
|
|
116
|
+
expect(staleFiles).toContain(firstFile);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// Auto-hashing Uint8Array params
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
describe('Uint8Array auto-hashing', () => {
|
|
123
|
+
test('same bytes produce same cache key', async () => {
|
|
124
|
+
const { config, getCallCount } = makeTestConfig();
|
|
125
|
+
const fn = cachedGenerate(config);
|
|
126
|
+
const data = new Uint8Array([1, 2, 3]);
|
|
127
|
+
const r1 = await fn({ prompt: 'with data', inputData: data });
|
|
128
|
+
const r2 = await fn({ prompt: 'with data', inputData: new Uint8Array([1, 2, 3]) });
|
|
129
|
+
expect(getCallCount()).toBe(1); // same key → cached
|
|
130
|
+
expect(r1.src).toBe(r2.src);
|
|
131
|
+
});
|
|
132
|
+
test('different bytes produce different cache keys', async () => {
|
|
133
|
+
const { config, getCallCount } = makeTestConfig();
|
|
134
|
+
const fn = cachedGenerate(config);
|
|
135
|
+
await fn({ prompt: 'with data', inputData: new Uint8Array([1, 2, 3]) });
|
|
136
|
+
await fn({ prompt: 'with data', inputData: new Uint8Array([4, 5, 6]) });
|
|
137
|
+
expect(getCallCount()).toBe(2);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// JSON serialization
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
describe('JSON serialization', () => {
|
|
144
|
+
test('json serialize writes .json and deserialize reads it back', async () => {
|
|
145
|
+
const fn = cachedGenerate({
|
|
146
|
+
namespace: 'json-test',
|
|
147
|
+
prefixFrom: (p) => p.query,
|
|
148
|
+
generate: async (params) => ({ items: [params.query, 'b', 'c'] }),
|
|
149
|
+
serialize: (result) => ({
|
|
150
|
+
json: result,
|
|
151
|
+
extension: '.json',
|
|
152
|
+
}),
|
|
153
|
+
deserialize: ({ filePath }) => JSON.parse(fs.readFileSync(filePath, 'utf-8')),
|
|
154
|
+
});
|
|
155
|
+
const r1 = await fn({ query: 'search term' });
|
|
156
|
+
expect(r1).not.toBeInstanceOf(Error);
|
|
157
|
+
expect(r1.items).toEqual(['search term', 'b', 'c']);
|
|
158
|
+
// Second call reads from cache
|
|
159
|
+
const r2 = await fn({ query: 'search term' });
|
|
160
|
+
expect(r2).not.toBeInstanceOf(Error);
|
|
161
|
+
expect(r2.items).toEqual(['search term', 'b', 'c']);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// Prefix override from serialize
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
describe('prefix override', () => {
|
|
168
|
+
test('serialize can return a prefix to override the filename', async () => {
|
|
169
|
+
const fn = cachedGenerate({
|
|
170
|
+
namespace: 'prefix-test',
|
|
171
|
+
prefixFrom: () => 'initial',
|
|
172
|
+
generate: async () => ({ text: 'the actual content is here' }),
|
|
173
|
+
serialize: (result) => ({
|
|
174
|
+
bytes: new Uint8Array(Buffer.from(result.text)),
|
|
175
|
+
extension: '.txt',
|
|
176
|
+
prefix: 'the actual content is here',
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
const r = await fn({ id: '123' });
|
|
180
|
+
expect(r).not.toBeInstanceOf(Error);
|
|
181
|
+
// The filename should use the override prefix, not 'initial'
|
|
182
|
+
expect(r.src).toMatch(/the-actual-content-is-here/);
|
|
183
|
+
expect(r.src).not.toMatch(/initial/);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// Progress tracking
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
describe('progress tracking', () => {
|
|
190
|
+
test('generation is registered and unregistered', async () => {
|
|
191
|
+
let resolveGenerate;
|
|
192
|
+
const fn = cachedGenerate({
|
|
193
|
+
namespace: 'progress-test',
|
|
194
|
+
prefixFrom: (p) => p.prompt,
|
|
195
|
+
modelFrom: (p) => `model-${p.seed}`,
|
|
196
|
+
generate: () => new Promise((resolve) => { resolveGenerate = resolve; }),
|
|
197
|
+
serialize: (result) => ({
|
|
198
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
199
|
+
extension: '.txt',
|
|
200
|
+
}),
|
|
201
|
+
});
|
|
202
|
+
const promise = fn({ prompt: 'tracking test', seed: 42 });
|
|
203
|
+
// While generating, progress should show the entry
|
|
204
|
+
const progress = getGenerationProgress();
|
|
205
|
+
expect(progress.summary.total).toBe(1);
|
|
206
|
+
expect(progress.summary.counts['progress-test']).toBe(1);
|
|
207
|
+
expect(progress.generations[0]?.namespace).toBe('progress-test');
|
|
208
|
+
expect(progress.generations[0]?.label).toBe('tracking test');
|
|
209
|
+
expect(progress.generations[0]?.model).toBe('model-42');
|
|
210
|
+
// Resolve the generation
|
|
211
|
+
resolveGenerate({ data: 'done' });
|
|
212
|
+
await promise;
|
|
213
|
+
// After completion, progress should be empty
|
|
214
|
+
const after = getGenerationProgress();
|
|
215
|
+
expect(after.summary.total).toBe(0);
|
|
216
|
+
});
|
|
217
|
+
test('failed generation reports error in progress', async () => {
|
|
218
|
+
const fn = cachedGenerate({
|
|
219
|
+
namespace: 'error-progress',
|
|
220
|
+
prefixFrom: (p) => p.prompt,
|
|
221
|
+
generate: async () => { throw new Error('API failed'); },
|
|
222
|
+
serialize: (result) => ({
|
|
223
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
224
|
+
extension: '.txt',
|
|
225
|
+
}),
|
|
226
|
+
});
|
|
227
|
+
const result = await fn({ prompt: 'fail test' });
|
|
228
|
+
expect(result).toBeInstanceOf(Error);
|
|
229
|
+
expect(result.message).toBe('API failed');
|
|
230
|
+
// Error should be in the progress drain
|
|
231
|
+
const progress = getGenerationProgress();
|
|
232
|
+
expect(progress.errors.length).toBe(1);
|
|
233
|
+
expect(progress.errors[0]?.error).toBe('API failed');
|
|
234
|
+
expect(progress.errors[0]?.namespace).toBe('error-progress');
|
|
235
|
+
// Errors drain on read — second call should have none
|
|
236
|
+
const progress2 = getGenerationProgress();
|
|
237
|
+
expect(progress2.errors.length).toBe(0);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Error handling
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
describe('error handling', () => {
|
|
244
|
+
test('generate throwing returns Error', async () => {
|
|
245
|
+
const fn = cachedGenerate({
|
|
246
|
+
namespace: 'error-test',
|
|
247
|
+
prefixFrom: (p) => p.prompt,
|
|
248
|
+
generate: async () => { throw new Error('boom'); },
|
|
249
|
+
serialize: (result) => ({
|
|
250
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
251
|
+
extension: '.txt',
|
|
252
|
+
}),
|
|
253
|
+
});
|
|
254
|
+
const result = await fn({ prompt: 'error test' });
|
|
255
|
+
expect(result).toBeInstanceOf(Error);
|
|
256
|
+
expect(result.message).toBe('boom');
|
|
257
|
+
});
|
|
258
|
+
test('non-Error throw is wrapped in Error', async () => {
|
|
259
|
+
const fn = cachedGenerate({
|
|
260
|
+
namespace: 'error-test-2',
|
|
261
|
+
prefixFrom: (p) => p.prompt,
|
|
262
|
+
generate: async () => { throw 'string error'; },
|
|
263
|
+
serialize: (result) => ({
|
|
264
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
265
|
+
extension: '.txt',
|
|
266
|
+
}),
|
|
267
|
+
});
|
|
268
|
+
const result = await fn({ prompt: 'string error test' });
|
|
269
|
+
expect(result).toBeInstanceOf(Error);
|
|
270
|
+
expect(result.message).toBe('string error');
|
|
271
|
+
});
|
|
272
|
+
test('failed call does not cache — retry regenerates', async () => {
|
|
273
|
+
let shouldFail = true;
|
|
274
|
+
const fn = cachedGenerate({
|
|
275
|
+
namespace: 'retry-test',
|
|
276
|
+
prefixFrom: (p) => p.prompt,
|
|
277
|
+
generate: async (params) => {
|
|
278
|
+
if (shouldFail)
|
|
279
|
+
throw new Error('temporary failure');
|
|
280
|
+
return { data: `ok-${params.prompt}` };
|
|
281
|
+
},
|
|
282
|
+
serialize: (result) => ({
|
|
283
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
284
|
+
extension: '.txt',
|
|
285
|
+
}),
|
|
286
|
+
});
|
|
287
|
+
const r1 = await fn({ prompt: 'retry' });
|
|
288
|
+
expect(r1).toBeInstanceOf(Error);
|
|
289
|
+
shouldFail = false;
|
|
290
|
+
const r2 = await fn({ prompt: 'retry' });
|
|
291
|
+
expect(r2).not.toBeInstanceOf(Error);
|
|
292
|
+
expect(r2.src).toMatch(/\/generated\/retry-test\//);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Namespace isolation in dedup queue
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
describe('namespace isolation', () => {
|
|
299
|
+
test('same params in different namespaces generate independently', async () => {
|
|
300
|
+
let imageCallCount = 0;
|
|
301
|
+
let videoCallCount = 0;
|
|
302
|
+
const imageFn = cachedGenerate({
|
|
303
|
+
namespace: 'ns-image',
|
|
304
|
+
prefixFrom: (p) => p.prompt,
|
|
305
|
+
generate: async (params) => {
|
|
306
|
+
imageCallCount++;
|
|
307
|
+
return { data: `image-${params.prompt}` };
|
|
308
|
+
},
|
|
309
|
+
serialize: (result) => ({
|
|
310
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
311
|
+
extension: '.png',
|
|
312
|
+
}),
|
|
313
|
+
});
|
|
314
|
+
const videoFn = cachedGenerate({
|
|
315
|
+
namespace: 'ns-video',
|
|
316
|
+
prefixFrom: (p) => p.prompt,
|
|
317
|
+
generate: async (params) => {
|
|
318
|
+
videoCallCount++;
|
|
319
|
+
return { data: `video-${params.prompt}` };
|
|
320
|
+
},
|
|
321
|
+
serialize: (result) => ({
|
|
322
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
323
|
+
extension: '.mp4',
|
|
324
|
+
}),
|
|
325
|
+
});
|
|
326
|
+
const [imgResult, vidResult] = await Promise.all([
|
|
327
|
+
imageFn({ prompt: 'same params' }),
|
|
328
|
+
videoFn({ prompt: 'same params' }),
|
|
329
|
+
]);
|
|
330
|
+
expect(imgResult).not.toBeInstanceOf(Error);
|
|
331
|
+
expect(vidResult).not.toBeInstanceOf(Error);
|
|
332
|
+
expect(imageCallCount).toBe(1);
|
|
333
|
+
expect(videoCallCount).toBe(1);
|
|
334
|
+
// Different src paths
|
|
335
|
+
expect(imgResult.src).toMatch(/ns-image/);
|
|
336
|
+
expect(vidResult.src).toMatch(/ns-video/);
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
// ---------------------------------------------------------------------------
|
|
340
|
+
// Recursive Uint8Array hashing
|
|
341
|
+
// ---------------------------------------------------------------------------
|
|
342
|
+
describe('recursive binary hashing', () => {
|
|
343
|
+
test('nested Uint8Array values produce stable cache keys', async () => {
|
|
344
|
+
const { config, getCallCount } = makeTestConfig();
|
|
345
|
+
const fn = cachedGenerate(config);
|
|
346
|
+
// Pass nested binary data inside an object
|
|
347
|
+
const r1 = await fn({ prompt: 'nested', inputData: new Uint8Array([10, 20, 30]) });
|
|
348
|
+
const r2 = await fn({ prompt: 'nested', inputData: new Uint8Array([10, 20, 30]) });
|
|
349
|
+
expect(getCallCount()).toBe(1); // same nested bytes → cached
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
// ---------------------------------------------------------------------------
|
|
353
|
+
// Serialize failure handling
|
|
354
|
+
// ---------------------------------------------------------------------------
|
|
355
|
+
describe('serialize failure', () => {
|
|
356
|
+
test('serialize throwing unregisters progress and returns Error', async () => {
|
|
357
|
+
// Drain any leftover errors from previous tests
|
|
358
|
+
getGenerationProgress();
|
|
359
|
+
const fn = cachedGenerate({
|
|
360
|
+
namespace: 'serialize-fail',
|
|
361
|
+
prefixFrom: (p) => p.prompt,
|
|
362
|
+
generate: async () => ({ data: 'ok' }),
|
|
363
|
+
serialize: () => { throw new Error('serialize exploded'); },
|
|
364
|
+
});
|
|
365
|
+
const result = await fn({ prompt: 'serialize test' });
|
|
366
|
+
expect(result).toBeInstanceOf(Error);
|
|
367
|
+
expect(result.message).toBe('serialize exploded');
|
|
368
|
+
// Progress should be clean — no stuck entries
|
|
369
|
+
const progress = getGenerationProgress();
|
|
370
|
+
expect(progress.summary.total).toBe(0);
|
|
371
|
+
// Error should be in the drain
|
|
372
|
+
expect(progress.errors.length).toBe(1);
|
|
373
|
+
expect(progress.errors[0]?.error).toBe('serialize exploded');
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// cacheKey config excludes non-identity fields
|
|
378
|
+
// ---------------------------------------------------------------------------
|
|
379
|
+
describe('cacheKey config', () => {
|
|
380
|
+
test('excluded fields do not affect cache identity', async () => {
|
|
381
|
+
let callCount = 0;
|
|
382
|
+
const fn = cachedGenerate({
|
|
383
|
+
namespace: 'cachekey-test',
|
|
384
|
+
prefixFrom: (p) => p.label ?? p.query,
|
|
385
|
+
cacheKey: ({ label, ...rest }) => rest,
|
|
386
|
+
generate: async (params) => {
|
|
387
|
+
callCount++;
|
|
388
|
+
return { data: `result-${params.query}` };
|
|
389
|
+
},
|
|
390
|
+
serialize: (result) => ({
|
|
391
|
+
bytes: new Uint8Array(Buffer.from(result.data)),
|
|
392
|
+
extension: '.txt',
|
|
393
|
+
}),
|
|
394
|
+
});
|
|
395
|
+
await fn({ query: 'search', label: 'label-a' });
|
|
396
|
+
await fn({ query: 'search', label: 'label-b' });
|
|
397
|
+
// Same query, different labels → should be same cache entry
|
|
398
|
+
expect(callCount).toBe(1);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
// ---------------------------------------------------------------------------
|
|
402
|
+
// getCacheInfo helper
|
|
403
|
+
// ---------------------------------------------------------------------------
|
|
404
|
+
describe('getCacheInfo', () => {
|
|
405
|
+
test('returns cache miss before any generation', () => {
|
|
406
|
+
const info = getCacheInfo('test-ns', { prompt: 'hello' }, 'hello');
|
|
407
|
+
expect(info.cached).toBeUndefined();
|
|
408
|
+
expect(info.hash).toMatch(/^[a-f0-9]{8}$/);
|
|
409
|
+
expect(info.prefix).toBe('hello');
|
|
410
|
+
});
|
|
411
|
+
test('returns cache hit after generation', async () => {
|
|
412
|
+
const { config } = makeTestConfig();
|
|
413
|
+
const fn = cachedGenerate(config);
|
|
414
|
+
await fn({ prompt: 'cached info test' });
|
|
415
|
+
const info = getCacheInfo('test', { prompt: 'cached info test' }, 'cached info test');
|
|
416
|
+
expect(info.cached).toBeDefined();
|
|
417
|
+
expect(info.cached).toMatch(/cached-info-test-[a-f0-9]{8}\.txt/);
|
|
418
|
+
});
|
|
419
|
+
test('returns fallbackSrc when a previous generation exists', async () => {
|
|
420
|
+
const { config } = makeTestConfig();
|
|
421
|
+
const fn = cachedGenerate(config);
|
|
422
|
+
// Generate first version
|
|
423
|
+
await fn({ prompt: 'fallback test', seed: 1 });
|
|
424
|
+
// Query for a different version that doesn't exist yet
|
|
425
|
+
// Version 1 is in the main dir and should be found as fallback
|
|
426
|
+
const info = getCacheInfo('test', { prompt: 'fallback test', seed: 2 }, 'fallback test');
|
|
427
|
+
expect(info.cached).toBeUndefined();
|
|
428
|
+
expect(info.fallbackSrc).toBeDefined();
|
|
429
|
+
expect(info.fallbackSrc).toMatch(/\/generated\/test\/fallback-test-/);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { SpeechProvider } from './speech-generate.js';
|
|
2
|
+
import type { WordTimestamp } from './transcription-generate.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse a Cartesia SSE stream into audio chunks and word timestamps.
|
|
5
|
+
* Uses eventsource-parser for robust SSE frame parsing.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseCartesiaSseStream(body: ReadableStream<Uint8Array>): Promise<{
|
|
8
|
+
audioChunks: Uint8Array[];
|
|
9
|
+
timestamps: WordTimestamp[];
|
|
10
|
+
}>;
|
|
11
|
+
/**
|
|
12
|
+
* Create a Cartesia speech provider using the SSE endpoint with word timestamps.
|
|
13
|
+
* Returns raw PCM audio (pcm_s16le) since the SSE endpoint only supports raw container.
|
|
14
|
+
*/
|
|
15
|
+
export declare function createCartesiaSpeechProvider(): SpeechProvider;
|
|
16
|
+
//# sourceMappingURL=cartesia-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cartesia-provider.d.ts","sourceRoot":"","sources":["../../src/cli/cartesia-provider.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,cAAc,EAA+C,MAAM,sBAAsB,CAAA;AACvG,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAA;AA8DhE;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,IAAI,EAAE,cAAc,CAAC,UAAU,CAAC,GAC/B,OAAO,CAAC;IAAE,WAAW,EAAE,UAAU,EAAE,CAAC;IAAC,UAAU,EAAE,aAAa,EAAE,CAAA;CAAE,CAAC,CAsCrE;AAqDD;;;GAGG;AACH,wBAAgB,4BAA4B,IAAI,cAAc,CA6D7D"}
|