@steipete/summarize 0.9.0 → 0.10.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 +75 -0
- package/LICENSE +1 -1
- package/README.md +307 -184
- package/dist/cli.js +1 -1
- package/dist/esm/cache.js +72 -4
- package/dist/esm/cache.js.map +1 -1
- package/dist/esm/config.js +127 -1
- package/dist/esm/config.js.map +1 -1
- package/dist/esm/daemon/agent.js +547 -0
- package/dist/esm/daemon/agent.js.map +1 -0
- package/dist/esm/daemon/chat.js +83 -175
- package/dist/esm/daemon/chat.js.map +1 -1
- package/dist/esm/daemon/cli.js +35 -3
- package/dist/esm/daemon/cli.js.map +1 -1
- package/dist/esm/daemon/env-snapshot.js +3 -0
- package/dist/esm/daemon/env-snapshot.js.map +1 -1
- package/dist/esm/daemon/flow-context.js +31 -8
- package/dist/esm/daemon/flow-context.js.map +1 -1
- package/dist/esm/daemon/launchd.js +27 -0
- package/dist/esm/daemon/launchd.js.map +1 -1
- package/dist/esm/daemon/process-registry.js +206 -0
- package/dist/esm/daemon/process-registry.js.map +1 -0
- package/dist/esm/daemon/schtasks.js +64 -0
- package/dist/esm/daemon/schtasks.js.map +1 -1
- package/dist/esm/daemon/server.js +712 -42
- package/dist/esm/daemon/server.js.map +1 -1
- package/dist/esm/daemon/summarize.js +33 -4
- package/dist/esm/daemon/summarize.js.map +1 -1
- package/dist/esm/daemon/systemd.js +61 -0
- package/dist/esm/daemon/systemd.js.map +1 -1
- package/dist/esm/flags.js +24 -0
- package/dist/esm/flags.js.map +1 -1
- package/dist/esm/llm/providers/openai.js +1 -1
- package/dist/esm/llm/providers/openai.js.map +1 -1
- package/dist/esm/media-cache.js +251 -0
- package/dist/esm/media-cache.js.map +1 -0
- package/dist/esm/processes.js +2 -0
- package/dist/esm/processes.js.map +1 -0
- package/dist/esm/run/bird.js +118 -5
- package/dist/esm/run/bird.js.map +1 -1
- package/dist/esm/run/cli-preflight.js +19 -1
- package/dist/esm/run/cli-preflight.js.map +1 -1
- package/dist/esm/run/finish-line.js +11 -4
- package/dist/esm/run/finish-line.js.map +1 -1
- package/dist/esm/run/flows/asset/extract.js +70 -0
- package/dist/esm/run/flows/asset/extract.js.map +1 -0
- package/dist/esm/run/flows/asset/input.js +208 -24
- package/dist/esm/run/flows/asset/input.js.map +1 -1
- package/dist/esm/run/flows/asset/media-policy.js +3 -0
- package/dist/esm/run/flows/asset/media-policy.js.map +1 -0
- package/dist/esm/run/flows/asset/media.js +224 -0
- package/dist/esm/run/flows/asset/media.js.map +1 -0
- package/dist/esm/run/flows/asset/output.js +98 -0
- package/dist/esm/run/flows/asset/output.js.map +1 -0
- package/dist/esm/run/flows/asset/summary.js +157 -7
- package/dist/esm/run/flows/asset/summary.js.map +1 -1
- package/dist/esm/run/flows/url/extract.js +6 -6
- package/dist/esm/run/flows/url/extract.js.map +1 -1
- package/dist/esm/run/flows/url/flow.js +336 -36
- package/dist/esm/run/flows/url/flow.js.map +1 -1
- package/dist/esm/run/flows/url/markdown.js +6 -1
- package/dist/esm/run/flows/url/markdown.js.map +1 -1
- package/dist/esm/run/flows/url/slides-output.js +485 -0
- package/dist/esm/run/flows/url/slides-output.js.map +1 -0
- package/dist/esm/run/flows/url/slides-text.js +628 -0
- package/dist/esm/run/flows/url/slides-text.js.map +1 -0
- package/dist/esm/run/flows/url/summary.js +356 -82
- package/dist/esm/run/flows/url/summary.js.map +1 -1
- package/dist/esm/run/help.js +94 -5
- package/dist/esm/run/help.js.map +1 -1
- package/dist/esm/run/logging.js +12 -4
- package/dist/esm/run/logging.js.map +1 -1
- package/dist/esm/run/media-cache-state.js +33 -0
- package/dist/esm/run/media-cache-state.js.map +1 -0
- package/dist/esm/run/progress.js +19 -1
- package/dist/esm/run/progress.js.map +1 -1
- package/dist/esm/run/run-settings.js +39 -1
- package/dist/esm/run/run-settings.js.map +1 -1
- package/dist/esm/run/runner.js +196 -8
- package/dist/esm/run/runner.js.map +1 -1
- package/dist/esm/run/slides-cli.js +225 -0
- package/dist/esm/run/slides-cli.js.map +1 -0
- package/dist/esm/run/slides-render.js +163 -0
- package/dist/esm/run/slides-render.js.map +1 -0
- package/dist/esm/run/stream-output.js +10 -3
- package/dist/esm/run/stream-output.js.map +1 -1
- package/dist/esm/run/summary-engine.js +33 -7
- package/dist/esm/run/summary-engine.js.map +1 -1
- package/dist/esm/run/transcriber-cli.js +148 -0
- package/dist/esm/run/transcriber-cli.js.map +1 -0
- package/dist/esm/shared/sse-events.js +4 -0
- package/dist/esm/shared/sse-events.js.map +1 -1
- package/dist/esm/slides/extract.js +1942 -0
- package/dist/esm/slides/extract.js.map +1 -0
- package/dist/esm/slides/index.js +4 -0
- package/dist/esm/slides/index.js.map +1 -0
- package/dist/esm/slides/settings.js +73 -0
- package/dist/esm/slides/settings.js.map +1 -0
- package/dist/esm/slides/store.js +111 -0
- package/dist/esm/slides/store.js.map +1 -0
- package/dist/esm/slides/types.js +2 -0
- package/dist/esm/slides/types.js.map +1 -0
- package/dist/esm/tty/osc-progress.js +21 -1
- package/dist/esm/tty/osc-progress.js.map +1 -1
- package/dist/esm/tty/progress/fetch-html.js +8 -4
- package/dist/esm/tty/progress/fetch-html.js.map +1 -1
- package/dist/esm/tty/progress/transcript.js +82 -31
- package/dist/esm/tty/progress/transcript.js.map +1 -1
- package/dist/esm/tty/spinner.js +2 -2
- package/dist/esm/tty/spinner.js.map +1 -1
- package/dist/esm/tty/theme.js +189 -0
- package/dist/esm/tty/theme.js.map +1 -0
- package/dist/esm/tty/website-progress.js +17 -13
- package/dist/esm/tty/website-progress.js.map +1 -1
- package/dist/esm/version.js +1 -1
- package/dist/esm/version.js.map +1 -1
- package/dist/types/cache.d.ts +14 -2
- package/dist/types/config.d.ts +23 -0
- package/dist/types/daemon/agent.d.ts +25 -0
- package/dist/types/daemon/chat.d.ts +10 -18
- package/dist/types/daemon/env-snapshot.d.ts +1 -1
- package/dist/types/daemon/flow-context.d.ts +21 -1
- package/dist/types/daemon/launchd.d.ts +4 -0
- package/dist/types/daemon/process-registry.d.ts +73 -0
- package/dist/types/daemon/schtasks.d.ts +4 -0
- package/dist/types/daemon/summarize.d.ts +36 -5
- package/dist/types/daemon/systemd.d.ts +4 -0
- package/dist/types/flags.d.ts +1 -0
- package/dist/types/media-cache.d.ts +22 -0
- package/dist/types/processes.d.ts +1 -0
- package/dist/types/run/bird.d.ts +7 -0
- package/dist/types/run/finish-line.d.ts +2 -1
- package/dist/types/run/flows/asset/extract.d.ts +18 -0
- package/dist/types/run/flows/asset/input.d.ts +12 -2
- package/dist/types/run/flows/asset/media-policy.d.ts +2 -0
- package/dist/types/run/flows/asset/media.d.ts +21 -0
- package/dist/types/run/flows/asset/output.d.ts +42 -0
- package/dist/types/run/flows/asset/summary.d.ts +6 -0
- package/dist/types/run/flows/url/extract.d.ts +2 -1
- package/dist/types/run/flows/url/slides-output.d.ts +66 -0
- package/dist/types/run/flows/url/slides-text.d.ts +87 -0
- package/dist/types/run/flows/url/summary.d.ts +11 -3
- package/dist/types/run/flows/url/types.d.ts +29 -2
- package/dist/types/run/help.d.ts +3 -0
- package/dist/types/run/logging.d.ts +3 -2
- package/dist/types/run/media-cache-state.d.ts +7 -0
- package/dist/types/run/progress.d.ts +2 -1
- package/dist/types/run/run-settings.d.ts +7 -1
- package/dist/types/run/slides-cli.d.ts +9 -0
- package/dist/types/run/slides-render.d.ts +30 -0
- package/dist/types/run/stream-output.d.ts +2 -1
- package/dist/types/run/summary-engine.d.ts +11 -1
- package/dist/types/run/transcriber-cli.d.ts +8 -0
- package/dist/types/shared/sse-events.d.ts +20 -0
- package/dist/types/slides/extract.d.ts +42 -0
- package/dist/types/slides/index.d.ts +5 -0
- package/dist/types/slides/settings.d.ts +20 -0
- package/dist/types/slides/store.d.ts +15 -0
- package/dist/types/slides/types.d.ts +40 -0
- package/dist/types/tty/osc-progress.d.ts +2 -2
- package/dist/types/tty/progress/fetch-html.d.ts +3 -1
- package/dist/types/tty/progress/transcript.d.ts +3 -1
- package/dist/types/tty/spinner.d.ts +3 -1
- package/dist/types/tty/theme.d.ts +44 -0
- package/dist/types/tty/website-progress.d.ts +3 -1
- package/dist/types/version.d.ts +1 -1
- package/docs/README.md +1 -1
- package/docs/_config.yml +26 -0
- package/docs/_layouts/default.html +60 -0
- package/docs/agent.md +333 -0
- package/docs/assets/site.css +748 -0
- package/docs/assets/site.js +72 -0
- package/docs/assets/summarize-cli.png +0 -0
- package/docs/assets/summarize-extension.png +0 -0
- package/docs/assets/youtube-slides.png +0 -0
- package/docs/cache.md +29 -3
- package/docs/chrome-extension.md +61 -11
- package/docs/config.md +50 -2
- package/docs/extract-only.md +8 -0
- package/docs/index.html +205 -0
- package/docs/index.md +25 -0
- package/docs/llm.md +11 -1
- package/docs/manual-tests.md +2 -0
- package/docs/media.md +3 -0
- package/docs/nvidia-onnx-transcription.md +55 -0
- package/docs/site/assets/site.css +399 -228
- package/docs/site/assets/summarize-cli.png +0 -0
- package/docs/site/assets/summarize-extension.png +0 -0
- package/docs/site/docs/chrome-extension.html +89 -0
- package/docs/site/docs/config.html +1 -0
- package/docs/site/docs/extract-only.html +1 -0
- package/docs/site/docs/firecrawl.html +1 -0
- package/docs/site/docs/index.html +5 -0
- package/docs/site/docs/llm.html +1 -0
- package/docs/site/docs/openai.html +1 -0
- package/docs/site/docs/website.html +1 -0
- package/docs/site/docs/youtube.html +1 -0
- package/docs/site/index.html +148 -84
- package/docs/slides.md +74 -0
- package/docs/timestamps.md +103 -0
- package/docs/website.md +12 -0
- package/docs/youtube.md +16 -0
- package/package.json +16 -15
|
@@ -1,18 +1,26 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream, promises as fs } from 'node:fs';
|
|
2
3
|
import http from 'node:http';
|
|
4
|
+
import path from 'node:path';
|
|
3
5
|
import { Writable } from 'node:stream';
|
|
4
6
|
import { loadSummarizeConfig } from '../config.js';
|
|
5
7
|
import { createDaemonLogger } from '../logging/daemon.js';
|
|
8
|
+
import { runWithProcessContext, setProcessObserver } from '../processes.js';
|
|
6
9
|
import { refreshFree } from '../refresh-free.js';
|
|
7
10
|
import { createCacheStateFromConfig, refreshCacheStoreIfMissing } from '../run/cache-state.js';
|
|
11
|
+
import { resolveExecutableInPath } from '../run/env.js';
|
|
8
12
|
import { formatModelLabelForDisplay } from '../run/finish-line.js';
|
|
13
|
+
import { createMediaCacheFromConfig } from '../run/media-cache-state.js';
|
|
9
14
|
import { resolveRunOverrides } from '../run/run-settings.js';
|
|
10
15
|
import { encodeSseEvent } from '../shared/sse-events.js';
|
|
16
|
+
import { resolveSlideImagePath, resolveSlideSettings } from '../slides/index.js';
|
|
11
17
|
import { resolvePackageVersion } from '../version.js';
|
|
18
|
+
import { completeAgentResponse, streamAgentResponse } from './agent.js';
|
|
12
19
|
import { resolveAutoDaemonMode } from './auto-mode.js';
|
|
13
|
-
import { streamChatResponse } from './chat.js';
|
|
14
20
|
import { DAEMON_HOST, DAEMON_PORT_DEFAULT } from './constants.js';
|
|
21
|
+
import { resolveDaemonLogPaths } from './launchd.js';
|
|
15
22
|
import { buildModelPickerOptions } from './models.js';
|
|
23
|
+
import { buildProcessListResult, buildProcessLogsResult, ProcessRegistry, } from './process-registry.js';
|
|
16
24
|
import { extractContentForUrl, streamSummaryForUrl, streamSummaryForVisiblePage, } from './summarize.js';
|
|
17
25
|
function json(res, status, payload, headers) {
|
|
18
26
|
const body = `${JSON.stringify(payload)}\n`;
|
|
@@ -23,6 +31,39 @@ function json(res, status, payload, headers) {
|
|
|
23
31
|
});
|
|
24
32
|
res.end(body);
|
|
25
33
|
}
|
|
34
|
+
function clampNumber(value, min, max) {
|
|
35
|
+
if (!Number.isFinite(value))
|
|
36
|
+
return min;
|
|
37
|
+
return Math.max(min, Math.min(max, value));
|
|
38
|
+
}
|
|
39
|
+
async function readLogTail({ filePath, maxBytes, maxLines, }) {
|
|
40
|
+
const stat = await fs.stat(filePath);
|
|
41
|
+
const size = stat.size;
|
|
42
|
+
const readBytes = Math.max(0, Math.min(size, maxBytes));
|
|
43
|
+
const handle = await fs.open(filePath, 'r');
|
|
44
|
+
try {
|
|
45
|
+
const buffer = Buffer.alloc(readBytes);
|
|
46
|
+
const start = Math.max(0, size - readBytes);
|
|
47
|
+
await handle.read(buffer, 0, readBytes, start);
|
|
48
|
+
let text = buffer.toString('utf8');
|
|
49
|
+
let truncated = size > readBytes;
|
|
50
|
+
if (truncated) {
|
|
51
|
+
const firstNewline = text.indexOf('\n');
|
|
52
|
+
if (firstNewline !== -1) {
|
|
53
|
+
text = text.slice(firstNewline + 1);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
let lines = text.split(/\r?\n/).filter((line) => line.length > 0);
|
|
57
|
+
if (lines.length > maxLines) {
|
|
58
|
+
lines = lines.slice(lines.length - maxLines);
|
|
59
|
+
truncated = true;
|
|
60
|
+
}
|
|
61
|
+
return { lines, truncated, bytesRead: readBytes };
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
await handle.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
26
67
|
function text(res, status, body, headers) {
|
|
27
68
|
const out = body.endsWith('\n') ? body : `${body}\n`;
|
|
28
69
|
res.writeHead(status, {
|
|
@@ -75,6 +116,18 @@ async function readJsonBody(req, maxBytes) {
|
|
|
75
116
|
const text = Buffer.concat(chunks).toString('utf8');
|
|
76
117
|
return JSON.parse(text);
|
|
77
118
|
}
|
|
119
|
+
function wantsJsonResponse(req, url) {
|
|
120
|
+
const format = url.searchParams.get('format');
|
|
121
|
+
if (format && format.toLowerCase() === 'json')
|
|
122
|
+
return true;
|
|
123
|
+
const accept = req.headers.accept;
|
|
124
|
+
if (typeof accept !== 'string')
|
|
125
|
+
return false;
|
|
126
|
+
const lower = accept.toLowerCase();
|
|
127
|
+
if (lower.includes('text/event-stream'))
|
|
128
|
+
return false;
|
|
129
|
+
return lower.includes('application/json');
|
|
130
|
+
}
|
|
78
131
|
function parseDiagnostics(raw) {
|
|
79
132
|
if (!raw || typeof raw !== 'object') {
|
|
80
133
|
return { includeContent: false };
|
|
@@ -114,11 +167,21 @@ function createSession() {
|
|
|
114
167
|
bufferBytes: 0,
|
|
115
168
|
done: false,
|
|
116
169
|
clients: new Set(),
|
|
170
|
+
slidesBuffer: [],
|
|
171
|
+
slidesBufferBytes: 0,
|
|
172
|
+
slidesClients: new Set(),
|
|
173
|
+
slidesDone: false,
|
|
174
|
+
slidesRequested: false,
|
|
175
|
+
slidesLastStatus: null,
|
|
117
176
|
lastMeta: { model: null, modelLabel: null, inputSummary: null, summaryFromCache: null },
|
|
177
|
+
slides: null,
|
|
118
178
|
};
|
|
119
179
|
}
|
|
120
180
|
const MAX_SESSION_BUFFER_EVENTS = 2000;
|
|
121
181
|
const MAX_SESSION_BUFFER_BYTES = 512 * 1024;
|
|
182
|
+
const MAX_SLIDES_BUFFER_EVENTS = 600;
|
|
183
|
+
const MAX_SLIDES_BUFFER_BYTES = 256 * 1024;
|
|
184
|
+
const MAX_SESSION_LIFETIME_MS = 30 * 60_000;
|
|
122
185
|
function pushToSession(session, evt, onSessionEvent) {
|
|
123
186
|
const encoded = encodeSseEvent(evt);
|
|
124
187
|
for (const res of session.clients) {
|
|
@@ -139,6 +202,29 @@ function pushToSession(session, evt, onSessionEvent) {
|
|
|
139
202
|
session.done = true;
|
|
140
203
|
}
|
|
141
204
|
}
|
|
205
|
+
function pushSlidesToSession(session, evt, onSessionEvent) {
|
|
206
|
+
const encoded = encodeSseEvent(evt);
|
|
207
|
+
for (const res of session.slidesClients) {
|
|
208
|
+
res.write(encoded);
|
|
209
|
+
}
|
|
210
|
+
onSessionEvent?.(evt, session.id);
|
|
211
|
+
const bytes = Buffer.byteLength(encoded);
|
|
212
|
+
session.slidesBuffer.push({ event: evt, bytes });
|
|
213
|
+
session.slidesBufferBytes += bytes;
|
|
214
|
+
while (session.slidesBuffer.length > MAX_SLIDES_BUFFER_EVENTS ||
|
|
215
|
+
session.slidesBufferBytes > MAX_SLIDES_BUFFER_BYTES) {
|
|
216
|
+
const removed = session.slidesBuffer.shift();
|
|
217
|
+
if (!removed)
|
|
218
|
+
break;
|
|
219
|
+
session.slidesBufferBytes -= removed.bytes;
|
|
220
|
+
}
|
|
221
|
+
if (evt.event === 'done' || evt.event === 'error') {
|
|
222
|
+
session.slidesDone = true;
|
|
223
|
+
}
|
|
224
|
+
if (evt.event === 'status') {
|
|
225
|
+
session.slidesLastStatus = evt.data.text;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
142
228
|
function emitMeta(session, patch, onSessionEvent) {
|
|
143
229
|
const next = { ...session.lastMeta, ...patch };
|
|
144
230
|
if (next.model === session.lastMeta.model &&
|
|
@@ -150,11 +236,90 @@ function emitMeta(session, patch, onSessionEvent) {
|
|
|
150
236
|
session.lastMeta = next;
|
|
151
237
|
pushToSession(session, { event: 'meta', data: next }, onSessionEvent);
|
|
152
238
|
}
|
|
239
|
+
function emitSlides(session, data, onSessionEvent) {
|
|
240
|
+
pushToSession(session, { event: 'slides', data }, onSessionEvent);
|
|
241
|
+
pushSlidesToSession(session, { event: 'slides', data }, onSessionEvent);
|
|
242
|
+
}
|
|
243
|
+
function emitSlidesStatus(session, text, onSessionEvent) {
|
|
244
|
+
const trimmed = text.trim();
|
|
245
|
+
if (!trimmed)
|
|
246
|
+
return;
|
|
247
|
+
pushSlidesToSession(session, { event: 'status', data: { text: trimmed } }, onSessionEvent);
|
|
248
|
+
}
|
|
249
|
+
function emitSlidesDone(session, result, onSessionEvent) {
|
|
250
|
+
if (!result.ok) {
|
|
251
|
+
const message = result.error?.trim() || 'Slides failed.';
|
|
252
|
+
pushSlidesToSession(session, { event: 'error', data: { message } }, onSessionEvent);
|
|
253
|
+
}
|
|
254
|
+
pushSlidesToSession(session, { event: 'done', data: {} }, onSessionEvent);
|
|
255
|
+
}
|
|
256
|
+
function resolveHomeDir(env) {
|
|
257
|
+
const home = env.HOME?.trim() || env.USERPROFILE?.trim();
|
|
258
|
+
if (!home)
|
|
259
|
+
return process.cwd();
|
|
260
|
+
return home;
|
|
261
|
+
}
|
|
262
|
+
function resolveSlidesSettings({ env, request, }) {
|
|
263
|
+
const slidesValue = request.slides;
|
|
264
|
+
const tesseractAvailable = resolveToolPath('tesseract', env, 'TESSERACT_PATH') !== null;
|
|
265
|
+
const slidesOcrValue = tesseractAvailable ? request.slidesOcr : false;
|
|
266
|
+
return resolveSlideSettings({
|
|
267
|
+
slides: slidesValue,
|
|
268
|
+
slidesOcr: slidesOcrValue,
|
|
269
|
+
slidesDir: request.slidesDir ?? '.summarize/slides',
|
|
270
|
+
slidesSceneThreshold: request.slidesSceneThreshold,
|
|
271
|
+
slidesSceneThresholdExplicit: typeof request.slidesSceneThreshold !== 'undefined',
|
|
272
|
+
slidesMax: request.slidesMax,
|
|
273
|
+
slidesMinDuration: request.slidesMinDuration,
|
|
274
|
+
cwd: resolveHomeDir(env),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
function buildSlidesPayload({ slides, port, }) {
|
|
278
|
+
// Use a stable URL that survives session GC, so images don't break while scrolling.
|
|
279
|
+
const baseUrl = `http://127.0.0.1:${port}/v1/slides/${slides.sourceId}`;
|
|
280
|
+
return {
|
|
281
|
+
sourceUrl: slides.sourceUrl,
|
|
282
|
+
sourceId: slides.sourceId,
|
|
283
|
+
sourceKind: slides.sourceKind,
|
|
284
|
+
ocrAvailable: slides.ocrAvailable,
|
|
285
|
+
slides: slides.slides.map((slide) => ({
|
|
286
|
+
index: slide.index,
|
|
287
|
+
timestamp: slide.timestamp,
|
|
288
|
+
imageUrl: `${baseUrl}/${slide.index}${typeof slide.imageVersion === 'number' && slide.imageVersion > 0
|
|
289
|
+
? `?v=${slide.imageVersion}`
|
|
290
|
+
: ''}`,
|
|
291
|
+
ocrText: slide.ocrText ?? null,
|
|
292
|
+
ocrConfidence: slide.ocrConfidence ?? null,
|
|
293
|
+
})),
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
function resolveToolPath(binary, env, explicitEnvKey) {
|
|
297
|
+
const explicit = explicitEnvKey && typeof env[explicitEnvKey] === 'string' ? env[explicitEnvKey]?.trim() : '';
|
|
298
|
+
if (explicit)
|
|
299
|
+
return resolveExecutableInPath(explicit, env);
|
|
300
|
+
return resolveExecutableInPath(binary, env);
|
|
301
|
+
}
|
|
153
302
|
function endSession(session) {
|
|
154
303
|
for (const res of session.clients) {
|
|
155
304
|
res.end();
|
|
156
305
|
}
|
|
157
306
|
session.clients.clear();
|
|
307
|
+
for (const res of session.slidesClients) {
|
|
308
|
+
res.end();
|
|
309
|
+
}
|
|
310
|
+
session.slidesClients.clear();
|
|
311
|
+
}
|
|
312
|
+
function scheduleSessionCleanup({ session, sessions, delayMs = 60_000, }) {
|
|
313
|
+
setTimeout(() => {
|
|
314
|
+
const ageMs = Date.now() - session.createdAtMs;
|
|
315
|
+
const slidesPending = session.slidesRequested && !session.slidesDone;
|
|
316
|
+
if (!slidesPending || ageMs > MAX_SESSION_LIFETIME_MS) {
|
|
317
|
+
sessions.delete(session.id);
|
|
318
|
+
endSession(session);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
scheduleSessionCleanup({ session, sessions, delayMs });
|
|
322
|
+
}, delayMs).unref();
|
|
158
323
|
}
|
|
159
324
|
export function buildHealthPayload(importMetaUrl) {
|
|
160
325
|
return { ok: true, pid: process.pid, version: resolvePackageVersion(importMetaUrl) };
|
|
@@ -162,12 +327,21 @@ export function buildHealthPayload(importMetaUrl) {
|
|
|
162
327
|
export async function runDaemonServer({ env, fetchImpl, config, port = config.port ?? DAEMON_PORT_DEFAULT, signal, onListening, onSessionEvent, }) {
|
|
163
328
|
const { config: summarizeConfig } = loadSummarizeConfig({ env });
|
|
164
329
|
const daemonLogger = createDaemonLogger({ env, config: summarizeConfig });
|
|
330
|
+
const daemonLogPaths = resolveDaemonLogPaths(env);
|
|
331
|
+
const daemonLogFile = daemonLogger.config?.file ?? path.join(daemonLogPaths.logDir, 'daemon.jsonl');
|
|
165
332
|
const cacheState = await createCacheStateFromConfig({
|
|
166
333
|
envForRun: env,
|
|
167
334
|
config: summarizeConfig,
|
|
168
335
|
noCacheFlag: false,
|
|
169
336
|
transcriptNamespace: 'yt:auto',
|
|
170
337
|
});
|
|
338
|
+
const mediaCache = await createMediaCacheFromConfig({
|
|
339
|
+
envForRun: env,
|
|
340
|
+
config: summarizeConfig,
|
|
341
|
+
noMediaCacheFlag: false,
|
|
342
|
+
});
|
|
343
|
+
const processRegistry = new ProcessRegistry();
|
|
344
|
+
setProcessObserver(processRegistry.createObserver());
|
|
171
345
|
const sessions = new Map();
|
|
172
346
|
const refreshSessions = new Map();
|
|
173
347
|
let activeRefreshSessionId = null;
|
|
@@ -196,6 +370,76 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
196
370
|
json(res, 200, { ok: true }, cors);
|
|
197
371
|
return;
|
|
198
372
|
}
|
|
373
|
+
if (req.method === 'GET' && pathname === '/v1/logs') {
|
|
374
|
+
const source = url.searchParams.get('source')?.trim() || 'daemon';
|
|
375
|
+
const tailParam = url.searchParams.get('tail')?.trim() || '';
|
|
376
|
+
const tail = clampNumber(Number(tailParam || '800'), 50, 5000);
|
|
377
|
+
const maxBytes = clampNumber(Number(url.searchParams.get('maxBytes') ?? '262144'), 16_384, 2_000_000);
|
|
378
|
+
const sources = {
|
|
379
|
+
daemon: {
|
|
380
|
+
filePath: daemonLogFile,
|
|
381
|
+
format: daemonLogger.config?.format ?? 'json',
|
|
382
|
+
enabled: daemonLogger.enabled,
|
|
383
|
+
},
|
|
384
|
+
stdout: { filePath: daemonLogPaths.stdoutPath, format: 'text' },
|
|
385
|
+
stderr: { filePath: daemonLogPaths.stderrPath, format: 'text' },
|
|
386
|
+
};
|
|
387
|
+
const selected = sources[source];
|
|
388
|
+
if (!selected) {
|
|
389
|
+
json(res, 400, { ok: false, error: `Unknown log source "${source}".` }, cors);
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
const stat = await fs.stat(selected.filePath).catch(() => null);
|
|
393
|
+
if (!stat?.isFile()) {
|
|
394
|
+
const disabledNote = source === 'daemon' && selected.enabled === false
|
|
395
|
+
? 'Daemon logging is disabled (no log file).'
|
|
396
|
+
: 'Log file not found.';
|
|
397
|
+
json(res, 404, { ok: false, error: disabledNote }, cors);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const { lines, truncated, bytesRead } = await readLogTail({
|
|
401
|
+
filePath: selected.filePath,
|
|
402
|
+
maxBytes,
|
|
403
|
+
maxLines: tail,
|
|
404
|
+
});
|
|
405
|
+
const warning = source === 'daemon' && selected.enabled === false
|
|
406
|
+
? 'Daemon logging disabled; showing existing file only.'
|
|
407
|
+
: null;
|
|
408
|
+
json(res, 200, {
|
|
409
|
+
ok: true,
|
|
410
|
+
source,
|
|
411
|
+
format: selected.format,
|
|
412
|
+
lines,
|
|
413
|
+
truncated,
|
|
414
|
+
bytesRead,
|
|
415
|
+
sizeBytes: stat.size,
|
|
416
|
+
mtimeMs: stat.mtimeMs,
|
|
417
|
+
...(warning ? { warning } : {}),
|
|
418
|
+
}, cors);
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const processLogsMatch = pathname.match(/^\/v1\/processes\/([^/]+)\/logs$/);
|
|
422
|
+
if (req.method === 'GET' && processLogsMatch) {
|
|
423
|
+
const id = processLogsMatch[1];
|
|
424
|
+
const tail = clampNumber(Number(url.searchParams.get('tail') ?? '200'), 20, 1000);
|
|
425
|
+
const streamRaw = (url.searchParams.get('stream') ?? 'merged').toLowerCase();
|
|
426
|
+
const stream = streamRaw === 'stdout' || streamRaw === 'stderr' ? streamRaw : 'merged';
|
|
427
|
+
const result = buildProcessLogsResult(processRegistry, id, { tail, stream });
|
|
428
|
+
if (!result) {
|
|
429
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
json(res, 200, result, cors);
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (req.method === 'GET' && pathname === '/v1/processes') {
|
|
436
|
+
const includeCompleted = (url.searchParams.get('includeCompleted') ?? '').toLowerCase() === 'true' ||
|
|
437
|
+
url.searchParams.get('includeCompleted') === '1';
|
|
438
|
+
const limit = clampNumber(Number(url.searchParams.get('limit') ?? '80'), 10, 200);
|
|
439
|
+
const result = buildProcessListResult(processRegistry, { includeCompleted, limit });
|
|
440
|
+
json(res, 200, result, cors);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
199
443
|
if (req.method === 'GET' && pathname === '/v1/models') {
|
|
200
444
|
const result = await buildModelPickerOptions({
|
|
201
445
|
env,
|
|
@@ -206,6 +450,20 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
206
450
|
json(res, 200, result, cors);
|
|
207
451
|
return;
|
|
208
452
|
}
|
|
453
|
+
if (req.method === 'GET' && pathname === '/v1/tools') {
|
|
454
|
+
const ytDlpPath = resolveToolPath('yt-dlp', env, 'YT_DLP_PATH');
|
|
455
|
+
const ffmpegPath = resolveToolPath('ffmpeg', env, 'FFMPEG_PATH');
|
|
456
|
+
const tesseractPath = resolveToolPath('tesseract', env, 'TESSERACT_PATH');
|
|
457
|
+
json(res, 200, {
|
|
458
|
+
ok: true,
|
|
459
|
+
tools: {
|
|
460
|
+
ytDlp: { available: Boolean(ytDlpPath), path: ytDlpPath },
|
|
461
|
+
ffmpeg: { available: Boolean(ffmpegPath), path: ffmpegPath },
|
|
462
|
+
tesseract: { available: Boolean(tesseractPath), path: tesseractPath },
|
|
463
|
+
},
|
|
464
|
+
}, cors);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
209
467
|
if (req.method === 'POST' && pathname === '/v1/refresh-free') {
|
|
210
468
|
if (activeRefreshSessionId) {
|
|
211
469
|
json(res, 200, { ok: true, id: activeRefreshSessionId, running: true }, cors);
|
|
@@ -272,19 +530,27 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
272
530
|
const extractOnly = Boolean(obj.extractOnly);
|
|
273
531
|
const modeRaw = typeof obj.mode === 'string' ? obj.mode.trim().toLowerCase() : '';
|
|
274
532
|
const mode = modeRaw === 'url' ? 'url' : modeRaw === 'page' ? 'page' : 'auto';
|
|
275
|
-
const
|
|
276
|
-
? obj.
|
|
277
|
-
:
|
|
533
|
+
const maxCharactersCandidate = typeof obj.maxExtractCharacters === 'number' && Number.isFinite(obj.maxExtractCharacters)
|
|
534
|
+
? obj.maxExtractCharacters
|
|
535
|
+
: typeof obj.maxCharacters === 'number' && Number.isFinite(obj.maxCharacters)
|
|
536
|
+
? obj.maxCharacters
|
|
537
|
+
: null;
|
|
538
|
+
const maxCharacters = maxCharactersCandidate && maxCharactersCandidate > 0 ? maxCharactersCandidate : null;
|
|
539
|
+
const formatRaw = typeof obj.format === 'string' ? obj.format.trim().toLowerCase() : '';
|
|
540
|
+
const format = formatRaw === 'markdown' || formatRaw === 'md' ? 'markdown' : 'text';
|
|
278
541
|
const overrides = resolveRunOverrides({
|
|
279
542
|
firecrawl: obj.firecrawl,
|
|
280
543
|
markdownMode: obj.markdownMode,
|
|
281
544
|
preprocess: obj.preprocess,
|
|
282
545
|
youtube: obj.youtube,
|
|
283
546
|
videoMode: obj.videoMode,
|
|
547
|
+
timestamps: obj.timestamps,
|
|
548
|
+
forceSummary: obj.forceSummary,
|
|
284
549
|
timeout: obj.timeout,
|
|
285
550
|
retries: obj.retries,
|
|
286
551
|
maxOutputTokens: obj.maxOutputTokens,
|
|
287
552
|
});
|
|
553
|
+
const slidesSettings = resolveSlidesSettings({ env, request: obj });
|
|
288
554
|
const diagnostics = parseDiagnostics(obj.diagnostics);
|
|
289
555
|
const includeContentLog = daemonLogger.enabled && diagnostics.includeContent;
|
|
290
556
|
const hasText = Boolean(textContent.trim());
|
|
@@ -301,13 +567,31 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
301
567
|
const requestCache = noCache
|
|
302
568
|
? { ...cacheState, mode: 'bypass', store: null }
|
|
303
569
|
: cacheState;
|
|
304
|
-
const
|
|
570
|
+
const runId = randomUUID();
|
|
571
|
+
const { extracted, slides } = await runWithProcessContext({ runId, source: 'extract' }, async () => extractContentForUrl({
|
|
305
572
|
env,
|
|
306
573
|
fetchImpl,
|
|
307
574
|
input: { url: pageUrl, title, maxCharacters },
|
|
308
575
|
cache: requestCache,
|
|
576
|
+
mediaCache,
|
|
309
577
|
overrides,
|
|
310
|
-
|
|
578
|
+
format,
|
|
579
|
+
slides: slidesSettings,
|
|
580
|
+
}));
|
|
581
|
+
const slidesPayload = slides && slides.slides.length > 0
|
|
582
|
+
? {
|
|
583
|
+
sourceUrl: slides.sourceUrl,
|
|
584
|
+
sourceId: slides.sourceId,
|
|
585
|
+
sourceKind: slides.sourceKind,
|
|
586
|
+
ocrAvailable: slides.ocrAvailable,
|
|
587
|
+
slides: slides.slides.map((slide) => ({
|
|
588
|
+
index: slide.index,
|
|
589
|
+
timestamp: slide.timestamp,
|
|
590
|
+
ocrText: slide.ocrText ?? null,
|
|
591
|
+
ocrConfidence: slide.ocrConfidence ?? null,
|
|
592
|
+
})),
|
|
593
|
+
}
|
|
594
|
+
: null;
|
|
311
595
|
json(res, 200, {
|
|
312
596
|
ok: true,
|
|
313
597
|
extracted: {
|
|
@@ -321,10 +605,13 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
321
605
|
transcriptCharacters: extracted.transcriptCharacters ?? null,
|
|
322
606
|
transcriptWordCount: extracted.transcriptWordCount ?? null,
|
|
323
607
|
transcriptLines: extracted.transcriptLines ?? null,
|
|
608
|
+
transcriptSegments: extracted.transcriptSegments ?? null,
|
|
609
|
+
transcriptTimedText: extracted.transcriptTimedText ?? null,
|
|
324
610
|
transcriptionProvider: extracted.transcriptionProvider ?? null,
|
|
325
611
|
mediaDurationSeconds: extracted.mediaDurationSeconds ?? null,
|
|
326
612
|
diagnostics: extracted.diagnostics,
|
|
327
613
|
},
|
|
614
|
+
...(slidesPayload ? { slides: slidesPayload } : {}),
|
|
328
615
|
}, cors);
|
|
329
616
|
}
|
|
330
617
|
catch (error) {
|
|
@@ -338,6 +625,7 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
338
625
|
return;
|
|
339
626
|
}
|
|
340
627
|
const session = createSession();
|
|
628
|
+
session.slidesRequested = Boolean(slidesSettings);
|
|
341
629
|
sessions.set(session.id, session);
|
|
342
630
|
const requestLogger = daemonLogger.getSubLogger('daemon.summarize', {
|
|
343
631
|
requestId: session.id,
|
|
@@ -355,6 +643,17 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
355
643
|
truncated: hasText ? truncated : null,
|
|
356
644
|
}
|
|
357
645
|
: null;
|
|
646
|
+
const logSlidesSettings = includeContentLog && slidesSettings
|
|
647
|
+
? {
|
|
648
|
+
enabled: slidesSettings.enabled,
|
|
649
|
+
ocr: slidesSettings.ocr,
|
|
650
|
+
outputDir: slidesSettings.outputDir,
|
|
651
|
+
sceneThreshold: slidesSettings.sceneThreshold,
|
|
652
|
+
autoTuneThreshold: slidesSettings.autoTuneThreshold,
|
|
653
|
+
maxSlides: slidesSettings.maxSlides,
|
|
654
|
+
minDurationSeconds: slidesSettings.minDurationSeconds,
|
|
655
|
+
}
|
|
656
|
+
: null;
|
|
358
657
|
requestLogger?.info({
|
|
359
658
|
event: 'summarize.request',
|
|
360
659
|
url: pageUrl,
|
|
@@ -365,9 +664,23 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
365
664
|
language: languageRaw,
|
|
366
665
|
model: modelOverride,
|
|
367
666
|
includeContent: includeContentLog,
|
|
667
|
+
slides: Boolean(slidesSettings),
|
|
668
|
+
...(logSlidesSettings ? { slidesSettings: logSlidesSettings } : {}),
|
|
669
|
+
...(includeContentLog ? { diagnostics } : {}),
|
|
368
670
|
});
|
|
369
671
|
json(res, 200, { ok: true, id: session.id }, cors);
|
|
370
|
-
void (async () => {
|
|
672
|
+
void runWithProcessContext({ runId: session.id, source: 'summarize' }, async () => {
|
|
673
|
+
const slideLogState = {
|
|
674
|
+
startedAt: null,
|
|
675
|
+
requested: Boolean(slidesSettings),
|
|
676
|
+
cacheHit: false,
|
|
677
|
+
lastStatus: null,
|
|
678
|
+
statusCount: 0,
|
|
679
|
+
elapsedMs: null,
|
|
680
|
+
slidesCount: null,
|
|
681
|
+
ocrAvailable: null,
|
|
682
|
+
warnings: [],
|
|
683
|
+
};
|
|
371
684
|
try {
|
|
372
685
|
let emittedOutput = false;
|
|
373
686
|
const sink = {
|
|
@@ -410,7 +723,20 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
410
723
|
const requestCache = noCache
|
|
411
724
|
? { ...cacheState, mode: 'bypass', store: null }
|
|
412
725
|
: cacheState;
|
|
726
|
+
let liveSlides = null;
|
|
413
727
|
const runWithMode = async (resolved) => {
|
|
728
|
+
if (resolved === 'url' && slideLogState.requested) {
|
|
729
|
+
slideLogState.startedAt = Date.now();
|
|
730
|
+
console.log(`[summarize-daemon] slides: start url=${pageUrl} (session=${session.id})`);
|
|
731
|
+
if (includeContentLog) {
|
|
732
|
+
requestLogger?.info({
|
|
733
|
+
event: 'slides.start',
|
|
734
|
+
url: pageUrl,
|
|
735
|
+
sessionId: session.id,
|
|
736
|
+
...(logSlidesSettings ? { settings: logSlidesSettings } : {}),
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
}
|
|
414
740
|
return resolved === 'url'
|
|
415
741
|
? await streamSummaryForUrl({
|
|
416
742
|
env,
|
|
@@ -419,17 +745,123 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
419
745
|
promptOverride,
|
|
420
746
|
lengthRaw,
|
|
421
747
|
languageRaw,
|
|
748
|
+
format,
|
|
422
749
|
input: { url: pageUrl, title, maxCharacters },
|
|
423
750
|
sink,
|
|
424
751
|
cache: requestCache,
|
|
752
|
+
mediaCache,
|
|
425
753
|
overrides,
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
754
|
+
slides: slidesSettings,
|
|
755
|
+
hooks: {
|
|
756
|
+
...(includeContentLog
|
|
757
|
+
? {
|
|
758
|
+
onExtracted: (content) => {
|
|
759
|
+
logExtracted = content;
|
|
760
|
+
},
|
|
761
|
+
}
|
|
762
|
+
: {}),
|
|
763
|
+
onSlidesExtracted: (slides) => {
|
|
764
|
+
session.slides = slides;
|
|
765
|
+
slideLogState.slidesCount = slides.slides.length;
|
|
766
|
+
slideLogState.ocrAvailable = slides.ocrAvailable;
|
|
767
|
+
slideLogState.warnings = slides.warnings;
|
|
768
|
+
if (slideLogState.startedAt) {
|
|
769
|
+
slideLogState.elapsedMs = Date.now() - slideLogState.startedAt;
|
|
770
|
+
}
|
|
771
|
+
if (slideLogState.startedAt) {
|
|
772
|
+
const elapsedMs = Date.now() - slideLogState.startedAt;
|
|
773
|
+
console.log(`[summarize-daemon] slides: done count=${slides.slides.length} ocr=${slides.ocrAvailable} elapsedMs=${elapsedMs} warnings=${slides.warnings.join('; ')}`);
|
|
774
|
+
}
|
|
775
|
+
if (includeContentLog) {
|
|
776
|
+
requestLogger?.info({
|
|
777
|
+
event: 'slides.done',
|
|
778
|
+
url: pageUrl,
|
|
779
|
+
sessionId: session.id,
|
|
780
|
+
slidesCount: slides.slides.length,
|
|
781
|
+
ocrAvailable: slides.ocrAvailable,
|
|
782
|
+
elapsedMs: slideLogState.elapsedMs,
|
|
783
|
+
cacheHit: slideLogState.cacheHit,
|
|
784
|
+
warnings: slides.warnings,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
emitSlides(session, buildSlidesPayload({
|
|
788
|
+
slides,
|
|
789
|
+
port,
|
|
790
|
+
}), onSessionEvent);
|
|
791
|
+
},
|
|
792
|
+
onSlidesDone: (result) => {
|
|
793
|
+
emitSlidesDone(session, result, onSessionEvent);
|
|
794
|
+
},
|
|
795
|
+
onSlidesProgress: (text) => {
|
|
796
|
+
const clean = typeof text === 'string' ? text.trim() : '';
|
|
797
|
+
if (!clean)
|
|
798
|
+
return;
|
|
799
|
+
slideLogState.lastStatus = clean;
|
|
800
|
+
slideLogState.statusCount += 1;
|
|
801
|
+
if (clean.toLowerCase().includes('cached')) {
|
|
802
|
+
slideLogState.cacheHit = true;
|
|
803
|
+
}
|
|
804
|
+
const progressMatch = clean.match(/(\d+)%/);
|
|
805
|
+
const progress = progressMatch ? Number(progressMatch[1]) : null;
|
|
806
|
+
if (includeContentLog) {
|
|
807
|
+
requestLogger?.info({
|
|
808
|
+
event: 'slides.status',
|
|
809
|
+
url: pageUrl,
|
|
810
|
+
sessionId: session.id,
|
|
811
|
+
status: clean,
|
|
812
|
+
...(progress !== null ? { progress } : {}),
|
|
813
|
+
});
|
|
814
|
+
}
|
|
815
|
+
emitSlidesStatus(session, clean, onSessionEvent);
|
|
816
|
+
},
|
|
817
|
+
onSlideChunk: (chunk) => {
|
|
818
|
+
const { slide, meta } = chunk;
|
|
819
|
+
if (slide == null ||
|
|
820
|
+
!meta?.slidesDir ||
|
|
821
|
+
!meta.sourceUrl ||
|
|
822
|
+
!meta.sourceId ||
|
|
823
|
+
!meta.sourceKind) {
|
|
824
|
+
return;
|
|
825
|
+
}
|
|
826
|
+
const nextSlides = liveSlides ?? {
|
|
827
|
+
sourceUrl: meta.sourceUrl,
|
|
828
|
+
sourceKind: meta.sourceKind,
|
|
829
|
+
sourceId: meta.sourceId,
|
|
830
|
+
slidesDir: meta.slidesDir,
|
|
831
|
+
sceneThreshold: 0,
|
|
832
|
+
autoTuneThreshold: false,
|
|
833
|
+
autoTune: {
|
|
834
|
+
enabled: false,
|
|
835
|
+
chosenThreshold: 0,
|
|
836
|
+
confidence: 0,
|
|
837
|
+
strategy: 'none',
|
|
838
|
+
},
|
|
839
|
+
maxSlides: 0,
|
|
840
|
+
minSlideDuration: 0,
|
|
841
|
+
ocrRequested: meta.ocrAvailable,
|
|
842
|
+
ocrAvailable: meta.ocrAvailable,
|
|
843
|
+
slides: [],
|
|
844
|
+
warnings: [],
|
|
845
|
+
};
|
|
846
|
+
liveSlides = nextSlides;
|
|
847
|
+
const existingIndex = nextSlides.slides.findIndex((item) => item.index === slide.index);
|
|
848
|
+
if (existingIndex >= 0) {
|
|
849
|
+
nextSlides.slides[existingIndex] = {
|
|
850
|
+
...nextSlides.slides[existingIndex],
|
|
851
|
+
...slide,
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
else {
|
|
855
|
+
nextSlides.slides.push(slide);
|
|
856
|
+
}
|
|
857
|
+
nextSlides.slides.sort((a, b) => a.index - b.index);
|
|
858
|
+
session.slides = nextSlides;
|
|
859
|
+
emitSlides(session, buildSlidesPayload({
|
|
860
|
+
slides: nextSlides,
|
|
861
|
+
port,
|
|
862
|
+
}), onSessionEvent);
|
|
863
|
+
},
|
|
864
|
+
},
|
|
433
865
|
})
|
|
434
866
|
: await streamSummaryForVisiblePage({
|
|
435
867
|
env,
|
|
@@ -438,9 +870,11 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
438
870
|
promptOverride,
|
|
439
871
|
lengthRaw,
|
|
440
872
|
languageRaw,
|
|
873
|
+
format,
|
|
441
874
|
input: { url: pageUrl, title, text: textContent, truncated },
|
|
442
875
|
sink,
|
|
443
876
|
cache: requestCache,
|
|
877
|
+
mediaCache,
|
|
444
878
|
overrides,
|
|
445
879
|
});
|
|
446
880
|
};
|
|
@@ -481,6 +915,20 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
481
915
|
elapsedMs: Date.now() - logStartedAt,
|
|
482
916
|
summaryFromCache: logSummaryFromCache,
|
|
483
917
|
inputSummary: logInputSummary,
|
|
918
|
+
...(includeContentLog && slideLogState.requested
|
|
919
|
+
? {
|
|
920
|
+
slides: {
|
|
921
|
+
requested: true,
|
|
922
|
+
cacheHit: slideLogState.cacheHit,
|
|
923
|
+
lastStatus: slideLogState.lastStatus,
|
|
924
|
+
statusCount: slideLogState.statusCount,
|
|
925
|
+
elapsedMs: slideLogState.elapsedMs,
|
|
926
|
+
slidesCount: slideLogState.slidesCount,
|
|
927
|
+
ocrAvailable: slideLogState.ocrAvailable,
|
|
928
|
+
warnings: slideLogState.warnings,
|
|
929
|
+
},
|
|
930
|
+
}
|
|
931
|
+
: {}),
|
|
484
932
|
...(includeContentLog && !logSummaryFromCache
|
|
485
933
|
? {
|
|
486
934
|
input: logInput,
|
|
@@ -493,6 +941,9 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
493
941
|
catch (error) {
|
|
494
942
|
const message = error instanceof Error ? error.message : String(error);
|
|
495
943
|
pushToSession(session, { event: 'error', data: { message } }, onSessionEvent);
|
|
944
|
+
if (session.slidesRequested && !session.slidesDone) {
|
|
945
|
+
emitSlidesDone(session, { ok: false, error: message }, onSessionEvent);
|
|
946
|
+
}
|
|
496
947
|
// Preserve full stack trace in daemon logs for debugging.
|
|
497
948
|
console.error('[summarize-daemon] summarize failed', error);
|
|
498
949
|
requestLogger?.error({
|
|
@@ -502,6 +953,20 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
502
953
|
elapsedMs: Date.now() - logStartedAt,
|
|
503
954
|
summaryFromCache: logSummaryFromCache,
|
|
504
955
|
inputSummary: logInputSummary,
|
|
956
|
+
...(includeContentLog && slideLogState.requested
|
|
957
|
+
? {
|
|
958
|
+
slides: {
|
|
959
|
+
requested: true,
|
|
960
|
+
cacheHit: slideLogState.cacheHit,
|
|
961
|
+
lastStatus: slideLogState.lastStatus,
|
|
962
|
+
statusCount: slideLogState.statusCount,
|
|
963
|
+
elapsedMs: slideLogState.elapsedMs,
|
|
964
|
+
slidesCount: slideLogState.slidesCount,
|
|
965
|
+
ocrAvailable: slideLogState.ocrAvailable,
|
|
966
|
+
warnings: slideLogState.warnings,
|
|
967
|
+
},
|
|
968
|
+
}
|
|
969
|
+
: {}),
|
|
505
970
|
error: {
|
|
506
971
|
message,
|
|
507
972
|
stack: error instanceof Error ? error.stack : null,
|
|
@@ -516,19 +981,15 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
516
981
|
});
|
|
517
982
|
}
|
|
518
983
|
finally {
|
|
519
|
-
|
|
520
|
-
sessions.delete(session.id);
|
|
521
|
-
endSession(session);
|
|
522
|
-
}, 60_000).unref();
|
|
984
|
+
scheduleSessionCleanup({ session, sessions });
|
|
523
985
|
}
|
|
524
|
-
})
|
|
986
|
+
});
|
|
525
987
|
return;
|
|
526
988
|
}
|
|
527
|
-
if (req.method === 'POST' && pathname === '/v1/
|
|
528
|
-
await refreshCacheStoreIfMissing({ cacheState, transcriptNamespace: 'yt:auto' });
|
|
989
|
+
if (req.method === 'POST' && pathname === '/v1/agent') {
|
|
529
990
|
let body;
|
|
530
991
|
try {
|
|
531
|
-
body = await readJsonBody(req,
|
|
992
|
+
body = await readJsonBody(req, 4_000_000);
|
|
532
993
|
}
|
|
533
994
|
catch (error) {
|
|
534
995
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -543,42 +1004,203 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
543
1004
|
const pageUrl = typeof obj.url === 'string' ? obj.url.trim() : '';
|
|
544
1005
|
const pageTitle = typeof obj.title === 'string' ? obj.title.trim() : null;
|
|
545
1006
|
const pageContent = typeof obj.pageContent === 'string' ? obj.pageContent : '';
|
|
546
|
-
const messages =
|
|
1007
|
+
const messages = obj.messages;
|
|
547
1008
|
const modelOverride = typeof obj.model === 'string' ? obj.model.trim() : null;
|
|
1009
|
+
const tools = Array.isArray(obj.tools)
|
|
1010
|
+
? obj.tools.filter((tool) => typeof tool === 'string')
|
|
1011
|
+
: [];
|
|
1012
|
+
const automationEnabled = Boolean(obj.automationEnabled);
|
|
548
1013
|
if (!pageUrl) {
|
|
549
1014
|
json(res, 400, { ok: false, error: 'missing url' }, cors);
|
|
550
1015
|
return;
|
|
551
1016
|
}
|
|
552
|
-
const
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
void (async () => {
|
|
1017
|
+
const runId = `agent-${randomUUID()}`;
|
|
1018
|
+
const wantsJson = wantsJsonResponse(req, url);
|
|
1019
|
+
if (wantsJson) {
|
|
556
1020
|
try {
|
|
557
|
-
await
|
|
1021
|
+
const assistant = await runWithProcessContext({ runId, source: 'agent' }, async () => completeAgentResponse({
|
|
558
1022
|
env,
|
|
559
|
-
fetchImpl,
|
|
560
|
-
session,
|
|
561
1023
|
pageUrl,
|
|
562
1024
|
pageTitle,
|
|
563
1025
|
pageContent,
|
|
564
|
-
messages
|
|
1026
|
+
messages,
|
|
565
1027
|
modelOverride: modelOverride && modelOverride.toLowerCase() !== 'auto' ? modelOverride : null,
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
});
|
|
1028
|
+
tools,
|
|
1029
|
+
automationEnabled,
|
|
1030
|
+
}));
|
|
1031
|
+
json(res, 200, { ok: true, assistant }, cors);
|
|
569
1032
|
}
|
|
570
1033
|
catch (error) {
|
|
571
1034
|
const message = error instanceof Error ? error.message : String(error);
|
|
572
|
-
|
|
573
|
-
|
|
1035
|
+
console.error('[summarize-daemon] agent failed', error);
|
|
1036
|
+
json(res, 500, { ok: false, error: message }, cors);
|
|
574
1037
|
}
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
res.writeHead(200, {
|
|
1041
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1042
|
+
'cache-control': 'no-cache',
|
|
1043
|
+
connection: 'keep-alive',
|
|
1044
|
+
'x-accel-buffering': 'no',
|
|
1045
|
+
...cors,
|
|
1046
|
+
});
|
|
1047
|
+
const controller = new AbortController();
|
|
1048
|
+
const abort = () => controller.abort();
|
|
1049
|
+
req.on('close', abort);
|
|
1050
|
+
res.on('close', abort);
|
|
1051
|
+
const writeEvent = (event) => {
|
|
1052
|
+
if (res.writableEnded)
|
|
1053
|
+
return;
|
|
1054
|
+
res.write(encodeSseEvent(event));
|
|
1055
|
+
};
|
|
1056
|
+
try {
|
|
1057
|
+
await runWithProcessContext({ runId, source: 'agent' }, async () => streamAgentResponse({
|
|
1058
|
+
env,
|
|
1059
|
+
pageUrl,
|
|
1060
|
+
pageTitle,
|
|
1061
|
+
pageContent,
|
|
1062
|
+
messages,
|
|
1063
|
+
modelOverride: modelOverride && modelOverride.toLowerCase() !== 'auto' ? modelOverride : null,
|
|
1064
|
+
tools,
|
|
1065
|
+
automationEnabled,
|
|
1066
|
+
onChunk: (text) => writeEvent({ event: 'chunk', data: { text } }),
|
|
1067
|
+
onAssistant: (assistant) => writeEvent({ event: 'assistant', data: assistant }),
|
|
1068
|
+
signal: controller.signal,
|
|
1069
|
+
}));
|
|
1070
|
+
writeEvent({ event: 'done', data: {} });
|
|
1071
|
+
res.end();
|
|
1072
|
+
}
|
|
1073
|
+
catch (error) {
|
|
1074
|
+
if (controller.signal.aborted)
|
|
1075
|
+
return;
|
|
1076
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1077
|
+
console.error('[summarize-daemon] agent failed', error);
|
|
1078
|
+
writeEvent({ event: 'error', data: { message } });
|
|
1079
|
+
writeEvent({ event: 'done', data: {} });
|
|
1080
|
+
res.end();
|
|
1081
|
+
}
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
const slidesMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides$/);
|
|
1085
|
+
if (req.method === 'GET' && slidesMatch) {
|
|
1086
|
+
const id = slidesMatch[1];
|
|
1087
|
+
const session = id ? sessions.get(id) : null;
|
|
1088
|
+
if (!session || !session.slides) {
|
|
1089
|
+
json(res, 200, { ok: false, error: 'not found' }, cors);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
json(res, 200, { ok: true, slides: buildSlidesPayload({ slides: session.slides, port }) }, cors);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const slideImageMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides\/(\d+)$/);
|
|
1096
|
+
if (req.method === 'GET' && slideImageMatch) {
|
|
1097
|
+
const id = slideImageMatch[1];
|
|
1098
|
+
const index = Number(slideImageMatch[2]);
|
|
1099
|
+
const session = id ? sessions.get(id) : null;
|
|
1100
|
+
if (!session || !session.slides || !Number.isFinite(index)) {
|
|
1101
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
const slide = session.slides.slides.find((item) => item.index === index);
|
|
1105
|
+
if (!slide) {
|
|
1106
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1107
|
+
return;
|
|
1108
|
+
}
|
|
1109
|
+
try {
|
|
1110
|
+
const stat = await fs.stat(slide.imagePath);
|
|
1111
|
+
res.writeHead(200, {
|
|
1112
|
+
'content-type': 'image/png',
|
|
1113
|
+
'content-length': stat.size.toString(),
|
|
1114
|
+
'cache-control': 'no-cache',
|
|
1115
|
+
...cors,
|
|
1116
|
+
});
|
|
1117
|
+
const stream = createReadStream(slide.imagePath);
|
|
1118
|
+
stream.pipe(res);
|
|
1119
|
+
stream.on('error', () => res.end());
|
|
1120
|
+
}
|
|
1121
|
+
catch {
|
|
1122
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1123
|
+
}
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const stableSlideImageMatch = pathname.match(/^\/v1\/slides\/([^/]+)\/(\d+)$/);
|
|
1127
|
+
if (req.method === 'GET' && stableSlideImageMatch) {
|
|
1128
|
+
const sourceId = stableSlideImageMatch[1];
|
|
1129
|
+
const index = Number(stableSlideImageMatch[2]);
|
|
1130
|
+
if (!sourceId || !Number.isFinite(index) || index <= 0) {
|
|
1131
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const slidesRoot = path.resolve(resolveHomeDir(env), '.summarize', 'slides');
|
|
1135
|
+
const slidesDir = path.join(slidesRoot, sourceId);
|
|
1136
|
+
const payloadPath = path.join(slidesDir, 'slides.json');
|
|
1137
|
+
const resolveFromDisk = async () => {
|
|
1138
|
+
const raw = await fs.readFile(payloadPath, 'utf8').catch(() => null);
|
|
1139
|
+
if (raw) {
|
|
1140
|
+
try {
|
|
1141
|
+
const parsed = JSON.parse(raw);
|
|
1142
|
+
const slide = parsed?.slides?.find?.((item) => item?.index === index);
|
|
1143
|
+
if (slide?.imagePath) {
|
|
1144
|
+
const resolved = resolveSlideImagePath(slidesDir, slide.imagePath);
|
|
1145
|
+
if (resolved)
|
|
1146
|
+
return resolved;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
// fall through
|
|
1151
|
+
}
|
|
580
1152
|
}
|
|
581
|
-
|
|
1153
|
+
const prefix = `slide_${String(index).padStart(4, '0')}`;
|
|
1154
|
+
const entries = await fs.readdir(slidesDir).catch(() => null);
|
|
1155
|
+
if (!entries)
|
|
1156
|
+
return null;
|
|
1157
|
+
const candidates = entries
|
|
1158
|
+
.filter((name) => name.startsWith(prefix) && name.endsWith('.png'))
|
|
1159
|
+
.map((name) => path.join(slidesDir, name));
|
|
1160
|
+
if (candidates.length === 0)
|
|
1161
|
+
return null;
|
|
1162
|
+
let best = null;
|
|
1163
|
+
for (const filePath of candidates) {
|
|
1164
|
+
const stat = await fs.stat(filePath).catch(() => null);
|
|
1165
|
+
if (!stat?.isFile())
|
|
1166
|
+
continue;
|
|
1167
|
+
const mtimeMs = stat.mtimeMs;
|
|
1168
|
+
if (!best || mtimeMs > best.mtimeMs)
|
|
1169
|
+
best = { filePath, mtimeMs };
|
|
1170
|
+
}
|
|
1171
|
+
return best?.filePath ?? null;
|
|
1172
|
+
};
|
|
1173
|
+
const filePath = await resolveFromDisk();
|
|
1174
|
+
if (!filePath) {
|
|
1175
|
+
// Return a tiny transparent PNG (placeholder) instead of 404 to avoid broken-image icons
|
|
1176
|
+
// while extraction is still running.
|
|
1177
|
+
const placeholder = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO3kq0cAAAAASUVORK5CYII=', 'base64');
|
|
1178
|
+
res.writeHead(200, {
|
|
1179
|
+
'content-type': 'image/png',
|
|
1180
|
+
'content-length': placeholder.length.toString(),
|
|
1181
|
+
'cache-control': 'no-store',
|
|
1182
|
+
'x-summarize-slide-ready': '0',
|
|
1183
|
+
...cors,
|
|
1184
|
+
});
|
|
1185
|
+
res.end(placeholder);
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
try {
|
|
1189
|
+
const stat = await fs.stat(filePath);
|
|
1190
|
+
res.writeHead(200, {
|
|
1191
|
+
'content-type': 'image/png',
|
|
1192
|
+
'content-length': stat.size.toString(),
|
|
1193
|
+
'cache-control': 'no-store',
|
|
1194
|
+
'x-summarize-slide-ready': '1',
|
|
1195
|
+
...cors,
|
|
1196
|
+
});
|
|
1197
|
+
const stream = createReadStream(filePath);
|
|
1198
|
+
stream.pipe(res);
|
|
1199
|
+
stream.on('error', () => res.end());
|
|
1200
|
+
}
|
|
1201
|
+
catch {
|
|
1202
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1203
|
+
}
|
|
582
1204
|
return;
|
|
583
1205
|
}
|
|
584
1206
|
const eventsMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/events$/);
|
|
@@ -618,6 +1240,54 @@ export async function runDaemonServer({ env, fetchImpl, config, port = config.po
|
|
|
618
1240
|
});
|
|
619
1241
|
return;
|
|
620
1242
|
}
|
|
1243
|
+
const slidesEventsMatch = pathname.match(/^\/v1\/summarize\/([^/]+)\/slides\/events$/);
|
|
1244
|
+
if (req.method === 'GET' && slidesEventsMatch) {
|
|
1245
|
+
const id = slidesEventsMatch[1];
|
|
1246
|
+
if (!id) {
|
|
1247
|
+
json(res, 404, { ok: false }, cors);
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
const session = sessions.get(id);
|
|
1251
|
+
if (!session || !session.slidesRequested) {
|
|
1252
|
+
json(res, 404, { ok: false, error: 'not found' }, cors);
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
res.writeHead(200, {
|
|
1256
|
+
...cors,
|
|
1257
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
1258
|
+
'cache-control': 'no-cache, no-transform',
|
|
1259
|
+
connection: 'keep-alive',
|
|
1260
|
+
});
|
|
1261
|
+
session.slidesClients.add(res);
|
|
1262
|
+
for (const entry of session.slidesBuffer) {
|
|
1263
|
+
res.write(encodeSseEvent(entry.event));
|
|
1264
|
+
}
|
|
1265
|
+
const hasSlidesEvent = session.slidesBuffer.some((entry) => entry.event.event === 'slides');
|
|
1266
|
+
if (!hasSlidesEvent && session.slides) {
|
|
1267
|
+
res.write(encodeSseEvent({
|
|
1268
|
+
event: 'slides',
|
|
1269
|
+
data: buildSlidesPayload({ slides: session.slides, port }),
|
|
1270
|
+
}));
|
|
1271
|
+
}
|
|
1272
|
+
const hasStatusEvent = session.slidesBuffer.some((entry) => entry.event.event === 'status');
|
|
1273
|
+
if (!hasStatusEvent && session.slidesLastStatus) {
|
|
1274
|
+
res.write(encodeSseEvent({ event: 'status', data: { text: session.slidesLastStatus } }));
|
|
1275
|
+
}
|
|
1276
|
+
if (session.slidesDone) {
|
|
1277
|
+
res.end();
|
|
1278
|
+
session.slidesClients.delete(res);
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
const keepalive = setInterval(() => {
|
|
1282
|
+
res.write(`: keepalive ${Date.now()}\n\n`);
|
|
1283
|
+
}, 15_000);
|
|
1284
|
+
keepalive.unref();
|
|
1285
|
+
res.on('close', () => {
|
|
1286
|
+
clearInterval(keepalive);
|
|
1287
|
+
session.slidesClients.delete(res);
|
|
1288
|
+
});
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
621
1291
|
const refreshEventsMatch = pathname.match(/^\/v1\/refresh-free\/([^/]+)\/events$/);
|
|
622
1292
|
if (req.method === 'GET' && refreshEventsMatch) {
|
|
623
1293
|
const id = refreshEventsMatch[1];
|