fix #11 #12 update readme

This commit is contained in:
Zhe Fang
2025-07-09 09:19:48 -04:00
parent 90d2055dff
commit 806f3fdd63
12 changed files with 196 additions and 121 deletions

View File

@@ -43,6 +43,7 @@
<PackageReference Include="Dubya.WindowsMediaController" Version="2.5.5" />
<PackageReference Include="H.NotifyIcon.WinUI" Version="2.3.0" />
<PackageReference Include="iTunesSearch" Version="1.0.44" />
<PackageReference Include="LanguageDetection.NETStandard" Version="1.3.1" />
<PackageReference Include="Lyricify.Lyrics.Helper-NativeAot" Version="0.1.4-alpha.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.6" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.6" />

View File

@@ -19,8 +19,11 @@ namespace BetterLyrics.WinUI3.Enums
if (string.IsNullOrWhiteSpace(content))
return null;
// TTML
if (content.StartsWith("<?xml") && System.Text.RegularExpressions.Regex.IsMatch(content, @"<tt(:\w+)?\b"))
// TTML: 检查 <tt ... xmlns="http://www.w3.org/ns/ttml"
if (System.Text.RegularExpressions.Regex.IsMatch(
content,
@"<tt\b[^>]*\bxmlns\s*=\s*[""']http://www\.w3\.org/ns/ttml[""']",
System.Text.RegularExpressions.RegexOptions.IgnoreCase))
{
return LyricsFormat.Ttml;
}

View File

@@ -0,0 +1,48 @@
using LanguageDetection;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace BetterLyrics.WinUI3.Services
{
public class LanguageDetectionHelper
{
private static readonly LanguageDetector _detector = new();
static LanguageDetectionHelper()
{
_detector.AddAllLanguages();
}
private static string? ThreeLetterToTwoLetter(string threeLetterCode)
{
foreach (var ci in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
if (string.Equals(ci.ThreeLetterISOLanguageName, threeLetterCode, StringComparison.OrdinalIgnoreCase))
{
return ci.TwoLetterISOLanguageName;
}
}
return null;
}
public static string? DetectLanguageCode(string? text)
{
if (text == null) return null;
return ThreeLetterToTwoLetter(_detector.Detect(text));
}
public static bool IsCJK(string text)
{
return DetectLanguageCode(text) switch
{
"zh" or "ja" or "ko" => true,
_ => false
};
}
}
}

View File

@@ -2,6 +2,7 @@
using BetterLyrics.WinUI3.Enums;
using BetterLyrics.WinUI3.Models;
using BetterLyrics.WinUI3.Services;
using Lyricify.Lyrics.Models;
using System;
using System.Collections.Generic;
@@ -167,7 +168,8 @@ namespace BetterLyrics.WinUI3.Helper
{
try
{
List<LyricsLine> singleLangLyricsLine = [];
List<LyricsLine> originalLines = [];
List<LyricsLine> translationLines = [];
var xdoc = XDocument.Parse(raw);
var body = xdoc.Descendants().FirstOrDefault(e => e.Name.LocalName == "body");
if (body == null) return;
@@ -176,53 +178,90 @@ namespace BetterLyrics.WinUI3.Helper
{
// 句级时间
string? pBegin = p.Attribute("begin")?.Value;
string? pEnd = p.Attribute("end")?.Value;
int pStartMs = ParseTtmlTime(pBegin);
int pEndMs = ParseTtmlTime(pEnd);
// 处理分词分时
var spans = p.Elements().Where(s => s.Name.LocalName == "span").ToList();
// 只获取一级span且排除ttm:role="x-bg"的span
var spans = p.Elements()
.Where(s => s.Name.LocalName == "span" &&
s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-bg")
.ToList();
string text = string.Concat(spans.Select(s => s.Value));
var charTimings = new List<CharTiming>();
// 原文和翻译分离
var originalTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value != "x-translation")
.ToList();
var translationTextSpans = spans
.Where(s => s.Attribute(XName.Get("role", "http://www.w3.org/ns/ttml#metadata"))?.Value == "x-translation")
.ToList();
int startIndex = 0;
for (int i = 0; i < spans.Count; i++)
// 原文(非 CJK 语言添加空格)
string originalText = string.Concat(originalTextSpans.Select(s => s.Value));
if (!LanguageDetectionHelper.IsCJK(originalText))
{
var span = spans[i];
string? sBegin = span.Attribute("begin")?.Value;
string? sEnd = span.Attribute("end")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
int sEndMs = ParseTtmlTime(sEnd);
if (sStartMs == 0 && sEndMs == 0)
continue;
if (sEndMs == 0)
sEndMs =
(i + 1 < spans.Count)
? ParseTtmlTime(spans[i + 1].Attribute("begin")?.Value)
: pEndMs;
charTimings.Add(new CharTiming { StartMs = sStartMs, EndMs = 0, StartIndex = startIndex, Text = span.Value });
startIndex += span.Value.Length;
foreach (var span in originalTextSpans)
{
span.Value += " ";
}
originalText = string.Concat(originalTextSpans.Select(s => s.Value));
}
if (spans.Count == 0)
text = p.Value;
var originalCharTimings = new List<CharTiming>();
int originalStartIndex = 0;
foreach (var span in originalTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
originalCharTimings.Add(new CharTiming
{
StartMs = sStartMs,
EndMs = 0,
StartIndex = originalStartIndex,
Text = span.Value
});
originalStartIndex += span.Value.Length;
}
if (originalTextSpans.Count == 0)
originalText = p.Value;
singleLangLyricsLine.Add(
new LyricsLine
originalLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = 0,
OriginalText = originalText,
CharTimings = originalCharTimings,
});
// 翻译
string translationText = string.Concat(translationTextSpans.Select(s => s.Value));
var translationCharTimings = new List<CharTiming>();
int translationStartIndex = 0;
foreach (var span in translationTextSpans)
{
string? sBegin = span.Attribute("begin")?.Value;
int sStartMs = ParseTtmlTime(sBegin);
translationCharTimings.Add(new CharTiming
{
StartMs = sStartMs,
EndMs = 0,
StartIndex = translationStartIndex,
Text = span.Value
});
translationStartIndex += span.Value.Length;
}
if (translationTextSpans.Count > 0)
{
translationLines.Add(new LyricsLine
{
StartMs = pStartMs,
EndMs = 0,
OriginalText = text,
CharTimings = charTimings,
}
);
OriginalText = translationText,
CharTimings = translationCharTimings,
});
}
}
_multiLangLyricsLines.Add(singleLangLyricsLine);
_multiLangLyricsLines.Add(originalLines);
if (translationLines.Count > 0)
_multiLangLyricsLines.Add(translationLines);
}
catch
{

View File

@@ -9,6 +9,6 @@ namespace BetterLyrics.WinUI3.Services
{
public interface ILibreTranslateService
{
Task<string> TranslateAsync(string text, CancellationToken? token);
Task<string> TranslateAsync(string text, string targetLangCode, CancellationToken? token);
}
}

View File

@@ -24,22 +24,14 @@ namespace BetterLyrics.WinUI3.Services
_httpClient = new HttpClient();
}
public async Task<string> TranslateAsync(string text, CancellationToken? token)
public async Task<string> TranslateAsync(string text, string targetLangCode, CancellationToken? token)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("Text and target language must be provided.");
}
string targetLangCode = AppInfo.GetAllTranslationLanguagesInfo()[_settingsService.SelectedTargetLanguageIndex].Code;
string originalLangCode = await DetectLanguageCode(text);
token?.ThrowIfCancellationRequested();
if (string.IsNullOrWhiteSpace(originalLangCode) || originalLangCode == targetLangCode)
{
return text; // No translation needed
}
string? originalLangCode = LanguageDetectionHelper.DetectLanguageCode(text);
var url = $"{_settingsService.LibreTranslateServer}/translate";
var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(
@@ -57,22 +49,5 @@ namespace BetterLyrics.WinUI3.Services
var result = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.TranslateResponse);
return result?.TranslatedText ?? string.Empty;
}
private async Task<string> DetectLanguageCode(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
throw new ArgumentException("Text must be provided.");
}
var url = $"{_settingsService.LibreTranslateServer}/detect";
var response = await _httpClient.PostAsync(url, new FormUrlEncodedContent(
[
new("q", text),
]));
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var resultList = System.Text.Json.JsonSerializer.Deserialize(json, SourceGenerationContext.Default.ListDetectLanguageResult);
return resultList?.OrderByDescending(x => x.Confidence).FirstOrDefault()?.Language ?? string.Empty;
}
}
}

View File

@@ -44,7 +44,7 @@ namespace BetterLyrics.WinUI3.Services
public async Task<bool> DownloadAmllTtmlDbIndexAsync()
{
const string url = "https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/metadata/raw-img-index.jsonl";
const string url = "https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/metadata/raw-lyrics-index.jsonl";
try
{
using var response = await _amllTtmlDbHttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
@@ -388,7 +388,7 @@ namespace BetterLyrics.WinUI3.Services
return null;
// 下载歌词内容
var url = $"https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/raw-img/{rawLyricFile}";
var url = $"https://raw.githubusercontent.com/Steve-xmh/amll-ttml-db/refs/heads/main/raw-lyrics/{rawLyricFile}";
try
{
var response = await _amllTtmlDbHttpClient.GetAsync(url);

View File

@@ -121,7 +121,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private int _langIndex = 0;
private List<List<LyricsLine>> _multiLangLyrics = [];
private List<string> _translations = [];
private List<string> _translationList = [];
private bool _isTranslationEnabled = false;
private int _targetLanguageIndex = 6;
@@ -346,10 +346,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private void PlaybackService_PositionChanged(object? sender, PositionChangedEventArgs e)
{
if (Math.Abs(_totalTime.TotalMilliseconds - e.Position.TotalMilliseconds) > 300)
{
_totalTime = e.Position;
}
_totalTime = e.Position;
}
private async void PlaybackService_SongInfoChanged(object? sender, SongInfoChangedEventArgs e)
@@ -439,51 +436,60 @@ namespace BetterLyrics.WinUI3.ViewModels
private async Task ShowTranslationsCoreAsync(CancellationToken token)
{
_logger.LogInformation("Showing translations for lyrics...");
_logger.LogInformation("Showing translation for lyrics...");
try
{
if (string.IsNullOrEmpty(_settingsService.LibreTranslateServer))
string targetLangCode = AppInfo.GetAllTranslationLanguagesInfo()[_settingsService.SelectedTargetLanguageIndex].Code;
var originalText = string.Join("\n", _multiLangLyrics.FirstOrDefault()?.Select(x => x.OriginalText) ?? []);
string? originalLangCode = LanguageDetectionHelper.DetectLanguageCode(originalText);
if (originalLangCode == targetLangCode)
{
_dispatcherQueue.TryEnqueue(() =>
{
App.Current.LyricsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString("TranslateServerNotSet"),
Microsoft.UI.Xaml.Controls.InfoBarSeverity.Warning
);
});
_logger.LogInformation("Original lyrics already in target language: {TargetLangCode}", targetLangCode);
ShowOriginalsOnly();
return;
}
var text = string.Join("\n", _multiLangLyrics.FirstOrDefault()?.Select(x => x.OriginalText) ?? []);
var translated = await _libreTranslateService.TranslateAsync(text, token);
token.ThrowIfCancellationRequested();
_translations = translated.Split('\n').ToList();
bool totallySame = true;
foreach (var langLyrics in _multiLangLyrics)
// Try get translation from itself first
if (_multiLangLyrics.Count > 1)
{
int i = 0;
foreach (var line in langLyrics)
foreach (var langLyrics in _multiLangLyrics.Skip(1))
{
if (line.OriginalText != _translations[i])
var translationList = langLyrics.Select(x => x.OriginalText).ToList();
var translation = string.Join("\n", translationList);
if (LanguageDetectionHelper.DetectLanguageCode(translation) == targetLangCode)
{
totallySame = false;
_translationList = translationList;
break;
}
i++;
}
break;
}
else
{
if (string.IsNullOrEmpty(_settingsService.LibreTranslateServer))
{
_dispatcherQueue.TryEnqueue(() =>
{
App.Current.LyricsWindowNotificationPanel?.Notify(
App.ResourceLoader!.GetString("TranslateServerNotSet"),
Microsoft.UI.Xaml.Controls.InfoBarSeverity.Warning
);
});
ShowOriginalsOnly();
return;
}
var translated = await _libreTranslateService.TranslateAsync(originalText, targetLangCode, token);
token.ThrowIfCancellationRequested();
_translationList = translated.Split('\n').ToList();
}
foreach (var langLyrics in _multiLangLyrics)
int i = 0;
foreach (var line in _multiLangLyrics.FirstOrDefault() ?? [])
{
int i = 0;
foreach (var line in langLyrics)
{
line.DisplayedText = totallySame ? line.OriginalText : $"{line.OriginalText}\n{_translations[i]}";
i++;
}
break;
line.DisplayedText = i < _translationList.Count ? $"{line.OriginalText}\n{_translationList[i]}" : line.OriginalText;
i++;
}
_isLayoutChanged = true;
}
@@ -495,7 +501,7 @@ namespace BetterLyrics.WinUI3.ViewModels
private void ShowOriginalsOnly()
{
_logger.LogInformation("Showing original lyrics only, translations disabled.");
_logger.LogInformation("Showing original lyrics only, translation disabled.");
foreach (var langLyrics in _multiLangLyrics)
{
foreach (var line in langLyrics)

View File

@@ -385,7 +385,8 @@ namespace BetterLyrics.WinUI3.ViewModels
{
try
{
string result = await _libreTranslateService.TranslateAsync("Hello, world!", null);
string targetLangCode = AppInfo.GetAllTranslationLanguagesInfo()[SelectedTargetLanguageIndex].Code;
string result = await _libreTranslateService.TranslateAsync("Hello, world!", targetLangCode, null);
_dispatcherQueue.TryEnqueue(() =>
{
App.Current.SettingsWindowNotificationPanel?.Notify(App.ResourceLoader!.GetString("SettingsPageLibreTranslateTestSuccessInfo"), Microsoft.UI.Xaml.Controls.InfoBarSeverity.Success);

View File

@@ -94,8 +94,8 @@
<StackPanel>
<Slider
x:Uid="MainPagePositionOffsetSlider"
Maximum="2000"
Minimum="-2000"
Maximum="5000"
Minimum="-5000"
SnapsTo="Ticks"
StepFrequency="100"
TickFrequency="100"
@@ -114,7 +114,7 @@
RelativePanel.AlignVerticalCenterWithPanel="True"
Style="{StaticResource GhostButtonStyle}" />
</RelativePanel>
<TextBlock Opacity="0.5" x:Uid="LyricsPagePositionOffsetHint"/>
<TextBlock x:Uid="LyricsPagePositionOffsetHint" Opacity="0.5" />
</StackPanel>
</Flyout>
</Button.Flyout>

View File

@@ -6,11 +6,16 @@
<h2 align="center">
BetterLyrics
</div>
</h2>
<h3 align="center">
使用 WinUI 3 构建的流畅动态歌词显示工具
</div>
</h3>
---
- QQ[「BetterLyrics」反馈交流群简体中文](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388)
- Discord [「BetterLyrics」反馈交流群繁体中文/英文)](https://discord.gg/5yAQPnyCKv)
---
@@ -121,8 +126,4 @@ BetterLyrics
## 欢迎提出任何问题和 PR
如果您发现错误,请提交至 issues如果您有任何想法请随时在此处分享。
或者,您也可以加入群聊,分享您的宝贵反馈:
- QQ[「BetterLyrics」反馈交流群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388)
- Discord [「BetterLyrics」反馈交流群](https://discord.gg/rbnF556r)
如果您发现错误,请提交至 issues如果您有任何想法请随时在此处分享。

View File

@@ -6,11 +6,16 @@
<h2 align="center">
BetterLyrics
</div>
</h2>
<h3 align="center">
Your smooth dynamic lyrics display tool built with WinUI 3
</div>
</h3>
---
- [「BetterLyrics」反馈交流群简体中文](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) on QQ
- [「BetterLyrics」Feedback Chat Group (Traditional Chinese / English)](https://discord.gg/5yAQPnyCKv) on Discord
---
@@ -121,8 +126,4 @@ You can `git clone` this project and build it yourself.
## Any issues and PRs are welcomed
If you find a bug please file it in issues or if you have any ideas feel free to share it here.
Or alternatively join group chat to share your valuable feedback:
- [「BetterLyrics」反馈交流群](https://qun.qq.com/universal-share/share?ac=1&authKey=4Q%2BYTq3wZldYpF5SbS5c19ECFsiYoLZFAIcBNNzYpBUtiEjaZ8sZ%2F%2BnFN0qw3lad&busi_data=eyJncm91cENvZGUiOiIxMDU0NzAwMzg4IiwidG9rZW4iOiJiVnhqemVYN0N5QVc3b1ZkR24wWmZOTUtvUkJoWm1JRWlaWW5iZnlBcXJtZUtGc2FFTHNlUlFZMi9iRm03cWF5IiwidWluIjoiMTM5NTczOTY2MCJ9&data=39UmAihyH_o6CZaOs7nk2mO_lz2ruODoDou6pxxh7utcxP4WF5sbDBDOPvZ_Wqfzeey4441anegsLYQJxkrBAA&svctype=4&tempid=h5_group_info) (1054700388) on QQ
- [「BetterLyrics」Feedback Chat Group](https://discord.gg/rbnF556r) on Discord
If you find a bug please file it in issues or if you have any ideas feel free to share it here.