Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions se5/BrazilianToEuropeanPortuguese.Tests/BatchModeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,38 @@ private static PluginRequest Request(params string[] texts)
private static List<string> Texts(PluginResponse response) =>
SubRipParser.Parse(response.Subtitle!.Native).Select(b => b.Text).ToList();

[Fact]
public void Batch_SpokenInformationUsesAdjacentNewsAndKeepsDialogueAndTimings()
{
var source = new List<SrtBlock>
{
new() { StartMs = 17126, EndMs = 18792, Text = "Más notícias." },
new() { StartMs = 18792, EndMs = 20887, Text = "-Como sabe?\n-Só sei. Dê-me." },
};
var english = new List<SrtBlock>
{
new() { StartMs = 17251, EndMs = 18752, Text = "Bad news." },
new() { StartMs = 18786, EndMs = 19653, Text = "How do you know?" },
new() { StartMs = 19687, EndMs = 20821, Text = "I just know. Gimme." },
};
var request = Request();
request.Subtitle.SubRip = SubRipParser.Serialize(source);
request.Subtitle.FileName = Path.Combine(_folder, "Show.S01E01.pt-br.srt");
File.WriteAllText(Path.Combine(_folder, "Show.S01E01.en.srt"), SubRipParser.Serialize(english));
request.SettingsVersion = ConversionOptions.Version;
request.Settings = new ConversionOptions(Address: AddressMode.Formal).ToJson();
var response = BatchMode.BuildBatchResponse(request, WordListPath, new EmbeddingSettings(CheckMeaning: false));
Assert.Equal(new[] { "Más notícias.", "-Como sabe?\n-Simplesmente sei. Diga-me." }, Texts(response));
Assert.Equal(source.Select(b => (b.StartMs, b.EndMs)),
SubRipParser.Parse(response.Subtitle!.Native).Select(b => (b.StartMs, b.EndMs)));
var aligned = ReferenceAligner.AlignByTiming(source, english);
source[1].StartMs = 26000;
source[1].EndMs = 28000;
var distant = SubtitleConversion.Convert(source, new ConversionOptions(Address: AddressMode.Formal),
english: aligned.Texts);
Assert.DoesNotContain(distant[1].Rules, r => r.Id == "spoken-information");
}

[Fact]
public void Batch_EnglishSportswearPreservesCueTimingAndReferenceSwitch()
{
Expand Down
62 changes: 62 additions & 0 deletions se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,68 @@ public sealed class ConstructionTests
{
private static readonly PortugueseConverter Converter = new();

[Theory]
[InlineData("-Como sabe?\n-Só sei. Dê-me.", "Más notícias.", "How do you know? I just know. Gimme.",
"-Como sabe?\n-Simplesmente sei. Diga-me.")]
[InlineData("Dê-me.", "Tenho novidades.", "Give it to me.", "Diga-me.")]
[InlineData("Dá-me!", "Os detalhes?", "Give it to me straight!", "Diz-me!")]
[InlineData("Deem-me.", "A verdade.", "Give it to me.", "Digam-me.")]
[InlineData("Dai-me.", "Uma explicação.", "Gimme.", "Dizei-me.")]
[InlineData("Dê-me.", null, "Give me the bad news.", "Diga-me.")]
[InlineData("Dê-me.", null, "Give me the details.", "Diga-me.")]
[InlineData("DÊ-ME.", "MÁS NOTÍCIAS.", "Gimme.", "DIGA-ME.")]
[InlineData("<i>Só sei. Dê-me.</i>", "Más notícias.", "I just know. Gimme.",
"<i>Simplesmente sei. Diga-me.</i>")]
public void FigurativeGiveMeMeansTellMeInInformationContext(string source, string? previous, string english, string expected)
{
var reference = ReferenceHint.FromText(english)! with { FormalAddress = true };
var trace = new List<RuleChange>();
Assert.Equal(expected, Converter.Convert(source, previous, reference, trace));
Assert.Equal(expected, Converter.Convert(expected, previous, reference));
Assert.Contains(trace, r => r.Id == "spoken-information");
foreach (var options in new[] { new ConversionOptions(Colloquial: false),
new ConversionOptions(Vocabulary: false), new ConversionOptions(DisabledRules: "spoken-information") })
{
var disabledTrace = new List<RuleChange>();
new PortugueseConverter(options).Convert(source, previous, reference, disabledTrace);
Assert.DoesNotContain(disabledTrace, r => r.Id == "spoken-information");
}
}

[Fact]
public void SpokenInformationFollowsExplicitInformalAddress()
{
var reference = ReferenceHint.FromText("I just know. Gimme.")! with { ForceInformal = true };
Assert.Equal("Simplesmente sei. Diz-me.", Converter.Convert("Só sei. Dê-me.", "Más notícias.", reference));
}

[Theory]
[InlineData("Dê-me.", null, "Gimme.")]
[InlineData("Dê-me.", "Más notícias.", null)]
[InlineData("Dê-me.", "A arma.", "Gimme.")]
[InlineData("Dê-me.", "Más notícias.", "Give me the gun.")]
[InlineData("Dê-me.", "Más notícias.", "Don't give it to me.")]
[InlineData("Dê-me.", "Más notícias.", "Did you give it to me?")]
[InlineData("Dê-me.", "Más notícias.", "He said give it to me.")]
[InlineData("Dê-me as más notícias.", "Más notícias.", "Give me the bad news.")]
[InlineData("Dê-me o jornal.", "Más notícias.", "Gimme.")]
[InlineData("Não dê-me.", "Más notícias.", "Gimme.")]
[InlineData("Se ele disser dê-me.", "Más notícias.", "Gimme.")]
[InlineData("Dê-me. Dê-me.", "Más notícias.", "Gimme.")]
[InlineData("Dê-me uma explicação.", "A verdade.", "Give me the explanation.")]
[InlineData("Só sei.", "Más notícias.", "I just know.")]
[InlineData("Dê-me.", "O jornal tem más notícias.", "Gimme.")]
public void SpokenInformationPreservesLiteralCompleteAndUnconfirmedRequests(string source, string? previous, string? english)
{
var reference = ReferenceHint.FromText(english);
Assert.Equal(source, ConversionConstructions.SpokenInformation(source, previous, reference));
var trace = new List<RuleChange>();
var converted = Converter.Convert(source, previous, reference, trace);
Assert.Equal(new PortugueseConverter(new ConversionOptions(DisabledRules: "spoken-information"))
.Convert(source, previous, reference), converted);
Assert.DoesNotContain(trace, r => r.Id == "spoken-information");
}

[Theory]
[InlineData("Você é meu vizinho de andar.", "Hey, we live on the same floor.", "Vivemos no mesmo andar.")]
[InlineData("És meu vizinho de andar.", "We live on the same floor.", "Vivemos no mesmo andar.")]
Expand Down
34 changes: 34 additions & 0 deletions se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,40 @@ public static string NaturalWordOrder(string text)
public static string PhysicalDescriptions(string text) => PersonMeasurement.Replace(text, m =>
m.Value[..^m.Groups["pre"].Length] + CaseHelper.MatchCase(m.Groups["pre"].Value, "com"));

private static readonly Regex InformationTopic = new(
@"^(?:(?:tenho|temos|há|são|e|quais\s+são)\s+)?(?:(?:o|a|os|as|um|uma|uns|umas|ess[ea]s?|est[ea]s?)\s+)?" +
@"(?:(?:boas?|más?|últimas?|novas?)\s+)?(?:notícias|novidades|informações|detalhes|verdade|explicação)" +
@"\s*[.!?…]?$", Ci);
private static readonly Regex GiveInformationReference = new(
@"(?:^|[.!?]\s+)(?<request>gimme|give\s+(?:it\s+to\s+me|me))" +
@"(?<explicit>\s+(?:the\s+)?(?:(?:good|bad)\s+)?(?:news|details|truth|information|explanation|update))?" +
@"(?:\s+straight)?[.!…]?$", Ci);
private static readonly Regex BareGiveMe = new(
@"(?<head>(?:^|[.!?…]\s*|\n)[\uE000-\uF8FF \t]*(?:[-–—]\s*)?)" +
@"(?<verb>dê|dá|deem|dai)-me(?=[.!…](?:\s|[\uE000-\uF8FF]|$)|\s*[\uE000-\uF8FF]*$)", Ci);
private static readonly Regex JustKnow = new(
@"(?<![\p{L}\p{M}])(?<just>só)(?<gap>\s+)sei(?=\s*[.!…]\s*(?:diga|diz|digam|dizei)-me\b)", Ci);

public static string SpokenInformation(string text, string? previous, ReferenceHint? reference)
{
var english = reference?.EnglishText;
if (english == null) return text;
var request = GiveInformationReference.Match(english);
if (!request.Success || !request.Groups["explicit"].Success &&
!InformationTopic.IsMatch(ReferenceAligner.Clean(previous ?? ""))) return text;
if (BareGiveMe.Matches(text).Count != 1) return text;
text = BareGiveMe.Replace(text, m => m.Groups["head"].Value + CaseHelper.MatchCase(m.Groups["verb"].Value,
m.Groups["verb"].Value.ToLowerInvariant() switch
{
"dê" => "diga", "dá" => "diz", "deem" => "digam", _ => "dizei",
}) + (CaseHelper.IsAllUpper(m.Groups["verb"].Value) ? "-ME" : "-me"));
if (Regex.IsMatch(english, @"(?:^|[.!?]\s+)i\s+just\s+know[.!…]\s+", Ci))
text = JustKnow.Replace(text, m =>
CaseHelper.MatchCase(m.Groups["just"].Value, "simplesmente") + m.Groups["gap"].Value +
(CaseHelper.IsAllUpper(m.Value) ? "SEI" : "sei"));
return text;
}

private static readonly Regex SharedResidenceReference = new(
@"^(?:(?:hey|hi|hello|well)[,!]?\s+)?we\s+(?<verb>live|lived)\s+" +
@"(?:on|in)\s+the\s+same\s+(?<place>floor|building|street|block|neighbou?rhood)[.!…]?$", Ci);
Expand Down
4 changes: 4 additions & 0 deletions se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -771,6 +771,10 @@ void Track(string id, string description)
}
Track("reflexive-clitics", "Reflexives and clitic allomorphs");

if (_options.Colloquial && _options.Vocabulary)
s = ConversionConstructions.SpokenInformation(s, previousText, reference);
Track("spoken-information", "English-confirmed figurative give me requests about information");

if (_options.Colloquial) s = ConversionConstructions.NaturalWordOrder(s);
Track("natural-word-order", "Habitual sempre after lexical verbs and recoverable progressive subjects");

Expand Down
12 changes: 12 additions & 0 deletions se5/BrazilianToEuropeanPortuguese/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,18 @@ These rules change Portuguese constructions, rather than merely adding review wa
are not blanket replacements. Fato(s) de treino/mergulho are protected from the
unrelated Brazilian *fato* → *facto* spelling conversion, including on reruns.

- **Spoken information requests:** English *Gimme / Give it to me* with a recent
short Portuguese topic cue about news, details, truth or an explanation turns
an isolated *Dê-me* into *Diga-me* (informal *Dá-me* → *Diz-me*). An explicit
English information object (*Give me the bad news*) also supplies this evidence.
In that guarded construction, *Só sei* with *I just know* becomes *Simplesmente
sei*. Dialogue lines, punctuation and formal/informal address are retained.
Complete objects such as *Dê-me as más notícias*, literal handovers, missing
English, questions, negation and repeated ambiguous requests are not rewritten.
Cross-cue context uses the existing five-second adjacency limit. This requires
**Colloquial** and **Vocabulary** and can be disabled as `spoken-information`.
Timing alignment can merge two English cues into one Portuguese dialogue cue;
no MiniLM or generative model is required for this correction.
- **Shared-residence paraphrases:** when the English explicitly says *We live on
the same floor*, *Você é meu vizinho de andar* becomes *Vivemos no mesmo andar*.
The `shared-residence` stage supports matching floor/building/street/block/
Expand Down
Loading