Files
slahiriandClaude Opus 4.5 9f6f30805b feat: Z-Image prompt pipeline redesign (WIP)
Changes:
- Add Z-Image prompt style with 6-part formula support
- Add vocabulary enhancement system (984 terms)
- Add classification improvements (age, clothing, subject count)
- Add Full and Z-Image prompt styles preserving Florence prose
- Add pipeline design document (docs/COMPOSE_PIPELINE_DESIGN.md)
- Remove RAG system (not working well)
- Remove use_rag input from compose node

Key insight: Pipeline should PRESERVE information, not destroy it.
- Florence descriptions kept intact (not tokenized)
- Tags organized into Z-Image categories
- NLP mode = structured output
- LLM mode = natural prose generation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 23:30:13 +05:30

313 lines
8.5 KiB
Python

"""Normalize and deduplicate extracted tokens.
This module handles:
- Deduplication of tokens with the same normalized text
- Confidence aggregation for duplicate tokens
- Source reliability weighting
- Near-duplicate detection (fuzzy matching)
"""
from typing import Dict, List, Set, Tuple, Optional
from collections import defaultdict
import re
from .base import ImageToken, TokenType, TokenSource, TokenBatch, SOURCE_WEIGHTS
def normalize_for_comparison(text: str) -> str:
"""Normalize text for duplicate comparison.
- Lowercase
- Remove extra whitespace
- Remove common suffixes/prefixes that don't change meaning
"""
text = text.lower().strip()
text = ' '.join(text.split())
# Remove trailing punctuation
text = text.rstrip('.,;:!?')
return text
def are_near_duplicates(text1: str, text2: str) -> bool:
"""Check if two texts are near-duplicates.
Near-duplicates are texts that differ only by:
- Word order (for short phrases)
- Common synonyms
- Singular/plural forms
"""
norm1 = normalize_for_comparison(text1)
norm2 = normalize_for_comparison(text2)
# Exact match after normalization
if norm1 == norm2:
return True
# Check if one is substring of another (for very similar phrases)
if len(norm1) > 5 and len(norm2) > 5:
if norm1 in norm2 or norm2 in norm1:
return True
# Check word set overlap for short phrases
words1 = set(norm1.split())
words2 = set(norm2.split())
if len(words1) <= 3 and len(words2) <= 3:
# For short phrases, check if they share most words
overlap = words1 & words2
total = words1 | words2
if len(total) > 0 and len(overlap) / len(total) >= 0.8:
return True
return False
def aggregate_confidence(confidences: List[Tuple[float, TokenSource]]) -> float:
"""Aggregate confidence from multiple sources.
Uses weighted combination based on source reliability.
Multiple sources agreeing increases overall confidence.
Args:
confidences: List of (confidence, source) tuples
Returns:
Aggregated confidence score (0.0-1.0)
"""
if not confidences:
return 0.0
if len(confidences) == 1:
conf, source = confidences[0]
weight = SOURCE_WEIGHTS.get(source, 0.5)
return conf * weight
# Multiple sources - combine with weighted average + agreement bonus
total_weight = 0.0
weighted_sum = 0.0
for conf, source in confidences:
weight = SOURCE_WEIGHTS.get(source, 0.5)
weighted_sum += conf * weight
total_weight += weight
base_confidence = weighted_sum / total_weight if total_weight > 0 else 0.0
# Agreement bonus: more sources = higher confidence
agreement_bonus = min(0.1 * (len(confidences) - 1), 0.2)
return min(1.0, base_confidence + agreement_bonus)
def deduplicate_tokens(tokens: List[ImageToken]) -> List[ImageToken]:
"""Remove duplicate tokens, aggregating confidence.
Tokens with the same normalized text are merged:
- Highest-confidence source becomes primary
- All sources are recorded in metadata
- Confidence is aggregated
Args:
tokens: List of tokens to deduplicate
Returns:
Deduplicated list of tokens
"""
# Group by normalized text
groups: Dict[str, List[ImageToken]] = defaultdict(list)
for token in tokens:
key = normalize_for_comparison(token.text)
groups[key].append(token)
# Merge each group
result = []
for normalized_text, group in groups.items():
if len(group) == 1:
result.append(group[0])
continue
# Multiple tokens with same text - merge them
# Sort by weighted confidence to pick best source
group.sort(key=lambda t: t.weighted_confidence, reverse=True)
primary = group[0]
# Collect all confidences and sources
confidences = [(t.confidence, t.source) for t in group]
sources = list(set(t.source for t in group))
# Aggregate confidence
aggregated_conf = aggregate_confidence(confidences)
# Create merged token
merged = ImageToken(
text=primary.text, # Use text from highest-confidence source
confidence=aggregated_conf,
source=primary.source, # Primary source
token_type=primary.token_type,
metadata={
**primary.metadata,
"merged_from": len(group),
"all_sources": [s.value for s in sources],
"original_confidences": [(c, s.value) for c, s in confidences]
}
)
result.append(merged)
return result
def deduplicate_near_duplicates(tokens: List[ImageToken]) -> List[ImageToken]:
"""Remove near-duplicate tokens (fuzzy matching).
This is more aggressive than exact deduplication.
Use with caution - may merge tokens that shouldn't be merged.
Args:
tokens: List of tokens to deduplicate
Returns:
Deduplicated list of tokens
"""
if len(tokens) <= 1:
return tokens
# Sort by confidence (descending) to keep highest-confidence version
sorted_tokens = sorted(tokens, key=lambda t: t.weighted_confidence, reverse=True)
result = []
used_texts: Set[str] = set()
for token in sorted_tokens:
normalized = normalize_for_comparison(token.text)
# Check if this is a near-duplicate of something we've kept
is_duplicate = False
for used in used_texts:
if are_near_duplicates(normalized, used):
is_duplicate = True
break
if not is_duplicate:
result.append(token)
used_texts.add(normalized)
return result
def filter_low_confidence(
tokens: List[ImageToken],
min_confidence: float = 0.3
) -> List[ImageToken]:
"""Filter out tokens below confidence threshold.
Args:
tokens: List of tokens to filter
min_confidence: Minimum confidence threshold
Returns:
Filtered list of tokens
"""
return [t for t in tokens if t.confidence >= min_confidence]
def filter_by_length(
tokens: List[ImageToken],
min_length: int = 2,
max_length: int = 200,
max_words: int = 8
) -> List[ImageToken]:
"""Filter tokens by text length and word count.
Filters out tokens that are too short, too long, or contain too many words
(likely full sentences from captions that shouldn't be individual tokens).
Args:
tokens: List of tokens to filter
min_length: Minimum text length
max_length: Maximum text length
max_words: Maximum word count (filters out long sentences, default 8)
Returns:
Filtered list of tokens
"""
result = []
for t in tokens:
# Check character length
if not (min_length <= len(t.text) <= max_length):
continue
# Check word count (filter out full sentences)
word_count = len(t.text.split())
if word_count > max_words:
continue
result.append(t)
return result
def filter_meta_tokens(tokens: List[ImageToken]) -> Tuple[List[ImageToken], List[ImageToken]]:
"""Separate filtered (META) tokens from valid tokens.
Args:
tokens: List of tokens to filter
Returns:
Tuple of (valid_tokens, filtered_tokens)
"""
valid = []
filtered = []
for token in tokens:
if token.metadata.get("filtered", False):
filtered.append(token)
else:
valid.append(token)
return valid, filtered
def normalize_batch(
batch: TokenBatch,
min_confidence: float = 0.3,
deduplicate: bool = True,
fuzzy_dedupe: bool = False
) -> TokenBatch:
"""Apply all normalization steps to a token batch.
Args:
batch: TokenBatch to normalize
min_confidence: Minimum confidence threshold
deduplicate: Whether to deduplicate exact matches
fuzzy_dedupe: Whether to also remove near-duplicates
Returns:
Normalized TokenBatch
"""
tokens = list(batch.tokens)
# Filter by confidence
tokens = filter_low_confidence(tokens, min_confidence)
# Filter by length
tokens = filter_by_length(tokens)
# Deduplicate
if deduplicate:
tokens = deduplicate_tokens(tokens)
if fuzzy_dedupe:
tokens = deduplicate_near_duplicates(tokens)
# Create new batch
result = TokenBatch()
result.image_info = batch.image_info
result.add_all(tokens)
return result