@fugood/bricks-ctor 2.25.0-beta.1 → 2.25.0-beta.10

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.
@@ -0,0 +1,115 @@
1
+ import { compile } from '../index'
2
+
3
+ const SUBSPACE_ID = 'SUBSPACE_00000000-0000-0000-0000-000000000001'
4
+ const CANVAS_ID = 'CANVAS_00000000-0000-0000-0000-000000000001'
5
+ const ANIMATION_ID = 'ANIMATION_00000000-0000-0000-0000-000000000001'
6
+
7
+ const makeApp = (animations) => {
8
+ const rootCanvas = {
9
+ __typename: 'Canvas',
10
+ id: CANVAS_ID,
11
+ items: [],
12
+ }
13
+ const rootSubspace = {
14
+ __typename: 'Subspace',
15
+ id: SUBSPACE_ID,
16
+ title: 'Main Subspace',
17
+ layout: {
18
+ width: 96,
19
+ height: 54,
20
+ },
21
+ rootCanvas,
22
+ canvases: [rootCanvas],
23
+ animations,
24
+ bricks: [],
25
+ generators: [],
26
+ data: [],
27
+ dataRouting: [],
28
+ dataCalculation: [],
29
+ }
30
+
31
+ return {
32
+ name: 'Compile animation test',
33
+ rootSubspace,
34
+ subspaces: [rootSubspace],
35
+ }
36
+ }
37
+
38
+ describe('compile animations', () => {
39
+ test('normalizes mixed legacy spring config', async () => {
40
+ const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {})
41
+ try {
42
+ const config = await compile(
43
+ makeApp([
44
+ {
45
+ __typename: 'Animation',
46
+ id: ANIMATION_ID,
47
+ alias: 'btnPressOut',
48
+ title: 'Button Press Out',
49
+ property: 'transform.scale',
50
+ config: {
51
+ __type: 'AnimationSpringConfig',
52
+ toValue: 1,
53
+ friction: 5,
54
+ tension: 200,
55
+ speed: 14,
56
+ bounciness: 8,
57
+ },
58
+ },
59
+ ]),
60
+ )
61
+
62
+ expect(config.subspace_map[SUBSPACE_ID].animation_map[ANIMATION_ID].config).toEqual({
63
+ toValue: 1,
64
+ friction: 5,
65
+ tension: 200,
66
+ })
67
+ const warning = warnSpy.mock.calls[0][0]
68
+ expect(warning).toContain('Resolved animation spring config')
69
+ expect(warning).toContain('Button Press Out')
70
+ expect(warning).toContain('btnPressOut')
71
+ expect(warning).toContain('transform.scale')
72
+ expect(warning).toContain('Main Subspace')
73
+ expect(warning).not.toContain('ANIMATION_')
74
+ expect(warning).not.toContain('SUBSPACE_')
75
+ } finally {
76
+ warnSpy.mockRestore()
77
+ }
78
+ })
79
+
80
+ test('rejects invalid animation property', async () => {
81
+ await expect(
82
+ compile(
83
+ makeApp([
84
+ {
85
+ __typename: 'Animation',
86
+ id: ANIMATION_ID,
87
+ property: 'transform.skewX',
88
+ config: {
89
+ __type: 'AnimationTimingConfig',
90
+ toValue: 1,
91
+ },
92
+ },
93
+ ]),
94
+ ),
95
+ ).rejects.toThrow(/Invalid animation property/)
96
+ })
97
+
98
+ test('rejects invalid animation config type', async () => {
99
+ await expect(
100
+ compile(
101
+ makeApp([
102
+ {
103
+ __typename: 'Animation',
104
+ id: ANIMATION_ID,
105
+ property: 'opacity',
106
+ config: {
107
+ __type: 'AnimationUnknownConfig',
108
+ toValue: 1,
109
+ },
110
+ },
111
+ ]),
112
+ ),
113
+ ).rejects.toThrow(/Invalid animation config type/)
114
+ })
115
+ })
@@ -327,6 +327,29 @@ export const templateActionNameMap = {
327
327
  animated: 'BRICK_MAPS_ANIMATED',
328
328
  },
329
329
  },
330
+ BRICK_SKETCH: {
331
+ BRICK_SKETCH_SET_TOOL: {
332
+ tool: 'BRICK_SKETCH_TOOL',
333
+ },
334
+ BRICK_SKETCH_SET_COLOR: {
335
+ color: 'BRICK_SKETCH_COLOR',
336
+ },
337
+ BRICK_SKETCH_SET_STROKE_WIDTH: {
338
+ strokeWidth: 'BRICK_SKETCH_STROKE_WIDTH',
339
+ },
340
+ BRICK_SKETCH_IMPORT_STATE: {
341
+ stateJson: 'BRICK_SKETCH_STATE_JSON',
342
+ },
343
+ BRICK_SKETCH_EXPORT_IMAGE: {
344
+ imageFormat: 'BRICK_SKETCH_IMAGE_FORMAT',
345
+ imageQuality: 'BRICK_SKETCH_IMAGE_QUALITY',
346
+ },
347
+ BRICK_SKETCH_ADD_STROKE: {
348
+ strokePoints: 'BRICK_SKETCH_STROKE_POINTS',
349
+ strokeColor: 'BRICK_SKETCH_STROKE_COLOR',
350
+ strokeWidth: 'BRICK_SKETCH_STROKE_WIDTH',
351
+ },
352
+ },
330
353
 
331
354
  GENERATOR_FILE: {
332
355
  GENERATOR_FILE_READ_CONTENT: {
package/compile/index.ts CHANGED
@@ -301,6 +301,162 @@ const animationTypeMap = {
301
301
  AnimationTimingConfig: 'timing',
302
302
  AnimationSpringConfig: 'spring',
303
303
  AnimationDecayConfig: 'decay',
304
+ } as const
305
+
306
+ type CompiledAnimationType = (typeof animationTypeMap)[keyof typeof animationTypeMap]
307
+ type WarningMetadata = Record<string, unknown>
308
+
309
+ const animationProperties = new Set([
310
+ 'transform.translateX',
311
+ 'transform.translateY',
312
+ 'transform.scale',
313
+ 'transform.scaleX',
314
+ 'transform.scaleY',
315
+ 'transform.rotate',
316
+ 'transform.rotateX',
317
+ 'transform.rotateY',
318
+ 'opacity',
319
+ ])
320
+
321
+ const animationComposeTypes = new Set(['parallel', 'sequence'])
322
+ const springConfigFamilies = [
323
+ ['stiffness', 'damping', 'mass'],
324
+ ['tension', 'friction'],
325
+ ['bounciness', 'speed'],
326
+ ]
327
+ const springConfigFamilyKeys = new Set(springConfigFamilies.flat())
328
+
329
+ const isRecord = (value: unknown): value is Record<string, unknown> =>
330
+ Boolean(value) && typeof value === 'object' && !Array.isArray(value)
331
+
332
+ const hasDefinedConfigValue = (config: Record<string, unknown>, key: string) =>
333
+ config[key] !== undefined
334
+
335
+ const assertConfigValue = (
336
+ config: Record<string, unknown>,
337
+ key: string,
338
+ errorReference: string,
339
+ ) => {
340
+ if (!hasDefinedConfigValue(config, key)) {
341
+ throw new Error(`Invalid animation config ${errorReference}: missing "${key}"`)
342
+ }
343
+ }
344
+
345
+ const assertAnimationProperty = (property: unknown, errorReference: string) => {
346
+ if (typeof property !== 'string' || !animationProperties.has(property)) {
347
+ throw new Error(
348
+ `Invalid animation property${errorReference ? ` ${errorReference}` : ''}: ${String(
349
+ property,
350
+ )}`,
351
+ )
352
+ }
353
+ return property
354
+ }
355
+
356
+ const getAnimationType = (config: unknown, errorReference: string): CompiledAnimationType => {
357
+ if (!isRecord(config)) {
358
+ throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
359
+ }
360
+
361
+ const animationType = animationTypeMap[config.__type as keyof typeof animationTypeMap]
362
+ if (!animationType) {
363
+ throw new Error(`Invalid animation config type ${errorReference}: ${String(config.__type)}`)
364
+ }
365
+ return animationType
366
+ }
367
+
368
+ const assertAnimationComposeType = (composeType: unknown, errorReference: string) => {
369
+ if (typeof composeType !== 'string' || !animationComposeTypes.has(composeType)) {
370
+ throw new Error(`Invalid animation compose type ${errorReference}: ${String(composeType)}`)
371
+ }
372
+ return composeType
373
+ }
374
+
375
+ const pickDefinedConfigValues = (config: Record<string, unknown>, keys: string[]) =>
376
+ keys.reduce((acc, key) => {
377
+ if (hasDefinedConfigValue(config, key)) acc[key] = config[key]
378
+ return acc
379
+ }, {})
380
+
381
+ const getDefinedConfigKeys = (config: Record<string, unknown>, keys: string[]) =>
382
+ keys.filter((key) => hasDefinedConfigValue(config, key))
383
+
384
+ const formatWarningMetadata = (metadata: WarningMetadata = {}) =>
385
+ Object.entries(metadata)
386
+ .filter(([, value]) => value !== undefined && value !== null && value !== '')
387
+ .map(([key, value]) => `${key}: ${String(value)}`)
388
+ .join(', ')
389
+
390
+ const formatWarningReference = (metadata?: WarningMetadata) => {
391
+ const metadataText = formatWarningMetadata(metadata)
392
+ return metadataText ? ` [${metadataText}]` : ''
393
+ }
394
+
395
+ const normalizeSpringConfig = (
396
+ config: Record<string, unknown>,
397
+ errorReference: string,
398
+ warningMetadata?: WarningMetadata,
399
+ ): Record<string, unknown> => {
400
+ assertConfigValue(config, 'toValue', errorReference)
401
+
402
+ const usedFamilies = springConfigFamilies.filter((keys) =>
403
+ keys.some((key) => hasDefinedConfigValue(config, key)),
404
+ )
405
+ if (usedFamilies.length <= 1) return config
406
+
407
+ const configWithoutSpringFamily = Object.entries(config).reduce((acc, [key, value]) => {
408
+ if (!springConfigFamilyKeys.has(key)) acc[key] = value
409
+ return acc
410
+ }, {})
411
+
412
+ // Match runtime normalization: physical spring values are most explicit,
413
+ // otherwise preserve BRICKS' historical tension/friction controls.
414
+ const resolvedFamily =
415
+ usedFamilies.find((keys) => keys.includes('stiffness')) ||
416
+ usedFamilies.find((keys) => keys.includes('tension')) ||
417
+ usedFamilies[0]
418
+ const resolvedFamilyKeys = getDefinedConfigKeys(config, resolvedFamily)
419
+ const droppedFamilyKeys = usedFamilies
420
+ .filter((keys) => keys !== resolvedFamily)
421
+ .flatMap((keys) => getDefinedConfigKeys(config, keys))
422
+
423
+ console.warn(
424
+ `[Warning] Resolved animation spring config${formatWarningReference(
425
+ warningMetadata,
426
+ )}: using ${resolvedFamilyKeys.join('/')}, dropping ${droppedFamilyKeys.join('/')}`,
427
+ )
428
+
429
+ return {
430
+ ...configWithoutSpringFamily,
431
+ ...pickDefinedConfigValues(config, resolvedFamily),
432
+ }
433
+ }
434
+
435
+ const compileAnimationConfig = (
436
+ animationType: CompiledAnimationType,
437
+ config: unknown,
438
+ errorReference: string,
439
+ warningMetadata?: WarningMetadata,
440
+ ) => {
441
+ if (!isRecord(config)) {
442
+ throw new Error(`Invalid animation config ${errorReference}: config must be an object`)
443
+ }
444
+
445
+ const compiledConfig = compileProperty(omit(config, '__type'), errorReference)
446
+
447
+ if (!isRecord(compiledConfig)) {
448
+ throw new Error(`Invalid animation config ${errorReference}: config must compile to an object`)
449
+ }
450
+
451
+ if (animationType === 'timing') {
452
+ assertConfigValue(compiledConfig, 'toValue', errorReference)
453
+ } else if (animationType === 'spring') {
454
+ return normalizeSpringConfig(compiledConfig, errorReference, warningMetadata)
455
+ } else if (animationType === 'decay') {
456
+ assertConfigValue(compiledConfig, 'velocity', errorReference)
457
+ }
458
+
459
+ return compiledConfig
304
460
  }
305
461
 
306
462
  const compileFrame = (frame: Canvas['items'][number]['frame']) => ({
@@ -629,39 +785,63 @@ export const compile = async (app: Application) => {
629
785
  `(animation index: ${animationIndex}, subspace: ${subspaceId})`,
630
786
  )
631
787
 
632
- if (animation.__typename === 'Animation') {
788
+ const animationTypename = animation.__typename
789
+ if (animationTypename === 'Animation') {
633
790
  const animationDef = animation as AnimationDef
791
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
792
+ const animationWarningMetadata = {
793
+ animationIndex,
794
+ animationTitle: animationDef.title,
795
+ animationAlias: animationDef.alias,
796
+ animationProperty: animationDef.property,
797
+ subspaceIndex,
798
+ subspaceTitle: subspace.title,
799
+ }
800
+ const animationType = getAnimationType(animationDef.config, animationErrorReference)
634
801
  map[animationId] = {
635
802
  alias: animationDef.alias,
636
803
  title: animationDef.title,
637
804
  description: animationDef.description,
638
805
  hide_short_ref: animationDef.hideShortRef,
639
806
  animationRunType: animationDef.runType,
640
- property: animationDef.property,
641
- type: animationTypeMap[animationDef.config.__type],
642
- config: compileProperty(
643
- omit(animationDef.config, '__type'),
644
- `(animation: ${animationId}, subspace ${subspaceId})`,
807
+ property: assertAnimationProperty(animationDef.property, animationErrorReference),
808
+ type: animationType,
809
+ config: compileAnimationConfig(
810
+ animationType,
811
+ animationDef.config,
812
+ animationErrorReference,
813
+ animationWarningMetadata,
645
814
  ),
646
815
  }
647
- } else if (animation.__typename === 'AnimationCompose') {
816
+ } else if (animationTypename === 'AnimationCompose') {
648
817
  const animationDef = animation as AnimationComposeDef
818
+ const animationErrorReference = `(animation: ${animationId}, subspace ${subspaceId})`
649
819
  map[animationId] = {
650
820
  alias: animationDef.alias,
651
821
  title: animationDef.title,
652
822
  description: animationDef.description,
653
823
  hide_short_ref: animationDef.hideShortRef,
654
824
  animationRunType: animationDef.runType,
655
- compose_type: animationDef.composeType,
825
+ compose_type: assertAnimationComposeType(
826
+ animationDef.composeType,
827
+ animationErrorReference,
828
+ ),
656
829
  item_list: animationDef.items.map((item, index) => {
657
830
  const innerAnimation = item()
658
- if (!innerAnimation?.id)
659
- throw new Error(
660
- `Invalid animation index: ${index} (animation: ${innerAnimation.id}, subspace ${subspaceId})`,
661
- )
662
- return { animation_id: innerAnimation.id }
831
+ const innerAnimationId = assertEntryId(
832
+ innerAnimation?.id,
833
+ 'ANIMATION',
834
+ `(animation item index: ${index}, animation: ${animationId}, subspace ${subspaceId})`,
835
+ )
836
+ return { animation_id: innerAnimationId }
663
837
  }),
664
838
  }
839
+ } else {
840
+ throw new Error(
841
+ `Invalid animation typename (animation: ${animationId}, subspace ${subspaceId}): ${String(
842
+ animationTypename,
843
+ )}`,
844
+ )
665
845
  }
666
846
  return map
667
847
  }, {}),
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@fugood/bricks-ctor",
3
- "version": "2.25.0-beta.1",
3
+ "version": "2.25.0-beta.10",
4
4
  "main": "index.ts",
5
5
  "scripts": {
6
6
  "typecheck": "tsc --noEmit",
7
7
  "build": "bun scripts/build.js"
8
8
  },
9
9
  "dependencies": {
10
- "@fugood/bricks-cli": "^2.25.0-beta.1",
10
+ "@fugood/bricks-cli": "^2.25.0-beta.10",
11
11
  "@huggingface/gguf": "^0.3.2",
12
12
  "@iarna/toml": "^3.0.0",
13
13
  "@modelcontextprotocol/sdk": "^1.15.0",
@@ -25,5 +25,5 @@
25
25
  "peerDependencies": {
26
26
  "oxfmt": "^0.36.0"
27
27
  },
28
- "gitHead": "775ccfb5dd8a2af732c936d16455049aa4e69085"
28
+ "gitHead": "7b909dc28862872743cf0459ff65d16af67721c6"
29
29
  }
@@ -41,12 +41,13 @@ const bounce: AnimationDef = {
41
41
  toValue: 1,
42
42
  friction: 7,
43
43
  tension: 40,
44
- speed: 12,
45
- bounciness: 8,
46
44
  },
47
45
  }
48
46
  ```
49
47
 
48
+ Use one spring parameter family per config: `tension/friction`, `speed/bounciness`, or
49
+ `stiffness/damping/mass`.
50
+
50
51
  ### Decay Animation
51
52
  Velocity-based deceleration animation.
52
53
 
@@ -20,16 +20,26 @@ When mobile devices or embedded systems lack hardware for local AI inference (LL
20
20
 
21
21
  ## Client Configuration
22
22
 
23
- In generator properties, configure Buttress settings:
23
+ In generator properties, configure `buttressConnectionSettings`:
24
24
 
25
25
  | Setting | Description |
26
26
  |---------|-------------|
27
- | `Enabled` | Toggle Buttress offloading |
28
- | `URL` | Buttress server URL (e.g., `http://192.168.1.100:2080`) |
29
- | `Fallback Type` | Action if Buttress unavailable: `use-local` or `no-op` |
30
- | `Strategy` | Execution preference |
27
+ | `enabled` | Toggle Buttress offloading |
28
+ | `autoDiscoverType` | `auto` (LAN auto-discovery, recommended) or `manual` (typed URL) |
29
+ | `url` | Server URL (`manual` mode only — leave empty for `auto`) |
30
+ | `fallbackType` | If Buttress is unreachable: `use-local` runs locally, `no-op` blocks the load |
31
+ | `strategy` | Execution preference (see below) |
31
32
 
32
- ### Strategy Options
33
+ The launcher provides the access token automatically — there is **no user-facing access-token setting**. In `auto` mode the launcher discovers servers bound to the device's workspace and uses its workspace-scoped JWT; in `manual` mode it sends the same JWT only when the typed URL belongs to a server bound to the same workspace (verified against the server's `/buttress/info`).
34
+
35
+ ### `autoDiscoverType` options
36
+
37
+ | Mode | When to use | What the device does |
38
+ |------|-------------|----------------------|
39
+ | `auto` | LAN deployment with a workspace-bound buttress-server | Listens for UDP announcements and picks the strongest server bound to its workspace; reconnects automatically when the workspace flips |
40
+ | `manual` | Internet-reachable server, mixed networks, or a public/unbound server | Connects to the typed `url` directly; sends a token only when the server is bound to the same workspace |
41
+
42
+ ### Strategy options
33
43
 
34
44
  | Strategy | Description |
35
45
  |----------|-------------|
@@ -37,7 +47,11 @@ In generator properties, configure Buttress settings:
37
47
  | `prefer-buttress` | Use Buttress if available, fallback to local |
38
48
  | `prefer-best` | Auto-select based on capability comparison |
39
49
 
40
- ## Generator Configuration Example
50
+ ## Generator Configuration Examples
51
+
52
+ ### Auto-discovery (recommended)
53
+
54
+ Workspace-bound launcher + workspace-bound buttress-server on the same LAN. No URL needed — discovery picks the highest-scoring server that runs the requested backend.
41
55
 
42
56
  ```typescript
43
57
  import { makeId } from 'bricks-ctor'
@@ -53,7 +67,7 @@ const llmGenerator: GeneratorLLM = {
53
67
  contextSize: 8192,
54
68
  buttressConnectionSettings: {
55
69
  enabled: true,
56
- url: 'http://192.168.1.100:2080',
70
+ autoDiscoverType: 'auto',
57
71
  fallbackType: 'use-local',
58
72
  strategy: 'prefer-best',
59
73
  },
@@ -63,6 +77,44 @@ const llmGenerator: GeneratorLLM = {
63
77
  }
64
78
  ```
65
79
 
80
+ ### Manual URL
81
+
82
+ Use when the server isn't on the launcher's broadcast domain (cross-subnet, internet, behind a reverse proxy) or you want to point at a specific public/unbound server.
83
+
84
+ ```typescript
85
+ buttressConnectionSettings: {
86
+ enabled: true,
87
+ autoDiscoverType: 'manual',
88
+ url: 'http://192.168.1.100:2080',
89
+ fallbackType: 'use-local',
90
+ strategy: 'prefer-best',
91
+ }
92
+ ```
93
+
94
+ > ⚠️ With `fallbackType: 'no-op'` the generator refuses to load locally if Buttress is unreachable. Use `'use-local'` whenever the device can also run the model standalone.
95
+
96
+ ## Integrating a discovered buttress-server
97
+
98
+ The ctor-desktop "Local Devices" panel (and the `bricks buttress scan` CLI) can hand you a server's identity, workspace, and announced generator caps. When the user asks to integrate one:
99
+
100
+ 1. **Confirm workspace match.** The discovery message says whether the server's workspace matches this project's. If it doesn't, do NOT add the integration — instruct the user to run `bricks buttress unbind && bricks buttress bind` from this workspace's owner CLI on the server host. The launcher won't trust a cross-workspace server.
101
+
102
+ 2. **Map announced types to generator templates.** For each `generators[].type` in the announce, target the matching templateKey (create the generator if absent):
103
+
104
+ | Announced type | templateKey | Default model |
105
+ |----------------|------------------------------|---------------|
106
+ | `ggml-llm` | `GENERATOR_LLM` | `ggml-org/gemma-3-12b-it-qat-GGUF` (≥20 GB usable) or `ggml-org/gpt-oss-20b-GGUF` (~12 GB usable) |
107
+ | `mlx-llm` | `GENERATOR_MLX_LLM` | `mlx-community/Qwen3-4B-4bit` (or a larger 4-bit quant if `usableBytes` allows) |
108
+ | `ggml-stt` | `GENERATOR_SPEECH_INFERENCE` | `BricksDisplay/whisper-ggml` (filename `ggml-small-q8_0.bin`) |
109
+
110
+ Pick a model that fits the announced `usableBytes`. Set `contextSize` to match.
111
+
112
+ 3. **Use the canonical auto-discovery config** from "Auto-discovery (recommended)" above. Don't switch to manual mode just because the server is on the LAN — auto mode picks the workspace-bound server automatically and rebinds when the device's workspace flips.
113
+
114
+ ### Real-device caveat
115
+
116
+ Buttress LAN auto-discovery uses native UDP via `react-native-jsi-udp`. The iOS Simulator silently fails to bind UDP, so auto mode is a no-op there. With `fallbackType: 'use-local'` the generator falls back to local inference in the simulator — dev/test stays functional. **Validate the buttress path itself only when deploying to a real Foundation device; don't gate the build on simulator validation.**
117
+
66
118
  ## Server Setup
67
119
 
68
120
  ### Requirements
@@ -98,6 +150,43 @@ bricks-buttress --config ./config.toml
98
150
  |----------|-------------|
99
151
  | `HF_TOKEN` | Hugging Face token for model downloads |
100
152
  | `ENABLE_OPENAI_COMPAT_ENDPOINT` | Set to `1` for OpenAI-compatible API |
153
+ | `BRICKS_BUTTRESS_STATE_DIR` | Override the workspace state directory (default `~/.bricks-cli/buttress`) |
154
+
155
+ ## Bind the Server to a Workspace
156
+
157
+ To use `autoDiscoverType: 'auto'` — and to require workspace-scoped JWT auth — pair the buttress-server with a BRICKS workspace using the `bricks buttress` CLI commands. An unbound server stays in legacy public mode and accepts any LAN client.
158
+
159
+ ### One-time bind
160
+
161
+ ```bash
162
+ # Log into the cloud bricks-server with the workspace owner's account
163
+ bricks auth login
164
+
165
+ # Pair the buttress-server running on this machine with the active workspace
166
+ bricks buttress bind
167
+
168
+ # Restart bricks-buttress so it picks up the new state.json
169
+ ```
170
+
171
+ `bricks buttress bind` writes `~/.bricks-cli/buttress/state.json` containing the workspace id, the issuer's Ed25519 public key, and a stable `serverId` (defaults to `buttress-<machineId>`). Override location with `--state-dir` or `$BRICKS_BUTTRESS_STATE_DIR`. For headless setups, use `bricks buttress bind --print` to emit state.json to stdout.
172
+
173
+ ### Verify and manage
174
+
175
+ ```bash
176
+ # Show local binding + workspace-side bound list
177
+ bricks buttress status
178
+
179
+ # Discover buttress-servers on the LAN (with version, auth state, generator caps)
180
+ bricks buttress scan
181
+
182
+ # Issue a long-lived workspace access token (CI / ctor agents that already hold a workspace token)
183
+ bricks buttress issue-token --ttl 86400
184
+
185
+ # Detach this server from the workspace
186
+ bricks buttress unbind
187
+ ```
188
+
189
+ After binding, launchers in the same workspace will auto-discover this server; manual-mode generators will only send their access token if the server's reported workspace matches their own.
101
190
 
102
191
  ## Server Configuration (TOML)
103
192
 
@@ -6,6 +6,12 @@ import { createServer } from 'http'
6
6
  import { parseArgs } from 'util'
7
7
  import * as TOON from '@toon-format/toon'
8
8
 
9
+ for (const stream of [process.stdout, process.stderr]) {
10
+ stream.on('error', (err) => {
11
+ if (err.code !== 'EPIPE') throw err
12
+ })
13
+ }
14
+
9
15
  const { values } = parseArgs({
10
16
  args: process.argv,
11
17
  options: {
@@ -224,16 +230,23 @@ app.on('ready', () => {
224
230
  )
225
231
  if (takeScreenshotConfig) {
226
232
  const { delay, width, height, path } = takeScreenshotConfig
227
- setTimeout(() => {
233
+ setTimeout(async () => {
234
+ let screenshotFailed = false
228
235
  console.log('Taking screenshot')
229
- mainWindow.webContents.capturePage().then((image) => {
236
+ try {
237
+ const image = await mainWindow.webContents.capturePage()
230
238
  console.log('Writing screenshot to', path)
231
- writeFile(path, image.resize({ width, height }).toJPEG(75))
239
+ await writeFile(path, image.resize({ width, height }).toJPEG(75))
240
+ } catch (err) {
241
+ screenshotFailed = true
242
+ console.error('Screenshot capture/write failed', err)
243
+ } finally {
232
244
  if (noKeepOpen) {
233
245
  console.log('Closing app')
234
- app.quit()
246
+ if (screenshotFailed) app.exit(1)
247
+ else app.quit()
235
248
  }
236
- })
249
+ }
237
250
  }, delay)
238
251
  }
239
252
  }
@@ -44,10 +44,21 @@ export interface AnimationTimingConfig {
44
44
  export interface AnimationSpringConfig {
45
45
  __type: 'AnimationSpringConfig'
46
46
  toValue: number // BRICKS Grid unit
47
- friction: number
48
- tension: number
49
- speed: number
50
- bounciness: number
47
+ // Use one spring parameter family: tension/friction, speed/bounciness,
48
+ // or stiffness/damping/mass.
49
+ friction?: number
50
+ tension?: number
51
+ speed?: number
52
+ bounciness?: number
53
+ stiffness?: number
54
+ damping?: number
55
+ mass?: number
56
+ velocity?: number
57
+ delay?: number
58
+ isInteraction?: boolean
59
+ overshootClamping?: boolean
60
+ restDisplacementThreshold?: number
61
+ restSpeedThreshold?: number
51
62
  }
52
63
 
53
64
  export interface AnimationDecayConfig {
@@ -62,7 +73,7 @@ export interface AnimationDef {
62
73
  __typename: 'Animation'
63
74
  id: string
64
75
  alias?: string
65
- title: string
76
+ title?: string
66
77
  description?: string
67
78
  hideShortRef?: boolean
68
79
  runType?: 'once' | 'loop'
@@ -25,6 +25,10 @@ Default property:
25
25
  "templateType": "${}",
26
26
  "fadeDuration": 0,
27
27
  "blurBackgroundRadius": 8,
28
+ "imageFilterEnabled": false,
29
+ "imageFilterBlur": 0,
30
+ "imageFilterBlurMode": "clamp",
31
+ "imageFilterColorMatrix": [],
28
32
  "loadSystemIos": "auto",
29
33
  "loadSystemAndroid": "auto"
30
34
  }
@@ -50,6 +54,14 @@ Default property:
50
54
  enableBlurBackground?: boolean | DataLink
51
55
  /* The blur radius of the blur filter added to the image background */
52
56
  blurBackgroundRadius?: number | DataLink
57
+ /* Enable Skia image filters. When disabled, Image uses the normal platform image renderer. */
58
+ imageFilterEnabled?: boolean | DataLink
59
+ /* Blur amount for the Skia image filter. */
60
+ imageFilterBlur?: number | DataLink
61
+ /* Tile mode for the Skia blur image filter. */
62
+ imageFilterBlurMode?: 'clamp' | 'decal' | 'repeat' | 'mirror' | DataLink
63
+ /* Optional 4x5 color matrix for the Skia image filter. Provide 20 numbers. */
64
+ imageFilterColorMatrix?: Array<number | DataLink> | DataLink
53
65
  /* [iOS] The use priority of image loading system (Auto: sdwebimage, fallback to default if failed) */
54
66
  loadSystemIos?: 'auto' | 'sdwebimage' | 'default' | DataLink
55
67
  /* [Android] The use priority of image loading system (Auto: glide, fallback to fresco if failed) */
@@ -0,0 +1,254 @@
1
+ /* Auto generated by build script
2
+ *
3
+ * Drawing canvas with undo/redo, import/export state, and image export
4
+ */
5
+ import type { SwitchCondInnerStateCurrentCanvas, SwitchCondData, SwitchDef } from '../switch'
6
+ import type { Data, DataLink } from '../data'
7
+ import type { Animation, AnimationBasicEvents } from '../animation'
8
+ import type {
9
+ Brick,
10
+ EventAction,
11
+ EventActionForItem,
12
+ ActionWithDataParams,
13
+ ActionWithParams,
14
+ Action,
15
+ EventProperty,
16
+ } from '../common'
17
+ import type { BrickBasicProperty, BrickBasicEvents, BrickBasicEventsForItem } from '../brick-base'
18
+ import type { TemplateEventPropsMap } from '../../utils/event-props'
19
+
20
+ /* Undo the last stroke */
21
+ export type BrickSketchActionUndo = Action & {
22
+ __actionName: 'BRICK_SKETCH_UNDO'
23
+ }
24
+
25
+ /* Redo the last undone stroke */
26
+ export type BrickSketchActionRedo = Action & {
27
+ __actionName: 'BRICK_SKETCH_REDO'
28
+ }
29
+
30
+ /* Clear all strokes from the canvas */
31
+ export type BrickSketchActionClear = Action & {
32
+ __actionName: 'BRICK_SKETCH_CLEAR'
33
+ }
34
+
35
+ /* Set the active tool */
36
+ export type BrickSketchActionSetTool = ActionWithParams & {
37
+ __actionName: 'BRICK_SKETCH_SET_TOOL'
38
+ params?: Array<{
39
+ input: 'tool'
40
+ value?: 'pen' | 'eraser' | DataLink | EventProperty
41
+ mapping?: string
42
+ }>
43
+ }
44
+
45
+ /* Set the stroke color */
46
+ export type BrickSketchActionSetColor = ActionWithParams & {
47
+ __actionName: 'BRICK_SKETCH_SET_COLOR'
48
+ params?: Array<{
49
+ input: 'color'
50
+ value?: string | DataLink | EventProperty
51
+ mapping?: string
52
+ }>
53
+ }
54
+
55
+ /* Set the stroke width (px) */
56
+ export type BrickSketchActionSetStrokeWidth = ActionWithParams & {
57
+ __actionName: 'BRICK_SKETCH_SET_STROKE_WIDTH'
58
+ params?: Array<{
59
+ input: 'strokeWidth'
60
+ value?: number | DataLink | EventProperty
61
+ mapping?: string
62
+ }>
63
+ }
64
+
65
+ /* Import sketch state (JSON string from a previous export) */
66
+ export type BrickSketchActionImportState = ActionWithParams & {
67
+ __actionName: 'BRICK_SKETCH_IMPORT_STATE'
68
+ params?: Array<{
69
+ input: 'stateJson'
70
+ value?: string | DataLink | EventProperty
71
+ mapping?: string
72
+ }>
73
+ }
74
+
75
+ /* Export the current sketch state to the BRICK_SKETCH_STATE outlet */
76
+ export type BrickSketchActionExportState = Action & {
77
+ __actionName: 'BRICK_SKETCH_EXPORT_STATE'
78
+ }
79
+
80
+ /* Export the canvas as an image (file URI emitted to BRICK_SKETCH_IMAGE_URI) */
81
+ export type BrickSketchActionExportImage = ActionWithParams & {
82
+ __actionName: 'BRICK_SKETCH_EXPORT_IMAGE'
83
+ params?: Array<
84
+ | {
85
+ input: 'imageFormat'
86
+ value?: 'png' | 'jpeg' | DataLink | EventProperty
87
+ mapping?: string
88
+ }
89
+ | {
90
+ input: 'imageQuality'
91
+ value?: number | DataLink | EventProperty
92
+ mapping?: string
93
+ }
94
+ >
95
+ }
96
+
97
+ /* Add a stroke programmatically */
98
+ export type BrickSketchActionAddStroke = ActionWithParams & {
99
+ __actionName: 'BRICK_SKETCH_ADD_STROKE'
100
+ params?: Array<
101
+ | {
102
+ input: 'strokePoints'
103
+ value?: any | EventProperty
104
+ mapping?: string
105
+ }
106
+ | {
107
+ input: 'strokeColor'
108
+ value?: string | DataLink | EventProperty
109
+ mapping?: string
110
+ }
111
+ | {
112
+ input: 'strokeWidth'
113
+ value?: number | DataLink | EventProperty
114
+ mapping?: string
115
+ }
116
+ >
117
+ }
118
+
119
+ interface BrickSketchDef {
120
+ /*
121
+ Default property:
122
+ {
123
+ "tool": "pen",
124
+ "strokeColor": "#000000",
125
+ "strokeWidth": 3,
126
+ "pressureSensitive": true,
127
+ "layoutType": "cover",
128
+ "scale": 1,
129
+ "mode": "pen",
130
+ "theme": "auto",
131
+ "backgroundPattern": "dot",
132
+ "hideToolbar": false,
133
+ "hideUndo": false,
134
+ "hideRedo": false,
135
+ "hidePencilTool": false,
136
+ "hideColorPicker": false,
137
+ "hideThickness": false,
138
+ "availableColors": [
139
+ "#000000",
140
+ "#ffffff",
141
+ "#e53935",
142
+ "#fb8c00",
143
+ "#fdd835",
144
+ "#43a047",
145
+ "#1e88e5",
146
+ "#8e24aa"
147
+ ]
148
+ }
149
+ */
150
+ property?: BrickBasicProperty & {
151
+ /* Initial sketch state to load on mount (object or JSON string from a previous export). When `BRICK_SKETCH_STATE` outlet feeds the same value back via binding, the echo is detected and ignored to avoid an update loop. */
152
+ initialState?: string | DataLink | DataLink | {} | DataLink
153
+ /* Drawing tool */
154
+ tool?: 'pen' | 'eraser' | DataLink
155
+ /* Stroke color */
156
+ strokeColor?: string | DataLink
157
+ /* Stroke width (px) */
158
+ strokeWidth?: number | DataLink
159
+ /* Vary stroke width by pen pressure */
160
+ pressureSensitive?: boolean | DataLink
161
+ /* Canvas layout. `cover` fits the brick frame; `extend` lets the canvas grow with content */
162
+ layoutType?: 'cover' | 'extend' | DataLink
163
+ /* Canvas resolution scale (device-pixel multiplier) */
164
+ scale?: number | DataLink
165
+ /* Interaction mode. `pen` = drawing (scroll disabled), `scroll` = scroll/pan (drawing disabled) */
166
+ mode?: 'pen' | 'scroll' | DataLink
167
+ /* Toolbar/UI theme. `auto` follows the system color scheme; `light`/`dark` force the palette. */
168
+ theme?: 'auto' | 'light' | 'dark' | DataLink
169
+ /* Background color of the canvas */
170
+ backgroundColor?: string | DataLink
171
+ /* Background image URL (covers the canvas; rendered behind strokes) */
172
+ backgroundImage?: string | DataLink
173
+ /* Decorative background pattern when no background image is provided */
174
+ backgroundPattern?: 'dot' | 'grid' | 'none' | DataLink
175
+ /* Hide the entire built-in toolbar */
176
+ hideToolbar?: boolean | DataLink
177
+ /* Hide the undo button in the toolbar */
178
+ hideUndo?: boolean | DataLink
179
+ /* Hide the redo button in the toolbar */
180
+ hideRedo?: boolean | DataLink
181
+ /* Hide the pen/eraser tool toggle */
182
+ hidePencilTool?: boolean | DataLink
183
+ /* Hide the color picker */
184
+ hideColorPicker?: boolean | DataLink
185
+ /* Hide the stroke thickness slider */
186
+ hideThickness?: boolean | DataLink
187
+ /* Colors shown in the built-in color picker */
188
+ availableColors?: Array<string | DataLink> | DataLink
189
+ }
190
+ events?: BrickBasicEvents & {
191
+ /* A stroke was just committed (drawn or added programmatically) */
192
+ onStrokeEnd?: Array<EventAction<string & keyof TemplateEventPropsMap['Sketch']['onStrokeEnd']>>
193
+ /* The canvas was cleared */
194
+ onClear?: Array<EventAction>
195
+ /* Sketch state changed (any commit, undo, redo, clear, or import) */
196
+ onStateChange?: Array<EventAction>
197
+ /* Active tool changed */
198
+ onToolChange?: Array<
199
+ EventAction<string & keyof TemplateEventPropsMap['Sketch']['onToolChange']>
200
+ >
201
+ /* Image export finished */
202
+ onExportImage?: Array<
203
+ EventAction<string & keyof TemplateEventPropsMap['Sketch']['onExportImage']>
204
+ >
205
+ }
206
+ outlets?: {
207
+ /* Current sketch state (strokes, settings, undo/redo stacks) */
208
+ state?: () => Data<{
209
+ strokes?: any[]
210
+ undone?: any[]
211
+ settings?: { [key: string]: any }
212
+ [key: string]: any
213
+ }>
214
+ /* URI of the most recently exported image */
215
+ imageUri?: () => Data<string>
216
+ /* Most recently committed stroke (points, color, width) */
217
+ lastStroke?: () => Data<{
218
+ color?: string
219
+ width?: number
220
+ tool?: string
221
+ points?: any[]
222
+ [key: string]: any
223
+ }>
224
+ }
225
+ animation?: AnimationBasicEvents & {
226
+ onStrokeEnd?: Animation
227
+ onClear?: Animation
228
+ onStateChange?: Animation
229
+ onToolChange?: Animation
230
+ onExportImage?: Animation
231
+ }
232
+ }
233
+
234
+ /* Drawing canvas with undo/redo, import/export state, and image export */
235
+ export type BrickSketch = Brick &
236
+ BrickSketchDef & {
237
+ templateKey: 'BRICK_SKETCH'
238
+ switches?: Array<
239
+ SwitchDef &
240
+ BrickSketchDef & {
241
+ conds?: Array<{
242
+ method: '==' | '!=' | '>' | '<' | '>=' | '<='
243
+ cond:
244
+ | SwitchCondInnerStateCurrentCanvas
245
+ | SwitchCondData
246
+ | {
247
+ __typename: 'SwitchCondInnerStateOutlet'
248
+ outlet: 'state' | 'imageUri' | 'lastStroke'
249
+ value: any
250
+ }
251
+ }>
252
+ }
253
+ >
254
+ }
@@ -19,3 +19,4 @@ export * from './Camera'
19
19
  export * from './WebRtcStream'
20
20
  export * from './GenerativeMedia'
21
21
  export * from './Maps'
22
+ export * from './Sketch'
package/types/data.ts CHANGED
@@ -18,7 +18,7 @@ export type Data<T = any> = DataDef & {
18
18
  __typename: 'Data'
19
19
  id: string
20
20
  alias?: string
21
- title: string
21
+ title?: string
22
22
  description?: string
23
23
  hideShortRef?: boolean
24
24
  metadata?: {
@@ -713,6 +713,7 @@ Default property:
713
713
  | {
714
714
  enabled?: boolean | DataLink
715
715
  url?: string | DataLink
716
+ autoDiscoverType?: 'manual' | 'auto' | DataLink
716
717
  fallbackType?: 'use-local' | 'no-op' | DataLink
717
718
  strategy?: 'prefer-local' | 'prefer-buttress' | 'prefer-best' | DataLink
718
719
  }
@@ -158,6 +158,7 @@ Default property:
158
158
  | {
159
159
  enabled?: boolean | DataLink
160
160
  url?: string | DataLink
161
+ autoDiscoverType?: 'manual' | 'auto' | DataLink
161
162
  fallbackType?: 'use-local' | 'no-op' | DataLink
162
163
  strategy?: 'prefer-local' | 'prefer-buttress' | 'prefer-best' | DataLink
163
164
  }
@@ -174,6 +174,7 @@ Default property:
174
174
  isCapturing?: boolean
175
175
  data?: {
176
176
  result?: string
177
+ language?: string
177
178
  [key: string]: any
178
179
  }
179
180
  vadEvent?: {
@@ -215,6 +216,7 @@ Default property:
215
216
  isCapturing?: boolean
216
217
  data?: {
217
218
  result?: string
219
+ language?: string
218
220
  [key: string]: any
219
221
  }
220
222
  vadEvent?: {
@@ -238,6 +240,10 @@ Default property:
238
240
  }>
239
241
  /* Stabilized transcription text from completed slices */
240
242
  stabilizedText?: () => Data<string>
243
+ /* Latest detected language code (e.g. `en`, `zh`, `ja`) from any transcribe slice (best-effort, may flicker on short slices) */
244
+ detectedLanguage?: () => Data<string>
245
+ /* Detected language code of the most recently stabilized (finalized) slice */
246
+ stabilizedLanguage?: () => Data<string>
241
247
  /* Audio output file path (auto-generated when saving audio) */
242
248
  audioOutputPath?: () => Data<string>
243
249
  /* Available microphone devices (Web / Desktop only) */
@@ -269,6 +275,8 @@ export type GeneratorRealtimeTranscription = Generator &
269
275
  | 'lastTranscribeEvent'
270
276
  | 'lastVadEvent'
271
277
  | 'stabilizedText'
278
+ | 'detectedLanguage'
279
+ | 'stabilizedLanguage'
272
280
  | 'audioOutputPath'
273
281
  | 'devices'
274
282
  value: any
@@ -323,6 +323,7 @@ Default property:
323
323
  | {
324
324
  enabled?: boolean | DataLink
325
325
  url?: string | DataLink
326
+ autoDiscoverType?: 'manual' | 'auto' | DataLink
326
327
  fallbackType?: 'use-local' | 'no-op' | DataLink
327
328
  strategy?: 'prefer-local' | 'prefer-buttress' | 'prefer-best' | DataLink
328
329
  }
@@ -358,9 +359,12 @@ Default property:
358
359
  transcribeProgress?: () => Data<number>
359
360
  /* Inference result */
360
361
  transcribeResult?: () => Data<string>
362
+ /* Detected language code of the latest transcription (e.g. `en`, `zh`, `ja`) */
363
+ detectedLanguage?: () => Data<string>
361
364
  /* Inference result details */
362
365
  transcribeDetails?: () => Data<{
363
366
  result?: string
367
+ language?: string
364
368
  segments?: Array<{
365
369
  start?: number
366
370
  end?: number
@@ -405,6 +409,7 @@ export type GeneratorSpeechInference = Generator &
405
409
  | 'isTranscribing'
406
410
  | 'transcribeProgress'
407
411
  | 'transcribeResult'
412
+ | 'detectedLanguage'
408
413
  | 'transcribeDetails'
409
414
  | 'recordedPath'
410
415
  value: any
package/types/subspace.ts CHANGED
@@ -7,7 +7,7 @@ import type { DataCalculation } from './data-calc'
7
7
  export type Subspace = {
8
8
  __typename: 'Subspace'
9
9
  id: string
10
- title: string
10
+ title?: string
11
11
  description?: string
12
12
  hideShortRef?: boolean
13
13
  unused?: boolean
@@ -0,0 +1,58 @@
1
+ import { makeId } from '../id'
2
+
3
+ const UUID = '[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}'
4
+ const UUID_ONLY_RE = new RegExp(`^${UUID}$`)
5
+
6
+ describe('makeId', () => {
7
+ describe('type prefixes', () => {
8
+ const cases = [
9
+ ['animation', 'ANIMATION_'],
10
+ ['brick', 'BRICK_'],
11
+ ['dynamic-brick', 'DYNAMIC_BRICK_'],
12
+ ['canvas', 'CANVAS_'],
13
+ ['generator', 'GENERATOR_'],
14
+ ['data', 'PROPERTY_BANK_DATA_NODE_'],
15
+ ['switch', 'BRICK_STATE_GROUP_'],
16
+ ['property_bank_command', 'PROPERTY_BANK_COMMAND_NODE_'],
17
+ ['property_bank_calc', 'PROPERTY_BANK_COMMAND_MAP_'],
18
+ ['automation_map', 'AUTOMATION_MAP_'],
19
+ ['test', 'TEST_'],
20
+ ['test_case', 'TEST_CASE_'],
21
+ ['test_var', 'TEST_VAR_'],
22
+ ]
23
+
24
+ test.each(cases)('produces %p prefix', (type, prefix) => {
25
+ expect(makeId(type)).toMatch(new RegExp(`^${prefix}${UUID}$`))
26
+ })
27
+
28
+ test('returns unique random uuids across calls', () => {
29
+ expect(makeId('brick')).not.toBe(makeId('brick'))
30
+ })
31
+
32
+ test('unknown type falls through to an unprefixed uuid', () => {
33
+ expect(makeId('not-a-real-type')).toMatch(UUID_ONLY_RE)
34
+ })
35
+ })
36
+
37
+ describe('subspace', () => {
38
+ test('throws because subspace IDs must be fixed', () => {
39
+ expect(() => makeId('subspace')).toThrow(/subspace is not supported/)
40
+ })
41
+ })
42
+
43
+ describe('snapshotMode', () => {
44
+ test('produces sequential, deterministic uuids', () => {
45
+ const a = makeId('brick', { snapshotMode: true })
46
+ const b = makeId('brick', { snapshotMode: true })
47
+ expect(a).toMatch(/^BRICK_00000000-0000-0000-0000-\d{12}$/)
48
+ expect(b).toMatch(/^BRICK_00000000-0000-0000-0000-\d{12}$/)
49
+ expect(Number(b.slice(-12))).toBe(Number(a.slice(-12)) + 1)
50
+ })
51
+
52
+ test('snapshotMode false falls back to random uuid', () => {
53
+ const id = makeId('canvas', { snapshotMode: false })
54
+ expect(id).toMatch(new RegExp(`^CANVAS_${UUID}$`))
55
+ expect(id).not.toMatch(/CANVAS_00000000-0000-0000-0000-/)
56
+ })
57
+ })
58
+ })
package/utils/data.ts CHANGED
@@ -86,7 +86,7 @@ type SystemDataInfo = {
86
86
  name: SystemDataName
87
87
  id: string
88
88
  type: 'string' | 'number' | 'bool' | 'array' | 'object' | 'any'
89
- title: string
89
+ title?: string
90
90
  description?: string
91
91
  schema?: object
92
92
  value?: any
@@ -141,6 +141,11 @@ export const templateEventPropsMap = {
141
141
  BRICK_MAPS_REGION_LONGITUDE_DELTA: 'number',
142
142
  },
143
143
  },
144
+ Sketch: {
145
+ onStrokeEnd: { BRICK_SKETCH_STROKE_COUNT: 'number' },
146
+ onToolChange: { BRICK_SKETCH_TOOL: 'string' },
147
+ onExportImage: { BRICK_SKETCH_IMAGE_URI: 'string' },
148
+ },
144
149
  Tick: {
145
150
  ticking: { GENERATOR_TICK_COUNTDOWN: 'number', GENERATOR_TICK_VALUE: 'any' },
146
151
  completed: { GENERATOR_TICK_COUNTDOWN: 'number', GENERATOR_TICK_VALUE: 'any' },
@@ -598,6 +603,7 @@ export const templateEventPropsMap = {
598
603
  onError: { GENERATOR_SPEECH_INFERENCE_ERROR: 'string' },
599
604
  onTranscribed: {
600
605
  GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_RESULT: 'string',
606
+ GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_LANGUAGE: 'string',
601
607
  GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_START_TIME: 'number',
602
608
  GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_END_TIME: 'number',
603
609
  GENERATOR_SPEECH_INFERENCE_TRANSCRIBE_TIME: 'number',
@@ -619,10 +625,11 @@ export const templateEventPropsMap = {
619
625
  RealtimeTranscription: {
620
626
  onTranscribe: {
621
627
  GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_EVENT:
622
- '{ type?: string sliceIndex?: number isCapturing?: boolean data?: { result?: string [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any }',
628
+ '{ type?: string sliceIndex?: number isCapturing?: boolean data?: { result?: string language?: string [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any }',
623
629
  GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_TYPE: 'string',
624
630
  GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_SLICE_INDEX: 'number',
625
631
  GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_RESULT_TEXT: 'string',
632
+ GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_LANGUAGE: 'string',
626
633
  GENERATOR_REALTIME_TRANSCRIPTION_TRANSCRIBE_IS_CAPTURING: 'boolean',
627
634
  GENERATOR_REALTIME_TRANSCRIPTION_VAD_EVENT:
628
635
  '{ type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any }',
@@ -644,10 +651,13 @@ export const templateEventPropsMap = {
644
651
  GENERATOR_REALTIME_TRANSCRIPTION_STATS:
645
652
  '{ type?: string timestamp?: number data?: { [key: string]: any } [key: string]: any }',
646
653
  },
647
- onStabilized: { GENERATOR_REALTIME_TRANSCRIPTION_STABILIZED_TEXT: 'string' },
654
+ onStabilized: {
655
+ GENERATOR_REALTIME_TRANSCRIPTION_STABILIZED_TEXT: 'string',
656
+ GENERATOR_REALTIME_TRANSCRIPTION_STABILIZED_LANGUAGE: 'string',
657
+ },
648
658
  onEnd: {
649
659
  GENERATOR_REALTIME_TRANSCRIPTION_END_RESULTS:
650
- 'Array<{ transcribeEvent?: { type?: string sliceIndex?: number isCapturing?: boolean data?: { result?: string [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any }>',
660
+ 'Array<{ transcribeEvent?: { type?: string sliceIndex?: number isCapturing?: boolean data?: { result?: string language?: string [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any } vadEvent?: { type?: string confidence?: number duration?: number sliceIndex?: number timestamp?: number [key: string]: any } [key: string]: any }>',
651
661
  GENERATOR_REALTIME_TRANSCRIPTION_END_AUDIO_OUTPUT_PATH: 'string',
652
662
  },
653
663
  },