scenri 0.9.4 → 0.10.1

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 (57) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/dist/{chunk-3ZRMKVBB.js → chunk-UUYMUOZQ.js} +148 -5
  3. package/dist/{cli-MQO66C5O.js → cli-3KKS7BE4.js} +3 -3
  4. package/dist/index.js +2 -2
  5. package/dist/serve.js +2266 -339
  6. package/dist/{src-3ITTV2UE.js → src-QZZDZM7I.js} +3 -3
  7. package/package.json +1 -1
  8. package/studio-dist/assets/facial-2-BmVnDzjJ.webp +0 -0
  9. package/studio-dist/assets/facial-3-DZtKCJBc.webp +0 -0
  10. package/studio-dist/assets/facial-4-D3bBk4Y-.webp +0 -0
  11. package/studio-dist/assets/facial-5-BZUOhBlt.webp +0 -0
  12. package/studio-dist/assets/facial-6-BjNlybGd.webp +0 -0
  13. package/studio-dist/assets/freckles-1-EZ0x3-vK.webp +0 -0
  14. package/studio-dist/assets/freckles-2-DuHEq26I.webp +0 -0
  15. package/studio-dist/assets/freckles-3-WGP6r0Fh.webp +0 -0
  16. package/studio-dist/assets/glasses-1-DPlo67j6.webp +0 -0
  17. package/studio-dist/assets/glasses-2-mRgaHYWV.webp +0 -0
  18. package/studio-dist/assets/glasses-3-DuTs0m8y.webp +0 -0
  19. package/studio-dist/assets/glasses-4-DGjoOjFS.webp +0 -0
  20. package/studio-dist/assets/glasses-5-DoEVdOaY.webp +0 -0
  21. package/studio-dist/assets/glasses-6-CcUVUrfz.webp +0 -0
  22. package/studio-dist/assets/index-D1MJJGXq.js +107 -0
  23. package/studio-dist/assets/{index-BGts89lc.css → index-ELZxH19c.css} +1 -1
  24. package/studio-dist/assets/length-1-androgynous-DiFL-HbJ.webp +0 -0
  25. package/studio-dist/assets/length-1-man-OI1u_7yA.webp +0 -0
  26. package/studio-dist/assets/length-1-woman-BbYkzjnG.webp +0 -0
  27. package/studio-dist/assets/length-2-androgynous-zBgi1XzH.webp +0 -0
  28. package/studio-dist/assets/length-2-man-Df3fVDUO.webp +0 -0
  29. package/studio-dist/assets/length-2-woman-BP1NSRcI.webp +0 -0
  30. package/studio-dist/assets/length-3-androgynous-BN_SLD6d.webp +0 -0
  31. package/studio-dist/assets/length-3-man-Dk_3OkKz.webp +0 -0
  32. package/studio-dist/assets/length-3-woman-BFV6b4If.webp +0 -0
  33. package/studio-dist/assets/length-4-androgynous-BblLWRi-.webp +0 -0
  34. package/studio-dist/assets/length-4-man-Cszq08t7.webp +0 -0
  35. package/studio-dist/assets/length-4-woman-B8gzWrbX.webp +0 -0
  36. package/studio-dist/assets/length-5-androgynous-DwxP_mgy.webp +0 -0
  37. package/studio-dist/assets/length-5-man-Yt6YRiWE.webp +0 -0
  38. package/studio-dist/assets/length-5-woman-BTz0YJMm.webp +0 -0
  39. package/studio-dist/assets/length-6-androgynous-BD7OoH6P.webp +0 -0
  40. package/studio-dist/assets/length-6-man-oceHSSPF.webp +0 -0
  41. package/studio-dist/assets/length-6-woman-BvX37wCz.webp +0 -0
  42. package/studio-dist/assets/makeup-1-CvGOSJO_.webp +0 -0
  43. package/studio-dist/assets/makeup-3-yoncgA8G.webp +0 -0
  44. package/studio-dist/assets/makeup-4-Buulx0wj.webp +0 -0
  45. package/studio-dist/assets/makeup-5-5Y_Qpn53.webp +0 -0
  46. package/studio-dist/assets/makeup-6-CSQm_m8e.webp +0 -0
  47. package/studio-dist/assets/prosthetic-1-C-bbKoKE.webp +0 -0
  48. package/studio-dist/assets/prosthetic-2-DCGXnKzb.webp +0 -0
  49. package/studio-dist/assets/prosthetic-3-BW_gdjlG.webp +0 -0
  50. package/studio-dist/assets/prosthetic-4-ec_5-7Tb.webp +0 -0
  51. package/studio-dist/assets/prosthetic-5-res5u7gj.webp +0 -0
  52. package/studio-dist/assets/prosthetic-6-0AvaEGMf.webp +0 -0
  53. package/studio-dist/assets/tattoo-3-33-3gd4C.webp +0 -0
  54. package/studio-dist/assets/tattoo-4-BW22hxdb.webp +0 -0
  55. package/studio-dist/assets/tattoo-5-BWJwjBfm.webp +0 -0
  56. package/studio-dist/index.html +2 -2
  57. package/studio-dist/assets/index-uiX1mW43.js +0 -104
package/CHANGELOG.md CHANGED
@@ -1,5 +1,89 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.10.1](https://github.com/tonygorb/Scenri/compare/v0.10.0...v0.10.1) (2026-09-16)
4
+
5
+
6
+ ### Features
7
+
8
+ * add placeTip function for hover card positioning ([de6d069](https://github.com/tonygorb/Scenri/commit/de6d069024b3d0c4b305f945a15322b1290d6458))
9
+ * add presenter duplication and deletion functionality ([11c83be](https://github.com/tonygorb/Scenri/commit/11c83be7ee167435fc8a6656c938380ec9b08c68))
10
+ * duplicate or delete a saved presenter from its card ([2e4dc00](https://github.com/tonygorb/Scenri/commit/2e4dc008f3bccbf9a9b2e444331bd2627775f1f4))
11
+ * enhance presenter view and retry functionality ([dbeb9e9](https://github.com/tonygorb/Scenri/commit/dbeb9e9e48c52510ebb9ca2949a27e0cebfb69ca))
12
+
13
+
14
+ ### Bug Fixes
15
+
16
+ * a fresh library keeps started_at through the status rebuild ([14aced1](https://github.com/tonygorb/Scenri/commit/14aced1fa5d0b841e8202b639e3ae95b615598ac))
17
+ * centre the Create first run and dock the assets panel from 1024 ([b102204](https://github.com/tonygorb/Scenri/commit/b102204d3e42107319a72923aea28de3e04298f2))
18
+ * retry in the same card, side profiles, chip fields and rails ([0fd3099](https://github.com/tonygorb/Scenri/commit/0fd309969320b0fe8f22cba0940cdf16f5bc68a4))
19
+ * type and lint errors in the chip field and the reference set ([53e5314](https://github.com/tonygorb/Scenri/commit/53e5314120de03cf7a5c4c3ef1d9946655e567a3))
20
+
21
+
22
+ ### Miscellaneous Chores
23
+
24
+ * **release:** pin the next version to 0.10.1 ([85a5df4](https://github.com/tonygorb/Scenri/commit/85a5df4711c81c30b60b18dfbd4a16098adff98b))
25
+
26
+ ## [0.10.0](https://github.com/tonygorb/Scenri/compare/v0.9.4...v0.10.0) (2026-09-16)
27
+
28
+
29
+ ### Features
30
+
31
+ * presenter creation as a conversation, with a view and an editor ([39e1a36](https://github.com/tonygorb/Scenri/commit/39e1a3633ab27712a628508fd5c1db51c3bcfb51))
32
+ * **presenter:** the studio arrives the way everything else you make here does ([7fee9b7](https://github.com/tonygorb/Scenri/commit/7fee9b77160475490c5e4dc39dd19725f7e2ba53))
33
+ * **studio:** show an import where you are, and fill the wall as it lands ([667d8ac](https://github.com/tonygorb/Scenri/commit/667d8acd913818400b5ea7fee0502439e06fc0e9))
34
+
35
+
36
+ ### Bug Fixes
37
+
38
+ * **brand:** teach the brand scraper the manners the crawler already had ([b62f4da](https://github.com/tonygorb/Scenri/commit/b62f4da7d929dc9a3fdbdba5c4840e5bba3c9c84))
39
+ * **catalog:** a card asks for a thumbnail, not the picture it will import ([2a91200](https://github.com/tonygorb/Scenri/commit/2a91200a96cc240e7b0e144ece6670747efccee3))
40
+ * **catalog:** capture the site's own icon, and open the import from its dock ([cb76556](https://github.com/tonygorb/Scenri/commit/cb76556c1b85bd001deffbc9e1f3f35595bca679))
41
+ * **catalog:** count a catalogue the budget can actually reach ([8ba5d85](https://github.com/tonygorb/Scenri/commit/8ba5d85111e46c6c4a0afb7c48c9b972c067a168))
42
+ * **catalog:** count the pictures a run owes, not the ones it has looked at ([7913f21](https://github.com/tonygorb/Scenri/commit/7913f21949ea58823f0eab1a19bd175a84ae376c))
43
+ * **catalog:** count what a person is missing, in the unit they asked in ([e763b93](https://github.com/tonygorb/Scenri/commit/e763b93cdf0cfce12a84fe6a5e703e55bc7db973))
44
+ * **catalog:** read a guarded endpoint as guarded, not as a busy one ([1643170](https://github.com/tonygorb/Scenri/commit/1643170dcb93c80cc06d5be565e85d6aa7dfdc9a))
45
+ * **catalog:** reliable website import — accurate counts, guarded endpoints, live progress ([37658f5](https://github.com/tonygorb/Scenri/commit/37658f53a846d42bc87b7fc522c68c589aef20a6))
46
+ * **cli:** a revision does not carry a picture of the face it replaced ([072a353](https://github.com/tonygorb/Scenri/commit/072a353f8c43328df3e8b066a1f78637398c5394))
47
+ * **cli:** putting a picture back is a swap, not a decision to build the next view ([12d1787](https://github.com/tonygorb/Scenri/commit/12d17879e711a8275e34f27879030f5082226439))
48
+ * **composer:** a chip for a presenter who is gone says so ([58de149](https://github.com/tonygorb/Scenri/commit/58de1497623ab4b512eca3a3ac7e77d73a067f24))
49
+ * **core:** a draft becoming a saved presenter is one write ([970c0dd](https://github.com/tonygorb/Scenri/commit/970c0dd2c1a571ba524a719bf05c4b06d653f454))
50
+ * **presenter:** "Describe someone instead" describes someone ([b115d2e](https://github.com/tonygorb/Scenri/commit/b115d2e70a1e0642b4350d74c1149ba3804539b8))
51
+ * **presenter:** a record from before the studio opens, reads right, and can be drawn ([24f6ae1](https://github.com/tonygorb/Scenri/commit/24f6ae1dfaa3a92148c9caad2c915965eb702755))
52
+ * **presenter:** a reload is not a way of holding state ([3f7231d](https://github.com/tonygorb/Scenri/commit/3f7231dd6d5e51ae71ac46dbf58dcd2dc6df77fa))
53
+ * **presenter:** a view drawn from a face that moved lands stale, not current ([ce7d5fe](https://github.com/tonygorb/Scenri/commit/ce7d5fe6b40c93173a937095902089803352fe4b))
54
+ * **presenter:** an answer is installed only for the draft it was asked about ([c62ae1a](https://github.com/tonygorb/Scenri/commit/c62ae1ac9a261d319295f30e76f113bec1a6f845))
55
+ * **presenter:** arriving at a draft that is drawing reads its conversation ([edec9e3](https://github.com/tonygorb/Scenri/commit/edec9e35d213a0e6db30f78792cd1cffd098daff))
56
+ * **presenter:** Create presenter is a new presenter, every time ([4188c42](https://github.com/tonygorb/Scenri/commit/4188c4230865981366fb3ccccfe4c55ea40788b3))
57
+ * **presenter:** deleting a presenter reaches every surface, and takes its session with it ([73ceea2](https://github.com/tonygorb/Scenri/commit/73ceea292f3c124aeb85903dc0b40677e1cdf4c7))
58
+ * **presenter:** opening a draft does not ask a question it is about to withdraw ([4e4c6a4](https://github.com/tonygorb/Scenri/commit/4e4c6a427797f721cdb936474f3d7778a4de3a6b))
59
+ * **presenter:** the close on a phone head sits on the screen, and in the middle of the bar ([54634bd](https://github.com/tonygorb/Scenri/commit/54634bdcd5a050ad3cabc032bef67a68d22e001f))
60
+ * **presenter:** the editor gets the latch the creation flow already has ([56119c7](https://github.com/tonygorb/Scenri/commit/56119c7731ab71d69806a99944531b33235eedf3))
61
+ * **presenter:** the picture is simply there, and a picture put back brings its view up ([8665646](https://github.com/tonygorb/Scenri/commit/866564696cab338829c77dee9851bb65a69f7cef))
62
+ * **presenter:** the question over a picture put back says what happened to it ([d678a96](https://github.com/tonygorb/Scenri/commit/d678a969aa92260d0d02f6dcb615b78eb477dd80))
63
+ * **presenter:** the stage is the shape of the picture, so a bad draw is not a crop ([52f6bde](https://github.com/tonygorb/Scenri/commit/52f6bde7b6dfbade987b1e24324315c62a950a48))
64
+ * **presenter:** throwing an unfinished person away asks first ([9226470](https://github.com/tonygorb/Scenri/commit/9226470fe7cb0001695699f446d5e4c2f16ad579))
65
+ * **presenter:** without an engine, the photo is the face rather than a dead end ([8ddae49](https://github.com/tonygorb/Scenri/commit/8ddae49a74a191e126024d65340ee89ab78d84c9))
66
+ * **studio:** a discarded draft hands its place on, instead of dropping focus ([5180237](https://github.com/tonygorb/Scenri/commit/5180237bcbf18d2d76eff0cbe64f6644084ad071))
67
+ * **studio:** a kit you walk away from is not a brand you made ([03b3613](https://github.com/tonygorb/Scenri/commit/03b36139699e665669e75d7722c170930c8ec87b))
68
+ * **studio:** close the silent paths through onboarding and import ([441d310](https://github.com/tonygorb/Scenri/commit/441d310ecdd6e30ab63f117c19507036bff06513))
69
+ * **studio:** count one thing in the task row, and say what a stop kept ([0aba7e5](https://github.com/tonygorb/Scenri/commit/0aba7e5ee42b1a05b9a4635a2557c3a0bca2a5c3))
70
+ * **studio:** offer the products a listing found, not only the ones read ([4ea36ef](https://github.com/tonygorb/Scenri/commit/4ea36ef0890a854e19375a865e2c8190e97b78c7))
71
+ * **studio:** one press is one act, for Enter and for a confirm ([d43a8c7](https://github.com/tonygorb/Scenri/commit/d43a8c7f99a083f2d8d559d02475f89e03952464))
72
+ * **studio:** say what was kept, not only what went wrong ([70f1855](https://github.com/tonygorb/Scenri/commit/70f1855ce4945d72f56bf43ece8080b5050cafa5))
73
+ * **studio:** say when the look for a shop did not finish ([be0e2f6](https://github.com/tonygorb/Scenri/commit/be0e2f600673f3f1c8f8aef36d80b7cba0430212))
74
+ * **studio:** Start over keeps drawn work, and lets go of a draft with none ([f5d1328](https://github.com/tonygorb/Scenri/commit/f5d132824d030ab604cf52a97f71c5a73118464f))
75
+ * **studio:** the import pill wears the brand's mark and a clock ([4b78b1a](https://github.com/tonygorb/Scenri/commit/4b78b1a6e93780e5389ce2990af929c3dc515638))
76
+ * **studio:** the presenter studio says what it will spend, like the other two flows ([3bb039c](https://github.com/tonygorb/Scenri/commit/3bb039cf22c5950fd5226f22b93405477a216a4c))
77
+ * **test:** keep the private repo out of the battery drivers' defaults ([cd0f674](https://github.com/tonygorb/Scenri/commit/cd0f674d4f7b98490bf70c840ef8c9292c5f8f53))
78
+ * **test:** read the reference image's basename, not its POSIX split ([442cc07](https://github.com/tonygorb/Scenri/commit/442cc07bdedb02c62a695b9c22ac090caee2d890))
79
+ * **test:** the analyzer's imagePaths assertion had the same split('/') bug ([7877088](https://github.com/tonygorb/Scenri/commit/787708802c94f46be158c4f3e93c147e0973ee97))
80
+
81
+
82
+ ### Performance Improvements
83
+
84
+ * **catalog:** ask the listing for a chosen set, not a page each ([cfe9a3c](https://github.com/tonygorb/Scenri/commit/cfe9a3c3aa7411ae1e8fe6d5002a2f1b4397de53))
85
+ * **catalog:** keep the catalogue the store already handed over ([9630378](https://github.com/tonygorb/Scenri/commit/9630378ed2e076f6cb9b7ce50dde28c68e615548))
86
+
3
87
  ## [0.9.4](https://github.com/tonygorb/Scenri/compare/v0.9.3...v0.9.4) (2026-09-14)
4
88
 
5
89
 
@@ -2,7 +2,7 @@ import { homedir } from 'os';
2
2
  import { join } from 'path';
3
3
  import Database from 'better-sqlite3';
4
4
  import { randomUUID, createHash } from 'crypto';
5
- import { mkdirSync, existsSync, chmodSync, readFileSync, writeFileSync, readdirSync, rmSync } from 'fs';
5
+ import { mkdirSync, existsSync, chmodSync, rmSync, readFileSync, writeFileSync, readdirSync } from 'fs';
6
6
 
7
7
  // ../core/src/index.ts
8
8
 
@@ -173,6 +173,14 @@ CREATE TABLE IF NOT EXISTS import_jobs (
173
173
  finished_at TEXT
174
174
  );
175
175
  CREATE INDEX IF NOT EXISTS idx_import_jobs_brand ON import_jobs(brand_id);
176
+ CREATE TABLE IF NOT EXISTS presenter_drafts (
177
+ id TEXT PRIMARY KEY,
178
+ brand_id TEXT NOT NULL REFERENCES brands(id) ON DELETE CASCADE,
179
+ json TEXT NOT NULL,
180
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
181
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
182
+ );
183
+ CREATE INDEX IF NOT EXISTS idx_presenter_drafts_brand ON presenter_drafts(brand_id);
176
184
  `;
177
185
  function widenNodeStatusCheck(db) {
178
186
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='nodes'").get();
@@ -197,12 +205,13 @@ function widenNodeStatusCheck(db) {
197
205
  brief TEXT,
198
206
  archived INTEGER NOT NULL DEFAULT 0,
199
207
  duration_ms INTEGER,
208
+ started_at TEXT,
200
209
  batch_id TEXT,
201
210
  batch_index INTEGER NOT NULL DEFAULT 0
202
211
  );
203
212
  INSERT INTO nodes_new
204
213
  SELECT id, project_id, parent_id, kind, prompt, engine_id, status, images, cost_usd, kept, error,
205
- created_at, overlays, brief, archived, duration_ms, batch_id, batch_index
214
+ created_at, overlays, brief, archived, duration_ms, started_at, batch_id, batch_index
206
215
  FROM nodes;
207
216
  DROP TABLE nodes;
208
217
  ALTER TABLE nodes_new RENAME TO nodes;
@@ -517,6 +526,9 @@ function openDb(homeDir) {
517
526
  if (!nodeCols.includes("duration_ms")) {
518
527
  db.exec("ALTER TABLE nodes ADD COLUMN duration_ms INTEGER");
519
528
  }
529
+ if (!nodeCols.includes("started_at")) {
530
+ db.exec("ALTER TABLE nodes ADD COLUMN started_at TEXT");
531
+ }
520
532
  if (!nodeCols.includes("batch_id")) {
521
533
  db.exec("ALTER TABLE nodes ADD COLUMN batch_id TEXT");
522
534
  }
@@ -577,6 +589,12 @@ function createImageStore(homeDir) {
577
589
  },
578
590
  has(hash) {
579
591
  return /^[a-f0-9]{32}$/.test(hash) && EXTS.some((ext) => existsSync(fileFor(hash, ext)));
592
+ },
593
+ remove(hash) {
594
+ const file = this.pathFor(hash);
595
+ if (!existsSync(file)) return false;
596
+ rmSync(file, { force: true });
597
+ return true;
580
598
  }
581
599
  };
582
600
  }
@@ -674,6 +692,9 @@ function uniqueSetSlug(db, brandId, name, id) {
674
692
  return firstFree(slugifyWithId(name, id, "set"), (c) => !!stmt.get(brandId, c, id));
675
693
  }
676
694
  var SET_NAME_SEP = String.fromCharCode(31);
695
+ function rowToPresenterDraft(r) {
696
+ return { id: r.id, brandId: r.brand_id, json: JSON.parse(r.json), createdAt: r.created_at, updatedAt: r.updated_at };
697
+ }
677
698
  function rowToSet(r) {
678
699
  return {
679
700
  id: r.id,
@@ -690,7 +711,7 @@ var LINEAGE_SIBLINGS_RADIUS = 25;
690
711
  var LINEAGE_CHILDREN_MAX = 60;
691
712
  var LINEAGE_HISTORY_MAX = 60;
692
713
  var FEED_COLS = `n.id, n.project_id, n.parent_id, n.kind, substr(n.prompt, 1, ${PROMPT_HEAD_CHARS}) AS prompt_head,
693
- n.engine_id, n.status, n.images, n.cost_usd, n.duration_ms, n.kept, n.error, n.created_at, n.brief, n.archived,
714
+ n.engine_id, n.status, n.images, n.cost_usd, n.duration_ms, n.kept, n.error, n.created_at, n.started_at, n.brief, n.archived,
694
715
  n.batch_id, n.batch_index, ${CHILD_COUNT_SQL} AS child_count`;
695
716
  function rowToFeedNode(r) {
696
717
  return {
@@ -707,6 +728,7 @@ function rowToFeedNode(r) {
707
728
  kept: !!r.kept,
708
729
  error: r.error,
709
730
  createdAt: r.created_at,
731
+ startedAt: r.started_at ?? r.created_at,
710
732
  brief: r.brief ? JSON.parse(r.brief) : null,
711
733
  archived: !!r.archived,
712
734
  batchId: r.batch_id ?? null,
@@ -850,6 +872,21 @@ function createStore(db) {
850
872
  );
851
873
  return this.getBrand(id);
852
874
  },
875
+ /**
876
+ * A draft becoming a saved asset: the document and the row move together.
877
+ *
878
+ * These were two statements with nothing holding them. A crash between
879
+ * them left the presenter in the brand AND its draft row alive, so the
880
+ * wall showed a finished person and an unfinished one, and saving that
881
+ * draft again appended a second copy of the same person.
882
+ */
883
+ updateBrandAndDropPresenterDraft(id, json, draftId) {
884
+ return db.transaction(() => {
885
+ const row = this.updateBrand(id, json);
886
+ this.deletePresenterDraft(draftId);
887
+ return row;
888
+ })();
889
+ },
853
890
  deleteBrand(id) {
854
891
  db.prepare("DELETE FROM brands WHERE id=?").run(id);
855
892
  },
@@ -972,6 +1009,42 @@ function createStore(db) {
972
1009
  membersOf(setId) {
973
1010
  return db.prepare("SELECT node_id FROM set_nodes WHERE set_id=? ORDER BY added_at, node_id").all(setId).map((r) => r.node_id);
974
1011
  },
1012
+ // presenter drafts
1013
+ /** Upsert: the same id replaces the json and moves updated_at forward; created_at stays. */
1014
+ putPresenterDraft(row) {
1015
+ db.prepare(
1016
+ // Millisecond stamps, the nodes table's own format: a draft is touched
1017
+ // on every step, and two touches inside one second must still order.
1018
+ `INSERT INTO presenter_drafts (id, brand_id, json, created_at, updated_at)
1019
+ VALUES (?,?,?, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'))
1020
+ ON CONFLICT(id) DO UPDATE SET json=excluded.json, updated_at=strftime('%Y-%m-%d %H:%M:%f','now')`
1021
+ ).run(row.id, row.brandId, JSON.stringify(row.json));
1022
+ return this.getPresenterDraft(row.id);
1023
+ },
1024
+ getPresenterDraft(id) {
1025
+ const r = db.prepare("SELECT * FROM presenter_drafts WHERE id=?").get(id);
1026
+ return r ? rowToPresenterDraft(r) : null;
1027
+ },
1028
+ /** Most recently touched first, the same order sets keep. */
1029
+ listPresenterDrafts(brandId) {
1030
+ return db.prepare("SELECT * FROM presenter_drafts WHERE brand_id=? ORDER BY updated_at DESC, created_at DESC, id").all(brandId).map(rowToPresenterDraft);
1031
+ },
1032
+ deletePresenterDraft(id) {
1033
+ db.prepare("DELETE FROM presenter_drafts WHERE id=?").run(id);
1034
+ },
1035
+ /**
1036
+ * Whether any row still points at a stored image: a shot's images, a
1037
+ * shot's brief (source image, attachments), an imported catalog image, or
1038
+ * another presenter draft. The store is content-addressed, so the same
1039
+ * photo uploaded twice is one file; whoever removes an image asks this
1040
+ * first, and the brand documents are the caller's half of the answer.
1041
+ */
1042
+ imageReferenced(hash) {
1043
+ if (!/^[a-f0-9]{32}$/.test(hash)) return false;
1044
+ const like = `%${hash}%`;
1045
+ const hit = (sql, ...args) => !!db.prepare(sql).get(...args);
1046
+ return hit("SELECT 1 FROM nodes WHERE images LIKE ? LIMIT 1", `%"${hash}"%`) || hit("SELECT 1 FROM nodes WHERE brief LIKE ? LIMIT 1", like) || hit("SELECT 1 FROM catalog_images WHERE asset_ref LIKE ? LIMIT 1", like) || hit("SELECT 1 FROM presenter_drafts WHERE json LIKE ? LIMIT 1", like);
1047
+ },
975
1048
  // nodes / version tree
976
1049
  addNode(input) {
977
1050
  if (input.parentId) {
@@ -1044,6 +1117,19 @@ function createStore(db) {
1044
1117
  cancelNode(id) {
1045
1118
  db.prepare("UPDATE nodes SET status='cancelled' WHERE id=?").run(id);
1046
1119
  },
1120
+ /**
1121
+ * Run this shot again on the same row. The card keeps its place: id,
1122
+ * brief, prompt, parent and created_at stay put. Status goes back to
1123
+ * running, the old error and pictures go, so the tile becomes the wait.
1124
+ * started_at moves to now so the clock on the card is this run, not the
1125
+ * minutes the first attempt already spent.
1126
+ */
1127
+ reopenNode(id) {
1128
+ db.prepare(
1129
+ "UPDATE nodes SET status='running', error=NULL, images='[]', duration_ms=NULL, cost_usd=0, started_at=strftime('%Y-%m-%d %H:%M:%f','now') WHERE id=?"
1130
+ ).run(id);
1131
+ return this.getNode(id);
1132
+ },
1047
1133
  getNode(id) {
1048
1134
  const r = db.prepare(`SELECT n.*, ${CHILD_COUNT_SQL} AS child_count FROM nodes n WHERE n.id=?`).get(id);
1049
1135
  return r ? rowToNode(r) : null;
@@ -1711,6 +1797,24 @@ function productImportMethods(db) {
1711
1797
  WHERE product_id=? AND source_url=?`
1712
1798
  ).run(assetRef, meta?.width ?? null, meta?.height ?? null, productId, sourceUrl);
1713
1799
  },
1800
+ /**
1801
+ * How many pictures are still owed, for a progress that can be trusted.
1802
+ *
1803
+ * The importer used to add up the rounds it had fetched and call that the
1804
+ * total, so the total was really "pictures discovered so far": it grew by
1805
+ * sixty every round, and the fraction fell back every time it did. A run
1806
+ * went 60/60, then 64/120, then 122/172 - a bar sliding backwards twice
1807
+ * while nothing had gone wrong. Counting what is left is one query and it
1808
+ * is the actual answer.
1809
+ */
1810
+ countImagesNeedingAssets(brandId, imagesPerProduct) {
1811
+ const row = db.prepare(
1812
+ `SELECT COUNT(*) AS n FROM catalog_images i
1813
+ JOIN catalog_products p ON p.id = i.product_id
1814
+ WHERE p.brand_id=? AND (i.asset_ref IS NULL OR i.asset_ref='') AND i.position < ?`
1815
+ ).get(brandId, imagesPerProduct);
1816
+ return row?.n ?? 0;
1817
+ },
1714
1818
  listImagesNeedingAssets(brandId, limit = 500) {
1715
1819
  return db.prepare(
1716
1820
  `SELECT i.* FROM catalog_images i
@@ -1730,6 +1834,38 @@ function productImportMethods(db) {
1730
1834
  excluded: !!r.excluded
1731
1835
  }));
1732
1836
  },
1837
+ /**
1838
+ * Hide pictures that turn up on product after product.
1839
+ *
1840
+ * A photograph of a thing belongs to that thing. A picture the crawl found
1841
+ * on eighteen different product pages is the shop talking, not the
1842
+ * product: a promotion flash, a collection sticker, a delivery badge.
1843
+ * Measured on a real storefront, three addresses out of 265 were shared
1844
+ * past this threshold and all three were merchandising furniture, while
1845
+ * every genuine packshot appeared once.
1846
+ *
1847
+ * Names nothing and knows nothing about any platform, which is the point:
1848
+ * the badges that caused this carry no word a filter could match, and the
1849
+ * next store's will be different words.
1850
+ *
1851
+ * Excluded rather than deleted, the same state the picture chooser uses,
1852
+ * so anything wrongly caught is still there to be put back.
1853
+ */
1854
+ excludeSharedImages(sourceId, minProducts = 3) {
1855
+ const r = db.prepare(
1856
+ `UPDATE catalog_images SET excluded=1
1857
+ WHERE excluded=0
1858
+ AND product_id IN (SELECT id FROM catalog_products WHERE source_id=?)
1859
+ AND source_url IN (
1860
+ SELECT ci.source_url FROM catalog_images ci
1861
+ JOIN catalog_products cp ON cp.id = ci.product_id
1862
+ WHERE cp.source_id=?
1863
+ GROUP BY ci.source_url
1864
+ HAVING COUNT(DISTINCT ci.product_id) >= ?
1865
+ )`
1866
+ ).run(sourceId, sourceId, minProducts);
1867
+ return r.changes;
1868
+ },
1733
1869
  markMissingUnavailable(sourceId, seenExternalKeys) {
1734
1870
  if (!seenExternalKeys.length) {
1735
1871
  const r2 = db.prepare(
@@ -1952,6 +2088,12 @@ function createCatalogStore(db) {
1952
2088
  var REFERENCE_ROLE_DIRECTIVE = {
1953
2089
  product: "the exact product \u2014 preserve its label, shape, colors and design faithfully; do not redesign it",
1954
2090
  character: "the exact person \u2014 match their face, facial structure, skin, hair and build exactly; their clothing, pose and background are capture context, not styling to reproduce",
2091
+ // One thing in the picture, and nothing else in it. A person uploads a pair
2092
+ // of glasses they like, or the exact tattoo, and every other role would have
2093
+ // handed the model a second face to blend with the first: `character` says
2094
+ // "this is who they are", `style` takes the mood of the whole frame. This
2095
+ // says take the object and leave the rest of the photograph behind.
2096
+ detail: "a reference for one detail only \u2014 match the design, shape, proportions, colours and finish of the thing itself, exactly as drawn or made. Take nothing else from this image: not the identity of anyone in it, not their face, skin, hair, body or build, not their clothing, not the background, not the lighting. The person in the picture being drawn is somebody else entirely",
1955
2097
  brand: "the brand's own mark \u2014 if the direction calls for the mark to appear, reproduce it exactly as drawn, same colours, letterforms and proportions, every character down to the smallest secondary lettering, in its original script and reading direction, never translated, transliterated or re-spelled; otherwise take only its colour and treatment, and never its subject, geometry or composition",
1956
2098
  // Only a figure-led scene attaches one of these now, so this says what that
1957
2099
  // case actually needs. It used to read "environment and light only - take no
@@ -1976,6 +2118,7 @@ var REFERENCE_ROLE_DIRECTIVE = {
1976
2118
  var EDIT_REFERENCE_ROLE_DIRECTIVE = {
1977
2119
  product: "the exact product: keep or restore its label, shape and design faithfully",
1978
2120
  character: "the exact person: keep their face, facial structure, skin, hair and build faithfully; take no clothing, pose or background from this reference, and keep the source image's existing outfit unless the instruction changes it",
2121
+ detail: "one detail to match: its design, shape, colours and finish only. Take no identity, skin, hair, body, clothing or background from it",
1979
2122
  brand: "the brand's own mark: reproduce it exactly as drawn wherever it appears \u2014 every character down to the smallest secondary lettering, in its original script and reading direction \u2014 never redrawn, re-lettered, translated or transliterated",
1980
2123
  scene: "a reference for environment, light and treatment only \u2014 take no identity from any person in it",
1981
2124
  composition: "a reference for framing and pose only",
@@ -2032,5 +2175,5 @@ function createCore(homeDir = defaultHome()) {
2032
2175
  }
2033
2176
 
2034
2177
  export { ASPECT_TOLERANCE, BUDGET_EXHAUSTED, EDIT_REFERENCE_ROLE_DIRECTIVE, REFERENCE_ROLE_DIRECTIVE, SCHEMA_VERSION, STEM_MIN, SchemaTooNewError, SpendCapError, TRIGRAM_MIN, budgetSize, createCatalogStore, createCore, createStore, defaultHome, fold, ftsMatch, matchesQuery, ratioLabel, searchTerms, termMatches, uniqueProjectSlug, uniqueSetSlug, uniqueSlug };
2035
- //# sourceMappingURL=chunk-3ZRMKVBB.js.map
2036
- //# sourceMappingURL=chunk-3ZRMKVBB.js.map
2178
+ //# sourceMappingURL=chunk-UUYMUOZQ.js.map
2179
+ //# sourceMappingURL=chunk-UUYMUOZQ.js.map
@@ -463,7 +463,7 @@ async function addOrRepair(ownEntry, say, over) {
463
463
  }
464
464
  async function rememberDecline(home) {
465
465
  try {
466
- const { createCore } = await import('./src-3ITTV2UE.js');
466
+ const { createCore } = await import('./src-QZZDZM7I.js');
467
467
  const core = createCore(home);
468
468
  try {
469
469
  core.store.setSetting("desktop.prompt", "declined");
@@ -530,5 +530,5 @@ function tail(path, bytes = 4096) {
530
530
  }
531
531
 
532
532
  export { addToDesktop, assetsDirFor, installDeps, runDesktopCommand, runExecFile, runOpenCommand };
533
- //# sourceMappingURL=cli-MQO66C5O.js.map
534
- //# sourceMappingURL=cli-MQO66C5O.js.map
533
+ //# sourceMappingURL=cli-3KKS7BE4.js.map
534
+ //# sourceMappingURL=cli-3KKS7BE4.js.map
package/dist/index.js CHANGED
@@ -86,12 +86,12 @@ try {
86
86
  break;
87
87
  }
88
88
  case "desktop": {
89
- const { runDesktopCommand } = await import('./cli-MQO66C5O.js');
89
+ const { runDesktopCommand } = await import('./cli-3KKS7BE4.js');
90
90
  process.exit(await runDesktopCommand(command, fileURLToPath(import.meta.url)));
91
91
  break;
92
92
  }
93
93
  case "open": {
94
- const { runOpenCommand } = await import('./cli-MQO66C5O.js');
94
+ const { runOpenCommand } = await import('./cli-3KKS7BE4.js');
95
95
  process.exit(await runOpenCommand(fileURLToPath(import.meta.url)));
96
96
  break;
97
97
  }