@wrenautomation/read 0.0.0-stage → 0.30.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.
Files changed (72) hide show
  1. package/LICENSE.md +61 -0
  2. package/README.md +36 -2
  3. package/dist/fetch/fetcher.d.ts +84 -0
  4. package/dist/fetch/fetcher.js +180 -0
  5. package/dist/fetch/fetcher.js.map +1 -0
  6. package/dist/fetch/index.d.ts +9 -0
  7. package/dist/fetch/index.js +10 -0
  8. package/dist/fetch/index.js.map +1 -0
  9. package/dist/fetch/ladder.d.ts +44 -0
  10. package/dist/fetch/ladder.js +18 -0
  11. package/dist/fetch/ladder.js.map +1 -0
  12. package/dist/fetch/robots.d.ts +14 -0
  13. package/dist/fetch/robots.js +92 -0
  14. package/dist/fetch/robots.js.map +1 -0
  15. package/dist/index.d.ts +11 -0
  16. package/dist/index.js +12 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/study/claims.d.ts +19 -0
  19. package/dist/study/claims.js +13 -0
  20. package/dist/study/claims.js.map +1 -0
  21. package/dist/study/drafts.d.ts +43 -0
  22. package/dist/study/drafts.js +93 -0
  23. package/dist/study/drafts.js.map +1 -0
  24. package/dist/study/evidence.d.ts +17 -0
  25. package/dist/study/evidence.js +72 -0
  26. package/dist/study/evidence.js.map +1 -0
  27. package/dist/study/grounding.d.ts +29 -0
  28. package/dist/study/grounding.js +58 -0
  29. package/dist/study/grounding.js.map +1 -0
  30. package/dist/study/index.d.ts +11 -0
  31. package/dist/study/index.js +12 -0
  32. package/dist/study/index.js.map +1 -0
  33. package/dist/study/report.d.ts +55 -0
  34. package/dist/study/report.js +56 -0
  35. package/dist/study/report.js.map +1 -0
  36. package/dist/text/entities.d.ts +2 -0
  37. package/dist/text/entities.js +4 -0
  38. package/dist/text/entities.js.map +1 -0
  39. package/dist/text/htmltext.d.ts +30 -0
  40. package/dist/text/htmltext.js +208 -0
  41. package/dist/text/htmltext.js.map +1 -0
  42. package/dist/text/index.d.ts +7 -0
  43. package/dist/text/index.js +8 -0
  44. package/dist/text/index.js.map +1 -0
  45. package/dist/text/text.d.ts +8 -0
  46. package/dist/text/text.js +38 -0
  47. package/dist/text/text.js.map +1 -0
  48. package/dist/video/gemini.d.ts +46 -0
  49. package/dist/video/gemini.js +93 -0
  50. package/dist/video/gemini.js.map +1 -0
  51. package/dist/video/index.d.ts +39 -0
  52. package/dist/video/index.js +16 -0
  53. package/dist/video/index.js.map +1 -0
  54. package/dist/video/list.d.ts +35 -0
  55. package/dist/video/list.js +32 -0
  56. package/dist/video/list.js.map +1 -0
  57. package/dist/video/read.d.ts +53 -0
  58. package/dist/video/read.js +92 -0
  59. package/dist/video/read.js.map +1 -0
  60. package/dist/video/shots.d.ts +35 -0
  61. package/dist/video/shots.js +139 -0
  62. package/dist/video/shots.js.map +1 -0
  63. package/dist/video/tools.d.ts +16 -0
  64. package/dist/video/tools.js +29 -0
  65. package/dist/video/tools.js.map +1 -0
  66. package/dist/video/url.d.ts +73 -0
  67. package/dist/video/url.js +255 -0
  68. package/dist/video/url.js.map +1 -0
  69. package/dist/video/watch.d.ts +94 -0
  70. package/dist/video/watch.js +321 -0
  71. package/dist/video/watch.js.map +1 -0
  72. package/package.json +55 -3
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Tiny HTML reading: visible text, links, title, mailto/tel hrefs in one pass.
3
+ * Deliberately not a DOM: enrichment needs the words and the links, not the tree.
4
+ * htmlparser2's streaming parser feeds the Extractor; it is tolerant of the malformed
5
+ * markup small-business sites serve and closes what HTML implies closed. Never throws:
6
+ * a pathological page degrades to whatever was read so far, never sinks a crawl.
7
+ */
8
+ import { Parser } from "htmlparser2";
9
+ import { decodeHtml } from "./entities.js";
10
+ const SKIP_CONTENT = new Set([
11
+ "script",
12
+ "style",
13
+ "noscript",
14
+ "template",
15
+ "svg",
16
+ ]);
17
+ /** Elements that end a run of inline text; a newline keeps extracted text readable. */
18
+ const BLOCK = new Set([
19
+ "p",
20
+ "div",
21
+ "br",
22
+ "li",
23
+ "tr",
24
+ "h1",
25
+ "h2",
26
+ "h3",
27
+ "h4",
28
+ "h5",
29
+ "h6",
30
+ "section",
31
+ "article",
32
+ "header",
33
+ "footer",
34
+ "nav",
35
+ "main",
36
+ "aside",
37
+ "table",
38
+ "td",
39
+ "th",
40
+ "ul",
41
+ "ol",
42
+ "dl",
43
+ "dt",
44
+ "dd",
45
+ "blockquote",
46
+ "pre",
47
+ "address",
48
+ "figure",
49
+ "figcaption",
50
+ "form",
51
+ "hr",
52
+ ]);
53
+ /** HTML character references to text (the WHATWG table). */
54
+ export const decodeEntities = decodeHtml;
55
+ /** Absolute URL for a link, or the raw href when neither it nor the base parses. */
56
+ function joinUrl(base, href) {
57
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(href))
58
+ return href;
59
+ if (!base)
60
+ return href;
61
+ try {
62
+ return new URL(href, base).href;
63
+ }
64
+ catch {
65
+ return href;
66
+ }
67
+ }
68
+ class Extractor {
69
+ baseUrl;
70
+ parts = [];
71
+ links = [];
72
+ mailtos = [];
73
+ tels = [];
74
+ title = "";
75
+ skipDepth = 0;
76
+ inTitle = false;
77
+ href = null;
78
+ anchorParts = [];
79
+ constructor(baseUrl) {
80
+ this.baseUrl = baseUrl;
81
+ }
82
+ startTag(tag, attrs) {
83
+ if (SKIP_CONTENT.has(tag))
84
+ this.skipDepth++;
85
+ else if (tag === "title")
86
+ this.inTitle = true;
87
+ else if (tag === "a") {
88
+ const href = attrs.href;
89
+ if (href?.startsWith("mailto:")) {
90
+ const list = safeDecode(href.slice(7).split("?", 1)[0] ?? "");
91
+ for (const addr of list.split(",")) {
92
+ const clean = addr.trim();
93
+ if (clean)
94
+ this.mailtos.push(clean);
95
+ }
96
+ }
97
+ else if (href?.startsWith("tel:")) {
98
+ const number = href.slice(4).trim();
99
+ if (number)
100
+ this.tels.push(safeDecode(number));
101
+ }
102
+ else if (href && !href.startsWith("javascript:") && !href.startsWith("#")) {
103
+ this.href = joinUrl(this.baseUrl, href);
104
+ this.anchorParts = [];
105
+ }
106
+ }
107
+ if (BLOCK.has(tag))
108
+ this.parts.push("\n");
109
+ }
110
+ endTag(tag) {
111
+ if (SKIP_CONTENT.has(tag) && this.skipDepth)
112
+ this.skipDepth--;
113
+ else if (tag === "title")
114
+ this.inTitle = false;
115
+ else if (tag === "a" && this.href) {
116
+ this.links.push({
117
+ url: this.href,
118
+ anchor: this.anchorParts.join("").split(/\s+/).filter(Boolean).join(" "),
119
+ });
120
+ this.href = null;
121
+ }
122
+ if (BLOCK.has(tag))
123
+ this.parts.push("\n");
124
+ }
125
+ /** Text as the parser hands it: entities already decoded once. */
126
+ data(decoded) {
127
+ const text = decoded.replaceAll("\u00a0", " ");
128
+ // The page's title, never words of the body.
129
+ if (this.inTitle) {
130
+ this.title += text;
131
+ return;
132
+ }
133
+ if (this.skipDepth)
134
+ return;
135
+ this.parts.push(text);
136
+ if (this.href !== null)
137
+ this.anchorParts.push(text);
138
+ }
139
+ }
140
+ function safeDecode(s) {
141
+ try {
142
+ return decodeURIComponent(s);
143
+ }
144
+ catch {
145
+ return s;
146
+ }
147
+ }
148
+ export function readPage(html, baseUrl = "") {
149
+ const out = new Extractor(baseUrl);
150
+ try {
151
+ const parser = new Parser({
152
+ onopentag: (name, attrs) => out.startTag(name, attrs),
153
+ onclosetag: (name) => out.endTag(name),
154
+ ontext: (text) => out.data(text),
155
+ });
156
+ parser.write(html);
157
+ parser.end();
158
+ }
159
+ catch {
160
+ // degrade to whatever was extracted so far
161
+ }
162
+ const text = out.parts
163
+ .join("")
164
+ .split(/\r?\n/)
165
+ .map((line) => line.trim())
166
+ .filter(Boolean)
167
+ .join("\n");
168
+ return {
169
+ title: out.title.split(/\s+/).filter(Boolean).join(" "),
170
+ text,
171
+ links: out.links,
172
+ mailtos: out.mailtos,
173
+ tels: out.tels,
174
+ };
175
+ }
176
+ /** SPA mount-point / framework markers, checked against lowercased markup. */
177
+ const SHELL_MARKERS = [
178
+ 'id="root"',
179
+ "id='root'",
180
+ 'id="app"',
181
+ "id='app'",
182
+ 'id="__next"',
183
+ "__next_data__",
184
+ "data-reactroot",
185
+ 'id="___gatsby"',
186
+ "ng-version=",
187
+ 'id="q-app"',
188
+ "enable javascript",
189
+ "requires javascript",
190
+ ];
191
+ const SHELL_MAX_TEXT = 200;
192
+ const SHELL_MIN_HTML = 2000;
193
+ /**
194
+ * Is this page a JS mount point whose words live client-side? Thin text alone must
195
+ * NOT fire (a tiny server-rendered contact page is real content): a shell is thin
196
+ * text plus substantial, script-dominated markup or a framework mount marker.
197
+ */
198
+ export function looksLikeJsShell(html, text) {
199
+ if (text.trim().length >= SHELL_MAX_TEXT)
200
+ return false;
201
+ if (html.length < SHELL_MIN_HTML)
202
+ return false;
203
+ const lowered = html.toLowerCase();
204
+ if (SHELL_MARKERS.some((marker) => lowered.includes(marker)))
205
+ return true;
206
+ return lowered.split("<script").length - 1 >= 3;
207
+ }
208
+ //# sourceMappingURL=htmltext.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"htmltext.js","sourceRoot":"","sources":["../../src/text/htmltext.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,MAAM,YAAY,GAAwB,IAAI,GAAG,CAAC;IAChD,QAAQ;IACR,OAAO;IACP,UAAU;IACV,UAAU;IACV,KAAK;CACN,CAAC,CAAC;AACH,uFAAuF;AACvF,MAAM,KAAK,GAAwB,IAAI,GAAG,CAAC;IACzC,GAAG;IACH,KAAK;IACL,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,SAAS;IACT,SAAS;IACT,QAAQ;IACR,QAAQ;IACR,KAAK;IACL,MAAM;IACN,OAAO;IACP,OAAO;IACP,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,YAAY;IACZ,KAAK;IACL,SAAS;IACT,QAAQ;IACR,YAAY;IACZ,MAAM;IACN,IAAI;CACL,CAAC,CAAC;AAiBH,4DAA4D;AAC5D,MAAM,CAAC,MAAM,cAAc,GAAG,UAAU,CAAC;AAEzC,oFAAoF;AACpF,SAAS,OAAO,CAAC,IAAY,EAAE,IAAY;IACzC,IAAI,2BAA2B,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,IAAI,CAAC;IACxD,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,IAAI,CAAC;QACH,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,IAAI,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,SAAS;IAWgB;IAVpB,KAAK,GAAa,EAAE,CAAC;IACrB,KAAK,GAAe,EAAE,CAAC;IACvB,OAAO,GAAa,EAAE,CAAC;IACvB,IAAI,GAAa,EAAE,CAAC;IAC7B,KAAK,GAAG,EAAE,CAAC;IACH,SAAS,GAAG,CAAC,CAAC;IACd,OAAO,GAAG,KAAK,CAAC;IAChB,IAAI,GAAkB,IAAI,CAAC;IAC3B,WAAW,GAAa,EAAE,CAAC;IAEnC,YAA6B,OAAe;QAAf,YAAO,GAAP,OAAO,CAAQ;IAAG,CAAC;IAEhD,QAAQ,CAAC,GAAW,EAAE,KAAuC;QAC3D,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,SAAS,EAAE,CAAC;aACvC,IAAI,GAAG,KAAK,OAAO;YAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;aACzC,IAAI,GAAG,KAAK,GAAG,EAAE,CAAC;YACrB,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;YACxB,IAAI,IAAI,EAAE,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;gBAChC,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC9D,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;oBACnC,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;oBAC1B,IAAI,KAAK;wBAAE,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;gBACtC,CAAC;YACH,CAAC;iBAAM,IAAI,IAAI,EAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBACpC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBACpC,IAAI,MAAM;oBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;YACjD,CAAC;iBAAM,IAAI,IAAI,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC5E,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;gBACxC,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;YACxB,CAAC;QACH,CAAC;QACD,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAC,SAAS;YAAE,IAAI,CAAC,SAAS,EAAE,CAAC;aACzD,IAAI,GAAG,KAAK,OAAO;YAAE,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;aAC1C,IAAI,GAAG,KAAK,GAAG,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAClC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;gBACd,GAAG,EAAE,IAAI,CAAC,IAAI;gBACd,MAAM,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;aACzE,CAAC,CAAC;YACH,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACnB,CAAC;QACD,IAAI,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;IAED,kEAAkE;IAClE,IAAI,CAAC,OAAe;QAClB,MAAM,IAAI,GAAG,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC/C,6CAA6C;QAC7C,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC;YACnB,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,SAAS;YAAE,OAAO;QAC3B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI;YAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtD,CAAC;CACF;AAED,SAAS,UAAU,CAAC,CAAS;IAC3B,IAAI,CAAC;QACH,OAAO,kBAAkB,CAAC,CAAC,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAY,EAAE,OAAO,GAAG,EAAE;IACjD,MAAM,GAAG,GAAG,IAAI,SAAS,CAAC,OAAO,CAAC,CAAC;IACnC,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC;YACxB,SAAS,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;YACrD,UAAU,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;YACtC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC;SACjC,CAAC,CAAC;QACH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACnB,MAAM,CAAC,GAAG,EAAE,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,2CAA2C;IAC7C,CAAC;IACD,MAAM,IAAI,GAAG,GAAG,CAAC,KAAK;SACnB,IAAI,CAAC,EAAE,CAAC;SACR,KAAK,CAAC,OAAO,CAAC;SACd,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,OAAO,CAAC;SACf,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,OAAO;QACL,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;QACvD,IAAI;QACJ,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,IAAI,EAAE,GAAG,CAAC,IAAI;KACf,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,aAAa,GAAG;IACpB,WAAW;IACX,WAAW;IACX,UAAU;IACV,UAAU;IACV,aAAa;IACb,eAAe;IACf,gBAAgB;IAChB,gBAAgB;IAChB,aAAa;IACb,YAAY;IACZ,mBAAmB;IACnB,qBAAqB;CACb,CAAC;AACX,MAAM,cAAc,GAAG,GAAG,CAAC;AAC3B,MAAM,cAAc,GAAG,IAAI,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAY,EAAE,IAAY;IACzD,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,IAAI,cAAc;QAAE,OAAO,KAAK,CAAC;IACvD,IAAI,IAAI,CAAC,MAAM,GAAG,cAAc;QAAE,OAAO,KAAK,CAAC;IAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACnC,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IAC1E,OAAO,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC;AAClD,CAAC"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Files and markup in, readable text out: HTML to text, or to title, text and links (`readPage`);
3
+ * PDF to text; character references. No database here.
4
+ */
5
+ export * from "./entities.js";
6
+ export * from "./htmltext.js";
7
+ export * from "./text.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Files and markup in, readable text out: HTML to text, or to title, text and links (`readPage`);
3
+ * PDF to text; character references. No database here.
4
+ */
5
+ export * from "./entities.js";
6
+ export * from "./htmltext.js";
7
+ export * from "./text.js";
8
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/text/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,WAAW,CAAC"}
@@ -0,0 +1,8 @@
1
+ /** Postgres text refuses NUL; a PDF glyph with no text mapping comes out as one. */
2
+ export declare const withoutNul: (text: string) => string;
3
+ /** Tidy lines: trimmed, inner runs of spaces collapsed, at most one blank line between blocks. */
4
+ export declare function tidy(text: string): string;
5
+ /** HTML as readable text: blocks become lines, table cells stay on their row. */
6
+ export declare function htmlText(html: string): string;
7
+ /** A PDF's text, pages joined; null when it cannot be read (scanned, encrypted, broken). */
8
+ export declare function pdfText(bytes: Uint8Array): Promise<string | null>;
@@ -0,0 +1,38 @@
1
+ import { decodeHtml } from "./entities.js";
2
+ /** Postgres text refuses NUL; a PDF glyph with no text mapping comes out as one. */
3
+ export const withoutNul = (text) => text.replaceAll("\u0000", " ");
4
+ /** Tidy lines: trimmed, inner runs of spaces collapsed, at most one blank line between blocks. */
5
+ export function tidy(text) {
6
+ return withoutNul(text)
7
+ .replace(/\r\n?/g, "\n")
8
+ .split("\n")
9
+ .map((line) => line.replace(/(?:[ \t\u00a0\u2007\u200b\u200c\u2060]|\u200d|\u034f)+/g, " ").trim())
10
+ .join("\n")
11
+ .replace(/\n{3,}/g, "\n\n")
12
+ .trim();
13
+ }
14
+ /** HTML as readable text: blocks become lines, table cells stay on their row. */
15
+ export function htmlText(html) {
16
+ const text = html
17
+ .replace(/<!--[\s\S]*?-->/g, "")
18
+ .replace(/<(head|style|script|title)\b[\s\S]*?<\/\1\s*>/gi, "")
19
+ .replace(/<br\s*\/?>/gi, "\n")
20
+ .replace(/<\/(p|div|tr|li|h[1-6]|table|section|header|footer|blockquote)\s*>/gi, "\n")
21
+ .replace(/<\/(td|th)\s*>/gi, " \t ")
22
+ .replace(/<[^>]+>/g, "");
23
+ return tidy(decodeHtml(text));
24
+ }
25
+ /** A PDF's text, pages joined; null when it cannot be read (scanned, encrypted, broken). */
26
+ export async function pdfText(bytes) {
27
+ try {
28
+ const { extractText, getDocumentProxy } = await import("unpdf");
29
+ const pdf = await getDocumentProxy(new Uint8Array(bytes));
30
+ const { text } = await extractText(pdf, { mergePages: true });
31
+ const out = tidy(text);
32
+ return out || null;
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ //# sourceMappingURL=text.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/text/text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE3C,oFAAoF;AACpF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;AAE3E,kGAAkG;AAClG,MAAM,UAAU,IAAI,CAAC,IAAY;IAC/B,OAAO,UAAU,CAAC,IAAI,CAAC;SACpB,OAAO,CAAC,QAAQ,EAAE,IAAI,CAAC;SACvB,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CACZ,IAAI,CAAC,OAAO,CAAC,yDAAyD,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CACpF;SACA,IAAI,CAAC,IAAI,CAAC;SACV,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC;SAC1B,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,IAAI,GAAG,IAAI;SACd,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;SAC/B,OAAO,CAAC,iDAAiD,EAAE,EAAE,CAAC;SAC9D,OAAO,CAAC,cAAc,EAAE,IAAI,CAAC;SAC7B,OAAO,CAAC,sEAAsE,EAAE,IAAI,CAAC;SACrF,OAAO,CAAC,kBAAkB,EAAE,MAAM,CAAC;SACnC,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IAC3B,OAAO,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;AAChC,CAAC;AAED,4FAA4F;AAC5F,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAAiB;IAC7C,IAAI,CAAC;QACH,MAAM,EAAE,WAAW,EAAE,gBAAgB,EAAE,GAAG,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC;QAChE,MAAM,GAAG,GAAG,MAAM,gBAAgB,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC;QAC1D,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,WAAW,CAAC,GAAG,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;QAC9D,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;QACvB,OAAO,GAAG,IAAI,IAAI,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,46 @@
1
+ import { type Model, type Part } from "@wrenautomation/models";
2
+ /** Newest first: a busy or refused model hands the call to the next. */
3
+ export declare const GEMINI_MODELS: string[];
4
+ /** One read, every model, key and pass together, gives up after this long. */
5
+ export declare const READ_MS = 360000;
6
+ /** The answer's ceiling: a 10-minute clip's text runs long. A cut answer keeps what it said. */
7
+ export declare const READ_TOKENS = 32000;
8
+ /**
9
+ * What to read off the frames; shared by YouTube clips and downloaded videos. Gemini won't repeat
10
+ * text it knows from the web (RECITATION); `paraphrase` asks for what that text says instead.
11
+ */
12
+ export declare const SHOWN: (start: number, paraphrase: boolean) => string;
13
+ /**
14
+ * The video reader on a Gemini key ring: `GEMINI_MODELS` in turn, each over every key that still
15
+ * works, three passes while they only say "later". Null when no key works: a revoked fleet must
16
+ * not get a 14 MB inline body posted to each key.
17
+ */
18
+ export declare function geminiVideo(keys: readonly string[], o?: {
19
+ fetch?: typeof fetch;
20
+ }): Promise<Model | null>;
21
+ /**
22
+ * `geminiVideo` with no key check: for keys known to work, or a proxy that holds the keys and
23
+ * passes only generate calls (the check lists models, which such a proxy refuses).
24
+ */
25
+ export declare function geminiReader(keys: readonly string[], o?: {
26
+ fetch?: typeof fetch;
27
+ }): Model;
28
+ /**
29
+ * One read of media: the parts, then the ask. An answer the output cap cut keeps what it said. A
30
+ * Filtered stop (RECITATION: Gemini won't repeat text it knows from the web) is asked once more
31
+ * with `paraphrase`, on the same model list from the top: a fallback does not rotate on Filtered,
32
+ * since the next model knows the same web. A plain string ask has no paraphrase; Filtered throws.
33
+ */
34
+ export declare function readMedia(model: Model, media: readonly Part[], ask: string | ((paraphrase: boolean) => string), o?: {
35
+ resolution?: "low" | "medium" | "high";
36
+ }): Promise<string>;
37
+ export declare const INLINE_MAX = 14000000;
38
+ /**
39
+ * A video re-encoded to fit `INLINE_MAX`: short side 360, 15 fps, mono speech, and a bitrate
40
+ * that spends 85% of the cap over its length (four minutes when yt-dlp didn't say).
41
+ */
42
+ export declare function shrink(src: string, out: string, info: {
43
+ duration?: number;
44
+ width?: number;
45
+ height?: number;
46
+ }, ffmpeg?: string): Promise<Buffer>;
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Gemini, as the video readers ask it: the reader over a key ring, one read of media with the
3
+ * paraphrase retry, the on-screen ask, and the 360p copy that fits inline. Every call goes through
4
+ * `@wrenautomation/models`.
5
+ */
6
+ import { readFile } from "node:fs/promises";
7
+ import { fallback, gemini, geminiLiveKeys, isCut, isFiltered, } from "@wrenautomation/models";
8
+ import { clock, runFor, TOOL_MS } from "./tools.js";
9
+ /** Newest first: a busy or refused model hands the call to the next. */
10
+ export const GEMINI_MODELS = ["gemini-3.8-flash", "gemini-3.5-flash", "gemini-3.5-flash-lite"];
11
+ /** One call gives up after this long: a stalled socket would wait out undici's 5 minutes. */
12
+ const CALL_MS = 180_000;
13
+ /** One read, every model, key and pass together, gives up after this long. */
14
+ export const READ_MS = 360_000;
15
+ /** The answer's ceiling: a 10-minute clip's text runs long. A cut answer keeps what it said. */
16
+ export const READ_TOKENS = 32_000;
17
+ /**
18
+ * What to read off the frames; shared by YouTube clips and downloaded videos. Gemini won't repeat
19
+ * text it knows from the web (RECITATION); `paraphrase` asks for what that text says instead.
20
+ */
21
+ export const SHOWN = (start, paraphrase) => `Transcribe in full, verbatim, every document, list, table, slide, template or other text the speaker shows on screen in this clip, each under a [h:mm:ss] heading of the video time it appears (the clip starts at ${clock(start)}). For every image, screenshot, chart, diagram or UI on screen, extract the information it carries, not what it looks like: the text in it, setting names and their values, numbers, prices, limits, labels, before and after states, and what is highlighted, circled, crossed out, ticked or pointed at. Write each as a line starting with its [h:mm:ss] then [visual], stating the fact as a reader would use it ("Instantly warmup settings: daily limit 10, slow ramp on, +1 per day, reply rate 95%"), not "a screenshot of the settings page". Skip visuals that carry no information (decoration, layout, gestures, transitions) and repeats of anything already captured. Skip the speaker's face, chat windows, b-roll and stock footage, and anything already shown earlier in the clip.${paraphrase ? " Where a shown passage is published text (a post, article, page), give its first line word for word, then every point it makes in your own words." : ""}`;
22
+ /**
23
+ * The video reader on a Gemini key ring: `GEMINI_MODELS` in turn, each over every key that still
24
+ * works, three passes while they only say "later". Null when no key works: a revoked fleet must
25
+ * not get a 14 MB inline body posted to each key.
26
+ */
27
+ export async function geminiVideo(keys, o = {}) {
28
+ const live = await geminiLiveKeys(keys, o);
29
+ return live.length ? geminiReader(live, o) : null;
30
+ }
31
+ /**
32
+ * `geminiVideo` with no key check: for keys known to work, or a proxy that holds the keys and
33
+ * passes only generate calls (the check lists models, which such a proxy refuses).
34
+ */
35
+ export function geminiReader(keys, o = {}) {
36
+ const wire = o.fetch ? { fetch: o.fetch } : {};
37
+ return fallback(GEMINI_MODELS.map((model) => gemini({ key: keys, model, limitMs: CALL_MS, ...wire })), { passes: 3 });
38
+ }
39
+ /**
40
+ * One read of media: the parts, then the ask. An answer the output cap cut keeps what it said. A
41
+ * Filtered stop (RECITATION: Gemini won't repeat text it knows from the web) is asked once more
42
+ * with `paraphrase`, on the same model list from the top: a fallback does not rotate on Filtered,
43
+ * since the next model knows the same web. A plain string ask has no paraphrase; Filtered throws.
44
+ */
45
+ export async function readMedia(model, media, ask, o = {}) {
46
+ const once = async (paraphrase) => {
47
+ try {
48
+ const r = await model.complete({
49
+ prompt: [...media, { type: "text", text: typeof ask === "string" ? ask : ask(paraphrase) }],
50
+ maxTokens: READ_TOKENS,
51
+ signal: AbortSignal.timeout(READ_MS),
52
+ ...(o.resolution ? { resolution: o.resolution } : {}),
53
+ });
54
+ return r.text.trim();
55
+ }
56
+ catch (err) {
57
+ if (isCut(err))
58
+ return err.partial.trim();
59
+ throw err;
60
+ }
61
+ };
62
+ try {
63
+ return await once(false);
64
+ }
65
+ catch (err) {
66
+ if (!isFiltered(err) || typeof ask === "string")
67
+ throw err;
68
+ console.warn(`${model.id}: stopped (${err.stop}); asking for a paraphrase`);
69
+ return once(true);
70
+ }
71
+ }
72
+ // Gemini takes 20 MB a request inline, and base64 adds a third.
73
+ export const INLINE_MAX = 14_000_000;
74
+ /**
75
+ * A video re-encoded to fit `INLINE_MAX`: short side 360, 15 fps, mono speech, and a bitrate
76
+ * that spends 85% of the cap over its length (four minutes when yt-dlp didn't say).
77
+ */
78
+ export async function shrink(src, out, info, ffmpeg = "ffmpeg") {
79
+ const seconds = info.duration && info.duration > 0 ? info.duration : 240;
80
+ const audio = 48_000;
81
+ const video = Math.max(120_000, Math.floor((INLINE_MAX * 8 * 0.85) / seconds - audio));
82
+ const tall = (info.height ?? 1) >= (info.width ?? 0);
83
+ await runFor(ffmpeg, [
84
+ ...["-y", "-loglevel", "error", "-i", src],
85
+ ...["-vf", tall ? "scale=360:-2" : "scale=-2:360", "-r", "15"],
86
+ ...["-c:v", "libx264", "-preset", "veryfast", "-b:v", String(video)],
87
+ ...["-maxrate", String(video), "-bufsize", String(video * 2)],
88
+ ...["-c:a", "aac", "-b:a", String(audio), "-ac", "1"],
89
+ ...["-movflags", "+faststart", out],
90
+ ], TOOL_MS);
91
+ return readFile(out);
92
+ }
93
+ //# sourceMappingURL=gemini.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gemini.js","sourceRoot":"","sources":["../../src/video/gemini.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Video: `watchFile` (ffmpeg facts; an ear for the speech, an eye for the frames, a video model
3
+ * when one is given) over a file on disk; the `VideoRead` shape with the captions renderer and the
4
+ * audio hearer (`read.ts`); the readers over a URL (`url.ts`: YouTube by captions or by URL, any
5
+ * other video by yt-dlp, a podcast by its audio URL, `watch` over a URL or a file); the list
6
+ * reader (`list.ts`: what a profile, channel or search holds). Every caller passes its own models
7
+ * and tools. No database here.
8
+ */
9
+ export * from "./gemini.js";
10
+ export * from "./list.js";
11
+ export * from "./read.js";
12
+ export * from "./shots.js";
13
+ export * from "./tools.js";
14
+ export * from "./url.js";
15
+ export * from "./watch.js";
16
+ /** What `yt-dlp -J` prints; only what the readers use. */
17
+ export interface VideoInfo {
18
+ id: string;
19
+ title: string;
20
+ channel?: string;
21
+ upload_date?: string;
22
+ webpage_url?: string;
23
+ duration?: number;
24
+ /** Its thumbnail's address. */
25
+ thumbnail?: string;
26
+ chapters?: {
27
+ start_time: number;
28
+ end_time: number;
29
+ title: string;
30
+ }[] | null;
31
+ subtitles?: Record<string, {
32
+ ext: string;
33
+ url: string;
34
+ }[]>;
35
+ automatic_captions?: Record<string, {
36
+ ext: string;
37
+ url: string;
38
+ }[]>;
39
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Video: `watchFile` (ffmpeg facts; an ear for the speech, an eye for the frames, a video model
3
+ * when one is given) over a file on disk; the `VideoRead` shape with the captions renderer and the
4
+ * audio hearer (`read.ts`); the readers over a URL (`url.ts`: YouTube by captions or by URL, any
5
+ * other video by yt-dlp, a podcast by its audio URL, `watch` over a URL or a file); the list
6
+ * reader (`list.ts`: what a profile, channel or search holds). Every caller passes its own models
7
+ * and tools. No database here.
8
+ */
9
+ export * from "./gemini.js";
10
+ export * from "./list.js";
11
+ export * from "./read.js";
12
+ export * from "./shots.js";
13
+ export * from "./tools.js";
14
+ export * from "./url.js";
15
+ export * from "./watch.js";
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/video/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,cAAc,aAAa,CAAC;AAC5B,cAAc,WAAW,CAAC;AAC1B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC;AAC3B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC"}
@@ -0,0 +1,35 @@
1
+ /** One entry of yt-dlp's playlist view (`--flat-playlist`): a TikTok profile, a YouTube search. */
2
+ export interface YtDlpEntry {
3
+ id?: string;
4
+ url?: string;
5
+ webpage_url?: string;
6
+ title?: string;
7
+ description?: string;
8
+ timestamp?: number;
9
+ duration?: number;
10
+ uploader?: string;
11
+ channel?: string;
12
+ thumbnails?: {
13
+ url?: string;
14
+ }[];
15
+ view_count?: number;
16
+ like_count?: number;
17
+ comment_count?: number;
18
+ }
19
+ /**
20
+ * yt-dlp's playlist view of `target` (a profile, a channel's videos, `ytsearchN:<words>`), no
21
+ * downloads: the entries with an id, in yt-dlp's order. `range` is 1-based and inclusive. Some
22
+ * sites answer only a home IP. `ytDlp` is the command, split on spaces (`uvx yt-dlp`).
23
+ */
24
+ export declare function flatPlaylist(ytDlp: string, target: string, range?: {
25
+ start: number;
26
+ end: number;
27
+ }): Promise<YtDlpEntry[]>;
28
+ /**
29
+ * yt-dlp's JSON for one page, flat: a playlist's `entries`, or one video's fields
30
+ * (`channel_url`, `uploader_url`) when the target is a video.
31
+ */
32
+ export declare function pageInfo(ytDlp: string, target: string, range?: {
33
+ start: number;
34
+ end: number;
35
+ }): Promise<Record<string, unknown>>;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * What a profile, channel or search holds, through yt-dlp's playlist view: titles, ids and
3
+ * counts, no downloads. The list reader beside the piece readers in `url.ts`.
4
+ */
5
+ import { runFor } from "./tools.js";
6
+ const LIST_MS = 180_000;
7
+ /**
8
+ * yt-dlp's playlist view of `target` (a profile, a channel's videos, `ytsearchN:<words>`), no
9
+ * downloads: the entries with an id, in yt-dlp's order. `range` is 1-based and inclusive. Some
10
+ * sites answer only a home IP. `ytDlp` is the command, split on spaces (`uvx yt-dlp`).
11
+ */
12
+ export async function flatPlaylist(ytDlp, target, range) {
13
+ const page = (await pageInfo(ytDlp, target, range));
14
+ return (page.entries ?? []).filter((e) => e.id);
15
+ }
16
+ /**
17
+ * yt-dlp's JSON for one page, flat: a playlist's `entries`, or one video's fields
18
+ * (`channel_url`, `uploader_url`) when the target is a video.
19
+ */
20
+ export async function pageInfo(ytDlp, target, range) {
21
+ const [cmd, ...pre] = ytDlp.split(/\s+/);
22
+ const { stdout } = await runFor(cmd, [
23
+ ...pre,
24
+ ...["-J", "--flat-playlist", "--no-warnings"],
25
+ ...(range
26
+ ? ["--playlist-start", String(range.start), "--playlist-end", String(range.end)]
27
+ : []),
28
+ target,
29
+ ], LIST_MS, { maxBuffer: 64 * 1024 * 1024 });
30
+ return JSON.parse(stdout);
31
+ }
32
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["../../src/video/list.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAC;AAmBpC,MAAM,OAAO,GAAG,OAAO,CAAC;AAExB;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,KAAa,EACb,MAAc,EACd,KAAsC;IAEtC,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,CAA+B,CAAC;IAClF,OAAO,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AAClD,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,KAAa,EACb,MAAc,EACd,KAAsC;IAEtC,MAAM,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAA0B,CAAC;IAClE,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAC7B,GAAG,EACH;QACE,GAAG,GAAG;QACN,GAAG,CAAC,IAAI,EAAE,iBAAiB,EAAE,eAAe,CAAC;QAC7C,GAAG,CAAC,KAAK;YACP,CAAC,CAAC,CAAC,kBAAkB,EAAE,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,gBAAgB,EAAE,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAChF,CAAC,CAAC,EAAE,CAAC;QACP,MAAM;KACP,EACD,OAAO,EACP,EAAE,SAAS,EAAE,EAAE,GAAG,IAAI,GAAG,IAAI,EAAE,CAChC,CAAC;IACF,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAA4B,CAAC;AACvD,CAAC"}
@@ -0,0 +1,53 @@
1
+ import type { Model } from "@wrenautomation/models";
2
+ import type { VideoInfo } from "./index.js";
3
+ /** What a reader gives back: where the piece is, and its markdown sections. */
4
+ export interface VideoRead {
5
+ /** The platform, lower case: `youtube`, `instagram`, `tiktok`, `podcast` (yt-dlp's extractor key). */
6
+ kind: string;
7
+ /** Its id on the platform; a podcast's is a hash of its audio URL. */
8
+ id: string;
9
+ title: string;
10
+ url: string;
11
+ channel?: string;
12
+ /** YYYY-MM-DD. */
13
+ uploaded?: string;
14
+ /** Seconds, rounded. */
15
+ duration?: number;
16
+ thumbnail?: string;
17
+ /** The sections as markdown, headings at `##`: one per chapter, or Caption, Speech, On screen, Cuts. */
18
+ body: string;
19
+ }
20
+ /** YouTube's json3 caption track; only what is read. */
21
+ export interface Json3 {
22
+ events: {
23
+ tStartMs: number;
24
+ segs?: {
25
+ utf8: string;
26
+ }[];
27
+ }[];
28
+ }
29
+ /** Seconds of video per model call. */
30
+ export declare const CLIP_S = 600;
31
+ /** yt-dlp's `YYYYMMDD` as an ISO date. */
32
+ export declare const isoDate: (d: string | undefined) => string | undefined;
33
+ /** A model's "none": nothing shown or said. */
34
+ export declare const real: (s: string) => boolean | "";
35
+ /** Captions as markdown: a heading per chapter, a `[m:ss]` marker every minute so a reader can cite a moment. */
36
+ export declare function captionsBody(info: VideoInfo, track: Json3): string;
37
+ /**
38
+ * A podcast episode heard from its audio file on disk: cut by ffmpeg into `CLIP_S` mono clips
39
+ * small enough to send inline (about 2 MB each), each transcribed by `model` (`geminiVideo`), 4 at
40
+ * a time, times running on across clips. The clips are written beside the file and left for the
41
+ * caller's cleanup. `label` names the calls in errors.
42
+ */
43
+ export declare function hearAudio(file: string, opts: {
44
+ /** Takes audio: Gemini reads speakers and times where a plain transcriber gives words. */
45
+ model: Model;
46
+ ffmpeg?: string;
47
+ /** Seconds per clip; `CLIP_S` unless a test cuts a short tone. */
48
+ clipS?: number;
49
+ label?: string;
50
+ }): Promise<{
51
+ speech: string;
52
+ duration: number;
53
+ }>;