merge: tail audio ASR fix (P0+P1) into feature/polish-style-packs
Co-authored-by: Rocky <hkgood@users.noreply.github.com>
This commit is contained in:
@@ -115,6 +115,34 @@ final class ChunkedUtterancePipelineTests: XCTestCase {
|
||||
}
|
||||
XCTAssertTrue(success.text.contains("merged"))
|
||||
}
|
||||
|
||||
func testPipelineRetriesEmptyFinalChunkWithOverlap() async {
|
||||
let config = FlowUtteranceChunkConfig(
|
||||
maxChunkDurationSeconds: 0.05,
|
||||
overlapDurationSeconds: 10,
|
||||
pauseExtensionMaxSeconds: 0,
|
||||
pauseRMSThreshold: 0.02,
|
||||
minFinalChunkDurationSeconds: 0.05,
|
||||
sampleRate: 1_000
|
||||
)
|
||||
let asr = EmptyFinalRetryStubASR()
|
||||
let pipeline = ChunkedUtterancePipeline(
|
||||
asr: asr,
|
||||
locale: Locale(identifier: "zh-Hans"),
|
||||
config: config
|
||||
)
|
||||
|
||||
let (stream, continuation) = AsyncStream<AudioBufferSnapshot>.makeStream()
|
||||
continuation.yield(AudioBufferSnapshot(samples: [Float](repeating: 0.1, count: 80), sampleRate: 1_000))
|
||||
continuation.yield(AudioBufferSnapshot(samples: [Float](repeating: 0.1, count: 80), sampleRate: 1_000))
|
||||
continuation.finish()
|
||||
|
||||
let outcome = await pipeline.transcribe(stream: stream) { _ in }
|
||||
guard case .success(let success) = outcome else {
|
||||
return XCTFail("expected success, got \(outcome)")
|
||||
}
|
||||
XCTAssertTrue(success.text.contains("recovered-tail"))
|
||||
}
|
||||
}
|
||||
|
||||
private struct FailingSecondChunkASR: ASRService, @unchecked Sendable {
|
||||
@@ -171,3 +199,32 @@ private struct ShortFinalMergeStubASR: ASRService, @unchecked Sendable {
|
||||
return .success("short")
|
||||
}
|
||||
}
|
||||
|
||||
private struct EmptyFinalRetryStubASR: ASRService, @unchecked Sendable {
|
||||
private let callIndex = OSAllocatedUnfairLock(initialState: 0)
|
||||
|
||||
func transcribe(
|
||||
stream: AsyncStream<AudioBufferSnapshot>,
|
||||
locale: Locale
|
||||
) -> AsyncStream<ASREvent> {
|
||||
AsyncStream { $0.finish() }
|
||||
}
|
||||
|
||||
func cancel() {}
|
||||
|
||||
func transcribeChunk(samples: [Float], locale: Locale) async -> ASRChunkResult {
|
||||
_ = locale
|
||||
let current = callIndex.withLock { state in
|
||||
let value = state
|
||||
state += 1
|
||||
return value
|
||||
}
|
||||
if current == 0 {
|
||||
return .success("head")
|
||||
}
|
||||
if current == 1 {
|
||||
return .success("")
|
||||
}
|
||||
return .success("recovered-tail")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// FinalChunkRecoveryTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class FinalChunkRecoveryTests: XCTestCase {
|
||||
|
||||
private let config = FlowUtteranceChunkConfig(
|
||||
maxChunkDurationSeconds: 5.0,
|
||||
overlapDurationSeconds: 0.5,
|
||||
pauseExtensionMaxSeconds: 2,
|
||||
pauseRMSThreshold: 0.015,
|
||||
minFinalChunkDurationSeconds: 0.8,
|
||||
sampleRate: 16_000
|
||||
)
|
||||
|
||||
func testPreMergePlanForShortFinalChunk() {
|
||||
let chunk = UtteranceAudioChunk(
|
||||
index: 1,
|
||||
samples: [Float](repeating: 0.1, count: 4_000),
|
||||
isLast: true
|
||||
)
|
||||
let previous = [Float](repeating: 0.2, count: 80_000)
|
||||
|
||||
let plan = FinalChunkRecovery.preMergePlan(
|
||||
chunk: chunk,
|
||||
processedChunks: 2,
|
||||
previousChunkSamples: previous,
|
||||
config: config
|
||||
)
|
||||
|
||||
XCTAssertNotNil(plan)
|
||||
XCTAssertGreaterThan(plan?.samples.count ?? 0, chunk.samples.count)
|
||||
XCTAssertEqual(plan?.stitchIndex, 0)
|
||||
}
|
||||
|
||||
func testEmptyResultRetryPlanUsesOverlapWhenPriorChunkExists() {
|
||||
let chunk = UtteranceAudioChunk(
|
||||
index: 1,
|
||||
samples: [Float](repeating: 0.1, count: 20_000),
|
||||
isLast: true
|
||||
)
|
||||
let previous = [Float](repeating: 0.2, count: 80_000)
|
||||
|
||||
let plan = FinalChunkRecovery.emptyResultRetryPlan(
|
||||
chunk: chunk,
|
||||
previousChunkSamples: previous,
|
||||
config: config,
|
||||
asrText: " "
|
||||
)
|
||||
|
||||
XCTAssertNotNil(plan)
|
||||
XCTAssertGreaterThan(plan?.samples.count ?? 0, chunk.samples.count)
|
||||
}
|
||||
|
||||
func testEmptyResultRetryPlanRetriesSingleChunkSamples() {
|
||||
let chunk = UtteranceAudioChunk(
|
||||
index: 0,
|
||||
samples: [Float](repeating: 0.1, count: 20_000),
|
||||
isLast: true
|
||||
)
|
||||
|
||||
let plan = FinalChunkRecovery.emptyResultRetryPlan(
|
||||
chunk: chunk,
|
||||
previousChunkSamples: [],
|
||||
config: config,
|
||||
asrText: ""
|
||||
)
|
||||
|
||||
XCTAssertEqual(plan?.samples.count, chunk.samples.count)
|
||||
XCTAssertEqual(plan?.stitchIndex, 0)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// FlowUtteranceEndCoordinatorTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class FlowUtteranceEndCoordinatorTests: XCTestCase {
|
||||
|
||||
func testAwaitTailCaptureRunsPostRollAfterSilenceDrain() async {
|
||||
let policy = FlowCaptureTailDrainPolicy(
|
||||
silenceRMSThreshold: 0.02,
|
||||
silenceDurationSeconds: 0.05,
|
||||
maxDrainSeconds: 1.0,
|
||||
postRollSeconds: 0.08
|
||||
)
|
||||
let tracker = FlowCaptureDrainTracker()
|
||||
let start = Date().timeIntervalSince1970
|
||||
tracker.beginDrain(now: start)
|
||||
|
||||
let timing = await FlowUtteranceEndCoordinator.awaitTailCapture(
|
||||
tracker: tracker,
|
||||
policy: policy,
|
||||
pollIntervalNs: 5_000_000
|
||||
)
|
||||
|
||||
XCTAssertTrue(timing.endedBySilence)
|
||||
XCTAssertGreaterThanOrEqual(timing.postRollDurationSeconds, 0.07)
|
||||
}
|
||||
|
||||
func testIOSFlowPresetUsesLongerSilenceAndPostRoll() {
|
||||
XCTAssertEqual(FlowCaptureTailDrainPolicy.iosFlow.silenceDurationSeconds, 0.35)
|
||||
XCTAssertEqual(FlowCaptureTailDrainPolicy.iosFlow.postRollSeconds, 0.15)
|
||||
XCTAssertEqual(FlowCaptureTailDrainPolicy.flowDefault, FlowCaptureTailDrainPolicy.iosFlow)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
// FlowUtterancePCMStoreTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class FlowUtterancePCMStoreTests: XCTestCase {
|
||||
|
||||
func testAppendAndConsume() {
|
||||
let store = FlowUtterancePCMStore(maxSampleCount: 100)
|
||||
store.append([1, 2, 3])
|
||||
store.append([4, 5])
|
||||
XCTAssertEqual(store.sampleCount, 5)
|
||||
XCTAssertEqual(store.consume(), [1, 2, 3, 4, 5])
|
||||
XCTAssertEqual(store.sampleCount, 0)
|
||||
}
|
||||
|
||||
func testTrimsOldestWhenOverCap() {
|
||||
let store = FlowUtterancePCMStore(maxSampleCount: 4)
|
||||
store.append([1, 2, 3, 4, 5])
|
||||
XCTAssertEqual(store.consume(), [2, 3, 4, 5])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// UtteranceBatchFallbackPolicyTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class UtteranceBatchFallbackPolicyTests: XCTestCase {
|
||||
|
||||
func testShouldRunWhenPartialClearlyLonger() {
|
||||
XCTAssertTrue(
|
||||
UtteranceBatchFallbackPolicy.shouldRunBatchFallback(
|
||||
stitchedFinal: "今天很好",
|
||||
partialSnapshot: "今天很好,我们一起去公园吧"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testShouldRunWhenFinalEmptyButPartialPresent() {
|
||||
XCTAssertTrue(
|
||||
UtteranceBatchFallbackPolicy.shouldRunBatchFallback(
|
||||
stitchedFinal: "",
|
||||
partialSnapshot: "最后一段"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testShouldNotRunWhenPartialNotLonger() {
|
||||
XCTAssertFalse(
|
||||
UtteranceBatchFallbackPolicy.shouldRunBatchFallback(
|
||||
stitchedFinal: "今天很好,我们一起去公园吧",
|
||||
partialSnapshot: "今天很好"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func testPreferredTranscriptPicksLongestCandidate() {
|
||||
let resolved = UtteranceBatchFallbackPolicy.preferredTranscript(
|
||||
batch: "今天很好,我们一起去公园吧",
|
||||
stitchedFinal: "今天很好",
|
||||
partialSnapshot: "今天很好,我们",
|
||||
current: "今天很好,我们"
|
||||
)
|
||||
XCTAssertEqual(resolved, "今天很好,我们一起去公园吧")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// UtteranceTranscriptGuardTests.swift
|
||||
// OSGKeyboardTests
|
||||
|
||||
import XCTest
|
||||
@testable import OSGKeyboardShared
|
||||
|
||||
final class UtteranceTranscriptGuardTests: XCTestCase {
|
||||
|
||||
func testResolvePrefersPartialWhenClearlyLonger() {
|
||||
let resolved = UtteranceTranscriptGuard.resolve(
|
||||
stitchedFinal: "今天天气很好",
|
||||
partialSnapshot: "今天天气很好,我们一起去公园吧"
|
||||
)
|
||||
XCTAssertEqual(resolved, "今天天气很好,我们一起去公园吧")
|
||||
}
|
||||
|
||||
func testResolveKeepsFinalWhenPartialIsNotLonger() {
|
||||
let resolved = UtteranceTranscriptGuard.resolve(
|
||||
stitchedFinal: "今天天气很好,我们一起去公园吧",
|
||||
partialSnapshot: "今天天气很好"
|
||||
)
|
||||
XCTAssertEqual(resolved, "今天天气很好,我们一起去公园吧")
|
||||
}
|
||||
|
||||
func testResolveUsesPartialWhenFinalEmpty() {
|
||||
let resolved = UtteranceTranscriptGuard.resolve(
|
||||
stitchedFinal: "",
|
||||
partialSnapshot: "最后一段 partial"
|
||||
)
|
||||
XCTAssertEqual(resolved, "最后一段 partial")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user