Skip to content

Commit

Permalink
ad-hock word play (#20)
Browse files Browse the repository at this point in the history
* words audio pre-processed
  • Loading branch information
NicklausBrain authored Sep 14, 2024
1 parent db0f442 commit 2412cbb
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 31 deletions.
22 changes: 22 additions & 0 deletions ConsoleApp/ConsoleApp.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<UserSecretsId>716f7a77-339c-46aa-89a6-2dc47eab1d23</UserSecretsId>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\My1kWordsEe\My1kWordsEe.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
</ItemGroup>

</Project>
79 changes: 79 additions & 0 deletions ConsoleApp/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

using My1kWordsEe.Services;
using My1kWordsEe.Services.Cqs;
using My1kWordsEe.Services.Db;

namespace ConsoleApp
{
internal class Program
{

private static readonly string[] Words = new string[]
{
};

static async Task Main(string[] args)
{
using ILoggerFactory factory = LoggerFactory.Create(builder => builder.AddConsole());
IConfigurationRoot config = new ConfigurationBuilder()
.AddUserSecrets<Program>()
.Build();

var openAiKey = config["Secrets:OpenAiKey"];

if (string.IsNullOrWhiteSpace(openAiKey))
{
throw new ApplicationException("Secrets:OpenAiKey is missing");
}

var azureBlobConnectionString = config["Secrets:AzureBlobConnectionString"];

if (string.IsNullOrWhiteSpace(azureBlobConnectionString))
{
throw new ApplicationException("Secrets:AzureBlobConnectionString is missing");
}

var blob = new AzureBlobService(azureBlobConnectionString);
var openAi = new OpenAiService(factory.CreateLogger<OpenAiService>(), openAiKey);
var ensure = new EnsureWordCommand(blob, openAi);

var errors = new List<string>();

Console.WriteLine(Words.Length);
foreach (var word in Words)
{
Console.WriteLine($"Processing {word}");

var ensureCmd = await ensure.Invoke(word);
if (ensureCmd.IsFailure)
{
Console.WriteLine($"Failed to ensure {word}: {ensureCmd.Error}");
errors.Add($"Failed to ensure {word}: {ensureCmd.Error}");
}

var data = await blob.GetWordData(word);

if (data.IsSuccess)
{
var wordData = data.Value;
var update = wordData with
{
EeAudioUrl = new Uri(
$"https://my1kee.blob.core.windows.net/audio/{wordData.EeWord}.wav")
};

await blob.SaveWordData(update);
}
else
{
errors.Add($"Failed to get data for {word}");
Console.WriteLine($"Failed to get data for {word}");
}
}

File.WriteAllLines("cmd-errors-5.txt", errors);
}
}
}
24 changes: 23 additions & 1 deletion My1kWordsEe/Components/Pages/Word.razor
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,33 @@
}
this.isGenerationInProgress = false;
}

private async Task SpeakWord(MouseEventArgs e)
{
await JS.InvokeVoidAsync("speakWord", EeWord);
}

private bool IsDataLoadedOk => WordMetadata.HasValue && WordMetadata.Value.IsSuccess;
}

<script>
window.speakWord = async (audioId) => {
const audio = document.getElementById(audioId);
await audio.play();
};
</script>

<div class="text-left">
<div class="row">
<h1>@EeWord
<h1>
@EeWord

@if (IsDataLoadedOk)
{
<i class="bi bi-volume-down-fill small" @onclick="SpeakWord">
<audio id="@EeWord" src="@WordMetadata.Value.Value.EeAudioUrl"></audio>
</i>
}
</h1>

@if (WordMetadata.HasValue)
Expand Down
8 changes: 7 additions & 1 deletion My1kWordsEe/My1kWordsEe.sln
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.35122.118
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "My1kWordsEe", "My1kWordsEe.csproj", "{497B021C-F03E-4CB6-BC42-3A9D8BC68F68}"
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "My1kWordsEe", "My1kWordsEe.csproj", "{497B021C-F03E-4CB6-BC42-3A9D8BC68F68}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "..\ConsoleApp\ConsoleApp.csproj", "{ECE5CED5-98E9-4FAB-A3CB-0A5002C88D9C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Expand All @@ -15,6 +17,10 @@ Global
{497B021C-F03E-4CB6-BC42-3A9D8BC68F68}.Debug|Any CPU.Build.0 = Debug|Any CPU
{497B021C-F03E-4CB6-BC42-3A9D8BC68F68}.Release|Any CPU.ActiveCfg = Release|Any CPU
{497B021C-F03E-4CB6-BC42-3A9D8BC68F68}.Release|Any CPU.Build.0 = Release|Any CPU
{ECE5CED5-98E9-4FAB-A3CB-0A5002C88D9C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ECE5CED5-98E9-4FAB-A3CB-0A5002C88D9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ECE5CED5-98E9-4FAB-A3CB-0A5002C88D9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ECE5CED5-98E9-4FAB-A3CB-0A5002C88D9C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
7 changes: 4 additions & 3 deletions My1kWordsEe/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ public class Program
{
public static void Main(string[] args)
{
// default log: Console, Debug, EventSource
var builder = WebApplication.CreateBuilder(args);
builder.Configuration.AddEnvironmentVariables();

var openApiKey =
var openAiKey =
builder.Configuration["Secrets:OpenAiKey"] ??
Environment.GetEnvironmentVariable("Secrets_OpenAiKey");

if (string.IsNullOrWhiteSpace(openApiKey))
if (string.IsNullOrWhiteSpace(openAiKey))
{
throw new ApplicationException("Secrets:OpenAiKey is missing");
}
Expand All @@ -40,7 +41,7 @@ public static void Main(string[] args)
}

builder.Services.AddSingleton(new StabilityAiService(stabilityAiKey));
builder.Services.AddSingleton(new OpenAiService(openApiKey));
builder.Services.AddSingleton((p) => new OpenAiService(p.GetRequiredService<ILogger<OpenAiService>>(), openAiKey));
builder.Services.AddSingleton(new AzureBlobService(azureBlobConnectionString));
builder.Services.AddSingleton(new TartuNlpService());
builder.Services.AddSingleton<EnsureWordCommand>();
Expand Down
1 change: 1 addition & 0 deletions My1kWordsEe/Services/Cqs/EnsureWordCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public async Task<Result<SampleWord>> Invoke(string eeWord)

if (sampleWord.IsSuccess)
{
// generate audio
await azureBlobService.SaveWordData(sampleWord.Value);
}

Expand Down
8 changes: 5 additions & 3 deletions My1kWordsEe/Services/Db/AzureBlobService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,16 @@ await blob.UploadAsync(
overwrite: true);
}

public async Task<Uri> SaveAudio(Stream audioStream)
public async Task<Uri> SaveAudio(Stream audioStream, string blobName)
{
BlobContainerClient container = await GetAudioContainer();
BlobClient blob = container.GetBlobClient(WavBlobName());
await blob.UploadAsync(audioStream);
BlobClient blob = container.GetBlobClient(blobName);
await blob.UploadAsync(audioStream, overwrite: true);
return blob.Uri;
}

public Task<Uri> SaveAudio(Stream audioStream) => SaveAudio(audioStream, WavBlobName());

public async Task<Uri> SaveImage(Stream imageStream)
{
BlobContainerClient container = await GetImageContainer();
Expand Down
49 changes: 26 additions & 23 deletions My1kWordsEe/Services/OpenAiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,12 @@ namespace My1kWordsEe.Services
{
public class OpenAiService
{
public OpenAiService(string apiKey)
private readonly ILogger logger;

public OpenAiService(ILogger<OpenAiService> logger, string apiKey)
{
ApiKey = apiKey;
this.logger = logger;
this.ApiKey = apiKey;
}

private string ApiKey { get; }
Expand Down Expand Up @@ -93,21 +96,20 @@ public async Task<Result<Sentence>> GetSampleSentence(string eeWord)
ChatCompletion chatCompletion = await client.CompleteChatAsync(
[
new SystemChatMessage(
"Sa oled keeleõppe süsteemi abiline, mis aitab õppida enim levinud eesti keele sõnu. " +
"Sinu sisend on üks sõna eesti keeles. " +
"Sinu ülesanne on kirjutada selle kasutamise kohta lihtne lühike näitelause, kasutades seda sõna. " +
"Lauses kasuta kõige levinuimaid ja lihtsamaid sõnu eesti keeles et toetada keeleõpet. " +
"Sinu väljund on JSON objekt, milles on näitelaus eesti keeles ja selle vastav tõlge inglise keelde:\n" +
"```\n" +
"{\"ee_sentence\": \"<näide eesti keeles>\", \"en_sentence\": \"<näide inglise keeles>\" }" +
"\n```" +
"\n Tagastab ainult json-objekti!"),
"Sa oled keeleõppe süsteemi abiline, mis aitab õppida enim levinud eesti keele sõnu.\n" +
"Sinu sisend on üks sõna eesti keeles.\n" +
"Sinu ülesanne on kirjutada selle kasutamise kohta lihtne lühike näitelause, kasutades seda sõna.\n" +
"Lauses kasuta kõige levinuimaid ja lihtsamaid sõnu eesti keeles et toetada keeleõpet.\n" +
"Teie väljundiks on JSON-objekt koos eestikeelse näidislausega ja sellele vastav tõlge inglise keelde vastavalt lepingule:\n" +
"```\n{\n" +
"\"ee_sentence\": \"<näide eesti keeles>\", \"en_sentence\": \"<näide inglise keeles>\"" +
"\n}\n```"),
new UserChatMessage(eeWord),
]);

foreach (var c in chatCompletion.Content)
{
var jsonStr = c.Text.Trim('`', ' ', '\'', '"');
var jsonStr = c.Text.Replace("json", "", StringComparison.OrdinalIgnoreCase).Trim('`', ' ', '\'', '"');
var sentence = JsonSerializer.Deserialize<Sentence>(jsonStr);
if (sentence == null)
{
Expand All @@ -124,26 +126,26 @@ public async Task<Result<Sentence>> GetSampleSentence(string eeWord)

public async Task<Result<SampleWord>> GetWordMetadata(string word)
{
ChatClient client = new(model: "gpt-4o-mini", ApiKey);
ChatClient client = new(model: "gpt-4o", ApiKey);

ChatCompletion chatCompletion = await client.CompleteChatAsync(
[
new SystemChatMessage(
"Your input is an Estonian word. " +
"Your output is word metadata in JSON:\n" +
"Sinu sisend on eestikeelne sõna.\n" +
"Kui antud sõna ei ole eestikeelne, tagasta 404\n"+
"Teie väljund on sõna metaandmed JSON-is vastavalt antud lepingule:\n" +
"```\n{\n" +
"ee_word: \"<given word>\",\n" +
"en_word: \"<english translation>\"\n" +
"en_words: [<alternative translations if applicable>]\n" +
"en_explanation: \"<explanation of the word in english>\"\n" +
"}\n```\n" +
"If the given word is not Estonian return 404"),
"\"ee_word\": \"<antud sõna>\",\n" +
"\"en_word\": \"<english translation>\"\n" +
"\"en_words\": [<array of alternative english translations if applicable>]\n" +
"\"en_explanation\": \"<explanation of the word meaning in english>\"\n" +
"}\n```\n"),
new UserChatMessage(word),
]);

foreach (var c in chatCompletion.Content)
{
var jsonStr = c.Text.Trim('`', ' ', '\'', '"');
var jsonStr = c.Text.Replace("json", "", StringComparison.OrdinalIgnoreCase).Trim('`', ' ', '\'', '"');

if (jsonStr.Contains("404"))
{
Expand All @@ -168,8 +170,9 @@ public async Task<Result<SampleWord>> GetWordMetadata(string word)
});
}
}
catch (JsonException)
catch (JsonException jsonException)
{
this.logger.LogError(jsonException, "Failed to deserialize JSON: {jsonStr}", jsonStr);
return Result.Failure<SampleWord>("Unexpected data returned by AI");
}
}
Expand Down

0 comments on commit 2412cbb

Please sign in to comment.