rvlib-shipkit 2.2.0 → 2.4.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 (140) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/package.json +1 -1
  3. package/src/agent-time/mask.ts +43 -0
  4. package/src/agent-time/render.ts +187 -0
  5. package/src/agent-time/report.ts +335 -0
  6. package/src/agent-time/shape.ts +271 -0
  7. package/src/agent-time/signals.ts +84 -0
  8. package/src/agent-time/store.ts +379 -0
  9. package/src/agent-time/timeline.ts +218 -0
  10. package/src/agent-time/turns.ts +135 -0
  11. package/src/assets/kits/identity-kit.ts +5 -2
  12. package/src/cli/cmd_Commands.ts +9 -0
  13. package/src/cli/cmd_Review.ts +974 -0
  14. package/src/cli/cmd_Sound.ts +209 -0
  15. package/src/cli/cmd_Time.ts +112 -0
  16. package/src/cli/cmd_Trailer.ts +264 -6
  17. package/src/cli/kit/verb.ts +22 -4
  18. package/src/cli.ts +8 -4
  19. package/src/collections/collection-types.ts +6 -1
  20. package/src/collections/providers.ts +3 -0
  21. package/src/collections/review.ts +70 -0
  22. package/src/core/app-icon.ts +10 -6
  23. package/src/core/layout.ts +2 -0
  24. package/src/reflect/prompt-corpus.ts +15 -119
  25. package/src/reflect/transcript.ts +254 -0
  26. package/src/repo-config.ts +3 -0
  27. package/src/review/anim.ts +228 -0
  28. package/src/review/auto.ts +141 -0
  29. package/src/review/format.ts +158 -0
  30. package/src/review/http.ts +23 -0
  31. package/src/review/marks.ts +43 -0
  32. package/src/review/media.ts +45 -0
  33. package/src/review/model.ts +896 -0
  34. package/src/review/project.ts +126 -0
  35. package/src/review/role.ts +48 -0
  36. package/src/review/routes.ts +239 -0
  37. package/src/review/store.ts +1011 -0
  38. package/src/review/threads.ts +237 -0
  39. package/src/review/trailer-desk.ts +385 -0
  40. package/src/review/trailer-model.ts +183 -0
  41. package/src/review/trailer-routes.ts +136 -0
  42. package/src/review/unit.ts +183 -0
  43. package/src/review/watchers.ts +54 -0
  44. package/src/serve/embed.ts +35 -0
  45. package/src/serve/hub-app/activity-view.tsx +1 -1
  46. package/src/serve/hub-app/activity.css +0 -4
  47. package/src/serve/hub-app/admin-svc.ts +1 -1
  48. package/src/serve/hub-app/admin-view.tsx +1 -1
  49. package/src/serve/hub-app/dock-view.tsx +5 -4
  50. package/src/serve/hub-app/hash-route.ts +36 -1
  51. package/src/serve/hub-app/header-bar.tsx +3 -1
  52. package/src/serve/hub-app/home-st.ts +17 -3
  53. package/src/serve/hub-app/home-strip.tsx +3 -2
  54. package/src/serve/hub-app/home.html +1 -0
  55. package/src/serve/hub-app/hub-app.tsx +80 -11
  56. package/src/serve/hub-app/hub.css +83 -14
  57. package/src/serve/hub-app/icons.tsx +15 -0
  58. package/src/serve/hub-app/index.html +1 -0
  59. package/src/serve/hub-app/insights-view.tsx +4 -3
  60. package/src/serve/hub-app/keys.ts +14 -10
  61. package/src/serve/hub-app/layout-st.ts +63 -2
  62. package/src/serve/hub-app/md.ts +32 -0
  63. package/src/serve/hub-app/planned-view.tsx +3 -2
  64. package/src/serve/hub-app/planned.css +0 -1
  65. package/src/serve/hub-app/project-list.css +26 -6
  66. package/src/serve/hub-app/project-list.tsx +46 -10
  67. package/src/serve/hub-app/projects-svc.ts +7 -2
  68. package/src/serve/hub-app/repo-sections.ts +15 -3
  69. package/src/serve/hub-app/repo-st.ts +61 -4
  70. package/src/serve/hub-app/review/clip-st.ts +258 -0
  71. package/src/serve/hub-app/review/kind-view.tsx +240 -0
  72. package/src/serve/hub-app/review/kit.tsx +113 -0
  73. package/src/serve/hub-app/review/review-answer.tsx +269 -0
  74. package/src/serve/hub-app/review/review-board.tsx +46 -0
  75. package/src/serve/hub-app/review/review-list.tsx +156 -0
  76. package/src/serve/hub-app/review/review-page.tsx +96 -0
  77. package/src/serve/hub-app/review/review-st.ts +902 -0
  78. package/src/serve/hub-app/review/review-views.tsx +648 -0
  79. package/src/serve/hub-app/review/trailer-page.tsx +416 -0
  80. package/src/serve/hub-app/review/trailer-st.ts +602 -0
  81. package/src/serve/hub-app/review/trailer-timeline.tsx +207 -0
  82. package/src/serve/hub-app/review.css +389 -0
  83. package/src/serve/hub-app/theme-st.ts +56 -0
  84. package/src/serve/hub-app/tree-build.ts +4 -0
  85. package/src/serve/hub-app/tree-row.tsx +1 -1
  86. package/src/serve/hub-app/tree-svc.ts +4 -0
  87. package/src/serve/hub-app/wiki-st.ts +190 -0
  88. package/src/serve/hub-app/wikis-view.tsx +134 -5
  89. package/src/serve/hub-runtime.ts +9 -7
  90. package/src/serve/project-gutter-route.ts +25 -1
  91. package/src/serve/project-gutter.ts +1 -1
  92. package/src/serve/same-origin.ts +5 -1
  93. package/src/serve/serve-hub.ts +15 -0
  94. package/src/shipkit-files.ts +9 -5
  95. package/src/shot/client.ts +17 -5
  96. package/src/shot/shooter.ts +19 -3
  97. package/src/shot/shot-ask.ts +9 -1
  98. package/src/shot/stepped-clip.ts +53 -0
  99. package/src/trailer/adapter.ts +90 -6
  100. package/src/trailer/build.ts +459 -59
  101. package/src/trailer/ffmpeg.ts +22 -0
  102. package/src/trailer/fonts.ts +55 -0
  103. package/src/trailer/footage.ts +112 -43
  104. package/src/trailer/grammar.ts +864 -127
  105. package/src/trailer/graph.ts +308 -49
  106. package/src/trailer/holes.ts +72 -0
  107. package/src/trailer/load-unit.ts +20 -3
  108. package/src/trailer/look.ts +20 -3
  109. package/src/trailer/motion.ts +207 -0
  110. package/src/trailer/passage.ts +129 -0
  111. package/src/trailer/plan.ts +82 -23
  112. package/src/trailer/prune.ts +204 -0
  113. package/src/trailer/score/compose.ts +256 -0
  114. package/src/trailer/score/dsp.ts +272 -0
  115. package/src/trailer/score/gm-render.ts +43 -0
  116. package/src/trailer/score/gm.ts +90 -0
  117. package/src/trailer/score/midi.ts +143 -0
  118. package/src/trailer/score/palette.ts +188 -0
  119. package/src/trailer/score/pattern.ts +98 -0
  120. package/src/trailer/score/score.ts +228 -0
  121. package/src/trailer/score/sound.ts +123 -0
  122. package/src/trailer/score/synth.ts +383 -0
  123. package/src/trailer/sound/audio.ts +148 -0
  124. package/src/trailer/sound/brand.ts +42 -0
  125. package/src/trailer/sound/comfy.ts +38 -0
  126. package/src/trailer/sound/eleven.ts +72 -0
  127. package/src/trailer/sound/fluid.ts +71 -0
  128. package/src/trailer/sound/gen.ts +139 -0
  129. package/src/trailer/sound/index.ts +60 -0
  130. package/src/trailer/sound/presets.ts +69 -0
  131. package/src/trailer/sound/run.ts +189 -0
  132. package/src/trailer/states.ts +76 -0
  133. package/src/trailer/stitch.ts +92 -9
  134. package/src/trailer/titles.ts +471 -150
  135. package/src/trailer/url.ts +18 -3
  136. package/src/trailer/vocab.ts +34 -4
  137. package/src/trailer/words.ts +275 -0
  138. package/src/wiki/graph-mount.ts +3 -0
  139. package/src/wiki/graph-page.ts +45 -9
  140. package/src/wiki/graph-pages.ts +128 -0
@@ -0,0 +1,76 @@
1
+ // what each frame of a take holds that a cut must not land on (wiki build/trailer.md, Film lines): a film command may
2
+ // print `states <rle>`, each run of one state and its frames (`dialog*12,-*30`, `-` a plain frame, parts joined by `+`).
3
+ // The stamp keeps it; the timeline names a shot whose edges cut into a change. Pure: no page, no file
4
+
5
+ /** a frame with nothing on it a cut must mind */
6
+ export const FREE = '-'
7
+
8
+ /** `dialog*12,-*30`: each run of one state and its length */
9
+ export function encodeStates(states: readonly string[]): string {
10
+ const runs: string[] = []
11
+ let i = 0
12
+ while (i < states.length) {
13
+ let j = i
14
+ while (j < states.length && states[j] === states[i]) j++
15
+ runs.push(`${states[i]}*${j - i}`)
16
+ i = j
17
+ }
18
+ return runs.join(',')
19
+ }
20
+
21
+ export function decodeStates(text: string): string[] {
22
+ const out: string[] = []
23
+ for (const run of text.split(',').filter(Boolean)) {
24
+ const at = run.lastIndexOf('*')
25
+ const n = Number(run.slice(at + 1))
26
+ if (at <= 0 || !Number.isInteger(n) || n < 1) throw new Error(`states: ${run} is no <state>*<frames>`)
27
+ for (let k = 0; k < n; k++) out.push(run.slice(0, at))
28
+ }
29
+ return out
30
+ }
31
+
32
+ /** the states a film's output says, its `states <rle>` word (on its own line or after ` · `); null when it says none */
33
+ export function statesOf(said: string): string | null {
34
+ return /(?:^|\s·\s|\n)states (\S+)/.exec(said)?.[1] ?? null
35
+ }
36
+
37
+ /** frames at each edge of a shot where a change shows: a box rising or a change still playing reads as a flash */
38
+ export const EDGE_FRAMES = 6
39
+
40
+ export interface StatesRule {
41
+ /** parts that open on their own way in, never a change a cut minds */
42
+ ignore?: readonly string[]
43
+ /** parts that are a change under way: a shot never opens or ends on one */
44
+ underWay?: readonly string[]
45
+ }
46
+
47
+ /** what changes within EDGE_FRAMES of the shot's first or last frame (`first`, `last`: frames of the take), and a change
48
+ * under way the shot opens or ends on: one line each, none when the edges are settled */
49
+ export function edgeChanges(states: readonly string[], first: number, last: number, rule: StatesRule = {}): string[] {
50
+ const ignore = new Set(rule.ignore ?? [])
51
+ const parts = (s: string): string[] => (s === FREE ? [] : s.split('+').filter((x) => !ignore.has(x)))
52
+ const change = (from: string, to: string): string => {
53
+ const a = parts(from)
54
+ const b = parts(to)
55
+ return [...b.filter((x) => !a.includes(x)).map((x) => `${x} in`), ...a.filter((x) => !b.includes(x)).map((x) => `${x} out`)].join(', ')
56
+ }
57
+ const end = Math.min(last, states.length - 1)
58
+ if (first > end || first < 0) return []
59
+ const out: string[] = []
60
+ const seen = new Set<number>()
61
+ const at = (i: number): void => {
62
+ if (seen.has(i)) return
63
+ seen.add(i)
64
+ const after = i - first
65
+ const before = end - i + 1
66
+ out.push(`${change(states[i - 1]!, states[i]!)} ${after <= before ? `${after} frame${after === 1 ? '' : 's'} after its start` : `${before} frame${before === 1 ? '' : 's'} before its end`}`)
67
+ }
68
+ const moved = (i: number): boolean => change(states[i - 1]!, states[i]!) !== ''
69
+ for (let i = first + 1; i <= Math.min(end, first + EDGE_FRAMES); i++) if (moved(i)) at(i)
70
+ for (let i = Math.max(first + 1, end - EDGE_FRAMES + 1); i <= end; i++) if (moved(i)) at(i)
71
+ for (const u of rule.underWay ?? []) {
72
+ if (parts(states[first]!).includes(u)) out.unshift(`opens on a ${u}`)
73
+ if (end > first && parts(states[end]!).includes(u)) out.push(`ends on a ${u}`)
74
+ }
75
+ return out
76
+ }
@@ -5,8 +5,9 @@ import { existsSync, mkdirSync, renameSync, rmSync, statSync, utimesSync, writeF
5
5
  import { availableParallelism } from 'node:os'
6
6
  import { dirname, join } from 'node:path'
7
7
  import sharp from 'sharp'
8
- import { ffmpeg } from './ffmpeg.ts'
9
- import { type Encoder, mixArgs, pieceArgs, type StitchInput, THUMB_W } from './graph.ts'
8
+ import { ffmpeg, loudnessKept } from './ffmpeg.ts'
9
+ import { type Encoder, GRID_RADIUS, gridLayout, MIX_TRUE_PEAK, mixArgs, pieceArgs, type SplitMasks, splitPane, type StitchInput, THUMB_W } from './graph.ts'
10
+ import type { TrailerLook } from './look.ts'
10
11
  import { type Piece, pieces } from './plan.ts'
11
12
 
12
13
  /** bump when the stitch changes what the same arguments make */
@@ -50,6 +51,67 @@ export async function cachedAs(dir: string, stem: string, key: string, exts: str
50
51
  return { files, made: true }
51
52
  }
52
53
 
54
+ const n3 = (x: number): string => (+x.toFixed(4)).toString()
55
+
56
+ /** a drawing's own temporary file beside `file`: two builds drawing one piece at once each rename their own */
57
+ const partOf = (file: string): string => `${file}.${process.pid}-${Math.random().toString(36).slice(2, 8)}.part.png`
58
+
59
+ async function drawSvg(file: string, svg: string): Promise<void> {
60
+ mkdirSync(dirname(file), { recursive: true })
61
+ const part = partOf(file)
62
+ await sharp(Buffer.from(svg)).png().toFile(part)
63
+ renameSync(part, file)
64
+ }
65
+
66
+ const colourKey = (look: Pick<TrailerLook, 'ground' | 'light' | 'edge'>): string => new Bun.CryptoHasher('sha1').update(JSON.stringify([look.ground, look.light, look.edge])).digest('hex').slice(0, 8)
67
+
68
+ /** a grid's frame for `n` cells: the look's ground with a hole per cell, its corners rounded and rimmed in its light;
69
+ * laid over the cells. Drawn once per layout and look and kept, so its file never changes a piece's key */
70
+ export async function gridMask(dir: string, n: number, size: readonly [number, number], look: Pick<TrailerLook, 'ground' | 'light' | 'edge'>): Promise<string> {
71
+ const [W, H] = size
72
+ const file = join(dir, 'grid', `mask-${STITCH_VERSION}-${n}-${W}x${H}-${colourKey(look)}.png`)
73
+ if (existsSync(file)) return file
74
+ const r = H * GRID_RADIUS
75
+ const rim = Math.max(1.5, H * 0.0028)
76
+ const cells = gridLayout(n, W, H)
77
+ const holes = cells.map((c) => `<rect x="${c.x}" y="${c.y}" width="${c.w}" height="${c.h}" rx="${n3(r)}" fill="black"/>`).join('')
78
+ const rims = cells.map((c) => `<rect x="${c.x + rim / 2}" y="${c.y + rim / 2}" width="${c.w - rim}" height="${c.h - rim}" rx="${n3(r - rim / 2)}" fill="none" stroke="${look.light}" stroke-opacity="0.85" stroke-width="${n3(rim)}"/>`).join('')
79
+ await drawSvg(
80
+ file,
81
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}">
82
+ <defs><radialGradient id="g" cx="50%" cy="45%" r="75%"><stop offset="0" stop-color="${look.ground[0]}"/><stop offset="1" stop-color="${look.ground[1]}"/></radialGradient>
83
+ <mask id="m"><rect width="${W}" height="${H}" fill="white"/>${holes}</mask></defs>
84
+ <rect width="${W}" height="${H}" fill="url(#g)" mask="url(#m)"/>${rims}</svg>`,
85
+ )
86
+ return file
87
+ }
88
+
89
+ /** a split's panes and seams for `n` panes at `angle`: each pane's shape, and the seams, a line of the look's light over
90
+ * a soft shadow of its edge, its width a 150th of the frame's height. Drawn once and kept like the grid's frame */
91
+ export async function splitMasks(dir: string, n: number, angle: number, size: readonly [number, number], look: Pick<TrailerLook, 'ground' | 'light' | 'edge'>): Promise<SplitMasks> {
92
+ const [W, H] = size
93
+ const stem = join(dir, 'split', `${STITCH_VERSION}-${n}-${n3(angle)}-${W}x${H}`)
94
+ const panes = Array.from({ length: n }, (_, j) => `${stem}-pane${j}.png`)
95
+ const seams = `${stem}-seams-${colourKey(look)}.png`
96
+ if ([...panes, seams].every((f) => existsSync(f))) return { panes, seams }
97
+ const poly = (pts: [number, number][]): string => pts.map(([x, y]) => `${n3(x)},${n3(y)}`).join(' ')
98
+ for (let j = 0; j < n; j++)
99
+ if (!existsSync(panes[j]!))
100
+ await drawSvg(panes[j]!, `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}"><rect width="${W}" height="${H}" fill="black"/><polygon points="${poly(splitPane(n, j, angle, W, H).points)}" fill="white"/></svg>`)
101
+ const t = Math.tan((angle * Math.PI) / 180)
102
+ // each seam drawn from well above the frame to well below it, so it crosses it whole
103
+ const lines = Array.from({ length: n - 1 }, (_, i) => (W * (i + 1)) / n)
104
+ const wLine = Math.max(2, H / 150)
105
+ const seg = (stroke: string, width: number): string =>
106
+ lines.map((x) => `<line x1="${n3(x + 1.5 * H * t)}" y1="${-H}" x2="${n3(x - 1.5 * H * t)}" y2="${2 * H}" stroke="${stroke}" stroke-width="${n3(width)}"/>`).join('')
107
+ await drawSvg(
108
+ seams,
109
+ `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}"><defs><filter id="s" x="-50%" y="-50%" width="200%" height="200%"><feGaussianBlur stdDeviation="${n3(H / 90)}"/></filter></defs>` +
110
+ `<g filter="url(#s)" opacity="0.55">${seg(look.edge, wLine * 4)}</g>${seg(look.light, wLine)}</svg>`,
111
+ )
112
+ return { panes, seams }
113
+ }
114
+
53
115
  /** runs the jobs, at most `n` at once */
54
116
  export async function pool<T>(jobs: (() => Promise<T>)[], n: number): Promise<T[]> {
55
117
  const out: T[] = []
@@ -95,19 +157,31 @@ async function renderPieces(a: StitchRun, ps: Piece[]): Promise<Stitched['pieces
95
157
  )
96
158
  }
97
159
 
98
- async function renderMix(a: StitchRun): Promise<{ file: string; made: boolean }> {
99
- const { files, made } = await cached(join(a.cache, 'mix'), `mix-${a.t.name}`, ['m4a'], ([out]) => mixArgs(a, out!))
100
- return { file: files[0]!, made }
160
+ /** the mix, its true peak held under MIX_TRUE_PEAK after the aac encode: the encoder's band limit rings over a limited
161
+ * harsh sound (a square wave measured +2.1 dB), so a mix that overshoots is made again that much lower */
162
+ export async function renderMix(a: Pick<StitchRun, 't' | 'music' | 'sfx' | 'voices' | 'cache'>): Promise<{ file: string; made: boolean }> {
163
+ let trim = 0
164
+ let made = false
165
+ for (let pass = 0; ; pass++) {
166
+ const r = await cached(join(a.cache, 'mix'), `mix-${a.t.name}`, ['m4a'], ([out]) => mixArgs(a, out!, trim))
167
+ made ||= r.made
168
+ const { truePeak } = await loudnessKept(r.files[0]!)
169
+ if (truePeak <= MIX_TRUE_PEAK || pass === 2) return { file: r.files[0]!, made }
170
+ trim = +(trim - (truePeak - MIX_TRUE_PEAK) - 0.1).toFixed(2)
171
+ }
101
172
  }
102
173
 
103
- /** the pieces joined as they are, no encode, with the sound beside them */
104
- async function joinPieces(files: string[], sound: string | null, out: string): Promise<void> {
174
+ /** the pieces joined as they are, no encode, with the sound beside them: the whole mix copied, or a window of it (a
175
+ * section alone) cut on its samples, so encoded again */
176
+ async function joinPieces(files: string[], sound: { file: string; start?: number; seconds?: number } | null, out: string): Promise<void> {
105
177
  mkdirSync(dirname(out), { recursive: true })
106
178
  const list = `${out}.list.txt`
107
179
  writeFileSync(list, files.map((f) => `file '${f.replaceAll("'", "'\\''")}'`).join('\n'))
108
180
  const part = out.replace(/(\.\w+)$/, '.part$1')
181
+ const audio = sound ? [...(sound.start !== undefined ? ['-ss', n3(sound.start), '-t', n3(sound.seconds ?? 0)] : []), '-i', sound.file] : []
182
+ const acodec = sound?.start !== undefined ? ['-c:a', 'aac', '-b:a', '192k'] : ['-c:a', 'copy']
109
183
  try {
110
- await ffmpeg(['-f', 'concat', '-safe', '0', '-i', list, ...(sound ? ['-i', sound] : []), '-map', '0:v', ...(sound ? ['-map', '1:a', '-c:a', 'copy'] : []), '-c:v', 'copy', '-movflags', '+faststart', part], { nice: 10 })
184
+ await ffmpeg(['-f', 'concat', '-safe', '0', '-i', list, ...audio, '-map', '0:v', ...(sound ? ['-map', '1:a', ...acodec] : []), '-c:v', 'copy', '-movflags', '+faststart', part], { nice: 10 })
111
185
  renameSync(part, out)
112
186
  } finally {
113
187
  rmSync(list, { force: true })
@@ -118,7 +192,16 @@ async function joinPieces(files: string[], sound: string | null, out: string): P
118
192
  /** the whole cut: every piece (rendered where the cache misses it), joined, with its sound */
119
193
  export async function stitch(a: StitchRun): Promise<Stitched> {
120
194
  const [ps, mix] = await Promise.all([renderPieces(a, pieces(a.t)), a.silent ? null : renderMix(a)])
121
- await joinPieces(ps.map((p) => p.file), mix?.file ?? null, a.out)
195
+ await joinPieces(ps.map((p) => p.file), mix ? { file: mix.file } : null, a.out)
196
+ return { pieces: ps, mixed: mix?.made ?? false, mix: mix?.file ?? null }
197
+ }
198
+
199
+ /** one piece alone (a section, `start` the shots before the first), with its window of the sound */
200
+ export async function stitchPiece(a: StitchRun, name: string): Promise<Stitched> {
201
+ const p = pieces(a.t).find((x) => x.name === name)
202
+ if (!p) throw new Error(`no section ${name}: ${pieces(a.t).map((x) => x.name).join(', ')}`)
203
+ const [ps, mix] = await Promise.all([renderPieces(a, [p]), a.silent ? null : renderMix(a)])
204
+ await joinPieces(ps.map((x) => x.file), mix && { file: mix.file, start: p.from / a.t.fps, seconds: (p.to - p.from) / a.t.fps }, a.out)
122
205
  return { pieces: ps, mixed: mix?.made ?? false, mix: mix?.file ?? null }
123
206
  }
124
207