Skip to content

Commit 16e211c

Browse files
committed
[FIX] Bloated token usage for images workflow
1 parent e7064fb commit 16e211c

File tree

12 files changed

+1964
-49
lines changed

12 files changed

+1964
-49
lines changed

_tmp/conversation.md

Lines changed: 1730 additions & 0 deletions
Large diffs are not rendered by default.

houdini/scripts/python/fxhoudinimcp_server/handlers/chop_handlers.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ def _get_node(node_path: str) -> hou.Node:
2626
return node
2727

2828

29+
def _focus_network_editor(node: hou.Node) -> None:
30+
"""Best-effort: layout the parent network, then pan the editor to *node*."""
31+
try:
32+
parent = node.parent()
33+
if parent is not None:
34+
parent.layoutChildren()
35+
for pane_tab in hou.ui.paneTabs():
36+
if pane_tab.type() == hou.paneTabType.NetworkEditor:
37+
if parent is not None:
38+
pane_tab.cd(parent.path())
39+
pane_tab.setCurrentNode(node)
40+
pane_tab.homeToSelection()
41+
return
42+
except Exception:
43+
pass
44+
45+
2946
###### chops.get_chop_data
3047

3148
def _get_chop_data(
@@ -140,6 +157,7 @@ def _create_chop_node(
140157
)
141158

142159
node.moveToGoodPosition()
160+
_focus_network_editor(node)
143161

144162
return {
145163
"node_path": node.path(),

houdini/scripts/python/fxhoudinimcp_server/handlers/cop_handlers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,23 @@ def _get_cop_node(node_path: str) -> hou.Node:
2828
return node
2929

3030

31+
def _focus_network_editor(node: hou.Node) -> None:
32+
"""Best-effort: layout the parent network, then pan the editor to *node*."""
33+
try:
34+
parent = node.parent()
35+
if parent is not None:
36+
parent.layoutChildren()
37+
for pane_tab in hou.ui.paneTabs():
38+
if pane_tab.type() == hou.paneTabType.NetworkEditor:
39+
if parent is not None:
40+
pane_tab.cd(parent.path())
41+
pane_tab.setCurrentNode(node)
42+
pane_tab.homeToSelection()
43+
return
44+
except Exception:
45+
pass
46+
47+
3148
###### cops.get_cop_info
3249

3350
def get_cop_info(node_path: str) -> dict:
@@ -268,6 +285,8 @@ def create_cop_node(
268285
f"under {parent_path}: {e}"
269286
)
270287

288+
_focus_network_editor(node)
289+
271290
return {
272291
"success": True,
273292
"node_path": node.path(),

houdini/scripts/python/fxhoudinimcp_server/handlers/lops_handlers.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,23 @@
2525

2626
###### Helpers
2727

28+
def _focus_network_editor(node: hou.Node) -> None:
29+
"""Best-effort: layout the parent network, then pan the editor to *node*."""
30+
try:
31+
parent = node.parent()
32+
if parent is not None:
33+
parent.layoutChildren()
34+
for pane_tab in hou.ui.paneTabs():
35+
if pane_tab.type() == hou.paneTabType.NetworkEditor:
36+
if parent is not None:
37+
pane_tab.cd(parent.path())
38+
pane_tab.setCurrentNode(node)
39+
pane_tab.homeToSelection()
40+
return
41+
except Exception:
42+
pass
43+
44+
2845
def _require_pxr() -> None:
2946
"""Raise if pxr modules are not available."""
3047
if not HAS_PXR:
@@ -449,6 +466,7 @@ def _create_lop_node(
449466
parm.set(prim_path)
450467

451468
node.moveToGoodPosition()
469+
_focus_network_editor(node)
452470

453471
return {
454472
"node_path": node.path(),
@@ -503,6 +521,7 @@ def _set_usd_attribute(
503521
"""
504522
python_node.parm("python").set(snippet.strip())
505523
python_node.moveToGoodPosition()
524+
_focus_network_editor(python_node)
506525

507526
# Cook to apply
508527
python_node.cook(force=True)
@@ -839,6 +858,7 @@ def _create_light(
839858
parm.set(position[i])
840859

841860
node.moveToGoodPosition()
861+
_focus_network_editor(node)
842862

843863
# Determine the prim path
844864
prim_path_parm = node.parm("primpath")
@@ -986,6 +1006,7 @@ def _set_light_properties(
9861006
python_node.setInput(0, node)
9871007
python_node.parm("python").set(snippet)
9881008
python_node.moveToGoodPosition()
1009+
_focus_network_editor(python_node)
9891010
python_node.cook(force=True)
9901011

9911012
return {
@@ -1105,8 +1126,11 @@ def _create_light_rig(
11051126
node.moveToGoodPosition()
11061127
created_nodes.append(node.path())
11071128

1108-
# Layout all children
1109-
parent.layoutChildren()
1129+
# Focus on the last created light to keep the editor alive
1130+
if created_nodes:
1131+
last_node = hou.node(created_nodes[-1])
1132+
if last_node is not None:
1133+
_focus_network_editor(last_node)
11101134

11111135
return {
11121136
"nodes": created_nodes,

houdini/scripts/python/fxhoudinimcp_server/handlers/material_handlers.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ def _get_node(node_path: str) -> hou.Node:
2626
return node
2727

2828

29+
def _focus_network_editor(node: hou.Node) -> None:
30+
"""Best-effort: layout the parent network, then pan the editor to *node*."""
31+
try:
32+
parent = node.parent()
33+
if parent is not None:
34+
parent.layoutChildren()
35+
for pane_tab in hou.ui.paneTabs():
36+
if pane_tab.type() == hou.paneTabType.NetworkEditor:
37+
if parent is not None:
38+
pane_tab.cd(parent.path())
39+
pane_tab.setCurrentNode(node)
40+
pane_tab.homeToSelection()
41+
return
42+
except Exception:
43+
pass
44+
45+
2946
def _material_summary(node: hou.Node) -> dict[str, Any]:
3047
"""Return a compact summary dict for a material node."""
3148
return {
@@ -197,6 +214,7 @@ def _create_material_network(
197214
pass
198215

199216
node.moveToGoodPosition()
217+
_focus_network_editor(node)
200218

201219
return {
202220
"material_path": node.path(),
@@ -258,6 +276,7 @@ def _assign_material(
258276
mat_sop.setDisplayFlag(True)
259277
mat_sop.setRenderFlag(True)
260278
mat_sop.moveToGoodPosition()
279+
_focus_network_editor(mat_sop)
261280

262281
return {
263282
"material_sop_path": mat_sop.path(),

houdini/scripts/python/fxhoudinimcp_server/handlers/node_handlers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,13 @@ def _node_summary(node: hou.Node) -> dict:
3434

3535

3636
def _focus_network_editor(node: hou.Node) -> None:
37-
"""Best-effort: pan the network editor to *node* so the user sees it."""
37+
"""Best-effort: layout the parent network, then pan the editor to *node*."""
3838
try:
39+
parent = node.parent()
40+
if parent is not None:
41+
parent.layoutChildren()
3942
for pane_tab in hou.ui.paneTabs():
4043
if pane_tab.type() == hou.paneTabType.NetworkEditor:
41-
parent = node.parent()
4244
if parent is not None:
4345
pane_tab.cd(parent.path())
4446
pane_tab.setCurrentNode(node)

houdini/scripts/python/fxhoudinimcp_server/handlers/rendering_handlers.py

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -110,20 +110,12 @@ def render_viewport(
110110
# Handle frame number that flipbook may insert into the filename
111111
actual_path = _find_flipbook_output(output_path, cur_frame)
112112

113-
# Read the captured image and base64-encode it so the MCP client
114-
# can display it inline.
113+
# Downscale + JPEG-compress before base64 to avoid token bloat.
115114
image_base64 = None
116-
mime_type = "image/png"
117-
actual_lower = actual_path.lower()
118-
if actual_lower.endswith((".jpg", ".jpeg")):
119-
mime_type = "image/jpeg"
120-
115+
mime_type = "image/jpeg"
121116
if os.path.isfile(actual_path):
122-
try:
123-
with open(actual_path, "rb") as f:
124-
image_base64 = base64.b64encode(f.read()).decode("ascii")
125-
except Exception as e:
126-
logger.warning("Could not read captured viewport image: %s", e)
117+
from fxhoudinimcp_server.handlers.viewport_handlers import _downscale_and_encode
118+
image_base64, mime_type = _downscale_and_encode(actual_path)
127119

128120
return {
129121
"success": True,
@@ -524,23 +516,23 @@ def render_node_network(
524516
network_editor.setCurrentNode(node)
525517
network_editor.homeToSelection()
526518

527-
# Capture the network editor as an image
528-
try:
529-
network_editor.saveAsImage(output_path)
530-
except AttributeError:
531-
# Fallback: use the desktop screenshot approach
532-
try:
533-
desktop = hou.ui.curDesktop()
534-
desktop.saveAsImage(output_path)
535-
except Exception as e:
536-
raise RuntimeError(
537-
f"Failed to capture network editor screenshot: {e}"
538-
)
519+
# Capture the network editor as an image via Qt widget grab
520+
from fxhoudinimcp_server.handlers.viewport_handlers import (
521+
_capture_pane_tab_qt, _downscale_and_encode,
522+
)
523+
_capture_pane_tab_qt(network_editor, output_path)
524+
525+
image_base64 = None
526+
mime_type = "image/jpeg"
527+
if os.path.isfile(output_path):
528+
image_base64, mime_type = _downscale_and_encode(output_path)
539529

540530
return {
541531
"success": True,
542532
"node_path": node_path,
543533
"output_path": output_path,
534+
"image_base64": image_base64,
535+
"mime_type": mime_type,
544536
}
545537

546538

houdini/scripts/python/fxhoudinimcp_server/handlers/scene_handlers.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,25 @@
1616
from fxhoudinimcp_server.dispatcher import register_handler
1717

1818

19+
###### Helpers
20+
21+
def _focus_network_editor(node: hou.Node) -> None:
22+
"""Best-effort: layout the parent network, then pan the editor to *node*."""
23+
try:
24+
parent = node.parent()
25+
if parent is not None:
26+
parent.layoutChildren()
27+
for pane_tab in hou.ui.paneTabs():
28+
if pane_tab.type() == hou.paneTabType.NetworkEditor:
29+
if parent is not None:
30+
pane_tab.cd(parent.path())
31+
pane_tab.setCurrentNode(node)
32+
pane_tab.homeToSelection()
33+
return
34+
except Exception:
35+
pass
36+
37+
1938
###### scene.get_scene_info
2039

2140
def get_scene_info() -> dict:
@@ -181,6 +200,11 @@ def import_file(
181200
file_node.parm("file").set(file_path)
182201
created_path = file_node.path()
183202

203+
# Focus on the created node
204+
created_node = hou.node(created_path)
205+
if created_node is not None:
206+
_focus_network_editor(created_node)
207+
184208
return {
185209
"success": True,
186210
"node_path": created_path,

houdini/scripts/python/fxhoudinimcp_server/handlers/vex_handlers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,13 @@ def _reverse_class_label(node: hou.Node) -> str | None:
9696

9797

9898
def _focus_network_editor(node: hou.Node) -> None:
99-
"""Best-effort: pan the network editor to *node* so the user sees it."""
99+
"""Best-effort: layout the parent network, then pan the editor to *node*."""
100100
try:
101+
parent = node.parent()
102+
if parent is not None:
103+
parent.layoutChildren()
101104
for pane_tab in hou.ui.paneTabs():
102105
if pane_tab.type() == hou.paneTabType.NetworkEditor:
103-
parent = node.parent()
104106
if parent is not None:
105107
pane_tab.cd(parent.path())
106108
pane_tab.setCurrentNode(node)

0 commit comments

Comments
 (0)