Skip to content

Commit 20bbbec

Browse files
authored
Add support for per-chapter remarks (#934)
* Add support for per-chapter remarks * Updated per-chapter remarks to match new wording
1 parent dc1b0db commit 20bbbec

3 files changed

Lines changed: 155 additions & 147 deletions

File tree

src/Serval/src/Serval.Translation/Services/UsfmGenerationService.cs

Lines changed: 116 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using SIL.Machine.Corpora;
1+
using SIL.Machine.Corpora;
22
using SIL.Machine.PunctuationAnalysis;
33
using SIL.Machine.Translation;
44
using SIL.Scripture;
@@ -17,7 +17,7 @@ ContractMapper contractMapper
1717
private readonly IRepository<Build> _builds = builds;
1818
private readonly ContractMapper _contractMapper = contractMapper;
1919
private const string AIDisclaimerRemark =
20-
"This draft of {0} was generated using AI on {1}. It should be reviewed and edited carefully.";
20+
"This draft of {0} was generated by AI from {1} on {2}. It should be reviewed carefully for errors before use. {3}";
2121

2222
public async Task<string> GetUsfmAsync(
2323
string engineId,
@@ -76,24 +76,61 @@ public async Task<string> GetUsfmAsync(
7676
Build? build = (await _builds.GetAllAsync(b => b.EngineRef == engineId, cancellationToken))
7777
.OrderByDescending(b => b.DateFinished)
7878
.FirstOrDefault();
79-
if (build is null || build.DateFinished is null)
79+
if (build?.DateFinished is null)
8080
throw new InvalidOperationException($"Could not find any completed builds for engine '{engineId}'.");
8181

82-
string disclaimerRemark = string.Format(
83-
CultureInfo.InvariantCulture,
84-
AIDisclaimerRemark,
85-
textId,
86-
build.DateFinished.Value.ToUniversalTime().ToString("u")
87-
);
8882
string markerPlacementRemark = GenerateMarkerPlacementRemark(
8983
paragraphMarkerBehavior,
9084
embedBehavior,
9185
styleMarkerBehavior
9286
);
9387

94-
List<string> remarks = [disclaimerRemark, markerPlacementRemark];
88+
ParallelCorpusContract[] parallelCorpora = [.. _contractMapper.Map(build, engine)];
89+
90+
// Get the versification for the project
91+
CorpusBundle corpusBundle = new(parallelCorpora);
92+
ParallelCorpusContract corpusContract = corpusBundle.ParallelCorpora.Single(c => c.Id == corpusId);
93+
CorpusFileContract sourceFile = corpusContract.SourceCorpora[0].Files[0];
94+
ParatextProjectSettings? sourceSettings = corpusBundle.GetSettings(sourceFile.Location);
95+
ScrVers versification = sourceSettings?.Versification ?? ScrVers.Original;
96+
var scriptureRangeParser = new ScriptureRangeParser(versification);
97+
98+
// Generate remarks for every chapter in the book
99+
List<(int, string)> remarks = [];
100+
List<int>? chapters =
101+
build
102+
.Pretranslate?.SelectMany(p => p.SourceFilters ?? [])
103+
.SelectMany(s =>
104+
scriptureRangeParser
105+
.GetChapters(s.ScriptureRange)
106+
.TryGetValue(textId, out List<int>? filterChapters)
107+
? filterChapters
108+
: []
109+
)
110+
.ToList()
111+
?? [];
112+
113+
// If there are no chapters, we need to set it to null so that the USFM updater
114+
if (chapters.Count == 0)
115+
chapters = null;
95116

96-
ParallelCorpusContract[] parallelCorpora = _contractMapper.Map(build, engine).ToArray();
117+
// Get all the chapters needing remarks
118+
IEnumerable<int> chaptersNeedingRemarks =
119+
chapters ?? Enumerable.Range(1, versification.GetLastChapter(Canon.BookIdToNumber(textId)));
120+
121+
// Add remarks to each chapter
122+
foreach (int chapterNum in chaptersNeedingRemarks)
123+
{
124+
string disclaimerRemark = string.Format(
125+
CultureInfo.InvariantCulture,
126+
AIDisclaimerRemark,
127+
$"{textId} {chapterNum}",
128+
sourceSettings?.Name ?? "Unknown",
129+
build.DateFinished.Value.ToUniversalTime().ToString("u"),
130+
markerPlacementRemark
131+
);
132+
remarks.Add((chapterNum, disclaimerRemark));
133+
}
97134

98135
IReadOnlyList<Pretranslation> pretranslations = await _pretranslations.GetAllAsync(
99136
pt =>
@@ -126,6 +163,7 @@ public async Task<string> GetUsfmAsync(
126163
corpusId,
127164
textId,
128165
textOrigin == PretranslationUsfmTextOrigin.OnlyExisting ? [] : pretranslations,
166+
chapters,
129167
textBehavior,
130168
Map(paragraphMarkerBehavior),
131169
Map(embedBehavior),
@@ -146,6 +184,7 @@ public async Task<string> GetUsfmAsync(
146184
corpusId,
147185
textId,
148186
textOrigin == PretranslationUsfmTextOrigin.OnlyExisting ? [] : pretranslations,
187+
chapters,
149188
Map(paragraphMarkerBehavior),
150189
Map(embedBehavior),
151190
Map(styleMarkerBehavior),
@@ -163,11 +202,12 @@ private static string UpdateSourceUsfm(
163202
string corpusId,
164203
string bookId,
165204
IReadOnlyList<Pretranslation> pretranslations,
205+
IReadOnlyList<int>? chapters,
166206
UpdateUsfmMarkerBehavior paragraphBehavior,
167207
UpdateUsfmMarkerBehavior embedBehavior,
168208
UpdateUsfmMarkerBehavior styleBehavior,
169209
bool placeParagraphMarkers,
170-
IEnumerable<string>? remarks,
210+
IEnumerable<(int, string)> remarks,
171211
string? targetQuoteConvention
172212
)
173213
{
@@ -176,6 +216,7 @@ private static string UpdateSourceUsfm(
176216
corpusId,
177217
bookId,
178218
pretranslations,
219+
chapters,
179220
UpdateUsfmTextBehavior.StripExisting,
180221
paragraphBehavior,
181222
embedBehavior,
@@ -192,11 +233,12 @@ private static string UpdateTargetUsfm(
192233
string corpusId,
193234
string bookId,
194235
IReadOnlyList<Pretranslation> pretranslations,
236+
IReadOnlyList<int>? chapters,
195237
UpdateUsfmTextBehavior textBehavior,
196238
UpdateUsfmMarkerBehavior paragraphBehavior,
197239
UpdateUsfmMarkerBehavior embedBehavior,
198240
UpdateUsfmMarkerBehavior styleBehavior,
199-
IEnumerable<string>? remarks,
241+
IEnumerable<(int, string)> remarks,
200242
string? targetQuoteConvention
201243
)
202244
{
@@ -205,6 +247,7 @@ private static string UpdateTargetUsfm(
205247
corpusId,
206248
bookId,
207249
pretranslations,
250+
chapters,
208251
textBehavior,
209252
paragraphBehavior,
210253
embedBehavior,
@@ -221,12 +264,13 @@ private static string UpdateUsfm(
221264
string corpusId,
222265
string bookId,
223266
IEnumerable<Pretranslation> pretranslations,
267+
IReadOnlyList<int>? chapters,
224268
UpdateUsfmTextBehavior textBehavior,
225269
UpdateUsfmMarkerBehavior paragraphBehavior,
226270
UpdateUsfmMarkerBehavior embedBehavior,
227271
UpdateUsfmMarkerBehavior styleBehavior,
228272
IEnumerable<IUsfmUpdateBlockHandler>? updateBlockHandlers,
229-
IEnumerable<string>? remarks,
273+
IEnumerable<(int, string)> remarks,
230274
string? targetQuoteConvention,
231275
bool isSource
232276
)
@@ -244,33 +288,36 @@ bool isSource
244288
string usfm =
245289
updater.UpdateUsfm(
246290
bookId,
247-
pretranslations
248-
.Select(p =>
249-
Map(
250-
p,
251-
isSource,
252-
sourceSettings?.Versification,
253-
targetSettings?.Versification,
254-
paragraphBehavior,
255-
styleBehavior
291+
[
292+
.. pretranslations
293+
.Select(p =>
294+
Map(
295+
p,
296+
isSource,
297+
sourceSettings?.Versification,
298+
targetSettings?.Versification,
299+
paragraphBehavior,
300+
styleBehavior
301+
)
256302
)
257-
)
258-
.Where(row => row.Refs.Any())
259-
.OrderBy(row => row.Refs[0])
260-
.ToArray(),
303+
.Where(row => row.Refs.Any())
304+
.OrderBy(row => row.Refs[0]),
305+
],
306+
chapters: chapters,
261307
fullName: isSource ? sourceSettings?.FullName : targetSettings?.FullName,
262308
textBehavior: textBehavior,
263309
paragraphBehavior: paragraphBehavior,
264310
embedBehavior: embedBehavior,
265311
styleBehavior: styleBehavior,
312+
preserveParagraphStyles: null,
266313
updateBlockHandlers: updateBlockHandlers,
267-
remarks: remarks?.Select(r => (0, r)),
314+
!string.IsNullOrEmpty(targetQuoteConvention) ? null : remarks, // Ensure we only add remarks once
268315
errorHandler: (_) => true,
269316
compareSegments: isSource
270317
) ?? "";
271318

272319
if (!string.IsNullOrEmpty(targetQuoteConvention))
273-
usfm = DenormalizeQuotationMarks(usfm, targetQuoteConvention);
320+
usfm = DenormalizeQuotationMarks(usfm, targetQuoteConvention, remarks);
274321
return usfm;
275322
}
276323

@@ -351,7 +398,11 @@ pretranslation.Alignment is null
351398
return matrix;
352399
}
353400

354-
private static string DenormalizeQuotationMarks(string usfm, string quoteConvention)
401+
private static string DenormalizeQuotationMarks(
402+
string usfm,
403+
string quoteConvention,
404+
IEnumerable<(int, string)> remarks
405+
)
355406
{
356407
QuoteConvention targetQuoteConvention = QuoteConventions.Standard.GetQuoteConventionByName(quoteConvention);
357408
if (targetQuoteConvention is null)
@@ -372,79 +423,35 @@ private static string DenormalizeQuotationMarks(string usfm, string quoteConvent
372423
int denormalizableChapterCount = bestChapterStrategies.Count(tup =>
373424
tup.Strategy != QuotationMarkUpdateStrategy.Skip
374425
);
375-
List<string> remarks = [];
376-
string quotationDenormalizationRemark;
377-
if (denormalizableChapterCount == bestChapterStrategies.Count)
378-
{
379-
quotationDenormalizationRemark =
380-
"The quote style in all chapters has been automatically adjusted to match the rest of the project.";
381-
}
382-
else if (denormalizableChapterCount > 0)
383-
{
384-
quotationDenormalizationRemark =
385-
"The quote style in the following chapters has been automatically adjusted to match the rest of the project: "
386-
+ GetChapterRangesString(
387-
bestChapterStrategies
388-
.Where(tuple => tuple.Strategy != QuotationMarkUpdateStrategy.Skip)
389-
.Select(tuple => tuple.ChapterNumber)
390-
.ToList()
391-
)
392-
+ ".";
393-
}
394-
else
426+
const string QuotationDenormalizationRemark =
427+
"Quotation marks have been adjusted automatically to match the rest of the project.";
428+
List<(int Chapter, string Remark)> combinedRemarks = [.. remarks];
429+
for (int i = 1; i <= denormalizableChapterCount; i++)
395430
{
396-
quotationDenormalizationRemark =
397-
"The quote style was not automatically adjusted to match the rest of your project in any chapters.";
431+
int index = combinedRemarks.FindLastIndex(r => r.Chapter == i);
432+
if (index > -1)
433+
{
434+
combinedRemarks[index] = combinedRemarks[index] with
435+
{
436+
Remark = $"{combinedRemarks[index].Remark} {QuotationDenormalizationRemark}",
437+
};
438+
}
439+
else
440+
{
441+
combinedRemarks.Add((i, QuotationDenormalizationRemark));
442+
}
398443
}
399-
remarks.Add(quotationDenormalizationRemark);
400444

401445
var updater = new UpdateUsfmParserHandler(
402446
updateBlockHandlers: [quotationMarkDenormalizer],
403-
remarks: remarks.Select(r => (0, r))
447+
remarks: combinedRemarks
404448
);
405449
UsfmParser.Parse(usfm, updater);
406450

407451
usfm = updater.GetUsfm();
408452
return usfm;
409453
}
410454

411-
internal static string GetChapterRangesString(List<int> chapterNumbers)
412-
{
413-
chapterNumbers = chapterNumbers.Order().ToList();
414-
int start = chapterNumbers[0];
415-
int end = chapterNumbers[0];
416-
List<string> chapterRangeStrings = [];
417-
foreach (int chapterNumber in chapterNumbers[1..])
418-
{
419-
if (chapterNumber == end + 1)
420-
{
421-
end = chapterNumber;
422-
}
423-
else
424-
{
425-
if (start == end)
426-
{
427-
chapterRangeStrings.Add(start.ToString(CultureInfo.InvariantCulture));
428-
}
429-
else
430-
{
431-
chapterRangeStrings.Add($"{start}-{end}");
432-
}
433-
start = chapterNumber;
434-
end = chapterNumber;
435-
}
436-
}
437-
if (start == end)
438-
{
439-
chapterRangeStrings.Add(start.ToString(CultureInfo.InvariantCulture));
440-
}
441-
else
442-
{
443-
chapterRangeStrings.Add($"{start}-{end}");
444-
}
445-
return string.Join(", ", chapterRangeStrings);
446-
}
447-
448455
/// <summary>
449456
/// Generate a natural sounding remark/comment describing marker placement.
450457
/// </summary>
@@ -456,13 +463,13 @@ internal static string GetChapterRangesString(List<int> chapterNumbers)
456463
/// <para>Remarks are generated in the format:</para>
457464
/// <list type="bullet">
458465
/// <item><description>
459-
/// Paragraph breaks, embed markers, and style markers were moved to the end of the verse.
466+
/// Paragraph breaks, embed markers, and style marker positions were moved to the end of the verse.
460467
/// </description></item>
461468
/// <item><description>
462-
/// Paragraph breaks were moved to the end of the verse. Embed markers have positions preserved. Style markers were removed.
469+
/// Paragraph break positions were moved to the end of the verse. Embed marker positions have been preserved. Style markers were removed.
463470
/// </description></item>
464471
/// <item><description>
465-
/// Paragraph breaks and style markers were moved to the end of the verse. Embed markers were removed.
472+
/// Paragraph break and style marker positions were moved to the end of the verse. Embed markers were removed.
466473
/// </description></item>
467474
/// </list>
468475
/// </remarks>
@@ -479,25 +486,29 @@ PretranslationUsfmMarkerBehavior styleMarkerBehavior
479486
{ PretranslationUsfmMarkerBehavior.Strip, [] },
480487
};
481488

482-
behaviorMap[paragraphMarkerBehavior].Add("paragraph breaks");
483-
behaviorMap[embedBehavior].Add("embed markers");
484-
behaviorMap[styleMarkerBehavior].Add("style markers");
489+
behaviorMap[paragraphMarkerBehavior].Add("paragraph break");
490+
behaviorMap[embedBehavior].Add("embed marker");
491+
behaviorMap[styleMarkerBehavior].Add("style marker");
485492

486493
IEnumerable<string> sentences = behaviorMap
487494
.Where(kvp => kvp.Value.Count > 0)
488495
.Select(kvp =>
489496
{
490-
string markers =
497+
string markersSingular =
491498
kvp.Value.Count == 1 ? kvp.Value[0] : string.Join(", ", kvp.Value[..^1]) + " and " + kvp.Value[^1];
492-
markers = char.ToUpperInvariant(markers[0]) + markers[1..];
493-
string behavior = kvp.Key switch
499+
string markersPlural =
500+
kvp.Value.Count == 1
501+
? kvp.Value[0] + "s"
502+
: string.Join(", ", kvp.Value[..^1] + "s") + " and " + kvp.Value[^1] + "s";
503+
string sentence = kvp.Key switch
494504
{
495-
PretranslationUsfmMarkerBehavior.Preserve => "were moved to the end of the verse",
496-
PretranslationUsfmMarkerBehavior.PreservePosition => "have positions preserved",
497-
PretranslationUsfmMarkerBehavior.Strip => "were removed",
498-
_ => "have unknown behavior",
505+
PretranslationUsfmMarkerBehavior.Preserve =>
506+
$"{markersSingular} positions were moved to the end of the verse.",
507+
PretranslationUsfmMarkerBehavior.PreservePosition => $"{markersSingular} positions were preserved.",
508+
PretranslationUsfmMarkerBehavior.Strip => $"{markersPlural} were removed.",
509+
_ => $"{markersPlural} have unknown behavior.",
499510
};
500-
return $"{markers} {behavior}.";
511+
return char.ToUpperInvariant(sentence[0]) + sentence[1..];
501512
});
502513

503514
return string.Join(" ", sentences);

src/Serval/test/Serval.ApiServer.IntegrationTests/TranslationEngineTests.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2291,10 +2291,9 @@ await _env.Builds.InsertAsync(
22912291
usfm.Replace("\r\n", "\n"),
22922292
Is.EqualTo(
22932293
@"\id MAT - Test1
2294-
\rem This draft of MAT was generated using AI on 1970-01-01 00:00:00Z. It should be reviewed and edited carefully.
2295-
\rem Paragraph breaks and embed markers were moved to the end of the verse. Style markers were removed.
22962294
\h
22972295
\c 1
2296+
\rem This draft of MAT 1 was generated by AI from Te1 on 1970-01-01 00:00:00Z. It should be reviewed carefully for errors before use. Paragraph break and embed marker positions were moved to the end of the verse. Style markers were removed.
22982297
\p
22992298
\v 1 translation
23002299
\v 2

0 commit comments

Comments
 (0)