blob: 7d62775ca71825b4028f95224727189ea9a6d690 [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/services/speech/speech_timestamp_estimator.h"
#include "base/time/time.h"
#include "base/types/zip.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace speech {
namespace {
using SpeechTimestamp = SpeechTimestampEstimator::SpeechTimestamp;
using PlaybackDuration = SpeechTimestampEstimator::PlaybackDuration;
using MediaTimestamp = SpeechTimestampEstimator::MediaTimestamp;
using MediaTimestampRange = media::MediaTimestampRange;
using MediaRanges = SpeechTimestampEstimator::MediaRanges;
using SpeechTimestampRange = std::pair<SpeechTimestamp, SpeechTimestamp>;
SpeechTimestampRange SpeechSecondsRange(int start, int end) {
return {SpeechTimestamp(base::Seconds(start)),
SpeechTimestamp(base::Seconds(end))};
}
MediaTimestampRange MediaSecondsRange(int start, int end) {
return {.start = base::Seconds(start), .end = base::Seconds(end)};
}
void VerifyRanges(const MediaRanges& actual_ranges,
const MediaRanges& expected_ranges) {
EXPECT_EQ(actual_ranges.size(), expected_ranges.size());
for (auto [actual, expected] : base::zip(actual_ranges, expected_ranges)) {
EXPECT_EQ(actual.start, expected.start);
EXPECT_EQ(actual.end, expected.end);
EXPECT_LT(actual.start, actual.end);
}
}
class SpeechTimestampEstimatorTest : public testing::Test {
public:
SpeechTimestampEstimatorTest() = default;
~SpeechTimestampEstimatorTest() override = default;
// Helper functions for taking the timestamps.
MediaRanges TakeRange(SpeechTimestampRange range) {
return estimator_.TakeTimestampsInRange(range.first, range.second);
}
MediaRanges TakeAllRanges() {
// Completely empties out `estimator_`.
return TakeRange(SpeechSecondsRange(0, base::Days(1).InSeconds()));
}
MediaRanges PeekRange(SpeechTimestampRange range) {
return estimator_.PeekTimestampsInRange(range.first, range.second);
}
void AppendDuration(base::TimeDelta duration) {
estimator_.AppendDuration(PlaybackDuration(duration));
}
void SkipSilence(base::TimeDelta duration) {
estimator_.OnSilentMediaDropped(PlaybackDuration(duration));
}
void AddNewPlayback(base::TimeDelta start) {
estimator_.AddPlaybackStart(MediaTimestamp(start));
}
private:
SpeechTimestampEstimator estimator_;
};
TEST_F(SpeechTimestampEstimatorTest, NoAudio) {
auto results = TakeAllRanges();
EXPECT_TRUE(results.empty());
}
TEST_F(SpeechTimestampEstimatorTest, NoPlayback) {
AppendDuration(base::Seconds(10));
auto results = TakeAllRanges();
EXPECT_TRUE(results.empty());
}
TEST_F(SpeechTimestampEstimatorTest, SinglePlayback_Basic) {
constexpr base::TimeDelta kPlaybackDuration = base::Seconds(10);
{
SCOPED_TRACE("[0s,10s)");
AddNewPlayback(base::Seconds(0));
AppendDuration(kPlaybackDuration);
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(0, 10)});
}
{
SCOPED_TRACE("[100s,110s)");
AddNewPlayback(base::Seconds(100));
AppendDuration(kPlaybackDuration);
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(100, 110)});
}
{
SCOPED_TRACE("[-1s,9s)");
AddNewPlayback(base::Seconds(-1));
AppendDuration(kPlaybackDuration);
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(-1, 9)});
}
{
SCOPED_TRACE("[1s,11s)");
AddNewPlayback(base::Seconds(1));
AppendDuration(kPlaybackDuration);
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(1, 11)});
}
}
TEST_F(SpeechTimestampEstimatorTest, SinglePlayback_MultiplePlaybackChunks) {
{
SCOPED_TRACE("[0s,10s) - two chunks");
AddNewPlayback(base::Seconds(0));
AppendDuration(base::Seconds(5));
AppendDuration(base::Seconds(5));
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(0, 10)});
}
{
SCOPED_TRACE("[0s,10s) - ten chunks");
AddNewPlayback(base::Seconds(0));
for (int i = 0; i < 10; ++i) {
AppendDuration(base::Seconds(1));
}
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(0, 10)});
}
}
TEST_F(SpeechTimestampEstimatorTest, SinglePlayback_OffsetStart) {
// "Play" 5s of audio, without playback info.
AppendDuration(base::Seconds(5));
// Add [100, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Take the first 10s of playback. Only 5 of those seconds contain playback
// info.
VerifyRanges(TakeRange(SpeechSecondsRange(0, 10)),
{MediaSecondsRange(100, 105)});
}
TEST_F(SpeechTimestampEstimatorTest, SinglePlayback_RangeLimits) {
constexpr int kPlaybackSeconds = 10;
// Add [100, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(kPlaybackSeconds));
// Only [0, kPlaybackSeconds) makes sense technically, so make sure ranges
// that are completely outside of those bounds are ignored.
EXPECT_TRUE(TakeRange(SpeechSecondsRange(-100, -50)).empty());
EXPECT_TRUE(TakeRange(SpeechSecondsRange(1000, 2000)).empty());
// Make sure ranges right at the boundary are ignored.
EXPECT_TRUE(
TakeRange(SpeechSecondsRange(kPlaybackSeconds, 2 * kPlaybackSeconds))
.empty());
// Ranges which include [0,kPlaybackSeconds) are allowed.
VerifyRanges(
TakeRange(SpeechSecondsRange(-kPlaybackSeconds, 10 * kPlaybackSeconds)),
{MediaSecondsRange(100, 110)});
}
TEST_F(SpeechTimestampEstimatorTest, SinglePlayback_AppendAfterDrainingFifo) {
constexpr base::TimeDelta kPlaybackDuration = base::Seconds(10);
// Add [100, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(kPlaybackDuration);
// Take all of the current range. Use a huge upper bound for good measure.
std::ignore = TakeRange(SpeechSecondsRange(0, base::Days(25).InSeconds()));
// Add another [110, 120s), without a AddNewPlayback() call.
AppendDuration(kPlaybackDuration);
VerifyRanges(TakeRange(SpeechSecondsRange(10, 20)),
{MediaSecondsRange(110, 120)});
}
TEST_F(SpeechTimestampEstimatorTest, SinglePlayback_PartialRanges) {
// Add [100, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
VerifyRanges(TakeRange(SpeechSecondsRange(0, 5)),
{MediaSecondsRange(100, 105)});
// Getting the same range twice should yield empty results
EXPECT_TRUE(TakeRange(SpeechSecondsRange(0, 5)).empty());
VerifyRanges(TakeRange(SpeechSecondsRange(5, 10)),
{MediaSecondsRange(105, 110)});
}
TEST_F(SpeechTimestampEstimatorTest,
SinglePlayback_LaterRangesDiscardPreviousRanges) {
// Add [100, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Add [200, 210s).
AddNewPlayback(base::Seconds(200));
AppendDuration(base::Seconds(10));
VerifyRanges(TakeRange(SpeechSecondsRange(15, 20)),
{MediaSecondsRange(205, 210)});
// All ranges before `base::Seconds(15)` should be discarded.
EXPECT_TRUE(TakeRange(SpeechSecondsRange(0, 10)).empty());
EXPECT_TRUE(TakeRange(SpeechSecondsRange(10, 15)).empty());
}
TEST_F(SpeechTimestampEstimatorTest, MultiplePlaybacks_Simple) {
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Add [120s, 140s).
AddNewPlayback(base::Seconds(120));
AppendDuration(base::Seconds(20));
// Add [95s, 105s).
AddNewPlayback(base::Seconds(95));
AppendDuration(base::Seconds(10));
// Add [105s, 115s).
AddNewPlayback(base::Seconds(105));
AppendDuration(base::Seconds(10));
// Expect [100s, 110s), [120s, 140s), [95s, 105s), [105s, 115s).
VerifyRanges(TakeAllRanges(),
{MediaSecondsRange(100, 110), MediaSecondsRange(120, 140),
MediaSecondsRange(95, 105), MediaSecondsRange(105, 115)});
}
TEST_F(SpeechTimestampEstimatorTest, MultiplePlaybacks_EmptyPlaybacks) {
// Add a playback without duration.
AddNewPlayback(base::Seconds(60));
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Add more playbacks without duration.
AddNewPlayback(base::Seconds(110));
AddNewPlayback(base::Seconds(50));
AddNewPlayback(base::Seconds(50));
// Add [200s, 210s).
AddNewPlayback(base::Seconds(200));
AppendDuration(base::Seconds(10));
// Add more playbacks without duration.
AddNewPlayback(base::Seconds(300));
// Expect [100s, 110s), [200s, 210s).
VerifyRanges(TakeAllRanges(),
{MediaSecondsRange(100, 110), MediaSecondsRange(200, 210)});
// This should add [300s, 310s).
AppendDuration(base::Seconds(10));
// Expect [300s, 310s).
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(300, 310)});
}
TEST_F(SpeechTimestampEstimatorTest, MultiplePlaybacks_PartialRanges) {
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Add [140s, 145s).
AddNewPlayback(base::Seconds(140));
AppendDuration(base::Seconds(5));
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Take 20s of playback, starting at 1s.
// Expect [101s, 110s), [140s, 145s), [100s, 106s).
VerifyRanges(TakeRange(SpeechSecondsRange(1, 21)),
{MediaSecondsRange(101, 110), MediaSecondsRange(140, 145),
MediaSecondsRange(100, 106)});
// Take the last second of playback.
// Expect [109s, 110s).
VerifyRanges(TakeRange(SpeechSecondsRange(24, 25)),
{MediaSecondsRange(109, 110)});
}
TEST_F(SpeechTimestampEstimatorTest, Silences_Simple) {
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Media time should increase to 120s, speech time should not.
SkipSilence(base::Seconds(10));
// Should result in [120,130s) being added
AppendDuration(base::Seconds(10));
// Take 20s of playback. Silences shouldn't be included.
VerifyRanges(TakeRange(SpeechSecondsRange(0, 20)),
{MediaSecondsRange(100, 110), MediaSecondsRange(120, 130)});
}
TEST_F(SpeechTimestampEstimatorTest, Silences_MultipleChunks) {
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Media time should increase to 120s, speech time should not.
SkipSilence(base::Seconds(5));
SkipSilence(base::Seconds(5));
// Should result in [120,130s) being added
AppendDuration(base::Seconds(10));
// Take 20s of playback. Silences shouldn't be included.
VerifyRanges(TakeRange(SpeechSecondsRange(0, 20)),
{MediaSecondsRange(100, 110), MediaSecondsRange(120, 130)});
}
TEST_F(SpeechTimestampEstimatorTest, Silences_EndSilence) {
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
SkipSilence(base::Seconds(10));
// Trailing silence should be ignored.
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(100, 110)});
}
TEST_F(SpeechTimestampEstimatorTest, Silences_StartSilence) {
SkipSilence(base::Seconds(10));
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Silences before playback start should be ignored.
VerifyRanges(TakeAllRanges(), {MediaSecondsRange(100, 110)});
}
TEST_F(SpeechTimestampEstimatorTest, Silences_SilenceAndDurationBeforeStart) {
SkipSilence(base::Seconds(10));
AppendDuration(base::Seconds(10));
SkipSilence(base::Seconds(10));
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Only 10s of speech time should have no media associated with it.
VerifyRanges(TakeRange(SpeechSecondsRange(0, 10)), {});
VerifyRanges(TakeRange(SpeechSecondsRange(10, 20)),
{MediaSecondsRange(100, 110)});
}
TEST_F(SpeechTimestampEstimatorTest, Silences_TakeDuringSilence) {
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Media time should be 120s.
SkipSilence(base::Seconds(10));
// Take for more than the duration of the first playback. This should only
// return the first playback, since the silence is not included.
VerifyRanges(TakeRange(SpeechSecondsRange(0, 20)),
{MediaSecondsRange(100, 110)});
// Media time should be 130s.
SkipSilence(base::Seconds(10));
// Should add [130, 140s)
AppendDuration(base::Seconds(10));
// Media time should have kept increasing with the silence, regardless of us
// taking ranges.
VerifyRanges(TakeRange(SpeechSecondsRange(10, 20)),
{MediaSecondsRange(130, 140)});
}
TEST_F(SpeechTimestampEstimatorTest, Silences_PlaybackResetsSilence_Simple) {
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Media time should be 120s.
SkipSilence(base::Seconds(10));
// Add [200s, 210s). Should discard the silence above.
AddNewPlayback(base::Seconds(200));
AppendDuration(base::Seconds(10));
// Take all all ranges. The second playback should have discarded the silence.
VerifyRanges(TakeRange(SpeechSecondsRange(0, 30)),
{MediaSecondsRange(100, 110), MediaSecondsRange(200, 210)});
}
TEST_F(SpeechTimestampEstimatorTest,
Silences_PlaybackResetsSilence_MultiplePlaybacks) {
// Add [100s, 110s).
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Media time should be 120s.
SkipSilence(base::Seconds(10));
AddNewPlayback(base::Seconds(200));
// Media time should be 210s.
SkipSilence(base::Seconds(10));
// Add many media start times during the silences
AddNewPlayback(base::Seconds(300));
AddNewPlayback(base::Seconds(400));
AddNewPlayback(base::Seconds(500));
// Media time should be 510s.
SkipSilence(base::Seconds(10));
// Add [510s, 520s)
AppendDuration(base::Seconds(10));
VerifyRanges(TakeRange(SpeechSecondsRange(0, 20)),
{MediaSecondsRange(100, 110), MediaSecondsRange(510, 520)});
}
TEST_F(SpeechTimestampEstimatorTest, PeekTimestampsInRange) {
// Add [100s, 110s). Speech time: [0, 10)
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// Add [140s, 145s). Speech time: [10, 15)
AddNewPlayback(base::Seconds(140));
AppendDuration(base::Seconds(5));
// Add [100s, 110s). Speech time: [15, 25)
AddNewPlayback(base::Seconds(100));
AppendDuration(base::Seconds(10));
// 1. Peek into a range.
// Speech range [1, 21) should correspond to media ranges:
// [101, 110), [140, 145), [100, 106)
MediaRanges expected_ranges = {MediaSecondsRange(101, 110),
MediaSecondsRange(140, 145),
MediaSecondsRange(100, 106)};
VerifyRanges(PeekRange(SpeechSecondsRange(1, 21)), expected_ranges);
// 2. Verify that peeking again yields the exact same result.
// This is the key difference from TakeRange.
VerifyRanges(PeekRange(SpeechSecondsRange(1, 21)), expected_ranges);
// 3. Take the same range to prove the state was indeed unchanged.
// The result of taking should be identical to the result of peeking.
VerifyRanges(TakeRange(SpeechSecondsRange(1, 21)), expected_ranges);
// 4. Now that we've *taken* the range [1, 21), the estimator is modified.
// Peeking into the full range should now only return the final chunk.
// Speech range [21, 25) -> Media range [106, 110)
VerifyRanges(PeekRange(SpeechSecondsRange(0, 100)),
{MediaSecondsRange(106, 110)});
}
} // namespace
} // namespace speech