Files
ttulttul-ComfyUI-Modal/__init__.py
T
Ken SimpsonandClaude Opus 5.5 dba3a6abd9 Add Remote Progressive Preview node and run every mapped item (#9)
* Expect resolved STRING type for ModalMapInput boundaries

69a33e3 made _remote_output_io_type resolve wildcard "*" outputs through
their linked input, so a ModalMapInput boundary fed by a STRING list now
reports STRING, matching the mapped_input type the test already expects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Add Remote Progressive Preview node

A local PreviewImage-style node that shows one empty slot per mapped item
when remote work starts and fills each slot as soon as that item's images
stream back, instead of waiting for ComfyUI to deliver the whole result.

- Planning records direct consumers as progressive_preview_target_node_ids
  and exempts them from local bridge materializers.
- The remote shim streams boundary outputs for those targets; mapped item
  payloads carry map_item_count and dispatch emits a start event.
- The host saves streamed images to temp, records them per prompt/node, and
  sends modal_progressive_preview websocket events.
- The node reuses streamed slots on final execution and only materializes
  bridge refs that were never streamed.
- web/progressive_preview.js renders the slot grid and hands off to the
  native preview on the final executed event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Execute every mapped item instead of reusing shared-session outputs

Mapped items share one remote session. The restored-session short-circuit,
meant for identical per-lane seed phases, looked outputs up by node id
only, so after the first item on a container every later item skipped
sampling and returned that earlier item's image under its own bridge key.
Payloads carrying map_item_index now always execute.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep finished progressive previews handed off to the native preview

Hide the panel with an inline display style so the frontend's DOM-widget
wrapper cannot override the hidden attribute, and ignore replayed events
for a prompt whose final images were already delivered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:09:02 -07:00

135 lines
4.7 KiB
Python

"""ComfyUI Modal-Sync extension entrypoint."""
from __future__ import annotations
import logging
if __package__:
from .modal_sdk import ensure_modal_sdk_available
else: # pragma: no cover - pytest may collect this file as a top-level module.
from modal_sdk import ensure_modal_sdk_available
logger = logging.getLogger(__name__)
_EXTENSION_LOGGER_NAME = __name__.split(".")[0]
_EXTENSION_HANDLER_NAME = "comfyui-modal-sync-timestamped"
def _build_extension_log_formatter() -> logging.Formatter:
"""Return the default formatter for Modal-Sync logs with wall-clock and relative timestamps."""
return logging.Formatter(
fmt="%(asctime)s.%(msecs)03d +%(relativeCreated)07.0fms %(levelname)s [%(name)s] %(message)s",
datefmt="%H:%M:%S",
)
def _configure_extension_logging() -> logging.Logger:
"""Install a dedicated timestamped handler for the Modal-Sync logger hierarchy."""
extension_logger = logging.getLogger(_EXTENSION_LOGGER_NAME)
for existing_handler in extension_logger.handlers:
if getattr(existing_handler, "name", "") == _EXTENSION_HANDLER_NAME:
return extension_logger
handler = logging.StreamHandler()
handler.set_name(_EXTENSION_HANDLER_NAME)
handler.setFormatter(_build_extension_log_formatter())
extension_logger.addHandler(handler)
extension_logger.propagate = False
return extension_logger
_configure_extension_logging()
ensure_modal_sdk_available()
WEB_DIRECTORY = "./web"
try:
from comfy_api.latest import ComfyExtension, io
from .api_intercept import setup_modal_queue_route
from .modal_endpoint_node import ModalEndpointChat
from .modal_llm_node import ModalLLM
from .modal_executor_node import (
ModalArtifactFinalizer,
ModalMapInput,
ModalUniversalExecutor,
)
from .progressive_preview_node import RemoteProgressivePreview
from .remote_configuration_nodes import (
ModalConfiguration,
R2StorageConfiguration,
RemoteExecutionConfigurator,
SshConfiguration,
VastConfiguration,
)
from .vast_config_node import VastAILeaseConfiguration
except ModuleNotFoundError: # pragma: no cover - used during local non-Comfy imports.
ComfyExtension = object # type: ignore[assignment]
io = None # type: ignore[assignment]
ModalMapInput = None # type: ignore[assignment]
ModalArtifactFinalizer = None # type: ignore[assignment]
ModalEndpointChat = None # type: ignore[assignment]
ModalLLM = None # type: ignore[assignment]
ModalUniversalExecutor = None # type: ignore[assignment]
RemoteProgressivePreview = None # type: ignore[assignment]
ModalConfiguration = None # type: ignore[assignment]
R2StorageConfiguration = None # type: ignore[assignment]
RemoteExecutionConfigurator = None # type: ignore[assignment]
SshConfiguration = None # type: ignore[assignment]
VastConfiguration = None # type: ignore[assignment]
VastAILeaseConfiguration = None # type: ignore[assignment]
class ComfyModalSyncExtension: # type: ignore[no-redef]
"""Fallback placeholder used when ComfyUI is not importable."""
async def comfy_entrypoint() -> object: # type: ignore[no-redef]
"""Raise a helpful error when imported outside ComfyUI."""
raise RuntimeError(
"ComfyUI Modal-Sync requires the ComfyUI runtime on sys.path to load."
)
else:
class ComfyModalSyncExtension(ComfyExtension):
"""Expose Modal-Sync nodes to the ComfyUI extension loader."""
async def get_node_list(self) -> list[type[io.ComfyNode]]:
"""Return the node list after registering API routes."""
setup_modal_queue_route()
return [
ModalUniversalExecutor,
ModalMapInput,
ModalArtifactFinalizer,
RemoteProgressivePreview,
ModalEndpointChat,
ModalLLM,
ModalConfiguration,
VastConfiguration,
SshConfiguration,
R2StorageConfiguration,
RemoteExecutionConfigurator,
VastAILeaseConfiguration,
]
async def comfy_entrypoint() -> ComfyExtension:
"""Create the ComfyUI v3 extension entrypoint."""
setup_modal_queue_route()
return ComfyModalSyncExtension()
__all__ = [
"ComfyModalSyncExtension",
"ModalArtifactFinalizer",
"ModalEndpointChat",
"ModalLLM",
"ModalMapInput",
"ModalConfiguration",
"ModalUniversalExecutor",
"RemoteExecutionConfigurator",
"RemoteProgressivePreview",
"R2StorageConfiguration",
"SshConfiguration",
"VastConfiguration",
"VastAILeaseConfiguration",
"WEB_DIRECTORY",
"comfy_entrypoint",
]