diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/BatchModeTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/BatchModeTests.cs index a0a6cc0d..d9f5be16 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/BatchModeTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/BatchModeTests.cs @@ -28,6 +28,38 @@ private static PluginRequest Request(params string[] texts) private static List Texts(PluginResponse response) => SubRipParser.Parse(response.Subtitle!.Native).Select(b => b.Text).ToList(); + [Fact] + public void Batch_SpokenInformationUsesAdjacentNewsAndKeepsDialogueAndTimings() + { + var source = new List + { + 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 + { + 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() { diff --git a/se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs b/se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs index 909bffe4..7334729f 100644 --- a/se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs +++ b/se5/BrazilianToEuropeanPortuguese.Tests/ConstructionTests.cs @@ -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("Só sei. Dê-me.", "Más notícias.", "I just know. Gimme.", + "Simplesmente sei. Diga-me.")] + public void FigurativeGiveMeMeansTellMeInInformationContext(string source, string? previous, string english, string expected) + { + var reference = ReferenceHint.FromText(english)! with { FormalAddress = true }; + var trace = new List(); + 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(); + 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(); + 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.")] diff --git a/se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs b/se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs index 43890a26..764b15c8 100644 --- a/se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs +++ b/se5/BrazilianToEuropeanPortuguese/ConversionConstructions.cs @@ -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+)(?gimme|give\s+(?:it\s+to\s+me|me))" + + @"(?\s+(?:the\s+)?(?:(?:good|bad)\s+)?(?:news|details|truth|information|explanation|update))?" + + @"(?:\s+straight)?[.!…]?$", Ci); + private static readonly Regex BareGiveMe = new( + @"(?(?:^|[.!?…]\s*|\n)[\uE000-\uF8FF \t]*(?:[-–—]\s*)?)" + + @"(?dê|dá|deem|dai)-me(?=[.!…](?:\s|[\uE000-\uF8FF]|$)|\s*[\uE000-\uF8FF]*$)", Ci); + private static readonly Regex JustKnow = new( + @"(?só)(?\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+(?live|lived)\s+" + @"(?:on|in)\s+the\s+same\s+(?floor|building|street|block|neighbou?rhood)[.!…]?$", Ci); diff --git a/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs b/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs index 06cedd96..2aa6c9ad 100644 --- a/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs +++ b/se5/BrazilianToEuropeanPortuguese/PortugueseConverter.cs @@ -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"); diff --git a/se5/BrazilianToEuropeanPortuguese/README.md b/se5/BrazilianToEuropeanPortuguese/README.md index 60b545eb..d735f73e 100644 --- a/se5/BrazilianToEuropeanPortuguese/README.md +++ b/se5/BrazilianToEuropeanPortuguese/README.md @@ -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/