#!/usr/bin/env python3 """ Version Management Utilities for ComfyUI ChatterBox Voice Provides centralized version reading/writing functionality """ import os import re import sys from datetime import datetime from typing import Dict, List, Tuple, Optional class VersionManager: """Manages version updates across multiple files""" def __init__(self, project_root: str = None): self.project_root = project_root or os.path.dirname(os.path.dirname(os.path.abspath(__file__))) self.version_files = { 'README.md': { 'pattern': r'# TTS Audio Suite v(\d+\.\d+\.\d+)', 'template': '# TTS Audio Suite v{version}' }, 'nodes.py': { 'pattern': r'VERSION = "(\d+\.\d+\.\d+)"', 'template': 'VERSION = "{version}"' }, 'pyproject.toml': { 'pattern': r'version = "(\d+\.\d+\.\d+)"', 'template': 'version = "{version}"' } } def validate_version(self, version: str) -> bool: """Validate semantic version format""" pattern = r'^\d+\.\d+\.\d+$' return bool(re.match(pattern, version)) def get_current_version(self) -> Optional[str]: """Get current version from nodes.py""" try: nodes_file = os.path.join(self.project_root, 'nodes.py') with open(nodes_file, 'r') as f: content = f.read() match = re.search(self.version_files['nodes.py']['pattern'], content) return match.group(1) if match else None except Exception as e: print(f"Error reading current version: {e}") return None def update_version_in_file(self, file_path: str, version: str) -> bool: """Update version in a specific file""" try: relative_path = os.path.relpath(file_path, self.project_root) if relative_path not in self.version_files: print(f"Warning: {relative_path} not in version files list") return False config = self.version_files[relative_path] with open(file_path, 'r') as f: content = f.read() # Find and replace version new_line = config['template'].format(version=version) updated_content = re.sub(config['pattern'], new_line, content) if updated_content == content: print(f"Warning: No version found in {relative_path}") return False with open(file_path, 'w') as f: f.write(updated_content) print(f"βœ“ Updated {relative_path}") return True except Exception as e: print(f"Error updating {file_path}: {e}") return False def update_all_versions(self, version: str) -> bool: """Update version in all files""" if not self.validate_version(version): print(f"Error: Invalid version format '{version}'. Use semantic versioning (e.g., 3.0.1)") return False success = True for file_path in self.version_files.keys(): full_path = os.path.join(self.project_root, file_path) if os.path.exists(full_path): if not self.update_version_in_file(full_path, version): success = False else: print(f"Warning: {file_path} not found") success = False return success def _categorize_simple_description(self, description: str) -> str: """Categorize a simple description for changelog (legacy fallback)""" import re desc_lower = description.lower() # Helper function to check for whole words only (avoids "add" in "advanced") def has_whole_word(text, words): for word in words: # Use word boundary \b to match whole words only if re.search(rf'\b{re.escape(word)}\b', text): return True return False # Check Fixed FIRST (most important - fixes take priority) if has_whole_word(desc_lower, [ 'fix', 'bug', 'error', 'issue', 'resolve', 'correct', 'patch', 'crash', 'problem', 'fail', 'broken', 'dependency', 'missing' ]): return "### Fixed" # Check Added second (for new features/documentation) elif has_whole_word(desc_lower, [ 'add', 'new', 'implement', 'feature', 'create', 'introduce', 'support', 'documentation', 'readme', 'guide', 'section', 'workflow', 'example' ]): return "### Added" # Check Changed third (for improvements/UI) elif has_whole_word(desc_lower, [ 'update', 'enhance', 'improve', 'change', 'modify', 'optimize', 'ui', 'interface', 'slider', 'tooltip', 'dropdown', 'better', 'cleaner' ]): return "### Changed" # Check Removed last elif has_whole_word(desc_lower, [ 'remove', 'delete', 'deprecate', 'drop', 'eliminate' ]): return "### Removed" else: return "### Added" # Default to Added for new features def _generate_changelog_content(self, version: str, description: str, details: List[str] = None, simple_mode: bool = False) -> str: """Generate changelog entry content""" today = datetime.now().strftime('%Y-%m-%d') # Simple mode - use exact text with proper formatting if simple_mode: # Smart categorization for simple mode change_type = self._categorize_simple_description(description) # Split multi-sentence descriptions into proper bullet points if '. ' in description and len(description) > 100: # Break long descriptions into bullet points sentences = [s.strip() for s in description.split('. ') if s.strip()] items = [] for sentence in sentences: if not sentence.endswith('.') and sentence != sentences[-1]: sentence += '.' items.append(f"- {sentence}") item_text = '\n'.join(items) else: # Single item item_text = f"- {description}" return f"""## [{version}] - {today} {change_type} {item_text} """ # Parse description for structured changelog elif '\n' in description or (details and len(details) > 0): # Multiline description - create detailed changelog with better parsing lines = description.split('\n') if '\n' in description else [description] if details: lines.extend(details) # Group by change type added_items = [] fixed_items = [] changed_items = [] removed_items = [] # Process each line with better formatting for line in lines: line = line.strip() if not line or line.startswith('#'): continue # Clean up the line - remove existing bullet points for consistent formatting clean_line = re.sub(r'^[-*β€’]\s*', '', line) if not clean_line: continue # Skip section headers or emoji-only lines if re.match(r'^[πŸŒπŸ“‹πŸš€βš‘πŸŽ―πŸ”§πŸŽ­πŸŽ™οΈπŸ“ΊπŸŽ΅πŸ—οΈ]+\s*[\w\s]*:?\s*$', line): continue # Enhanced categorization based on keywords (whole words only) line_lower = clean_line.lower() # Helper function for whole word matching in multiline context def has_whole_word_in_line(text, words): import re for word in words: # Use word boundary \b to match whole words only if re.search(rf'\b{re.escape(word)}\b', text): return True return False # Check for specific patterns first if has_whole_word_in_line(line_lower, [ 'fix', 'bug', 'error', 'issue', 'resolve', 'correct', 'patch', 'crash', 'problem', 'fail', 'broken', 'dependency', 'missing', 'compatibility', 'mismatch' ]): fixed_items.append(clean_line) elif has_whole_word_in_line(line_lower, [ 'remove', 'delete', 'deprecate', 'drop', 'eliminate' ]): removed_items.append(clean_line) elif has_whole_word_in_line(line_lower, [ 'update', 'enhance', 'improve', 'change', 'modify', 'optimize', 'performance', 'smart', 'efficient', 'better', 'cleaner', 'reorganize', 'refactor', 'streamline' ]): changed_items.append(clean_line) else: # Default to Added for new features, documentation, etc. added_items.append(clean_line) # Build changelog entry with proper spacing entry_parts = [f"## [{version}] - {today}"] # Add sections in conventional order: Added, Changed, Fixed, Removed if added_items: entry_parts.extend(["", "### Added", ""]) for item in added_items: entry_parts.append(f"- {item}") if changed_items: entry_parts.extend(["", "### Changed", ""]) for item in changed_items: entry_parts.append(f"- {item}") if fixed_items: entry_parts.extend(["", "### Fixed", ""]) for item in fixed_items: entry_parts.append(f"- {item}") if removed_items: entry_parts.extend(["", "### Removed", ""]) for item in removed_items: entry_parts.append(f"- {item}") # Ensure proper ending entry_parts.append("") return "\n".join(entry_parts) else: # Single line description - provide helpful error message error_msg = f""" ❌ ERROR: Single-line descriptions are no longer supported for better changelog quality. Please use the proper format with detailed descriptions: CORRECT USAGE: python3 scripts/bump_version_enhanced.py {version} "commit message" "changelog description Explain what users will experience. Focus on benefits and improvements. Use bullet points for multiple changes." EXAMPLE: python3 scripts/bump_version_enhanced.py patch "Fix RVC dropdown" "Fix RVC model dropdown to show both downloadable and local models The Load RVC Character Model node now properly displays both downloadable character models and local models in the same dropdown. This matches the F5-TTS dropdown behavior for consistent user experience across all engines." """ raise ValueError(error_msg) def preview_changelog_entry(self, version: str, description: str, details: List[str] = None, simple_mode: bool = False) -> str: """Preview changelog entry content without writing to file (for dry-run)""" return self._generate_changelog_content(version, description, details, simple_mode) def add_changelog_entry(self, version: str, description: str, details: List[str] = None, simple_mode: bool = False) -> bool: """Add entry to CHANGELOG.md with proper formatting""" try: changelog_path = os.path.join(self.project_root, 'CHANGELOG.md') with open(changelog_path, 'r') as f: content = f.read() # Generate changelog entry content using extracted method try: new_entry = self._generate_changelog_content(version, description, details, simple_mode) except ValueError as e: print(str(e)) return False # Insert after the header (find first ## line) lines = content.split('\n') insert_index = 0 for i, line in enumerate(lines): if line.startswith('## [') and ']' in line: insert_index = i break # Insert the new entry with proper spacing lines.insert(insert_index, new_entry.rstrip()) with open(changelog_path, 'w') as f: f.write('\n'.join(lines)) print(f"βœ“ Added changelog entry for v{version}") return True except Exception as e: print(f"Error updating changelog: {e}") return False def backup_files(self) -> Dict[str, str]: """Create backup of all version files""" backups = {} for file_path in self.version_files.keys(): full_path = os.path.join(self.project_root, file_path) if os.path.exists(full_path): with open(full_path, 'r') as f: backups[file_path] = f.read() return backups def restore_files(self, backups: Dict[str, str]) -> bool: """Restore files from backup""" try: for file_path, content in backups.items(): full_path = os.path.join(self.project_root, file_path) with open(full_path, 'w') as f: f.write(content) return True except Exception as e: print(f"Error restoring files: {e}") return False