Files
ten31-transcripts/Ten31TranscriptsTests/Phase5Tests.swift
T
Grant Gilliam 863136aeec 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.
2026-06-06 00:15:49 -05:00

48 lines
2.7 KiB
Swift

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"])
}
}