@hraness/slopcamera 3.8.1 → 3.9.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.
package/src/operations.ts CHANGED
@@ -29,6 +29,29 @@ import {
29
29
  slopcameraGalleryLimits,
30
30
  type SlopcameraGalleryReceipt,
31
31
  } from "./image-gallery.js"
32
+ import {
33
+ composeSlopcameraIcon,
34
+ renderSlopcameraIcon,
35
+ slopcameraIconLanguages,
36
+ slopcameraIconPalettes,
37
+ slopcameraIconSceneLimits,
38
+ type SlopcameraIconComposeInput,
39
+ type SlopcameraIconComposeReceipt,
40
+ type SlopcameraIconPalette,
41
+ type SlopcameraIconRenderInput,
42
+ type SlopcameraIconRenderReceipt,
43
+ } from "./icon-scene.js"
44
+ import {
45
+ composeSlopcameraSoundtrack,
46
+ deriveSlopcameraSoundtrackGrid,
47
+ slopcameraSoundtrackFormats,
48
+ slopcameraSoundtrackLimits,
49
+ type SlopcameraSoundtrackComposeInput,
50
+ type SlopcameraSoundtrackComposeReceipt,
51
+ type SlopcameraSoundtrackFormat,
52
+ type SlopcameraSoundtrackGridInput,
53
+ type SlopcameraSoundtrackGridReceipt,
54
+ } from "./soundtrack.js"
32
55
  import {
33
56
  vectorizeImage,
34
57
  type VectorizeReceipt,
@@ -51,6 +74,10 @@ export const slopcameraOperationCodes = [
51
74
  "slopcamera.image.generate",
52
75
  "slopcamera.image.icon",
53
76
  "slopcamera.image.gallery",
77
+ "slopcamera.icon.compose",
78
+ "slopcamera.icon.render",
79
+ "slopcamera.soundtrack.compose",
80
+ "slopcamera.soundtrack.grid",
54
81
  ] as const
55
82
 
56
83
  export type SlopcameraOperationCode = (typeof slopcameraOperationCodes)[number]
@@ -103,6 +130,16 @@ const pathSchema = {
103
130
  maxLength: 4_096,
104
131
  } as const
105
132
 
133
+ const localOperationResources = [
134
+ { resource: "cpu", amount: 1 },
135
+ { resource: "local-io", amount: 1 },
136
+ ] as const
137
+
138
+ const soundtrackFormatSchema = {
139
+ type: "string",
140
+ enum: slopcameraSoundtrackFormats,
141
+ } as const
142
+
106
143
  function deepFreeze<T>(value: T): T {
107
144
  if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
108
145
  return value
@@ -390,6 +427,102 @@ export const slopcameraOperationRegistry: readonly SlopcameraOperationDescriptor
390
427
  retry: "never",
391
428
  },
392
429
  },
430
+ {
431
+ code: "slopcamera.icon.compose",
432
+ title: "Compose icon scene",
433
+ description:
434
+ "Parse and solve a local vector icon scene, collection, construction program, or recipe, and report its digests and geometry. Optionally write the canonical solved document. Local and deterministic.",
435
+ execution: "local",
436
+ authentication: "none",
437
+ destructive: true,
438
+ idempotent: true,
439
+ inputSchema: {
440
+ type: "object",
441
+ additionalProperties: false,
442
+ required: ["sourcePath"],
443
+ properties: {
444
+ sourcePath: pathSchema,
445
+ outputPath: pathSchema,
446
+ },
447
+ },
448
+ resources: [...localOperationResources],
449
+ },
450
+ {
451
+ code: "slopcamera.icon.render",
452
+ title: "Render icon scene",
453
+ description:
454
+ "Draw a local vector icon scene or construction program to an inert SVG, or replay a recipe and require its recorded digests. Optionally write a replayable recipe. Local and deterministic.",
455
+ execution: "local",
456
+ authentication: "none",
457
+ destructive: true,
458
+ idempotent: true,
459
+ inputSchema: {
460
+ type: "object",
461
+ additionalProperties: false,
462
+ required: ["sourcePath", "outputPath"],
463
+ properties: {
464
+ sourcePath: pathSchema,
465
+ outputPath: pathSchema,
466
+ recipePath: pathSchema,
467
+ size: {
468
+ type: "integer",
469
+ minimum: slopcameraIconSceneLimits.sizeMin,
470
+ maximum: slopcameraIconSceneLimits.sizeMax,
471
+ },
472
+ background: { type: "boolean" },
473
+ palette: { type: "string", enum: slopcameraIconPalettes },
474
+ language: { type: "string", enum: slopcameraIconLanguages },
475
+ },
476
+ },
477
+ resources: [...localOperationResources],
478
+ },
479
+ {
480
+ code: "slopcamera.soundtrack.compose",
481
+ title: "Compose soundtrack score",
482
+ description:
483
+ "Parse and verify a local loop or song from compose text, song text, score JSON, or a Standard MIDI file, and report its canonical digest, tempo, and length. Optionally write the score document. Renders no audio.",
484
+ execution: "local",
485
+ authentication: "none",
486
+ destructive: true,
487
+ idempotent: true,
488
+ inputSchema: {
489
+ type: "object",
490
+ additionalProperties: false,
491
+ required: ["sourcePath"],
492
+ properties: {
493
+ sourcePath: pathSchema,
494
+ outputPath: pathSchema,
495
+ format: soundtrackFormatSchema,
496
+ },
497
+ },
498
+ resources: [...localOperationResources],
499
+ },
500
+ {
501
+ code: "slopcamera.soundtrack.grid",
502
+ title: "Derive soundtrack beat grid",
503
+ description:
504
+ "Derive bpm, beatOffsetUs, beatsPerBar, and section cue times from a local loop, song, or Standard MIDI file, in the music timing HTML scene requests accept. Optionally write the grid. Renders no audio.",
505
+ execution: "local",
506
+ authentication: "none",
507
+ destructive: true,
508
+ idempotent: true,
509
+ inputSchema: {
510
+ type: "object",
511
+ additionalProperties: false,
512
+ required: ["sourcePath"],
513
+ properties: {
514
+ sourcePath: pathSchema,
515
+ outputPath: pathSchema,
516
+ format: soundtrackFormatSchema,
517
+ startUs: {
518
+ type: "integer",
519
+ minimum: 0,
520
+ maximum: slopcameraSoundtrackLimits.maxTimeUs,
521
+ },
522
+ },
523
+ },
524
+ resources: [...localOperationResources],
525
+ },
393
526
  ] satisfies readonly SlopcameraOperationDescriptor[])
394
527
 
395
528
  export interface CheckSlopcameraOperationInput {
@@ -448,6 +581,10 @@ export interface SlopcameraOperationInputMap {
448
581
  readonly "slopcamera.image.generate": GenerateSlopcameraOperationInput
449
582
  readonly "slopcamera.image.icon": IconSlopcameraOperationInput
450
583
  readonly "slopcamera.image.gallery": GallerySlopcameraOperationInput
584
+ readonly "slopcamera.icon.compose": SlopcameraIconComposeInput
585
+ readonly "slopcamera.icon.render": SlopcameraIconRenderInput
586
+ readonly "slopcamera.soundtrack.compose": SlopcameraSoundtrackComposeInput
587
+ readonly "slopcamera.soundtrack.grid": SlopcameraSoundtrackGridInput
451
588
  }
452
589
 
453
590
  export interface SlopcameraOperationResultMap {
@@ -467,6 +604,10 @@ export interface SlopcameraOperationResultMap {
467
604
  readonly "slopcamera.image.generate": GeneratedSlopcameraImageFile
468
605
  readonly "slopcamera.image.icon": SlopcameraIconReceipt
469
606
  readonly "slopcamera.image.gallery": SlopcameraGalleryReceipt
607
+ readonly "slopcamera.icon.compose": SlopcameraIconComposeReceipt
608
+ readonly "slopcamera.icon.render": SlopcameraIconRenderReceipt
609
+ readonly "slopcamera.soundtrack.compose": SlopcameraSoundtrackComposeReceipt
610
+ readonly "slopcamera.soundtrack.grid": SlopcameraSoundtrackGridReceipt
470
611
  }
471
612
 
472
613
  function operationFailure(message: string): never {
@@ -835,6 +976,123 @@ function parseGallery(value: unknown): GallerySlopcameraOperationInput {
835
976
  }
836
977
  }
837
978
 
979
+ function optionalPathValue(value: unknown, name: string): string | undefined {
980
+ return value === undefined ? undefined : pathValue(value, name)
981
+ }
982
+
983
+ function requireSuffix(path: string | undefined, suffix: string, name: string): void {
984
+ if (path !== undefined && !path.toLowerCase().endsWith(suffix)) {
985
+ operationFailure(`${name} must end in ${suffix}.`)
986
+ }
987
+ }
988
+
989
+ function parseIconCompose(value: unknown): SlopcameraIconComposeInput {
990
+ const input = record(value, ["sourcePath", "outputPath"])
991
+ const outputPath = optionalPathValue(input.outputPath, "outputPath")
992
+ requireSuffix(outputPath, ".json", "outputPath")
993
+ return {
994
+ sourcePath: pathValue(input.sourcePath, "sourcePath"),
995
+ ...(outputPath === undefined ? {} : { outputPath }),
996
+ }
997
+ }
998
+
999
+ function parseIconRender(value: unknown): SlopcameraIconRenderInput {
1000
+ const input = record(value, [
1001
+ "sourcePath",
1002
+ "outputPath",
1003
+ "recipePath",
1004
+ "size",
1005
+ "background",
1006
+ "palette",
1007
+ "language",
1008
+ ])
1009
+ const outputPath = pathValue(input.outputPath, "outputPath")
1010
+ requireSuffix(outputPath, ".svg", "outputPath")
1011
+ const recipePath = optionalPathValue(input.recipePath, "recipePath")
1012
+ requireSuffix(recipePath, ".json", "recipePath")
1013
+ const { size, background, palette, language } = input
1014
+ if (
1015
+ size !== undefined &&
1016
+ (!Number.isSafeInteger(size) ||
1017
+ (size as number) < slopcameraIconSceneLimits.sizeMin ||
1018
+ (size as number) > slopcameraIconSceneLimits.sizeMax)
1019
+ ) {
1020
+ operationFailure(
1021
+ `size must be an integer from ${slopcameraIconSceneLimits.sizeMin} through ${slopcameraIconSceneLimits.sizeMax}.`,
1022
+ )
1023
+ }
1024
+ if (background !== undefined && typeof background !== "boolean") {
1025
+ operationFailure("background must be a boolean.")
1026
+ }
1027
+ if (
1028
+ palette !== undefined &&
1029
+ !slopcameraIconPalettes.includes(palette as SlopcameraIconPalette)
1030
+ ) {
1031
+ operationFailure(`palette must be one of ${slopcameraIconPalettes.join(", ")}.`)
1032
+ }
1033
+ if (
1034
+ language !== undefined &&
1035
+ (typeof language !== "string" || !slopcameraIconLanguages.includes(language as never))
1036
+ ) {
1037
+ operationFailure(`language must be one of ${slopcameraIconLanguages.join(", ")}.`)
1038
+ }
1039
+ return {
1040
+ sourcePath: pathValue(input.sourcePath, "sourcePath"),
1041
+ outputPath,
1042
+ ...(recipePath === undefined ? {} : { recipePath }),
1043
+ ...(size === undefined ? {} : { size: size as number }),
1044
+ ...(background === undefined ? {} : { background: background as boolean }),
1045
+ ...(palette === undefined ? {} : { palette: palette as SlopcameraIconPalette }),
1046
+ ...(language === undefined
1047
+ ? {}
1048
+ : { language: language as NonNullable<SlopcameraIconRenderInput["language"]> }),
1049
+ }
1050
+ }
1051
+
1052
+ function parseSoundtrackFormat(value: unknown): SlopcameraSoundtrackFormat | undefined {
1053
+ if (value === undefined) return undefined
1054
+ if (!slopcameraSoundtrackFormats.includes(value as SlopcameraSoundtrackFormat)) {
1055
+ operationFailure(`format must be one of ${slopcameraSoundtrackFormats.join(", ")}.`)
1056
+ }
1057
+ return value as SlopcameraSoundtrackFormat
1058
+ }
1059
+
1060
+ function parseSoundtrackCompose(value: unknown): SlopcameraSoundtrackComposeInput {
1061
+ const input = record(value, ["sourcePath", "outputPath", "format"])
1062
+ const outputPath = optionalPathValue(input.outputPath, "outputPath")
1063
+ requireSuffix(outputPath, ".json", "outputPath")
1064
+ const format = parseSoundtrackFormat(input.format)
1065
+ return {
1066
+ sourcePath: pathValue(input.sourcePath, "sourcePath"),
1067
+ ...(outputPath === undefined ? {} : { outputPath }),
1068
+ ...(format === undefined ? {} : { format }),
1069
+ }
1070
+ }
1071
+
1072
+ function parseSoundtrackGrid(value: unknown): SlopcameraSoundtrackGridInput {
1073
+ const input = record(value, ["sourcePath", "outputPath", "format", "startUs"])
1074
+ const outputPath = optionalPathValue(input.outputPath, "outputPath")
1075
+ requireSuffix(outputPath, ".json", "outputPath")
1076
+ const format = parseSoundtrackFormat(input.format)
1077
+ const startUs = input.startUs
1078
+ if (
1079
+ startUs !== undefined &&
1080
+ (!Number.isSafeInteger(startUs) ||
1081
+ (startUs as number) < 0 ||
1082
+ (startUs as number) > slopcameraSoundtrackLimits.maxTimeUs)
1083
+ ) {
1084
+ operationFailure(
1085
+ `startUs must be an integer from 0 through ${slopcameraSoundtrackLimits.maxTimeUs}.`,
1086
+ )
1087
+ }
1088
+ return {
1089
+ sourcePath: pathValue(input.sourcePath, "sourcePath"),
1090
+ ...(outputPath === undefined ? {} : { outputPath }),
1091
+ ...(format === undefined ? {} : { format }),
1092
+ ...(startUs === undefined ? {} : { startUs: startUs as number }),
1093
+ }
1094
+ }
1095
+
838
1096
  export function parseSlopcameraOperationInput<C extends SlopcameraOperationCode>(
839
1097
  code: C,
840
1098
  input: unknown,
@@ -852,6 +1110,14 @@ export function parseSlopcameraOperationInput<C extends SlopcameraOperationCode>
852
1110
  return parseIcon(input) as SlopcameraOperationInputMap[C]
853
1111
  case "slopcamera.image.gallery":
854
1112
  return parseGallery(input) as SlopcameraOperationInputMap[C]
1113
+ case "slopcamera.icon.compose":
1114
+ return parseIconCompose(input) as SlopcameraOperationInputMap[C]
1115
+ case "slopcamera.icon.render":
1116
+ return parseIconRender(input) as SlopcameraOperationInputMap[C]
1117
+ case "slopcamera.soundtrack.compose":
1118
+ return parseSoundtrackCompose(input) as SlopcameraOperationInputMap[C]
1119
+ case "slopcamera.soundtrack.grid":
1120
+ return parseSoundtrackGrid(input) as SlopcameraOperationInputMap[C]
855
1121
  default:
856
1122
  throw new SlopcameraOperationError(
857
1123
  "INVALID_OPERATION",
@@ -1166,6 +1432,46 @@ async function executeSlopcameraOperationUncoordinated<
1166
1432
  dependencies,
1167
1433
  )) as SlopcameraOperationResultMap[C]
1168
1434
  }
1435
+ case "slopcamera.icon.compose": {
1436
+ const options = input as SlopcameraIconComposeInput
1437
+ return (await composeSlopcameraIcon({
1438
+ sourcePath: resolve(options.sourcePath),
1439
+ ...(options.outputPath === undefined
1440
+ ? {}
1441
+ : { outputPath: resolve(options.outputPath) }),
1442
+ })) as SlopcameraOperationResultMap[C]
1443
+ }
1444
+ case "slopcamera.icon.render": {
1445
+ const options = input as SlopcameraIconRenderInput
1446
+ return (await renderSlopcameraIcon({
1447
+ ...options,
1448
+ sourcePath: resolve(options.sourcePath),
1449
+ outputPath: resolve(options.outputPath),
1450
+ ...(options.recipePath === undefined
1451
+ ? {}
1452
+ : { recipePath: resolve(options.recipePath) }),
1453
+ })) as SlopcameraOperationResultMap[C]
1454
+ }
1455
+ case "slopcamera.soundtrack.compose": {
1456
+ const options = input as SlopcameraSoundtrackComposeInput
1457
+ return (await composeSlopcameraSoundtrack({
1458
+ ...options,
1459
+ sourcePath: resolve(options.sourcePath),
1460
+ ...(options.outputPath === undefined
1461
+ ? {}
1462
+ : { outputPath: resolve(options.outputPath) }),
1463
+ })) as SlopcameraOperationResultMap[C]
1464
+ }
1465
+ case "slopcamera.soundtrack.grid": {
1466
+ const options = input as SlopcameraSoundtrackGridInput
1467
+ return (await deriveSlopcameraSoundtrackGrid({
1468
+ ...options,
1469
+ sourcePath: resolve(options.sourcePath),
1470
+ ...(options.outputPath === undefined
1471
+ ? {}
1472
+ : { outputPath: resolve(options.outputPath) }),
1473
+ })) as SlopcameraOperationResultMap[C]
1474
+ }
1169
1475
  default:
1170
1476
  throw new SlopcameraOperationError(
1171
1477
  "INVALID_OPERATION",