286 lines
8.7 KiB
C#
286 lines
8.7 KiB
C#
|
|
using Robovoice.Core.Voices;
|
||
|
|
using System.ComponentModel;
|
||
|
|
|
||
|
|
namespace Robovoice.App;
|
||
|
|
|
||
|
|
internal sealed partial class VoiceManagerForm : Form
|
||
|
|
{
|
||
|
|
private readonly string _voicesDir;
|
||
|
|
private readonly VoiceCatalogue _catalogue;
|
||
|
|
private IReadOnlyList<VoiceInfo> _allVoices = Array.Empty<VoiceInfo>();
|
||
|
|
private HashSet<string> _installedKeys = new();
|
||
|
|
private bool _catalogueLoaded;
|
||
|
|
|
||
|
|
public string? SelectedVoiceKey { get; private set; }
|
||
|
|
|
||
|
|
public VoiceManagerForm(string voicesDir, string? currentVoiceKey)
|
||
|
|
{
|
||
|
|
InitializeComponent();
|
||
|
|
_voicesDir = voicesDir;
|
||
|
|
_catalogue = new VoiceCatalogue();
|
||
|
|
SelectedVoiceKey = currentVoiceKey;
|
||
|
|
|
||
|
|
txtFilter.TextChanged += (_, _) => ApplyFilter();
|
||
|
|
btnDownload.Click += OnDownload;
|
||
|
|
btnRemove.Click += OnRemove;
|
||
|
|
btnClose.Click += (_, _) => Close();
|
||
|
|
gridVoices.SelectionChanged += OnSelectionChanged;
|
||
|
|
Load += OnLoad;
|
||
|
|
FormClosing += OnFormClosing;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async void OnLoad(object? sender, EventArgs e)
|
||
|
|
{
|
||
|
|
btnDownload.Enabled = false;
|
||
|
|
btnRemove.Enabled = false;
|
||
|
|
|
||
|
|
RefreshInstalled();
|
||
|
|
lblStatus.Text = "Fetching voice catalogue...";
|
||
|
|
progressBar.Visible = true;
|
||
|
|
progressBar.Style = ProgressBarStyle.Marquee;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
_allVoices = await _catalogue.GetCatalogueAsync();
|
||
|
|
_catalogueLoaded = true;
|
||
|
|
PopulateGrid();
|
||
|
|
lblStatus.Text = $"{_allVoices.Count} voices available, {_installedKeys.Count} installed";
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
lblStatus.Text = $"Failed to fetch catalogue: {ex.Message}";
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
progressBar.Visible = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void RefreshInstalled()
|
||
|
|
{
|
||
|
|
_installedKeys = VoiceCatalogue
|
||
|
|
.GetInstalledVoices(_voicesDir)
|
||
|
|
.ToHashSet();
|
||
|
|
}
|
||
|
|
|
||
|
|
private void PopulateGrid()
|
||
|
|
{
|
||
|
|
gridVoices.Rows.Clear();
|
||
|
|
gridVoices.Columns.Clear();
|
||
|
|
|
||
|
|
gridVoices.Columns.Add(new DataGridViewTextBoxColumn
|
||
|
|
{
|
||
|
|
Name = "Key",
|
||
|
|
HeaderText = "Key",
|
||
|
|
FillWeight = 40,
|
||
|
|
});
|
||
|
|
gridVoices.Columns.Add(new DataGridViewTextBoxColumn
|
||
|
|
{
|
||
|
|
Name = "Language",
|
||
|
|
HeaderText = "Language",
|
||
|
|
FillWeight = 25,
|
||
|
|
});
|
||
|
|
gridVoices.Columns.Add(new DataGridViewTextBoxColumn
|
||
|
|
{
|
||
|
|
Name = "Voice",
|
||
|
|
HeaderText = "Voice",
|
||
|
|
FillWeight = 20,
|
||
|
|
});
|
||
|
|
gridVoices.Columns.Add(new DataGridViewTextBoxColumn
|
||
|
|
{
|
||
|
|
Name = "Quality",
|
||
|
|
HeaderText = "Quality",
|
||
|
|
FillWeight = 15,
|
||
|
|
});
|
||
|
|
gridVoices.Columns.Add(new DataGridViewTextBoxColumn
|
||
|
|
{
|
||
|
|
Name = "Size",
|
||
|
|
HeaderText = "Size",
|
||
|
|
FillWeight = 15,
|
||
|
|
});
|
||
|
|
gridVoices.Columns.Add(new DataGridViewTextBoxColumn
|
||
|
|
{
|
||
|
|
Name = "Status",
|
||
|
|
HeaderText = "Status",
|
||
|
|
FillWeight = 15,
|
||
|
|
});
|
||
|
|
|
||
|
|
foreach (var voice in _allVoices)
|
||
|
|
{
|
||
|
|
bool installed = _installedKeys.Contains(voice.Key);
|
||
|
|
int rowIdx = gridVoices.Rows.Add(
|
||
|
|
voice.Key,
|
||
|
|
voice.Language.NameEnglish,
|
||
|
|
voice.Name,
|
||
|
|
voice.Quality,
|
||
|
|
voice.SizeDisplay,
|
||
|
|
installed ? "Installed" : "Available");
|
||
|
|
|
||
|
|
var row = gridVoices.Rows[rowIdx];
|
||
|
|
row.Tag = voice.Key;
|
||
|
|
if (installed)
|
||
|
|
{
|
||
|
|
row.DefaultCellStyle.BackColor = Color.FromArgb(235, 245, 235);
|
||
|
|
if (voice.Key == SelectedVoiceKey)
|
||
|
|
{
|
||
|
|
row.DefaultCellStyle.Font = new Font(gridVoices.Font, FontStyle.Bold);
|
||
|
|
row.Selected = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
row.DefaultCellStyle.ForeColor = Color.Gray;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (gridVoices.SelectedRows.Count == 0 && gridVoices.Rows.Count > 0)
|
||
|
|
gridVoices.Rows[0].Selected = true;
|
||
|
|
}
|
||
|
|
|
||
|
|
private void ApplyFilter()
|
||
|
|
{
|
||
|
|
string filter = txtFilter.Text.Trim().ToLowerInvariant();
|
||
|
|
if (string.IsNullOrEmpty(filter))
|
||
|
|
{
|
||
|
|
foreach (DataGridViewRow row in gridVoices.Rows)
|
||
|
|
row.Visible = true;
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach (DataGridViewRow row in gridVoices.Rows)
|
||
|
|
{
|
||
|
|
if (row.Tag is not string key)
|
||
|
|
{
|
||
|
|
row.Visible = false;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
var voice = _allVoices.FirstOrDefault(v => v.Key == key);
|
||
|
|
if (voice is null)
|
||
|
|
{
|
||
|
|
row.Visible = false;
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
string haystack = $"{key} {voice.Name} {voice.Language.NameEnglish} {voice.Language.Code} {voice.Language.Family} {voice.Quality}".ToLowerInvariant();
|
||
|
|
row.Visible = haystack.Contains(filter);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnSelectionChanged(object? sender, EventArgs e)
|
||
|
|
{
|
||
|
|
if (gridVoices.SelectedRows.Count == 0) return;
|
||
|
|
var row = gridVoices.SelectedRows[0];
|
||
|
|
if (row.Tag is not string key) return;
|
||
|
|
|
||
|
|
bool installed = _installedKeys.Contains(key);
|
||
|
|
btnDownload.Enabled = !installed && _catalogueLoaded;
|
||
|
|
btnRemove.Enabled = installed;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async void OnDownload(object? sender, EventArgs e)
|
||
|
|
{
|
||
|
|
if (gridVoices.SelectedRows.Count == 0) return;
|
||
|
|
var row = gridVoices.SelectedRows[0];
|
||
|
|
if (row.Tag is not string key) return;
|
||
|
|
|
||
|
|
var voice = _allVoices.FirstOrDefault(v => v.Key == key);
|
||
|
|
if (voice is null) return;
|
||
|
|
|
||
|
|
btnDownload.Enabled = false;
|
||
|
|
btnRemove.Enabled = false;
|
||
|
|
progressBar.Visible = true;
|
||
|
|
progressBar.Style = ProgressBarStyle.Continuous;
|
||
|
|
progressBar.Value = 0;
|
||
|
|
lblStatus.Text = $"Downloading {key} ({voice.SizeDisplay})...";
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var progress = new Progress<(long downloaded, long total)>(p =>
|
||
|
|
{
|
||
|
|
if (p.total > 0)
|
||
|
|
{
|
||
|
|
progressBar.Value = (int)(p.downloaded * 100 / p.total);
|
||
|
|
lblStatus.Text = $"Downloading {key}... {p.downloaded / (1024 * 1024)} / {p.total / (1024 * 1024)} MB";
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
await _catalogue.DownloadVoiceAsync(voice, _voicesDir, progress);
|
||
|
|
RefreshInstalled();
|
||
|
|
UpdateRowStatus(key, installed: true);
|
||
|
|
lblStatus.Text = $"Downloaded {key} successfully.";
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
lblStatus.Text = $"Download failed: {ex.Message}";
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
progressBar.Visible = false;
|
||
|
|
OnSelectionChanged(null, EventArgs.Empty);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnRemove(object? sender, EventArgs e)
|
||
|
|
{
|
||
|
|
if (gridVoices.SelectedRows.Count == 0) return;
|
||
|
|
var row = gridVoices.SelectedRows[0];
|
||
|
|
if (row.Tag is not string key) return;
|
||
|
|
|
||
|
|
var dlgResult = MessageBox.Show(
|
||
|
|
$"Remove voice '{key}'?\nThis will delete the .onnx and .onnx.json files.",
|
||
|
|
"Confirm Remove",
|
||
|
|
MessageBoxButtons.YesNo,
|
||
|
|
MessageBoxIcon.Question);
|
||
|
|
|
||
|
|
if (dlgResult != DialogResult.Yes) return;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
VoiceCatalogue.RemoveVoice(_voicesDir, key);
|
||
|
|
RefreshInstalled();
|
||
|
|
UpdateRowStatus(key, installed: false);
|
||
|
|
if (SelectedVoiceKey == key)
|
||
|
|
SelectedVoiceKey = null;
|
||
|
|
lblStatus.Text = $"Removed {key}.";
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
lblStatus.Text = $"Remove failed: {ex.Message}";
|
||
|
|
}
|
||
|
|
|
||
|
|
OnSelectionChanged(null, EventArgs.Empty);
|
||
|
|
}
|
||
|
|
|
||
|
|
private void UpdateRowStatus(string key, bool installed)
|
||
|
|
{
|
||
|
|
foreach (DataGridViewRow row in gridVoices.Rows)
|
||
|
|
{
|
||
|
|
if (row.Tag is string rowKey && rowKey == key)
|
||
|
|
{
|
||
|
|
row.Cells["Status"].Value = installed ? "Installed" : "Available";
|
||
|
|
row.DefaultCellStyle.BackColor = installed
|
||
|
|
? Color.FromArgb(235, 245, 235)
|
||
|
|
: Color.White;
|
||
|
|
row.DefaultCellStyle.ForeColor = installed
|
||
|
|
? Color.Black
|
||
|
|
: Color.Gray;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
lblStatus.Text = $"{_allVoices.Count} voices available, {_installedKeys.Count} installed";
|
||
|
|
}
|
||
|
|
|
||
|
|
private void OnFormClosing(object? sender, FormClosingEventArgs e)
|
||
|
|
{
|
||
|
|
if (gridVoices.SelectedRows.Count > 0 && gridVoices.SelectedRows[0].Tag is string key)
|
||
|
|
{
|
||
|
|
if (_installedKeys.Contains(key))
|
||
|
|
SelectedVoiceKey = key;
|
||
|
|
}
|
||
|
|
|
||
|
|
_catalogue.DisposeAsync().AsTask().Wait();
|
||
|
|
}
|
||
|
|
}
|