rvlib-shipkit 2.3.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.
- package/CHANGELOG.md +11 -2
- package/package.json +1 -1
- package/src/assets/kits/identity-kit.ts +5 -2
- package/src/cli/cmd_Commands.ts +4 -0
- package/src/cli/cmd_Review.ts +36 -10
- package/src/cli/cmd_Sound.ts +209 -0
- package/src/cli/cmd_Trailer.ts +7 -2
- package/src/cli/kit/verb.ts +11 -3
- package/src/cli.ts +2 -0
- package/src/core/app-icon.ts +10 -6
- package/src/repo-config.ts +3 -0
- package/src/review/format.ts +11 -3
- package/src/review/model.ts +47 -5
- package/src/review/project.ts +20 -4
- package/src/review/routes.ts +12 -1
- package/src/review/store.ts +15 -6
- package/src/review/unit.ts +19 -1
- package/src/serve/hub-app/admin-svc.ts +1 -1
- package/src/serve/hub-app/admin-view.tsx +1 -1
- package/src/serve/hub-app/hash-route.ts +12 -4
- package/src/serve/hub-app/home-st.ts +7 -1
- package/src/serve/hub-app/hub.css +38 -1
- package/src/serve/hub-app/icons.tsx +6 -0
- package/src/serve/hub-app/layout-st.ts +2 -2
- package/src/serve/hub-app/md.ts +32 -0
- package/src/serve/hub-app/project-list.css +9 -7
- package/src/serve/hub-app/project-list.tsx +5 -2
- package/src/serve/hub-app/projects-svc.ts +3 -2
- package/src/serve/hub-app/repo-st.ts +22 -7
- package/src/serve/hub-app/review/clip-st.ts +57 -7
- package/src/serve/hub-app/review/kind-view.tsx +78 -9
- package/src/serve/hub-app/review/review-answer.tsx +83 -53
- package/src/serve/hub-app/review/review-page.tsx +9 -14
- package/src/serve/hub-app/review/review-st.ts +141 -18
- package/src/serve/hub-app/review/review-views.tsx +154 -9
- package/src/serve/hub-app/review.css +70 -10
- package/src/serve/hub-app/wiki-st.ts +190 -0
- package/src/serve/hub-app/wikis-view.tsx +134 -5
- package/src/serve/hub-runtime.ts +4 -4
- package/src/serve/project-gutter-route.ts +25 -1
- package/src/serve/project-gutter.ts +1 -1
- package/src/serve/serve-hub.ts +7 -0
- package/src/shot/client.ts +17 -5
- package/src/shot/shooter.ts +19 -3
- package/src/shot/shot-ask.ts +9 -1
- package/src/shot/stepped-clip.ts +53 -0
- package/src/trailer/adapter.ts +33 -4
- package/src/trailer/build.ts +170 -29
- package/src/trailer/fonts.ts +55 -0
- package/src/trailer/footage.ts +4 -4
- package/src/trailer/grammar.ts +429 -67
- package/src/trailer/graph.ts +130 -56
- package/src/trailer/look.ts +3 -0
- package/src/trailer/motion.ts +207 -0
- package/src/trailer/plan.ts +2 -2
- package/src/trailer/prune.ts +13 -2
- package/src/trailer/score/compose.ts +18 -2
- package/src/trailer/score/gm-render.ts +43 -0
- package/src/trailer/score/gm.ts +90 -0
- package/src/trailer/score/midi.ts +143 -0
- package/src/trailer/score/pattern.ts +13 -6
- package/src/trailer/score/score.ts +28 -14
- package/src/trailer/score/sound.ts +12 -1
- package/src/trailer/score/synth.ts +24 -2
- package/src/trailer/sound/audio.ts +148 -0
- package/src/trailer/sound/brand.ts +42 -0
- package/src/trailer/sound/comfy.ts +38 -0
- package/src/trailer/sound/eleven.ts +72 -0
- package/src/trailer/sound/fluid.ts +71 -0
- package/src/trailer/sound/gen.ts +139 -0
- package/src/trailer/sound/index.ts +60 -0
- package/src/trailer/sound/presets.ts +69 -0
- package/src/trailer/sound/run.ts +189 -0
- package/src/trailer/titles.ts +451 -235
- package/src/trailer/url.ts +18 -3
- package/src/trailer/vocab.ts +12 -1
- package/src/trailer/words.ts +29 -8
- package/src/wiki/graph-mount.ts +3 -0
- package/src/wiki/graph-page.ts +45 -9
- package/src/wiki/graph-pages.ts +128 -0
package/CHANGELOG.md
CHANGED
|
@@ -15,8 +15,17 @@
|
|
|
15
15
|
- **`sk time` shows turn count, what makes agents slow when no command is.** Every call costs a model turn before it. The summary gives, for the main sessions and for the sub-agents, the model time before each call, the share of single read calls and the share sent in parallel. The sub-agent lines lead with a verdict (turn count or command speed, with a fix line when reads pile up one per turn), then their work split (model, tools, their own waits), run length p50 and p90, and calls per run. The `turns` view lists the runs that worked longest with their turns, read and parallel shares and model per turn, then the read shapes by the model time of the turns that issued them. A read is Read, Grep, Glob, or a Bash command of a read shape whose every part reads (a redirect to a file, `sed -i`, `find -delete` or `-exec`, `tee`, or a second command that writes or runs makes it not one), judged when a call is indexed and again when the rules change (`src/agent-time/turns.ts`). A call is parallel when it starts while the previous call still runs, within 2 s of its start. The model stretch before a call comes from the same sweep that cuts the wall. pkm-multi, every recorded session: sub-agents worked 905.4h, model 67%, 7.7 s of model before each of 283,821 calls, 51% single reads, 2.3% in parallel.
|
|
16
16
|
- **A message relayed into a sub-agent ends its park.** The harness writes "The coordinator sent a message while you were working" (or the user, or another session) into a sub-agent as an `isMeta` user entry with no peer origin, which the transcript reader dropped, so a sub-agent that handed back and sat for hours until someone wrote to it counted those hours as model: 352h of pkm-multi's sub-agent time. It is now a `message` event that counts as a prompt (idle past 30 minutes). The index version moves to 6, so every file is read again once.
|
|
17
17
|
|
|
18
|
-
##
|
|
19
|
-
|
|
18
|
+
## 2.4.0
|
|
19
|
+
|
|
20
|
+
- **The wiki preview opens on the index, beside the graph on the left, right or bottom.** A switch of three icons in the preview's head places it, kept per browser; buttons open the page in VS Code and copy its path. Refs read by kind: on demand (`→`, a link, a path in a code span, written `→ path` whichever the page used) in orange, loaded with the page (`@`) in blue and bold. A page that names `agent/spec.md` opens the agent/ graph's `spec.md`.
|
|
21
|
+
- **The wikis page searches and previews.** A search bar over the graph finds every page by title and full text (`/graph/<id>/search`), a dropdown the arrows walk; enter, or a click on a ball, opens the page in a resizable panel on the right (`/graph/<id>/page`), whose refs open their page and focus its ball. The graph follows the hub's theme. `serve.wikiGraph.dir` names the folder a custom wiki's node ids are relative to (default the repo root).
|
|
22
|
+
- **A choice card.** `sk review add --choice --view "voice A" a.flac --note "low" --view "voice B" b.flac`: every view is an option he scores (1 to 5 stars) and picks one of. His pick is an approval carrying `pick`; `inbox` and `watch` print `picked [voice B]` and each score. An approval with reactions or a pick is no longer counted as a plain one by `inbox`.
|
|
23
|
+
- **Options as thin rows.** A choice, and any card of two clips or sounds or more, shows its views one below the other: ▶, the label, a strip that scrubs it, the time, then stars and « pick » (a choice) or OK, fix, no (a group), ✎ a word, ⤢ the full view. Keys 1 to 9 play a row, ⇧1 to ⇧5 score it, P picks it. « rows | full » switches back to every view stacked.
|
|
24
|
+
- **One clip or sound plays at a time on the whole page.** Any audio or video that starts pauses the one playing (the review page, the trailer page, the atelier's rolls). A card's first clip loops alone; the others wait on their first frame.
|
|
25
|
+
- **The card's header reads.** The title, then one line: the list's own pill, the lane, when it was sent or answered, the commit, an id to copy. No subject row, no id row, no banner. `top` is one column from the top: the header and the verdicts, the views, then the answer. A link to a card on a phone opens the card.
|
|
26
|
+
- **An svg mark is a repo's icon.** `.shipkit/identity/icon.svg` comes right after `icon.png` for the project list's icon and for the app icon, which draws it on the identity plate like a png.
|
|
27
|
+
- **The hub's pages read from the left.** Every page card of a repo (admin, checks, tests, plan, git, when, processes, status, readme) and a review card's pictures start at the left of their pane instead of floating centred; a guard keeps it.
|
|
28
|
+
- **The rail draws bigger icons: each repo's app icon.** The sizes are 24, 32 and 40px (S, M, L) with 6px a side, the rail 36, 44 or 52px; the ask badge sits on the icon's top right corner. Folded, a row wears the repo's app icon, its mark on the identity plate the admin page previews, cropped to the plate (`appIcon` on `/api/projects.json`, `appIconPngs(…, 'plate')`, drawn once per mark and colours); a repo with no png mark keeps its plain icon. A repo closed in VS Code dims its name, never its icon.
|
|
20
29
|
- **The rail's icons come in three sizes.** Folded, the list's foot shows an S, M or L button: 18, 24 or 32px icons, the rail 44, 50 or 58px wide to fit, kept per browser (`hub:rail-icon`). M is the default.
|
|
21
30
|
- **The project list folds to an icon rail.** ⌘B, the fold button at the list's foot, or a drag of its separator below the list's min folds it to 44px of icons: the names, the ⌘ hints and the step hint hide, a badge rides its icon's corner, the hover card still names the row. A drag out or ⌘B again unfolds it to its kept width; the fold is kept per browser (`hub:list-rail`). The dock moves to ⌘⇧B.
|
|
22
31
|
- **⌘; opens the insights**, its filter focused, so a package name is one key and a word away.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"funding": "https://github.com/sponsors/rvion",
|
|
5
5
|
"description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
|
|
6
6
|
"license": "MIT",
|
|
@@ -16,6 +16,7 @@ import { dirname, join } from 'path'
|
|
|
16
16
|
import sharp from 'sharp'
|
|
17
17
|
import { defineKind, type CanvasGen, type ComfyGen, type Kind } from '../kit-config.ts'
|
|
18
18
|
import { HOUSE_FAMILIES, type TrailerLookInput } from '../../trailer/look.ts'
|
|
19
|
+
import type { TrailerSoundInput } from '../../trailer/sound/brand.ts'
|
|
19
20
|
|
|
20
21
|
export type Brand = {
|
|
21
22
|
name: string
|
|
@@ -31,8 +32,10 @@ export type Brand = {
|
|
|
31
32
|
* name/tagline right); the hook owns compositing the mark if it wants one */
|
|
32
33
|
drawOg?: (ctx: SKRSContext2D, brand: Brand, size: { w: number; h: number }) => void
|
|
33
34
|
/** the look every trailer of the repo wears (titles, card, face, logo);
|
|
34
|
-
* each value left out is derived from the palette (trailer/look.ts)
|
|
35
|
-
|
|
35
|
+
* each value left out is derived from the palette (trailer/look.ts);
|
|
36
|
+
* its sound section (`comfy`, `elevenKey`, `voice`, `voices`) makes the
|
|
37
|
+
* voice, sfx and music lines (trailer/sound/brand.ts) */
|
|
38
|
+
trailer?: TrailerLookInput & TrailerSoundInput
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export const FAVICON_SIZE = 512
|
package/src/cli/cmd_Commands.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type CommandRow = {
|
|
|
20
20
|
/** a filmable command's take version and length (build/trailer.md, Film lines) */
|
|
21
21
|
version?: number | string
|
|
22
22
|
length?: 'own'
|
|
23
|
+
words?: Record<string, unknown>
|
|
24
|
+
stamp?: string[]
|
|
23
25
|
/** listed on the essentials page */
|
|
24
26
|
essential?: Essential
|
|
25
27
|
/** what a kit row runs */
|
|
@@ -81,6 +83,8 @@ export function commandRows(plain: Definition[], detailed: Definition[] = [], bi
|
|
|
81
83
|
if (m.film != null) row.film = m.film
|
|
82
84
|
if (m.version != null) row.version = m.version
|
|
83
85
|
if (m.length != null) row.length = m.length
|
|
86
|
+
if (m.words != null) row.words = m.words
|
|
87
|
+
if (m.stamp != null) row.stamp = m.stamp
|
|
84
88
|
if (m.essential != null) row.essential = m.essential
|
|
85
89
|
const v = ROW_OF.get(c)
|
|
86
90
|
if (v != null) row.runs = runText(v.run)
|
package/src/cli/cmd_Review.ts
CHANGED
|
@@ -83,7 +83,7 @@ function tileOf(arg: string): { file: string; label: string } {
|
|
|
83
83
|
|
|
84
84
|
type ViewInput = import('../review/store.ts').ViewInput
|
|
85
85
|
|
|
86
|
-
/** `--view "<label>" a.png [b.png]` and `--gallery "<label>" a.png b.png …` out of the args */
|
|
86
|
+
/** `--view "<label>" a.png [b.png] [--note "<text>"]` and `--gallery "<label>" a.png b.png …` out of the args */
|
|
87
87
|
function takeViews(args: readonly string[]): { views: ViewInput[]; rest: string[] } {
|
|
88
88
|
const views: ViewInput[] = []
|
|
89
89
|
const rest: string[] = []
|
|
@@ -100,6 +100,13 @@ function takeViews(args: readonly string[]): { views: ViewInput[]; rest: string[
|
|
|
100
100
|
if (a === '--gallery') views.push({ label, gallery: files.map(tileOf) })
|
|
101
101
|
else views.push(files.length === 1 ? { label, image: files[0] } : { label, before: files[0], after: files[1] })
|
|
102
102
|
i += 1 + files.length
|
|
103
|
+
if (args[i + 1] === '--note') {
|
|
104
|
+
const note = args[i + 2]
|
|
105
|
+
if (note === undefined || note.startsWith('--')) throw new Error(`--view "${label}" … --note "<text>": a line on that view`)
|
|
106
|
+
const v = views[views.length - 1]
|
|
107
|
+
if (v) v.note = note
|
|
108
|
+
i += 2
|
|
109
|
+
}
|
|
103
110
|
}
|
|
104
111
|
return { views, rest }
|
|
105
112
|
}
|
|
@@ -139,7 +146,8 @@ const ADD_FLAGS = `flags:
|
|
|
139
146
|
--audio <sound.wav> a sound: a waveform and a player (wav, mp3, flac, ogg, opus, m4a)
|
|
140
147
|
--before <a.png> --after <b.png> a before and after: two images, two clips of one size, or two sounds
|
|
141
148
|
--label "<label>" the first view's label
|
|
142
|
-
--view "<label>" <a.png> [<b.png>] another view of the same subject (repeat it)
|
|
149
|
+
--view "<label>" <a.png> [<b.png>] another view of the same subject (repeat it); --note "<text>" right after it: a line on that view
|
|
150
|
+
--choice every view is an option he scores and picks one of (two --view or more)
|
|
143
151
|
--gallery "<label>" <a.png> … a grid of small images; "<tile label>=<c.png>" names a tile (repeat it)
|
|
144
152
|
--topic <topic> what part of the project it is about (sk review topics)
|
|
145
153
|
--subject <kind> what is judged (default shot; the repo's unit names its kinds)
|
|
@@ -150,6 +158,19 @@ const ADD_FLAGS = `flags:
|
|
|
150
158
|
--keeps "<what stays>" with --supersedes on an approved look: what of it stays
|
|
151
159
|
--new --why "<reason>" a new card beside an active one of the same subject`
|
|
152
160
|
|
|
161
|
+
/** the help of add or add-view, then the flags this repo's review unit adds (any kind's, then each kind's under
|
|
162
|
+
* `--subject <kind>`); a repo with no unit, or one that does not load, says so on one line */
|
|
163
|
+
async function helpWithUnit(context: Ctx, usage: string, kinds: boolean): Promise<number> {
|
|
164
|
+
context.stdout.write(usage)
|
|
165
|
+
await run(context, (h) => {
|
|
166
|
+
const lines = Object.entries(h.unit?.flags ?? {}).map(([k, d]) => ` --${k} ${d}`)
|
|
167
|
+
if (kinds) for (const [name, k] of Object.entries(h.unit?.kinds ?? {})) for (const [f, d] of Object.entries(k.flags ?? {})) lines.push(` --${f} ${d} (with --subject ${name})`)
|
|
168
|
+
h.out(lines.length > 0 ? `\nthis repo's review unit adds:\n${lines.join('\n')}` : "\nthis repo's review unit adds no flags")
|
|
169
|
+
return 0
|
|
170
|
+
})
|
|
171
|
+
return 0
|
|
172
|
+
}
|
|
173
|
+
|
|
153
174
|
export class ReviewAddCommand extends Command {
|
|
154
175
|
static override paths = [['review', 'add']]
|
|
155
176
|
static override usage = Command.Usage({
|
|
@@ -160,14 +181,14 @@ export class ReviewAddCommand extends Command {
|
|
|
160
181
|
['one image', 'sk review add --title "the new menu" --what "the menu, bigger" --image shots/menu.png'],
|
|
161
182
|
['a before and after', 'sk review add --title "lamps lit" --what "..." --before a.png --after b.png'],
|
|
162
183
|
['a second version after his answer', 'sk review add --title "lamps lit" --what "..." --image c.png --supersedes <id>'],
|
|
184
|
+
['a pick among three voices', 'sk review add --choice --title "the narrator" --what "..." --view "voice 0" a.flac --note "low, breathy" --view "voice 1" b.flac --view "voice 2" c.flac'],
|
|
163
185
|
],
|
|
164
186
|
})
|
|
165
187
|
args = Option.Proxy()
|
|
166
188
|
|
|
167
189
|
async execute(): Promise<number> {
|
|
168
190
|
if (this.args.some(isHelp)) {
|
|
169
|
-
this.context
|
|
170
|
-
return 0
|
|
191
|
+
return helpWithUnit(this.context, this.cli.usage(ReviewAddCommand, { detailed: true }), true)
|
|
171
192
|
}
|
|
172
193
|
return run(this.context, async (h) => {
|
|
173
194
|
const { addItem } = await import('../review/store.ts')
|
|
@@ -176,7 +197,7 @@ export class ReviewAddCommand extends Command {
|
|
|
176
197
|
const kindName = rest[rest.indexOf('--subject') + 1]
|
|
177
198
|
const kind = rest.includes('--subject') && kindName ? h.unit?.kinds?.[kindName] : undefined
|
|
178
199
|
const own = [...Object.keys(kind?.flags ?? {}), ...Object.keys(h.unit?.flags ?? {})]
|
|
179
|
-
const f = flagsOf(rest, ['title', 'what', 'image', 'video', 'audio', 'before', 'after', 'label', 'topic', 'subject', 'commit', 'lane', 'supersedes', 'keeps', 'why', 'live'], ['answers', ...own], ['new', 'wip'])
|
|
200
|
+
const f = flagsOf(rest, ['title', 'what', 'image', 'video', 'audio', 'before', 'after', 'label', 'topic', 'subject', 'commit', 'lane', 'supersedes', 'keeps', 'why', 'live'], ['answers', ...own], ['new', 'wip', 'choice'])
|
|
180
201
|
if (f.positionals.length > 0) throw new Error(`unexpected ${f.positionals.join(' ')}`)
|
|
181
202
|
const o = f.one
|
|
182
203
|
const repoFlags = Object.fromEntries(own.flatMap((k) => (f.many[k] ? [[k, f.many[k]]] : [])))
|
|
@@ -209,6 +230,7 @@ export class ReviewAddCommand extends Command {
|
|
|
209
230
|
...(o.why ? { why: o.why } : {}),
|
|
210
231
|
...(answers.length > 0 ? { answers } : {}),
|
|
211
232
|
...(f.on.has('new') ? { fresh: true } : {}),
|
|
233
|
+
...(f.on.has('choice') ? { choice: true } : {}),
|
|
212
234
|
...(o.live !== undefined ? { image: wipViews[0]?.image ?? '', views: wipViews.slice(1), wip: { live: o.live } } : { views }),
|
|
213
235
|
}
|
|
214
236
|
if (h.unit?.add) input = await h.unit.add(input, repoFlags)
|
|
@@ -225,27 +247,29 @@ export class ReviewAddViewCommand extends Command {
|
|
|
225
247
|
static override usage = Command.Usage({
|
|
226
248
|
category: 'Review',
|
|
227
249
|
description: 'another angle on a card he has not answered yet: an image, a pair, a clip or a gallery',
|
|
228
|
-
details: `${DETAILS}\n\nsk review add-view <id> "<label>" <a.png> [<b.png>] | --video <clip.mp4> | --gallery <a.png> <b.png>
|
|
250
|
+
details: `${DETAILS}\n\nsk review add-view <id> "<label>" <a.png> [<b.png>] | --video <clip.mp4> | --audio <sound.wav> | --gallery <a.png> <b.png> … [--note "<a line on that view>"]`,
|
|
229
251
|
examples: [['a view from above', 'sk review add-view <id> "from above" top.png']],
|
|
230
252
|
})
|
|
231
253
|
args = Option.Proxy()
|
|
232
254
|
|
|
233
255
|
async execute(): Promise<number> {
|
|
234
256
|
if (this.args.some(isHelp)) {
|
|
235
|
-
this.context
|
|
236
|
-
return 0
|
|
257
|
+
return helpWithUnit(this.context, this.cli.usage(ReviewAddViewCommand, { detailed: true }), false)
|
|
237
258
|
}
|
|
238
259
|
return run(this.context, async (h) => {
|
|
239
260
|
const { addView } = await import('../review/store.ts')
|
|
240
261
|
const own = Object.keys(h.unit?.flags ?? {})
|
|
241
262
|
const repoFlags: Record<string, string[]> = {}
|
|
242
263
|
const args: string[] = []
|
|
264
|
+
let note: string | undefined
|
|
243
265
|
for (let i = 0; i < this.args.length; i++) {
|
|
244
266
|
const a = this.args[i] ?? ''
|
|
245
267
|
const name = a.startsWith('--') ? a.slice(2) : ''
|
|
246
268
|
if (own.includes(name)) (repoFlags[name] ??= []).push(this.args[++i] ?? '')
|
|
269
|
+
else if (name === 'note') note = this.args[++i]
|
|
247
270
|
else args.push(a)
|
|
248
271
|
}
|
|
272
|
+
if (this.args.includes('--note') && (note === undefined || note.startsWith('--'))) throw new Error('--note "<text>": a line on that view')
|
|
249
273
|
const [id, label, ...rest] = args
|
|
250
274
|
if (!id || !label) throw new Error('add-view <id> "<label>" <media>')
|
|
251
275
|
const v: ViewInput =
|
|
@@ -262,6 +286,7 @@ export class ReviewAddViewCommand extends Command {
|
|
|
262
286
|
: (() => {
|
|
263
287
|
throw new Error('add-view <id> "<label>" <a.png> [<b.png>] | --video <clip> | --gallery <a.png> …')
|
|
264
288
|
})()
|
|
289
|
+
if (note !== undefined) v.note = note
|
|
265
290
|
const extra = h.unit?.addView ? await h.unit.addView(v, repoFlags) : {}
|
|
266
291
|
const item = await addView(h.dir, id, v, extra)
|
|
267
292
|
h.out(`${id}: view ${item.views.length} "${label}" added`)
|
|
@@ -380,10 +405,11 @@ export class ReviewInboxCommand extends Command {
|
|
|
380
405
|
const followed = followedIds(all.items)
|
|
381
406
|
// what he or a player raised and nobody closed is the agents' move, read or not
|
|
382
407
|
const open = all.items.filter((x) => of(x) && x.item.origin.by !== 'agent' && moveOf(stateOf(x, followed.has(x.item.id)).state) === 'agents' && !news.includes(x))
|
|
383
|
-
// a plain approval (no comment, no rectangle, no reply) asks for no work: counted on one
|
|
408
|
+
// a plain approval (no comment, no rectangle, no reaction, no pick, no reply) asks for no work: counted on one
|
|
409
|
+
// line unless --all
|
|
384
410
|
const plain = (x: (typeof news)[number]): boolean => {
|
|
385
411
|
const v = verdictOf(x)
|
|
386
|
-
return v?.status === 'approved' && !v.seen && !v.comment && v.rects.length === 0 && unreadBy(x, 'agent').length === 0
|
|
412
|
+
return v?.status === 'approved' && !v.seen && !v.comment && v.rects.length === 0 && !v.reactions && v.pick === undefined && unreadBy(x, 'agent').length === 0
|
|
387
413
|
}
|
|
388
414
|
const shown = [...(this.all ? news : news.filter((x) => !plain(x))), ...open]
|
|
389
415
|
if (shown.length === 0 && news.length === 0) h.out('review: no unseen feedback or reply')
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// sk sound: the house sound generators, one verb each, for a trailer as much as a game. Each writes --out and says its
|
|
2
|
+
// measures; a trailer's `voice`, `sfx` and `music` words run them through its sound cache (trailer/sound/). Every
|
|
3
|
+
// module loads inside execute: the cli registers this file on every `sk` call
|
|
4
|
+
import { Command, Option } from 'clipanion'
|
|
5
|
+
|
|
6
|
+
const PAGE = 'Page: wiki build/trailer.md, Sound'
|
|
7
|
+
|
|
8
|
+
async function run(context: { stdout: NodeJS.WritableStream; stderr: NodeJS.WritableStream }, body: (say: (l: string) => void, root: string, cwd: string) => Promise<void>): Promise<number> {
|
|
9
|
+
const say = (l: string): void => void context.stdout.write(`${l}\n`)
|
|
10
|
+
const { findRepoRoot } = await import('../ts/config.ts')
|
|
11
|
+
const cwd = process.cwd()
|
|
12
|
+
try {
|
|
13
|
+
await body(say, findRepoRoot(cwd), cwd)
|
|
14
|
+
return 0
|
|
15
|
+
} catch (e) {
|
|
16
|
+
context.stderr.write(`FAIL sound: ${e instanceof Error ? e.message : String(e)}\n`)
|
|
17
|
+
return 1
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** the brand's sound section, when the repo has a brand */
|
|
22
|
+
async function brandSound(root: string): Promise<import('../trailer/sound/brand.ts').TrailerSoundInput | undefined> {
|
|
23
|
+
const { loadBrand } = await import('../assets/run-assets.ts')
|
|
24
|
+
const b = await loadBrand(root)
|
|
25
|
+
if (typeof b === 'string') throw new Error(b)
|
|
26
|
+
return b?.trailer
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const COMFY = 'the comfy-ts module folder (default: the brand\'s trailer.comfy)'
|
|
30
|
+
|
|
31
|
+
/** --out, which every making verb needs */
|
|
32
|
+
function outOf(out: string | undefined): string {
|
|
33
|
+
if (out === undefined) throw new Error('--out <file>: the file it writes')
|
|
34
|
+
return out
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const num = (name: string, v: string | undefined, fallback: number): number => {
|
|
38
|
+
if (v === undefined) return fallback
|
|
39
|
+
const n = Number(v)
|
|
40
|
+
if (!Number.isFinite(n)) throw new Error(`${name} takes a number, got ${v}`)
|
|
41
|
+
return n
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class SoundVoiceCommand extends Command {
|
|
45
|
+
static override paths = [['sound', 'voice']]
|
|
46
|
+
static override usage = Command.Usage({
|
|
47
|
+
category: 'Sound',
|
|
48
|
+
description: 'one spoken line in a voice: ElevenLabs, or comfy-ts cloning a clip or a designed voice',
|
|
49
|
+
details: `no voice flag: the brand's trailer.voice (--by <name>: one of its trailer.voices), else the house narrator. ElevenLabs is paid: it says the characters spent and left. The line is trimmed at both ends and set at -16 LUFS. ${PAGE}`,
|
|
50
|
+
examples: [
|
|
51
|
+
['the brand\'s voice', 'sk sound voice "The storm is coming." --out lines/storm.wav'],
|
|
52
|
+
['an ElevenLabs voice, its second take', 'sk sound voice "[whispers] Hey." --eleven pNInz6obpgDQGcFmaJgB --seed 2 --out hey.wav'],
|
|
53
|
+
['a clip cloned, free', 'sk sound voice "Ahoy." --clone voices/sailor.flac --out ahoy.wav'],
|
|
54
|
+
['a voice designed from words, free', 'sk sound voice "Ahoy." --design "an old sailor, gravelly and warm" --out ahoy.wav'],
|
|
55
|
+
],
|
|
56
|
+
})
|
|
57
|
+
text = Option.String({ required: true, name: 'line' })
|
|
58
|
+
by = Option.String('--by', { description: 'one of the brand\'s trailer.voices' })
|
|
59
|
+
eleven = Option.String('--eleven', { description: 'an ElevenLabs voice id (paid)' })
|
|
60
|
+
model = Option.String('--model', { description: 'the ElevenLabs model (default eleven_v3)' })
|
|
61
|
+
stability = Option.String('--stability')
|
|
62
|
+
similarity = Option.String('--similarity')
|
|
63
|
+
speed = Option.String('--speed')
|
|
64
|
+
clone = Option.String('--clone', { description: 'a clean 5 to 10 s clip to clone, repo relative' })
|
|
65
|
+
transcript = Option.String('--transcript', { description: 'what the clip says (default: <clip>.txt)' })
|
|
66
|
+
design = Option.String('--design', { description: 'a voice designed from its description' })
|
|
67
|
+
designLine = Option.String('--design-line', { description: 'what the designed voice\'s reference reads' })
|
|
68
|
+
designSeed = Option.String('--design-seed')
|
|
69
|
+
engine = Option.String('--engine', { description: 'comfy-ts engine: chatterbox, qwen3, fishs2, cosyvoice3, omnivoice' })
|
|
70
|
+
language = Option.String('--language', { description: 'en, fr… (ElevenLabs) or English, French… (comfy-ts)' })
|
|
71
|
+
comfy = Option.String('--comfy', { description: COMFY })
|
|
72
|
+
seed = Option.String('--seed', { description: 'the take (default 1)' })
|
|
73
|
+
out = Option.String('--out', { description: 'the file it writes (required)' })
|
|
74
|
+
|
|
75
|
+
execute(): Promise<number> {
|
|
76
|
+
return run(this.context, async (say, root, cwd) => {
|
|
77
|
+
const [{ resolve }, { genVoice }, { voiceOf }] = await Promise.all([import('node:path'), import('../trailer/sound/gen.ts'), import('../trailer/sound/brand.ts')])
|
|
78
|
+
const brand = await brandSound(root)
|
|
79
|
+
const given = [this.eleven, this.clone, this.design].filter((x) => x !== undefined).length
|
|
80
|
+
if (given > 1) throw new Error('one voice: --eleven, --clone or --design')
|
|
81
|
+
const n = (k: string, v: string | undefined): number | undefined => (v === undefined ? undefined : num(k, v, 0))
|
|
82
|
+
const lang = this.language !== undefined ? { language: this.language } : {}
|
|
83
|
+
const eng = this.engine !== undefined ? { engine: this.engine } : {}
|
|
84
|
+
let voice
|
|
85
|
+
if (this.eleven !== undefined) voice = { eleven: this.eleven, ...(this.model ? { model: this.model } : {}), ...strip({ stability: n('--stability', this.stability), similarity: n('--similarity', this.similarity), speed: n('--speed', this.speed) }), ...lang }
|
|
86
|
+
else if (this.clone !== undefined) voice = { clone: this.clone, ...(this.transcript !== undefined ? { transcript: this.transcript } : {}), ...eng, ...lang }
|
|
87
|
+
else if (this.design !== undefined) voice = { design: this.design, ...(this.designLine !== undefined ? { line: this.designLine } : {}), ...strip({ seed: n('--design-seed', this.designSeed) }), ...eng, ...lang }
|
|
88
|
+
else voice = voiceOf(brand, this.by)
|
|
89
|
+
await genVoice({ root, text: this.text, voice, seed: num('--seed', this.seed, 1), comfy: this.comfy ?? brand?.comfy, elevenKey: brand?.elevenKey, out: resolve(cwd, outOf(this.out)), say })
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const strip = <T extends Record<string, unknown>>(o: T): Partial<T> => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== undefined)) as Partial<T>
|
|
95
|
+
|
|
96
|
+
export class SoundSfxCommand extends Command {
|
|
97
|
+
static override paths = [['sound', 'sfx']]
|
|
98
|
+
static override usage = Command.Usage({
|
|
99
|
+
category: 'Sound',
|
|
100
|
+
description: 'a sound effect from a prompt: comfy-ts stable-audio, trimmed, its peak at -3 dBFS',
|
|
101
|
+
details: `its silence before is cut, its tail cut where it falls 45 dB under its peak with a 30 ms fade; --hit keeps its first hit alone. The GPU box must be up: sk gp start comfyui. ${PAGE}`,
|
|
102
|
+
examples: [
|
|
103
|
+
['a clash cut to its first hit', 'sk sound sfx "a sword clash, metal on metal" --hit --out sfx/clash.wav'],
|
|
104
|
+
['four seconds of rain, another take', 'sk sound sfx "rain on a tin roof" --seconds 4 --seed 2 --out sfx/rain.wav'],
|
|
105
|
+
],
|
|
106
|
+
})
|
|
107
|
+
prompt = Option.String({ required: true, name: 'prompt' })
|
|
108
|
+
seconds = Option.String('--seconds', { description: 'seconds made (default 2)' })
|
|
109
|
+
seed = Option.String('--seed', { description: 'the take (default 1)' })
|
|
110
|
+
hit = Option.Boolean('--hit', false, { description: 'its first hit alone' })
|
|
111
|
+
comfy = Option.String('--comfy', { description: COMFY })
|
|
112
|
+
out = Option.String('--out', { description: 'the file it writes (required)' })
|
|
113
|
+
|
|
114
|
+
execute(): Promise<number> {
|
|
115
|
+
return run(this.context, async (say, root, cwd) => {
|
|
116
|
+
const [{ resolve }, { genSfx }] = await Promise.all([import('node:path'), import('../trailer/sound/gen.ts')])
|
|
117
|
+
const comfy = this.comfy ?? (await brandSound(root))?.comfy
|
|
118
|
+
await genSfx({ prompt: this.prompt, seconds: num('--seconds', this.seconds, 2), seed: num('--seed', this.seed, 1), hit: this.hit, comfy, out: resolve(cwd, outOf(this.out)), say })
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export class SoundMusicCommand extends Command {
|
|
124
|
+
static override paths = [['sound', 'music']]
|
|
125
|
+
static override usage = Command.Usage({
|
|
126
|
+
category: 'Sound',
|
|
127
|
+
description: 'a music bed or a song from a prompt: comfy-ts stable-audio or ace-step, -16 LUFS',
|
|
128
|
+
details: `stable-audio plays an instrumental bed (up to 300 s); ace-step reads the prompt's first line as its style caption and sings the lines after it ([Verse], [Chorus]), else plays instrumental. The GPU box must be up: sk gp start comfyui. ${PAGE}`,
|
|
129
|
+
examples: [
|
|
130
|
+
['a 30 s bed', 'sk sound music "warm lofi piano bed, slow, soft" --seconds 30 --out music/bed.wav'],
|
|
131
|
+
['a sung theme, its third take', 'sk sound music "an epic orchestral theme, brass and choir" --engine ace-step --seed 3 --out music/theme.wav'],
|
|
132
|
+
],
|
|
133
|
+
})
|
|
134
|
+
prompt = Option.String({ required: true, name: 'prompt' })
|
|
135
|
+
engine = Option.String('--engine', { description: 'stable-audio (default) or ace-step' })
|
|
136
|
+
seconds = Option.String('--seconds', { description: 'seconds made (default 30)' })
|
|
137
|
+
seed = Option.String('--seed', { description: 'the take (default 1)' })
|
|
138
|
+
comfy = Option.String('--comfy', { description: COMFY })
|
|
139
|
+
out = Option.String('--out', { description: 'the file it writes (required)' })
|
|
140
|
+
|
|
141
|
+
execute(): Promise<number> {
|
|
142
|
+
return run(this.context, async (say, root, cwd) => {
|
|
143
|
+
const [{ resolve }, { genMusic }] = await Promise.all([import('node:path'), import('../trailer/sound/gen.ts')])
|
|
144
|
+
const engine = this.engine ?? 'stable-audio'
|
|
145
|
+
if (engine !== 'stable-audio' && engine !== 'ace-step') throw new Error(`--engine ${engine}: stable-audio or ace-step`)
|
|
146
|
+
const comfy = this.comfy ?? (await brandSound(root))?.comfy
|
|
147
|
+
await genMusic({ prompt: this.prompt.replaceAll('\\n', '\n'), engine, seconds: num('--seconds', this.seconds, 30), seed: num('--seed', this.seed, 1), comfy, out: resolve(cwd, outOf(this.out)), say })
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class SoundMidiCommand extends Command {
|
|
153
|
+
static override paths = [['sound', 'midi']]
|
|
154
|
+
static override usage = Command.Usage({
|
|
155
|
+
category: 'Sound',
|
|
156
|
+
description: 'a MIDI file through the soundfont: FluidSynth, its own room on, -16 LUFS',
|
|
157
|
+
details: `the machine's fluidsynth and ~/.local/share/soundfonts/GeneralUser-GS.sf2 (SK_SOUNDFONT names another). ${PAGE}`,
|
|
158
|
+
examples: [['a .mid as a wav', 'sk sound midi theme.mid --out music/theme.wav']],
|
|
159
|
+
})
|
|
160
|
+
file = Option.String({ required: true, name: 'file.mid' })
|
|
161
|
+
out = Option.String('--out', { description: 'the file it writes (required)' })
|
|
162
|
+
|
|
163
|
+
execute(): Promise<number> {
|
|
164
|
+
return run(this.context, async (say, _root, cwd) => {
|
|
165
|
+
const [{ resolve }, { genMidi }] = await Promise.all([import('node:path'), import('../trailer/sound/gen.ts')])
|
|
166
|
+
await genMidi({ file: resolve(cwd, this.file), out: resolve(cwd, outOf(this.out)), say })
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export class SoundMeasureCommand extends Command {
|
|
172
|
+
static override paths = [['sound', 'measure']]
|
|
173
|
+
static override usage = Command.Usage({
|
|
174
|
+
category: 'Sound',
|
|
175
|
+
description: 'a sound as numbers and a picture: length, peak, loudness, onsets, the waveform png',
|
|
176
|
+
details: `nobody hears a build: read these before you report a sound. The png lands at <file>.png. ${PAGE}`,
|
|
177
|
+
examples: [['a made sfx', 'sk sound measure sfx/clash.wav']],
|
|
178
|
+
})
|
|
179
|
+
file = Option.String({ required: true, name: 'file' })
|
|
180
|
+
|
|
181
|
+
execute(): Promise<number> {
|
|
182
|
+
return run(this.context, async (say, _root, cwd) => {
|
|
183
|
+
const [{ resolve }, { measure, measureLine, waveformPng }] = await Promise.all([import('node:path'), import('../trailer/sound/audio.ts')])
|
|
184
|
+
const f = resolve(cwd, this.file)
|
|
185
|
+
say(`${this.file}: ${measureLine(await measure(f))}`)
|
|
186
|
+
await waveformPng(f, `${f}.png`)
|
|
187
|
+
say(`ok: ${f}.png`)
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export class SoundGmCommand extends Command {
|
|
193
|
+
static override paths = [['sound', 'gm']]
|
|
194
|
+
static override usage = Command.Usage({
|
|
195
|
+
category: 'Sound',
|
|
196
|
+
description: 'the General MIDI programs, drums and kits a score\'s gm: instruments take',
|
|
197
|
+
details: `a score track plays one through the soundfont: track piano gm:acoustic-grand, drums k=gm:kick, or gm('strings', { vol, pan, reverb }) in its code. ${PAGE}`,
|
|
198
|
+
examples: [['every name', 'sk sound gm']],
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
execute(): Promise<number> {
|
|
202
|
+
return run(this.context, async (say) => {
|
|
203
|
+
const { gmTable } = await import('../trailer/score/gm.ts')
|
|
204
|
+
for (const l of gmTable()) say(l)
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const SOUND_COMMANDS = [SoundVoiceCommand, SoundSfxCommand, SoundMusicCommand, SoundMidiCommand, SoundMeasureCommand, SoundGmCommand]
|
package/src/cli/cmd_Trailer.ts
CHANGED
|
@@ -32,17 +32,19 @@ export class TrailerCommand extends Command {
|
|
|
32
32
|
['one cut by its file', 'sk trailer trailer/launch/main.trailer'],
|
|
33
33
|
['film again the takes of two shots', 'sk trailer --refilm intro,menu'],
|
|
34
34
|
['encoded by x264 for a release', 'sk trailer --final'],
|
|
35
|
+
['the music variant b on the same picture', 'sk trailer --variant b'],
|
|
35
36
|
],
|
|
36
37
|
})
|
|
37
38
|
|
|
38
39
|
file = Option.String({ required: false })
|
|
39
40
|
refilm = Option.String('--refilm', { tolerateBoolean: true, description: 'film again: every stale take, or the named shots (a,b)' })
|
|
40
41
|
final = Option.Boolean('--final', false, { description: 'encode with x264 for a release (default: the fast draft encoder)' })
|
|
42
|
+
variant = Option.String('--variant', { description: 'a music variant the cut names (music … variant <name>): out/<name>-<variant>.mp4' })
|
|
41
43
|
|
|
42
44
|
async execute(): Promise<number> {
|
|
43
45
|
return run(this.context, async (say, root, cwd) => {
|
|
44
46
|
const b = await import('../trailer/build.ts')
|
|
45
|
-
const l = await b.load(b.trailerFile(this.file, root, cwd), root)
|
|
47
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root, this.variant !== undefined ? { variant: this.variant } : {})
|
|
46
48
|
if (!b.sayErrors(l, say, cwd)) return 1
|
|
47
49
|
await b.build(l, { refilm: refilmOf(this.refilm), final: this.final, say, cwd })
|
|
48
50
|
return 0
|
|
@@ -64,14 +66,17 @@ export class TrailerCheckCommand extends Command {
|
|
|
64
66
|
})
|
|
65
67
|
|
|
66
68
|
file = Option.String({ required: false })
|
|
69
|
+
variant = Option.String('--variant', { description: 'a music variant the cut names' })
|
|
67
70
|
|
|
68
71
|
async execute(): Promise<number> {
|
|
69
72
|
return run(this.context, async (say, root, cwd) => {
|
|
70
73
|
const t0 = performance.now()
|
|
71
74
|
const b = await import('../trailer/build.ts')
|
|
72
|
-
const l = await b.load(b.trailerFile(this.file, root, cwd), root)
|
|
75
|
+
const l = await b.load(b.trailerFile(this.file, root, cwd), root, this.variant !== undefined ? { variant: this.variant } : {})
|
|
73
76
|
if (!b.sayErrors(l, say, cwd)) return 1
|
|
74
77
|
for (const line of b.timeline(l, b.takesOfLoaded(l), await b.worldOf(l), cwd)) say(line)
|
|
78
|
+
for (const line of await b.soundLines(l)) say(` ${line}`)
|
|
79
|
+
for (const line of await b.fontLines(l)) say(` ${line}`)
|
|
75
80
|
say(`ok: checked in ${((performance.now() - t0) / 1000).toFixed(2)} s`)
|
|
76
81
|
return 0
|
|
77
82
|
})
|
package/src/cli/kit/verb.ts
CHANGED
|
@@ -65,6 +65,10 @@ export type Verb = {
|
|
|
65
65
|
version?: number | string
|
|
66
66
|
/** with film: `own`, a clip that films as long as it plays (no `--seconds`) */
|
|
67
67
|
length?: 'own'
|
|
68
|
+
/** with film: the words it takes and their types (trailer/words.ts), each checked at its line of a .trailer */
|
|
69
|
+
words?: Record<string, unknown>
|
|
70
|
+
/** with film: the paths its takes film, their world */
|
|
71
|
+
stamp?: string[]
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
/** the argv a row runs */
|
|
@@ -126,6 +130,8 @@ export function verbs(rows: Verb[]): CommandClass[] {
|
|
|
126
130
|
static film = v.film
|
|
127
131
|
static filmVersion = v.version
|
|
128
132
|
static filmLength = v.length
|
|
133
|
+
static filmWords = v.words
|
|
134
|
+
static filmStamp = v.stamp
|
|
129
135
|
args = Option.Proxy()
|
|
130
136
|
async execute(): Promise<number> {
|
|
131
137
|
const { root, cwd } = this.context
|
|
@@ -154,14 +160,16 @@ export function verbs(rows: Verb[]): CommandClass[] {
|
|
|
154
160
|
|
|
155
161
|
/** the marks a command class carries: set by `verbs()` from its row, or
|
|
156
162
|
* declared as statics on a hand written class */
|
|
157
|
-
export function marksOf(c: object): { essential?: Essential; film?: Film; version?: number | string; length?: 'own' } {
|
|
158
|
-
const o = c as { essential?: unknown; film?: unknown; filmVersion?: unknown; filmLength?: unknown }
|
|
159
|
-
const out: { essential?: Essential; film?: Film; version?: number | string; length?: 'own' } = {}
|
|
163
|
+
export function marksOf(c: object): { essential?: Essential; film?: Film; version?: number | string; length?: 'own'; words?: Record<string, unknown>; stamp?: string[] } {
|
|
164
|
+
const o = c as { essential?: unknown; film?: unknown; filmVersion?: unknown; filmLength?: unknown; filmWords?: unknown; filmStamp?: unknown }
|
|
165
|
+
const out: { essential?: Essential; film?: Film; version?: number | string; length?: 'own'; words?: Record<string, unknown>; stamp?: string[] } = {}
|
|
160
166
|
if (typeof o.essential === 'object' && o.essential !== null) out.essential = o.essential as Essential
|
|
161
167
|
if (o.film === 'still' || o.film === 'clip') {
|
|
162
168
|
out.film = o.film
|
|
163
169
|
if (typeof o.filmVersion === 'number' || typeof o.filmVersion === 'string') out.version = o.filmVersion
|
|
164
170
|
if (o.filmLength === 'own') out.length = 'own'
|
|
171
|
+
if (typeof o.filmWords === 'object' && o.filmWords !== null) out.words = o.filmWords as Record<string, unknown>
|
|
172
|
+
if (Array.isArray(o.filmStamp)) out.stamp = o.filmStamp.map(String)
|
|
165
173
|
}
|
|
166
174
|
return out
|
|
167
175
|
}
|
package/src/cli.ts
CHANGED
|
@@ -41,6 +41,7 @@ import { ShotCommand } from './cli/cmd_Shot.ts'
|
|
|
41
41
|
import { StatusCommand } from './cli/cmd_Status.ts'
|
|
42
42
|
import { StartCommand } from './cli/cmd_Start.ts'
|
|
43
43
|
import { TRAILER_COMMANDS } from './cli/cmd_Trailer.ts'
|
|
44
|
+
import { SOUND_COMMANDS } from './cli/cmd_Sound.ts'
|
|
44
45
|
import { REVIEW_COMMANDS } from './cli/cmd_Review.ts'
|
|
45
46
|
import { TuiCommand } from './cli/cmd_Tui.ts'
|
|
46
47
|
import { WikiLintCommand, WikiServeCommand } from './cli/cmd_Wiki.ts'
|
|
@@ -73,6 +74,7 @@ export function buildCli(): Cli {
|
|
|
73
74
|
cli.register(CheckCommand)
|
|
74
75
|
cli.register(TuiCommand)
|
|
75
76
|
for (const c of TRAILER_COMMANDS) cli.register(c)
|
|
77
|
+
for (const c of SOUND_COMMANDS) cli.register(c)
|
|
76
78
|
for (const c of REVIEW_COMMANDS) cli.register(c)
|
|
77
79
|
cli.register(ServeCommand)
|
|
78
80
|
cli.register(PmCommand)
|
package/src/core/app-icon.ts
CHANGED
|
@@ -10,8 +10,9 @@ import { identityOfName, identityOfText, type Identity } from './identity-color.
|
|
|
10
10
|
import { configName, CONFIG_FILE } from './identity.ts'
|
|
11
11
|
import { LAYOUT } from './layout.ts'
|
|
12
12
|
|
|
13
|
-
/** the mark an app wears: the repo's own, largest first
|
|
14
|
-
|
|
13
|
+
/** the mark an app wears: the repo's own, largest first; an svg mark is drawn
|
|
14
|
+
* as an image too (the canvas decodes it) */
|
|
15
|
+
export const APP_ICON_FILES = [`${LAYOUT.identity}/icon.png`, `${LAYOUT.identity}/icon.svg`, 'public/logo.png', 'public/favicon.png']
|
|
15
16
|
|
|
16
17
|
export function appIconOf(root: string): string | null {
|
|
17
18
|
for (const f of APP_ICON_FILES) if (existsSync(join(root, f))) return join(root, f)
|
|
@@ -133,15 +134,18 @@ export async function drawAppIcon(bytes: Uint8Array, id: Identity): Promise<{ ca
|
|
|
133
134
|
return { canvas: out, plate: colour }
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
/** the icon as pngs, one per size asked, drawn once
|
|
137
|
-
|
|
137
|
+
/** the icon as pngs, one per size asked, drawn once. `plate` crops the macOS
|
|
138
|
+
* grid's margin away, the rounded plate edge to edge (the hub's rail) */
|
|
139
|
+
export async function appIconPngs(bytes: Uint8Array, id: Identity, sizes: number[] = [ICON_PX], crop: 'canvas' | 'plate' = 'canvas'): Promise<Buffer[]> {
|
|
138
140
|
const { canvas } = await drawAppIcon(bytes, id)
|
|
141
|
+
const o = crop === 'plate' ? (ICON_PX - PLATE) / 2 : 0
|
|
142
|
+
const side = crop === 'plate' ? PLATE : ICON_PX
|
|
139
143
|
return sizes.map((px) => {
|
|
140
|
-
if (px === ICON_PX) return canvas.toBuffer('image/png')
|
|
144
|
+
if (px === ICON_PX && crop === 'canvas') return canvas.toBuffer('image/png')
|
|
141
145
|
const small = createCanvas(px, px)
|
|
142
146
|
const ctx = small.getContext('2d')
|
|
143
147
|
ctx.imageSmoothingQuality = 'high'
|
|
144
|
-
ctx.drawImage(canvas, 0, 0, px, px)
|
|
148
|
+
ctx.drawImage(canvas, o, o, side, side, 0, 0, px, px)
|
|
145
149
|
return small.toBuffer('image/png')
|
|
146
150
|
})
|
|
147
151
|
}
|
package/src/repo-config.ts
CHANGED
|
@@ -57,6 +57,9 @@ export type ServeConfig = {
|
|
|
57
57
|
* one failed, green when all passed. Default: the built-in md lint
|
|
58
58
|
* (orphans and dangling refs) over the repo root */
|
|
59
59
|
lint?: string[]
|
|
60
|
+
/** the folder its node ids are relative to, where the wikis page reads
|
|
61
|
+
* a page to search and preview (default: the repo root) */
|
|
62
|
+
dir?: string
|
|
60
63
|
}
|
|
61
64
|
/** agent/ graph entrypoint (default: index.md AND README.md) */
|
|
62
65
|
agentIndex?: string
|
package/src/review/format.ts
CHANGED
|
@@ -19,7 +19,13 @@ const atText = (t: number | undefined): string => (t === undefined ? '' : ` at $
|
|
|
19
19
|
|
|
20
20
|
/** each reaction to one view or tile: its place by label, its status, its comment */
|
|
21
21
|
const reactionTexts = (x: ReviewItem): string[] =>
|
|
22
|
-
(verdictOf(x)?.reactions ?? []).map((q) => `${placeText(q, x.item.views)}${q.status ? ` ${q.status}` : ''}${q.comment ? `: ${flat(q.comment)}` : ''}`)
|
|
22
|
+
(verdictOf(x)?.reactions ?? []).map((q) => `${placeText(q, x.item.views)}${q.status ? ` ${q.status}` : ''}${q.stars ? ` ★${q.stars}` : ''}${q.comment ? `: ${flat(q.comment)}` : ''}`)
|
|
23
|
+
|
|
24
|
+
/** the view he picked on a choice card, `picked [voice 1]`; null when he picked none */
|
|
25
|
+
const pickText = (x: ReviewItem): string | null => {
|
|
26
|
+
const p = verdictOf(x)?.pick
|
|
27
|
+
return p === undefined ? null : `picked ${placeText({ view: p }, x.item.views)}`
|
|
28
|
+
}
|
|
23
29
|
|
|
24
30
|
/** one line per feedback, for a monitor: the status, the overall comment, each view's reaction, then each rectangle's comment */
|
|
25
31
|
export function line(x: ReviewItem): string {
|
|
@@ -28,7 +34,8 @@ export function line(x: ReviewItem): string {
|
|
|
28
34
|
const views = x.item.views
|
|
29
35
|
const many = views.length > 1
|
|
30
36
|
const rects = f.rects.map((q, i) => `#${i + 1}${many ? ` ${placeText(q, views)}` : ''}${q.side ? ` ${q.side}` : ''}${atText(q.t)} ${flat(q.comment) || '(no comment)'}`)
|
|
31
|
-
const
|
|
37
|
+
const pick = pickText(x)
|
|
38
|
+
const parts = [...(pick ? [pick] : []), ...reactionTexts(x), ...rects]
|
|
32
39
|
return `${x.item.id} ${f.status} "${x.item.title}": ${flat(f.comment) || '(no overall comment)'}${parts.length ? ` | ${parts.join(' | ')}` : ''}`
|
|
33
40
|
}
|
|
34
41
|
|
|
@@ -131,7 +138,8 @@ export function detail(x: ReviewItem, dir: string, full = false, followed = fals
|
|
|
131
138
|
].join('\n')
|
|
132
139
|
}
|
|
133
140
|
if (!f) return [`${r.id} NO VERDICT YET`, head, ...sub, ...replies].join('\n')
|
|
134
|
-
const
|
|
141
|
+
const pick = pickText(x)
|
|
142
|
+
const out = [`${r.id} ${f.status.toUpperCase()} ${f.at}`, head, ...sub, ...(pick ? [` ${pick}`] : []), ` comment: ${f.comment || '(none)'}`]
|
|
135
143
|
for (const t of reactionTexts(x)) out.push(` view ${t}`)
|
|
136
144
|
const px = (v: number, of: number): number => Math.round(v * of)
|
|
137
145
|
const views = r.views
|