tajik + persian text backend

This commit is contained in:
Hamid Reza Mohammadi 2026-08-16 16:53:49 +03:30
parent 061853a365
commit 978c6f6e0e
5 changed files with 125 additions and 149 deletions

View File

@ -8,7 +8,6 @@ using Newtonsoft.Json;
using RMuseum.Models.Auth.Memory; using RMuseum.Models.Auth.Memory;
using RMuseum.Models.Auth.ViewModel; using RMuseum.Models.Auth.ViewModel;
using RMuseum.Models.Ganjoor; using RMuseum.Models.Ganjoor;
using RMuseum.Models.Ganjoor.PublicExport;
using RMuseum.Models.Ganjoor.ViewModels; using RMuseum.Models.Ganjoor.ViewModels;
using RMuseum.Models.GanjoorAudio.ViewModels; using RMuseum.Models.GanjoorAudio.ViewModels;
using RMuseum.Models.GanjoorIntegration; using RMuseum.Models.GanjoorIntegration;
@ -2424,81 +2423,6 @@ namespace RMuseum.Controllers
} }
} }
/// <summary>
/// start exporting all published Ganjoor data to the public git-tracked JSON data set
/// </summary>
/// <returns></returns>
[HttpPost("publicdata/batchexport")]
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
public IActionResult StartBatchExportPublicGitData()
{
try
{
var res = _ganjoorService.StartBatchExportPublicGitData();
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok();
}
catch (Exception exp)
{
return BadRequest(exp.ToString());
}
}
/// <summary>
/// start exporting the Tajik (Cyrillic) overlay data to its own, separate public
/// git-tracked JSON data set — only poets/categories/poems with an actual Tajik
/// translation are included
/// </summary>
/// <returns></returns>
[HttpPost("tajik/publicdata/batchexport")]
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
public IActionResult StartBatchExportTajikPublicGitData()
{
try
{
var res = _ganjoorService.StartBatchExportTajikPublicGitData();
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok();
}
catch (Exception exp)
{
return BadRequest(exp.ToString());
}
}
/// <summary>
/// (re)build local Ganjoor content from a public data export tree (local folder or HTTP) —
/// intended for local development use, not for production servers
/// </summary>
/// <returns></returns>
[HttpPost("publicdata/import")]
[Authorize(Policy = RMuseumSecurableItem.GanjoorEntityShortName + ":" + RMuseumSecurableItem.ImportOperationShortName)]
[ProducesResponseType((int)HttpStatusCode.OK)]
[ProducesResponseType((int)HttpStatusCode.BadRequest, Type = typeof(string))]
[ProducesResponseType((int)HttpStatusCode.Unauthorized)]
public IActionResult StartImportFromPublicDataRepo([FromBody] PublicDataImportRequestDto request)
{
try
{
var res = _ganjoorService.StartImportFromPublicDataRepo(request.UseHttp, request.Location, request.PoetId);
if (!string.IsNullOrEmpty(res.ExceptionString))
return BadRequest(res.ExceptionString);
return Ok();
}
catch (Exception exp)
{
return BadRequest(exp.ToString());
}
}
/// <summary> /// <summary>
/// Get user public profile /// Get user public profile
/// </summary> /// </summary>

View File

@ -1,6 +1,5 @@
namespace RMuseum.Models.Ganjoor.ViewModels namespace RMuseum.Models.Ganjoor.ViewModels
{ { /// <summary>
/// <summary>
/// Ganjoor Verse View Model /// Ganjoor Verse View Model
/// </summary> /// </summary>
public class GanjoorVerseViewModel public class GanjoorVerseViewModel
@ -60,6 +59,13 @@
/// </summary> /// </summary>
public string CoupletSummary { get; set; } public string CoupletSummary { get; set; }
/// <summary>
/// original (Persian) text - only populated for Tajik responses that need to show
/// the Persian original alongside the Tajik transliteration. This is a pure view
/// model (not an EF entity), so no [NotMapped] or migration concerns here.
/// </summary>
public string OriginalText { get; set; }
public override string ToString() public override string ToString()
{ {
return Text; return Text;

View File

@ -1775,27 +1775,6 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Controllers.GanjoorController.StartBatchExportPublicGitData">
<summary>
start exporting all published Ganjoor data to the public git-tracked JSON data set
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.StartBatchExportTajikPublicGitData">
<summary>
start exporting the Tajik (Cyrillic) overlay data to its own, separate public
git-tracked JSON data set — only poets/categories/poems with an actual Tajik
translation are included
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.StartImportFromPublicDataRepo(RMuseum.Models.Ganjoor.PublicExport.PublicDataImportRequestDto)">
<summary>
(re)build local Ganjoor content from a public data export tree (local folder or HTTP) —
intended for local development use, not for production servers
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Controllers.GanjoorController.GetUserPublicProfile(System.Guid)"> <member name="M:RMuseum.Controllers.GanjoorController.GetUserPublicProfile(System.Guid)">
<summary> <summary>
Get user public profile Get user public profile
@ -13590,6 +13569,13 @@
couplet summary couplet summary
</summary> </summary>
</member> </member>
<member name="P:RMuseum.Models.Ganjoor.ViewModels.GanjoorVerseViewModel.OriginalText">
<summary>
original (Persian) text - only populated for Tajik responses that need to show
the Persian original alongside the Tajik transliteration. This is a pure view
model (not an EF entity), so no [NotMapped] or migration concerns here.
</summary>
</member>
<member name="T:RMuseum.Models.Ganjoor.ViewModels.PoemMusicTrackViewModel"> <member name="T:RMuseum.Models.Ganjoor.ViewModels.PoemMusicTrackViewModel">
<summary> <summary>
Ganjoor Poem related music track model Ganjoor Poem related music track model
@ -17537,31 +17523,6 @@
</summary> </summary>
<returns></returns> <returns></returns>
</member> </member>
<member name="M:RMuseum.Services.IGanjoorService.StartBatchExportPublicGitData">
<summary>
start exporting all published data (poets/categories/poems/verses) to the public
git-tracked JSON data set, committing and pushing changes since the last run
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorService.StartBatchExportTajikPublicGitData">
<summary>
start exporting the Tajik (Cyrillic) overlay data to its own separate git-tracked JSON
data set — only entities that actually have a Tajik translation are included
</summary>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorService.StartImportFromPublicDataRepo(System.Boolean,System.String,System.Int32)">
<summary>
(re)build local Ganjoor content (poets/categories/poems/verses/sections + their pages)
from a public data export tree, read locally or over HTTP. Safe to re-run — existing
entities (by id) are left untouched, only missing ones are added.
</summary>
<param name="useHttp">true: fetch over HTTP (location is a base URL). false: read from a local folder (location is a path).</param>
<param name="location">base URL or local folder path of the exported data tree</param>
<param name="poetId">0 imports every poet; a specific id imports only that poet (useful on a slow connection)</param>
<returns></returns>
</member>
<member name="M:RMuseum.Services.IGanjoorService.HealthCheckContents"> <member name="M:RMuseum.Services.IGanjoorService.HealthCheckContents">
<summary> <summary>
examine site pages for broken links examine site pages for broken links
@ -21657,6 +21618,16 @@
short plain-text excerpt (first non-empty line) for a Tajik poem's next/previous nav link short plain-text excerpt (first non-empty line) for a Tajik poem's next/previous nav link
</summary> </summary>
</member> </member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.PrepareBilingualHtmlText(RMuseum.Models.Ganjoor.ViewModels.GanjoorVerseViewModel[])">
<summary>
Tajik counterpart of PrepareHtmlText (GanjoorService-SQLiteImport.cs) - same
VersePosition-based structure/CSS classes, but each verse's Persian original
(GanjoorVerse.OriginalText, set just before this is called) is rendered as an
extra &lt;p class="tg-fa" dir="rtl"&gt; line directly under its Tajik line, for
an interlinear/bilingual display. Computed fresh per-request rather than stored,
since it's cheap given the verses are already loaded for the response.
</summary>
</member>
<member name="M:RMuseum.Services.Implementation.GanjoorService.RegenerateTajikCatAndPoetHtmlTextAsync"> <member name="M:RMuseum.Services.Implementation.GanjoorService.RegenerateTajikCatAndPoetHtmlTextAsync">
<summary> <summary>
re-runs the Tajik poet/category HTML generators for every already-imported poet and re-runs the Tajik poet/category HTML generators for every already-imported poet and

View File

@ -633,31 +633,6 @@ namespace RMuseum.Services
/// <returns></returns> /// <returns></returns>
RServiceResult<bool> StartBatchGenerateGDBFiles(); RServiceResult<bool> StartBatchGenerateGDBFiles();
/// <summary>
/// start exporting all published data (poets/categories/poems/verses) to the public
/// git-tracked JSON data set, committing and pushing changes since the last run
/// </summary>
/// <returns></returns>
RServiceResult<bool> StartBatchExportPublicGitData();
/// <summary>
/// start exporting the Tajik (Cyrillic) overlay data to its own separate git-tracked JSON
/// data set — only entities that actually have a Tajik translation are included
/// </summary>
/// <returns></returns>
RServiceResult<bool> StartBatchExportTajikPublicGitData();
/// <summary>
/// (re)build local Ganjoor content (poets/categories/poems/verses/sections + their pages)
/// from a public data export tree, read locally or over HTTP. Safe to re-run — existing
/// entities (by id) are left untouched, only missing ones are added.
/// </summary>
/// <param name="useHttp">true: fetch over HTTP (location is a base URL). false: read from a local folder (location is a path).</param>
/// <param name="location">base URL or local folder path of the exported data tree</param>
/// <param name="poetId">0 imports every poet; a specific id imports only that poet (useful on a slow connection)</param>
/// <returns></returns>
RServiceResult<bool> StartImportFromPublicDataRepo(bool useHttp, string location, int poetId = 0);
/// <summary> /// <summary>
/// examine site pages for broken links /// examine site pages for broken links
/// </summary> /// </summary>

View File

@ -180,8 +180,13 @@ namespace RMuseum.Services.Implementation
var tajikVerses = await _context.TajikVerses.AsNoTracking().Where(v => v.PoemId == page.Poem.Id).OrderBy(v => v.VOrder).ToListAsync(); var tajikVerses = await _context.TajikVerses.AsNoTracking().Where(v => v.PoemId == page.Poem.Id).OrderBy(v => v.VOrder).ToListAsync();
foreach( var verse in page.Poem.Verses) foreach( var verse in page.Poem.Verses)
{ {
verse.OriginalText = verse.Text;
verse.Text = tajikVerses.Where(v => v.VOrder == verse.VOrder).Single().TajikText; verse.Text = tajikVerses.Where(v => v.VOrder == verse.VOrder).Single().TajikText;
} }
// recompute fresh on every request (verses are already in memory) rather than
// relying on the precomputed TajikHtmlText blob - keeps this in sync with the
// verse data automatically, no regeneration job needed if it ever changes again
page.HtmlText = PrepareBilingualHtmlText(page.Poem.Verses);
if (page.Poem.Recitations != null) if (page.Poem.Recitations != null)
{ {
@ -245,6 +250,101 @@ namespace RMuseum.Services.Implementation
return firstLine.Length > 80 ? firstLine.Substring(0, 80) + "…" : firstLine; return firstLine.Length > 80 ? firstLine.Substring(0, 80) + "…" : firstLine;
} }
/// <summary>
/// Tajik counterpart of PrepareHtmlText (GanjoorService-SQLiteImport.cs) - same
/// VersePosition-based structure/CSS classes, but each verse's Persian original
/// (GanjoorVerse.OriginalText, set just before this is called) is rendered as an
/// extra &lt;p class="tg-fa" dir="rtl"&gt; line directly under its Tajik line, for
/// an interlinear/bilingual display. Computed fresh per-request rather than stored,
/// since it's cheap given the verses are already loaded for the response.
/// </summary>
private static string PrepareBilingualHtmlText(GanjoorVerseViewModel[] verses)
{
string FaLine(string text) => string.IsNullOrEmpty(text) ? "" : $"<p class=\"tg-fa\" dir=\"rtl\">{text}</p>";
string htmlText = "";
int coupletIndex = 0;
for (int vIndex = 0; vIndex < verses.Length; vIndex++)
{
GanjoorVerseViewModel v = verses[vIndex];
if (v.VersePosition == VersePosition.CenteredVerse1)
{
coupletIndex++;
if (((vIndex + 1) < verses.Length) && (verses[vIndex + 1].VersePosition == VersePosition.CenteredVerse2))
{
htmlText += $"<div class=\"b2\" id=\"bn{coupletIndex}\"><p>{v.Text}</p>{FaLine(v.OriginalText)}{Environment.NewLine}";
}
else
{
htmlText += $"<div class=\"b2\" id=\"bn{coupletIndex}\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";
}
}
else
if (v.VersePosition == VersePosition.CenteredVerse2)
{
htmlText += $"<p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";
}
else
if (v.VersePosition == VersePosition.Right)
{
coupletIndex++;
htmlText += $"<div class=\"b\" id=\"bn{coupletIndex}\"><div class=\"m1\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";
}
else
if (v.VersePosition == VersePosition.Left)
{
htmlText += $"<div class=\"m2\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div></div>{Environment.NewLine}";
}
else
if (v.VersePosition == VersePosition.Comment)
{
htmlText += $"<div class=\"c\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";
}
else
if (v.VersePosition == VersePosition.Paragraph || v.VersePosition == VersePosition.Single)
{
coupletIndex++;
string[] lines = v.Text.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
string[] faLines = string.IsNullOrEmpty(v.OriginalText) ? new string[0] : v.OriginalText.Split(new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
string cssClass = v.VersePosition == VersePosition.Paragraph ? "n" : "l";
if (lines.Length != 0)
{
if (v.Text.Length / lines.Length < 150)
{
htmlText += $"<div class=\"{cssClass}\" id=\"bn{coupletIndex}\"><p>{v.Text.Replace("\r\n", " ")}</p>{FaLine(v.OriginalText?.Replace("\r\n", " "))}</div>{Environment.NewLine}";
}
else
{
// Persian original might not split into the exact same number of
// lines as its Tajik translation - matched by index on a best-effort
// basis; this branch only fires for long paragraph/note text, not the
// actual poem verses (Right/Left/CenteredVerse), so a rare misalignment
// here is a minor cosmetic issue, not a poem-text-accuracy one
for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++)
{
string faOne = lineIndex < faLines.Length ? faLines[lineIndex] : "";
htmlText += $"<div class=\"{cssClass}\" id=\"bn{coupletIndex}\"><p>{lines[lineIndex]}</p>{FaLine(faOne)}</div>{Environment.NewLine}";
}
}
}
else
{
if (string.IsNullOrEmpty(v.Text))
{
htmlText += $"<div class=\"{cssClass}\" id=\"bn{coupletIndex}\"><p>&nbsp;</p></div>{Environment.NewLine}";//empty line!
}
else
{
htmlText += $"<div class=\"{cssClass}\" id=\"bn{coupletIndex}\"><p>{v.Text}</p>{FaLine(v.OriginalText)}</div>{Environment.NewLine}";//not brave enough to ignore it!
}
}
}
}
return htmlText.Trim();
}
/// <summary> /// <summary>
/// re-runs the Tajik poet/category HTML generators for every already-imported poet and /// re-runs the Tajik poet/category HTML generators for every already-imported poet and
/// category page, overwriting their stored TajikHtmlText. Needed one-time after changing /// category page, overwriting their stored TajikHtmlText. Needed one-time after changing