@deepblend/dsh-blender-tool 0.7.0 → 0.8.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.
@@ -8,7 +8,12 @@ const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
8
8
 
9
9
  /** Attach the receipt's existing preview, with no compile, render or output-time IO. */
10
10
  export async function attachCommittedPreview(ctx, exec, resolved, value, { projectId, summary }) {
11
- const artifact = summary?.previews?.[0]
11
+ return attachVerifiedRenderedImage(ctx, exec, resolved, value, { projectId,
12
+ revision: summary?.revision, digest: summary?.digest, artifact: summary?.previews?.[0], kind: 'preview' })
13
+ }
14
+
15
+ /** Read the rendered receipt's pixels once; preserve successful rendering if attachment fails. */
16
+ export async function attachVerifiedRenderedImage(ctx, exec, resolved, value, { projectId, revision, digest, artifact, kind }) {
12
17
  if (!artifact) return value
13
18
  const cancelled = () => {
14
19
  if (exec?.signal?.aborted) throw new BlenderError(BlenderErrorCode.ABORTED,
@@ -16,18 +21,20 @@ export async function attachCommittedPreview(ctx, exec, resolved, value, { proje
16
21
  }
17
22
  try {
18
23
  cancelled()
19
- const revision = summary.revision
20
- if (typeof projectId !== 'string' || !projectId || !/^r[0-9]{4,}$/.test(revision) ||
21
- !/^[a-f0-9]{64}$/.test(summary.digest) || artifact.kind !== 'preview' ||
22
- artifact.sourceRevision !== revision || artifact.sourceDigest !== summary.digest ||
24
+ if (!['preview', 'contact-sheet'].includes(kind) || typeof projectId !== 'string' || !projectId || !/^r[0-9]{4,}$/.test(revision) ||
25
+ !/^[a-f0-9]{64}$/.test(digest) || artifact.kind !== kind ||
26
+ artifact.sourceRevision !== revision || artifact.sourceDigest !== digest ||
23
27
  artifact.mime !== 'image/png' || typeof artifact.path !== 'string' ||
24
- !artifact.path.startsWith(`revisions/${revision}/previews/`) ||
28
+ !artifact.path.startsWith(`revisions/${revision}/${kind === 'contact-sheet' ? 'contact-sheets' : 'previews'}/`) ||
25
29
  artifact.path.split('/').some(part => !part || part === '.' || part === '..') ||
26
30
  !/^[a-f0-9]{64}$/.test(artifact.sha256) ||
27
31
  !Number.isSafeInteger(artifact.bytes) || artifact.bytes <= 0 || artifact.bytes > MAX_IMAGE_BYTES ||
28
- !Number.isSafeInteger(artifact.width) || artifact.width <= 0 || artifact.width > 2048 ||
29
- !Number.isSafeInteger(artifact.height) || artifact.height <= 0 || artifact.height > 2048 ||
30
- typeof artifact.cameraId !== 'string' || !artifact.cameraId || !Number.isSafeInteger(artifact.frame) ||
32
+ !Number.isSafeInteger(artifact.width) || artifact.width <= 0 || artifact.width > (kind === 'contact-sheet' ? 8192 : 2048) ||
33
+ !Number.isSafeInteger(artifact.height) || artifact.height <= 0 || artifact.height > (kind === 'contact-sheet' ? 8192 : 2048) ||
34
+ (kind === 'preview' && (typeof artifact.cameraId !== 'string' || !artifact.cameraId || !Number.isSafeInteger(artifact.frame))) ||
35
+ (kind === 'contact-sheet' && (artifact.slot !== 'preview-current' || !Array.isArray(artifact.views) || artifact.views.length === 0 ||
36
+ artifact.views.length > 64 || new Set(artifact.views).size !== artifact.views.length ||
37
+ artifact.views.some(id => typeof id !== 'string' || !id))) ||
31
38
  typeof resolved.studio.readArtifact !== 'function') throw new Error('Invalid saved preview receipt')
32
39
  const read = await resolved.studio.readArtifact({ projectId, path: artifact.path })
33
40
  cancelled()
@@ -39,19 +46,23 @@ export async function attachCommittedPreview(ctx, exec, resolved, value, { proje
39
46
  throw new Error('Saved PNG differs from its receipt')
40
47
  }
41
48
  const attached = await persistVerifiedPng(resolved.attachments, read.bytes, artifact,
42
- `preview-${revision}-${artifact.cameraId}-${artifact.frame}.png`, exec?.signal)
49
+ kind === 'contact-sheet' ? `preview-sheet-${revision}.png` : `preview-${revision}-${artifact.cameraId}-${artifact.frame}.png`, exec?.signal)
43
50
  cancelled()
44
51
  return await imageForCallingModel(ctx, exec, {
45
52
  ...value, image: attached.image,
46
- imageSource: { ...artifact, projectId, revision, digest: summary.digest },
47
- text: value.text + '\n\nThe image uses the preview already saved with this revision; no extra render was started.' +
53
+ imageSource: { ...artifact, projectId, revision, digest },
54
+ text: value.text + (kind === 'contact-sheet'
55
+ ? '\n\nThe verified contact sheet contains the views just rendered and saved; no additional render or reviewer call was started.'
56
+ : '\n\nThe image uses the preview already saved with this revision; no extra render was started.') +
48
57
  (attached.note ? '\n' + attached.note : ''),
49
58
  })
50
59
  } catch (cause) {
51
60
  cancelled()
52
61
  if (cause?.code === BlenderErrorCode.ABORTED) throw cause
53
62
  return { ...value, image: null,
54
- text: value.text + '\n\nThe revision remains saved, but its preview could not be verified and attached here. ' +
63
+ text: value.text + (kind === 'contact-sheet'
64
+ ? '\n\nThe rendered views remain saved, but their contact sheet could not be verified and attached here. '
65
+ : '\n\nThe revision remains saved, but its preview could not be verified and attached here. ') +
55
66
  'The model has not seen this image. Use the receipt to inspect the saved artifact; do not recreate the project or repeat the render to repair an attachment.' }
56
67
  }
57
68
  }
@@ -40,6 +40,7 @@
40
40
  */
41
41
 
42
42
  import { defineTool } from '@deepseek-ai/dsh-tools'
43
+ import { attachVerifiedRenderedImage } from './committed-preview.js'
43
44
  import { imageForCallingModel } from './image-delivery.js'
44
45
 
45
46
  import { BlenderWarningCode, warning } from '@deepblend/dsh-blender-contracts'
@@ -87,7 +88,7 @@ function previewViews(ctx) {
87
88
  description:
88
89
  'Render a MULTI-VIEW preview of a revision in one go: the active camera, a three-quarter angle, a ' +
89
90
  'top-down view and a subject close-up, all from one Blender process, and measure each one. Returns ' +
90
- 'the images\' paths, a composed contact sheet, and per-view measurements. Rendering does NOT create a ' +
91
+ 'the saved images, a verified contact sheet attached for the calling model, and per-view measurements. Rendering does NOT create a ' +
91
92
  'revision — a preview observes a scene rather than changing it — so this is always safe to call and ' +
92
93
  'never needs a baseRevision. Use it to look at a scene; use blender_visual_review when you also want a ' +
93
94
  'score and a judgment.\n\n' + MEASUREMENT_GLOSSARY,
@@ -114,12 +115,12 @@ function previewViews(ctx) {
114
115
  description: 'Override the preview profile\'s sample count; a value above the configured budget is reduced and reported.',
115
116
  },
116
117
  },
117
- output: TOOL_OUTPUT,
118
+ output: TOOL_OUTPUT_WITH_IMAGE,
118
119
  async execute(args, exec) {
119
120
  const resolved = resolveStudio(ctx)
120
121
  if (resolved.unavailable !== undefined) return { ok: false, ...resolved.unavailable }
121
122
  try {
122
- const { data, canonicalWarnings } = await canonicalCall(resolved.studio.renderViews({
123
+ const { data, canonicalWarnings } = await canonicalCall(Promise.resolve(resolved.studio.renderViews({
123
124
  ...definedFields({
124
125
  projectId: args.projectId,
125
126
  revision: args.revision,
@@ -130,7 +131,7 @@ function previewViews(ctx) {
130
131
  samples: args.samples,
131
132
  }),
132
133
  signal: exec.signal,
133
- }), warning)
134
+ })).then(({ pngs, ...metadata }) => metadata), warning)
134
135
  const scored = resolved.studio.scoreVisualViews({ views: data.views, subjectId: data.subjectId })
135
136
  const notes = [
136
137
  `Revision: ${data.revision}`,
@@ -149,13 +150,15 @@ function previewViews(ctx) {
149
150
  const sheet = (data.revisionPreviews ?? data.contactSheets ?? [])
150
151
  notes.push('')
151
152
  notes.push('The paths are relative to the project directory. The measured score is what the automated')
152
- notes.push('scorer makes of these pixels; call blender_visual_review to score AND hear what a vision')
153
- notes.push('model sees on the contact sheet.')
154
- return {
153
+ notes.push('scorer makes of these pixels. Inspect the attached sheet directly; use blender_visual_review')
154
+ notes.push('when you need a separate vision reviewer judgement, not just to obtain these images.')
155
+ return await attachVerifiedRenderedImage(ctx, exec, resolved, {
155
156
  ok: true,
156
157
  text: renderSuccess(`Rendered ${data.views.length} view(s) of ${data.revision}.`, { ...data, score: scored.score, issues: scored.issues }, { notes, warnings: [...(data.warnings ?? []), ...canonicalWarnings] }),
157
158
  data: { ...data, score: scored.score, issues: scored.issues, sheet },
158
- }
159
+ }, { projectId: args.projectId, revision: data.revision, digest: data.digest,
160
+ artifact: data.previewSheets?.current, kind: 'contact-sheet' })
161
+
159
162
  } catch (cause) {
160
163
  const failure = renderFailure(cause, 'PREVIEW_VIEWS_FAILED')
161
164
  return { ok: false, ...failure }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deepblend/dsh-blender-tool",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "type": "module",
5
5
  "description": "Agent-preset tool consumer: registers the model-visible DeepBlend Blender tools. Publishes no service.",
6
6
  "main": "lib/index.js",
@@ -11,7 +11,7 @@
11
11
  "./package.json": "./package.json"
12
12
  },
13
13
  "dependencies": {
14
- "@deepblend/dsh-blender-contracts": "0.7.0"
14
+ "@deepblend/dsh-blender-contracts": "0.8.1"
15
15
  },
16
16
  "license": "MIT",
17
17
  "repository": {