@mcptoolshop/style-dataset-lab 3.4.0 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.es.md +111 -59
  3. package/README.fr.md +117 -67
  4. package/README.hi.md +109 -70
  5. package/README.it.md +111 -60
  6. package/README.ja.md +116 -76
  7. package/README.md +58 -2
  8. package/README.pt-BR.md +119 -64
  9. package/README.zh.md +108 -67
  10. package/bin/sdlab.js +50 -1
  11. package/lib/adapters/ai-toolkit.js +187 -30
  12. package/lib/adapters/comfyui-workflows.js +84 -9
  13. package/lib/asset-ingest.js +575 -0
  14. package/lib/asset-masks.js +323 -0
  15. package/lib/asset-source.js +1176 -0
  16. package/lib/canon-author.js +350 -0
  17. package/lib/canon-review-gate.js +205 -0
  18. package/lib/colab-launcher-guards.js +285 -0
  19. package/lib/export.js +1 -1
  20. package/lib/freeze-gate.js +3 -0
  21. package/lib/generated-provenance.js +178 -5
  22. package/lib/lane1-render.js +668 -0
  23. package/lib/memory-store.js +663 -0
  24. package/lib/model-licences.js +305 -0
  25. package/lib/npy-meta.js +130 -0
  26. package/lib/png-meta.js +512 -0
  27. package/lib/records.js +54 -8
  28. package/lib/reingest-selected.js +15 -1
  29. package/lib/reingest.js +27 -3
  30. package/lib/run-manifest.js +59 -11
  31. package/lib/train-runs.js +214 -0
  32. package/lib/verifier.js +3 -0
  33. package/lib/verify-chain.js +367 -0
  34. package/package.json +2 -2
  35. package/scripts/asset-ingest.js +160 -0
  36. package/scripts/canon-draft.js +82 -0
  37. package/scripts/canon-freeze.js +4 -0
  38. package/scripts/colab/REAL_SERVICE_ASSUMPTIONS.md +14 -0
  39. package/scripts/colab/ai-toolkit-override-py315.txt +45 -0
  40. package/scripts/colab/colab_window.sh +587 -0
  41. package/scripts/colab/lane1_render.py +970 -0
  42. package/scripts/colab/launch_guards.py +564 -0
  43. package/scripts/colab/launch_lane1_stub.sh +309 -0
  44. package/scripts/colab/launch_lane3_stub.sh +366 -0
  45. package/scripts/colab/remote_runner.py +59 -0
  46. package/scripts/doctor.js +1 -0
  47. package/scripts/generate.js +38 -12
  48. package/scripts/verify.js +67 -0
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env python3
2
+ """Remote kernel runner executed via `colab exec -s <session> -f remote_runner.py`.
3
+
4
+ Runs the uploaded remote shell script (`SDLAB_REMOTE_SCRIPT`, e.g.
5
+ `/content/remote_lane1_stub.sh` or `/content/remote_lane3_stub.sh`) via
6
+ `subprocess.Popen`, streaming each stdout/stderr line to the IPython kernel's
7
+ stdout as it arrives so the rig's vitals and DEAD HARNESS watcher read it in
8
+ real time, and finishes by printing a `SDLAB_REMOTE_RC=<rc>` marker line.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+
18
+
19
+ def _resolve_bash(env: dict[str, str]) -> str:
20
+ git_bash = "C:/Program Files/Git/bin/bash.exe"
21
+ if os.name == "nt" and os.path.exists(git_bash):
22
+ return git_bash
23
+ found = shutil.which("bash", path=env.get("PATH"))
24
+ return found or "bash"
25
+
26
+
27
+ def run_remote_script(
28
+ script_path: str | None = None,
29
+ *,
30
+ env: dict[str, str] | None = None,
31
+ ) -> int:
32
+ effective_env = dict(os.environ if env is None else env)
33
+ target_script = script_path or effective_env.get("SDLAB_REMOTE_SCRIPT", "").strip()
34
+ if not target_script:
35
+ print("REFUSED: SDLAB_REMOTE_SCRIPT is required", file=sys.stderr, flush=True)
36
+ print("SDLAB_REMOTE_RC=2", flush=True)
37
+ return 2
38
+
39
+ cmd = [_resolve_bash(effective_env), target_script]
40
+ proc = subprocess.Popen(
41
+ cmd,
42
+ stdin=subprocess.DEVNULL,
43
+ stdout=subprocess.PIPE,
44
+ stderr=subprocess.STDOUT,
45
+ text=True,
46
+ bufsize=1,
47
+ env=effective_env,
48
+ )
49
+ assert proc.stdout is not None
50
+ for line in proc.stdout:
51
+ sys.stdout.write(line)
52
+ sys.stdout.flush()
53
+ rc = int(proc.wait())
54
+ print(f"SDLAB_REMOTE_RC={rc}", flush=True)
55
+ return rc
56
+
57
+
58
+ if __name__ == "__main__":
59
+ run_remote_script()
package/scripts/doctor.js CHANGED
@@ -151,6 +151,7 @@ export async function run(argv = process.argv.slice(2)) {
151
151
 
152
152
  if (meta.domain) { pass(`project.json: domain = "${meta.domain}"`); passes++; }
153
153
  else { warn('project.json: no "domain" set'); warnings++; }
154
+ if (meta.display_name === 'TEMPLATE') { fail('project.json: display_name is still "TEMPLATE"', 'Set a real display_name in project.json instead of the unedited template placeholder.'); failures++; }
154
155
  }
155
156
 
156
157
  // ── 5. Constitution validation ──
@@ -19,8 +19,10 @@ import { readJsonFile, loadProjectMeta, resolveGpuModel } from "../lib/config.js
19
19
  import { runtimeError, inputError, handleCliError } from "../lib/errors.js";
20
20
  import { comfyHealth, submitAndWait, downloadImage } from "../lib/comfyui.js";
21
21
  import { pickOutputImage } from "../lib/comfyui-output.js";
22
- import { buildQwenGraph, QWEN_DEFAULTS } from "../lib/adapters/comfyui-workflows.js";
22
+ import { buildQwenGraph, QWEN_DEFAULTS, resolveQwenLoras } from "../lib/adapters/comfyui-workflows.js";
23
23
  import { buildPinning, SEED_POLICIES, createHashCache, flushHashCache } from "../lib/run-manifest.js";
24
+ import { STUDIO_OWNER, STUDIO_LICENCE, buildDefaultRights } from "../lib/records.js";
25
+ import { resolveCanonLink } from "../lib/generated-provenance.js";
24
26
  import { info, result, warn } from "../lib/log.js";
25
27
 
26
28
  /**
@@ -226,7 +228,7 @@ function findPlaceholderPrompts(jobs) {
226
228
  * `base` resolves: input.base → input.defaults.base → project.json defaults.base
227
229
  * → "sdxl". `"qwen-image"` routes to the non-anime Qwen graph.
228
230
  */
229
- function normalizeGenerationInput(raw, projectMeta) {
231
+ export function normalizeGenerationInput(raw, projectMeta) {
230
232
  if (!Array.isArray(raw.subjects) || raw.subjects.length === 0) {
231
233
  throw inputError(
232
234
  'INPUT_MISSING_FIELDS',
@@ -260,10 +262,11 @@ function normalizeGenerationInput(raw, projectMeta) {
260
262
  jobs.push({
261
263
  assetId: `${subject.id}_${variation.id}`,
262
264
  subjectId: subject.id,
265
+ entryId: subject.entry_id || subject.id,
263
266
  variationId: variation.id,
264
267
  prompt: compose(subject.prompt),
265
268
  seed: baseSeed + k + (variation.seed_offset || 0),
266
- loras: variation.loras || [],
269
+ loras: variation.loras || subject.loras || [],
267
270
  });
268
271
  k++;
269
272
  }
@@ -275,14 +278,16 @@ function normalizeGenerationInput(raw, projectMeta) {
275
278
  const negative = raw.negative ?? defaults.negative ?? '';
276
279
  for (const subject of raw.subjects) {
277
280
  const nvar = Number.isFinite(subject.variations) ? subject.variations : 1;
281
+ const subjectLoras = subject.loras || defaults.loras || raw.loras || [];
278
282
  for (let v = 0; v < nvar; v++) {
279
283
  jobs.push({
280
284
  assetId: `${subject.id}_v${v}`,
281
285
  subjectId: subject.id,
286
+ entryId: subject.entry_id || subject.id,
282
287
  variationId: `v${v}`,
283
288
  prompt: compose(subject.prompt),
284
289
  seed: baseSeed + k,
285
- loras: [],
290
+ loras: subjectLoras,
286
291
  });
287
292
  k++;
288
293
  }
@@ -384,7 +389,10 @@ export async function run(argv = process.argv.slice(2)) {
384
389
  const runStartMs = Date.now();
385
390
 
386
391
  for (const job of pack.jobs) {
387
- const { assetId, prompt: fullPrompt, seed, loras } = job;
392
+ const { assetId, entryId, prompt: fullPrompt, seed, loras } = job;
393
+ const effectiveLoras = isQwen
394
+ ? resolveQwenLoras({ projectDefaults: d, loras })
395
+ : loras;
388
396
 
389
397
  // --resume: skip jobs whose record + image are both already on disk.
390
398
  // Seeds are precomputed per job index, so skipping never shifts later seeds.
@@ -399,7 +407,7 @@ export async function run(argv = process.argv.slice(2)) {
399
407
  }
400
408
  }
401
409
 
402
- console.log(` [${generated + 1}/${totalExpected}] ${assetId} (seed: ${seed}, loras: ${isQwen ? (d.loras || []).length : loras.length})`);
410
+ console.log(` [${generated + 1}/${totalExpected}] ${assetId} (seed: ${seed}, loras: ${effectiveLoras.length})`);
403
411
 
404
412
  if (dryRun) {
405
413
  generated++;
@@ -424,12 +432,12 @@ export async function run(argv = process.argv.slice(2)) {
424
432
  vae: d.vae || QWEN_DEFAULTS.vae,
425
433
  clipType: d.clip_type || QWEN_DEFAULTS.clip_type,
426
434
  weightDtype: d.weight_dtype || QWEN_DEFAULTS.weight_dtype,
427
- loras: d.loras || [],
435
+ loras: effectiveLoras,
428
436
  filenamePrefix: 'sdl',
429
437
  })
430
438
  : buildWorkflow(
431
439
  fullPrompt, pack.negative,
432
- d.checkpoint, loras, seed,
440
+ d.checkpoint, effectiveLoras, seed,
433
441
  resolved.steps, resolved.cfg, resolved.sampler, resolved.scheduler,
434
442
  resolved.width, resolved.height,
435
443
  );
@@ -452,9 +460,16 @@ export async function run(argv = process.argv.slice(2)) {
452
460
  const destPath = `outputs/candidates/${assetId}.png`;
453
461
  await writeFile(join(GAME_ROOT, destPath), imgData);
454
462
 
463
+ const canonLink = resolveCanonLink(GAME_ROOT, {
464
+ canonSha: raw.canon_sha || null,
465
+ entryId,
466
+ });
467
+
455
468
  // Write provenance record (base-aware: Qwen records unet/clip/vae/shift,
456
469
  // SDXL records checkpoint/loras).
457
470
  const provenance = {
471
+ owner: STUDIO_OWNER,
472
+ licence: STUDIO_LICENCE,
458
473
  workflow_id: pack.lane,
459
474
  workflow_version: "1.0.0",
460
475
  base,
@@ -470,16 +485,23 @@ export async function run(argv = process.argv.slice(2)) {
470
485
  generation_time_ms: elapsed,
471
486
  gpu_model: gpuModel,
472
487
  batch_index: generated,
488
+ ...(canonLink
489
+ ? {
490
+ canon_sha: canonLink.canon_sha,
491
+ entry_id: canonLink.entry_id,
492
+ entry_sha256: canonLink.entry_sha256,
493
+ }
494
+ : {}),
473
495
  };
474
496
  if (isQwen) {
475
497
  provenance.unet = d.unet || QWEN_DEFAULTS.unet;
476
498
  provenance.clip = d.clip || QWEN_DEFAULTS.clip;
477
499
  provenance.vae = d.vae || QWEN_DEFAULTS.vae;
478
500
  provenance.shift = d.shift ?? QWEN_DEFAULTS.shift;
479
- provenance.loras = d.loras || [];
501
+ provenance.loras = effectiveLoras;
480
502
  } else {
481
503
  provenance.checkpoint = d.checkpoint;
482
- provenance.loras = loras;
504
+ provenance.loras = effectiveLoras;
483
505
  }
484
506
 
485
507
  // PIN_PER_STEP: pin the exact graph + model/LoRA content identity + seed
@@ -490,21 +512,25 @@ export async function run(argv = process.argv.slice(2)) {
490
512
  models: isQwen
491
513
  ? { unet: provenance.unet, clip: provenance.clip, vae: provenance.vae }
492
514
  : { checkpoint: d.checkpoint },
493
- loras: isQwen ? (d.loras || []) : loras,
515
+ loras: effectiveLoras,
494
516
  seedPolicy: SEED_POLICIES.BASE_INCREMENT,
495
517
  hashModels,
496
518
  cache: hashCache,
497
519
  });
520
+ provenance.comfy_workflow_sha = provenance.pinning.comfy_workflow_sha;
498
521
 
499
522
  const record = {
500
523
  id: assetId,
501
524
  schema_version: "1.0.0",
502
525
  created_at: new Date().toISOString(),
526
+ owner: STUDIO_OWNER,
527
+ licence: STUDIO_LICENCE,
528
+ rights: buildDefaultRights(),
503
529
  asset_path: destPath,
504
530
  image: { format: "png", width: resolved.width, height: resolved.height, bytes: imgData.length },
505
531
  provenance,
506
532
  judgment: null,
507
- canon: null,
533
+ canon: canonLink,
508
534
  iteration: null,
509
535
  };
510
536
 
@@ -0,0 +1,67 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * sdlab verify — Offline end-to-end provenance chain verification (G9, §2.2–§2.3).
5
+ *
6
+ * Walks:
7
+ * image -> record (G5 owner + CC-BY-4.0)
8
+ * -> canon (G6 canon_sha + entry_sha256)
9
+ * -> model licence (G4 SPDX id + licence_url + date_read)
10
+ * -> run manifest (workflow sha + quarantine guard + verdict separation)
11
+ */
12
+
13
+ import { parseArgs, getProjectName } from '../lib/args.js';
14
+ import { getProjectRoot } from '../lib/paths.js';
15
+ import { inputError, handleCliError } from '../lib/errors.js';
16
+ import { verifyProvenanceChain } from '../lib/verify-chain.js';
17
+
18
+ export async function run(argv = process.argv.slice(2)) {
19
+ const parsed = parseArgs(argv, {
20
+ flags: {
21
+ project: { type: 'string' },
22
+ game: { type: 'string' },
23
+ record: { type: 'string' },
24
+ snapshot: { type: 'string' },
25
+ json: { type: 'boolean' },
26
+ 'allow-empty': { type: 'boolean' },
27
+ },
28
+ deprecated: { game: 'project' },
29
+ });
30
+
31
+ const projectName = getProjectName(argv);
32
+ const projectDir = getProjectRoot(projectName);
33
+
34
+ const report = await verifyProvenanceChain(projectDir, {
35
+ recordId: parsed.flags.record || null,
36
+ snapshotId: parsed.flags.snapshot || null,
37
+ allowEmpty: parsed.flags['allow-empty'] === true,
38
+ });
39
+
40
+ if (parsed.flags.json) {
41
+ console.log(JSON.stringify(report, null, 2));
42
+ } else {
43
+ console.log(`\x1b[1msdlab verify\x1b[0m — ${projectName}`);
44
+ console.log(` Records checked: ${report.records_checked} (${report.passed_count} passed, ${report.failed_count} failed)`);
45
+ for (const f of report.failures) {
46
+ console.error(` \x1b[31m✗\x1b[0m [${f.code}] ${f.message}`);
47
+ }
48
+ if (report.ok) {
49
+ console.log(` \x1b[32m✓\x1b[0m Provenance chain intact (CC-BY-4.0 eligible)`);
50
+ }
51
+ }
52
+
53
+ if (!report.ok) {
54
+ const codes = [...new Set(report.failures.map((f) => f.code))].join(', ');
55
+ throw inputError(
56
+ 'PROVENANCE_VERIFY_FAILED',
57
+ `Provenance verification failed for project "${projectName}" (${report.failures.length} failure(s): ${codes})`,
58
+ 'Fix the broken provenance link(s) reported above before releasing or exporting under CC-BY-4.0.',
59
+ );
60
+ }
61
+
62
+ return report;
63
+ }
64
+
65
+ if (process.argv[1] && (process.argv[1].endsWith('verify.js') || process.argv[1].endsWith('verify'))) {
66
+ run().catch(handleCliError);
67
+ }