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.
174 lines
5.3 KiB
Python
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."
|
|
)
|