Implement threading, logging, and CJK font support
Major improvements to GUI stability and internationalization: - Fix GUI freezing by implementing threaded audio analysis - Add AudioAnalysisWorker thread for background processing - Progress signals with percentage updates - Thread-safe communication via Qt signals - Add comprehensive CLI logging system - 5 log levels: ERROR, WARN, INFO, DEBUG, TRACE - Command line control: --log-level, --log-file - Real-time feedback during analysis operations - Implement CJK font fallback system - FontManager with 3-tier fallback (custom → system → default) - Cross-platform CJK font detection (Windows/macOS/Linux) - Licensing-safe fonts/ directory with gitignored font files - Setup utility and comprehensive documentation - Fix numpy array formatting issue with BPM detection - Add progress indicators for long-running operations - Preserve fonts directory structure with placeholder file 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"WebFetch(domain:docs.anthropic.com)",
|
||||
"Bash(cat:*)",
|
||||
"Bash(lsb_release:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(python:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git push:*)",
|
||||
"WebFetch(domain:docs.github.com)",
|
||||
"WebFetch(domain:docs.github.com)",
|
||||
"Bash(ssh:*)",
|
||||
"Bash(rm:*)"
|
||||
],
|
||||
"deny": [],
|
||||
"ask": [],
|
||||
"defaultMode": "acceptEdits"
|
||||
},
|
||||
"outputStyle": "haruki"
|
||||
}
|
||||
+24
-1
@@ -1 +1,24 @@
|
||||
files.txt
|
||||
files.txt
|
||||
|
||||
# Font directory - avoid licensing issues by not committing font files
|
||||
# Keep the directory structure but ignore actual font files
|
||||
fonts/*.ttf
|
||||
fonts/*.otf
|
||||
fonts/*.ttc
|
||||
# But preserve the placeholder file
|
||||
!fonts/PLACE_YOUR_FONT_FILES_HERE
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
|
||||
# IDE files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
# CJK Font Support Implementation
|
||||
|
||||
This document describes the CJK (Chinese, Japanese, Korean) font fallback system implemented for the Audio Analysis Toolkit.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
The application displays song titles and metadata that may contain CJK characters from Japanese, Chinese, or Korean music files. The default matplotlib font (DejaVu Sans) lacks CJK glyphs, causing:
|
||||
- Matplotlib warnings about missing glyphs
|
||||
- Incorrect character rendering (squares, question marks, etc.)
|
||||
- Poor user experience for CJK music collections
|
||||
|
||||
## Solution Architecture
|
||||
|
||||
### 1. Font Manager System (`font_manager.py`)
|
||||
|
||||
A centralized font management system that handles both matplotlib and Qt font configuration:
|
||||
|
||||
**Key Features:**
|
||||
- **Licensing-safe**: Uses local `fonts/` directory (gitignored) for custom fonts
|
||||
- **Graceful fallback**: System CJK fonts → matplotlib defaults
|
||||
- **Cross-platform**: Windows, macOS, Linux font detection
|
||||
- **Modular design**: Single responsibility for font configuration
|
||||
|
||||
**Font Priority Order:**
|
||||
1. Custom fonts from `fonts/` directory (highest priority)
|
||||
2. System CJK fonts (platform-specific)
|
||||
3. Default fonts (fallback)
|
||||
|
||||
### 2. Safe Title Processing
|
||||
|
||||
All text that might contain CJK characters is processed through `safe_title()` function:
|
||||
- Ensures proper encoding handling
|
||||
- Provides fallback for problematic characters
|
||||
- Maintains original text when possible
|
||||
|
||||
### 3. Integration Points
|
||||
|
||||
The font system is integrated at these key locations:
|
||||
|
||||
#### Application Startup (`main.py`)
|
||||
```python
|
||||
# Initialize font system before creating any widgets
|
||||
font_success = initialize_fonts()
|
||||
```
|
||||
|
||||
#### Plot Titles (`plotting_engine.py`, `master_core.py`)
|
||||
```python
|
||||
ax.set_title(safe_title(os.path.basename(file_path)))
|
||||
```
|
||||
|
||||
#### Metadata Display
|
||||
```python
|
||||
song_name = safe_title(f"{audio['artist'][0]} - {audio['title'][0]}")
|
||||
```
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
### Quick Setup
|
||||
|
||||
1. **Run the setup utility:**
|
||||
```bash
|
||||
python setup_fonts.py
|
||||
```
|
||||
|
||||
2. **For enhanced CJK support, add fonts to the `fonts/` directory:**
|
||||
- Download free CJK fonts (Noto Sans CJK, Source Han Sans, etc.)
|
||||
- Place .ttf/.otf/.ttc files in `fonts/` directory
|
||||
- Restart the application
|
||||
|
||||
### Font Directory Structure
|
||||
```
|
||||
uj-mastering-master/
|
||||
├── fonts/ # Gitignored
|
||||
│ ├── NotoSansCJK-Regular.ttc
|
||||
│ ├── SourceHanSans-Regular.otf
|
||||
│ └── [other CJK fonts]
|
||||
└── [application files]
|
||||
```
|
||||
|
||||
### Supported Font Formats
|
||||
- `.ttf` (TrueType Font)
|
||||
- `.otf` (OpenType Font)
|
||||
- `.ttc` (TrueType Collection)
|
||||
|
||||
## Platform-Specific Behavior
|
||||
|
||||
### Windows
|
||||
**System Fonts Used:**
|
||||
- Yu Gothic UI, Meiryo, MS Gothic (sans-serif)
|
||||
- Yu Mincho, MS Mincho (serif)
|
||||
|
||||
### macOS
|
||||
**System Fonts Used:**
|
||||
- Hiragino Sans, Yu Gothic (sans-serif)
|
||||
- Hiragino Mincho ProN, Yu Mincho (serif)
|
||||
|
||||
### Linux
|
||||
**System Fonts Used:**
|
||||
- Noto Sans CJK JP, Source Han Sans (sans-serif)
|
||||
- Noto Serif CJK JP, Source Han Serif (serif)
|
||||
|
||||
## Technical Implementation Details
|
||||
|
||||
### Font Detection Algorithm
|
||||
|
||||
1. **Custom Font Loading:**
|
||||
```python
|
||||
# Load for matplotlib
|
||||
fm.fontManager.addfont(str(font_file))
|
||||
|
||||
# Load for Qt
|
||||
font_id = QFontDatabase.addApplicationFont(str(font_file))
|
||||
```
|
||||
|
||||
2. **System Font Fallback:**
|
||||
```python
|
||||
available_fonts = set(fm.get_font_names())
|
||||
for font_name in system_fonts['sans-serif']:
|
||||
if font_name in available_fonts:
|
||||
return font_name
|
||||
```
|
||||
|
||||
3. **Matplotlib Configuration:**
|
||||
```python
|
||||
plt.rcParams['font.sans-serif'] = font_list
|
||||
plt.rcParams['font.family'] = 'sans-serif'
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
The system is designed to be fault-tolerant:
|
||||
- Missing fonts directory → Use system fonts
|
||||
- Font loading failures → Log warnings, continue
|
||||
- Encoding errors → Apply safe character replacement
|
||||
- No CJK fonts found → Graceful degradation to defaults
|
||||
|
||||
## Licensing Considerations
|
||||
|
||||
### Safe Practices
|
||||
- **Custom fonts directory is gitignored** to avoid committing proprietary fonts
|
||||
- **System fonts are detected, not redistributed**
|
||||
- **Open source font recommendations** (Noto, Source Han families)
|
||||
- **No font files included in repository**
|
||||
|
||||
### Recommended Free CJK Fonts
|
||||
1. **Google Noto Fonts** (SIL Open Font License)
|
||||
- Noto Sans CJK JP/SC/TC/KR
|
||||
- Comprehensive CJK coverage
|
||||
|
||||
2. **Adobe Source Han Fonts** (SIL Open Font License)
|
||||
- Source Han Sans
|
||||
- Source Han Serif
|
||||
|
||||
## Testing and Debugging
|
||||
|
||||
### Font Status Report
|
||||
```python
|
||||
from font_manager import get_font_manager
|
||||
status = get_font_manager().get_status_report()
|
||||
print(status)
|
||||
```
|
||||
|
||||
### Test CJK Characters
|
||||
```bash
|
||||
python setup_fonts.py
|
||||
```
|
||||
|
||||
### Logging
|
||||
Font system operations are logged at appropriate levels:
|
||||
- INFO: Successful initialization
|
||||
- DEBUG: Font loading details
|
||||
- WARNING: Missing fonts, fallbacks used
|
||||
- ERROR: Critical font system failures
|
||||
|
||||
## Future Improvements
|
||||
|
||||
### Potential Enhancements
|
||||
1. **Dynamic Font Switching:** Per-language font selection
|
||||
2. **Font Caching:** Faster startup with font cache
|
||||
3. **User Preferences:** GUI for font selection
|
||||
4. **Font Metrics:** Analyze font quality for CJK rendering
|
||||
|
||||
### Performance Considerations
|
||||
- Font loading is done once at startup
|
||||
- Font cache clearing only when necessary
|
||||
- Minimal performance impact on audio processing
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Issue:** CJK characters still show as squares
|
||||
- **Solution:** Install CJK fonts in `fonts/` directory or check system font availability
|
||||
|
||||
**Issue:** Font warnings in console
|
||||
- **Solution:** Run `python setup_fonts.py` to check font configuration
|
||||
|
||||
**Issue:** Application startup slower after font system
|
||||
- **Solution:** This is normal on first run; subsequent starts should be faster
|
||||
|
||||
### Debug Commands
|
||||
```bash
|
||||
# Check font system status
|
||||
python setup_fonts.py
|
||||
|
||||
# Test with specific log level
|
||||
python main.py --log-level DEBUG
|
||||
|
||||
# Test matplotlib font configuration
|
||||
python -c "import matplotlib.pyplot as plt; print(plt.rcParams['font.sans-serif'])"
|
||||
```
|
||||
|
||||
## Conclusion
|
||||
|
||||
This CJK font support implementation provides:
|
||||
- **Robust fallback system** ensuring CJK characters display properly
|
||||
- **Licensing compliance** by avoiding font redistribution
|
||||
- **Cross-platform compatibility** with platform-specific font preferences
|
||||
- **Clean architecture** with separation of font management concerns
|
||||
- **User-friendly setup** with clear instructions and status reporting
|
||||
|
||||
The system gracefully handles missing fonts and provides clear guidance for optimal CJK character rendering while maintaining the existing application functionality.
|
||||
+91
-32
@@ -3,10 +3,11 @@ Analysis Results Manager - Bridge between audio processing and GUI.
|
||||
Manages analysis queue and coordinates between components.
|
||||
"""
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, QThread
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
import os
|
||||
import logging
|
||||
|
||||
from master_core import AudioFile
|
||||
from plotting_engine import PlottingEngine
|
||||
@@ -26,26 +27,86 @@ class AnalysisResult:
|
||||
error_message: str = ""
|
||||
|
||||
|
||||
class AudioAnalysisWorker(QThread):
|
||||
"""
|
||||
Worker thread for audio analysis to prevent GUI freezing.
|
||||
Performs heavy librosa operations in background.
|
||||
"""
|
||||
|
||||
# Signals for communicating with main thread
|
||||
progressUpdate = pyqtSignal(str, int) # message, percentage
|
||||
analysisCompleted = pyqtSignal(str, object) # file_path, AnalysisResult
|
||||
analysisError = pyqtSignal(str, str) # file_path, error_message
|
||||
|
||||
def __init__(self, file_path: str, window: int = 10, hop: int = 2):
|
||||
super().__init__()
|
||||
self.file_path = file_path
|
||||
self.window = window
|
||||
self.hop = hop
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def run(self):
|
||||
"""Main thread execution - performs audio analysis."""
|
||||
try:
|
||||
self.logger.info(f"Starting analysis of: {os.path.basename(self.file_path)}")
|
||||
self.progressUpdate.emit("Loading audio file...", 10)
|
||||
|
||||
# Create AudioFile and load audio data
|
||||
audio_file = AudioFile(self.file_path)
|
||||
self.progressUpdate.emit("Audio loaded, detecting tempo...", 30)
|
||||
|
||||
# BPM is already calculated in __init__, now do RMS analysis
|
||||
self.progressUpdate.emit("Computing RMS power levels...", 60)
|
||||
audio_file.get_energy_levels_over_time(window=self.window, hop=self.hop)
|
||||
|
||||
self.progressUpdate.emit("Finalizing analysis...", 90)
|
||||
|
||||
# Extract analysis results
|
||||
result = AnalysisResult(
|
||||
file_path=self.file_path,
|
||||
song_name=audio_file.song_name,
|
||||
bpm=audio_file.get_bpm(),
|
||||
max_amplitude=audio_file.max_amplitude,
|
||||
avg_amplitude=audio_file.avg_amplitude,
|
||||
times=audio_file._get_times(),
|
||||
rms_array=audio_file.rms_array,
|
||||
analysis_successful=True
|
||||
)
|
||||
|
||||
self.progressUpdate.emit("Analysis complete!", 100)
|
||||
self.logger.info(f"Analysis completed: {os.path.basename(self.file_path)} (BPM: {result.bpm:.1f})")
|
||||
|
||||
# Emit success signal
|
||||
self.analysisCompleted.emit(self.file_path, result)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Analysis failed: {str(e)}"
|
||||
self.logger.error(f"Analysis error for {self.file_path}: {error_msg}")
|
||||
self.analysisError.emit(self.file_path, error_msg)
|
||||
|
||||
|
||||
class AnalysisResultsManager(QObject):
|
||||
"""
|
||||
Manages audio file analysis and coordinates between processing and GUI.
|
||||
Threading-ready architecture for future background processing.
|
||||
Now uses background threads to prevent GUI freezing.
|
||||
"""
|
||||
|
||||
# Signals for GUI communication
|
||||
analysisStarted = pyqtSignal(str) # file_path
|
||||
analysisCompleted = pyqtSignal(str, object) # file_path, AnalysisResult
|
||||
analysisError = pyqtSignal(str, str) # file_path, error_message
|
||||
progressUpdate = pyqtSignal(str, int) # message, percentage
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.results_cache = {} # Store analysis results
|
||||
self.plotting_engine = PlottingEngine()
|
||||
self.current_worker = None # Track active worker thread
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def analyze_file(self, file_path: str, window: int = 10, hop: int = 2):
|
||||
"""
|
||||
Analyze an audio file and emit results.
|
||||
Currently synchronous - ready for threading later.
|
||||
Analyze an audio file using background thread to prevent GUI freezing.
|
||||
|
||||
Args:
|
||||
file_path: Path to audio file
|
||||
@@ -54,40 +115,38 @@ class AnalysisResultsManager(QObject):
|
||||
"""
|
||||
if not os.path.exists(file_path):
|
||||
error_msg = f"File not found: {file_path}"
|
||||
self.logger.error(error_msg)
|
||||
self.analysisError.emit(file_path, error_msg)
|
||||
return
|
||||
|
||||
# Stop any existing worker
|
||||
if self.current_worker and self.current_worker.isRunning():
|
||||
self.logger.info("Stopping previous analysis to start new one")
|
||||
self.current_worker.quit()
|
||||
self.current_worker.wait()
|
||||
|
||||
# Emit analysis started signal
|
||||
self.analysisStarted.emit(file_path)
|
||||
self.logger.info(f"Queuing analysis: {os.path.basename(file_path)}")
|
||||
|
||||
try:
|
||||
# Create AudioFile and perform analysis
|
||||
audio_file = AudioFile(file_path)
|
||||
|
||||
# Get RMS analysis data
|
||||
audio_file.get_energy_levels_over_time(window=window, hop=hop)
|
||||
|
||||
# Extract analysis results
|
||||
result = AnalysisResult(
|
||||
file_path=file_path,
|
||||
song_name=audio_file.song_name,
|
||||
bpm=audio_file.get_bpm(),
|
||||
max_amplitude=audio_file.max_amplitude,
|
||||
avg_amplitude=audio_file.avg_amplitude,
|
||||
times=audio_file._get_times(), # We'll need to add this method
|
||||
rms_array=audio_file.rms_array,
|
||||
analysis_successful=True
|
||||
)
|
||||
|
||||
# Cache the result
|
||||
self.results_cache[file_path] = result
|
||||
|
||||
# Emit completion signal
|
||||
self.analysisCompleted.emit(file_path, result)
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Analysis failed: {str(e)}"
|
||||
self.analysisError.emit(file_path, error_msg)
|
||||
# Create and start worker thread
|
||||
self.current_worker = AudioAnalysisWorker(file_path, window, hop)
|
||||
|
||||
# Connect worker signals
|
||||
self.current_worker.progressUpdate.connect(self.progressUpdate.emit)
|
||||
self.current_worker.analysisCompleted.connect(self._on_worker_completed)
|
||||
self.current_worker.analysisError.connect(self.analysisError.emit)
|
||||
|
||||
# Start the background analysis
|
||||
self.current_worker.start()
|
||||
|
||||
def _on_worker_completed(self, file_path: str, result: AnalysisResult):
|
||||
"""Handle completion of worker thread analysis."""
|
||||
# Cache the result
|
||||
self.results_cache[file_path] = result
|
||||
|
||||
# Forward the signal to GUI
|
||||
self.analysisCompleted.emit(file_path, result)
|
||||
|
||||
def get_analysis_figure(self, file_path: str):
|
||||
"""
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Demo script showing the different logging levels available.
|
||||
Usage examples:
|
||||
|
||||
python demo_logging.py --log-level=ERROR
|
||||
python demo_logging.py --log-level=INFO
|
||||
python demo_logging.py --log-level=DEBUG
|
||||
python demo_logging.py --log-level=TRACE --log-file
|
||||
"""
|
||||
|
||||
import sys
|
||||
from logger_setup import setup_logging, parse_log_args
|
||||
|
||||
def main():
|
||||
# Parse logging configuration
|
||||
log_level, log_to_file = parse_log_args()
|
||||
|
||||
# Initialize logging
|
||||
logger = setup_logging(log_level, log_to_file)
|
||||
|
||||
# Demo different log levels
|
||||
print(f"\n=== Audio Mastering Toolkit Logging Demo ===")
|
||||
print(f"Log Level: {log_level}")
|
||||
print(f"Log to File: {log_to_file}")
|
||||
print(f"============================================\n")
|
||||
|
||||
# Test all logging levels
|
||||
logger.error("This is an ERROR message - critical failures only")
|
||||
logger.warning("This is a WARNING message - non-fatal issues")
|
||||
logger.info("This is an INFO message - key operations")
|
||||
logger.debug("This is a DEBUG message - detailed processing steps")
|
||||
logger.trace("This is a TRACE message - granular details")
|
||||
|
||||
print(f"\nDemo complete! Messages above {log_level} level are visible.")
|
||||
if log_to_file:
|
||||
print("Check 'audio_analysis.log' for file output.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
+357
@@ -0,0 +1,357 @@
|
||||
"""
|
||||
Font management system with CJK fallback support.
|
||||
Handles matplotlib and Qt font configuration with licensing-safe approach.
|
||||
"""
|
||||
|
||||
import os
|
||||
import logging
|
||||
import platform
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict, Any
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.font_manager as fm
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
from PyQt5.QtGui import QFontDatabase, QFont
|
||||
|
||||
|
||||
class FontManager:
|
||||
"""
|
||||
Centralized font management for CJK character support.
|
||||
|
||||
Provides licensing-safe font fallback by:
|
||||
1. Loading fonts from local fonts/ directory (gitignored)
|
||||
2. Falling back to system CJK fonts
|
||||
3. Gracefully degrading to default fonts
|
||||
"""
|
||||
|
||||
def __init__(self, fonts_dir: str = "fonts"):
|
||||
"""
|
||||
Initialize font manager.
|
||||
|
||||
Args:
|
||||
fonts_dir: Directory name for custom fonts (relative to project root)
|
||||
"""
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.fonts_dir = Path(__file__).parent / fonts_dir
|
||||
self.loaded_fonts: Dict[str, str] = {}
|
||||
self._matplotlib_configured = False
|
||||
self._qt_configured = False
|
||||
|
||||
# CJK font preferences by platform and type
|
||||
self.system_font_fallbacks = {
|
||||
'Windows': {
|
||||
'serif': ['Yu Mincho', 'MS Mincho', '游明朝', 'MS 明朝'],
|
||||
'sans-serif': ['Yu Gothic UI', 'Meiryo', 'MS Gothic', '游ゴシック', 'メイリオ', 'MS ゴシック'],
|
||||
'monospace': ['MS Gothic', 'MS ゴシック']
|
||||
},
|
||||
'Darwin': { # macOS
|
||||
'serif': ['Hiragino Mincho ProN', 'Yu Mincho', 'Times New Roman'],
|
||||
'sans-serif': ['Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Arial Unicode MS'],
|
||||
'monospace': ['Menlo', 'Monaco', 'Courier New']
|
||||
},
|
||||
'Linux': {
|
||||
'serif': ['Noto Serif CJK JP', 'Source Han Serif', 'DejaVu Serif'],
|
||||
'sans-serif': ['Noto Sans CJK JP', 'Source Han Sans', 'DejaVu Sans'],
|
||||
'monospace': ['Noto Sans Mono CJK JP', 'Source Code Pro', 'DejaVu Sans Mono']
|
||||
}
|
||||
}
|
||||
|
||||
def initialize(self) -> bool:
|
||||
"""
|
||||
Initialize font system for both matplotlib and Qt.
|
||||
|
||||
Returns:
|
||||
bool: True if initialization was successful
|
||||
"""
|
||||
try:
|
||||
self.logger.info("Initializing font management system...")
|
||||
|
||||
# Load custom fonts if available
|
||||
custom_fonts_loaded = self._load_custom_fonts()
|
||||
|
||||
# Configure matplotlib
|
||||
matplotlib_success = self._configure_matplotlib()
|
||||
|
||||
# Configure Qt
|
||||
qt_success = self._configure_qt()
|
||||
|
||||
success = matplotlib_success and qt_success
|
||||
|
||||
if success:
|
||||
self.logger.info(f"Font system initialized successfully. Custom fonts: {custom_fonts_loaded}")
|
||||
else:
|
||||
self.logger.warning("Font system initialized with some issues")
|
||||
|
||||
return success
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Font system initialization failed: {e}")
|
||||
return False
|
||||
|
||||
def _load_custom_fonts(self) -> int:
|
||||
"""
|
||||
Load fonts from the fonts/ directory if it exists.
|
||||
|
||||
Returns:
|
||||
int: Number of custom fonts loaded
|
||||
"""
|
||||
if not self.fonts_dir.exists():
|
||||
self.logger.info(f"Custom fonts directory {self.fonts_dir} not found - using system fonts")
|
||||
return 0
|
||||
|
||||
font_extensions = {'.ttf', '.otf', '.ttc'}
|
||||
fonts_loaded = 0
|
||||
|
||||
try:
|
||||
for font_file in self.fonts_dir.iterdir():
|
||||
if font_file.suffix.lower() in font_extensions:
|
||||
try:
|
||||
# Load for matplotlib
|
||||
fm.fontManager.addfont(str(font_file))
|
||||
|
||||
# Load for Qt
|
||||
font_id = QFontDatabase.addApplicationFont(str(font_file))
|
||||
if font_id != -1:
|
||||
font_families = QFontDatabase.applicationFontFamilies(font_id)
|
||||
for family in font_families:
|
||||
self.loaded_fonts[family] = str(font_file)
|
||||
|
||||
fonts_loaded += 1
|
||||
self.logger.debug(f"Loaded custom font: {font_file.name} -> {font_families}")
|
||||
else:
|
||||
self.logger.warning(f"Failed to load font for Qt: {font_file.name}")
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"Failed to load custom font {font_file.name}: {e}")
|
||||
|
||||
if fonts_loaded > 0:
|
||||
# Clear matplotlib's font cache to recognize new fonts
|
||||
fm.fontManager._load_fontmanager(try_read_cache=False)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error loading custom fonts: {e}")
|
||||
|
||||
return fonts_loaded
|
||||
|
||||
def _configure_matplotlib(self) -> bool:
|
||||
"""Configure matplotlib with appropriate CJK font fallbacks."""
|
||||
try:
|
||||
current_system = platform.system()
|
||||
fallback_fonts = self.system_font_fallbacks.get(current_system,
|
||||
self.system_font_fallbacks['Linux'])
|
||||
|
||||
# Build font list: custom fonts + system fallbacks + matplotlib defaults
|
||||
font_list = []
|
||||
|
||||
# Add custom fonts first (highest priority)
|
||||
font_list.extend(self.loaded_fonts.keys())
|
||||
|
||||
# Add system CJK fonts
|
||||
font_list.extend(fallback_fonts['sans-serif'])
|
||||
|
||||
# Add matplotlib defaults as final fallback
|
||||
font_list.extend(['DejaVu Sans', 'Arial', 'sans-serif'])
|
||||
|
||||
# Update matplotlib configuration
|
||||
plt.rcParams['font.sans-serif'] = font_list
|
||||
plt.rcParams['font.family'] = 'sans-serif'
|
||||
|
||||
# Ensure matplotlib can handle Unicode
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
self.logger.info(f"Matplotlib configured with font list: {font_list[:3]}...")
|
||||
self._matplotlib_configured = True
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Matplotlib font configuration failed: {e}")
|
||||
return False
|
||||
|
||||
def _configure_qt(self) -> bool:
|
||||
"""Configure Qt application with appropriate CJK fonts."""
|
||||
try:
|
||||
app = QCoreApplication.instance()
|
||||
if not app:
|
||||
self.logger.warning("No Qt application instance found - Qt font configuration skipped")
|
||||
return True
|
||||
|
||||
# Get best available CJK font
|
||||
cjk_font = self._get_best_cjk_font()
|
||||
|
||||
if cjk_font:
|
||||
# Set application-wide font
|
||||
font = QFont(cjk_font)
|
||||
app.setFont(font)
|
||||
self.logger.info(f"Qt configured with CJK font: {cjk_font}")
|
||||
else:
|
||||
self.logger.info("Qt using default system font (no specific CJK font found)")
|
||||
|
||||
self._qt_configured = True
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Qt font configuration failed: {e}")
|
||||
return False
|
||||
|
||||
def _get_best_cjk_font(self) -> Optional[str]:
|
||||
"""
|
||||
Find the best available CJK font for the current system.
|
||||
|
||||
Returns:
|
||||
str or None: Name of best available CJK font
|
||||
"""
|
||||
# Check custom fonts first
|
||||
for font_name in self.loaded_fonts.keys():
|
||||
if self._is_cjk_capable(font_name):
|
||||
return font_name
|
||||
|
||||
# Check system fonts
|
||||
current_system = platform.system()
|
||||
system_fonts = self.system_font_fallbacks.get(current_system,
|
||||
self.system_font_fallbacks['Linux'])
|
||||
|
||||
available_fonts = set(fm.get_font_names())
|
||||
for font_name in system_fonts['sans-serif']:
|
||||
if font_name in available_fonts:
|
||||
return font_name
|
||||
|
||||
return None
|
||||
|
||||
def _is_cjk_capable(self, font_name: str) -> bool:
|
||||
"""
|
||||
Check if a font supports CJK characters.
|
||||
Simple heuristic based on font name.
|
||||
"""
|
||||
cjk_indicators = [
|
||||
'cjk', 'japanese', 'chinese', 'korean', 'han', 'noto', 'yu',
|
||||
'hiragino', 'meiryo', 'gothic', 'mincho', '游', 'メイリオ',
|
||||
'ゴシック', '明朝'
|
||||
]
|
||||
font_lower = font_name.lower()
|
||||
return any(indicator in font_lower for indicator in cjk_indicators)
|
||||
|
||||
def get_cjk_safe_title(self, title: str, fallback_encoding: str = 'utf-8') -> str:
|
||||
"""
|
||||
Ensure title string is safe for display with current font configuration.
|
||||
|
||||
Args:
|
||||
title: Original title string
|
||||
fallback_encoding: Encoding to use for problematic characters
|
||||
|
||||
Returns:
|
||||
str: Safe title string for display
|
||||
"""
|
||||
if not title:
|
||||
return title
|
||||
|
||||
try:
|
||||
# Test if the string can be encoded/decoded properly
|
||||
title.encode(fallback_encoding).decode(fallback_encoding)
|
||||
return title
|
||||
except UnicodeError:
|
||||
# If there are encoding issues, create a safe fallback
|
||||
safe_title = title.encode(fallback_encoding, errors='replace').decode(fallback_encoding)
|
||||
self.logger.debug(f"Title encoding adjusted: {title[:50]}... -> {safe_title[:50]}...")
|
||||
return safe_title
|
||||
|
||||
def create_fonts_directory_if_needed(self) -> Path:
|
||||
"""
|
||||
Create the fonts directory and return its path.
|
||||
Useful for setup instructions.
|
||||
|
||||
Returns:
|
||||
Path: Path to the fonts directory
|
||||
"""
|
||||
self.fonts_dir.mkdir(exist_ok=True)
|
||||
return self.fonts_dir
|
||||
|
||||
def get_font_installation_instructions(self) -> str:
|
||||
"""
|
||||
Generate user instructions for installing CJK fonts.
|
||||
|
||||
Returns:
|
||||
str: Multi-line instruction string
|
||||
"""
|
||||
fonts_path = self.create_fonts_directory_if_needed()
|
||||
|
||||
instructions = f"""CJK Font Installation Instructions:
|
||||
|
||||
1. Create or use the fonts directory: {fonts_path.absolute()}
|
||||
|
||||
2. Download CJK fonts (legally) from sources like:
|
||||
- Google Fonts (Noto Sans CJK, free & open source)
|
||||
- Adobe Source Han fonts (free & open source)
|
||||
- System fonts from your OS (if redistribution is allowed)
|
||||
|
||||
3. Place .ttf, .otf, or .ttc font files in the fonts/ directory
|
||||
|
||||
4. Restart the application to load the new fonts
|
||||
|
||||
Note: The fonts/ directory is gitignored to avoid licensing issues.
|
||||
System CJK fonts will be used as fallback if available.
|
||||
|
||||
Current system: {platform.system()}
|
||||
Recommended fonts: {', '.join(self.system_font_fallbacks.get(platform.system(), {}).get('sans-serif', ['System default'])[:3])}
|
||||
"""
|
||||
return instructions
|
||||
|
||||
def get_status_report(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Generate a status report of the font system.
|
||||
|
||||
Returns:
|
||||
dict: Status information
|
||||
"""
|
||||
return {
|
||||
'matplotlib_configured': self._matplotlib_configured,
|
||||
'qt_configured': self._qt_configured,
|
||||
'custom_fonts_loaded': len(self.loaded_fonts),
|
||||
'custom_font_families': list(self.loaded_fonts.keys()),
|
||||
'fonts_directory_exists': self.fonts_dir.exists(),
|
||||
'fonts_directory_path': str(self.fonts_dir.absolute()),
|
||||
'current_matplotlib_fonts': plt.rcParams.get('font.sans-serif', [])[:5],
|
||||
'current_system': platform.system()
|
||||
}
|
||||
|
||||
|
||||
# Global font manager instance
|
||||
_font_manager: Optional[FontManager] = None
|
||||
|
||||
|
||||
def get_font_manager() -> FontManager:
|
||||
"""
|
||||
Get the global font manager instance.
|
||||
Creates one if it doesn't exist.
|
||||
|
||||
Returns:
|
||||
FontManager: Global font manager instance
|
||||
"""
|
||||
global _font_manager
|
||||
if _font_manager is None:
|
||||
_font_manager = FontManager()
|
||||
return _font_manager
|
||||
|
||||
|
||||
def initialize_fonts() -> bool:
|
||||
"""
|
||||
Initialize the global font system.
|
||||
Call this early in application startup.
|
||||
|
||||
Returns:
|
||||
bool: True if successful
|
||||
"""
|
||||
return get_font_manager().initialize()
|
||||
|
||||
|
||||
def safe_title(title: str) -> str:
|
||||
"""
|
||||
Convenience function to get CJK-safe title.
|
||||
|
||||
Args:
|
||||
title: Original title
|
||||
|
||||
Returns:
|
||||
str: Safe title for display
|
||||
"""
|
||||
return get_font_manager().get_cjk_safe_title(title)
|
||||
@@ -0,0 +1,12 @@
|
||||
This directory is for custom CJK fonts to improve character rendering.
|
||||
|
||||
Supported formats: .ttf, .otf, .ttc
|
||||
|
||||
Recommended free fonts for CJK support:
|
||||
- Noto Sans CJK (Google Fonts)
|
||||
- Source Han Sans (Adobe)
|
||||
|
||||
The application will automatically detect and use fonts placed here.
|
||||
For setup instructions, run: python setup_fonts.py
|
||||
|
||||
Note: Only add fonts you have proper licensing rights to distribute.
|
||||
@@ -0,0 +1,95 @@
|
||||
"""
|
||||
Logging configuration for the Audio Mastering Toolkit.
|
||||
Provides CLI configurable logging with different verbosity levels.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def setup_logging(level: str = "INFO", log_to_file: bool = False) -> logging.Logger:
|
||||
"""
|
||||
Setup logging configuration for the application.
|
||||
|
||||
Args:
|
||||
level: Logging level (ERROR, WARN, INFO, DEBUG, TRACE)
|
||||
log_to_file: Whether to also log to file
|
||||
|
||||
Returns:
|
||||
Configured logger instance
|
||||
"""
|
||||
# Convert level string to logging constant
|
||||
level_map = {
|
||||
'ERROR': logging.ERROR,
|
||||
'WARN': logging.WARNING,
|
||||
'WARNING': logging.WARNING,
|
||||
'INFO': logging.INFO,
|
||||
'DEBUG': logging.DEBUG,
|
||||
'TRACE': 5 # Custom level below DEBUG
|
||||
}
|
||||
|
||||
# Add custom TRACE level
|
||||
logging.addLevelName(5, 'TRACE')
|
||||
|
||||
numeric_level = level_map.get(level.upper(), logging.INFO)
|
||||
|
||||
# Create formatter
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
||||
datefmt='%H:%M:%S'
|
||||
)
|
||||
|
||||
# Setup console handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(formatter)
|
||||
console_handler.setLevel(numeric_level)
|
||||
|
||||
# Setup root logger
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(numeric_level)
|
||||
root_logger.handlers.clear() # Clear any existing handlers
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# Optional file logging
|
||||
if log_to_file:
|
||||
file_handler = logging.FileHandler('audio_analysis.log')
|
||||
file_handler.setFormatter(formatter)
|
||||
file_handler.setLevel(numeric_level)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
# Add trace method to all loggers
|
||||
def trace(self, message, *args, **kwargs):
|
||||
if self.isEnabledFor(5):
|
||||
self._log(5, message, args, **kwargs)
|
||||
|
||||
logging.Logger.trace = trace
|
||||
|
||||
# Create main application logger
|
||||
app_logger = logging.getLogger('audio_mastering')
|
||||
app_logger.info(f"Logging initialized at {level.upper()} level")
|
||||
|
||||
return app_logger
|
||||
|
||||
|
||||
def parse_log_args() -> tuple[str, bool]:
|
||||
"""
|
||||
Parse command line arguments for logging configuration.
|
||||
|
||||
Returns:
|
||||
Tuple of (log_level, log_to_file)
|
||||
"""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(add_help=False) # Don't interfere with main arg parsing
|
||||
parser.add_argument('--log-level', '-l',
|
||||
choices=['ERROR', 'WARN', 'INFO', 'DEBUG', 'TRACE'],
|
||||
default='INFO',
|
||||
help='Set logging verbosity level')
|
||||
parser.add_argument('--log-file', action='store_true',
|
||||
help='Also log to audio_analysis.log file')
|
||||
|
||||
# Parse known args only (ignore others for main app)
|
||||
args, _ = parser.parse_known_args()
|
||||
|
||||
return args.log_level, args.log_file
|
||||
@@ -1,5 +1,6 @@
|
||||
import sys
|
||||
import os
|
||||
import logging
|
||||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||||
QHBoxLayout, QSplitter, QLabel, QListWidget,
|
||||
QTextEdit, QListWidgetItem)
|
||||
@@ -7,6 +8,8 @@ from PyQt5.QtCore import Qt
|
||||
|
||||
from audio_visualization_widget import AudioVisualizationWidget
|
||||
from analysis_results_manager import AnalysisResultsManager
|
||||
from logger_setup import setup_logging, parse_log_args
|
||||
from font_manager import initialize_fonts, get_font_manager
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
@@ -14,6 +17,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.logger = logging.getLogger(__name__)
|
||||
self.analysis_manager = AnalysisResultsManager()
|
||||
self.initUI()
|
||||
self.connect_signals()
|
||||
@@ -81,6 +85,7 @@ class MainWindow(QMainWindow):
|
||||
self.analysis_manager.analysisStarted.connect(self.on_analysis_started)
|
||||
self.analysis_manager.analysisCompleted.connect(self.on_analysis_completed)
|
||||
self.analysis_manager.analysisError.connect(self.on_analysis_error)
|
||||
self.analysis_manager.progressUpdate.connect(self.on_progress_update)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
"""Handle drag enter event for file drops."""
|
||||
@@ -99,13 +104,17 @@ class MainWindow(QMainWindow):
|
||||
files = [u.toLocalFile() for u in event.mimeData().urls()]
|
||||
audio_files = [f for f in files if f.lower().endswith(('.mp3', '.wav', '.flac'))]
|
||||
|
||||
self.logger.info(f"Files dropped: {len(files)} total, {len(audio_files)} audio files")
|
||||
|
||||
if audio_files:
|
||||
# Analyze the first audio file
|
||||
# TODO: Add support for multiple file queue
|
||||
file_path = audio_files[0]
|
||||
self.logger.info(f"Starting analysis of dropped file: {os.path.basename(file_path)}")
|
||||
self.analysis_manager.analyze_file(file_path)
|
||||
else:
|
||||
self.visualization_widget.set_status("No audio files detected in drop")
|
||||
self.logger.warning("No supported audio files found in drop")
|
||||
|
||||
def on_analysis_started(self, file_path):
|
||||
"""Called when analysis starts."""
|
||||
@@ -143,8 +152,14 @@ class MainWindow(QMainWindow):
|
||||
def on_analysis_error(self, file_path, error_message):
|
||||
"""Called when analysis fails."""
|
||||
filename = os.path.basename(file_path)
|
||||
self.logger.error(f"Analysis failed for {filename}: {error_message}")
|
||||
self.visualization_widget.set_status(f"Error analyzing {filename}: {error_message}")
|
||||
|
||||
def on_progress_update(self, message, percentage):
|
||||
"""Called when analysis progress updates."""
|
||||
self.logger.debug(f"Progress: {message} ({percentage}%)")
|
||||
self.visualization_widget.set_status(f"{message} ({percentage}%)")
|
||||
|
||||
def on_file_selected(self, item):
|
||||
"""Called when a file is selected from the list."""
|
||||
file_path = item.data(Qt.UserRole)
|
||||
@@ -160,12 +175,34 @@ class MainWindow(QMainWindow):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Parse logging arguments before creating QApplication
|
||||
log_level, log_to_file = parse_log_args()
|
||||
|
||||
# Initialize logging
|
||||
logger = setup_logging(log_level, log_to_file)
|
||||
logger.info("Starting Audio Mastering Analysis Toolkit")
|
||||
logger.info(f"Command line args: log-level={log_level}, log-file={log_to_file}")
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
|
||||
# Initialize font system before creating any widgets
|
||||
font_success = initialize_fonts()
|
||||
if font_success:
|
||||
logger.info("Font system initialized successfully")
|
||||
# Log font status for debugging
|
||||
font_status = get_font_manager().get_status_report()
|
||||
logger.debug(f"Font status: matplotlib={font_status['matplotlib_configured']}, "
|
||||
f"qt={font_status['qt_configured']}, "
|
||||
f"custom_fonts={font_status['custom_fonts_loaded']}")
|
||||
else:
|
||||
logger.warning("Font system initialization failed - CJK characters may not display properly")
|
||||
|
||||
# Set application style
|
||||
app.setStyle('Fusion') # Modern cross-platform style
|
||||
logger.debug("Application style set to Fusion")
|
||||
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
logger.info("GUI window displayed")
|
||||
|
||||
sys.exit(app.exec_())
|
||||
+14
-7
@@ -8,6 +8,7 @@ import matplotlib.cm as cm
|
||||
|
||||
from mutagen.mp3 import MP3
|
||||
from mutagen.easyid3 import EasyID3
|
||||
from font_manager import safe_title, initialize_fonts
|
||||
|
||||
def try_mp3_tags(file_path):
|
||||
try:
|
||||
@@ -20,19 +21,19 @@ def try_mp3_tags(file_path):
|
||||
|
||||
def read_mp3_tags(file_path):
|
||||
if (audio := try_mp3_tags(file_path)) is not None:
|
||||
print(f"File name: {os.path.basename(file_path)}")
|
||||
print(f"{audio['artist'][0]} - {audio['title'][0]}")
|
||||
print(f"File name: {safe_title(os.path.basename(file_path))}")
|
||||
print(f"{safe_title(audio['artist'][0])} - {safe_title(audio['title'][0])}")
|
||||
else:
|
||||
print(f"File name: {os.path.basename(file_path)}")
|
||||
print(f"File name: {safe_title(os.path.basename(file_path))}")
|
||||
|
||||
class AudioFile:
|
||||
def __init__(self, file_path):
|
||||
self.file_path = file_path
|
||||
# file name / song name
|
||||
if (audio := try_mp3_tags(self.file_path)) is not None:
|
||||
self.song_name = f"{audio['artist'][0]} - {audio['title'][0]}"
|
||||
self.song_name = safe_title(f"{audio['artist'][0]} - {audio['title'][0]}")
|
||||
else:
|
||||
self.song_name = os.path.basename(self.file_path)
|
||||
self.song_name = safe_title(os.path.basename(self.file_path))
|
||||
|
||||
self.y, self.sr = librosa.load(file_path)
|
||||
# load automatically normalises everything to [-1.0, 1.0]
|
||||
@@ -49,7 +50,10 @@ class AudioFile:
|
||||
return self.max_amplitude, self.avg_amplitude
|
||||
|
||||
def get_bpm(self):
|
||||
return self.bpm
|
||||
# librosa.beat.beat_track returns numpy array - extract scalar value
|
||||
if isinstance(self.bpm, np.ndarray):
|
||||
return float(self.bpm[0]) if len(self.bpm) > 0 else 0.0
|
||||
return float(self.bpm)
|
||||
|
||||
def get_energy_levels_over_time(self, window = 10, hop = 2):
|
||||
"""_summary_
|
||||
@@ -121,7 +125,7 @@ class AudioFile:
|
||||
|
||||
ax.set_ylabel('Power')
|
||||
ax.set_xlabel('Time')
|
||||
ax.set_title(f'{os.path.basename(self.file_path)}')
|
||||
ax.set_title(safe_title(os.path.basename(self.file_path)))
|
||||
|
||||
plt.show(block=False)
|
||||
plt.pause(0.001)
|
||||
@@ -216,6 +220,9 @@ if __name__ == '__main__':
|
||||
print("For the new GUI interface, please run: python main.py")
|
||||
print()
|
||||
|
||||
# Initialize fonts for matplotlib
|
||||
initialize_fonts()
|
||||
|
||||
# Replace 'path/to/your/audiofile.mp3' with the path to your audio file
|
||||
file_path = []
|
||||
with open('./files.txt', 'r') as f:
|
||||
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "uj-mastering-master",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
+3
-2
@@ -9,6 +9,7 @@ import matplotlib.colors as mcolors
|
||||
import matplotlib.cm as cm
|
||||
from matplotlib.figure import Figure
|
||||
import os
|
||||
from font_manager import safe_title
|
||||
|
||||
|
||||
class PlottingEngine:
|
||||
@@ -58,7 +59,7 @@ class PlottingEngine:
|
||||
# Labels and title
|
||||
ax.set_ylabel('Power')
|
||||
ax.set_xlabel('Time (seconds)')
|
||||
ax.set_title(f'{os.path.basename(file_path)}')
|
||||
ax.set_title(safe_title(os.path.basename(file_path)))
|
||||
|
||||
# Tight layout for better appearance in GUI
|
||||
fig.tight_layout()
|
||||
@@ -73,7 +74,7 @@ class PlottingEngine:
|
||||
Returns:
|
||||
str: Formatted metadata text
|
||||
"""
|
||||
return f"""Track: {song_name}
|
||||
return f"""Track: {safe_title(song_name)}
|
||||
BPM: {bpm:.1f}
|
||||
Max Amplitude: {max_amplitude:.3f}
|
||||
Avg Amplitude: {avg_amplitude:.3f}"""
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Font setup utility for CJK character support.
|
||||
Provides installation instructions and system font detection.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import platform
|
||||
from pathlib import Path
|
||||
|
||||
# Add current directory to path to import our modules
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
|
||||
from font_manager import get_font_manager
|
||||
|
||||
|
||||
def main():
|
||||
"""Main font setup utility."""
|
||||
print("=== Audio Analysis Toolkit - CJK Font Setup ===\n")
|
||||
|
||||
# Get font manager and show current status
|
||||
font_manager = get_font_manager()
|
||||
|
||||
print("Current System Information:")
|
||||
print(f"Platform: {platform.system()} {platform.release()}")
|
||||
print(f"Python: {platform.python_version()}\n")
|
||||
|
||||
# Try to initialize fonts
|
||||
print("Initializing font system...")
|
||||
success = font_manager.initialize()
|
||||
|
||||
# Show detailed status
|
||||
status = font_manager.get_status_report()
|
||||
print(f"Font system status: {'✓ OK' if success else '⚠ Issues detected'}")
|
||||
print(f"Matplotlib configured: {'✓' if status['matplotlib_configured'] else '✗'}")
|
||||
print(f"Qt configured: {'✓' if status['qt_configured'] else '✗'}")
|
||||
print(f"Custom fonts loaded: {status['custom_fonts_loaded']}")
|
||||
|
||||
if status['custom_font_families']:
|
||||
print(f"Custom font families: {', '.join(status['custom_font_families'])}")
|
||||
|
||||
print(f"Fonts directory: {status['fonts_directory_path']}")
|
||||
print(f"Directory exists: {'✓' if status['fonts_directory_exists'] else '✗'}")
|
||||
print()
|
||||
|
||||
# Show current matplotlib font configuration
|
||||
print("Current matplotlib font stack:")
|
||||
for i, font in enumerate(status['current_matplotlib_fonts'][:8], 1):
|
||||
print(f" {i}. {font}")
|
||||
print()
|
||||
|
||||
# Show installation instructions
|
||||
print(font_manager.get_font_installation_instructions())
|
||||
|
||||
# Test CJK character handling
|
||||
print("\n=== Testing CJK Character Handling ===")
|
||||
test_strings = [
|
||||
"English Title",
|
||||
"日本語のタイトル", # Japanese
|
||||
"中文标题", # Chinese
|
||||
"한국어 제목", # Korean
|
||||
"Test - テスト", # Mixed
|
||||
]
|
||||
|
||||
print("Testing font-safe title conversion:")
|
||||
for test_str in test_strings:
|
||||
safe_str = font_manager.get_cjk_safe_title(test_str)
|
||||
status_indicator = "✓" if test_str == safe_str else "⚠"
|
||||
print(f" {status_indicator} '{test_str}' -> '{safe_str}'")
|
||||
|
||||
print("\n=== Setup Complete ===")
|
||||
if success:
|
||||
print("Font system is ready for use!")
|
||||
if status['custom_fonts_loaded'] == 0:
|
||||
print("Consider adding CJK fonts to improve character display.")
|
||||
else:
|
||||
print("There were issues with font setup. Check the logs for details.")
|
||||
print("The application will still work but CJK characters may not display correctly.")
|
||||
|
||||
return 0 if success else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user