@mcptoolshop/style-dataset-lab 3.0.0 → 3.0.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 (88) hide show
  1. package/CHANGELOG.md +68 -15
  2. package/CODE_OF_CONDUCT.md +9 -0
  3. package/CONTRIBUTING.md +71 -0
  4. package/README.md +46 -1
  5. package/SECURITY.md +6 -5
  6. package/bin/sdlab.js +281 -62
  7. package/lib/adapters/diffusers-lora.js +7 -2
  8. package/lib/adapters/generic-image-caption.js +6 -1
  9. package/lib/args.js +200 -13
  10. package/lib/batch-compiler.js +7 -3
  11. package/lib/batch-runs.js +100 -31
  12. package/lib/batch-sheet-render.js +5 -2
  13. package/lib/brief-compiler.js +38 -11
  14. package/lib/card.js +11 -4
  15. package/lib/comfyui.js +100 -6
  16. package/lib/config.js +68 -3
  17. package/lib/deprecation.js +1 -1
  18. package/lib/eval-pack.js +94 -4
  19. package/lib/eval-runs.js +42 -3
  20. package/lib/export.js +144 -34
  21. package/lib/implementation-packs.js +15 -1
  22. package/lib/log.js +41 -4
  23. package/lib/paths.js +41 -3
  24. package/lib/refine-briefs.js +16 -9
  25. package/lib/reingest-selected.js +14 -3
  26. package/lib/reingest.js +15 -1
  27. package/lib/run-summary.js +3 -2
  28. package/lib/runtime-runs.js +59 -9
  29. package/lib/selections.js +54 -2
  30. package/lib/snapshot.js +100 -7
  31. package/lib/split.js +175 -17
  32. package/lib/training-manifests.js +17 -3
  33. package/lib/training-packages.js +28 -14
  34. package/package.json +8 -2
  35. package/scripts/batch-generate.js +16 -7
  36. package/scripts/batch-sheet.js +3 -3
  37. package/scripts/batch-show.js +3 -3
  38. package/scripts/brief-compile.js +8 -8
  39. package/scripts/brief-show.js +4 -3
  40. package/scripts/canon-bind.js +7 -9
  41. package/scripts/card.js +22 -25
  42. package/scripts/compare.js +38 -33
  43. package/scripts/critique-show.js +3 -3
  44. package/scripts/critique.js +3 -3
  45. package/scripts/curate.js +91 -46
  46. package/scripts/doctor.js +3 -5
  47. package/scripts/eligibility.js +17 -14
  48. package/scripts/eval-pack.js +13 -11
  49. package/scripts/eval-run.js +30 -29
  50. package/scripts/export.js +27 -31
  51. package/scripts/generate-controlnet.js +31 -17
  52. package/scripts/generate-identity.js +37 -20
  53. package/scripts/generate-ipadapter.js +22 -16
  54. package/scripts/generate.js +68 -13
  55. package/scripts/implementation-pack.js +19 -14
  56. package/scripts/init.js +115 -17
  57. package/scripts/painterly.js +8 -10
  58. package/scripts/refine.js +3 -3
  59. package/scripts/reingest-selected.js +3 -3
  60. package/scripts/reingest.js +33 -20
  61. package/scripts/run-generate.js +4 -4
  62. package/scripts/run-list.js +3 -3
  63. package/scripts/run-show.js +3 -3
  64. package/scripts/select.js +5 -4
  65. package/scripts/selection-show.js +3 -3
  66. package/scripts/snapshot.js +22 -24
  67. package/scripts/split.js +26 -31
  68. package/scripts/training-manifest.js +29 -24
  69. package/scripts/training-package.js +24 -20
  70. package/scripts/training-profile.js +13 -8
  71. package/scripts/workflow-list.js +2 -2
  72. package/scripts/workflow-show.js +4 -3
  73. package/templates/domains/architecture/inputs/prompts/example-wave.json +32 -0
  74. package/templates/domains/architecture/terminology.json +10 -8
  75. package/templates/domains/character-design/inputs/prompts/example-wave.json +32 -0
  76. package/templates/domains/character-design/project.json +4 -2
  77. package/templates/domains/creature-design/inputs/prompts/example-wave.json +32 -0
  78. package/templates/domains/creature-design/project.json +4 -2
  79. package/templates/domains/creature-design/terminology.json +1 -0
  80. package/templates/domains/game-art/inputs/prompts/example-wave.json +37 -0
  81. package/templates/domains/game-art/project.json +4 -2
  82. package/templates/domains/vehicle-mech/inputs/prompts/example-wave.json +37 -0
  83. package/templates/domains/vehicle-mech/project.json +4 -2
  84. package/scripts/bulk-curate-wave2-5.js +0 -138
  85. package/scripts/bulk-curate-waves11-18.js +0 -413
  86. package/scripts/curate-wave25.js +0 -209
  87. package/scripts/migrate-records.js +0 -141
  88. package/templates/inputs/prompts/example-wave.json +0 -27
package/bin/sdlab.js CHANGED
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { enableDebug, handleCliError, inputError } from '../lib/errors.js';
11
11
  import { setLogLevel } from '../lib/log.js';
12
+ import { findClosest } from '../lib/args.js';
12
13
 
13
14
  const COMMANDS = {
14
15
  'init': '../scripts/init.js',
@@ -79,6 +80,204 @@ const SELECTION_COMMANDS = {
79
80
  'show': '../scripts/selection-show.js',
80
81
  };
81
82
 
83
+ // ─── Per-command help ─────────────────────────────────────────────────
84
+ //
85
+ // Keyed by the user-facing command (including two-word forms joined by
86
+ // a space). Help text is plain — no color codes — to stay readable in
87
+ // dumb terminals and when piped to files.
88
+
89
+ const HELP_TEXT = {
90
+ 'init': `sdlab init <project-name> [--domain <domain>]
91
+
92
+ Scaffold a new project from a domain template.
93
+
94
+ Positional:
95
+ project-name Lowercase, hyphen-separated (e.g. "my-project")
96
+
97
+ Flags:
98
+ --domain <id> Domain template (default: generic)
99
+
100
+ Examples:
101
+ sdlab init my-project
102
+ sdlab init heist-crew --domain character-design
103
+
104
+ Run "sdlab init" with no arguments to see the available domains.`,
105
+
106
+ 'project doctor': `sdlab project doctor --project <name>
107
+
108
+ Validate project config and directory structure. Reports missing
109
+ required files, unmigrated record schemas, and orphaned outputs.
110
+
111
+ Flags:
112
+ --project <name> Project to audit (required in practice)
113
+ --verbose Show per-check detail
114
+ --quiet Only print PASS / FAIL summary`,
115
+
116
+ 'generate': `sdlab generate [<pack-path>] --project <name> [--dry-run]
117
+
118
+ Generate candidates from a prompt pack (JSON file under
119
+ projects/<name>/inputs/prompts/).
120
+
121
+ Positional:
122
+ pack-path Path to the prompt pack (default: inputs/prompts/rpg-icons-lane1.json)
123
+
124
+ Flags:
125
+ --project <name> Project to operate on
126
+ --dry-run Preview work without contacting ComfyUI
127
+
128
+ Env:
129
+ COMFY_URL ComfyUI HTTP endpoint (default: http://127.0.0.1:8188)
130
+
131
+ Precedence: CLI flag > env COMFY_URL > project defaults > built-in default.`,
132
+
133
+ 'batch generate': `sdlab batch generate --mode <id> [--subject <id>] [--theme <label>] --project <name>
134
+
135
+ Execute a batch production mode (expression sheet, silhouette pack,
136
+ environment board, etc).
137
+
138
+ Flags:
139
+ --mode <id> Batch mode id (required)
140
+ --subject <id> Subject for subject-driven modes
141
+ --theme <label> Theme label for environment modes
142
+ --asset <id> Training asset reference
143
+ --project <name> Project to operate on
144
+ --dry-run Prepare batch without submitting to ComfyUI
145
+ --json Output manifest as JSON`,
146
+
147
+ 'brief compile': `sdlab brief compile --profile <id> --project <name>
148
+
149
+ Compile a generation brief from project canon (constitution, lanes,
150
+ workflow profile).
151
+
152
+ Flags:
153
+ --profile <id> Workflow profile id to compile against
154
+ --project <name> Project to operate on
155
+ --out <path> Optional explicit output path`,
156
+
157
+ 'run generate': `sdlab run generate --brief <id> --project <name> [--dry-run]
158
+
159
+ Execute a compiled brief through ComfyUI.
160
+
161
+ Flags:
162
+ --brief <id> Brief id to execute (required)
163
+ --project <name> Project to operate on
164
+ --dry-run Plan without submitting to ComfyUI`,
165
+
166
+ 'critique': `sdlab critique --run <id> --project <name>
167
+
168
+ Critique a completed run. Writes a critique record alongside the run.
169
+
170
+ Flags:
171
+ --run <id> Run id to critique (required)
172
+ --project <name> Project to operate on
173
+ --json Emit critique as JSON on stdout`,
174
+
175
+ 'refine': `sdlab refine --run <id> --pick <file> --project <name>
176
+
177
+ Generate a refined next-pass brief based on a specific pick from a
178
+ prior run.
179
+
180
+ Flags:
181
+ --run <id> Source run id (required)
182
+ --pick <file> Picked output filename inside the run (required)
183
+ --project <name> Project to operate on`,
184
+
185
+ 'batch sheet': `sdlab batch sheet <batch-id> --project <name>
186
+
187
+ Re-render a batch's contact sheet from its manifest.
188
+
189
+ Positional:
190
+ batch-id Batch id to re-render
191
+
192
+ Flags:
193
+ --project <name> Project to operate on`,
194
+
195
+ 'select': `sdlab select [--run <id>|--batch <id>] --approve <files> --project <name>
196
+
197
+ Select approved outputs from a run or batch.
198
+
199
+ Flags:
200
+ --run <id> Source run id
201
+ --batch <id> Source batch id
202
+ --approve <files> Comma-separated output filenames to approve
203
+ --project <name> Project to operate on`,
204
+
205
+ 'curate': `sdlab curate <asset_id> <approved|rejected|borderline> <explanation> [--scores k:v,...] [--failures f1,f2] --project <name>
206
+
207
+ Record a judgment on a candidate and move it to the matching outputs
208
+ subdirectory.
209
+
210
+ Positional:
211
+ asset_id Candidate asset id
212
+ status approved | rejected | borderline
213
+ explanation Short reason (quoted)
214
+
215
+ Flags:
216
+ --scores k:v,... Per-dimension scores (e.g. silhouette:0.9,palette:0.8)
217
+ --failures f1,f2 Named failure tags (comma-separated)
218
+ --notes "…" Optional free-form note
219
+ --list Print uncurated candidates and exit
220
+ --dry-run Preview without moving files
221
+ --project <name> Project to operate on`,
222
+
223
+ 'canon-bind': `sdlab canon-bind --project <name> [--lane <id>]
224
+
225
+ Bind approved records to constitution rules.
226
+
227
+ Flags:
228
+ --project <name> Project to operate on
229
+ --lane <id> Restrict binding to a single lane`,
230
+
231
+ 'snapshot': `sdlab snapshot <create|list|show|diff> [args] --project <name>
232
+
233
+ Manage frozen dataset snapshots.
234
+
235
+ Subcommands:
236
+ create Create a new snapshot from approved records
237
+ list List all snapshots (default subcommand)
238
+ show <id> Show snapshot details
239
+ diff <a> <b> Compare two snapshots
240
+
241
+ Flags:
242
+ --profile <name> Selection profile (create only)
243
+ --project <name> Project to operate on
244
+ --dry-run Preview snapshot contents without writing`,
245
+
246
+ 'split': `sdlab split <build|list|show|audit> [args] --project <name>
247
+
248
+ Manage train/val/test splits from a snapshot.
249
+
250
+ Subcommands:
251
+ build Build a split from a snapshot
252
+ list List all splits
253
+ show <id> Show split details
254
+ audit <id> Audit a split for leakage and balance
255
+
256
+ Flags:
257
+ --project <name> Project to operate on`,
258
+
259
+ 'export': `sdlab export <build|list> [args] --project <name>
260
+
261
+ Build and list self-contained export packages.
262
+
263
+ Subcommands:
264
+ build Build a new export package
265
+ list List all export packages
266
+
267
+ Flags:
268
+ --project <name> Project to operate on`,
269
+ };
270
+
271
+ // "bind" is a short alias for "canon-bind" — share help.
272
+ HELP_TEXT['bind'] = HELP_TEXT['canon-bind'];
273
+
274
+ function printCommandHelp(key) {
275
+ const text = HELP_TEXT[key];
276
+ if (!text) return false;
277
+ console.log(text);
278
+ return true;
279
+ }
280
+
82
281
  function printHelp() {
83
282
  console.log(`\x1b[1msdlab\x1b[0m — Style Dataset Lab CLI\n`);
84
283
  console.log('Usage: sdlab <command> [options]\n');
@@ -94,9 +293,10 @@ function printHelp() {
94
293
  console.log(' generate:ipadapter IP-Adapter reference-guided generation');
95
294
  console.log(' curate Move candidate to approved/rejected/borderline');
96
295
  console.log(' compare Record pairwise A-vs-B comparison');
97
- console.log(' bind Bind approved records to constitution rules');
296
+ console.log(' canon-bind Bind approved records to constitution rules');
297
+ console.log(' ("bind" is a short alias for canon-bind)');
98
298
  console.log(' painterly Post-processing painterly style pass');
99
- console.log(' painterly:test Test denoise levels on reference images');
299
+ console.log(' painterly:test Calibrate painterly denoise level on sample images');
100
300
  console.log('');
101
301
  console.log('Dataset:');
102
302
  console.log(' snapshot create Create a frozen dataset snapshot');
@@ -132,7 +332,8 @@ function printHelp() {
132
332
  console.log(' implementation-pack build Build implementation example pack');
133
333
  console.log(' implementation-pack show <id> Show pack details');
134
334
  console.log(' implementation-pack list List all packs');
135
- console.log(' reingest generated --source <dir> Re-ingest generated outputs');
335
+ console.log(' reingest generated --source <dir> --manifest <id> Re-ingest generated outputs');
336
+ console.log(' reingest selected --selection <id> Re-ingest selected outputs as records');
136
337
  console.log(' reingest audit Audit re-ingested record status');
137
338
  console.log('');
138
339
  console.log('Production:');
@@ -155,22 +356,59 @@ function printHelp() {
155
356
  console.log(' reingest selected --selection <id> Re-ingest selected outputs as records');
156
357
  console.log('');
157
358
  console.log('Options:');
158
- console.log(' --project <name> Project to operate on (default: star-freight)');
359
+ console.log(' --project <name> Project to operate on (no default — fallback to "star-freight" with warning)');
159
360
  console.log(' --game <name> Deprecated alias for --project');
160
361
  console.log(' --debug Show stack traces on error');
161
362
  console.log(' --verbose Verbose output');
162
363
  console.log(' --quiet Suppress non-essential output');
163
364
  console.log(' --dry-run Preview changes without writing (where supported)');
164
- console.log(' --help Show this help');
365
+ console.log(' --help Show this help (or per-command help with "sdlab <cmd> --help")');
165
366
  console.log('');
166
367
  console.log('Examples:');
167
368
  console.log(' sdlab init my-project --domain character-design');
369
+ console.log(' sdlab generate --help');
168
370
  console.log(' sdlab snapshot create --project star-freight');
169
371
  console.log(' sdlab split build --dry-run --project star-freight');
170
372
  console.log(' sdlab training-package build --debug --project star-freight');
171
373
  }
172
374
 
375
+ // All top-level command tokens users can type (including two-word heads).
376
+ function allKnownCommands() {
377
+ const oneWord = Object.keys(COMMANDS);
378
+ const heads = ['project', 'workflow', 'brief', 'run', 'critique', 'refine', 'batch', 'select', 'selection'];
379
+ return [...new Set([...oneWord, ...heads])];
380
+ }
381
+
382
+ function argvHasHelp(argv) {
383
+ return argv.includes('--help') || argv.includes('-h');
384
+ }
385
+
386
+ // ─── Signal + fatal-error handlers ────────────────────────────────────
387
+
388
+ function installProcessHandlers() {
389
+ const onSignal = (sig) => {
390
+ const code = sig === 'SIGINT' ? 130 : 143;
391
+ process.stderr.write(`\n\x1b[31m✗ Interrupted by ${sig}. Partial output may exist.\x1b[0m\n`);
392
+ process.exit(code);
393
+ };
394
+ process.on('SIGINT', () => onSignal('SIGINT'));
395
+ process.on('SIGTERM', () => onSignal('SIGTERM'));
396
+
397
+ process.on('uncaughtException', (err) => {
398
+ handleCliError(err);
399
+ // handleCliError exits, but guard anyway.
400
+ process.exit(2);
401
+ });
402
+ process.on('unhandledRejection', (reason) => {
403
+ const err = reason instanceof Error ? reason : new Error(String(reason));
404
+ handleCliError(err);
405
+ process.exit(2);
406
+ });
407
+ }
408
+
173
409
  async function main() {
410
+ installProcessHandlers();
411
+
174
412
  const args = process.argv.slice(2);
175
413
 
176
414
  // Global flags
@@ -186,94 +424,75 @@ async function main() {
186
424
  let command = args[0];
187
425
  let commandArgs = args.slice(1);
188
426
 
189
- // Handle "project <subcommand>" two-word form
190
- if (command === 'project' && args[1] && PROJECT_COMMANDS[args[1]]) {
191
- commandArgs = args.slice(2);
192
- const modulePath = PROJECT_COMMANDS[args[1]];
193
- const mod = await import(modulePath);
194
- await mod.run(commandArgs);
195
- return;
196
- }
197
-
198
- // Handle "workflow <subcommand>" two-word form
199
- if (command === 'workflow' && args[1] && WORKFLOW_COMMANDS[args[1]]) {
200
- commandArgs = args.slice(2);
201
- const modulePath = WORKFLOW_COMMANDS[args[1]];
202
- const mod = await import(modulePath);
203
- await mod.run(commandArgs);
204
- return;
205
- }
427
+ // ── Two-word dispatch tables and --help interception ──
428
+ // For every two-word namespace, we check "cmd sub --help" first,
429
+ // then the single-word "cmd --help", then fall through to dispatch.
206
430
 
207
- // Handle "brief <subcommand>" two-word form
208
- if (command === 'brief' && args[1] && BRIEF_COMMANDS[args[1]]) {
209
- commandArgs = args.slice(2);
210
- const modulePath = BRIEF_COMMANDS[args[1]];
211
- const mod = await import(modulePath);
212
- await mod.run(commandArgs);
213
- return;
214
- }
431
+ const twoWord = (head, table) => ({ head, table });
432
+ const namespaces = [
433
+ twoWord('project', PROJECT_COMMANDS),
434
+ twoWord('workflow', WORKFLOW_COMMANDS),
435
+ twoWord('brief', BRIEF_COMMANDS),
436
+ twoWord('run', RUN_COMMANDS),
437
+ twoWord('batch', BATCH_COMMANDS),
438
+ twoWord('selection', SELECTION_COMMANDS),
439
+ ];
215
440
 
216
- // Handle "run <subcommand>" two-word form
217
- if (command === 'run' && args[1] && RUN_COMMANDS[args[1]]) {
218
- commandArgs = args.slice(2);
219
- const modulePath = RUN_COMMANDS[args[1]];
220
- const mod = await import(modulePath);
221
- await mod.run(commandArgs);
222
- return;
441
+ // "project", "workflow", "brief", "run", "batch", "selection" two-word forms
442
+ for (const { head, table } of namespaces) {
443
+ if (command === head && args[1] && table[args[1]]) {
444
+ commandArgs = args.slice(2);
445
+ const twoWordKey = `${head} ${args[1]}`;
446
+ if (argvHasHelp(commandArgs) && printCommandHelp(twoWordKey)) return;
447
+ const mod = await import(table[args[1]]);
448
+ await mod.run(commandArgs);
449
+ return;
450
+ }
223
451
  }
224
452
 
225
- // Handle "critique" — bare or "critique show" two-word form
453
+ // "critique" — bare or "critique show"
226
454
  if (command === 'critique') {
227
455
  if (args[1] && CRITIQUE_COMMANDS[args[1]]) {
228
456
  commandArgs = args.slice(2);
229
- const modulePath = CRITIQUE_COMMANDS[args[1]];
230
- const mod = await import(modulePath);
457
+ if (argvHasHelp(commandArgs) && printCommandHelp(`critique ${args[1]}`)) return;
458
+ const mod = await import(CRITIQUE_COMMANDS[args[1]]);
231
459
  await mod.run(commandArgs);
232
460
  return;
233
461
  }
234
- // Bare "critique" → generate critique
462
+ if (argvHasHelp(commandArgs) && printCommandHelp('critique')) return;
235
463
  const mod = await import('../scripts/critique.js');
236
464
  await mod.run(commandArgs);
237
465
  return;
238
466
  }
239
467
 
240
- // Handle "refine" — single command
468
+ // "refine" — single command
241
469
  if (command === 'refine') {
470
+ if (argvHasHelp(commandArgs) && printCommandHelp('refine')) return;
242
471
  const mod = await import('../scripts/refine.js');
243
472
  await mod.run(commandArgs);
244
473
  return;
245
474
  }
246
475
 
247
- // Handle "batch <subcommand>" two-word form
248
- if (command === 'batch' && args[1] && BATCH_COMMANDS[args[1]]) {
249
- commandArgs = args.slice(2);
250
- const modulePath = BATCH_COMMANDS[args[1]];
251
- const mod = await import(modulePath);
252
- await mod.run(commandArgs);
253
- return;
254
- }
255
-
256
- // Handle "select" — single command
476
+ // "select" — single command
257
477
  if (command === 'select') {
478
+ if (argvHasHelp(commandArgs) && printCommandHelp('select')) return;
258
479
  const mod = await import('../scripts/select.js');
259
480
  await mod.run(commandArgs);
260
481
  return;
261
482
  }
262
483
 
263
- // Handle "selection <subcommand>" two-word form
264
- if (command === 'selection' && args[1] && SELECTION_COMMANDS[args[1]]) {
265
- commandArgs = args.slice(2);
266
- const modulePath = SELECTION_COMMANDS[args[1]];
267
- const mod = await import(modulePath);
268
- await mod.run(commandArgs);
269
- return;
270
- }
271
-
272
484
  const modulePath = COMMANDS[command];
273
485
  if (!modulePath) {
274
- throw inputError('INPUT_UNKNOWN_COMMAND', `Unknown command: ${command}`, 'Run "sdlab --help" for available commands.');
486
+ const suggestion = findClosest(command, allKnownCommands());
487
+ const hint = suggestion
488
+ ? `Did you mean "${suggestion}"? (Run "sdlab --help" for all commands.)`
489
+ : 'Run "sdlab --help" for available commands.';
490
+ throw inputError('INPUT_UNKNOWN_COMMAND', `Unknown command: ${command}`, hint);
275
491
  }
276
492
 
493
+ // Per-command --help for one-word commands
494
+ if (argvHasHelp(commandArgs) && printCommandHelp(command)) return;
495
+
277
496
  const mod = await import(modulePath);
278
497
  await mod.run(commandArgs);
279
498
  }
@@ -68,8 +68,13 @@ export async function buildPackage({ packageDir, partitions, profile, manifest,
68
68
  const sourcePath = join(projectRoot, record.asset_path);
69
69
  if (!existsSync(sourcePath)) continue;
70
70
 
71
- const filename = record.asset_path.split('/').pop();
72
- const stem = filename.replace(/\.[^.]+$/, '');
71
+ // D-009: Name output files by record.id (not source basename) to
72
+ // avoid silent collisions when two records share a filename stem.
73
+ const sourceBasename = record.asset_path.split(/[\\/]/).pop();
74
+ const extMatch = sourceBasename.match(/\.[^.]+$/);
75
+ const ext = extMatch ? extMatch[0] : '';
76
+ const filename = `${record.id}${ext}`;
77
+ const stem = record.id;
73
78
  const destPath = join(datasetDir, filename);
74
79
 
75
80
  try {
@@ -57,7 +57,12 @@ export async function buildPackage({ packageDir, partitions, profile, manifest,
57
57
  const sourcePath = join(projectRoot, record.asset_path);
58
58
  if (!existsSync(sourcePath)) continue;
59
59
 
60
- const filename = record.asset_path.split('/').pop();
60
+ // D-009: Name output files by record.id (not source basename) to
61
+ // avoid silent collisions when two records share a filename stem.
62
+ const sourceBasename = record.asset_path.split(/[\\/]/).pop();
63
+ const extMatch = sourceBasename.match(/\.[^.]+$/);
64
+ const ext = extMatch ? extMatch[0] : '';
65
+ const filename = `${record.id}${ext}`;
61
66
  const destPath = join(datasetDir, filename);
62
67
 
63
68
  try {