Wraps transformers.utils.auto_docstring.get_args_doc_from_source at prestartup to backfill high_res_size / merge_size / min_pixels / max_pixels that the transformers 5.15.x ImageProcessorKwargs TypedDict docstrings fail to document (deepseek_vl_hybrid, kimi_k25, paddleocr_vl). - No site-packages modification; runtime wrapper only. - Auto-skips when upstream transformers fixes the docstrings (subprocess import probe). - Same design as the existing CausalLM ModelOutput docstring patch.
263 lines
9.2 KiB
Python
263 lines
9.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
Patch transformers auto_docstring for ImageProcessorKwargs TypedDict fields.
|
|
|
|
transformers 5.15.x auto_docstring validation parses each *ImageProcessorKwargs
|
|
TypedDict's own __doc__ at import time and prints [ERROR] for every annotation
|
|
key that the parser cannot find in that docstring. Known upstream breakages
|
|
(transformers 5.15.1):
|
|
|
|
- DeepseekVLHybridImageProcessorKwargs: the `high_res_size` docstring entry
|
|
has a stray leading space, so the docstring parser never matches it.
|
|
- Kimi_K25ImageProcessorKwargs: the docstring documents `merge_kernel_size`,
|
|
but the TypedDict annotation is `merge_size`.
|
|
- PaddleOCRVLImageProcessorKwargs: `min_pixels` / `max_pixels` annotations
|
|
are not documented at all.
|
|
|
|
Fix: wrap transformers.utils.auto_docstring.get_args_doc_from_source so the
|
|
returned source dict also carries those four fields. _get_parameter_info()
|
|
falls back to source_args_dict when a TypedDict key is missing from its own
|
|
docstring, which stops the [ERROR] output without touching site-packages.
|
|
|
|
Fully automatic at ComfyUI prestartup (no user env vars):
|
|
- Probe upstream state before installing any wrapper.
|
|
- If the source dict already documents the fields, skip (no wrapper).
|
|
- If a subprocess import probe shows clean stdout, skip (no wrapper).
|
|
- Otherwise install the wrapper (default when upstream is still broken).
|
|
|
|
The wrapper is inert once upstream fixes the docstrings: the fields are only
|
|
consulted for TypedDict annotations that declare them, and only when they are
|
|
missing from that TypedDict's own docstring.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import logging
|
|
import subprocess
|
|
import sys
|
|
from typing import Any, Callable, Dict, Optional
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
_PATCH_TAG = "_qwen_lora_loader_image_processor_kwargs_patch"
|
|
|
|
# Fields missing from the affected TypedDict docstrings, added to the
|
|
# ImageProcessorArgs/VideoProcessorArgs source dict as a fallback.
|
|
_EXTRA_KWARGS: Dict[str, Dict[str, Any]] = {
|
|
"high_res_size": {
|
|
"type": "dict",
|
|
"shape": None,
|
|
"description": """
|
|
Size of the high resolution output image after resizing. Can be overridden by the `high_res_size` parameter in the `preprocess` method.
|
|
""",
|
|
},
|
|
"merge_size": {
|
|
"type": "int",
|
|
"shape": None,
|
|
"description": """
|
|
The merge size of the vision encoder to llm encoder.
|
|
""",
|
|
},
|
|
"min_pixels": {
|
|
"type": "int",
|
|
"shape": None,
|
|
"description": """
|
|
The minimum allowed number of pixels for the resized image. Ensures the total pixel count does not fall below this value after resizing.
|
|
""",
|
|
},
|
|
"max_pixels": {
|
|
"type": "int",
|
|
"shape": None,
|
|
"description": """
|
|
The maximum allowed number of pixels for the resized image. Ensures the total pixel count does not exceed this value after resizing.
|
|
""",
|
|
},
|
|
}
|
|
|
|
_AFFECTED_MODULES = (
|
|
"transformers.models.deepseek_vl_hybrid.image_processing_deepseek_vl_hybrid",
|
|
"transformers.models.deepseek_vl_hybrid.image_processing_pil_deepseek_vl_hybrid",
|
|
"transformers.models.kimi_k25.image_processing_kimi_k25",
|
|
"transformers.models.paddleocr_vl.image_processing_paddleocr_vl",
|
|
"transformers.models.paddleocr_vl.image_processing_pil_paddleocr_vl",
|
|
)
|
|
|
|
_patch_applied: bool = False
|
|
_original_get_args_doc_from_source: Optional[Callable[..., dict]] = None
|
|
|
|
|
|
def _affected_modules_already_imported() -> bool:
|
|
return any(name in sys.modules for name in _AFFECTED_MODULES)
|
|
|
|
|
|
def _upstream_documents_extra_kwargs(auto_docstring_module) -> bool:
|
|
"""Probe upstream native state (never via the patched get_args_doc_from_source)."""
|
|
args_classes = []
|
|
for name in ("ImageProcessorArgs", "VideoProcessorArgs"):
|
|
cls = getattr(auto_docstring_module, name, None)
|
|
if cls is not None:
|
|
args_classes.append(cls)
|
|
if not args_classes:
|
|
return False
|
|
try:
|
|
source = auto_docstring_module.get_args_doc_from_source(args_classes)
|
|
except Exception:
|
|
return False
|
|
return all(field in source for field in _EXTRA_KWARGS)
|
|
|
|
|
|
def _image_processor_args_requested(args_classes: Any, image_processor_args_type: type) -> bool:
|
|
if args_classes is image_processor_args_type:
|
|
return True
|
|
if isinstance(args_classes, (list, tuple)):
|
|
return image_processor_args_type in args_classes
|
|
return False
|
|
|
|
|
|
def _make_patched_get_args_doc_from_source(
|
|
auto_docstring_module,
|
|
original: Callable[..., dict],
|
|
) -> Callable[..., dict]:
|
|
image_processor_args_type = auto_docstring_module.ImageProcessorArgs
|
|
|
|
def patched_get_args_doc_from_source(args_classes: Any) -> dict:
|
|
result = original(args_classes)
|
|
|
|
if not _image_processor_args_requested(args_classes, image_processor_args_type):
|
|
return result
|
|
|
|
if all(field in result for field in _EXTRA_KWARGS):
|
|
return result
|
|
|
|
merged = dict(result)
|
|
for field, entry in _EXTRA_KWARGS.items():
|
|
merged.setdefault(field, dict(entry))
|
|
return merged
|
|
|
|
setattr(patched_get_args_doc_from_source, _PATCH_TAG, True)
|
|
return patched_get_args_doc_from_source
|
|
|
|
|
|
def _import_probe_reports_clean() -> Optional[bool]:
|
|
"""
|
|
True: affected transformers modules import with no [ERROR] ... not documented lines.
|
|
False: errors still present (patch may be needed).
|
|
None: probe could not run.
|
|
"""
|
|
python_exe = sys.executable
|
|
module_list = ", ".join(repr(m) for m in _AFFECTED_MODULES)
|
|
code = (
|
|
"import importlib, io, contextlib\n"
|
|
"buf = io.StringIO()\n"
|
|
"with contextlib.redirect_stdout(buf):\n"
|
|
f" for m in ({module_list}):\n"
|
|
" importlib.import_module(m)\n"
|
|
"lines = buf.getvalue().splitlines()\n"
|
|
"errs = [l for l in lines if '[ERROR]' in l and 'but not documented' in l]\n"
|
|
"print('CLEAN' if not errs else 'ERRORS')\n"
|
|
)
|
|
try:
|
|
proc = subprocess.run(
|
|
[python_exe, "-c", code],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=180,
|
|
)
|
|
except (OSError, subprocess.SubprocessError) as exc:
|
|
logger.debug("ImageProcessorKwargs docstring import probe failed: %s", exc)
|
|
return None
|
|
|
|
if proc.returncode != 0:
|
|
logger.debug(
|
|
"ImageProcessorKwargs docstring import probe exit %s stderr=%s",
|
|
proc.returncode,
|
|
proc.stderr[:500] if proc.stderr else "",
|
|
)
|
|
return None
|
|
|
|
last_line = (proc.stdout or "").strip().splitlines()
|
|
if not last_line:
|
|
return None
|
|
status = last_line[-1].strip()
|
|
if status == "CLEAN":
|
|
return True
|
|
if status == "ERRORS":
|
|
return False
|
|
return None
|
|
|
|
|
|
def apply_transformers_image_processor_kwargs_patch() -> bool:
|
|
"""
|
|
Install get_args_doc_from_source wrapper unless upstream already fixed the issue.
|
|
|
|
Returns True if the wrapper is active (or was already applied), False if skipped.
|
|
"""
|
|
global _patch_applied, _original_get_args_doc_from_source
|
|
|
|
if _patch_applied:
|
|
return True
|
|
|
|
if _affected_modules_already_imported():
|
|
logger.warning(
|
|
"ImageProcessorKwargs docstring patch skipped: affected transformers modules "
|
|
"already imported before prestartup; restart ComfyUI"
|
|
)
|
|
return False
|
|
|
|
try:
|
|
auto_docstring_module = importlib.import_module("transformers.utils.auto_docstring")
|
|
except ImportError:
|
|
logger.debug("transformers.utils.auto_docstring not available; patch skipped")
|
|
return False
|
|
|
|
get_args = getattr(auto_docstring_module, "get_args_doc_from_source", None)
|
|
if get_args is None:
|
|
return False
|
|
|
|
if getattr(get_args, _PATCH_TAG, False):
|
|
_patch_applied = True
|
|
return True
|
|
|
|
if _upstream_documents_extra_kwargs(auto_docstring_module):
|
|
logger.info(
|
|
"ImageProcessorKwargs docstring patch skipped: transformers source dict "
|
|
"already documents the fields (upstream fixed; patch not installed)"
|
|
)
|
|
return False
|
|
|
|
import_probe = _import_probe_reports_clean()
|
|
if import_probe is True:
|
|
logger.info(
|
|
"ImageProcessorKwargs docstring patch skipped: affected transformers modules "
|
|
"import without docstring errors (upstream fixed; patch not installed)"
|
|
)
|
|
return False
|
|
|
|
_original_get_args_doc_from_source = get_args
|
|
auto_docstring_module.get_args_doc_from_source = _make_patched_get_args_doc_from_source(
|
|
auto_docstring_module,
|
|
_original_get_args_doc_from_source,
|
|
)
|
|
_patch_applied = True
|
|
|
|
logger.info(
|
|
"Patched transformers.utils.auto_docstring.get_args_doc_from_source for "
|
|
"ImageProcessorKwargs fields (high_res_size/merge_size/min_pixels/max_pixels); "
|
|
"removes when upstream fixes the docstrings"
|
|
)
|
|
return True
|
|
|
|
|
|
def is_patch_applied() -> bool:
|
|
return _patch_applied
|
|
|
|
|
|
def is_patch_wrapped() -> bool:
|
|
try:
|
|
auto_docstring_module = importlib.import_module("transformers.utils.auto_docstring")
|
|
except ImportError:
|
|
return False
|
|
get_args = getattr(auto_docstring_module, "get_args_doc_from_source", None)
|
|
return get_args is not None and getattr(get_args, _PATCH_TAG, False)
|