#!/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)""" desc_lower = description.lower() # Check Added first (for new features/documentation) if any(word in desc_lower for word in [ 'add', 'new', 'implement', 'feature', 'create', 'introduce', 'support', 'documentation', 'readme', 'guide', 'section', 'workflow', 'example' ]): return "### Added" # Check Changed second (for improvements/UI) elif any(word in desc_lower for word in [ 'update', 'enhance', 'improve', 'change', 'modify', 'optimize', 'ui', 'interface', 'slider', 'tooltip', 'dropdown', 'better', 'cleaner' ]): return "### Changed" # Check Fixed last (for actual bug fixes) elif any(word in desc_lower for word in [ 'fix', 'bug', 'error', 'issue', 'resolve', 'correct', 'patch', 'crash', 'problem', 'fail', 'broken', 'dependency', 'missing' ]): return "### Fixed" # Check Removed elif any(word in desc_lower for word in ['remove', 'delete', 'deprecate', 'drop']): return "### Removed" else: return "### Added" # Default to Added for new features 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 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}" new_entry = 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 line_lower = clean_line.lower() # Check for specific patterns first if any(word in line_lower for word in [ 'fix', 'bug', 'error', 'issue', 'resolve', 'correct', 'patch', 'crash', 'problem', 'fail', 'broken', 'dependency', 'missing', 'compatibility', 'mismatch' ]): fixed_items.append(clean_line) elif any(word in line_lower for word in [ 'remove', 'delete', 'deprecate', 'drop', 'eliminate' ]): removed_items.append(clean_line) elif any(word in line_lower for word in [ '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("") new_entry = "\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." """ print(error_msg) 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