Files
Terpentine c12fe52019 Easy LoRA Merger Suite v1.0
Out of beta. First stable release.
2026-05-10 03:46:27 +03:00

207 lines
7.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Shared forensic report builder for merge/bake operations.
Consolidates three implementations:
- baker: _build_forensic_report() in baking_processor_baking.py
- triple: build_triple_forensic_report() in triple_forensics.py
- ckpt: generate_forensic_report() in checkpoint_weaver.py
Design: Each caller passes an ordered list of ``(section_title_or_None, lines)``
tuples, giving full control over section ordering. Shared helper functions
(``_build_component_breakdown``, ``_build_effect_bar``, ``_section``, …)
generate the content for standard section types.
Usage::
from engine.forensics import build_forensic_report, _build_component_breakdown
report = build_forensic_report(
report_type="EASY LoRA BAKER",
title_data=OrderedDict([("📦 BAKED LORA", loRA_name), …]),
sections=[
("IMPACT PROFILE", impact_lines),
(None, comp_lines), # raw – no section wrapper
("DELTA ANALYSIS", delta_lines),
],
)
"""
from typing import Any, Dict, List, Optional, Tuple
# ═══════════════════════════════════════════════════════════════════
# Shared building blocks
# ═══════════════════════════════════════════════════════════════════
def _build_header(report_type: str) -> str:
"""Return the shield-surrounded header line."""
return f"🛡️ --- {report_type}: FORENSIC REPORT --- 🛡️"
def _build_footer(width: int = 50) -> str:
"""Return the footer separator line."""
return "=" * width
def _build_effect_bar(mean_pct: float, max_pct: float, width: int = 20) -> str:
"""Build a compact visual bar representing the effect magnitude."""
filled = max(0, min(width, int((mean_pct / 5.0) * width)))
bar = '█' * filled + '▁' * (width - filled)
return f"{bar} mean={mean_pct:.2f}% max={max_pct:.2f}%"
def _section(title: str, lines: List[str], sep: str = '-') -> List[str]:
"""Wrap *lines* with a separator and a section title."""
result: List[str] = []
result.append(sep * 50)
result.append(title)
result.extend(lines)
return result
def _build_component_breakdown(
components: Dict[str, Any],
icons: Optional[Dict[str, str]] = None,
comp_order: Optional[List[str]] = None,
) -> List[str]:
"""
Build component-breakdown lines.
*components* is a ``{name: data}`` dict. *data* may be:
* ``int`` – rendered as ``{icon} {LABEL}: {count} keys``
* ``dict`` with one of the following shapes:
* Baker-style keys ``lora_keys``, ``matched``, ``baked``, ``reason``
* Checkpoint-style keys ``count``, ``params``
* Plain ``count``
*icons* overrides the default emoji mapping per component.
*comp_order* controls the display order (default: ``unet → model → te → clip → vae → other``).
"""
if not components:
return []
default_icons = {
'unet': '🔷',
'model': '🔷',
'te': '📝',
'clip': '📷',
'vae': '🎬',
'other': '❓',
}
effective_icons = {**default_icons, **(icons or {})}
if comp_order is None:
comp_order = ['unet', 'model', 'te', 'clip', 'vae', 'other']
lines: List[str] = []
for comp in comp_order:
data = components.get(comp)
if data is None:
continue
icon = effective_icons.get(comp, '▫️')
if isinstance(data, dict):
# Detect section style by present keys
has_lora_keys = 'lora_keys' in data
has_params = 'params' in data
count = data.get('count', 0)
lora_keys = data.get('lora_keys', 0)
matched = data.get('matched', 0)
baked = data.get('baked', 0)
params = data.get('params', 0)
reason = data.get('reason', '')
if has_lora_keys:
# Baker-style
label = comp.upper()
if reason:
lines.append(
f" {icon} {label}: {lora_keys} lora keys → SKIPPED ({reason})"
)
else:
lines.append(
f" {icon} {label}: {lora_keys} lora keys → {matched} matched → {baked} baked"
)
elif has_params and count:
# Checkpoint-style
lines.append(
f" {icon} {comp.upper():<6} {count:>4} tensors, {params:>12,} parameters"
)
else:
lines.append(f" {icon} {comp.upper()}: {count} keys")
elif isinstance(data, int):
lines.append(f" {icon} {comp.upper()}: {data} keys")
elif isinstance(data, str):
lines.append(f" {data}")
return lines
# ═══════════════════════════════════════════════════════════════════
# Main builder
# ═══════════════════════════════════════════════════════════════════
def build_forensic_report(
report_type: str,
title_data: Dict[str, str],
sections: Optional[List[Tuple[Optional[str], List[str]]]] = None,
footer_width: int = 50,
) -> str:
"""
Build a human-readable forensic report string.
Renders in order:
1. **Header** from *report_type*
2. **Title data** – one ``{label}: {value}`` line per entry
3. **Ordered sections** – each ``(title, lines)`` tuple is rendered with
``_section()`` if *title* is not ``None``, otherwise *lines* are added
verbatim (useful for blank separators or raw paragraphs).
4. **Footer** – ``=`` repeated *footer_width* times
Parameters
----------
report_type:
Short name for the header – e.g. ``"EASY LoRA BAKER"``,
``"EASY LoRA MERGER"``, ``"EASY CHECKPOINT MERGER"``.
title_data:
Ordered dict whose items become ``{label}: {value}`` lines right
after the header. Use ``collections.OrderedDict`` if insertion
order must be explicit.
sections:
Ordered list of ``(section_title_or_None, lines_for_section)``.
Pass ``None`` as title to emit raw lines without a section wrapper.
footer_width:
Character width of the footer separator (default 50).
Returns
-------
The formatted report as a single string.
"""
lines: List[str] = []
# ── 1. Header ──
lines.append(_build_header(report_type))
# ── 2. Title data ──
for label, value in title_data.items():
lines.append(f"{label}: {value}")
# ── 3. Ordered sections ──
if sections:
for section_title, section_lines in sections:
if section_title is not None:
lines.extend(_section(f"📊 {section_title}:", section_lines))
else:
lines.extend(section_lines)
# ── 4. Footer ──
lines.append(_build_footer(footer_width))
return '\n'.join(lines)