Files
diodiogod-TTS-Audio-Suite/scripts/version_utils.py
T
diodiogod 25aee4bd3f Fix changelog formatting and improve bump script
- Fix malformed recent changelog entries (4.3.1-4.3.7) with proper bullet points and section organization
- Improve bump script formatting logic with better line parsing and categorization
- Add consistent spacing and conventional changelog order (Added → Changed → Fixed → Removed)
- Ensure future version bumps maintain proper markdown formatting
2025-08-13 13:51:55 -03:00

307 lines
13 KiB
Python

#!/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