Speech, auto avoid, bug fixes, iu improments

This commit is contained in:
BOT Alex 2023-07-04 21:20:33 +02:00
parent bc45154b70
commit bb125e7c7d
8 changed files with 164 additions and 22 deletions

View File

@ -0,0 +1,28 @@
namespace LearningChineseFixed
{
public class CCharStats
{
public CChar cchar { get; set; }
public int NumOfCorrects { get; set; } = 0;
public int NumOfWrongs { get; set; } = 0;
public int TotalAnswers { get => NumOfCorrects + NumOfWrongs; }
public float Accuracy
{
get
{
return (float)NumOfCorrects / (float)TotalAnswers;
}
}
public CCharStats(CChar cchar)
{
this.cchar = cchar;
}
}
public enum StatType
{
NumOfCorrects,
NumOfWrongs
}
}

View File

@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MudBlazor; using MudBlazor;
using Blazored.LocalStorage; using Blazored.LocalStorage;
using MudBlazor.Services; using MudBlazor.Services;
using Toolbelt.Blazor.Extensions.DependencyInjection;
namespace LearningChineseFixed namespace LearningChineseFixed
{ {
@ -27,6 +28,7 @@ namespace LearningChineseFixed
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomCenter; config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomCenter;
}); });
builder.Services.AddBlazoredLocalStorage(); builder.Services.AddBlazoredLocalStorage();
builder.Services.AddSpeechSynthesis();
await builder.Build().RunAsync(); await builder.Build().RunAsync();
} }

View File

@ -15,6 +15,7 @@
<PackageReference Include="Microsoft.NET.Build.Containers" Version="7.0.305" /> <PackageReference Include="Microsoft.NET.Build.Containers" Version="7.0.305" />
<PackageReference Include="MudBlazor" Version="6.5.0" /> <PackageReference Include="MudBlazor" Version="6.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Toolbelt.Blazor.SpeechSynthesis" Version="10.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -2,6 +2,7 @@
@inject NavigationManager navigator @inject NavigationManager navigator
@inject HttpClient httpClient @inject HttpClient httpClient
@inject Blazored.LocalStorage.ILocalStorageService localStorage @inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject SpeechSynthesis SpeechSynthesis
<PageTitle>Index</PageTitle> <PageTitle>Index</PageTitle>
@ -105,17 +106,35 @@
Parallel.For(0, numOfChunks, async (i)=> Parallel.For(0, numOfChunks, async (i)=>
{ {
await Task.Delay(10*i); await Task.Delay(10*i);
jsonChunks[i] = await httpClient.GetStringAsync($"Data/Normalized_chunk_{i.ToString("000")}.json"); try
{
jsonChunks[i] = await httpClient.GetStringAsync($"Data/Normalized_chunk_{i.ToString("000")}.json");
}
catch (Exception)
{
await Task.Delay(100);
jsonChunks[i] = await httpClient.GetStringAsync($"Data/Normalized_chunk_{i.ToString("000")}.json");
}
if (string.IsNullOrEmpty(jsonChunks[i])) return; if (string.IsNullOrEmpty(jsonChunks[i])) throw new Exception("Couldn't load chunk: " + i);
await localStorage.SetItemAsync($"Normalized_chunk_{i.ToString("000")}.json", jsonChunks[i]); try
savedChunks = i + 1; {
await localStorage.SetItemAsync($"Normalized_chunk_{i.ToString("000")}.json", jsonChunks[i]);
}
catch (Exception)
{
await Task.Delay(100);
await localStorage.SetItemAsync($"Normalized_chunk_{i.ToString("000")}.json", jsonChunks[i]);
}
if (!await localStorage.ContainKeyAsync($"Normalized_chunk_{i.ToString("000")}.json")) throw new Exception("Couldn't save chunk: " + i);
}); });
while (jsonChunks.Any(x => x == null)) while (jsonChunks.Any(x => x == null))
{ {
await Task.Delay(1); await Task.Delay(1);
savedChunks = jsonChunks.Count(x=>x != null);
StateHasChanged(); StateHasChanged();
} }

View File

@ -6,7 +6,7 @@
@inject Blazored.LocalStorage.ILocalStorageService localStorage @inject Blazored.LocalStorage.ILocalStorageService localStorage
@inject HttpClient httpClient @inject HttpClient httpClient
@inject ISnackbar Snackbar @inject ISnackbar Snackbar
@inject SpeechSynthesis SpeechSynthesis
<style> <style>
.LargeCharecter{ .LargeCharecter{
@ -27,9 +27,13 @@
<MudContainer Class="justify-center d-flex ma-0" Style="height: 100%;"> <MudContainer Class="justify-center d-flex ma-0" Style="height: 100%;">
<MudStack Class="d-flex"> <MudStack Class="d-flex">
<MudStack Row=true Class="pt-2 d-flex justify-center"> <MudStack Row=true Class="pt-2 d-flex justify-center">
<MudButton Color="Color.Default" OnClick="RemoveCCharFromSelection" Variant="Variant.Outlined">Avoid</MudButton> @if (!Answers.Any(x => x == null))
{
@*Color="@(selectedCorrect ? Color.Success : Color.Error)"*@
<MudButton Disabled="@(!(Answers.Any(x=>x.isSelected)))" OnClick="()=>RemoveCCharFromSelection()" Variant="Variant.Outlined">Avoid</MudButton>
}
<MudButton Color="Color.Default" Variant="Variant.Outlined" OnClick="ShowMeaning">Meaning</MudButton> <MudButton Color="Color.Default" Variant="Variant.Outlined" OnClick="ShowMeaning">Meaning</MudButton>
<MudButton Color="Color.Default" Variant="Variant.Outlined" OnClick="ShowPinyin">Pinyin</MudButton> <MudButton Color="Color.Default" Variant="Variant.Outlined" OnClick="SayCorrectChar">Speech</MudButton>
</MudStack> </MudStack>
<MudContainer> <MudContainer>
<MudPaper Class="pa-16 ma-2 rounded-xl mud-dark" Elevation="1"> <MudPaper Class="pa-16 ma-2 rounded-xl mud-dark" Elevation="1">
@ -71,13 +75,11 @@
</MudStack> </MudStack>
</MudContainer> </MudContainer>
<MudOverlay @bind-Visible="ShowOverlay" DarkBackground="true" AutoClose="true" />
@code { @code {
public bool ShowOverlay = false;
bool isSavedLocally = false; bool isSavedLocally = false;
bool selectedCorrect = false;
public Answer[] Answers = new Answer[4]; public Answer[] Answers = new Answer[4];
public void SelectButton(int selectedIndex) public void SelectButton(int selectedIndex)
{ {
@ -85,6 +87,8 @@
{ {
Answers[i].isSelected = (i == selectedIndex); Answers[i].isSelected = (i == selectedIndex);
} }
selectedCorrect = Answers[selectedIndex].isCorrect;
} }
private CChar[]? _charecters; private CChar[]? _charecters;
@ -99,7 +103,8 @@
set { _charecters = value; } set { _charecters = value; }
} }
public List<CChar>? DontSkipTheseCChar;
public List<CCharStats>? DontSkipTheseCChar;
private async Task<string> GetFileContentsAsync(string filePath) private async Task<string> GetFileContentsAsync(string filePath)
{ {
@ -123,7 +128,7 @@
Charecters = JsonConvert.DeserializeObject<CChar[]>(json); Charecters = JsonConvert.DeserializeObject<CChar[]>(json);
} }
DontSkipTheseCChar = Charecters.ToList(); DontSkipTheseCChar = Charecters.Select(x=>new CCharStats(x)).ToList();
Program.CCharsLeft = DontSkipTheseCChar.Count; Program.CCharsLeft = DontSkipTheseCChar.Count;
Program.InvokeUiUpdate(); Program.InvokeUiUpdate();
@ -148,11 +153,21 @@
CChar randomCChar; CChar randomCChar;
repickRandomCChar: repickRandomCChar:
randomCChar = DontSkipTheseCChar[Random.Shared.Next(0, DontSkipTheseCChar.Count)]; randomCChar = DontSkipTheseCChar[Random.Shared.Next(0, DontSkipTheseCChar.Count)].cchar;
if (Answers.Any(x =>x != null && x.cchar == randomCChar)) goto repickRandomCChar; if (Answers.Any(x =>x != null && x.cchar == randomCChar)) goto repickRandomCChar;
Answers[i] = new Answer(randomCChar, isCorrect); Answers[i] = new Answer(randomCChar, isCorrect);
} }
string correctPinyin = GetCorrectCChar().pinyin;
for (int i = 0; i < Answers.Length; i++)
{
if (Answers[i].isCorrect) continue;
if (Answers[i].cchar.pinyin != correctPinyin) continue;
Answers[i].cchar.pinyin += "_";
}
} }
void FinishAndKickBackToLearn() void FinishAndKickBackToLearn()
@ -171,11 +186,13 @@
async void Submit() async void Submit()
{ {
bool isCorrect = Answers.Any(x => x.isCorrect && x.isSelected); bool isCorrect = Answers.Any(x => x.isCorrect && x.isSelected);
CChar correctCChar = GetCorrectCChar(); CCharStats correctCCharStats = GetCorrectCCharStats();
CChar correctCChar = correctCCharStats.cchar;
if (isCorrect) if (isCorrect)
{ {
Snackbar.Add($"<b>Definition:</b> {correctCChar.definition}", Severity.Success, config => { config.VisibleStateDuration = 1000; }); Snackbar.Add($"<b>Definition:</b> {correctCChar.definition}", Severity.Success, config => { config.VisibleStateDuration = 1000; });
increaseCCharStat(GetCorrectCCharStats(), StatType.NumOfCorrects);
} }
else else
{ {
@ -187,8 +204,19 @@
<li>Correct answer: @correctCChar.pinyin.ToTitleCase()</li> <li>Correct answer: @correctCChar.pinyin.ToTitleCase()</li>
<li>Meaning: @correctCChar.definition</li> <li>Meaning: @correctCChar.definition</li>
</ul> </ul>
</div> </div>
, Severity.Error); , Severity.Error);
increaseCCharStat(GetCorrectCCharStats(), StatType.NumOfWrongs);
}
if (correctCCharStats.TotalAnswers >= 3)
{
if (correctCCharStats.Accuracy > 0.7f)
{
RemoveCCharFromSelection(true);
Snackbar.Add($"CChar '{correctCChar.charcter}' compleated!", Severity.Success);
}
} }
GenerateQuestion(); GenerateQuestion();
@ -224,10 +252,43 @@
return cchar; return cchar;
} }
public void RemoveCCharFromSelection() public CCharStats GetCorrectCCharStats()
{ {
CChar correctChar = GetCorrectCChar(); CChar correctChar = GetCorrectCChar();
DontSkipTheseCChar.Remove(correctChar); CCharStats correctCCharStats = DontSkipTheseCChar.First(x => x.cchar == correctChar);
return correctCCharStats;
}
public void increaseCCharStat(CCharStats stats, StatType statType)
{
int correctStatsIndex = DontSkipTheseCChar.IndexOf(stats);
switch (statType)
{
case StatType.NumOfCorrects:
DontSkipTheseCChar[correctStatsIndex].NumOfCorrects++;
break;
case StatType.NumOfWrongs:
DontSkipTheseCChar[correctStatsIndex].NumOfWrongs++;
break;
}
}
public void RemoveCCharFromSelection(bool ignoreSelection = false)
{
CCharStats correctCCharStats = GetCorrectCCharStats();
if (!selectedCorrect && !ignoreSelection)
{
Snackbar.Add("Selected is wrong. Try again!", Severity.Error);
//return;
}
else if (!ignoreSelection)// Hacky way to only display this snackbar when is manually removed
{
Snackbar.Add($"Removed '{correctCCharStats.cchar.charcter}' from selection", Severity.Info, config => config.VisibleStateDuration = 3000);
}
DontSkipTheseCChar.Remove(correctCCharStats);
Console.WriteLine("Remaining CChars: " + DontSkipTheseCChar); Console.WriteLine("Remaining CChars: " + DontSkipTheseCChar);
Program.CCharsLeft = DontSkipTheseCChar.Count; Program.CCharsLeft = DontSkipTheseCChar.Count;
Program.InvokeUiUpdate(); Program.InvokeUiUpdate();
@ -243,4 +304,34 @@
{ {
Snackbar.Add($"<b>Pinyin:</b> {GetCorrectCChar().pinyin.ToTitleCase()}", Severity.Info); Snackbar.Add($"<b>Pinyin:</b> {GetCorrectCChar().pinyin.ToTitleCase()}", Severity.Info);
} }
SpeechSynthesisVoice? SpeechVoice;
protected async override Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Gets chinese voice on windows or iphone.
this.SpeechVoice = ((IEnumerable<SpeechSynthesisVoice>)(await this.SpeechSynthesis.GetVoicesAsync())).FirstOrDefault(v => v.Name.Contains("Yaoyao") || v.Name.Contains("Ting-Ting"));
this.StateHasChanged();
}
}
async Task SayCorrectChar()
{
if (SpeechVoice == null)
{
Snackbar.Add("Couldn't play sound", Severity.Error);
return;
}
var utterancet = new SpeechSynthesisUtterance
{
Text = this.GetCorrectCChar().charcter.ToString(),
Voice = this.SpeechVoice,
Rate = 0.75f
};
await this.SpeechSynthesis.SpeakAsync(utterancet); // 👈 Speak with "Haruka"'s voice!
}
} }

View File

@ -8,7 +8,7 @@
}, },
"dotnetRunMessages": true, "dotnetRunMessages": true,
"inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
"applicationUrl": "http://localhost:5198;https://0.0.0.0:420/" "applicationUrl": "http://localhost:5198"
}, },
"IIS Express": { "IIS Express": {
"commandName": "IISExpress", "commandName": "IISExpress",

View File

@ -10,4 +10,5 @@
@using LearningChineseFixed.Shared @using LearningChineseFixed.Shared
@using MudBlazor @using MudBlazor
@using Blazored.LocalStorage @using Blazored.LocalStorage
@using Newtonsoft.Json @using Newtonsoft.Json
@using Toolbelt.Blazor.SpeechSynthesis

View File

@ -25,7 +25,7 @@
</div> </div>
<script src="_framework/blazor.webassembly.js"></script> <script src="_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script> <script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script>navigator.serviceWorker.register('service-worker.js');</script> <!--<script>navigator.serviceWorker.register('service-worker.js');</script>-->
</body> </body>
</html> </html>