docs: add complete explanation of ImageProcessorKwargs docstring patch
English technical guide covering symptom, root cause, files changed, full code, and mechanism for the transformers ImageProcessorKwargs 'not documented' startup noise fix (commit 8d82442).
This commit is contained in:
@@ -0,0 +1,772 @@
|
||||
# Transformers ImageProcessorKwargs "not documented" Docstring Patch
|
||||
### deepseek_vl_hybrid / kimi_k25 / paddleocr_vl
|
||||
|
||||
This document is the complete explanation of the fix that removes the
|
||||
`[ERROR]` ... `but not documented` lines printed by Hugging Face `transformers`
|
||||
at ComfyUI startup. The fix lives **entirely inside the
|
||||
ComfyUI-QwenImageLoraLoader custom node** — no `site-packages` files are
|
||||
touched, no output is filtered, and the patch **removes itself automatically**
|
||||
once upstream `transformers` fixes the underlying defect.
|
||||
|
||||
**Environment when documented (2026-08-27):**
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| transformers | 5.15.1 |
|
||||
| ComfyUI-QwenImageLoraLoader | v2.5.8 (commit `8d82442`) |
|
||||
| Affected TypedDicts | `DeepseekVLHybridImageProcessorKwargs`, `Kimi_K25ImageProcessorKwargs`, `PaddleOCRVLImageProcessorKwargs` |
|
||||
| Missing fields | `high_res_size`, `merge_size`, `min_pixels`, `max_pixels` |
|
||||
|
||||
---
|
||||
|
||||
## Design constraints
|
||||
|
||||
1. **Do not edit `transformers` in `site-packages`.** The workaround only
|
||||
monkey-patches `get_args_doc_from_source` in-process from this custom node.
|
||||
2. **Apply only from `prestartup_script.py`.** The patch must be installed
|
||||
**before** any custom node imports the affected transformers modules
|
||||
(`main.execute_prestartup_script()` runs before custom node loading).
|
||||
3. **Fully automatic upstream auto-disable.** Probe upstream on every ComfyUI
|
||||
start; install the wrapper only while upstream still emits the `[ERROR]`
|
||||
lines. **No user env vars or toggles.**
|
||||
|
||||
### Decision table
|
||||
|
||||
| Condition | Action | Log level |
|
||||
|-----------|--------|-----------|
|
||||
| Tagged wrapper already on `get_args_doc_from_source` | **Return True** (idempotent) | — |
|
||||
| Affected transformers modules already in `sys.modules` | **Skip** — restart required | WARNING |
|
||||
| Source dict (ImageProcessorArgs/VideoProcessorArgs) already documents all 4 fields | **Skip** — upstream schema fixed | INFO |
|
||||
| Subprocess import probe prints `CLEAN` (no `[ERROR] ... not documented`) | **Skip** — upstream behavior fixed | INFO |
|
||||
| Subprocess probe prints `ERRORS`, or probe cannot run (`None`) | **Apply** wrapper if prior rows did not skip | INFO |
|
||||
| `transformers.utils.auto_docstring` missing / no `get_args_doc_from_source` | **Skip** | DEBUG |
|
||||
|
||||
### Decision flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[apply_transformers_image_processor_kwargs_patch] --> B{Already applied?}
|
||||
B -->|yes| Y[Return True]
|
||||
B -->|no| C{Affected modules in sys.modules?}
|
||||
C -->|yes| W[WARN skip - restart ComfyUI]
|
||||
C -->|no| D{Source dict already has all 4 fields?}
|
||||
D -->|yes| Z[Skip - upstream fixed]
|
||||
D -->|no| E[Subprocess import probe of 5 modules]
|
||||
E -->|CLEAN| Z
|
||||
E -->|ERRORS or probe failed| F[Wrap get_args_doc_from_source]
|
||||
F --> X[Return True]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. What was the problem
|
||||
|
||||
### 1.1 Symptom
|
||||
|
||||
At ComfyUI startup, after the custom-node initialization banners, `transformers`
|
||||
printed **13 `[ERROR]` lines** into the console, for example:
|
||||
|
||||
```text
|
||||
[ERROR] `high_res_size` is part of DeepseekVLHybridImageProcessorKwargs, but not documented. Make sure to add it to the docstring of the function in D:\USERFILES\ComfyUI\python_embeded\Lib\site-packages\transformers\models\deepseek_vl_hybrid\image_processing_deepseek_vl_hybrid.py.
|
||||
[ERROR] `merge_size` is part of Kimi_K25ImageProcessorKwargs, but not documented. Make sure to add it to the docstring of the function in D:\USERFILES\ComfyUI\python_embeded\Lib\site-packages\transformers\models\kimi_k25\image_processing_kimi_k25.py.
|
||||
[ERROR] `min_pixels` is part of PaddleOCRVLImageProcessorKwargs, but not documented. Make sure to add it to the docstring of the function in ...\image_processing_paddleocr_vl.py.
|
||||
[ERROR] `max_pixels` is part of PaddleOCRVLImageProcessorKwargs, but not documented. Make sure to add it to the docstring of the function in ...\image_processing_paddleocr_vl.py.
|
||||
```
|
||||
|
||||
The complete breakdown (13 lines in the observed startup log):
|
||||
|
||||
| Source file (in `transformers\models\...`) | Missing field(s) | Lines printed |
|
||||
|--------------------------------------------|------------------|---------------|
|
||||
| `deepseek_vl_hybrid\image_processing_deepseek_vl_hybrid.py` | `high_res_size` | 1 |
|
||||
| `deepseek_vl_hybrid\image_processing_pil_deepseek_vl_hybrid.py` | `high_res_size` | 2 |
|
||||
| `kimi_k25\image_processing_kimi_k25.py` | `merge_size` | 2 |
|
||||
| `paddleocr_vl\image_processing_paddleocr_vl.py` | `min_pixels`, `max_pixels` | 4 |
|
||||
| `paddleocr_vl\image_processing_pil_paddleocr_vl.py` | `min_pixels`, `max_pixels` | 4 |
|
||||
|
||||
### 1.2 Impact
|
||||
|
||||
- **No functional breakage.** These lines are validation noise produced while
|
||||
`transformers` auto-generates documentation strings at import time.
|
||||
ComfyUI, the image processors, and the models all work normally.
|
||||
- **Log pollution / alarm.** The lines look like real errors, make the startup
|
||||
log noisy, and make genuine errors harder to spot.
|
||||
- Because the messages originate **inside the installed `transformers`
|
||||
package**, a naive "fix" would edit files under `site-packages` — which is
|
||||
unacceptable (see §2.4): the change would be lost on every `pip` upgrade and
|
||||
would diverge from the upstream package.
|
||||
|
||||
---
|
||||
|
||||
## 2. Root cause
|
||||
|
||||
### 2.1 The validation machinery: `transformers.utils.auto_docstring`
|
||||
|
||||
In `transformers` 5.x, image-processor classes are decorated with
|
||||
`@auto_docstring`. At **module import time** the decorator inspects the class
|
||||
and its `__call__` signature and rebuilds the docstring. Part of that pipeline
|
||||
is `_process_kwargs_parameters()`, which handles `**kwargs` parameters typed
|
||||
with a TypedDict such as `DeepseekVLHybridImageProcessorKwargs`.
|
||||
|
||||
The relevant logic (from `transformers\utils\auto_docstring.py`,
|
||||
transformers 5.15.1):
|
||||
|
||||
```python
|
||||
# _process_kwargs_parameters()
|
||||
kwargs_documentation = kwarg_param.annotation.__args__[0].__doc__ # the TypedDict's OWN docstring
|
||||
if kwargs_documentation is not None:
|
||||
documented_kwargs = parse_docstring(kwargs_documentation)[0] # parse it
|
||||
|
||||
for param_name, param_type_annotation in kwarg_param.annotation.__args__[0].__annotations__.items():
|
||||
...
|
||||
param_type, optional_string, shape_string, additional_info, description, is_documented = (
|
||||
_get_parameter_info(param_name, documented_kwargs, source_args_dict, param_type, optional)
|
||||
)
|
||||
...
|
||||
else: # not documented
|
||||
undocumented_parameters.append(
|
||||
f"[ERROR] `{param_name}` is part of {kwarg_param.annotation.__args__[0].__qualname__}, "
|
||||
f"but not documented. Make sure to add it to the docstring of the function in {func.__code__.co_filename}."
|
||||
)
|
||||
```
|
||||
|
||||
and the collected messages are emitted with a plain `print()`:
|
||||
|
||||
```python
|
||||
# _process_parameters_section()
|
||||
if len(undocumented_parameters) > 0:
|
||||
print("\n".join(undocumented_parameters))
|
||||
```
|
||||
|
||||
Key detail: the error is **printed via `print()`**, not through the
|
||||
`transformers` logging system — so changing `TRANSFORMERS_VERBOSITY` or log
|
||||
levels cannot silence it.
|
||||
|
||||
`_get_parameter_info()` resolves each annotation key against three sources, in
|
||||
priority order:
|
||||
|
||||
```python
|
||||
if param_name in documented_params: # 1. the function's own docstring
|
||||
...
|
||||
elif param_name in source_args_dict: # 2. the generic args source dict (fallback)
|
||||
...
|
||||
else: # 3. nothing -> is_documented = False -> [ERROR]
|
||||
is_documented = False
|
||||
```
|
||||
|
||||
The **fallback** `source_args_dict` is produced by:
|
||||
|
||||
```python
|
||||
def get_args_doc_from_source(args_classes: object | list[object]) -> dict:
|
||||
if isinstance(args_classes, list | tuple):
|
||||
return _merge_args_dicts(tuple(args_classes)) # merges cls.__dict__ of each class
|
||||
return args_classes.__dict__
|
||||
```
|
||||
|
||||
For image processors it is called as
|
||||
`get_args_doc_from_source([ImageProcessorArgs, VideoProcessorArgs])`.
|
||||
|
||||
The docstring parser that decides whether a key is "documented" uses this
|
||||
pre-compiled regex:
|
||||
|
||||
```python
|
||||
_re_param = re.compile(
|
||||
r"^\s{0,0}(\w+)\s*\(\s*([^, \)]*)(\s*.*?)\s*\)\s*:\s*((?:(?!\n^\s{0,0}\w+\s*\().)*)",
|
||||
re.DOTALL | re.MULTILINE,
|
||||
)
|
||||
```
|
||||
|
||||
`^\s{0,0}` means: **the parameter name must start exactly at column 0** of the
|
||||
line (after the common indentation has been stripped by `set_min_indent`).
|
||||
Any leading whitespace left on the line makes the entry invisible to the
|
||||
parser.
|
||||
|
||||
### 2.2 Three distinct upstream defects (all in `transformers` 5.15.1)
|
||||
|
||||
**Defect A — `DeepseekVLHybridImageProcessorKwargs` (both backend files):
|
||||
a stray space hides `high_res_size`.**
|
||||
|
||||
The TypedDict's docstring *does* document `high_res_size`, but the line was
|
||||
written with **five** leading spaces instead of four:
|
||||
|
||||
```python
|
||||
class DeepseekVLHybridImageProcessorKwargs(ImagesKwargs, total=False):
|
||||
r"""
|
||||
min_size (`int`, *optional*, defaults to 14):
|
||||
The minimum allowed size for the resized image. ...
|
||||
high_res_size (`dict`, *optional*, defaults to `{"height": 1024, "width": 1024}`): # <-- 5 spaces!
|
||||
Size of the high resolution output image after resizing. ...
|
||||
```
|
||||
|
||||
`set_min_indent()` strips the smallest common indent (4 spaces), leaving
|
||||
`high_res_size` at 1 space — which `^\s{0,0}` cannot match. The field is
|
||||
therefore treated as *undocumented even though it is documented*.
|
||||
|
||||
**Defect B — `Kimi_K25ImageProcessorKwargs`: name mismatch.**
|
||||
|
||||
The TypedDict annotation declares `merge_size` (default `2`), but the
|
||||
docstring documents a non-existent parameter `merge_kernel_size`:
|
||||
|
||||
```python
|
||||
class Kimi_K25ImageProcessorKwargs(ImagesKwargs, total=False):
|
||||
r"""
|
||||
max_patches (`int`, *optional*, defaults to `16384`): ...
|
||||
patch_size (`int`, *optional*, defaults to 14): ...
|
||||
merge_kernel_size (`int`, *optional*, defaults to 2): # <-- annotation says "merge_size"
|
||||
"""
|
||||
```
|
||||
|
||||
The processor code itself uses `merge_size` (`merge_size = images_kwargs.get("merge_size", self.merge_size)`),
|
||||
so the docstring simply has the wrong name.
|
||||
|
||||
**Defect C — `PaddleOCRVLImageProcessorKwargs` (both backend files):
|
||||
`min_pixels` / `max_pixels` are never documented.**
|
||||
|
||||
The TypedDict declares five fields but its docstring documents only three:
|
||||
|
||||
```python
|
||||
class PaddleOCRVLImageProcessorKwargs(ImagesKwargs, total=False):
|
||||
r"""
|
||||
patch_size (`int`, *optional*, defaults to 14): ...
|
||||
temporal_patch_size (`int`, *optional*, defaults to 1): ...
|
||||
merge_size (`int`, *optional*, defaults to 2): ...
|
||||
""" # <-- min_pixels / max_pixels annotations exist but are missing here
|
||||
```
|
||||
|
||||
### 2.3 Why the error text mentions "the function" but the fix is in the TypedDict
|
||||
|
||||
The message string uses `func.__code__.co_filename` (the processor module
|
||||
file) for the location hint, but the **text that is actually parsed** is the
|
||||
TypedDict class's own `__doc__`
|
||||
(`kwarg_param.annotation.__args__[0].__doc__`). That is why the message is
|
||||
misleading: editing the *function's* docstring would not help at all — the
|
||||
TypedDict's docstring (or the fallback source dict) is what must change.
|
||||
|
||||
### 2.4 Why direct `site-packages` edits were rejected
|
||||
|
||||
The first attempt fixed the five files under
|
||||
`...\python_embeded\Lib\site-packages\transformers\models\...` directly
|
||||
(fix the indent, rename the kimi field, add the paddleocr entries). It worked,
|
||||
but it was **not acceptable** because:
|
||||
|
||||
- `pip` upgrades / reinstalls of `transformers` silently revert the edits;
|
||||
- every ComfyUI environment would need the same manual surgery;
|
||||
- it forks the local package away from upstream, hiding the upstream bug.
|
||||
|
||||
Therefore the direct edits were **fully reverted** (verified: the exact 13
|
||||
`[ERROR]` lines reproduced again), and the fix was re-implemented as a
|
||||
repo-native, in-process runtime patch described in §3–§5.
|
||||
|
||||
---
|
||||
|
||||
## 3. Files added / modified
|
||||
|
||||
| File | Change | Kind |
|
||||
|------|--------|------|
|
||||
| `patches/transformers_image_processor_kwargs_patch.py` | **Added** — new patch module implementing the wrapper + upstream probes | new file |
|
||||
| `prestartup_script.py` | **Modified** — one new path constant + one load/apply block for the new patch | edited |
|
||||
|
||||
Committed and pushed as:
|
||||
|
||||
```text
|
||||
8d82442 fix(patches): suppress transformers ImageProcessorKwargs 'not documented' startup noise
|
||||
```
|
||||
|
||||
> Note: `patches/nunchaku_patch.py` shows a local modification (one blank
|
||||
> line) that is unrelated to this fix and was intentionally **not** included
|
||||
> in the commit.
|
||||
|
||||
---
|
||||
|
||||
## 4. Full code
|
||||
|
||||
### 4.1 New file: `patches/transformers_image_processor_kwargs_patch.py`
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
### 4.2 Modified file: `prestartup_script.py` (current full content)
|
||||
|
||||
The **two additions** are marked with `[ADDED]`; everything else is
|
||||
pre-existing code.
|
||||
|
||||
```python
|
||||
"""
|
||||
Inject apply_rotary_emb on comfy.ldm.qwen_image.model before any custom node __init__.
|
||||
|
||||
ComfyUI-nunchaku loads before ComfyUI-QwenImageLoraLoader (Windows listdir order), so
|
||||
__init__.py alone is too late. prestartup_script.py runs from main.execute_prestartup_script().
|
||||
"""
|
||||
import importlib.util
|
||||
import logging
|
||||
import os
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_PATCH_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "patches")
|
||||
_NUNCHAKU_PATCH_PATH = os.path.join(_PATCH_DIR, "nunchaku_patch.py")
|
||||
_DOCSTRING_PATCH_PATH = os.path.join(_PATCH_DIR, "transformers_qwen_vl_docstring_patch.py")
|
||||
_IP_KWARGS_PATCH_PATH = os.path.join(_PATCH_DIR, "transformers_image_processor_kwargs_patch.py") # [ADDED]
|
||||
|
||||
|
||||
def _load_patch_module(module_name: str, path: str):
|
||||
spec = importlib.util.spec_from_file_location(module_name, path)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Failed to load patch module spec from {path}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
# Load the nunchaku patch module once; reuse it for warning suppression and the
|
||||
# apply_rotary_emb compat shim.
|
||||
_patch_module = None
|
||||
try:
|
||||
_patch_module = _load_patch_module(
|
||||
"comfyui_qwenimageloraloader_nunchaku_patch_prestartup",
|
||||
_NUNCHAKU_PATCH_PATH,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("ComfyUI-QwenImageLoraLoader prestartup: failed to load nunchaku patch module")
|
||||
|
||||
# Install the cosmetic 'Torch already imported' warning filter first, before any
|
||||
# prestartup step imports comfy.ldm (which imports torch) and before main.py logs it.
|
||||
if _patch_module is not None:
|
||||
try:
|
||||
if _patch_module.suppress_torch_preimport_warning():
|
||||
logger.debug(
|
||||
"ComfyUI-QwenImageLoraLoader prestartup: torch pre-import warning suppressed"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"ComfyUI-QwenImageLoraLoader prestartup: torch warning suppression failed"
|
||||
)
|
||||
|
||||
try:
|
||||
_docstring_patch_module = _load_patch_module(
|
||||
"comfyui_qwenimageloraloader_docstring_patch_prestartup",
|
||||
_DOCSTRING_PATCH_PATH,
|
||||
)
|
||||
if _docstring_patch_module.apply_transformers_causal_lm_docstring_patch():
|
||||
logger.info("ComfyUI-QwenImageLoraLoader prestartup: CausalLM ModelOutput docstring patch applied")
|
||||
else:
|
||||
logger.debug(
|
||||
"ComfyUI-QwenImageLoraLoader prestartup: CausalLM ModelOutput docstring patch not applied"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("ComfyUI-QwenImageLoraLoader prestartup: CausalLM ModelOutput docstring patch failed")
|
||||
|
||||
# ImageProcessorKwargs docstring patch (deepseek_vl_hybrid / kimi_k25 / paddleocr_vl # [ADDED] block start
|
||||
# [ERROR] "but not documented" noise): wraps get_args_doc_from_source at prestartup,
|
||||
# before any custom node imports those transformers modules.
|
||||
try:
|
||||
_ip_kwargs_patch_module = _load_patch_module(
|
||||
"comfyui_qwenimageloraloader_image_processor_kwargs_patch_prestartup",
|
||||
_IP_KWARGS_PATCH_PATH,
|
||||
)
|
||||
if _ip_kwargs_patch_module.apply_transformers_image_processor_kwargs_patch():
|
||||
logger.info(
|
||||
"ComfyUI-QwenImageLoraLoader prestartup: ImageProcessorKwargs docstring patch applied"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
"ComfyUI-QwenImageLoraLoader prestartup: ImageProcessorKwargs docstring patch not applied"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception(
|
||||
"ComfyUI-QwenImageLoraLoader prestartup: ImageProcessorKwargs docstring patch failed"
|
||||
)
|
||||
# [ADDED] block end
|
||||
|
||||
if _patch_module is not None:
|
||||
try:
|
||||
if _patch_module.apply_qwen_image_apply_rotary_emb_compat():
|
||||
logger.info("ComfyUI-QwenImageLoraLoader prestartup: apply_rotary_emb compat applied")
|
||||
else:
|
||||
logger.debug(
|
||||
"ComfyUI-QwenImageLoraLoader prestartup: apply_rotary_emb compat not needed or already present"
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("ComfyUI-QwenImageLoraLoader prestartup: apply_rotary_emb compat failed")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. What the fix means
|
||||
|
||||
### 5.1 Mechanism: closing the fallback gap
|
||||
|
||||
The validation logic in `_get_parameter_info()` consults, in order:
|
||||
|
||||
1. the TypedDict's own docstring (`documented_kwargs`) — where the fields are
|
||||
wrongly absent (the three upstream defects), then
|
||||
2. the generic source dict returned by
|
||||
`get_args_doc_from_source([ImageProcessorArgs, VideoProcessorArgs])` —
|
||||
which we now augment, then
|
||||
3. "undocumented" — which previously produced the `[ERROR]` lines.
|
||||
|
||||
The patch wraps step 2's function. Whenever `ImageProcessorArgs` is among the
|
||||
requested argument classes, the wrapper adds the four missing fields to the
|
||||
returned dict (`merged.setdefault(...)`, so it never overwrites real upstream
|
||||
entries). Now the second branch of `_get_parameter_info()` resolves
|
||||
`high_res_size`, `merge_size`, `min_pixels`, and `max_pixels`, `is_documented`
|
||||
becomes `True`, and no `[ERROR]` is appended.
|
||||
|
||||
Why this is safe and precise:
|
||||
|
||||
- The extra keys are only **consulted** by TypedDicts whose annotations
|
||||
actually declare them (the loop iterates
|
||||
`kwarg_param.annotation.__args__[0].__annotations__`). No other model's
|
||||
documentation changes.
|
||||
- The wrapper returns the original dict untouched for every non-image-processor
|
||||
call (e.g. `ModelArgs`-only calls) and delegates all actual work to the
|
||||
original function.
|
||||
- Only the *docstring generation input* is influenced. No runtime inference
|
||||
path, tensor shape, or model behavior is touched — the generated docstrings
|
||||
are cosmetic by design.
|
||||
|
||||
### 5.2 Self-disabling behavior
|
||||
|
||||
On every ComfyUI start the patch first asks: *"is the problem still there?"*
|
||||
|
||||
1. Cheap in-process check: does the source dict already contain all four
|
||||
fields? If yes, upstream has been fixed at the schema level — skip.
|
||||
2. Subprocess probe: import the five affected modules in a clean interpreter
|
||||
and look for `[ERROR] ... but not documented` on stdout. If clean — skip.
|
||||
3. Only if both checks fail is the wrapper installed.
|
||||
|
||||
So the day Hugging Face fixes the indent, the kimi field name, and the
|
||||
paddleocr entries, the startup log will simply show a *skip* line instead of
|
||||
`Patched ...`. No cleanup task, no uninstall step, and — because the wrapper
|
||||
only ever adds missing keys — even a stale wrapper would remain inert.
|
||||
|
||||
### 5.3 Why no `site-packages` edits
|
||||
|
||||
The wrapper monkey-patches one function **in process memory** from the custom
|
||||
node's own `prestartup_script.py`. Nothing under
|
||||
`python_embeded\Lib\site-packages` is written, so `pip` upgrades, environment
|
||||
moves, and other machines keep working without manual surgery. The patch is
|
||||
versioned with the custom node (commit `8d82442`) and travels with it.
|
||||
|
||||
### 5.4 Verification results
|
||||
|
||||
Verification was performed with the embedded Python
|
||||
(`D:\USERFILES\ComfyUI\python_embeded\python.exe`) in clean subprocesses:
|
||||
|
||||
| Phase | Setup | `[ERROR] ... not documented` lines |
|
||||
|-------|-------|------------------------------------|
|
||||
| A — baseline | Package reverted to original; patch not loaded | **13** (exactly matching the original startup log — proves the revert restored the pristine state and the errors are reproducible) |
|
||||
| B — patched | Patch module loaded + `apply_transformers_image_processor_kwargs_patch()` | **0** (`WRAPPED=True`, merged source dict contains all 4 keys) |
|
||||
|
||||
Both `prestartup_script.py` and the new patch module pass `py_compile`.
|
||||
|
||||
### 5.5 What you should see after restarting ComfyUI
|
||||
|
||||
With the wrapper active, the startup log shows:
|
||||
|
||||
```text
|
||||
INFO ComfyUI-QwenImageLoraLoader prestartup: ImageProcessorKwargs docstring patch applied
|
||||
```
|
||||
|
||||
and **no** `[ERROR] \`...\` is part of ...ImageProcessorKwargs` lines. After an
|
||||
upstream `transformers` fix, the line changes to a skip message instead:
|
||||
|
||||
```text
|
||||
INFO ... ImageProcessorKwargs docstring patch skipped: ... (upstream fixed; patch not installed)
|
||||
```
|
||||
|
||||
### 5.6 Removing the patch (if ever needed)
|
||||
|
||||
The patch is two small, self-contained changes. To remove it:
|
||||
|
||||
```bash
|
||||
git revert 8d82442
|
||||
```
|
||||
|
||||
or delete `patches/transformers_image_processor_kwargs_patch.py` and the
|
||||
`[ADDED]` block plus constant in `prestartup_script.py`.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — history of the fix
|
||||
|
||||
1. **First attempt (rejected):** the five transformer files under
|
||||
`site-packages` were edited directly (indent fix, field rename, added
|
||||
entries). It silenced the errors but modified installed package files —
|
||||
unacceptable: lost on every `pip` upgrade and invisible to version control.
|
||||
2. **Revert:** all five files were restored byte-for-byte; Phase A above
|
||||
confirmed the original 13 `[ERROR]` lines return.
|
||||
3. **Final fix (this document):** a repo-native runtime wrapper in
|
||||
`patches/transformers_image_processor_kwargs_patch.py`, wired through
|
||||
`prestartup_script.py`, following the exact design already used by the
|
||||
sibling patch `TRANSFORMERS_QWEN_VL_CAUSAL_LM_DOCSTRING_PATCH.md`.
|
||||
4. Committed and pushed: `8d82442` on `main`.
|
||||
|
||||
## Related documents
|
||||
|
||||
- `md/TRANSFORMERS_QWEN_VL_CAUSAL_LM_DOCSTRING_PATCH.md` — the sibling patch
|
||||
for Qwen VL `CausalLMOutputWithPast` `loss` / `logits` docstring errors
|
||||
(same design: probe, wrap `get_args_doc_from_source`, auto-disable).
|
||||
- `md/TORCH_PREIMPORT_WARNING_SUPPRESSION.md` — another startup-noise fix in
|
||||
the same prestartup pipeline.
|
||||
Reference in New Issue
Block a user