@q253595422/dsh-imagegen-video 0.0.0-stage → 1.7.7

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 ADDED
@@ -0,0 +1,618 @@
1
+ # Changelog
2
+
3
+ This project is a fork of [dsh-imagegen](https://github.com/dickpy/dsh-imagegen).
4
+ The fork starts from the upstream **v1.6.8** release source, which is preserved as
5
+ the first commit (`baseline: upstream @dickpy/dsh-imagegen v1.6.8 source`). Every
6
+ commit after that is this fork's own work.
7
+
8
+ The list below describes the fork's delta against that baseline. Commit-level
9
+ detail is in `git log 9305f9f..HEAD`.
10
+
11
+ ## Releases
12
+
13
+ ### 1.7.7 — deleting becomes undoable, the poster editor arrives, and the update chain stops lying
14
+
15
+ Three user-visible failures and one piece of upstream functionality, plus the
16
+ release process itself moving onto a machine that runs the gates.
17
+
18
+ - **Deleting a gallery entry no longer destroys its bytes.** Removal was an
19
+ unlink, so every history row and canvas node still holding that URL turned
20
+ into a broken image, and there was no way back. It is now a soft delete: the
21
+ entry is flagged `deletedAt` and listed as trash, the files stay put, and only
22
+ `purgeGallery` (or the retention sweep) unlinks. The retention window is
23
+ `GALLERY_TRASH_RETENTION_MS` = 30 days, enforced lazily inside `mutateGallery`
24
+ — no timer, nothing to shut down, and a process that is never opened again
25
+ still gets its cleanup on the first store call of the next one. Re-adding an
26
+ image whose old copy is in the trash produces a visible entry, not a silent
27
+ no-op. The panel's trash section was also covered by nothing: the fixture fed
28
+ it rows and no check ever looked at them, so the toggle, the per-row restore id
29
+ and the purge confirmation's count could all have broken silently. Each has a
30
+ check now, and the section stopped printing its own count heading directly
31
+ under the toggle that already says it.
32
+ - **The poster editor is ported from upstream v1.7.0.** Text and logo layers are
33
+ composited over an image node locally with Canvas 2D — no model call, no
34
+ quota. Saving uploads the flattened bitmap as a new image node and stores the
35
+ recipe in that node's `metadata.poster` (the new node is wired to its source),
36
+ so reopening the result restores every layer instead of flattening the work
37
+ into a picture. The source image is never rewritten. `Ctrl+Z`/`Delete` inside
38
+ the modal act on poster layers and never on the canvas beneath it.
39
+ The asset sweep counts those recipe references too: a poster node only stores
40
+ the flattened composite in `metadata.asset`, while its background and logo
41
+ layers live in `metadata.poster` and are re-read by the editor. Collecting
42
+ only `metadata.asset` meant the first canvas a user deleted removed the base
43
+ image behind every poster they had made, and reopening one drew the layers
44
+ over an empty stage.
45
+ - **The update chain reports what actually happened.** `profileFromProcess`
46
+ resolves the profile from `DSH_PROFILE`, then `--profile`, then a
47
+ `profiles/<name>` path segment in argv, and only then falls back to `web`. The
48
+ Electron host passes the profile as a bare *path* and sets neither of the
49
+ other two, so the flag-only reading targeted the wrong profile for every
50
+ Desktop user — even a successful update left the running host on the old
51
+ plugin. DSH Desktop's profile is locked by the CLI — spawning it anyway
52
+ produced a bare "update failed" with no cause, the dead end in upstream issue
53
+ #41 — so that profile now throws `ManualUpdateRequiredError`
54
+ (`update-manual-required`) carrying the archive URL, and the panel shows a
55
+ copyable install command instead of a scary error. A failed spawn keeps the
56
+ last three lines of the CLI's stderr (`STDERR_LIMIT` 4 000 chars) so a pnpm
57
+ store mismatch or a locked directory is readable.
58
+ - **The canvas has a keyboard shortcut legend.** `?` or `F1` opens it, the dock
59
+ gained an entry, `Esc` closes it above every other overlay, and the controls
60
+ that have a shortcut now name it in their own tooltip ("撤销 (Ctrl+Z)"). The
61
+ bindings live in one data table (`CANVAS_SHORTCUTS`) shared with the check that
62
+ compares them against the key handler, so the legend cannot drift from the code
63
+ it documents. `canvas.undo` / `canvas.redo` lost their hardcoded `(Ctrl+Z)`
64
+ suffix in all three languages — the key text is derived from the table now.
65
+ The layout is measured in a real browser at 1280×800 and 390px: the dialog
66
+ has to fit the viewport (its body scrolls instead), no row may overflow its
67
+ grid column, and the two-column grid has to collapse to one on a phone.
68
+ - **A malformed poster recipe is refused on the way in and on the way out.**
69
+ `isPosterDocument()` is strict (`version === 1`, integer sides within 4096, a
70
+ 64-hex `assetId` with an image extension, `url` on the same-origin asset route,
71
+ `type` not `kind`, unique ids, `#rrggbb` colours), and `isNode()` calls it, so
72
+ a canvas save carrying a half-valid recipe fails loudly instead of throwing
73
+ when the editor is reopened. A route-level test round-trips a valid one and
74
+ refuses two corrupt shapes.
75
+ - **The browser suites stop fighting over a debugging port.** They guessed
76
+ `9900 + random(300)`, and two runs (or a leftover browser) could land on the
77
+ same port — the intermittent `Error: CDP down` that made `verify-ui-contracts`
78
+ report 27/28. `scripts/lib/headless-chrome.mjs` launches with
79
+ `--remote-debugging-port=0` and reads the port Chromium actually published
80
+ from `DevToolsActivePort`, checks `exitCode`, keeps stderr for the error, and
81
+ uses a fresh profile directory per launch. All three guards share it, and
82
+ `verify-ui-contracts` now scans the other guards' sources for a guessed port,
83
+ so nobody can reintroduce one without a red check.
84
+ - **The release is built by CI, not by hand.** `.github/workflows/release.yml`
85
+ runs typecheck → build → ffmpeg → tests → verify-pack → `pnpm pack` →
86
+ `SHA256SUMS` → tag → GitHub Release, and the tarball the Release advertises is
87
+ the one the tests ran against (`--ignore-scripts` on the pack, so nothing
88
+ rebuilds into different bytes). Re-running is idempotent: same tag, replaced
89
+ assets, no duplicate Release. It never moves an existing tag. npm publishing is
90
+ a separate job gated on a repository variable, and it probes for the package
91
+ before the version — npm answers both "no such package" and "no such version"
92
+ with E404, and only the second one is a normal publish.
93
+ - **The browser launcher had never been committed.** `.gitignore` carried an
94
+ unanchored `lib/`, which matches `scripts/lib/` as well as the build output,
95
+ so `scripts/lib/headless-chrome.mjs` was excluded from the repository from the
96
+ day it was written. Three guard scripts import it: a clean clone died with
97
+ `ERR_MODULE_NOT_FOUND` before any suite could run, while the author's tree
98
+ stayed green because the file existed on exactly one disk. The first release
99
+ workflow is what surfaced it — `pnpm test` failed before `verify-pack` ran and
100
+ no tag was created. `.gitignore` now anchors the rule to `/lib/`, and
101
+ `scripts/verify-tracked.mjs` runs as the first step of `pnpm test`: every
102
+ relative import under `src/` and `scripts/` must name a file git tracks, and
103
+ no source file may be sitting in the tree untracked.
104
+ - **The archive check only confirmed what somebody remembered to list.**
105
+ `verify-pack.mjs` required `lib/index.js` and one template snapshot, both
106
+ hard-coded, and nothing compared them against what `package.json` tells a host
107
+ to load. A `files` edit that drops `lib/`, or an `exports` target pointing at
108
+ a renamed file, produced an archive that installs cleanly, boots cleanly, and
109
+ does nothing at all: the host throws at import time or the browser half 404s,
110
+ and neither error names packaging. The check now reads the manifest **inside**
111
+ the tarball and requires `main`, every `exports` target and
112
+ `dsh.bundle.patch` to actually be in it. Writing the case exposed a second
113
+ problem in the harness itself: `freshCopy()` copied `lib` but not
114
+ `cordis.patch.yml` or the other `files` entries, so the pack runner was
115
+ inspecting an archive that could never satisfy a packaging rule — the copy is
116
+ now built from the `files` list instead of a hand-kept one.
117
+ - **`test:negative` was not running every case it defines.** The `canvas-video`
118
+ runner was added for the gallery soft-delete case but never appended to the
119
+ `test:negative` chain, so the case passed when invoked by hand and was absent
120
+ from the gate. The chain now includes it, and `verify-negative.mjs` refuses to
121
+ run at all if any defined runner is missing from that command — a guard on the
122
+ gate, since a case can only fail after its runner has been selected.
123
+ - **The build was not a function of the source tree.** Two builds of the same
124
+ commit produced different `lib/client.js` bytes. `lightningcss` returns
125
+ CSS-module exports from a native hash map, so the class names arrive in a
126
+ different order on each process — five runs over one stylesheet produced five
127
+ different first keys — and `tsdown.config.ts` serialised that map in iteration
128
+ order. Object key order is invisible to every consumer of a CSS-module map,
129
+ which is why nothing ever noticed; but it is byte-stable input to the release
130
+ artifact, so `SHA256SUMS` could not be reproduced from source and "the Release
131
+ ships the bytes the gates approved" was true only of the run that made them.
132
+ The map is sorted at the one point where it becomes bytes, and
133
+ `scripts/verify-reproducible.mjs` now builds twice on every `pnpm test` and
134
+ requires all four artifacts to be byte-identical.
135
+ - **The npm job's "exactly one tarball" guard could not fail on zero.** Bash
136
+ leaves an unmatched glob in the array as its own literal, so
137
+ `packages=(dist/*.tgz)` against an empty `dist/` yielded count=1, the guard
138
+ passed, and the next line asked npm to publish a file named `dist/*.tgz`. The
139
+ job did fail — with `ENOENT`, which reads like an npm fault instead of "the
140
+ artifact never arrived". `shopt -s nullglob` makes the count mean what it
141
+ says, and the failure now prints `ls -la dist`.
142
+
143
+ ### 1.7.6 — the template library grows to eight sources, and a parser bug that hid half of one
144
+
145
+ The template library was smaller than it said it was. A snapshot keyed each case
146
+ by its number alone, and the upstream reuses numbers across AND inside its
147
+ category files, so roughly a thousand cases were overwritten without a single
148
+ error. Fixing that, plus three newly found upstream collections that can be
149
+ included on their licences, takes the library from 5 sources / 2462 cases to
150
+ **8 sources / 3774 cases**.
151
+
152
+ - **evolink: 462 → 967 cases.** `cases/character.md` and `cases/portrait.md`
153
+ both open at `### Case 1:` with images in different directories, and numbers
154
+ repeat inside one file too, so keying a case by its number discarded the rest
155
+ — about 1420 parseable prompts reduced to 462 with nothing to show for it. Ids
156
+ are now category-scoped (`portrait-53`) and de-duplicated per file: 967 cases,
157
+ zero duplicate ids. This is a repair, not an addition, which is why it is
158
+ called out separately.
159
+ - **Three new sources, each pinned to an exact commit.** `itgoyo/awesome-gpt-image2-prompt`
160
+ (602 cases, CC BY 4.0), `songtianlun/awesome-prompts` (55, MIT) and
161
+ `youart-open-source/awesome-gpt-image-2-5-prompts` (150, layered licensing —
162
+ see THIRD_PARTY_NOTICES.md). `ITGOYO_COMMIT`, `SONGTIANLUN_COMMIT` and
163
+ `YOUART_COMMIT` fix the revision the build reads; the sources are deliberately
164
+ not refreshable at runtime, so what was reviewed is what ships.
165
+ - **youart ships no images on purpose.** The corpus publishes none of its own;
166
+ every row credits the person who wrote the prompt and the post where it first
167
+ appeared. Those cards show the placeholder thumbnail and keep the full prompt,
168
+ title and author link.
169
+ - **The source tab row scrolls.** Eight `nowrap` tabs in a plain flex box
170
+ neither wrapped nor scrolled, so every source past the right edge was
171
+ invisible, unclickable, and undiscoverable. The row now scrolls horizontally
172
+ and the tabs do not shrink (shrinking truncates the label and its count badge
173
+ instead). Verified in a real browser, not by reading the declarations.
174
+ - **Favorites for evolink cases do not carry over.** Favorites are keyed
175
+ `sourceId:caseId`, and evolink's ids changed shape (`case-12` →
176
+ `portrait-12`), so a star put on an evolink case in 1.7.5 will not resolve.
177
+ No other source's ids changed. This is the unavoidable cost of fixing a
178
+ collision that was silently discarding cases.
179
+ - **Six preview images from an unlicensed repository are gone.** `prompt-signal`
180
+ carried six `image` URLs pointing at `songguoxs/gpt4o-image-prompts`, which
181
+ declares no licence. The URLs are shipped data, so serving them is this
182
+ project's call rather than the aggregator's: the field is now empty and those
183
+ cards show the placeholder thumbnail. Their prompt text, title and link to the
184
+ original author's own post all stay — the prompt was never the aggregator's,
185
+ and the `githubUrl` attribution link is kept. Both the generator and the
186
+ contract check guard this, so a regeneration that forgets the filter fails.
187
+ - **The installed archive is 4 MiB instead of 24 MiB.** `docs/images/` — the
188
+ README screenshots, including a 13 MiB demo GIF — is never read at runtime and
189
+ is rendered by GitHub from the repository. It is no longer in `files`, the way
190
+ `docs/videos/` already was not. The archive rule now runs as
191
+ `scripts/verify-pack.mjs` against a real `pnpm pack` in CI and in the negative
192
+ verification, instead of as inline shell in the workflow.
193
+ - **"Detect models" no longer drops `nanobanana2` and `nanobanana2-lite`.**
194
+ Both ship in `DEFAULT_IMAGE_MODELS`, and `describeModel()` recognises them,
195
+ but `isLikelyImageModelId()` required a separator after the family keyword —
196
+ `nanobanana2` has none between `nanobanana` and `2`. Against a gateway whose
197
+ `/models` returns ids without capability metadata, the panel therefore listed
198
+ two models fewer than the catalog declares. The predicate now accepts an
199
+ optional digit run before the boundary (`flux2`, `wanx2` likewise);
200
+ `gpt-4o` and `text-embedding-3-small` are still rejected. The smoke suite's
201
+ detection check now serves both ids from its mock gateway, so reverting the
202
+ regex turns it red.
203
+
204
+ ### 1.7.5 — every UI module reviewed, and a failed call that can be explained
205
+
206
+ Two things: a full pass over the plugin's UI, driven by real defects rather than
207
+ taste (the theme no longer leaks into component internals, and the layer order is
208
+ written down in one place), and a failed call that can actually be explained —
209
+ including on the video chain, which upstream's version of that work did not cover.
210
+
211
+ - **The nav follows the host theme at runtime.** `GooeyNav` had its four colors
212
+ baked in as literals and detected the surface only once, at mount. Switching
213
+ light/dark while it was on screen left it painted for the old surface and
214
+ unreadable against the new one. The palette now derives from the `--dsw-alias-*`
215
+ tokens, and a new `surface-theme.ts` subscription re-derives it on the host
216
+ attribute (including DSH's `data-ds-dark-theme`), on the OS preference, and on
217
+ re-parenting. **`--dsw-alias-brand-primary` is a neutral inverse, not an
218
+ accent** (near-white on light, near-black on dark) — it is used as one, and
219
+ the annotation marker deliberately does not.
220
+ - **The annotation marker is one token, read by both sides.** The on-screen boxes
221
+ were colored from CSS while the boxes burned into the image by Canvas2D were a
222
+ `#ff2d55` literal, so the two could drift. Both now read
223
+ `--canvas-annotation`. Because that token is declared on the canvas root
224
+ class — not on `documentElement` — `themeColor()` takes an origin element and
225
+ the caller passes the canvas root; reading it from the document root returns
226
+ empty and silently falls back to the literal again.
227
+ - **One stacking scale, and it does not renumber what already worked.** Every
228
+ z-index literal is gone from the module stylesheets in favor of a documented
229
+ scale in `panel.module.css`. The numbers are the values the layers already
230
+ had; only layers that genuinely *shared* a number were moved, and only as far
231
+ as the existing comparisons required (1000 → 1000/1005/1010, 130 → 130/131/132,
232
+ 60 → 60/61/62, 70 → 70/72, 20 → 20/21). Those ties were real: the winner was
233
+ decided by document order, so a harmless refactor could re-stack the UI. The
234
+ guard is threefold — the doc table must match the declarations, 26 required
235
+ orderings must hold, and the three rungs that interleave with the host shell
236
+ are pinned by value — plus a real-browser check that renders two of the former
237
+ ties in *reversed* order and requires the same winner.
238
+ - **Overlays no longer clear the canvas selection.** Every overlay (composer,
239
+ menus, dialogs) is a sibling of the viewport, so a pointerdown on one never
240
+ reached the viewport handler. This is now asserted structurally rather than by
241
+ a guard that turned out to be dead code.
242
+ - **Touch pinch zooms the canvas.** Two touch pointers moving apart scale the
243
+ view, the way a trackpad already did.
244
+ - **Keyboard fixes.** Space on a focused dock button activates the button again
245
+ instead of being swallowed by the pan handler; Escape closes the in-canvas
246
+ video preview, the skill menu, the polish dialog and the picker; a node's
247
+ hidden hover toolbar leaves the tab order and is revealed by focus, so Tab no
248
+ longer lands on invisible controls (including delete); every `role="tablist"`
249
+ wired the shared roving-tabIndex and arrow-key contract.
250
+ - **Failures stop masquerading as empty lists.** History, gallery and template
251
+ favorites showed "nothing here yet" when the read had actually failed. They
252
+ now show the failure and a retry, and a template favorite whose state could
253
+ not be read shows an unknown marker instead of an unstarred star.
254
+
255
+ #### The record of a failed call, and the video chain made equal to it
256
+
257
+ Upstream's diagnostics work (#38) is part of this release. Their history and ours
258
+ share no ancestor — this fork's baseline is a squash of v1.6.8 — so `git merge`
259
+ is refused outright and the work landed as a file-level port rather than a merge.
260
+ Attribution for their code is in [NOTICE](./NOTICE). The version line here is this
261
+ repository's own and does not follow theirs.
262
+
263
+ - **Diagnostics: every failed upstream attempt is now on the record.** A new
264
+ in-memory ring buffer (200 entries, cleared on restart, never written to disk)
265
+ keeps the *final* request URL, the HTTP status, the network code
266
+ (`ECONNREFUSED` / `ENOTFOUND` / `CERT_*`), the duration, the byte counts and the
267
+ raw upstream body. Credentials are redacted before storage — `Bearer` tokens,
268
+ keys in a query string and inline base64 all become placeholders — so the
269
+ copied report is safe to paste into a public issue. The panel offers copy-all
270
+ and states the failure instead of pretending there is nothing to show.
271
+ - **A failed task explains itself in the task list**, instead of the word
272
+ "failed": the message now carries the request URL and the network code, which
273
+ is what settles the usual "the request never reached the gateway" report.
274
+ - **The studio no longer hides behind the plugin-market page.** Both live in the
275
+ same `main` slot, so switching from the conversation to the studio left two
276
+ panels in the column. The keyed slot now yields for non-conversation content.
277
+ - **The video chain records into the same buffer.** Upstream's #38 covered
278
+ images only, so a user whose images worked and whose videos did not had
279
+ nowhere to look — and `DiagnosticPhase` could not even name the video legs.
280
+ Video is submit → poll → download rather than one synchronous call, so each leg
281
+ is its own phase (`video-submit` / `video-poll` / `video-download`), and
282
+ "the render never started" is now distinguishable from "the status endpoint
283
+ stopped answering". Reference frames are recorded as a count, never as the
284
+ megabytes of base64 they are; a transient blip the poll loop retries is
285
+ deliberately not recorded, while an exhausted retry budget is, with the status
286
+ the endpoint kept answering with. The entry point rides the always-rendered
287
+ toolbar in both studios, because a submit that never reached the provider
288
+ leaves no task row to open it from.
289
+
290
+ ### 1.7.4 — timing bugs the user actually hits
291
+
292
+ - **One artifact the host refuses no longer loses the whole skill run.** A
293
+ canvas skill hands each produced file to the host, which hard-refuses some
294
+ extensions (`.svg`, `.html`, …) and anything over 50MB. One refusal used to
295
+ bubble out of the whole run, discarding the artifacts that had already been
296
+ published to the canvas; the user saw one error and nothing else. The refused
297
+ file is now skipped, the rest land, and a localized warning names it.
298
+ - **A failing read stream no longer takes the host process down.** A clip is
299
+ served by piping the file to the response. `pipe` forwards errors to the
300
+ destination, never to the source, so an unhandled `error` on the read stream
301
+ terminated the whole web process. The source now has its own listener and
302
+ ends one response instead.
303
+ - **Switching canvases neither drops a pending edit nor resurrects the old
304
+ one.** Autosave is debounced, so a rename followed by a canvas switch could
305
+ silently lose the edit, or let the old canvas's late save answer land on
306
+ screen and replace the new canvas. Writes now carry a sequence number, an
307
+ answer behind the newest edit is discarded, the pending snapshot is flushed
308
+ before the canvas is replaced, and deleting a canvas marks it reconciled so
309
+ the follow-up switch cannot write it back.
310
+ - **The video studio's submit/cancel updates the list immediately.** The task
311
+ list is polled once by the parent (so the tab and the canvas share one view),
312
+ but the tab's own refresh returned early in controlled mode, leaving a new or
313
+ cancelled row invisible for a full 5s poll cycle. The request now travels up
314
+ to that single poller instead of starting a second loop.
315
+
316
+ ### 1.7.3 — small correctness fixes around the edges
317
+
318
+ - **A `~` in the data-folder setting now means the home directory.** The value
319
+ is typed by hand into the settings form, where `~/my-images` is the natural
320
+ thing to write, and `path.resolve` does not expand it — so the host created a
321
+ directory literally named `~` under its working directory and stored
322
+ everything there while the user looked in their home folder. A `~user` form
323
+ is deliberately left alone.
324
+ - **The chat-resize handle's `aria-label` follows the interface language.** It
325
+ is created once and lives outside the React tree, so nothing re-rendered it
326
+ on a DSH language switch and it kept the language it was built in. It now
327
+ subscribes, and releases the subscription when the element is replaced or the
328
+ panel unmounts.
329
+
330
+ ### 1.7.2 — video nodes could not be saved at all
331
+
332
+ **A canvas holding a video node could not be persisted.** The host's canvas
333
+ node guard accepted only `image` / `text` / `file` / `config` while
334
+ `CanvasNodeType` also declares `video`, so `coerceDocument` returned undefined
335
+ and `save()` answered `malformed canvas document`. Video nodes on the canvas
336
+ are this fork's headline feature, which made the feature unusable end to end:
337
+ place a clip, and the canvas stopped saving.
338
+
339
+ No suite caught it because the canvas-video suite answers `/canvas/save` with a
340
+ stub that records the payload, so the real guard never ran against a video
341
+ node. `smoke.mjs` now round-trips a video node through the real route and the
342
+ real store, in all three states a video node can be in (finished clip,
343
+ rendering, failed). Removing `video` from the guard makes that check fail with
344
+ the exact error a user saw.
345
+
346
+ Also in this release:
347
+
348
+ - The full-screen clip preview marks the node when its clip will not load,
349
+ instead of being the same dead player one click deeper.
350
+ - The dead-clip marker is now cleared by reload, single delete, bulk delete and
351
+ regenerate. Regenerating used to leave a node claiming its clip was gone even
352
+ after a fresh clip landed; bulk delete left marks for an undo to restore.
353
+ - The template favorites memo and the template list memo are keyed by the data
354
+ root they were read from. Both were module-level and outlived a repointed
355
+ root, so changing `localStoragePath` could read the old root and write back to
356
+ the new one.
357
+
358
+ ### 1.7.1 — the two fixes 1.7.0 shipped without, plus release hygiene
359
+
360
+ v1.7.0's release asset was uploaded before two fixes landed, and because the
361
+ version constant was not bumped the in-app updater could never offer them
362
+ (`updateAvailable` compares strictly greater). This release carries them, and
363
+ closes the class of gaps that let a release go out with no automated check.
364
+
365
+ **Fixes that were stranded on `main` after v1.7.0 was published**
366
+
367
+ - **A transient status blip no longer fails a video render.** Polling retries
368
+ transient failures (5xx, 429, transport errors) within a bounded budget
369
+ instead of marking a multi-minute render failed, and gives up with the
370
+ provider's own message when the budget runs out.
371
+ - **An unsatisfiable `Range` is answered with 416**, not a silent 200 carrying
372
+ the whole clip.
373
+
374
+ **Resource bounds (host side)**
375
+
376
+ - The image and video task queues keep only the newest terminal tasks (200 and
377
+ 100) alongside every live one. A finished image task pins its base64 result,
378
+ so the previous unbounded array held every image generated in the session.
379
+ - A heavy skill run's working directory is removed once the run reaches a
380
+ terminal state. Its outputs are already ingested into the canvas asset store
381
+ by then, which is what makes the deletion safe.
382
+ - Deleting a canvas now sweeps assets nothing references any more. The sweep is
383
+ deliberately conservative — a page that fails to parse aborts it, so a bug
384
+ here can leave garbage but cannot delete a file in use. Both reference
385
+ sources are collected: `metadata.asset` and `backgroundImage`.
386
+
387
+ **One data root**
388
+
389
+ - The "open data folder" route, the template snapshot cache and the template
390
+ favorites all used to hardcode `~/.dsh/dsh-imagegen` while the history,
391
+ gallery, video and canvas stores honoured `DSH_HOME` and a configured
392
+ `localStoragePath`. With either set, the button opened the wrong directory
393
+ and the template data lived in a second tree. All four now resolve through
394
+ `imageDataRoot()`, lazily, so a path set after import is still honoured.
395
+
396
+ **Network calls have ceilings**
397
+
398
+ - Prompt enhancement and the `/models` discovery walk are bounded (60s/30s),
399
+ and the vision call behind layer split is bounded (120s). The caller's own
400
+ cancellation is combined with the timeout, not replaced by it.
401
+
402
+ **Canvas UI**
403
+
404
+ - A video node whose clip is gone (history cleared, or evicted by the 30-entry
405
+ cap) now says so, with a retry and a remove control, instead of rendering a
406
+ dead player with no explanation.
407
+ - The canvas revision-conflict self-heal branches on the stable
408
+ `canvas-conflict` code instead of matching a translated message string.
409
+ - Hardcoded Chinese copy in the tool view, the panel resize handle, the video
410
+ panel and the canvas moved into the locale dictionaries, so the English and
411
+ Russian UIs no longer show Chinese.
412
+
413
+ **Release and verification**
414
+
415
+ - `package.json` `files` no longer ships `docs/videos/` (37 MiB of demo
416
+ recordings no shipped document references) or the rest of `src/`; the
417
+ bundled prompt snapshots stay, because the runtime reads them relative to
418
+ `lib/`.
419
+ - **CI** (`.github/workflows/ci.yml`): typecheck, build, the full suite with
420
+ ffmpeg installed, and an inspection of the packed archive that fails if the
421
+ tarball is missing the runtime files or has the demo recordings back in it.
422
+ - `scripts/verify-frame-grab.mjs` is part of `pnpm test` now, and uses a fresh
423
+ browser profile per run — a fixed path made its second run fail with
424
+ "CDP did not come up" and no hint why.
425
+ - `scripts/verify-video-layout.mjs` validates its fixture's class names against
426
+ the real component and stylesheet, so a renamed class fails the guard instead
427
+ of silently measuring markup the panel no longer renders.
428
+ - `scripts/smoke.mjs` gained a per-check timeout, and its data-folder assertion
429
+ now checks the route follows `DSH_HOME` rather than the literal `.dsh` path —
430
+ the old assertion passed precisely because the route was hardcoded.
431
+ - `scripts/smoke-compose.mjs` reports how many checks were skipped, so a
432
+ missing ffmpeg is visible instead of looking like a clean run.
433
+ - The update installer refuses GitHub's auto-generated source archives
434
+ (`<repo>-<tag>.tar.gz`), which carry no build output and would install a
435
+ plugin that cannot load.
436
+
437
+ **Docs and licensing**
438
+
439
+ - `THIRD_PARTY_NOTICES.md` now credits every upstream the bundled snapshots
440
+ actually come from — including `freestylefly/awesome-gpt-image-2` (MIT, the
441
+ largest single source), which was missing entirely — and states which
442
+ snapshots pin a commit versus which reference a moving branch. It also flags
443
+ `songguoxs/gpt4o-image-prompts`, which declares **no licence** upstream and is
444
+ the source of six on-demand image URLs, as an open question rather than
445
+ quietly listing it as fine.
446
+ - The compose and skill-config contracts were corrected against the code: a
447
+ type name that never existed (`ClipComposeResult` → `ClipStitchResult`), a
448
+ claim that `'compose'` appears as a video-panel mode tab (it does not — no
449
+ model catalog declares it), and a secret-masking claim that omitted the
450
+ four-character threshold in `mask()`.
451
+ - The issue templates lost a duplicated install-method option and an
452
+ outdated version placeholder.
453
+
454
+ ### 1.7.0 — first published release of the fork
455
+
456
+ The fork's first published release. It ships as a **GitHub Release asset**
457
+ (`q253595422-dsh-imagegen-video-1.7.0.tgz`), not as an npm package. Everything in
458
+ "What the fork adds" below is in this release.
459
+
460
+ - **The version line is this repository's own.** It starts at 1.7.0 rather than
461
+ continuing upstream's 1.6.x, because this fork is an independent project and
462
+ its releases are not upstream releases. (At the time, 1.6.8 would also have
463
+ looked like it *predated* the very baseline it derives from, since upstream's
464
+ own line had moved on.)
465
+ - **No absolute paths ship in the tarball.** `lib/` is published (it is in
466
+ `files`), and Rolldown stamps each module's id into a `//#region <id>` banner
467
+ that survives into the bundle. The CSS Modules virtual id used to be the CSS
468
+ file's absolute path, so `lib/client.js` embedded ten
469
+ `/Users/<name>/…/src/client/*.module.css.mjs` strings and the bundle's bytes
470
+ depended on where it was checked out. The virtual id is now repo-relative
471
+ (`\0dsh-css:src/client/panel.module.css.mjs`), which is stable, unique, and
472
+ reveals nothing the repository does not already publish.
473
+ - **The in-app update button installs the release archive.** `installUpdate` used
474
+ to run `dsh plugin add <package>@<version>`, which is a registry lookup: with no
475
+ npm publication it resolves to nothing and the button would fail while looking
476
+ like it worked. It now reads the archive URL from the release's own assets and
477
+ hands that URL to `dsh plugin add`. Two guards came with it — a release with no
478
+ archive is refused instead of falling back to a registry spec, and the download
479
+ host must be GitHub, because the URL arrives from the network. Asset ranking is
480
+ by name (the package archive for this version first), so a release that also
481
+ carries another version-named archive cannot win by alphabetical accident.
482
+
483
+ ## What the fork adds
484
+
485
+ ### Video generation (the original motivation for the fork)
486
+
487
+ Upstream generates images only. The fork adds a complete video pipeline:
488
+
489
+ - **Contract layer** — video types and the `VIDEO_API` route family, a video
490
+ model catalog with per-family request shaping (`src/video-engine.ts`,
491
+ `src/video-model-catalog.ts`).
492
+ - **Host side** — an async submit → poll → download engine, a bounded video
493
+ history store with Range-capable clip streaming, a serial task queue, and
494
+ per-channel video-model catalogs in settings.
495
+ - **Studio UI** — a video tab next to the image studio: text-to-video,
496
+ image-to-video, keyframe (first/last frame) and multi-reference modes, model
497
+ picker, polling with progress, an inline player and clip history.
498
+ - **Agent tools** — host-side video tools (generate, from-image, poll, cancel)
499
+ so an Agent can drive the pipeline too.
500
+ - **Conversation** — a finished clip can be attached to the conversation draft
501
+ through the existing file-draft bridge.
502
+
503
+ ### Infinite canvas: video nodes
504
+
505
+ - Video nodes render inline with generating/error states and a footer.
506
+ - A canvas video run reports "rendering", not "failed", while it is in flight.
507
+ - Node toolbar and context menu: clip preview, download, frame grab, and
508
+ favorite to the asset library.
509
+ - A finished clip can be added to the canvas as a reference node (the bytes are
510
+ not copied into the canvas asset store).
511
+ - Video nodes carry the same controls as image nodes: a per-node video model
512
+ picker and "regenerate with the current model".
513
+
514
+ ### Infinite canvas: clip stitching (compose)
515
+
516
+ Select two or more video nodes and stitch them into one clip with the local
517
+ `ffmpeg`, in canvas reading order (top-to-bottom, left-to-right).
518
+
519
+ - **Ordering** — `src/clip-order.ts` is a shared pure module: the browser draws
520
+ the ①②③ badges and the host builds the concat list from the same function, so
521
+ what the user confirms is what gets encoded. It went through nine revisions;
522
+ each earlier revision was certified green by the suites that existed at the
523
+ time. The criteria and the known suite blind spots are documented in
524
+ `docs/compose-contract.md`.
525
+ - **Compose is a canvas skill run**, not a new video mode and not a new route:
526
+ `compose.videos` has `tier: 'local'` (a local subprocess — no model, no agent,
527
+ no network). Stitched clips land in the video history, so they appear in the
528
+ sidebar and can be favorited into the asset library.
529
+ - Clips are read from **both** stores (video history and gallery favorites).
530
+ A clip that cannot be read fails the whole run rather than being silently
531
+ skipped, so five selected clips never become a three-clip output.
532
+
533
+ ### Asset library and history unification
534
+
535
+ - Clip history is merged into the image history sidebar instead of living in a
536
+ separate list.
537
+ - Favorited clips render in the asset library with a "video" badge, a play
538
+ overlay, download, and a full-screen viewer.
539
+ - The sidebar refreshes the clip list when a render finishes, so a new row
540
+ appears without a page reload.
541
+
542
+ ### Fixes to inherited behaviour
543
+
544
+ - **The update check no longer points at upstream.** `src/updater.ts` carried
545
+ `PACKAGE_NAME = '@dickpy/dsh-imagegen'` and upstream's GitHub Releases API URL,
546
+ so this fork's "check for updates" would have offered upstream's release notes
547
+ and installed upstream's package over itself. Both now point at this
548
+ repository. (Same class of problem as the package rename below.)
549
+ - The video studio composer was capped at 420px while an empty column took the
550
+ remaining width; it now uses the full panel width (guarded by
551
+ `scripts/verify-video-layout.mjs`).
552
+ - The shared-chunk filename is pinned so reinstalling built artifacts is a plain
553
+ overwrite instead of leaving a stale hashed chunk behind.
554
+ - Polling uses the resource `id`, because at least one upstream returns both
555
+ `id` and `task_id` in a flat body.
556
+ - `scripts/verify-frame-grab.mjs` no longer hardcodes one machine's ffmpeg and
557
+ Chrome paths (and a fixed CDP port); it discovers both, and skips loudly if
558
+ either is missing.
559
+
560
+ ### This repository's identity
561
+
562
+ The fork publishes under its own name so it cannot be confused with, or
563
+ mistaken for, upstream:
564
+
565
+ | Where | Value |
566
+ | --- | --- |
567
+ | release archive (`q253595422-dsh-imagegen-video-<version>.tgz`) | `https://github.com/q253595422/dsh-imagegen-video/releases` |
568
+ | plugin id (stamped by `tsdown.config.ts` into the `__ModuleLoader__` handoff) | `@q253595422/dsh-imagegen-video` |
569
+ | `cordis.patch.yml` row `name` | `@q253595422/dsh-imagegen-video` |
570
+ | update check (`src/updater.ts`) | this repository |
571
+
572
+ The npm package name is `@q253595422/dsh-imagegen-video` as well, but nothing is
573
+ published there: the fork distributes through GitHub Releases, so both the README
574
+ and the in-app update button install the release asset. `src/updater.ts` reads the
575
+ asset URL from the release itself and refuses to install a release that carries no
576
+ archive, rather than falling back to a registry spec that would resolve to nothing.
577
+
578
+ The **runtime identity is deliberately left alone**: the `/api/dsh-imagegen/*`
579
+ route family, the `dsh-imagegen` settings namespace, the `~/.dsh/dsh-imagegen/`
580
+ data root and the `dsh-imagegen:*` client events/storage keys all keep their
581
+ inherited names. Renaming them would orphan the settings and data of anyone
582
+ switching between the two installs, and the route prefix is upstream's design,
583
+ not this fork's.
584
+
585
+ ### Test infrastructure
586
+
587
+ Upstream ships one smoke suite. The fork adds offline suites that run without
588
+ network access or paid upstream calls, plus a guard for each defect found:
589
+
590
+ | Suite | Covers |
591
+ | --- | --- |
592
+ | `scripts/smoke-video.mjs` | video studio: modes, catalog, polling, history |
593
+ | `scripts/smoke-canvas-video.mjs` | canvas video nodes, ordering badges, compose entry points, video-node controls |
594
+ | `scripts/smoke-video-picker.mjs` | the in-canvas video picker (three tabs, upload, empty/error states) |
595
+ | `scripts/smoke-compose.mjs` | stitching end-to-end against a fake composer, plus one real-`ffmpeg` case |
596
+ | `scripts/verify-b-defect.mjs` | clip resolution against both stores, with real files on disk |
597
+ | `scripts/verify-video-layout.mjs` | real-browser check that the composer fills the panel |
598
+ | `scripts/verify-frame-grab.mjs` | real-browser proof of the frame-grab path (seek + capture) |
599
+ | `scripts/with-lock.sh` | serializes build/test runs across a shared working tree |
600
+
601
+ Browser-dependent suites **skip loudly and exit 0** when the browser or
602
+ `ffmpeg` is missing, so a contributor without those tools gets a clear "not
603
+ verified" instead of a false failure.
604
+
605
+ ## Configuration notes for this fork
606
+
607
+ - The `@q253595422/dsh-imagegen-video` package **cannot be installed alongside
608
+ upstream `@dickpy/dsh-imagegen`**: both register the same
609
+ `/api/dsh-imagegen/*` route family and both inject the same UI panel. The
610
+ route prefix is inherited from upstream, not introduced here.
611
+ - Data (history, gallery, canvases, settings) lives under
612
+ `~/.dsh/dsh-imagegen/` and keeps the inherited name, so it is *shared* with an
613
+ upstream install. Back it up before switching between the two.
614
+
615
+ ## Not in this fork
616
+
617
+ - No paid upstream verification is performed by the test suites; all upstream
618
+ interactions are replayed from fixtures.