stan44 e0f298ba36 Add LyricFlow .NET backend API and Python bridge integration
- introduce `LyricFlow.Core.Backend` with shared DTOs, rhyme/spellcheck engines, and REST endpoints
- wire Python GUI/core to run and call the backend via new bridge/client modules
- add backend parity/integration tests and update packaging/ignore settings
2026-03-15 01:44:56 -05:00

295 lines
9.6 KiB
C#

using System.Text.RegularExpressions;
using LyricFlow.Backend.Api;
using LyricFlow.Core.Dtos;
using LyricFlow.Core.Engine;
using LyricFlow.Core.Services;
using LyricFlow.Core.Storage;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http.Json;
// MARK: - Application Bootstrap
#region Application Bootstrap
var builder = WebApplication.CreateSlimBuilder(args);
builder.WebHost.UseUrls(builder.Configuration["Backend:Urls"] ?? "http://127.0.0.1:5000");
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.TypeInfoResolverChain.Insert(0, ApiJsonSerializerContext.Default);
});
var dbPath = builder.Configuration["Database:Path"] ?? "data/lyricflow.db";
builder.Services.AddSingleton(new StorageService(dbPath));
var cmuDictPath = NltkPathResolver.ResolveCmudictPath(builder.Configuration["Nltk:CmudictPath"]);
var wordNetPath = NltkPathResolver.ResolveWordNetPath(builder.Configuration["Nltk:WordNetPath"]);
var processor = new PhoneticProcessor(cmuDictPath);
builder.Services.AddSingleton(processor);
builder.Services.AddSingleton(new WordNetLexicon(wordNetPath));
builder.Services.AddSingleton(sp => new RhymeEngine(processor, sp.GetRequiredService<WordNetLexicon>()));
builder.Services.AddSingleton(sp => new SpellcheckEngine(processor, sp.GetRequiredService<WordNetLexicon>()));
builder.Services.AddSingleton(new ProjectStateService());
builder.Services.AddSingleton(new SessionStoreService(builder.Configuration["State:SessionPath"]));
builder.Services.AddSingleton(new CorePreferencesStore(builder.Configuration["State:CorePreferencesPath"]));
builder.Services.AddSingleton(new FileService());
var app = builder.Build();
#endregion
// MARK: - Health And Analysis Routes
#region Health And Analysis Routes
app.MapGet("/api/health", (PhoneticProcessor phonetics, WordNetLexicon wordNet) =>
{
var capabilities = new CapabilitiesDto(
Analysis: true,
Phonetics: true,
Spellcheck: true,
Synonyms: wordNet.IsAvailable,
Autocorrect: true,
Projects: true,
Sessions: true,
Settings: true,
Files: true,
History: true,
Scratchpad: true,
HasCmudict: phonetics.Dictionary.Count > 0,
HasWordNet: wordNet.IsAvailable
);
return Results.Ok(new HealthDto(
Status: "healthy",
Version: "2.0.0",
Ready: phonetics.Dictionary.Count > 0,
DictionaryCount: phonetics.Dictionary.Count,
Capabilities: capabilities
));
});
app.MapGet("/api/analysis/phonemes", (PhoneticProcessor phonetics, [FromQuery] string word) =>
{
return Results.Ok(phonetics.GetPhonemes(word));
});
app.MapGet("/api/analysis/syllables", (RhymeEngine engine, [FromQuery] string word) =>
{
return Results.Ok(new CountDto(engine.CountSyllables(word)));
});
app.MapGet("/api/analysis/suggestions", (RhymeEngine engine, [FromQuery] string word, [FromQuery] int? limit) =>
{
return Results.Ok(engine.FindSuggestions(word, limit ?? 20));
});
app.MapGet("/api/analysis/synonyms", (RhymeEngine engine, [FromQuery] string word, [FromQuery] int? limit) =>
{
return Results.Ok(engine.FindSynonyms(word, limit ?? 15));
});
app.MapPost("/api/analysis/rhyme-groups", (RhymeEngine engine, [FromBody] AnalyzeRequest request) =>
{
return Results.Ok(engine.GetRhymeGroups(request.Text));
});
app.MapPost("/api/analysis/line-densities", (RhymeEngine engine, [FromBody] AnalyzeRequest request) =>
{
return Results.Ok(CalculateLineDensities(engine, request.Text));
});
app.MapGet("/api/engine/rhymes", (RhymeEngine engine, [FromQuery] string word, [FromQuery] int? limit) =>
{
return Results.Ok(engine.FindSuggestions(word, limit ?? 20));
});
app.MapGet("/api/engine/syllables", (RhymeEngine engine, [FromQuery] string word) =>
{
return Results.Ok(new CountDto(engine.CountSyllables(word)));
});
app.MapPost("/api/engine/analyze", (RhymeEngine engine, [FromBody] AnalyzeRequest request) =>
{
return Results.Ok(engine.GetRhymeGroups(request.Text));
});
app.MapPost("/api/engine/densities", (RhymeEngine engine, [FromBody] AnalyzeRequest request) =>
{
return Results.Ok(CalculateLineDensities(engine, request.Text));
});
#endregion
// MARK: - Spellcheck And State Routes
#region Spellcheck And State Routes
app.MapGet("/api/spellcheck/known", (SpellcheckEngine engine, [FromQuery] string word) =>
{
return Results.Ok(new KnownWordDto(engine.IsKnownWord(word)));
});
app.MapGet("/api/spellcheck/suggestions", (SpellcheckEngine engine, [FromQuery] string word, [FromQuery] int? limit) =>
{
return Results.Ok(engine.GetSpellingSuggestions(word, limit ?? 6));
});
app.MapGet("/api/spellcheck/autocorrect", (SpellcheckEngine engine, [FromQuery] string word) =>
{
return Results.Ok(new CandidateDto(engine.GetAutocorrectCandidate(word)));
});
app.MapPost("/api/spellcheck/issues", (SpellcheckEngine engine, [FromBody] AnalyzeRequest request, [FromQuery] int? limit) =>
{
return Results.Ok(engine.GetTextSpellingIssues(request.Text, limit ?? 6));
});
app.MapPost("/api/projects/read", (ProjectStateService projects, [FromBody] ProjectReadRequest request) =>
{
return Results.Ok(projects.ReadProject(request.ProjectFile));
});
app.MapPost("/api/projects/write", (ProjectStateService projects, [FromBody] ProjectWriteRequest request) =>
{
projects.WriteProject(request.ProjectFile, request.State);
return Results.Ok(new SuccessDto(true));
});
app.MapPost("/api/session/load", (SessionStoreService sessionStore, [FromBody] SessionLoadRequest request) =>
{
return Results.Ok(sessionStore.Load(request.WorkspaceRoot));
});
app.MapPost("/api/session/save", (SessionStoreService sessionStore, [FromBody] SessionSaveRequest request) =>
{
sessionStore.Save(request.WorkspaceRoot, request.Snapshots);
return Results.Ok(new SuccessDto(true));
});
app.MapPost("/api/session/clear", (SessionStoreService sessionStore, [FromBody] SessionClearRequest request) =>
{
sessionStore.Clear(request.WorkspaceRoot);
return Results.Ok(new SuccessDto(true));
});
app.MapGet("/api/settings", (CorePreferencesStore settingsStore) =>
{
return Results.Ok(settingsStore.Load());
});
app.MapPost("/api/settings", (CorePreferencesStore settingsStore, [FromBody] AppCorePreferencesDto preferences) =>
{
settingsStore.Save(preferences);
return Results.Ok(new SuccessDto(true));
});
app.MapPost("/api/files/read", (FileService files, [FromBody] FileReadRequest request) =>
{
return Results.Ok(files.ReadFile(request.Path));
});
app.MapPost("/api/files/write", (FileService files, [FromBody] FileWriteRequest request) =>
{
return Results.Ok(files.WriteFile(request.Path, request.Content));
});
app.MapPost("/api/files/create", (FileService files, [FromBody] FileCreateRequest request) =>
{
return Results.Ok(request.IsDirectory ? files.CreateDirectory(request.Path) : files.CreateFile(request.Path));
});
app.MapPost("/api/files/rename", (FileService files, [FromBody] FileRenameRequest request) =>
{
return Results.Ok(files.Rename(request.OldPath, request.NewPath, request.RootPath));
});
app.MapPost("/api/files/delete", (FileService files, [FromBody] FileDeleteRequest request) =>
{
return Results.Ok(files.Delete(request.Path, request.RootPath));
});
#endregion
// MARK: - Storage Routes
#region Storage Routes
app.MapPost("/api/history/snapshots", async (StorageService storage, [FromBody] SaveSnapshotRequest request) =>
{
await storage.SaveSnapshotAsync(request.FilePath, request.Content);
return Results.Ok();
});
app.MapGet("/api/history/snapshots", async (StorageService storage, [FromQuery] string filePath) =>
{
var snapshots = await storage.GetSnapshotsAsync(filePath);
return Results.Ok(snapshots.Select(snapshot => snapshot.ToDto()).ToList());
});
app.MapPost("/api/snapshots", async (StorageService storage, [FromBody] SaveSnapshotRequest request) =>
{
await storage.SaveSnapshotAsync(request.FilePath, request.Content);
return Results.Ok();
});
app.MapGet("/api/snapshots", async (StorageService storage, [FromQuery] string filePath) =>
{
var snapshots = await storage.GetSnapshotsAsync(filePath);
return Results.Ok(snapshots.Select(snapshot => snapshot.ToDto()).ToList());
});
app.MapPost("/api/scratchpad", async (StorageService storage, [FromBody] SaveScratchpadRequest request) =>
{
await storage.SaveScratchpadAsync(request.ProjectId, request.Content);
return Results.Ok();
});
app.MapGet("/api/scratchpad/{projectId}", async (StorageService storage, string projectId) =>
{
return Results.Ok(new ScratchpadContentDto(await storage.GetScratchpadAsync(projectId)));
});
app.Run();
#endregion
// MARK: - Route Utilities
#region Route Utilities
static List<double> CalculateLineDensities(RhymeEngine engine, string text)
{
var lines = text.Split('\n');
var groups = engine.GetRhymeGroups(text);
using var groupIter = groups.GetEnumerator();
var densities = new List<double>();
foreach (var line in lines)
{
var stripped = line.Trim();
if (stripped.StartsWith("#") || stripped.StartsWith("@") || stripped.StartsWith(">"))
{
densities.Add(0.0);
continue;
}
var analysisText = Regex.Replace(line, @"\[.*?\]", "");
var words = Regex.Matches(analysisText, @"\b\w+\b");
if (words.Count == 0)
{
densities.Add(0.0);
continue;
}
var rhymeCount = 0;
for (var index = 0; index < words.Count; index++)
{
if (groupIter.MoveNext() && groupIter.Current.Group is not null)
{
rhymeCount++;
}
}
densities.Add((double)rhymeCount / words.Count);
}
return densities;
}
#endregion