osborn 0.9.251 → 0.9.252
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/dist/content/compaction-lens-worker.d.ts +7 -2
- package/dist/content/compaction-lens-worker.js +60 -7
- package/dist/content/compaction-lens.js +13 -2
- package/dist/content/content-brief.d.ts +90 -0
- package/dist/content/content-brief.js +165 -0
- package/dist/content/content-checks.d.ts +88 -0
- package/dist/content/content-checks.js +249 -0
- package/dist/content/content-ingest.d.ts +51 -0
- package/dist/content/content-ingest.js +107 -0
- package/dist/content/content-llm.d.ts +134 -0
- package/dist/content/content-llm.js +412 -0
- package/dist/content/content-manifest.d.ts +115 -0
- package/dist/content/content-manifest.js +59 -0
- package/dist/content/content-query-gate.d.ts +22 -0
- package/dist/content/content-query-gate.js +78 -0
- package/dist/content/content-redact.d.ts +22 -0
- package/dist/content/content-redact.js +49 -0
- package/dist/content/content-research.d.ts +90 -0
- package/dist/content/content-research.js +256 -0
- package/dist/content/content-run.d.ts +85 -0
- package/dist/content/content-run.js +321 -0
- package/dist/content/content-script-rules.d.ts +74 -0
- package/dist/content/content-script-rules.js +134 -0
- package/dist/content/content-script.d.ts +61 -0
- package/dist/content/content-script.js +107 -0
- package/dist/content/lens-ingest.d.ts +7 -0
- package/dist/content/lens-ingest.js +21 -4
- package/dist/content/transcript-sanitizer.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* content-script.ts — Stage A script generation: one script per brief.
|
|
3
|
+
* A period yields at most 1 highlight (61-90s narrated timeline) + up to 3
|
|
4
|
+
* how-tos (~150s "if you're in this situation…"); content-run enforces the count.
|
|
5
|
+
*
|
|
6
|
+
* Writing rules and per-tier beats come ONLY from content-script-rules.ts
|
|
7
|
+
* (SCRIPT_RULES / TIER_TEMPLATES), so they can be replaced without touching
|
|
8
|
+
* this logic. Dev-voice lines must be verbatim user rows: the model is shown
|
|
9
|
+
* row-tagged rows and must cite the row; content-checks.ts verifies every dev
|
|
10
|
+
* line deterministically before anything else runs. Throws CapError / Error.
|
|
11
|
+
*/
|
|
12
|
+
import { recordView } from './lens-db.js';
|
|
13
|
+
import { llmJson } from './content-llm.js';
|
|
14
|
+
import { FORMAT_CATALOG, SCRIPT_RULES, TIER_TEMPLATES, WORDS_PER_SECOND } from './content-script-rules.js';
|
|
15
|
+
const ROWS_MAX_CHARS = 40_000;
|
|
16
|
+
const PAGE_MAX_CHARS = 40_000;
|
|
17
|
+
const MAX_LINES = 60;
|
|
18
|
+
export const countWords = (s) => (s.match(/[A-Za-z0-9][A-Za-z0-9'’.-]*/g) ?? []).length;
|
|
19
|
+
export const estimateSeconds = (lines) => Number((lines.reduce((n, l) => n + countWords(l.text), 0) / WORDS_PER_SECOND).toFixed(1));
|
|
20
|
+
export function tierBounds(tier) {
|
|
21
|
+
const t = TIER_TEMPLATES[tier];
|
|
22
|
+
return { min: t.minS, max: t.maxS, target: t.targetS };
|
|
23
|
+
}
|
|
24
|
+
export function lengthCheck(s) {
|
|
25
|
+
const b = tierBounds(s.tier);
|
|
26
|
+
const seconds = estimateSeconds(s.lines);
|
|
27
|
+
return { ok: seconds >= b.min && seconds <= b.max, seconds, min: b.min, max: b.max };
|
|
28
|
+
}
|
|
29
|
+
/** Row-tagged session rows for the brief's range (widened to the whole period when thin), capped. */
|
|
30
|
+
export function rowsForBrief(records, brief) {
|
|
31
|
+
let rows = records.filter(r => r.id >= brief.fromRow && r.id <= brief.toRow);
|
|
32
|
+
if (rows.filter(r => r.speaker === 'user').length < 4)
|
|
33
|
+
rows = records;
|
|
34
|
+
const out = [];
|
|
35
|
+
let used = 0;
|
|
36
|
+
// Keep the range's start in view: walk forward, stop at the cap.
|
|
37
|
+
for (const r of rows) {
|
|
38
|
+
const c = recordView(r).length + 2;
|
|
39
|
+
if (used + c > ROWS_MAX_CHARS && out.length)
|
|
40
|
+
break;
|
|
41
|
+
used += c;
|
|
42
|
+
out.push(r);
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
export const SCRIPT_SYSTEM = 'You write the spoken script for a developer\'s own short vertical video, made from their real work session with an AI coding agent. ' +
|
|
47
|
+
'You never invent facts. Developer lines are copied verbatim from the user rows given. Reply with ONE JSON object only.';
|
|
48
|
+
export function scriptPrompt(brief, page, rows, researchText) {
|
|
49
|
+
const t = TIER_TEMPLATES[brief.tier];
|
|
50
|
+
const fmt = FORMAT_CATALOG.find(f => f.id === brief.format);
|
|
51
|
+
return [
|
|
52
|
+
'<period_page>',
|
|
53
|
+
page.length > PAGE_MAX_CHARS ? page.slice(0, PAGE_MAX_CHARS) : page,
|
|
54
|
+
'</period_page>',
|
|
55
|
+
'<session_rows> (header = [#row | timestamp | speaker]; dev lines may ONLY copy from rows whose speaker is "user")',
|
|
56
|
+
rows.map(recordView).join('\n\n'),
|
|
57
|
+
'</session_rows>',
|
|
58
|
+
'<research> (what people actually hit; cite nothing, just speak to it)',
|
|
59
|
+
researchText || '(none)',
|
|
60
|
+
'</research>',
|
|
61
|
+
`BRIEF:\n- Viewer: ${brief.viewer}\n- They are living through: ${brief.situation}\n- Angle: ${brief.angle}\n- Problems to speak to: ${brief.problems.join(' | ') || '(none)'}\n` +
|
|
62
|
+
(brief.stake ? `- STAKE (show it): ${brief.stake.quantity}: BEFORE ${brief.stake.before} -> AFTER ${brief.stake.after}\n` : '') +
|
|
63
|
+
`- Built on: "${brief.story}" (rows ${brief.fromRow}-${brief.toRow})\n- Format: ${fmt ? `${fmt.name} — beats: ${fmt.beats}` : brief.format}` +
|
|
64
|
+
(brief.owner ? `\n- Developer (owns the video): ${brief.owner.name}${brief.owner.handle ? ` (${brief.owner.handle})` : ''}` : '\n- Developer name unknown: no name in the sign-off'),
|
|
65
|
+
`TIER: ${t.label}. ${t.promise}\nBeats:\n${t.beats.map((b, i) => `${i + 1}. ${b}`).join('\n')}\n` +
|
|
66
|
+
`BUDGET: ${t.budget}\nLENGTH: ${t.words.min}-${t.words.max} spoken words in total (hard limit ${t.minS}-${t.maxS}s at ${WORDS_PER_SECOND} words/s). Dev lines: ${t.devLines.min}-${t.devLines.max}.`,
|
|
67
|
+
`RULES:\n${SCRIPT_RULES.map(r => `- ${r}`).join('\n')}`,
|
|
68
|
+
'Output ONLY: {"title": "<= 80 chars", "hook": "one line", "lines": [{"speaker": "narrator"|"dev"|"agent", "text": "...", "row": <row number, dev lines only>}], ' +
|
|
69
|
+
'"causes": [each distinct cause (fault or catch needing its own fix) the script tells, one short phrase each], ' +
|
|
70
|
+
'"terms": [each load-bearing term the viewer must understand, as written]}',
|
|
71
|
+
].join('\n\n');
|
|
72
|
+
}
|
|
73
|
+
const SPEAKER = {
|
|
74
|
+
dev: 'dev', developer: 'dev', user: 'dev', agent: 'agent', assistant: 'agent', ai: 'agent', narrator: 'narrator', narr: 'narrator',
|
|
75
|
+
};
|
|
76
|
+
/** Tolerant parse of the model's reply into a Script. Never throws. */
|
|
77
|
+
export function normalizeScript(raw, brief) {
|
|
78
|
+
const j = raw && typeof raw === 'object' ? raw : {};
|
|
79
|
+
const src = Array.isArray(j.lines) ? j : j.script && typeof j.script === 'object' ? j.script : j;
|
|
80
|
+
const lines = [];
|
|
81
|
+
for (const l of Array.isArray(src.lines) ? src.lines.slice(0, MAX_LINES) : []) {
|
|
82
|
+
const who = String(l?.speaker ?? l?.voice ?? l?.who ?? 'narrator').toLowerCase().trim();
|
|
83
|
+
const text = String(l?.text ?? l?.say ?? l?.line ?? '').replace(/\s+/g, ' ').trim();
|
|
84
|
+
if (!text)
|
|
85
|
+
continue;
|
|
86
|
+
const speaker = SPEAKER[who] ?? 'narrator';
|
|
87
|
+
const rowN = Math.floor(Number(String(l?.row ?? l?.source_row ?? '').replace(/^#/, '')));
|
|
88
|
+
lines.push({ speaker, text, ...(Number.isFinite(rowN) && rowN > 0 ? { row: rowN } : {}) });
|
|
89
|
+
}
|
|
90
|
+
const list = (v) => (Array.isArray(v) ? v : []).map(x => String(typeof x === 'object' && x ? (x.text ?? x.term ?? x.cause ?? '') : x).replace(/\s+/g, ' ').trim()).filter(Boolean).slice(0, 30);
|
|
91
|
+
const clip = (s, n) => String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, n);
|
|
92
|
+
return {
|
|
93
|
+
id: brief.id, tier: brief.tier, title: clip(src.title, 120), hook: clip(src.hook, 280), lines,
|
|
94
|
+
causes: list(src.causes ?? j.causes), terms: list(src.terms ?? j.terms),
|
|
95
|
+
words: lines.reduce((n, l) => n + countWords(l.text), 0), estSeconds: estimateSeconds(lines),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/** Generate one script. One model call. */
|
|
99
|
+
export async function writeScript(brief, page, records, researchText, llm) {
|
|
100
|
+
const rows = rowsForBrief(records, brief);
|
|
101
|
+
const maxOut = brief.tier === 'howto' ? 3000 : 1800;
|
|
102
|
+
const r = await llmJson(llm, { what: `script ${brief.id}`, system: SCRIPT_SYSTEM, user: scriptPrompt(brief, page, rows, researchText), maxOut, temperature: 0.4 });
|
|
103
|
+
return { script: normalizeScript(r.json, brief), costUsd: r.costUsd };
|
|
104
|
+
}
|
|
105
|
+
const LABEL = { narrator: 'NARRATOR', dev: 'DEVELOPER (verbatim)', agent: 'AGENT' };
|
|
106
|
+
/** Plain transcript (one line per spoken line) — the ingest `transcript` field. */
|
|
107
|
+
export const scriptTranscript = (s) => s.lines.map(l => `${LABEL[l.speaker]}: ${l.text}`).join('\n');
|
|
@@ -63,5 +63,12 @@ export declare function contentHashFor(p: Pick<IngestPage, 'sessionId' | 'startR
|
|
|
63
63
|
export declare function buildIngestPayload(p: IngestPage, scrub?: (s: string) => string): Record<string, unknown>;
|
|
64
64
|
/** POST one page to content_ingest. Never throws; returns the state to record in the manifest. */
|
|
65
65
|
export declare function ingestPage(p: IngestPage, o?: IngestOptions): Promise<IngestState>;
|
|
66
|
+
/**
|
|
67
|
+
* The content_ingest RPC call itself, shared by library pages and Stage A script
|
|
68
|
+
* drafts (content-ingest.ts). `payload` must already be redacted and must carry
|
|
69
|
+
* `content_hash`; null = only resolve config (returns skipped when not configured).
|
|
70
|
+
* Never throws. Logs outcomes only (never the token, headers or body).
|
|
71
|
+
*/
|
|
72
|
+
export declare function postContentIngest(payload: Record<string, unknown> | null, contentHash: string, tag: string, o?: IngestOptions): Promise<IngestState>;
|
|
66
73
|
/** Does this manifest entry still need an ingest attempt? */
|
|
67
74
|
export declare function needsIngest(state: IngestState | undefined, currentHash: string): boolean;
|
|
@@ -85,16 +85,33 @@ export function buildIngestPayload(p, scrub = s => s) {
|
|
|
85
85
|
}
|
|
86
86
|
/** POST one page to content_ingest. Never throws; returns the state to record in the manifest. */
|
|
87
87
|
export async function ingestPage(p, o = {}) {
|
|
88
|
-
const log = o.log ?? (() => { });
|
|
89
|
-
const at = new Date().toISOString();
|
|
90
88
|
let contentHash = '';
|
|
91
|
-
const tag = `library: ingest period ${p.index} (#${p.startRowId})`;
|
|
92
89
|
try {
|
|
93
90
|
contentHash = contentHashFor(p);
|
|
91
|
+
if (!resolveIngestConfig().ok)
|
|
92
|
+
return postContentIngest(null, contentHash, '', o);
|
|
93
|
+
return postContentIngest(buildIngestPayload(p, o.scrub), contentHash, `library: ingest period ${p.index} (#${p.startRowId})`, o);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
(o.log ?? (() => { }))(`library: ingest period ${p.index} (#${p.startRowId}) → failed: network (will retry next run)`);
|
|
97
|
+
return { status: 'error', reason: 'network', contentHash, at: new Date().toISOString() };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The content_ingest RPC call itself, shared by library pages and Stage A script
|
|
102
|
+
* drafts (content-ingest.ts). `payload` must already be redacted and must carry
|
|
103
|
+
* `content_hash`; null = only resolve config (returns skipped when not configured).
|
|
104
|
+
* Never throws. Logs outcomes only (never the token, headers or body).
|
|
105
|
+
*/
|
|
106
|
+
export async function postContentIngest(payload, contentHash, tag, o = {}) {
|
|
107
|
+
const log = o.log ?? (() => { });
|
|
108
|
+
const at = new Date().toISOString();
|
|
109
|
+
try {
|
|
94
110
|
const cfg = resolveIngestConfig();
|
|
95
111
|
if (!cfg.ok)
|
|
96
112
|
return { status: 'skipped', reason: cfg.reason, contentHash, at };
|
|
97
|
-
|
|
113
|
+
if (!payload)
|
|
114
|
+
return { status: 'skipped', reason: 'no-payload', contentHash, at };
|
|
98
115
|
const body = JSON.stringify({ p_token: cfg.token, p_payload: payload });
|
|
99
116
|
const bytes = Buffer.byteLength(body, 'utf-8');
|
|
100
117
|
if (bytes > INGEST_MAX_BYTES) {
|
|
@@ -107,6 +107,12 @@ export const SECRET_RULES = [
|
|
|
107
107
|
re: /(?<![A-Za-z0-9_])snx_proj_[A-Za-z0-9_-]{20,}/g,
|
|
108
108
|
replace: '[REDACTED-SONIOX-KEY]',
|
|
109
109
|
},
|
|
110
|
+
// Bare Soniox key (snx_<body>, no proj_ segment); the 16-char floor skips prefix mentions.
|
|
111
|
+
{
|
|
112
|
+
kind: 'SONIOX-KEY',
|
|
113
|
+
re: /(?<![A-Za-z0-9_])snx_(?!proj_)[A-Za-z0-9]{16,}/g,
|
|
114
|
+
replace: '[REDACTED-SONIOX-KEY]',
|
|
115
|
+
},
|
|
110
116
|
// JWTs (header.payload.signature, base64url).
|
|
111
117
|
{
|
|
112
118
|
kind: 'JWT',
|