dreamcontext 0.5.3 → 0.6.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/LICENSE +201 -21
- package/NOTICE +7 -0
- package/README.md +77 -12
- package/agents/dreamcontext-explore.md +1 -1
- package/agents/sleep-product.md +29 -8
- package/agents/sleep-tasks.md +44 -9
- package/dist/agents/dreamcontext-explore.md +1 -1
- package/dist/agents/sleep-product.md +29 -8
- package/dist/agents/sleep-tasks.md +44 -9
- package/dist/dashboard/assets/{BrainCanvas3D-1HMiA14F.js → BrainCanvas3D-LLqVeXtb.js} +1 -1
- package/dist/dashboard/assets/{_baseUniq-C7pfRmXz.js → _baseUniq-DW0uA0ty.js} +1 -1
- package/dist/dashboard/assets/{arc-B5x_GF9Q.js → arc-fVUGtlbL.js} +1 -1
- package/dist/dashboard/assets/{architectureDiagram-Q4EWVU46-CWmjaqno.js → architectureDiagram-Q4EWVU46-Dz2PKBsS.js} +1 -1
- package/dist/dashboard/assets/{blockDiagram-DXYQGD6D-B7SyUIdV.js → blockDiagram-DXYQGD6D-ByWKxzhL.js} +1 -1
- package/dist/dashboard/assets/{c4Diagram-AHTNJAMY-OeJBWXHU.js → c4Diagram-AHTNJAMY-dpHsVM3D.js} +1 -1
- package/dist/dashboard/assets/channel-zrLwggBX.js +1 -0
- package/dist/dashboard/assets/{chunk-4BX2VUAB-2Oop1B0M.js → chunk-4BX2VUAB-HEXb6Yg5.js} +1 -1
- package/dist/dashboard/assets/{chunk-4TB4RGXK-Bh7qSu_X.js → chunk-4TB4RGXK-DeVy5g6H.js} +1 -1
- package/dist/dashboard/assets/{chunk-55IACEB6-gNeHRO_i.js → chunk-55IACEB6-DGy3ZgDZ.js} +1 -1
- package/dist/dashboard/assets/{chunk-EDXVE4YY-BRniAH53.js → chunk-EDXVE4YY-CYJehjz4.js} +1 -1
- package/dist/dashboard/assets/{chunk-FMBD7UC4-B5r8U2gJ.js → chunk-FMBD7UC4-DKTOmvgZ.js} +1 -1
- package/dist/dashboard/assets/{chunk-OYMX7WX6-Dd1c17fm.js → chunk-OYMX7WX6-mmtWyiDA.js} +1 -1
- package/dist/dashboard/assets/{chunk-QZHKN3VN-BIw-OdTn.js → chunk-QZHKN3VN-DSdV0iwY.js} +1 -1
- package/dist/dashboard/assets/{chunk-YZCP3GAM-Dl9MlwCW.js → chunk-YZCP3GAM-DbY-KoDn.js} +1 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-BvejNiwH.js +1 -0
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-BvejNiwH.js +1 -0
- package/dist/dashboard/assets/clone-BFVdml6g.js +1 -0
- package/dist/dashboard/assets/{cose-bilkent-S5V4N54A-DlrROH5K.js → cose-bilkent-S5V4N54A-p0t1DY88.js} +1 -1
- package/dist/dashboard/assets/{dagre-KV5264BT-B0GpPXKL.js → dagre-KV5264BT-DYE0WzHM.js} +1 -1
- package/dist/dashboard/assets/{diagram-5BDNPKRD-BBYcDW2Z.js → diagram-5BDNPKRD-D9EiQCOP.js} +1 -1
- package/dist/dashboard/assets/{diagram-G4DWMVQ6-BxHxugdp.js → diagram-G4DWMVQ6-poDXSAfu.js} +1 -1
- package/dist/dashboard/assets/{diagram-MMDJMWI5-C9Jc19Wo.js → diagram-MMDJMWI5-CHZ7wgN1.js} +1 -1
- package/dist/dashboard/assets/{diagram-TYMM5635-Vmo5GAAU.js → diagram-TYMM5635-DVk6fYG7.js} +1 -1
- package/dist/dashboard/assets/{erDiagram-SMLLAGMA-pskr9x-V.js → erDiagram-SMLLAGMA-D2sqkGin.js} +1 -1
- package/dist/dashboard/assets/{flowDiagram-DWJPFMVM-B1D7Ky0W.js → flowDiagram-DWJPFMVM-DrMlKBYA.js} +1 -1
- package/dist/dashboard/assets/{ganttDiagram-T4ZO3ILL-BVJZZV1M.js → ganttDiagram-T4ZO3ILL-Cf4FbEFp.js} +1 -1
- package/dist/dashboard/assets/{gitGraphDiagram-UUTBAWPF-BiOGN6We.js → gitGraphDiagram-UUTBAWPF-BsbOq1C9.js} +1 -1
- package/dist/dashboard/assets/{graph-D58deEXr.js → graph-CUxxgCtS.js} +1 -1
- package/dist/dashboard/assets/index-CqSkXBSu.css +1 -0
- package/dist/dashboard/assets/index-flRpQtDj.js +476 -0
- package/dist/dashboard/assets/{infoDiagram-42DDH7IO-DgJYg1X1.js → infoDiagram-42DDH7IO-DTkMnZiD.js} +1 -1
- package/dist/dashboard/assets/{ishikawaDiagram-UXIWVN3A-arJgr-8P.js → ishikawaDiagram-UXIWVN3A-CahQ348K.js} +1 -1
- package/dist/dashboard/assets/{journeyDiagram-VCZTEJTY-B3DCvb8p.js → journeyDiagram-VCZTEJTY-BDY2Nslc.js} +1 -1
- package/dist/dashboard/assets/{kanban-definition-6JOO6SKY-B--dN6Ma.js → kanban-definition-6JOO6SKY-CgrwoTjI.js} +1 -1
- package/dist/dashboard/assets/{layout-z2qEbZWC.js → layout-IaUxkFkm.js} +1 -1
- package/dist/dashboard/assets/{linear-BQu1dwrM.js → linear-shc0iNFn.js} +1 -1
- package/dist/dashboard/assets/{min-DkF5b2sh.js → min-BIL7YgTN.js} +1 -1
- package/dist/dashboard/assets/{mindmap-definition-QFDTVHPH-Bw04koyq.js → mindmap-definition-QFDTVHPH-GmO0JRtA.js} +1 -1
- package/dist/dashboard/assets/{pieDiagram-DEJITSTG-vNqz64o8.js → pieDiagram-DEJITSTG-Bfm_toYA.js} +1 -1
- package/dist/dashboard/assets/{quadrantDiagram-34T5L4WZ-TuVP5Gi6.js → quadrantDiagram-34T5L4WZ-DIjTM3lm.js} +1 -1
- package/dist/dashboard/assets/{requirementDiagram-MS252O5E-D3mS8_ke.js → requirementDiagram-MS252O5E-B9AEoGYh.js} +1 -1
- package/dist/dashboard/assets/{sankeyDiagram-XADWPNL6-BmVRers6.js → sankeyDiagram-XADWPNL6-C_O5XLXm.js} +1 -1
- package/dist/dashboard/assets/{sequenceDiagram-FGHM5R23-CTYCgk5P.js → sequenceDiagram-FGHM5R23-CHSxSvmJ.js} +1 -1
- package/dist/dashboard/assets/{stateDiagram-FHFEXIEX-DNbP2aCg.js → stateDiagram-FHFEXIEX-B0URrhHw.js} +1 -1
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-FdAb9MBo.js +1 -0
- package/dist/dashboard/assets/{timeline-definition-GMOUNBTQ-DKHJd4z8.js → timeline-definition-GMOUNBTQ-CycU2gVC.js} +1 -1
- package/dist/dashboard/assets/{vennDiagram-DHZGUBPP-ChotNMNq.js → vennDiagram-DHZGUBPP-DcU2536G.js} +1 -1
- package/dist/dashboard/assets/{wardley-RL74JXVD-Dmo2_ksP.js → wardley-RL74JXVD-BWqqaX3S.js} +1 -1
- package/dist/dashboard/assets/{wardleyDiagram-NUSXRM2D-Z6Wbiv2H.js → wardleyDiagram-NUSXRM2D-CUnIHJgd.js} +1 -1
- package/dist/dashboard/assets/{xychartDiagram-5P7HB3ND-BJa0AHtN.js → xychartDiagram-5P7HB3ND-b9fcOYTj.js} +1 -1
- package/dist/dashboard/index.html +2 -2
- package/dist/dashboard/media/README.md +29 -0
- package/dist/dashboard/media/brain-hero.png +0 -0
- package/dist/dashboard/media/brain.mp4 +0 -0
- package/dist/dashboard/media/brain.webm +0 -0
- package/dist/dashboard/media/shot-disabled.png +0 -0
- package/dist/dashboard/media/shot-enabled.png +0 -0
- package/dist/index.js +3408 -2051
- package/dist/skill-packs/catalog.json +44 -0
- package/dist/skill-packs/excalidraw/SKILL.md +123 -0
- package/dist/skill-packs/excalidraw/examples/hello.spec.json +14 -0
- package/dist/skill-packs/excalidraw/examples/sample.png +0 -0
- package/dist/skill-packs/excalidraw/examples/style_board.js +31 -0
- package/dist/skill-packs/excalidraw/package.json +5 -0
- package/dist/skill-packs/excalidraw/reference/format.md +93 -0
- package/dist/skill-packs/excalidraw/scripts/build_excalidraw.js +357 -0
- package/dist/skill-packs/excalidraw/scripts/lib/fractional-indexing.LICENSE +121 -0
- package/dist/skill-packs/excalidraw/scripts/lib/fractional-indexing.js +311 -0
- package/dist/skill-packs/excalidraw/scripts/lib/imagesize.js +65 -0
- package/dist/skill-packs/excalidraw/scripts/lib/style.js +130 -0
- package/dist/skill-packs/video-watching/SKILL.md +151 -0
- package/dist/skill-packs/video-watching/scripts/build_frame_index.py +95 -0
- package/dist/skill-packs/video-watching/scripts/gap_fill.py +45 -0
- package/dist/skill-packs/video-watching/scripts/transcribe.sh +136 -0
- package/package.json +4 -2
- package/skill/SKILL.md +5 -0
- package/skill-packs/catalog.json +44 -0
- package/skill-packs/excalidraw/SKILL.md +123 -0
- package/skill-packs/excalidraw/examples/hello.spec.json +14 -0
- package/skill-packs/excalidraw/examples/sample.png +0 -0
- package/skill-packs/excalidraw/examples/style_board.js +31 -0
- package/skill-packs/excalidraw/package.json +5 -0
- package/skill-packs/excalidraw/reference/format.md +93 -0
- package/skill-packs/excalidraw/scripts/build_excalidraw.js +357 -0
- package/skill-packs/excalidraw/scripts/lib/fractional-indexing.LICENSE +121 -0
- package/skill-packs/excalidraw/scripts/lib/fractional-indexing.js +311 -0
- package/skill-packs/excalidraw/scripts/lib/imagesize.js +65 -0
- package/skill-packs/excalidraw/scripts/lib/style.js +130 -0
- package/skill-packs/video-watching/SKILL.md +151 -0
- package/skill-packs/video-watching/scripts/build_frame_index.py +95 -0
- package/skill-packs/video-watching/scripts/gap_fill.py +45 -0
- package/skill-packs/video-watching/scripts/transcribe.sh +136 -0
- package/dist/dashboard/assets/channel-CbuTKKsM.js +0 -1
- package/dist/dashboard/assets/classDiagram-6PBFFD2Q-HXl3A_sM.js +0 -1
- package/dist/dashboard/assets/classDiagram-v2-HSJHXN6E-HXl3A_sM.js +0 -1
- package/dist/dashboard/assets/clone-NKHOtdi8.js +0 -1
- package/dist/dashboard/assets/index-DK_2-eWY.css +0 -1
- package/dist/dashboard/assets/index-Ds-JXWtr.js +0 -476
- package/dist/dashboard/assets/stateDiagram-v2-QKLJ7IA2-CfN6kjo8.js +0 -1
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// License: CC0 (no rights reserved).
|
|
2
|
+
|
|
3
|
+
// This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
|
|
4
|
+
|
|
5
|
+
const BASE_62_DIGITS =
|
|
6
|
+
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
|
|
7
|
+
|
|
8
|
+
// `a` may be empty string, `b` is null or non-empty string.
|
|
9
|
+
// `a < b` lexicographically if `b` is non-null.
|
|
10
|
+
// no trailing zeros allowed.
|
|
11
|
+
// digits is a string such as '0123456789' for base 10. Digits must be in
|
|
12
|
+
// ascending character code order!
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} a
|
|
15
|
+
* @param {string | null | undefined} b
|
|
16
|
+
* @param {string} digits
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function midpoint(a, b, digits) {
|
|
20
|
+
const zero = digits[0];
|
|
21
|
+
if (b != null && a >= b) {
|
|
22
|
+
throw new Error(a + " >= " + b);
|
|
23
|
+
}
|
|
24
|
+
if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) {
|
|
25
|
+
throw new Error("trailing zero");
|
|
26
|
+
}
|
|
27
|
+
if (b) {
|
|
28
|
+
// remove longest common prefix. pad `a` with 0s as we
|
|
29
|
+
// go. note that we don't need to pad `b`, because it can't
|
|
30
|
+
// end before `a` while traversing the common prefix.
|
|
31
|
+
let n = 0;
|
|
32
|
+
while ((a[n] || zero) === b[n]) {
|
|
33
|
+
n++;
|
|
34
|
+
}
|
|
35
|
+
if (n > 0) {
|
|
36
|
+
return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// first digits (or lack of digit) are different
|
|
40
|
+
const digitA = a ? digits.indexOf(a[0]) : 0;
|
|
41
|
+
const digitB = b != null ? digits.indexOf(b[0]) : digits.length;
|
|
42
|
+
if (digitB - digitA > 1) {
|
|
43
|
+
const midDigit = Math.round(0.5 * (digitA + digitB));
|
|
44
|
+
return digits[midDigit];
|
|
45
|
+
} else {
|
|
46
|
+
// first digits are consecutive
|
|
47
|
+
if (b && b.length > 1) {
|
|
48
|
+
return b.slice(0, 1);
|
|
49
|
+
} else {
|
|
50
|
+
// `b` is null or has length 1 (a single digit).
|
|
51
|
+
// the first digit of `a` is the previous digit to `b`,
|
|
52
|
+
// or 9 if `b` is null.
|
|
53
|
+
// given, for example, midpoint('49', '5'), return
|
|
54
|
+
// '4' + midpoint('9', null), which will become
|
|
55
|
+
// '4' + '9' + midpoint('', null), which is '495'
|
|
56
|
+
return digits[digitA] + midpoint(a.slice(1), null, digits);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @param {string} int
|
|
63
|
+
* @return {void}
|
|
64
|
+
*/
|
|
65
|
+
|
|
66
|
+
function validateInteger(int) {
|
|
67
|
+
if (int.length !== getIntegerLength(int[0])) {
|
|
68
|
+
throw new Error("invalid integer part of order key: " + int);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @param {string} head
|
|
74
|
+
* @return {number}
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
function getIntegerLength(head) {
|
|
78
|
+
if (head >= "a" && head <= "z") {
|
|
79
|
+
return head.charCodeAt(0) - "a".charCodeAt(0) + 2;
|
|
80
|
+
} else if (head >= "A" && head <= "Z") {
|
|
81
|
+
return "Z".charCodeAt(0) - head.charCodeAt(0) + 2;
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error("invalid order key head: " + head);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @param {string} key
|
|
89
|
+
* @return {string}
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
function getIntegerPart(key) {
|
|
93
|
+
const integerPartLength = getIntegerLength(key[0]);
|
|
94
|
+
if (integerPartLength > key.length) {
|
|
95
|
+
throw new Error("invalid order key: " + key);
|
|
96
|
+
}
|
|
97
|
+
return key.slice(0, integerPartLength);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} key
|
|
102
|
+
* @param {string} digits
|
|
103
|
+
* @return {void}
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
function validateOrderKey(key, digits) {
|
|
107
|
+
if (key === "A" + digits[0].repeat(26)) {
|
|
108
|
+
throw new Error("invalid order key: " + key);
|
|
109
|
+
}
|
|
110
|
+
// getIntegerPart will throw if the first character is bad,
|
|
111
|
+
// or the key is too short. we'd call it to check these things
|
|
112
|
+
// even if we didn't need the result
|
|
113
|
+
const i = getIntegerPart(key);
|
|
114
|
+
const f = key.slice(i.length);
|
|
115
|
+
if (f.slice(-1) === digits[0]) {
|
|
116
|
+
throw new Error("invalid order key: " + key);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// note that this may return null, as there is a largest integer
|
|
121
|
+
/**
|
|
122
|
+
* @param {string} x
|
|
123
|
+
* @param {string} digits
|
|
124
|
+
* @return {string | null}
|
|
125
|
+
*/
|
|
126
|
+
function incrementInteger(x, digits) {
|
|
127
|
+
validateInteger(x);
|
|
128
|
+
const [head, ...digs] = x.split("");
|
|
129
|
+
let carry = true;
|
|
130
|
+
for (let i = digs.length - 1; carry && i >= 0; i--) {
|
|
131
|
+
const d = digits.indexOf(digs[i]) + 1;
|
|
132
|
+
if (d === digits.length) {
|
|
133
|
+
digs[i] = digits[0];
|
|
134
|
+
} else {
|
|
135
|
+
digs[i] = digits[d];
|
|
136
|
+
carry = false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (carry) {
|
|
140
|
+
if (head === "Z") {
|
|
141
|
+
return "a" + digits[0];
|
|
142
|
+
}
|
|
143
|
+
if (head === "z") {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
const h = String.fromCharCode(head.charCodeAt(0) + 1);
|
|
147
|
+
if (h > "a") {
|
|
148
|
+
digs.push(digits[0]);
|
|
149
|
+
} else {
|
|
150
|
+
digs.pop();
|
|
151
|
+
}
|
|
152
|
+
return h + digs.join("");
|
|
153
|
+
} else {
|
|
154
|
+
return head + digs.join("");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// note that this may return null, as there is a smallest integer
|
|
159
|
+
/**
|
|
160
|
+
* @param {string} x
|
|
161
|
+
* @param {string} digits
|
|
162
|
+
* @return {string | null}
|
|
163
|
+
*/
|
|
164
|
+
|
|
165
|
+
function decrementInteger(x, digits) {
|
|
166
|
+
validateInteger(x);
|
|
167
|
+
const [head, ...digs] = x.split("");
|
|
168
|
+
let borrow = true;
|
|
169
|
+
for (let i = digs.length - 1; borrow && i >= 0; i--) {
|
|
170
|
+
const d = digits.indexOf(digs[i]) - 1;
|
|
171
|
+
if (d === -1) {
|
|
172
|
+
digs[i] = digits.slice(-1);
|
|
173
|
+
} else {
|
|
174
|
+
digs[i] = digits[d];
|
|
175
|
+
borrow = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (borrow) {
|
|
179
|
+
if (head === "a") {
|
|
180
|
+
return "Z" + digits.slice(-1);
|
|
181
|
+
}
|
|
182
|
+
if (head === "A") {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const h = String.fromCharCode(head.charCodeAt(0) - 1);
|
|
186
|
+
if (h < "Z") {
|
|
187
|
+
digs.push(digits.slice(-1));
|
|
188
|
+
} else {
|
|
189
|
+
digs.pop();
|
|
190
|
+
}
|
|
191
|
+
return h + digs.join("");
|
|
192
|
+
} else {
|
|
193
|
+
return head + digs.join("");
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// `a` is an order key or null (START).
|
|
198
|
+
// `b` is an order key or null (END).
|
|
199
|
+
// `a < b` lexicographically if both are non-null.
|
|
200
|
+
// digits is a string such as '0123456789' for base 10. Digits must be in
|
|
201
|
+
// ascending character code order!
|
|
202
|
+
/**
|
|
203
|
+
* @param {string | null | undefined} a
|
|
204
|
+
* @param {string | null | undefined} b
|
|
205
|
+
* @param {string=} digits
|
|
206
|
+
* @return {string}
|
|
207
|
+
*/
|
|
208
|
+
function generateKeyBetween(a, b, digits = BASE_62_DIGITS) {
|
|
209
|
+
if (a != null) {
|
|
210
|
+
validateOrderKey(a, digits);
|
|
211
|
+
}
|
|
212
|
+
if (b != null) {
|
|
213
|
+
validateOrderKey(b, digits);
|
|
214
|
+
}
|
|
215
|
+
if (a != null && b != null && a >= b) {
|
|
216
|
+
throw new Error(a + " >= " + b);
|
|
217
|
+
}
|
|
218
|
+
if (a == null) {
|
|
219
|
+
if (b == null) {
|
|
220
|
+
return "a" + digits[0];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const ib = getIntegerPart(b);
|
|
224
|
+
const fb = b.slice(ib.length);
|
|
225
|
+
if (ib === "A" + digits[0].repeat(26)) {
|
|
226
|
+
return ib + midpoint("", fb, digits);
|
|
227
|
+
}
|
|
228
|
+
if (ib < b) {
|
|
229
|
+
return ib;
|
|
230
|
+
}
|
|
231
|
+
const res = decrementInteger(ib, digits);
|
|
232
|
+
if (res == null) {
|
|
233
|
+
throw new Error("cannot decrement any more");
|
|
234
|
+
}
|
|
235
|
+
return res;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (b == null) {
|
|
239
|
+
const ia = getIntegerPart(a);
|
|
240
|
+
const fa = a.slice(ia.length);
|
|
241
|
+
const i = incrementInteger(ia, digits);
|
|
242
|
+
return i == null ? ia + midpoint(fa, null, digits) : i;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const ia = getIntegerPart(a);
|
|
246
|
+
const fa = a.slice(ia.length);
|
|
247
|
+
const ib = getIntegerPart(b);
|
|
248
|
+
const fb = b.slice(ib.length);
|
|
249
|
+
if (ia === ib) {
|
|
250
|
+
return ia + midpoint(fa, fb, digits);
|
|
251
|
+
}
|
|
252
|
+
const i = incrementInteger(ia, digits);
|
|
253
|
+
if (i == null) {
|
|
254
|
+
throw new Error("cannot increment any more");
|
|
255
|
+
}
|
|
256
|
+
if (i < b) {
|
|
257
|
+
return i;
|
|
258
|
+
}
|
|
259
|
+
return ia + midpoint(fa, null, digits);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* same preconditions as generateKeysBetween.
|
|
264
|
+
* n >= 0.
|
|
265
|
+
* Returns an array of n distinct keys in sorted order.
|
|
266
|
+
* If a and b are both null, returns [a0, a1, ...]
|
|
267
|
+
* If one or the other is null, returns consecutive "integer"
|
|
268
|
+
* keys. Otherwise, returns relatively short keys between
|
|
269
|
+
* a and b.
|
|
270
|
+
* @param {string | null | undefined} a
|
|
271
|
+
* @param {string | null | undefined} b
|
|
272
|
+
* @param {number} n
|
|
273
|
+
* @param {string} digits
|
|
274
|
+
* @return {string[]}
|
|
275
|
+
*/
|
|
276
|
+
function generateNKeysBetween(a, b, n, digits = BASE_62_DIGITS) {
|
|
277
|
+
if (n === 0) {
|
|
278
|
+
return [];
|
|
279
|
+
}
|
|
280
|
+
if (n === 1) {
|
|
281
|
+
return [generateKeyBetween(a, b, digits)];
|
|
282
|
+
}
|
|
283
|
+
if (b == null) {
|
|
284
|
+
let c = generateKeyBetween(a, b, digits);
|
|
285
|
+
const result = [c];
|
|
286
|
+
for (let i = 0; i < n - 1; i++) {
|
|
287
|
+
c = generateKeyBetween(c, b, digits);
|
|
288
|
+
result.push(c);
|
|
289
|
+
}
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
if (a == null) {
|
|
293
|
+
let c = generateKeyBetween(a, b, digits);
|
|
294
|
+
const result = [c];
|
|
295
|
+
for (let i = 0; i < n - 1; i++) {
|
|
296
|
+
c = generateKeyBetween(a, c, digits);
|
|
297
|
+
result.push(c);
|
|
298
|
+
}
|
|
299
|
+
result.reverse();
|
|
300
|
+
return result;
|
|
301
|
+
}
|
|
302
|
+
const mid = Math.floor(n / 2);
|
|
303
|
+
const c = generateKeyBetween(a, b, digits);
|
|
304
|
+
return [
|
|
305
|
+
...generateNKeysBetween(a, c, mid, digits),
|
|
306
|
+
c,
|
|
307
|
+
...generateNKeysBetween(c, b, n - mid - 1, digits),
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = { BASE_62_DIGITS, generateKeyBetween, generateNKeysBetween };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Minimal, dependency-free image dimension reader. Supports PNG, JPEG, GIF, WebP, BMP.
|
|
2
|
+
// Returns { width, height, mime } or throws.
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
function fromBuffer(buf) {
|
|
6
|
+
// PNG: \x89PNG\r\n\x1a\n, IHDR width/height at bytes 16..24 (big-endian)
|
|
7
|
+
if (buf.length >= 24 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) {
|
|
8
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20), mime: 'image/png' };
|
|
9
|
+
}
|
|
10
|
+
// GIF: 'GIF87a' / 'GIF89a', width/height little-endian at bytes 6..10
|
|
11
|
+
if (buf.length >= 10 && buf[0] === 0x47 && buf[1] === 0x49 && buf[2] === 0x46) {
|
|
12
|
+
return { width: buf.readUInt16LE(6), height: buf.readUInt16LE(8), mime: 'image/gif' };
|
|
13
|
+
}
|
|
14
|
+
// BMP
|
|
15
|
+
if (buf.length >= 26 && buf[0] === 0x42 && buf[1] === 0x4d) {
|
|
16
|
+
return { width: buf.readInt32LE(18), height: Math.abs(buf.readInt32LE(22)), mime: 'image/bmp' };
|
|
17
|
+
}
|
|
18
|
+
// WebP: RIFF....WEBP
|
|
19
|
+
if (buf.length >= 30 && buf.toString('ascii', 0, 4) === 'RIFF' && buf.toString('ascii', 8, 12) === 'WEBP') {
|
|
20
|
+
const fmt = buf.toString('ascii', 12, 16);
|
|
21
|
+
if (fmt === 'VP8 ') {
|
|
22
|
+
return { width: buf.readUInt16LE(26) & 0x3fff, height: buf.readUInt16LE(28) & 0x3fff, mime: 'image/webp' };
|
|
23
|
+
} else if (fmt === 'VP8L') {
|
|
24
|
+
const b = buf.readUInt32LE(21);
|
|
25
|
+
return { width: (b & 0x3fff) + 1, height: ((b >> 14) & 0x3fff) + 1, mime: 'image/webp' };
|
|
26
|
+
} else if (fmt === 'VP8X') {
|
|
27
|
+
const w = 1 + (buf[24] | (buf[25] << 8) | (buf[26] << 16));
|
|
28
|
+
const h = 1 + (buf[27] | (buf[28] << 8) | (buf[29] << 16));
|
|
29
|
+
return { width: w, height: h, mime: 'image/webp' };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
// JPEG: scan SOFn markers
|
|
33
|
+
if (buf.length >= 4 && buf[0] === 0xff && buf[1] === 0xd8) {
|
|
34
|
+
let off = 2;
|
|
35
|
+
while (off < buf.length) {
|
|
36
|
+
if (buf[off] !== 0xff) { off++; continue; }
|
|
37
|
+
const marker = buf[off + 1];
|
|
38
|
+
// SOF0..SOF15 except DHT(c4) DAC(cc) RSTn — these carry frame dims
|
|
39
|
+
if (marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc) {
|
|
40
|
+
const height = buf.readUInt16BE(off + 5);
|
|
41
|
+
const width = buf.readUInt16BE(off + 7);
|
|
42
|
+
return { width, height, mime: 'image/jpeg' };
|
|
43
|
+
}
|
|
44
|
+
const len = buf.readUInt16BE(off + 2);
|
|
45
|
+
off += 2 + len;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error('unsupported or unrecognized image format');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function imageSize(filePath) {
|
|
52
|
+
// Read enough bytes for any header (JPEG may need more; read up to 256KB)
|
|
53
|
+
const fd = fs.openSync(filePath, 'r');
|
|
54
|
+
try {
|
|
55
|
+
const stat = fs.fstatSync(fd);
|
|
56
|
+
const n = Math.min(stat.size, 262144);
|
|
57
|
+
const buf = Buffer.alloc(n);
|
|
58
|
+
fs.readSync(fd, buf, 0, n, 0);
|
|
59
|
+
return fromBuffer(buf);
|
|
60
|
+
} finally {
|
|
61
|
+
fs.closeSync(fd);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { imageSize, fromBuffer };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// House style learned from the vault's presentation boards (MemoryOS High Level, Personalization
|
|
2
|
+
// Module, Research). Excalifont (fontFamily 5), solid fills, always-rounded rects, 2px #1e1e1e ink,
|
|
3
|
+
// Excalidraw's native pastel swatches used semantically. These helpers return ElementSpec[] for
|
|
4
|
+
// build_excalidraw.js's buildExcalidraw({ elements }).
|
|
5
|
+
|
|
6
|
+
const INK = '#1e1e1e';
|
|
7
|
+
|
|
8
|
+
// semantic palette: { fill, stroke } — matches the vault's swatches and their meaning
|
|
9
|
+
const PALETTE = {
|
|
10
|
+
green: { fill: '#b2f2bb', stroke: '#2f9e44' }, // benefit / positive / go
|
|
11
|
+
paleGreen: { fill: '#ebfbee', stroke: '#2f9e44' },
|
|
12
|
+
red: { fill: '#ffc9c9', stroke: '#e03131' }, // problem / pain / risk
|
|
13
|
+
blue: { fill: '#a5d8ff', stroke: '#1971c2' }, // system / neutral / flow node
|
|
14
|
+
paleBlue: { fill: '#e7f5ff', stroke: '#1971c2' },
|
|
15
|
+
purple: { fill: '#d0bfff', stroke: '#6741d9' }, // core service / special
|
|
16
|
+
palePurple: { fill: '#f3f0ff', stroke: '#6741d9' },
|
|
17
|
+
yellow: { fill: '#ffec99', stroke: '#f08c00' }, // processing / attention
|
|
18
|
+
mint: { fill: '#96f2d7', stroke: '#0c8599' }, // result / output
|
|
19
|
+
gray: { fill: '#e9ecef', stroke: '#868e96' }, // muted / inactive
|
|
20
|
+
};
|
|
21
|
+
function pal(c) { return PALETTE[c] || { fill: c, stroke: INK }; }
|
|
22
|
+
|
|
23
|
+
let _gc = 0;
|
|
24
|
+
const newGroup = (hint) => `g${++_gc}-${hint || ''}`;
|
|
25
|
+
const lines = (t) => String(t).split('\n').length;
|
|
26
|
+
// word-wrapped line count at a given width — keeps card labels vertically centered when they wrap
|
|
27
|
+
function wrapLineCount(text, fontSize, width) {
|
|
28
|
+
const perLine = Math.max(1, Math.floor(width / (fontSize * 0.52)));
|
|
29
|
+
let total = 0;
|
|
30
|
+
for (const ln of String(text).split('\n')) {
|
|
31
|
+
const words = ln.split(/\s+/).filter(Boolean);
|
|
32
|
+
if (!words.length) { total += 1; continue; }
|
|
33
|
+
let cur = 0, lc = 1;
|
|
34
|
+
for (const w of words) { const add = (cur ? 1 : 0) + w.length; if (cur + add > perLine && cur > 0) { lc++; cur = w.length; } else cur += add; }
|
|
35
|
+
total += lc;
|
|
36
|
+
}
|
|
37
|
+
return total;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// rounded card: filled rect + centered label, grouped so they move together
|
|
41
|
+
function card(o) {
|
|
42
|
+
const { x, y, w = 260, h = 92, text = '', color = 'blue', fontSize = 20, stroke = INK, strokeWidth = 2, textColor = INK, roundness = true } = o;
|
|
43
|
+
const group = o.group || newGroup('card');
|
|
44
|
+
const p = pal(color);
|
|
45
|
+
const textH = wrapLineCount(text, fontSize, w) * fontSize * 1.25;
|
|
46
|
+
const ty = y + Math.max(6, (h - textH) / 2);
|
|
47
|
+
return [
|
|
48
|
+
{ type: 'rectangle', x, y, width: w, height: h, strokeColor: stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth, roundness, group },
|
|
49
|
+
{ type: 'text', x, y: ty, text, fontSize, color: textColor, width: w, align: 'center', fontFamily: 5, group },
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
// small node (flow step) — card with tighter defaults
|
|
53
|
+
function node(o) { return card(Object.assign({ w: 200, h: 64, fontSize: 18 }, o)); }
|
|
54
|
+
|
|
55
|
+
// big plain section header (no box), like "Configuration" / "Flow"
|
|
56
|
+
function sectionTitle(o) {
|
|
57
|
+
const { x, y, text, fontSize = 44, color = INK } = o;
|
|
58
|
+
return [{ type: 'text', x, y, text, fontSize, color, fontFamily: 5, width: Math.max(200, String(text).length * fontSize * 0.6) }];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// labeled connector arrow between two points (centers of cards, etc.)
|
|
62
|
+
function connector(o) {
|
|
63
|
+
const { from, to, label, double = false, strokeColor = INK, strokeWidth = 2, dashed = false, labelColor = '#495057', fontSize = 16 } = o;
|
|
64
|
+
const out = [{ type: 'arrow', points: [from, to], strokeColor, strokeWidth, strokeStyle: dashed ? 'dashed' : 'solid', endArrow: 'arrow', startArrow: double ? 'arrow' : null }];
|
|
65
|
+
if (label) {
|
|
66
|
+
const mx = (from[0] + to[0]) / 2, my = (from[1] + to[1]) / 2;
|
|
67
|
+
const w = Math.max(40, String(label).length * fontSize * 0.62);
|
|
68
|
+
out.push({ type: 'text', x: mx - w / 2, y: my - fontSize - 4, text: label, fontSize, color: labelColor, width: w, align: 'center', fontFamily: 5 });
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// side-annotation: short arrow from a node edge to a spec note (the "90sec / deepseek..." pattern)
|
|
74
|
+
function annotate(o) {
|
|
75
|
+
const { from, to, text, fontSize = 16, color = '#495057' } = o;
|
|
76
|
+
return [
|
|
77
|
+
{ type: 'arrow', points: [from, to], strokeColor: color, strokeWidth: 1.5, endArrow: 'arrow' },
|
|
78
|
+
{ type: 'text', x: to[0] + 6, y: to[1] - fontSize * 0.6, text, fontSize, color, width: Math.max(80, String(text).split('\n')[0].length * fontSize * 0.62), align: 'left', fontFamily: 5 },
|
|
79
|
+
];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// vertical stack of cards (benefit/risk columns)
|
|
83
|
+
function column(o) {
|
|
84
|
+
const { x, y, w = 280, h = 70, gap = 16, fontSize = 18, color = 'green', items = [] } = o;
|
|
85
|
+
const out = [];
|
|
86
|
+
items.forEach((it, i) => {
|
|
87
|
+
const t = typeof it === 'string' ? it : it.text;
|
|
88
|
+
const c = (typeof it === 'object' && it.color) || color;
|
|
89
|
+
out.push(...card({ x, y: y + i * (h + gap), w, h, text: t, color: c, fontSize }));
|
|
90
|
+
});
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// hub-and-spoke: central node + satellites with arrows (the "Personalization Layer" + APIs pattern)
|
|
95
|
+
function hub(o) {
|
|
96
|
+
const { cx, cy, label, color = 'purple', rx = 64, ry = 44, radius = 170, spokes = [], spokeColor = 'blue', spokeRx = 30, spokeRy = 30, startAngle = -Math.PI / 2 } = o;
|
|
97
|
+
const group = newGroup('hub');
|
|
98
|
+
const out = [];
|
|
99
|
+
const p = pal(color);
|
|
100
|
+
out.push({ type: 'ellipse', x: cx - rx, y: cy - ry, width: rx * 2, height: ry * 2, strokeColor: p.stroke, backgroundColor: p.fill, fillStyle: 'solid', strokeWidth: 2 });
|
|
101
|
+
out.push({ type: 'text', x: cx - rx, y: cy - 10, text: label, fontSize: 16, color: INK, width: rx * 2, align: 'center', fontFamily: 5 });
|
|
102
|
+
const n = spokes.length;
|
|
103
|
+
spokes.forEach((s, i) => {
|
|
104
|
+
const a = startAngle + (i / Math.max(1, n)) * Math.PI * 2;
|
|
105
|
+
const sx = cx + Math.cos(a) * radius, sy = cy + Math.sin(a) * radius;
|
|
106
|
+
const c = (typeof s === 'object' && s.color) || spokeColor;
|
|
107
|
+
const t = typeof s === 'string' ? s : s.label;
|
|
108
|
+
const sp = pal(c);
|
|
109
|
+
out.push({ type: 'arrow', points: [[cx + Math.cos(a) * rx, cy + Math.sin(a) * ry], [sx - Math.cos(a) * spokeRx, sy - Math.sin(a) * spokeRy]], strokeColor: INK, strokeWidth: 2, endArrow: 'arrow' });
|
|
110
|
+
out.push({ type: 'ellipse', x: sx - spokeRx, y: sy - spokeRy, width: spokeRx * 2, height: spokeRy * 2, strokeColor: sp.stroke, backgroundColor: sp.fill, fillStyle: 'solid', strokeWidth: 2 });
|
|
111
|
+
out.push({ type: 'text', x: sx - spokeRx, y: sy - 8, text: t, fontSize: 14, color: INK, width: spokeRx * 2, align: 'center', fontFamily: 5 });
|
|
112
|
+
});
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// bullet list as a single left-aligned block (their "Configuration" spec list pattern)
|
|
117
|
+
function bullets(o) {
|
|
118
|
+
const { x, y, items = [], fontSize = 20, color = INK, bullet = '• ', width } = o;
|
|
119
|
+
const text = items.map((i) => bullet + i).join('\n');
|
|
120
|
+
return [{ type: 'text', x, y, text, fontSize, color, fontFamily: 5, width: width || Math.max(200, Math.max(...items.map((i) => i.length)) * fontSize * 0.6) }];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// center helpers for wiring connectors to card edges
|
|
124
|
+
const center = (x, y, w, h) => [x + w / 2, y + h / 2];
|
|
125
|
+
const rightOf = (x, y, w, h) => [x + w, y + h / 2];
|
|
126
|
+
const leftOf = (x, y, w, h) => [x, y + h / 2];
|
|
127
|
+
const bottomOf = (x, y, w, h) => [x + w / 2, y + h];
|
|
128
|
+
const topOf = (x, y, w, h) => [x + w / 2, y];
|
|
129
|
+
|
|
130
|
+
module.exports = { INK, PALETTE, pal, card, node, sectionTitle, connector, annotate, column, hub, bullets, center, rightOf, leftOf, bottomOf, topOf };
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: video-watching
|
|
3
|
+
description: >-
|
|
4
|
+
Watch a video the way a human would — turn it into a time-mapped transcript
|
|
5
|
+
with the on-screen visuals (slides, UI, diagrams, text) described inline, then
|
|
6
|
+
reason about it. Triggers: "watch this video", "şu videoyu izle", "videoyu
|
|
7
|
+
analiz et", "transkript çıkar", "video transcript", "bu videoda ne var", or any
|
|
8
|
+
time the user hands over a video file/link. Produces a curated
|
|
9
|
+
<video>.transcript.md NEXT TO the source video, then continues per use-case
|
|
10
|
+
(marketing teardown, FTE analysis, knowledge capture, …).
|
|
11
|
+
alwaysApply: false
|
|
12
|
+
ruleType: "Tool Workflow"
|
|
13
|
+
version: "1.0"
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# Video Watching
|
|
17
|
+
|
|
18
|
+
Turn a video into something an AI can fully reason about: a **time-mapped
|
|
19
|
+
transcript** with **what's on screen described inline at the same timestamps**.
|
|
20
|
+
The output is one self-contained file dropped **next to the source video**, so any
|
|
21
|
+
model (our cloud, this session, anything) can read the whole video without the
|
|
22
|
+
binary.
|
|
23
|
+
|
|
24
|
+
The clever part: **transcript first, then YOU decide which frames to look at.**
|
|
25
|
+
You don't blindly OCR every frame. You read the transcript, see where the speaker
|
|
26
|
+
references something visual ("as you can see here", "this screen", "the chart"),
|
|
27
|
+
look up that moment in `frames.json`, and open only those frames.
|
|
28
|
+
|
|
29
|
+
## The loop (do these in order)
|
|
30
|
+
|
|
31
|
+
### 0. Get the video
|
|
32
|
+
Ask for the path or link if not given. Local files work today. Remote links
|
|
33
|
+
(YouTube etc.) need `yt-dlp` (`brew install yt-dlp`) — the engine auto-downloads
|
|
34
|
+
if it's installed.
|
|
35
|
+
|
|
36
|
+
### 1. Run the engine (mechanical — one command)
|
|
37
|
+
```bash
|
|
38
|
+
.claude/skills/video-watching/scripts/transcribe.sh "/abs/path/to/video.mov"
|
|
39
|
+
# language auto-detects — no need to pass it. Override only if auto mislabels a
|
|
40
|
+
# short/ambiguous clip: ./transcribe.sh "/abs/path/clip.mp4" tr
|
|
41
|
+
```
|
|
42
|
+
This writes everything into `<video_dir>/<slug>.media/`:
|
|
43
|
+
- `transcript.srt` / `.json` / `.txt` — timestamped transcript (large-v3-turbo)
|
|
44
|
+
- `frames/anchor_first.jpg` / `anchor_last.jpg` — first + last frame, always captured
|
|
45
|
+
(short cut-heavy creatives carry the hook and CTA here; scene-detect misses both)
|
|
46
|
+
- `frames/scene_*.jpg` — frames at each scene change (slides/UI transitions)
|
|
47
|
+
- `frames/gap_*.jpg` — fill frames so no stretch > 10s goes unsampled (catches
|
|
48
|
+
static-but-important sections scene-detect misses: app demos, slides, CTAs)
|
|
49
|
+
- `frames.json` — **the index you read**: `[{file, t, at, type}]`, sorted by time,
|
|
50
|
+
near-duplicate timestamps collapsed (anchors always kept)
|
|
51
|
+
|
|
52
|
+
`audio.wav` is auto-deleted after transcription (it's a ~1.9MB/min whisper-only
|
|
53
|
+
intermediate). The whole `*.media/` dir is gitignored — it stays next to the video
|
|
54
|
+
as scratch but never enters the repo. Only `<slug>.transcript.md` is the keeper;
|
|
55
|
+
delete a `.media/` folder anytime to reclaim disk (re-running regenerates it).
|
|
56
|
+
|
|
57
|
+
### 2. Read the transcript
|
|
58
|
+
Read `transcript.srt`. It carries the spoken content with `[mm:ss]` timing. Form a
|
|
59
|
+
first-pass understanding of structure and topic.
|
|
60
|
+
|
|
61
|
+
### 3. Decide which frames you NEED — then view them
|
|
62
|
+
Read `frames.json`. For each moment where understanding depends on the visual —
|
|
63
|
+
the speaker points at something, a slide/UI/diagram/number is on screen, or the
|
|
64
|
+
transcript is ambiguous without the picture — pick the frame whose `at` is closest
|
|
65
|
+
to that moment and **open it with the Read tool** (it renders images). Don't open
|
|
66
|
+
all of them; open the ones that carry information. Talking-head stretches with
|
|
67
|
+
nothing on screen need no frame.
|
|
68
|
+
|
|
69
|
+
> Heuristic: the two `anchor` frames (first/last) almost always matter — the hook
|
|
70
|
+
> and the CTA. Every `scene` frame is a candidate (the picture changed for a
|
|
71
|
+
> reason). `gap` frames cover static stretches scene-detect skipped — often the
|
|
72
|
+
> most informative part (an app demo or slide that doesn't "move"), so check them.
|
|
73
|
+
|
|
74
|
+
**Need a frame the index doesn't have? Grab it on demand.** Scene-detect fires on
|
|
75
|
+
motion, not on meaning — on fast-cut video the most informative moment often sits
|
|
76
|
+
between the indexed frames. When the VO points at something ("look at this
|
|
77
|
+
screen", a number, a result) and no indexed frame lands there, fetch that exact
|
|
78
|
+
second yourself:
|
|
79
|
+
```bash
|
|
80
|
+
ffmpeg -y -loglevel error -ss <seconds> -i "<video>" -frames:v 1 -q:v 3 \
|
|
81
|
+
"<slug>.media/frames/grab_<mmss>.jpg"
|
|
82
|
+
```
|
|
83
|
+
This is the whole point of transcript-first: the frame set is yours to extend, not
|
|
84
|
+
a fixed dump you're stuck with.
|
|
85
|
+
|
|
86
|
+
### 4. Write the curated transcript NEXT TO the video
|
|
87
|
+
Write `<video_dir>/<slug>.transcript.md` — the deliverable. One timeline, voice and
|
|
88
|
+
visuals interleaved, every line timestamped:
|
|
89
|
+
|
|
90
|
+
```markdown
|
|
91
|
+
# <Video Title>
|
|
92
|
+
source: <path or URL> · duration: <mm:ss> · transcribed: large-v3-turbo
|
|
93
|
+
|
|
94
|
+
## Summary
|
|
95
|
+
2–4 sentences: what this video is and what it's for.
|
|
96
|
+
|
|
97
|
+
## Timeline
|
|
98
|
+
- **[00:00]** <what's said>
|
|
99
|
+
- **[00:06]** 🖼 <what's on screen — slide title, UI state, chart, on-screen text>
|
|
100
|
+
- **[00:12]** <what's said> … 🖼 <visual if relevant>
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
## Visual notes
|
|
104
|
+
- **[00:06]** Slide: "Funnel → Billing → Gateway → PSP" (4 layers)
|
|
105
|
+
- ...
|
|
106
|
+
```
|
|
107
|
+
Rules: never invent content not in transcript or frames; mark anything uncertain;
|
|
108
|
+
keep the source path in the header so the file stands alone.
|
|
109
|
+
|
|
110
|
+
### 5. Hand off to the use-case
|
|
111
|
+
Once the `.transcript.md` exists you know the video cold. Tell the user it's ready
|
|
112
|
+
and ask what they want from it — the answer is domain-specific. Triage to the
|
|
113
|
+
relevant dreamcontext skill (per the skill-triage rule) and load it:
|
|
114
|
+
- **Marketing video** → teardown, hook/CTA analysis, "we could do X" (→ `growth` / `meta-marketing` skills)
|
|
115
|
+
- **First-time-experience / app demo** → reconstruct the FTE flow, friction points (→ `design` / `onboarding-design`)
|
|
116
|
+
- **Knowledge / training** → if it should persist for the project, hand the transcript to `dreamcontext knowledge` so it becomes durable context
|
|
117
|
+
Don't guess the use-case — let the user direct it.
|
|
118
|
+
|
|
119
|
+
## What this skill is NOT
|
|
120
|
+
- Not a knowledge-base writer or ingestion pipeline. This skill produces the
|
|
121
|
+
transcript artifact; persisting it (chunking, embedding, ingest into a project's
|
|
122
|
+
long-term memory) is a separate, downstream concern — keep that boundary clean.
|
|
123
|
+
- Not an auto-summarizer that skips the frames. The visual pass is the point —
|
|
124
|
+
that's what separates "watched the video" from "read the subtitles".
|
|
125
|
+
|
|
126
|
+
## Setup (once per machine)
|
|
127
|
+
```bash
|
|
128
|
+
brew install whisper-cpp ffmpeg # yt-dlp too, only for remote links
|
|
129
|
+
# model: the engine prefers large-v3-turbo, falls back to large-v3 then medium.
|
|
130
|
+
# pull turbo if missing:
|
|
131
|
+
# curl -L -o ~/.cache/whisper.cpp/models/ggml-large-v3-turbo.bin \
|
|
132
|
+
# https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo.bin
|
|
133
|
+
```
|
|
134
|
+
`build_frame_index.py` is stdlib-only — no venv needed. Language **auto-detects**
|
|
135
|
+
(whisper); only pass a code (`tr`, `en`) to force it on a short/ambiguous clip.
|
|
136
|
+
|
|
137
|
+
## Files
|
|
138
|
+
```
|
|
139
|
+
.claude/skills/video-watching/
|
|
140
|
+
├── SKILL.md ← you are here
|
|
141
|
+
└── scripts/
|
|
142
|
+
├── transcribe.sh ← video → transcript + frames + frames.json (the engine)
|
|
143
|
+
├── gap_fill.py ← timestamps to fill scene-detect gaps (> MAX_GAP)
|
|
144
|
+
└── build_frame_index.py ← frames/*.jpg → frames.json (pts_time index)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Tuning
|
|
148
|
+
- Too few frames on a slide-heavy video? `SCENE_THRESHOLD=0.1 ./transcribe.sh …`
|
|
149
|
+
- Long static lecture over-sampled? Raise `MAX_GAP=20 ./transcribe.sh …` (default 10s).
|
|
150
|
+
- Capturing too many near-identical frames? `DEDUPE_SEC=1.0 ./transcribe.sh …`
|
|
151
|
+
- Force a model: `WHISPER_MODEL=/abs/ggml-large-v3.bin ./transcribe.sh …`
|