Phases 2-6: detection, visual timeline, backend hand-off, voiceprints
Phase 2 (call detection): CallDetector using CoreAudio per-process mic attribution (anarlog technique) — robust start+stop for Zoom/Teams/Signal/Meet, ignoring our own recording; auto-record toggle. Built; pending live multi-app confirmation by the user. Phase 3 (visual timeline foundation): AppAdapter protocol + SpeakerObservation, TimelineBuilder (hysteresis/overlap/self-merge/aliases), VisualTimeline (schema 1.1), TextRecognizer (Vision OCR), FrameSampler + GridCallAnalyzer (name OCR + saturated-highlight active-speaker attribution), SignalAdapter, VisualObserver (window capture; frames released, never saved; minimized->visual_gap, idle != gap). Synthetic-frame tested; adapter geometry pending real Signal fixtures + live VisualObserver validation. Phase 5 (backend hand-off): SparkControlClient (multipart label-merge, sequential, TLS-skip, 503 Retry-After/413), SessionPackager (chunk plan + WAV slice + timeline slice/rebase), TranscriptAssembler + SpeakersFile, TranscriptPipeline. Validated END-TO-END against the live backend (chunk -> label-merge -> speakers.json). Phase 6 (voiceprints): VoiceprintStore (known_voiceprints, persist named fingerprints, skip Unknown). Wired: 'Send to backend' button + transcript status, auto-send toggle (default off) + self-name setting. All adversarial-review findings fixed. App + XCTest suite build; tests pass.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import XCTest
|
||||
@testable import Ten31Transcripts
|
||||
|
||||
final class Phase5Tests: XCTestCase {
|
||||
func testPlanChunksShort() {
|
||||
let c = SessionPackager.planChunks(durationSec: 70)
|
||||
XCTAssertEqual(c.count, 1)
|
||||
XCTAssertEqual(c[0].end, 70, accuracy: 0.001)
|
||||
}
|
||||
|
||||
func testPlanChunksLong() {
|
||||
let c = SessionPackager.planChunks(durationSec: 400, chunkSeconds: 150)
|
||||
XCTAssertEqual(c.count, 3)
|
||||
XCTAssertEqual(c[0].start, 0); XCTAssertEqual(c[0].end, 150)
|
||||
XCTAssertEqual(c[1].start, 150); XCTAssertEqual(c[2].end, 400)
|
||||
}
|
||||
|
||||
func testRebaseClipsAndRebases() throws {
|
||||
let segs = [
|
||||
VisualTimeline.Segment(start: 140, end: 160, name: "A", confidence: 0.9, source: "vision"),
|
||||
VisualTimeline.Segment(start: 200, end: 260, name: "B", confidence: 0.8, source: "vision"),
|
||||
]
|
||||
let data = try SessionPackager.rebasedTimelineData(segs, start: 150, end: 300)
|
||||
let arr = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [[String: Any]])
|
||||
XCTAssertEqual(arr.count, 2)
|
||||
XCTAssertEqual(arr[0]["start"] as? Double, 0)
|
||||
XCTAssertEqual(arr[0]["end"] as? Double, 10)
|
||||
XCTAssertEqual(arr[1]["start"] as? Double, 50)
|
||||
XCTAssertEqual(arr[1]["end"] as? Double, 110)
|
||||
}
|
||||
|
||||
func testAssembleOffsetsAndUnifies() throws {
|
||||
let resp0 = #"{"duration":150,"speakers":[{"cluster":"Speaker_0","name":"Grant","source":"visual","overlap_confidence":0.99,"fingerprint":[0.1,0.2]}],"segments":[{"start_ms":1000,"end_ms":2000,"speaker":"Grant","text":"hi"}],"fingerprints":{"Grant":[0.1,0.2]},"models":{"diarization":"x"}}"#
|
||||
let resp1 = #"{"duration":100,"speakers":[{"cluster":"Speaker_0","name":"Sarah","source":"voiceprint","match_similarity":0.7,"fingerprint":[0.3,0.4]},{"cluster":"Speaker_1","name":"Unknown_0","source":"unmatched"}],"segments":[{"start_ms":500,"end_ms":1500,"speaker":"Sarah","text":"hello"}],"fingerprints":{"Sarah":[0.3,0.4]},"models":{"diarization":"x"}}"#
|
||||
let r0 = try JSONDecoder().decode(LabelMergeResponse.self, from: Data(resp0.utf8))
|
||||
let r1 = try JSONDecoder().decode(LabelMergeResponse.self, from: Data(resp1.utf8))
|
||||
let asm = TranscriptAssembler.assemble(sessionId: "s", app: "meet",
|
||||
chunks: [.init(chunkStart: 0, response: r0), .init(chunkStart: 150, response: r1)])
|
||||
XCTAssertEqual(asm.speakersFile.segments.count, 2)
|
||||
XCTAssertEqual(asm.speakersFile.segments[0].start, 1, accuracy: 0.001)
|
||||
XCTAssertEqual(asm.speakersFile.segments[1].start, 150.5, accuracy: 0.001)
|
||||
XCTAssertEqual(asm.speakersFile.speakers.count, 3)
|
||||
XCTAssertNotNil(asm.fingerprints["Grant"])
|
||||
XCTAssertNotNil(asm.fingerprints["Sarah"])
|
||||
XCTAssertNil(asm.fingerprints["Unknown_0"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user