Author SHA1 Message Date
Faych 90e4b8f733 Merge branch 'modelscope-api-refactor-8866516818649816546' into copilot/sub-pr-5 2026-01-05 02:19:55 +00:00
copilot-swe-agent[bot] 5b383e9021 Merge remote-tracking branch 'origin/master' into copilot/sub-pr-5 2026-01-05 01:55:50 +00:00
copilot-swe-agent[bot]andneverbiasu c6668b5261 Simplify loras parsing logic to avoid duplication
Co-authored-by: neverbiasu <90372299+neverbiasu@users.noreply.github.com>
2026-01-04 18:34:40 +00:00
google-labs-jules[bot] 71fe954494 Refactor ModelScope Image Generator API
Updates `ModelScopeImageGenerator` to use the asynchronous `v1/images/generations` endpoint.
- Implements task polling logic with timestamp-based timeout.
- Adds fail-fast logic for 4xx client errors during polling.
- Updates input parameters to match API: `steps`, `guidance`, `seed`, `loras`.
- Supports `loras` as JSON dict or string ID.
- Uses `logging` instead of `print`.
- Ensures UTF-8 charset in headers for multi-language support.
2026-01-04 18:34:21 +00:00
copilot-swe-agent[bot]andneverbiasu 8b9c55d2b6 Define logger at module level and fix formatting
Co-authored-by: neverbiasu <90372299+neverbiasu@users.noreply.github.com>
2026-01-04 18:33:47 +00:00
copilot-swe-agent[bot]andneverbiasu 9a5c9260d1 Move logging import to top and improve headers copy
Co-authored-by: neverbiasu <90372299+neverbiasu@users.noreply.github.com>
2026-01-04 18:32:40 +00:00
copilot-swe-agent[bot]andneverbiasu 66bbc5f810 Add .gitignore and remove __pycache__
Co-authored-by: neverbiasu <90372299+neverbiasu@users.noreply.github.com>
2026-01-04 18:31:46 +00:00
copilot-swe-agent[bot]andneverbiasu b68ff96af6 Address all review comments from PR review thread
Co-authored-by: neverbiasu <90372299+neverbiasu@users.noreply.github.com>
2026-01-04 18:31:17 +00:00
copilot-swe-agent[bot] 7c8afeea95 Initial plan 2026-01-04 18:28:20 +00:00
google-labs-jules[bot] ae8333be4a Refactor ModelScope Image Generator to use new Async API.
Updates `ModelScopeImageGenerator` in `nodes.py` to:
- Use `v1/images/generations` with `X-ModelScope-Async-Mode: true`.
- Implement polling logic for task completion.
- Update input parameters to match API documentation (steps, guidance, seed, loras, size).
- Support `loras` as string or JSON dict.
2026-01-04 02:40:09 +00:00
2 changed files with 52 additions and 71 deletions
-25
View File
@@ -1,25 +0,0 @@
name: Ruff
on:
push:
branches: ["master"]
pull_request:
jobs:
ruff:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install Ruff
run: pip install ruff
- name: Run Ruff check
run: ruff check .
+52 -46
View File
@@ -9,13 +9,9 @@ import logging
BASE_URL = "https://api-inference.modelscope.cn/v1"
MODELSCOPE_API_KEY = os.getenv("MODELSCOPE_API_KEY") or os.getenv(
"MODELSCOPE_ACCESS_TOKEN"
)
MODELSCOPE_API_KEY = os.getenv("MODELSCOPE_API_KEY") or os.getenv("MODELSCOPE_ACCESS_TOKEN")
DEFAULT_SYSTEM_PROMPT = (
"You are a helpful and harmless assistant. Answer concisely and helpfully."
)
DEFAULT_SYSTEM_PROMPT = "You are a helpful and harmless assistant. Answer concisely and helpfully."
# Constants
PLACEHOLDER_MODEL_ID = "Model ID"
@@ -29,7 +25,7 @@ logger = logging.getLogger(__name__)
class ModelScopeBase:
"""Base class for ModelScope nodes with common functionality."""
def _resolve_key(self, api_key: Optional[str]) -> str:
"""Resolve API key from input or environment variables."""
key = api_key or MODELSCOPE_API_KEY
@@ -41,35 +37,41 @@ class ModelScopeBase:
def _sanitize_text(self, text: str) -> str:
"""Sanitize text for ComfyUI showText node compatibility.
Args:
text: Raw text string
Returns:
str: Sanitized text safe for display
"""
if not isinstance(text, str):
text = str(text)
# Remove or replace problematic characters
import re
text = re.sub(r"[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]", "", text)
text = re.sub(r"\s+", " ", text).strip()
# Remove null bytes and other control characters except common ones
text = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]', '', text)
# Normalize whitespace
text = re.sub(r'\s+', ' ', text).strip()
# Ensure text is not empty
if not text:
text = "[Empty Response]"
# Limit length if extremely long (optional safeguard)
max_length = 10000
if len(text) > max_length:
text = text[:max_length] + "...[truncated]"
# Ensure proper encoding
try:
text.encode("utf-8").decode("utf-8")
text.encode('utf-8').decode('utf-8')
except UnicodeError:
# Fallback: replace problematic characters
text = text.encode("utf-8", errors="replace").decode("utf-8")
text = text.encode('utf-8', errors='replace').decode('utf-8')
return text
def _get_pil_image_module(self):
@@ -81,21 +83,22 @@ class ModelScopeBase:
def _download_image_from_url(self, url: str, headers: Optional[dict] = None) -> object:
"""Download image from URL and convert to ComfyUI IMAGE format.
Args:
url: Image URL to download
headers: HTTP headers for authentication (optional)
Returns:
torch.Tensor: Image tensor in ComfyUI format [B, H, W, C]
Raises:
RuntimeError: If download or conversion fails
"""
try:
import torch
import numpy as np
# Download image
response = requests.get(url, headers=headers, timeout=30)
response.raise_for_status()
@@ -112,18 +115,19 @@ class ModelScopeBase:
# Convert PIL to numpy array
image_np = np.array(image).astype(np.float32) / 255.0
# Convert to torch tensor with ComfyUI format [B, H, W, C]
image_tensor = torch.from_numpy(image_np)[None,]
return image_tensor
except Exception as e:
raise RuntimeError(f"Failed to download image: {e}") from e
class ModelScopeChatBase(ModelScopeBase):
"""Base class for chat-based ModelScope nodes."""
def _extract_text(self, data: dict) -> str:
"""Extract text content from chat completion response.
Tries to read choices[0].message.content (string or list) and falls back to str(data).
@@ -218,21 +222,22 @@ class ModelScopeLLM(ModelScopeChatBase):
request_timeout: int = 30,
) -> Tuple[str]:
"""Execute text generation using ModelScope LLM.
Args:
model_id: ModelScope LLM model identifier
prompt: User text prompt for the model
system_prompt: Optional system prompt for assistant behavior
api_key: Optional API key override
request_timeout: Request timeout in seconds
Returns:
tuple: Single-element tuple with LLM response text
Raises:
ValueError: If inputs are invalid
RuntimeError: If API call or processing fails
"""
# Validate inputs
if not prompt.strip():
raise ValueError("Prompt cannot be empty")
@@ -298,13 +303,13 @@ class ModelScopeLLM(ModelScopeChatBase):
elapsed = time.time() - start_time
print(f"LLM inference completed in {elapsed:.1f}s")
return (text,)
class ModelScopeVLM(ModelScopeChatBase):
"""ModelScope Visual Language Model node for image-text conversations.
Supports analyzing images with text prompts using ModelScope VLM models.
Handles various image input formats and provides robust error handling.
"""
@@ -312,7 +317,7 @@ class ModelScopeVLM(ModelScopeChatBase):
@classmethod
def INPUT_TYPES(cls):
"""Define input types and validation rules for VLM processing.
Returns:
dict: Input type definitions with validation parameters
"""
@@ -388,7 +393,7 @@ class ModelScopeVLM(ModelScopeChatBase):
request_timeout: int = 120,
) -> Tuple[str]:
"""Execute vision-language model inference on image and text.
Args:
model_id: ModelScope VLM model identifier
image_url: URL of image to analyze
@@ -396,25 +401,27 @@ class ModelScopeVLM(ModelScopeChatBase):
system_prompt: Optional system prompt for assistant behavior
api_key: Optional API key override
request_timeout: Request timeout in seconds
Returns:
tuple: Single-element tuple with VLM response text
Raises:
ValueError: If inputs are invalid
RuntimeError: If API call or processing fails
"""
# Validate inputs
if not image_url.strip():
raise ValueError("Image URL cannot be empty")
if not prompt.strip():
raise ValueError("Prompt cannot be empty")
if not (image_url.startswith("http://") or image_url.startswith("https://")):
# Validate image URL format
if not (image_url.startswith('http://') or image_url.startswith('https://')):
raise ValueError("Image URL must be a valid HTTP/HTTPS URL")
key = self._resolve_key(api_key)
system_prompt = system_prompt or DEFAULT_SYSTEM_PROMPT
url = f"{BASE_URL}/chat/completions"
headers = {
"Authorization": f"Bearer {key}",
@@ -456,6 +463,7 @@ class ModelScopeVLM(ModelScopeChatBase):
start_time = time.time()
try:
# Use longer timeout for VLM inference
resp = requests.post(
url,
headers=headers,
@@ -486,7 +494,7 @@ class ModelScopeVLM(ModelScopeChatBase):
elapsed = time.time() - start_time
print(f"VLM inference completed in {elapsed:.1f}s")
return (text,)
@@ -587,7 +595,7 @@ class ModelScopeImageGenerator(ModelScopeBase):
"tooltip": "ModelScope API key. If empty, reads from MODELSCOPE_API_KEY env var.",
},
),
},
}
}
RETURN_TYPES = ("IMAGE",)
@@ -611,7 +619,7 @@ class ModelScopeImageGenerator(ModelScopeBase):
loras: Optional[str] = None,
) -> Tuple[object]:
"""Generate image using ModelScope API.
Args:
model_id: ModelScope model identifier
prompt: Text description of desired image
@@ -626,7 +634,7 @@ class ModelScopeImageGenerator(ModelScopeBase):
Returns:
tuple: Single-element tuple containing image tensor
Raises:
RuntimeError: If API call or image processing fails
"""
@@ -679,9 +687,7 @@ class ModelScopeImageGenerator(ModelScopeBase):
resp = requests.post(url, headers=headers_with_charset, data=data_bytes, timeout=60)
resp.raise_for_status()
except requests.RequestException as e:
raise RuntimeError(
f"Network error calling ModelScope Image API: {e}"
) from e
raise RuntimeError(f"Network error calling ModelScope Image API: {e}") from e
try:
data = resp.json()