@signal4/s4 2.0.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/README.md +92 -0
- package/bin/s4.js +7 -0
- package/bin/signal4-mcp.js +2 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +737 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +194 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +397 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +37 -0
- package/dist/config.js.map +1 -0
- package/dist/format.d.ts +60 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +682 -0
- package/dist/format.js.map +1 -0
- package/dist/handler_types.d.ts +11 -0
- package/dist/handler_types.d.ts.map +1 -0
- package/dist/handler_types.js +3 -0
- package/dist/handler_types.js.map +1 -0
- package/dist/handlers/ads.d.ts +4 -0
- package/dist/handlers/ads.d.ts.map +1 -0
- package/dist/handlers/ads.js +42 -0
- package/dist/handlers/ads.js.map +1 -0
- package/dist/handlers/analyze.d.ts +8 -0
- package/dist/handlers/analyze.d.ts.map +1 -0
- package/dist/handlers/analyze.js +77 -0
- package/dist/handlers/analyze.js.map +1 -0
- package/dist/handlers/bookmarks.d.ts +4 -0
- package/dist/handlers/bookmarks.d.ts.map +1 -0
- package/dist/handlers/bookmarks.js +42 -0
- package/dist/handlers/bookmarks.js.map +1 -0
- package/dist/handlers/claims.d.ts +4 -0
- package/dist/handlers/claims.d.ts.map +1 -0
- package/dist/handlers/claims.js +47 -0
- package/dist/handlers/claims.js.map +1 -0
- package/dist/handlers/explore.d.ts +9 -0
- package/dist/handlers/explore.d.ts.map +1 -0
- package/dist/handlers/explore.js +120 -0
- package/dist/handlers/explore.js.map +1 -0
- package/dist/handlers/registry.d.ts +4 -0
- package/dist/handlers/registry.d.ts.map +1 -0
- package/dist/handlers/registry.js +33 -0
- package/dist/handlers/registry.js.map +1 -0
- package/dist/handlers/reports.d.ts +9 -0
- package/dist/handlers/reports.d.ts.map +1 -0
- package/dist/handlers/reports.js +160 -0
- package/dist/handlers/reports.js.map +1 -0
- package/dist/handlers/search.d.ts +5 -0
- package/dist/handlers/search.d.ts.map +1 -0
- package/dist/handlers/search.js +128 -0
- package/dist/handlers/search.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +482 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +52 -0
package/dist/format.js
ADDED
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
/** Shared formatting utilities — markdown output for MCP tools. */
|
|
2
|
+
const PUBLIC_URL = "https://api.signal4.ca";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Primitives
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
export function fmtTime(seconds) {
|
|
7
|
+
if (seconds == null)
|
|
8
|
+
return "0:00";
|
|
9
|
+
const s = Math.floor(seconds);
|
|
10
|
+
const h = Math.floor(s / 3600);
|
|
11
|
+
const m = Math.floor((s % 3600) / 60);
|
|
12
|
+
const sec = s % 60;
|
|
13
|
+
if (h)
|
|
14
|
+
return `${h}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
|
|
15
|
+
return `${m}:${String(sec).padStart(2, "0")}`;
|
|
16
|
+
}
|
|
17
|
+
export function fmtHours(seconds) {
|
|
18
|
+
if (!seconds)
|
|
19
|
+
return "?";
|
|
20
|
+
const h = seconds / 3600;
|
|
21
|
+
if (h >= 1)
|
|
22
|
+
return `${h.toLocaleString("en", { maximumFractionDigits: 1 })}h`;
|
|
23
|
+
return fmtTime(seconds);
|
|
24
|
+
}
|
|
25
|
+
export function fmtDuration(seconds) {
|
|
26
|
+
if (!seconds)
|
|
27
|
+
return "?";
|
|
28
|
+
const m = Math.floor(seconds / 60);
|
|
29
|
+
const s = Math.floor(seconds % 60);
|
|
30
|
+
if (m >= 60) {
|
|
31
|
+
const h = Math.floor(m / 60);
|
|
32
|
+
const rm = m % 60;
|
|
33
|
+
return `${h}h${String(rm).padStart(2, "0")}m`;
|
|
34
|
+
}
|
|
35
|
+
return `${m}m${String(s).padStart(2, "0")}s`;
|
|
36
|
+
}
|
|
37
|
+
export function truncate(text, maxlen = 200) {
|
|
38
|
+
if (!text)
|
|
39
|
+
return "";
|
|
40
|
+
const clean = text.replace(/\n/g, " ").trim();
|
|
41
|
+
if (clean.length <= maxlen)
|
|
42
|
+
return clean;
|
|
43
|
+
const cut = clean.slice(0, maxlen);
|
|
44
|
+
const last = cut.lastIndexOf(" ");
|
|
45
|
+
return (last > maxlen * 0.5 ? cut.slice(0, last) : cut) + "...";
|
|
46
|
+
}
|
|
47
|
+
export function fmtDate(iso) {
|
|
48
|
+
return iso ? iso.slice(0, 10) : "?";
|
|
49
|
+
}
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// Quote dedup
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
export function dedupQuotes(quotes) {
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const q of quotes) {
|
|
57
|
+
const key = [
|
|
58
|
+
q.episode_title ?? q.content_id ?? "",
|
|
59
|
+
q.timestamp_start ?? q.start_time ?? 0,
|
|
60
|
+
(q.text ?? "").slice(0, 80),
|
|
61
|
+
].join("|");
|
|
62
|
+
if (!seen.has(key)) {
|
|
63
|
+
seen.add(key);
|
|
64
|
+
out.push(q);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
// Similarity tier
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
72
|
+
function simTier(sim) {
|
|
73
|
+
if (sim >= 0.60)
|
|
74
|
+
return "strong";
|
|
75
|
+
if (sim >= 0.45)
|
|
76
|
+
return "relevant";
|
|
77
|
+
if (sim >= 0.40)
|
|
78
|
+
return "borderline";
|
|
79
|
+
if (sim > 0)
|
|
80
|
+
return "weak";
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// Clip URL
|
|
85
|
+
// ---------------------------------------------------------------------------
|
|
86
|
+
function clipUrl(contentId, start, end, existing) {
|
|
87
|
+
if (existing && !existing.includes("localhost"))
|
|
88
|
+
return existing;
|
|
89
|
+
if (contentId && start != null && end != null) {
|
|
90
|
+
return `${PUBLIC_URL}/api/v1/media/content/${contentId}?start_time=${start}&end_time=${end}&media_type=audio`;
|
|
91
|
+
}
|
|
92
|
+
return "";
|
|
93
|
+
}
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Empty state
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
export function emptyStateSuggestions(params) {
|
|
98
|
+
const suggestions = [];
|
|
99
|
+
if (params.days && params.days < 30)
|
|
100
|
+
suggestions.push("Try a wider window: days=30");
|
|
101
|
+
else if (params.days && params.days < 90)
|
|
102
|
+
suggestions.push("Try a wider window: days=90");
|
|
103
|
+
if (params.mode === "semantic" && params.topic)
|
|
104
|
+
suggestions.push("Try hybrid mode: mode='hybrid'");
|
|
105
|
+
if (params.mode === "keyword" && params.topic)
|
|
106
|
+
suggestions.push("Try semantic mode: mode='semantic'");
|
|
107
|
+
if (params.speaker_name && params.topic)
|
|
108
|
+
suggestions.push("Drop speaker_name to search all speakers");
|
|
109
|
+
const query = params.topic || params.speaker_name || "this query";
|
|
110
|
+
let msg = `No matching content for "${query}"`;
|
|
111
|
+
if (params.days)
|
|
112
|
+
msg += ` in the last ${params.days} days`;
|
|
113
|
+
msg += ".";
|
|
114
|
+
if (suggestions.length)
|
|
115
|
+
msg += "\n" + suggestions.map((s) => `- ${s}`).join("\n");
|
|
116
|
+
return msg;
|
|
117
|
+
}
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Quotes
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
export function formatQuotes(data) {
|
|
122
|
+
const quotes = dedupQuotes(data.quotes ?? []);
|
|
123
|
+
if (!quotes.length)
|
|
124
|
+
return "No quotes found matching your search criteria.";
|
|
125
|
+
const total = data.total_results ?? quotes.length;
|
|
126
|
+
const ms = data.processing_time_ms ?? 0;
|
|
127
|
+
const lines = [`**${total} results** (showing ${quotes.length}, ${ms.toFixed(0)}ms)\n`];
|
|
128
|
+
for (let i = 0; i < quotes.length; i++) {
|
|
129
|
+
const q = quotes[i];
|
|
130
|
+
const spk = q.speaker ?? {};
|
|
131
|
+
const spkName = spk.name ?? "Unknown";
|
|
132
|
+
const spkId = spk.identity_id;
|
|
133
|
+
const conf = spk.confidence ?? 0;
|
|
134
|
+
const appear = spk.appearance_type ?? "";
|
|
135
|
+
const sim = q.similarity ?? 0;
|
|
136
|
+
const ch = q.channel_name ?? "?";
|
|
137
|
+
const date = fmtDate(q.episode_date);
|
|
138
|
+
const text = (q.text ?? "").slice(0, 300);
|
|
139
|
+
const segId = q.segment_id ?? "?";
|
|
140
|
+
const contentId = q.content_id ?? "?";
|
|
141
|
+
const tStart = q.timestamp_start ?? q.start_time;
|
|
142
|
+
const tEnd = q.timestamp_end ?? q.end_time;
|
|
143
|
+
const clip = clipUrl(q.content_id, tStart ?? undefined, tEnd ?? undefined, q.clip_url);
|
|
144
|
+
const tier = simTier(sim);
|
|
145
|
+
const tierStr = tier ? ` ${tier}` : "";
|
|
146
|
+
const idStr = spkId ? ` id:${spkId}` : "";
|
|
147
|
+
const clipTag = appear === "clipped" ? " **[clipped]**" : "";
|
|
148
|
+
const timeStr = tStart != null ? ` ${fmtTime(tStart)}-${fmtTime(tEnd)}` : "";
|
|
149
|
+
lines.push(`${i + 1}. **${spkName}**${clipTag} (conf:${Math.round(conf * 100)}%${idStr}) on ${ch} ` +
|
|
150
|
+
`[${date}${timeStr}] sim:${sim.toFixed(2)}${tierStr}`);
|
|
151
|
+
lines.push(` seg:${segId} | episode:${contentId}`);
|
|
152
|
+
if (clip)
|
|
153
|
+
lines.push(` 🔊 ${clip}`);
|
|
154
|
+
lines.push(` > ${text}\n`);
|
|
155
|
+
}
|
|
156
|
+
if (total > quotes.length) {
|
|
157
|
+
lines.push(`_${total - quotes.length} more results available (use offset parameter)_`);
|
|
158
|
+
}
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
// Speaker
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
export function formatSpeaker(data) {
|
|
165
|
+
const name = data.name ?? data.primary_name ?? "Unknown";
|
|
166
|
+
const lines = [`# ${name}`];
|
|
167
|
+
if (data.occupation) {
|
|
168
|
+
let occ = `**${data.occupation}**`;
|
|
169
|
+
if (data.organization)
|
|
170
|
+
occ += ` at ${data.organization}`;
|
|
171
|
+
lines.push(occ);
|
|
172
|
+
}
|
|
173
|
+
if (data.bio)
|
|
174
|
+
lines.push(`\n${data.bio}`);
|
|
175
|
+
lines.push("");
|
|
176
|
+
const stats = [];
|
|
177
|
+
if (data.total_appearances)
|
|
178
|
+
stats.push(`${data.total_appearances} appearances`);
|
|
179
|
+
if (data.total_episodes)
|
|
180
|
+
stats.push(`${data.total_episodes} episodes`);
|
|
181
|
+
if (data.total_duration_seconds)
|
|
182
|
+
stats.push(`${fmtHours(data.total_duration_seconds)} total`);
|
|
183
|
+
if (stats.length)
|
|
184
|
+
lines.push(`**Stats:** ${stats.join(", ")}`);
|
|
185
|
+
const sid = data.speaker_id ?? data.id;
|
|
186
|
+
if (sid)
|
|
187
|
+
lines.push(`Speaker ID: \`${sid}\``);
|
|
188
|
+
const topChannels = data.top_channels ?? [];
|
|
189
|
+
if (topChannels.length) {
|
|
190
|
+
lines.push("\n**Top Channels:**");
|
|
191
|
+
for (const ch of topChannels.slice(0, 8)) {
|
|
192
|
+
lines.push(`- ${ch.name ?? "?"} (ID: ${ch.channel_id ?? "?"}, ${ch.count ?? 0} appearances)`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
const related = data.related_speakers ?? [];
|
|
196
|
+
if (related.length) {
|
|
197
|
+
lines.push("\n**Co-appearances:**");
|
|
198
|
+
for (const r of related.slice(0, 10)) {
|
|
199
|
+
const count = r.appearance_count ?? r.co_appearances ?? "?";
|
|
200
|
+
lines.push(`- ${r.name ?? "?"} (${count})`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const recent = data.recent_episodes ?? [];
|
|
204
|
+
if (recent.length) {
|
|
205
|
+
lines.push("\n**Recent Episodes:**");
|
|
206
|
+
for (const ep of recent.slice(0, 8)) {
|
|
207
|
+
const date = fmtDate(ep.publish_date);
|
|
208
|
+
lines.push(`- [${date}] ${ep.channel_name ?? "?"} — ${ep.title ?? "?"}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Channel
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
export function formatChannel(data) {
|
|
217
|
+
const name = data.display_name ?? data.name ?? "Channel";
|
|
218
|
+
const lines = [`# ${name}`];
|
|
219
|
+
const meta = [];
|
|
220
|
+
if (data.platform)
|
|
221
|
+
meta.push(`**Platform:** ${data.platform}`);
|
|
222
|
+
if (data.episode_count)
|
|
223
|
+
meta.push(`**Episodes:** ${data.episode_count}`);
|
|
224
|
+
if (data.language)
|
|
225
|
+
meta.push(`**Language:** ${data.language}`);
|
|
226
|
+
if (meta.length)
|
|
227
|
+
lines.push(meta.join(" | "));
|
|
228
|
+
lines.push(`Channel ID: \`${data.channel_id ?? data.id ?? ""}\``);
|
|
229
|
+
if (data.description)
|
|
230
|
+
lines.push(`\n${data.description.slice(0, 500)}`);
|
|
231
|
+
if (data.primary_url)
|
|
232
|
+
lines.push(`\nURL: ${data.primary_url}`);
|
|
233
|
+
const ranking = data.ranking;
|
|
234
|
+
if (ranking) {
|
|
235
|
+
const parts = [];
|
|
236
|
+
if (ranking.tier)
|
|
237
|
+
parts.push(`tier ${ranking.tier}`);
|
|
238
|
+
if (ranking.importance_rank)
|
|
239
|
+
parts.push(`rank #${ranking.importance_rank}`);
|
|
240
|
+
if (parts.length)
|
|
241
|
+
lines.push(`\n**Ranking:** ${parts.join(", ")}`);
|
|
242
|
+
for (const cp of (ranking.chart_positions ?? []).slice(0, 5)) {
|
|
243
|
+
lines.push(`- Chart: ${cp.platform ?? "?"} ${cp.country ?? "?"}/${cp.category ?? "?"} #${cp.rank ?? "?"}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
const speakers = data.regular_speakers ?? data.speakers ?? [];
|
|
247
|
+
if (speakers.length) {
|
|
248
|
+
lines.push("\n**Speakers:**");
|
|
249
|
+
for (const s of speakers.slice(0, 10)) {
|
|
250
|
+
const count = s.appearance_count ?? s.count ?? "?";
|
|
251
|
+
lines.push(`- ${s.name ?? "?"} (${count} appearances)`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const recent = data.recent_episodes ?? [];
|
|
255
|
+
if (recent.length) {
|
|
256
|
+
lines.push("\n**Recent Episodes:**");
|
|
257
|
+
for (const ep of recent.slice(0, 8)) {
|
|
258
|
+
lines.push(`- [${fmtDate(ep.publish_date)}] ${ep.title ?? "?"}`);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return lines.join("\n");
|
|
262
|
+
}
|
|
263
|
+
// ---------------------------------------------------------------------------
|
|
264
|
+
// Segment
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
export function formatSegment(data) {
|
|
267
|
+
const segId = data.segment_id ?? "?";
|
|
268
|
+
const spk = data.speaker_name ?? "Unknown";
|
|
269
|
+
const ch = data.channel_name ?? "?";
|
|
270
|
+
const title = data.episode_title ?? "?";
|
|
271
|
+
const contentId = data.content_id ?? "?";
|
|
272
|
+
const date = fmtDate(data.episode_date);
|
|
273
|
+
const tStart = data.start_time;
|
|
274
|
+
const tEnd = data.end_time;
|
|
275
|
+
const clip = clipUrl(data.content_id, tStart ?? undefined, tEnd ?? undefined, data.clip_url);
|
|
276
|
+
const lines = [
|
|
277
|
+
`**Segment ${segId}**`,
|
|
278
|
+
`- **Speaker:** ${spk}`,
|
|
279
|
+
`- **Channel:** ${ch}`,
|
|
280
|
+
`- **Episode:** ${title} (\`${contentId}\`)`,
|
|
281
|
+
`- **Date:** ${date}`,
|
|
282
|
+
];
|
|
283
|
+
if (tStart != null)
|
|
284
|
+
lines.push(`- **Time:** ${fmtTime(tStart)} – ${fmtTime(tEnd)}`);
|
|
285
|
+
if (clip)
|
|
286
|
+
lines.push(`- **Audio clip:** ${clip}`);
|
|
287
|
+
lines.push(`\n> ${data.text ?? ""}`);
|
|
288
|
+
return lines.join("\n");
|
|
289
|
+
}
|
|
290
|
+
// ---------------------------------------------------------------------------
|
|
291
|
+
// Analysis
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
export function formatAnalysis(data) {
|
|
294
|
+
if (data.error)
|
|
295
|
+
return `Error: ${data.error}`;
|
|
296
|
+
const lines = ["**Analysis complete**\n"];
|
|
297
|
+
const q = data.quantitative_metrics ?? {};
|
|
298
|
+
const centrality = q.discourse_centrality;
|
|
299
|
+
if (centrality?.label) {
|
|
300
|
+
lines.push(`Discourse centrality: ${centrality.label} (${centrality.level ?? "?"}/5)`);
|
|
301
|
+
}
|
|
302
|
+
const parts = [];
|
|
303
|
+
if (q.total_segments)
|
|
304
|
+
parts.push(`${q.total_segments} segments`);
|
|
305
|
+
if (q.unique_channels)
|
|
306
|
+
parts.push(`${q.unique_channels} channels`);
|
|
307
|
+
if (q.unique_videos ?? q.episode_count)
|
|
308
|
+
parts.push(`${q.unique_videos ?? q.episode_count} episodes`);
|
|
309
|
+
if (parts.length)
|
|
310
|
+
lines.push(parts.join(", ") + "\n");
|
|
311
|
+
const themes = data.themes ?? [];
|
|
312
|
+
if (themes.length) {
|
|
313
|
+
lines.push("**Themes:**");
|
|
314
|
+
for (let i = 0; i < themes.length; i++) {
|
|
315
|
+
const t = themes[i];
|
|
316
|
+
if (!t)
|
|
317
|
+
continue;
|
|
318
|
+
const name = t.name ?? t.label ?? `Theme ${i + 1}`;
|
|
319
|
+
const count = t.segment_count ?? t.count;
|
|
320
|
+
const countStr = count ? ` (${count} segments)` : "";
|
|
321
|
+
lines.push(`${i + 1}. ${name}${countStr}`);
|
|
322
|
+
const desc = t.summary ?? t.description;
|
|
323
|
+
if (desc)
|
|
324
|
+
lines.push(` ${desc.slice(0, 200)}`);
|
|
325
|
+
}
|
|
326
|
+
lines.push("");
|
|
327
|
+
}
|
|
328
|
+
if (data.summary)
|
|
329
|
+
lines.push(data.summary);
|
|
330
|
+
return lines.join("\n");
|
|
331
|
+
}
|
|
332
|
+
// ---------------------------------------------------------------------------
|
|
333
|
+
// Comparison
|
|
334
|
+
// ---------------------------------------------------------------------------
|
|
335
|
+
export function formatComparison(data, topic) {
|
|
336
|
+
if (data.error)
|
|
337
|
+
return `Error: ${data.error}`;
|
|
338
|
+
const lines = [`**Speaker comparison: ${topic}**\n`];
|
|
339
|
+
const speakers = data.speakers ?? {};
|
|
340
|
+
for (const [name, sp] of Object.entries(speakers)) {
|
|
341
|
+
const count = sp.segment_count ?? 0;
|
|
342
|
+
lines.push(`### ${name} (${count} segments)`);
|
|
343
|
+
if (sp.position_summary)
|
|
344
|
+
lines.push(`**Position:** ${sp.position_summary}`);
|
|
345
|
+
if (sp.framing)
|
|
346
|
+
lines.push(`**Framing:** ${sp.framing}`);
|
|
347
|
+
if (sp.tone)
|
|
348
|
+
lines.push(`**Tone:** ${sp.tone}`);
|
|
349
|
+
const claims = sp.key_claims ?? [];
|
|
350
|
+
if (claims.length) {
|
|
351
|
+
lines.push("**Key claims:**");
|
|
352
|
+
for (const c of claims.slice(0, 5)) {
|
|
353
|
+
const text = typeof c === "string" ? c : c.claim ?? String(c);
|
|
354
|
+
lines.push(`- ${text}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
lines.push("");
|
|
358
|
+
}
|
|
359
|
+
if (data.comparison || data.summary) {
|
|
360
|
+
lines.push("---");
|
|
361
|
+
lines.push(data.comparison ?? data.summary ?? "");
|
|
362
|
+
}
|
|
363
|
+
return lines.join("\n");
|
|
364
|
+
}
|
|
365
|
+
// ---------------------------------------------------------------------------
|
|
366
|
+
// Dossier
|
|
367
|
+
// ---------------------------------------------------------------------------
|
|
368
|
+
export function formatDossier(data, speakerName, topic) {
|
|
369
|
+
if (data.error)
|
|
370
|
+
return `Error: ${data.error}`;
|
|
371
|
+
const lines = [`**Dossier: ${speakerName} on ${topic}**\n`];
|
|
372
|
+
const sp = data.speaker ?? {};
|
|
373
|
+
if (sp.occupation)
|
|
374
|
+
lines.push(`*${sp.occupation}*`);
|
|
375
|
+
if (sp.segment_count)
|
|
376
|
+
lines.push(`${sp.segment_count} segments analyzed\n`);
|
|
377
|
+
const contras = data.contradictions;
|
|
378
|
+
if (contras?.verdict) {
|
|
379
|
+
lines.push(`**Contradiction check:** ${contras.verdict.replace(/_/g, " ")}`);
|
|
380
|
+
for (const c of contras.contradictions ?? []) {
|
|
381
|
+
lines.push(`- [${c.severity ?? "?"}] ${c.description ?? "?"}`);
|
|
382
|
+
}
|
|
383
|
+
lines.push("");
|
|
384
|
+
}
|
|
385
|
+
if (data.summary)
|
|
386
|
+
lines.push(data.summary);
|
|
387
|
+
return lines.join("\n");
|
|
388
|
+
}
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Contradictions
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
export function formatContradictions(data, speakerName, topic) {
|
|
393
|
+
if (data.error)
|
|
394
|
+
return `Error: ${data.error}`;
|
|
395
|
+
const lines = [`**Contradiction check: ${speakerName} on ${topic}**\n`];
|
|
396
|
+
const contras = data.contradictions ?? {};
|
|
397
|
+
const verdict = contras.verdict ?? "unknown";
|
|
398
|
+
lines.push(`**Verdict:** ${verdict.replace(/_/g, " ")}\n`);
|
|
399
|
+
for (const c of contras.contradictions ?? []) {
|
|
400
|
+
lines.push(`### [${c.severity ?? "?"}] ${c.description ?? "?"}`);
|
|
401
|
+
if (c.earlier?.claim)
|
|
402
|
+
lines.push(`**Earlier** (${c.earlier.date ?? "?"}): ${c.earlier.claim}`);
|
|
403
|
+
if (c.later?.claim)
|
|
404
|
+
lines.push(`**Later** (${c.later.date ?? "?"}): ${c.later.claim}`);
|
|
405
|
+
if (c.possible_context)
|
|
406
|
+
lines.push(`*Context:* ${c.possible_context}`);
|
|
407
|
+
lines.push("");
|
|
408
|
+
}
|
|
409
|
+
const consistent = contras.consistent_positions ?? [];
|
|
410
|
+
if (consistent.length) {
|
|
411
|
+
lines.push("**Consistent positions:**");
|
|
412
|
+
for (const pos of consistent)
|
|
413
|
+
lines.push(`- ${pos}`);
|
|
414
|
+
lines.push("");
|
|
415
|
+
}
|
|
416
|
+
const timeline = data.stance_timeline ?? [];
|
|
417
|
+
if (timeline.length) {
|
|
418
|
+
lines.push("**Stance timeline:**");
|
|
419
|
+
for (const t of timeline.slice(0, 20)) {
|
|
420
|
+
lines.push(`- ${t.segment_id ?? "?"}: ${t.stance ?? "?"} — ${(t.claim ?? "").slice(0, 100)}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (data.summary && !(contras.contradictions ?? []).length)
|
|
424
|
+
lines.push(data.summary);
|
|
425
|
+
return lines.join("\n");
|
|
426
|
+
}
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
// Briefing / Discourse Summary
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
export function formatBriefing(data) {
|
|
431
|
+
const lines = [];
|
|
432
|
+
const stats = data.statistics ?? {};
|
|
433
|
+
if (stats.total_episodes || stats.total_channels) {
|
|
434
|
+
lines.push(`**Briefing:** ${stats.total_episodes ?? "?"} episodes, ` +
|
|
435
|
+
`${stats.total_channels ?? "?"} channels, ` +
|
|
436
|
+
`${(stats.total_duration_hours ?? 0).toFixed(0)}h\n`);
|
|
437
|
+
}
|
|
438
|
+
for (let i = 0; i < (data.topics ?? []).length; i++) {
|
|
439
|
+
const t = data.topics[i];
|
|
440
|
+
const title = t.title ?? t.label ?? "?";
|
|
441
|
+
const segs = t.segment_count ?? 0;
|
|
442
|
+
const chs = t.channel_count ?? 0;
|
|
443
|
+
const score = t.score ?? 0;
|
|
444
|
+
lines.push(`${i + 1}. **${title}** (${segs} segments, ${chs} channels, score:${score.toFixed(2)})`);
|
|
445
|
+
const summary = t.summary ?? t.description;
|
|
446
|
+
if (summary)
|
|
447
|
+
lines.push(` ${truncate(summary, 250)}`);
|
|
448
|
+
for (const c of (t.citations ?? []).slice(0, 2)) {
|
|
449
|
+
const label = [c.speaker_name, c.channel_name].filter(Boolean).join(" on ");
|
|
450
|
+
if (c.text)
|
|
451
|
+
lines.push(` "${truncate(c.text, 120)}" — ${label}`);
|
|
452
|
+
}
|
|
453
|
+
lines.push("");
|
|
454
|
+
}
|
|
455
|
+
const events = data.events ?? [];
|
|
456
|
+
if (events.length) {
|
|
457
|
+
lines.push("**Key events:**");
|
|
458
|
+
for (const ev of events.slice(0, 5)) {
|
|
459
|
+
lines.push(`- ${ev.title ?? ev.description ?? "?"}`);
|
|
460
|
+
}
|
|
461
|
+
lines.push("");
|
|
462
|
+
}
|
|
463
|
+
const speakers = data.key_speakers ?? [];
|
|
464
|
+
if (speakers.length) {
|
|
465
|
+
const parts = speakers.slice(0, 8).map((s) => `${s.name ?? "?"} (${s.segment_count ?? "?"} seg)`);
|
|
466
|
+
lines.push(`**Key speakers:** ${parts.join(", ")}\n`);
|
|
467
|
+
}
|
|
468
|
+
if (data.synthesis)
|
|
469
|
+
lines.push(data.synthesis);
|
|
470
|
+
return lines.join("\n") || "No briefing data available.";
|
|
471
|
+
}
|
|
472
|
+
export function formatDiscourseSummary(data) {
|
|
473
|
+
const lines = [];
|
|
474
|
+
const synthesis = data.synthesis ??
|
|
475
|
+
data.summary ??
|
|
476
|
+
data.text ??
|
|
477
|
+
data.content;
|
|
478
|
+
if (synthesis)
|
|
479
|
+
lines.push(synthesis);
|
|
480
|
+
const topics = data.topics ?? [];
|
|
481
|
+
if (topics.length) {
|
|
482
|
+
lines.push("\n**Topics:**");
|
|
483
|
+
for (let i = 0; i < topics.length; i++) {
|
|
484
|
+
const t = topics[i];
|
|
485
|
+
const name = t.title ?? t.label ?? `Topic ${i + 1}`;
|
|
486
|
+
const count = t.segment_count;
|
|
487
|
+
const countStr = count ? ` (${count} segments)` : "";
|
|
488
|
+
lines.push(`${i + 1}. ${name}${countStr}`);
|
|
489
|
+
const desc = t.summary ?? t.description;
|
|
490
|
+
if (desc)
|
|
491
|
+
lines.push(` ${desc.slice(0, 200)}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const speakers = data.key_speakers ?? [];
|
|
495
|
+
if (speakers.length) {
|
|
496
|
+
lines.push("\n**Key Speakers:**");
|
|
497
|
+
for (const s of speakers.slice(0, 10)) {
|
|
498
|
+
lines.push(`- ${s.name ?? "?"}`);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
return lines.join("\n") || "No discourse data available.";
|
|
502
|
+
}
|
|
503
|
+
// ---------------------------------------------------------------------------
|
|
504
|
+
// Topics
|
|
505
|
+
// ---------------------------------------------------------------------------
|
|
506
|
+
export function formatTopics(topics, totalClusters, partition) {
|
|
507
|
+
if (!topics.length)
|
|
508
|
+
return "No topic clusters found.";
|
|
509
|
+
const lines = [
|
|
510
|
+
`**${topics.length} of ${totalClusters ?? topics.length} topic clusters** (${partition ?? "?"})\n`,
|
|
511
|
+
];
|
|
512
|
+
for (let i = 0; i < topics.length; i++) {
|
|
513
|
+
const t = topics[i];
|
|
514
|
+
const catTag = t.category ? ` [${t.category}]` : "";
|
|
515
|
+
lines.push(`${i + 1}. **${t.title ?? "?"}**${catTag}`);
|
|
516
|
+
lines.push(` ${t.segment_count ?? 0} segments across ${t.channel_count ?? 0} channels`);
|
|
517
|
+
const speakers = t.top_speakers ?? [];
|
|
518
|
+
if (speakers.length) {
|
|
519
|
+
lines.push(` Voices: ${speakers.slice(0, 3).map((s) => s.name ?? "?").join(", ")}`);
|
|
520
|
+
}
|
|
521
|
+
const topSeg = (t.top_segments ?? [])[0];
|
|
522
|
+
if (topSeg?.text) {
|
|
523
|
+
lines.push(` "${truncate(topSeg.text, 120)}" — ${topSeg.channel_name ?? ""}`);
|
|
524
|
+
}
|
|
525
|
+
lines.push("");
|
|
526
|
+
}
|
|
527
|
+
return lines.join("\n");
|
|
528
|
+
}
|
|
529
|
+
// ---------------------------------------------------------------------------
|
|
530
|
+
// Ad formatters
|
|
531
|
+
// ---------------------------------------------------------------------------
|
|
532
|
+
export function formatAdStats(data) {
|
|
533
|
+
const lines = ["# Ad Analytics Overview\n"];
|
|
534
|
+
lines.push(`**${(data.total_ad_hashes ?? 0).toLocaleString()}** confirmed ad sentences`);
|
|
535
|
+
lines.push(`**${data.total_brands ?? 0}** advertisers (+ ${(data.total_advertisers ?? 0) - (data.total_brands ?? 0)} network promos)`);
|
|
536
|
+
lines.push(`**${(data.ad_reads_in_window ?? 0).toLocaleString()}** ad reads in last ${data.time_window_days ?? 30}d`);
|
|
537
|
+
lines.push(`**${(data.episodes_with_ads_in_window ?? 0).toLocaleString()}** episodes with ads`);
|
|
538
|
+
lines.push(`**${data.avg_ad_read_duration_seconds ?? 0}s** avg ad read duration`);
|
|
539
|
+
const byCat = data.ads_by_category ?? data.by_category ?? {};
|
|
540
|
+
if (Object.keys(byCat).length) {
|
|
541
|
+
lines.push("\n**By Category:**");
|
|
542
|
+
const sorted = Object.entries(byCat).sort((a, b) => b[1] - a[1]);
|
|
543
|
+
for (const [cat, count] of sorted) {
|
|
544
|
+
lines.push(`- ${cat}: ${count.toLocaleString()}`);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const top = data.top_advertisers ?? [];
|
|
548
|
+
if (top.length) {
|
|
549
|
+
lines.push("\n**Top Advertisers:**");
|
|
550
|
+
for (let i = 0; i < top.length; i++) {
|
|
551
|
+
const a = top[i];
|
|
552
|
+
lines.push(`${i + 1}. **${a.name}** (${a.category ?? "?"}) — ` +
|
|
553
|
+
`${a.total_episode_count.toLocaleString()} episodes, ${a.total_channel_count} channels`);
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return lines.join("\n");
|
|
557
|
+
}
|
|
558
|
+
export function formatAdvertiserList(data) {
|
|
559
|
+
const advs = data.advertisers ?? [];
|
|
560
|
+
if (!advs.length)
|
|
561
|
+
return "No advertisers found.";
|
|
562
|
+
const total = data.total ?? advs.length;
|
|
563
|
+
const lines = [`**${total} advertisers** (showing ${advs.length})\n`];
|
|
564
|
+
for (let i = 0; i < advs.length; i++) {
|
|
565
|
+
const a = advs[i];
|
|
566
|
+
const aliases = a.aliases?.length ? ` (aka ${a.aliases.slice(0, 3).join(", ")})` : "";
|
|
567
|
+
lines.push(`${i + 1}. **${a.name}**${aliases} [${a.category ?? "?"}] — ` +
|
|
568
|
+
`${a.total_episode_count.toLocaleString()} episodes, ${a.total_channel_count} channels`);
|
|
569
|
+
}
|
|
570
|
+
return lines.join("\n");
|
|
571
|
+
}
|
|
572
|
+
export function formatAdvertiserDetail(data) {
|
|
573
|
+
if (data.error)
|
|
574
|
+
return `Error: ${data.error}`;
|
|
575
|
+
const lines = [`# ${data.name}`];
|
|
576
|
+
if (data.category)
|
|
577
|
+
lines.push(`**Category:** ${data.category}`);
|
|
578
|
+
if (data.aliases?.length)
|
|
579
|
+
lines.push(`**Also known as:** ${data.aliases.join(", ")}`);
|
|
580
|
+
if (data.website)
|
|
581
|
+
lines.push(`**Website:** ${data.website}`);
|
|
582
|
+
lines.push(`**Episodes:** ${data.total_episode_count.toLocaleString()} | **Channels:** ${data.total_channel_count}`);
|
|
583
|
+
if (data.first_seen)
|
|
584
|
+
lines.push(`**Active:** ${fmtDate(data.first_seen)} — ${fmtDate(data.last_seen)}`);
|
|
585
|
+
const channels = data.top_channels ?? [];
|
|
586
|
+
if (channels.length) {
|
|
587
|
+
lines.push("\n**Top Channels:**");
|
|
588
|
+
for (const ch of channels.slice(0, 10)) {
|
|
589
|
+
lines.push(`- ${ch.name} (${ch.platform ?? "?"}) — ${ch.episode_count} episodes`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
const samples = data.sample_reads ?? [];
|
|
593
|
+
if (samples.length) {
|
|
594
|
+
lines.push("\n**Recent Ad Reads:**");
|
|
595
|
+
for (const s of samples) {
|
|
596
|
+
const dur = s.duration_seconds ? ` (${s.duration_seconds}s)` : "";
|
|
597
|
+
lines.push(`- [${fmtDate(s.date)}] ${s.episode_title ?? "?"}${dur}`);
|
|
598
|
+
if (s.text)
|
|
599
|
+
lines.push(` > ${s.text.slice(0, 200)}`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return lines.join("\n");
|
|
603
|
+
}
|
|
604
|
+
export function formatChannelAds(data) {
|
|
605
|
+
if (data.error)
|
|
606
|
+
return `Error: ${data.error}`;
|
|
607
|
+
const chName = data.channel?.name ?? data.channel_name ?? "Channel";
|
|
608
|
+
const lines = [`# Ads on ${chName}`];
|
|
609
|
+
lines.push(`**${(data.total_ad_reads ?? 0).toLocaleString()}** ad reads in last ${data.time_window_days ?? 30}d\n`);
|
|
610
|
+
for (let i = 0; i < data.advertisers.length; i++) {
|
|
611
|
+
const a = data.advertisers[i];
|
|
612
|
+
const promo = a.is_network_promo ? " [network]" : "";
|
|
613
|
+
lines.push(`${i + 1}. **${a.name}**${promo} [${a.category ?? "?"}] — ` +
|
|
614
|
+
`${a.episode_count} episodes, ${a.read_count} reads`);
|
|
615
|
+
}
|
|
616
|
+
return lines.join("\n") || "No ad data for this channel.";
|
|
617
|
+
}
|
|
618
|
+
export function formatAdTrends(data) {
|
|
619
|
+
if (!data.advertisers?.length)
|
|
620
|
+
return "No trending data available.";
|
|
621
|
+
const lines = [`# Ad Trends (${data.comparison ?? ""})\n`];
|
|
622
|
+
for (const a of data.advertisers) {
|
|
623
|
+
const icon = { growing: "+", declining: "-", new: "NEW", gone: "GONE", stable: "=" }[a.trend] ?? "?";
|
|
624
|
+
lines.push(`[${icon}] **${a.name}** (${a.category ?? "?"}) — ` +
|
|
625
|
+
`${a.current_episodes} eps (was ${a.prior_episodes}, ${a.change_pct > 0 ? "+" : ""}${a.change_pct.toFixed(0)}%)`);
|
|
626
|
+
}
|
|
627
|
+
return lines.join("\n");
|
|
628
|
+
}
|
|
629
|
+
// ---------------------------------------------------------------------------
|
|
630
|
+
// Claims
|
|
631
|
+
// ---------------------------------------------------------------------------
|
|
632
|
+
export function formatClaims(data) {
|
|
633
|
+
const claims = data.claims ?? [];
|
|
634
|
+
if (!claims.length)
|
|
635
|
+
return "No claims found matching your search criteria.";
|
|
636
|
+
const total = data.total_results ?? claims.length;
|
|
637
|
+
const ms = data.processing_time_ms ?? 0;
|
|
638
|
+
const mode = data.search_mode ?? "";
|
|
639
|
+
const lines = [`**${total} claims** (showing ${claims.length}, ${ms.toFixed(0)}ms, mode: ${mode})\n`];
|
|
640
|
+
for (let i = 0; i < claims.length; i++) {
|
|
641
|
+
const c = claims[i];
|
|
642
|
+
const obs = c.observation_count ?? 0;
|
|
643
|
+
const eps = c.episode_count ?? 0;
|
|
644
|
+
const chs = c.channel_count ?? 0;
|
|
645
|
+
const sim = c.similarity;
|
|
646
|
+
const topic = c.topic ?? "";
|
|
647
|
+
const stance = c.stance ?? "";
|
|
648
|
+
const lastSeen = (c.last_seen ?? "").slice(0, 10);
|
|
649
|
+
const simStr = sim != null ? ` sim:${sim.toFixed(2)}` : "";
|
|
650
|
+
const tag = topic ? ` [${topic}]` : "";
|
|
651
|
+
const stanceStr = stance ? ` (${stance})` : "";
|
|
652
|
+
lines.push(`${i + 1}. ${c.summary}${tag}${stanceStr}${simStr}`);
|
|
653
|
+
lines.push(` ${obs} observations, ${eps} episodes, ${chs} channels, last: ${lastSeen}`);
|
|
654
|
+
const entities = c.entities ?? [];
|
|
655
|
+
if (entities.length)
|
|
656
|
+
lines.push(` Entities: ${entities.slice(0, 6).join(", ")}`);
|
|
657
|
+
const speakers = c.top_speakers ?? [];
|
|
658
|
+
if (speakers.length) {
|
|
659
|
+
const parts = speakers.slice(0, 3).map((s) => `${s.name} (${s.observation_count})`);
|
|
660
|
+
lines.push(` Speakers: ${parts.join(", ")}`);
|
|
661
|
+
}
|
|
662
|
+
const channels = c.top_channels ?? [];
|
|
663
|
+
if (channels.length) {
|
|
664
|
+
const parts = channels.slice(0, 3).map((ch) => `${ch.name} (${ch.observation_count})`);
|
|
665
|
+
lines.push(` Channels: ${parts.join(", ")}`);
|
|
666
|
+
}
|
|
667
|
+
const samples = c.sample_observations ?? [];
|
|
668
|
+
for (const s of samples.slice(0, 2)) {
|
|
669
|
+
const date = (s.publish_date ?? "").slice(0, 10);
|
|
670
|
+
const ep = truncate(s.episode_title, 60);
|
|
671
|
+
const ch = s.channel_name ?? "?";
|
|
672
|
+
const spk = s.speaker_name ? ` (${s.speaker_name})` : "";
|
|
673
|
+
lines.push(` - [${date}] ${ch} — ${ep}${spk}`);
|
|
674
|
+
}
|
|
675
|
+
lines.push("");
|
|
676
|
+
}
|
|
677
|
+
if (data.has_more) {
|
|
678
|
+
lines.push(`_${total - claims.length} more claims available (use offset parameter)_`);
|
|
679
|
+
}
|
|
680
|
+
return lines.join("\n");
|
|
681
|
+
}
|
|
682
|
+
//# sourceMappingURL=format.js.map
|