Files
diodiogod-TTS-Audio-Suite/web/audio_analyzer_regions.js
diodiogod 7b44654250 Rename Audio Analyzer to Audio Wave Analyzer and fix F5-TTS nfe_step bug
- Renamed '🎵 Audio Analyzer' to '🌊 Audio Wave Analyzer' throughout project
- Updated all node display names, tooltips, documentation, and web interface
- Preserved class names and internal identifiers for ComfyUI compatibility
- Fixed nfe_step bug in F5-TTS nodes: limited max value to 71 to prevent ODE solver issues
- Added validation clamping and warning messages in F5-TTS Node and F5-TTS Edit Node
- Updated tooltips to warn about ODE solver issues above 71
2025-07-13 01:02:08 -03:00

191 lines
8.7 KiB
JavaScript

/**
* Audio Wave Analyzer Regions Module
* Handles drawing and management of timing regions (manual, auto-detected, grouped)
*/
export class AudioAnalyzerRegions {
constructor(core) {
this.core = core;
}
drawSelectedRegions(ctx, width, height) {
if (!this.core.selectedRegions || this.core.selectedRegions.length === 0) return;
this.core.selectedRegions.forEach((region, index) => {
const startX = this.core.timeToPixel(region.start);
const endX = this.core.timeToPixel(region.end);
if (endX >= 0 && startX <= width) {
// Choose color based on state
let fillColor = this.core.colors.region;
let strokeColor = '#00ff00';
let lineWidth = 2;
if (this.core.selectedRegionIndices.includes(index)) {
// Selected for deletion (multiple selection)
fillColor = this.core.colors.regionSelected;
strokeColor = '#ff8c00';
lineWidth = 3;
} else if (index === this.core.highlightedRegionIndex) {
// Highlighted (click-to-highlight, persists)
fillColor = this.core.colors.regionHovered;
strokeColor = '#00ff00';
lineWidth = 2;
}
// Draw region background
ctx.fillStyle = fillColor;
ctx.fillRect(Math.max(0, startX), 0, Math.min(width, endX) - Math.max(0, startX), height);
// Draw region borders
ctx.strokeStyle = strokeColor;
ctx.lineWidth = lineWidth;
ctx.beginPath();
if (startX >= 0 && startX <= width) {
ctx.moveTo(startX, 0);
ctx.lineTo(startX, height);
}
if (endX >= 0 && endX <= width) {
ctx.moveTo(endX, 0);
ctx.lineTo(endX, height);
}
ctx.stroke();
// Draw region label with number
const labelX = Math.max(5, Math.min(width - 80, startX + 5));
ctx.fillStyle = strokeColor;
ctx.font = this.core.selectedRegionIndices.includes(index) ? 'bold 12px Arial' : '11px Arial';
ctx.textAlign = 'left';
const labelText = `${index + 1}. ${region.label}`;
ctx.fillText(labelText, labelX, 20 + (index * 15));
// Show deletion hint for selected regions
if (this.core.selectedRegionIndices.includes(index)) {
ctx.fillStyle = '#ff8c00';
ctx.font = '10px Arial';
const hint = this.core.selectedRegionIndices.length > 1 ?
`(${this.core.selectedRegionIndices.length} selected - Delete to remove)` :
'(Press Delete to remove)';
ctx.fillText(hint, labelX, 35 + (index * 15));
}
}
});
}
drawAnalysisResults(ctx, width, height) {
if (!this.core.waveformData || !this.core.waveformData.regions) return;
const regions = this.core.waveformData.regions;
// Draw detected regions with different colors
regions.forEach(region => {
const startX = this.core.timeToPixel(region.start);
const endX = this.core.timeToPixel(region.end);
if (endX >= 0 && startX <= width) {
// Get color based on region type
const color = this.getRegionColor(region);
// Draw region background
ctx.fillStyle = color;
ctx.fillRect(Math.max(0, startX), 0, Math.min(width, endX) - Math.max(0, startX), height);
// Draw region border
ctx.strokeStyle = color.replace('0.2', '0.8'); // More opaque border
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(startX, 0);
ctx.lineTo(startX, height);
ctx.moveTo(endX, 0);
ctx.lineTo(endX, height);
ctx.stroke();
// Draw region label with smart positioning
if (startX >= 0 && startX + 50 <= width) {
this.drawAutoRegionLabel(ctx, region, startX, width, height);
}
}
});
}
getRegionColor(region) {
// Color based on region label/type
let color = 'rgba(0, 255, 0, 0.2)'; // Default green
// Check for grouped regions first - use original type for color
if (region.metadata && region.metadata.type === 'grouped' && region.metadata.original_type) {
// Use the original type to determine color for grouped regions
const originalType = region.metadata.original_type;
if (originalType === 'peak') {
color = 'rgba(0, 150, 255, 0.25)'; // Blue/cyan for grouped peaks
} else if (originalType === 'silence') {
color = 'rgba(128, 128, 128, 0.3)'; // Gray for grouped silence
} else if (originalType === 'word_boundary') {
color = 'rgba(255, 255, 0, 0.2)'; // Yellow for grouped word boundaries
} else if (originalType === 'speech') {
color = 'rgba(34, 139, 34, 0.3)'; // Forest green for grouped speech
}
} else if (region.label === 'silence') {
color = 'rgba(128, 128, 128, 0.3)'; // Gray for silence
} else if (region.label === 'speech') {
color = 'rgba(34, 139, 34, 0.3)'; // Forest green for speech (inverted silence)
} else if (region.label.includes('word_boundary')) {
color = 'rgba(255, 255, 0, 0.2)'; // Yellow for word boundaries
} else if (region.label.includes('peak_')) {
color = 'rgba(0, 150, 255, 0.25)'; // Blue/cyan for detected peaks
} else if (region.label.includes('speech')) {
color = 'rgba(0, 255, 0, 0.2)'; // Green for other speech types
}
return color;
}
// Smart labeling for auto-detected regions
drawAutoRegionLabel(ctx, region, startX, width, height) {
const isGrouped = region.metadata && region.metadata.type === 'grouped';
// Apply density check to ALL auto-detected regions (both individual and grouped)
if (!this.shouldShowAutoLabels(width)) {
return; // Hide all auto-region labels when too dense - only show when zoomed in
}
// All auto regions use the same lower position (no stacking needed)
const labelY = 30; // Lower than timeline numbers (which are at y=12)
// Draw the label
ctx.fillStyle = '#fff';
ctx.font = '10px Arial';
ctx.textAlign = 'left';
const labelText = isGrouped ?
`${region.label}` : // Grouped: simpler label without confidence
`${region.label} (${region.confidence.toFixed(2)})`; // Individual: with confidence
ctx.fillText(labelText, startX + 2, labelY);
}
// Check if auto labels should be shown based on zoom/density
shouldShowAutoLabels(width) {
if (!this.core.waveformData || !this.core.waveformData.regions) return false;
const regions = this.core.waveformData.regions;
// Include ALL auto-detected regions (both individual and grouped) in density calculation
const autoRegions = regions.filter(r =>
!r.label.startsWith('Region') // Not manual (manual regions start with "Region")
);
// Calculate average pixels per region in visible area
const visibleDuration = this.core.waveformData.duration / this.core.zoomLevel;
const autoRegionsInView = autoRegions.filter(r => {
const startTime = this.core.scrollOffset;
const endTime = startTime + visibleDuration;
return r.start < endTime && r.end > startTime;
});
if (autoRegionsInView.length === 0) return false;
const pixelsPerRegion = width / autoRegionsInView.length;
// Show labels only if there's enough space (at least 80px per region)
return pixelsPerRegion >= 80;
}
}