Files
goodguy1963-ComfyUI-Thinkin…/AILab_OutputCleaner.py
T
Marcel Hofer e0985b4f7b feat: improve video prompting and refactor QwenVL
Refactor the monolithic QwenVL implementation into focused core modules for model access, Hugging Face runtime and configuration, prompt state, prompt contracts, and media helpers while preserving the public compatibility facade.

Add duration-aware LTX 2.3 and MiniMax H3 prompting, including effective-duration cache identity, MiniMax frame-grid normalization, dedicated T2VA/I2VA/FL2VA/L2VA/reference contracts, and best-effort output normalization.

Improve streamed output handling by separating reasoning from final content, rejecting unfinished thinking traces, sanitizing terminal-only punctuation for legacy Windows code pages, and retaining original Unicode in responses and traces.

Expand preset recommendations, ComfyUI tooltips, system prompts, and documentation for video token budgets, thinking settings, reference roles, dialogue fidelity, and duration behavior.

Keep HF and GGUF consumers on canonical shared imports, preserve mutable export identities across reloads, and extend regression coverage for compatibility, downloads, caching, prompting, streaming, and node registration.
2026-08-10 22:04:05 +02:00

174 lines
5.3 KiB
Python

import json
import re
from dataclasses import dataclass
OUTPUT_CLEANER_VERSION = 4
@dataclass(frozen=True)
class OutputCleanConfig:
mode: str = "prompt"
strip_think: bool = True
strip_code_fences: bool = True
strip_role_prefixes: bool = True
strip_json_wrappers: bool = True
strip_leading_preamble: bool = True
strip_planning: bool = True
keep_first_paragraph_only: bool = False
_ROLE_PREFIX_RE = re.compile(r"^\s*(assistant|final|output|response|result|prompt)\s*:\s*", re.IGNORECASE)
_CODE_FENCE_RE = re.compile(r"^\s*```[\w-]*\s*$", re.IGNORECASE)
_THINK_BLOCK_RE = re.compile(r"<think[^>]*>.*?</think>", flags=re.IGNORECASE | re.DOTALL)
_THINK_OPEN_RE = re.compile(r"<think[^>]*>", flags=re.IGNORECASE)
_THINK_CLOSE_RE = re.compile(r"</think\s*>", flags=re.IGNORECASE)
_MARKER_RE = re.compile(
r"(?im)^\s*(final|final answer|answer|output|result|prompt)\s*[:\-]\s*",
)
_IM_TOKEN_RE = re.compile(
r"(?i)<\|?im_(start|end)\|?>|<im_(start|end)>|<\|endoftext\|>",
)
_PLANNING_RE = re.compile(
r"(?im)^\s*(?:[-*]\s*)?(?:\*\*)?\s*(?:"
r"final\s+plan\b|"
r"final\s+check\b|"
r"i\s+(?:should|need|must|will|want|am\s+going\s+to|have\s+to)\b|"
r"let's\b|"
r"first\s*[,.:]|next\s*[,.:]|then\s*[,.:]|"
r"wait\s*[,.:]|"
r"ready\s+to\s+write\b|"
r"writing\s+the\s+prompt\b|"
r"so\s+i\s+need\s+to\b|"
r"i\s+should\s+focus\s+on\b"
r")"
)
def clean_model_output(text: str, config: OutputCleanConfig | None = None) -> str:
if not text:
return ""
cfg = config or OutputCleanConfig()
cleaned = (text or "").strip()
# Remove common chat template tokens that sometimes leak into output.
cleaned = _IM_TOKEN_RE.sub("", cleaned).strip()
if cfg.strip_think:
cleaned = _THINK_BLOCK_RE.sub("", cleaned)
cleaned = _THINK_CLOSE_RE.sub("", cleaned)
unclosed_think = _THINK_OPEN_RE.search(cleaned)
if unclosed_think:
# When generation ends before </think>, no final answer exists.
# Prompt-shaped examples inside that unfinished reasoning must not
# be exposed as RESPONSE.
cleaned = cleaned[:unclosed_think.start()]
cleaned = cleaned.strip()
cleaned = _IM_TOKEN_RE.sub("", cleaned).strip()
if cfg.strip_code_fences and "```" in cleaned:
lines = [ln for ln in cleaned.splitlines() if not _CODE_FENCE_RE.match(ln)]
cleaned = "\n".join(lines).strip()
if cfg.strip_json_wrappers:
maybe = _extract_from_json(cleaned, mode=cfg.mode)
if maybe is not None:
cleaned = maybe.strip()
if cfg.strip_leading_preamble:
cleaned = _drop_preamble(cleaned).strip()
if cfg.strip_planning and cfg.mode == "prompt":
without_planning = _strip_planning_paragraphs(cleaned)
if without_planning:
cleaned = without_planning
if cfg.strip_role_prefixes:
lines = cleaned.splitlines()
if lines:
lines[0] = _ROLE_PREFIX_RE.sub("", lines[0])
cleaned = "\n".join(lines).strip()
cleaned = _MARKER_RE.sub("", cleaned).strip()
if cfg.keep_first_paragraph_only:
parts = re.split(r"\n\s*\n", cleaned, maxsplit=1)
cleaned = parts[0].strip()
return cleaned
def _extract_from_json(text: str, mode: str) -> str | None:
candidate = text.strip()
if not candidate:
return None
if not (candidate.startswith("{") and candidate.endswith("}")):
return None
try:
payload = json.loads(candidate)
except Exception:
return None
if not isinstance(payload, dict):
return None
preferred_keys = []
if mode == "prompt":
preferred_keys = ["prompt", "final", "output", "text", "content"]
else:
preferred_keys = ["text", "content", "output", "final"]
for key in preferred_keys:
value = payload.get(key)
if isinstance(value, str) and value.strip():
return value
return None
def _drop_preamble(text: str) -> str:
lines = text.splitlines()
if not lines:
return text
keep_from = 0
for i, ln in enumerate(lines[:20]):
if _MARKER_RE.match(ln):
keep_from = i
if re.search(r"(?i)\bhere(?:'s| is)\b", ln) and i < 6:
keep_from = max(keep_from, i + 1)
return "\n".join(lines[keep_from:]).strip()
def _strip_planning_paragraphs(text: str) -> str:
paragraphs = [p.strip() for p in re.split(r"\n\s*\n", (text or "").strip()) if p.strip()]
if not paragraphs:
return ""
kept: list[str] = []
dropping = True
for p in paragraphs:
is_planning = bool(_PLANNING_RE.search(p))
if dropping and is_planning:
continue
dropping = False
kept.append(p)
# If everything looked like planning, don't destroy the output.
if not kept:
return text.strip()
return "\n\n".join(kept).strip()
def prompt_output_guard() -> str:
return (
"Output rules that override all style preferences: return only the final usable prompt text. "
"Do not include analysis, planning, bullet points, headings, markdown, JSON, explanations, alternatives, self-corrections, or notes. "
"Do not write phrases such as Final Plan, Final Check, Wait, Okay, First, Next, Then, I will, or I need. "
"Start directly with the required prompt text."
)