Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Firebase Integration and Confidence Filtering #35

Merged
merged 6 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CoughSync/CoughDetection/CoughClass.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,15 @@

import Foundation

class Cough {
class Cough: Identifiable, Codable {
let timestamp: Date
let id: UUID
let confidence: Double

init(timestamp: Date = Date()) {
init(timestamp: Date = Date(), id: UUID = UUID(), confidence: Double = 0) {

Check warning on line 21 in CoughSync/CoughDetection/CoughClass.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughDetection/CoughClass.swift#L21

Added line #L21 was not covered by tests
self.timestamp = timestamp
self.id = id
self.confidence = confidence

Check warning on line 24 in CoughSync/CoughDetection/CoughClass.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughDetection/CoughClass.swift#L23-L24

Added lines #L23 - L24 were not covered by tests
}
}

Expand Down
24 changes: 22 additions & 2 deletions CoughSync/CoughDetection/CoughDetectionViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,17 @@
import Foundation
import Observation
import SoundAnalysis
import Spezi
import SwiftUI

@Observable
@MainActor
class CoughDetectionViewModel {
@ObservationIgnored let coughAnalysisManager = CoughAnalysisManager.shared

// Environment property to access the standard
private let standard: CoughSyncStandard

@ObservationIgnored var lastTime: Double = 0

var detectionStarted = false
Expand All @@ -34,6 +39,11 @@
var identifiedSound: (identifier: String, confidence: String)?
private var detectionCancellable: AnyCancellable?

// Initialize with standard from environment
init(standard: CoughSyncStandard) {
self.standard = standard
}

private func formattedDetectionResult(_ result: SNClassificationResult) -> (identifier: String, confidence: String)? {
guard let classification = result.classifications.first else {
return nil }
Expand All @@ -50,9 +60,14 @@
let confidencePercentString = String(format: "%.2f%%", confidence * 100.0)
print("\(displayName): \(confidencePercentString) confidence.\n")

if displayName == "Coughs" {
let cough = Cough(timestamp: Date())
if displayName == "Coughs" && confidence > 0.5 {
let cough = Cough(timestamp: Date(), confidence: confidence)

Check warning on line 64 in CoughSync/CoughDetection/CoughDetectionViewModel.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughDetection/CoughDetectionViewModel.swift#L63-L64

Added lines #L63 - L64 were not covered by tests
coughCollection.addCough(cough)

// Store the cough in Firebase
Task {
await standard.add(cough: cough)
}

Check warning on line 70 in CoughSync/CoughDetection/CoughDetectionViewModel.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughDetection/CoughDetectionViewModel.swift#L66-L70

Added lines #L66 - L70 were not covered by tests
}

return (displayName, confidencePercentString)
Expand Down Expand Up @@ -80,4 +95,9 @@
identifiedSound = nil
coughAnalysisManager.stopCoughDetection()
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why isn't this used? If not necessary, please remove.

func syncCoughsToFirebase() async {

Check warning on line 99 in CoughSync/CoughDetection/CoughDetectionViewModel.swift

View workflow job for this annotation

GitHub Actions / Periphery / Periphery

unused

Function 'syncCoughsToFirebase()' is unused
// This could be called when app goes to background or when stopping detection
// Implementation depends on how you want to track unsaved coughs
}

Check warning on line 102 in CoughSync/CoughDetection/CoughDetectionViewModel.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughDetection/CoughDetectionViewModel.swift#L99-L102

Added lines #L99 - L102 were not covered by tests
}
31 changes: 31 additions & 0 deletions CoughSync/CoughSyncStandard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,4 +133,35 @@
await logger.error("Could not store consent form: \(error)")
}
}

func add(cough: Cough) async {
if FeatureFlags.disableFirebase {
logger.debug("Received new cough event: \(cough.timestamp)")
return
}

do {
try await configuration.userDocumentReference
.collection("CoughEvents") // Store all cough events in a /CoughEvents collection
.document(UUID().uuidString) // Generate a unique ID for each cough event
.setData([
"timestamp": cough.timestamp,
"confidence": cough.confidence
])
} catch {
logger.error("Could not store cough event: \(error)")
}
}

Check warning on line 154 in CoughSync/CoughSyncStandard.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughSyncStandard.swift#L137-L154

Added lines #L137 - L154 were not covered by tests

// For batch saving multiple coughs at once (optional)
func add(coughs: [Cough]) async {

Check warning on line 157 in CoughSync/CoughSyncStandard.swift

View workflow job for this annotation

GitHub Actions / Periphery / Periphery

unused

Function 'add(coughs:)' is unused
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If not used now, please remove.

if FeatureFlags.disableFirebase {
logger.debug("Received batch of \(coughs.count) cough events")
return
}

for cough in coughs {
await add(cough: cough)
}
}

Check warning on line 166 in CoughSync/CoughSyncStandard.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughSyncStandard.swift#L157-L166

Added lines #L157 - L166 were not covered by tests
}
50 changes: 31 additions & 19 deletions CoughSync/CoughView/CoughModelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,51 +13,63 @@
//

import SwiftUI
import Spezi

Check failure on line 16 in CoughSync/CoughView/CoughModelView.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Sorted Imports Violation: Imports should be sorted (sorted_imports)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix this error


struct CoughModelView: View {
@State var viewModel = CoughDetectionViewModel()
@Environment(CoughSyncStandard.self) private var standard
@State private var viewModel: CoughDetectionViewModel?

var body: some View {
VStack {
Spacer()
detectionStatusView()
Spacer()
microphoneButton()
if let viewModel = viewModel {
Spacer()
detectionStatusView()
Spacer()
microphoneButton()
.padding()
} else {
// Show a loading indicator
ProgressView("Loading...")
}
}
.onAppear {
// Initialize viewModel here when environment is available
viewModel = CoughDetectionViewModel(standard: standard)

Check warning on line 37 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L24-L37

Added lines #L24 - L37 were not covered by tests
}
.padding()
}

private var microphoneImage: some View {
Image(systemName: viewModel.detectionStarted ? "stop.fill" : "mic.fill")
Image(systemName: viewModel?.detectionStarted == true ? "stop.fill" : "mic.fill")

Check warning on line 42 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L42

Added line #L42 was not covered by tests
.font(.system(size: 50))
.padding(30)
.background(viewModel.detectionStarted ? .gray.opacity(0.7) : .blue)
.background(viewModel?.detectionStarted == true ? .gray.opacity(0.7) : .blue)

Check warning on line 45 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L45

Added line #L45 was not covered by tests
.foregroundStyle(.white)
.clipShape(Circle())
.shadow(color: .gray, radius: 5)
.contentTransition(.symbolEffect(.replace))
.accessibilityLabel(viewModel.detectionStarted ? "Stop sound detection" : "Start sound detection")
.accessibilityLabel(viewModel?.detectionStarted == true ? "Stop sound detection" : "Start sound detection")

Check warning on line 50 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L50

Added line #L50 was not covered by tests
}

@ViewBuilder
private func detectionStatusView() -> some View {
if !viewModel.detectionStarted {
if viewModel?.detectionStarted == false {

Check warning on line 55 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L55

Added line #L55 was not covered by tests
VStack(spacing: 10) {
ContentUnavailableView(
"No Sound Detected",
systemImage: "waveform.badge.magnifyingglass",
description: Text("Tap the microphone to start detecting")
)
Text("Cough Count: \(viewModel.coughCount)")
Text("Cough Count: \(viewModel?.coughCount ?? 0)")

Check warning on line 62 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L62

Added line #L62 was not covered by tests
.font(.title2)
.foregroundColor(.secondary)
}
} else if let predictedSound = viewModel.identifiedSound {
} else if let predictedSound = viewModel?.identifiedSound {

Check warning on line 66 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L66

Added line #L66 was not covered by tests
VStack(spacing: 10) {
Text(predictedSound.0)
.font(.system(size: 26))
Text("Cough Count: \(viewModel.coughCount)")
Text("Coughs Today: \(viewModel.coughCollection.coughsToday())")
Text("Cough Difference: \(viewModel.coughCollection.coughDiffDay())")
Text("Cough Count: \(viewModel?.coughCount ?? 0)")
Text("Coughs Today: \(viewModel?.coughCollection.coughsToday() ?? 0)")
Text("Cough Difference: \(viewModel?.coughCollection.coughDiffDay() ?? 0)")

Check warning on line 72 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L70-L72

Added lines #L70 - L72 were not covered by tests
}
.multilineTextAlignment(.center)
.padding()
Expand All @@ -78,12 +90,12 @@

private func toggleListening() {
withAnimation {
viewModel.detectionStarted.toggle()
viewModel?.detectionStarted.toggle()

Check warning on line 93 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L93

Added line #L93 was not covered by tests
}
if viewModel.detectionStarted {
viewModel.startListening()
if viewModel?.detectionStarted == true {
viewModel?.startListening()

Check warning on line 96 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L95-L96

Added lines #L95 - L96 were not covered by tests
} else {
viewModel.stopListening()
viewModel?.stopListening()

Check warning on line 98 in CoughSync/CoughView/CoughModelView.swift

View check run for this annotation

Codecov / codecov/patch

CoughSync/CoughView/CoughModelView.swift#L98

Added line #L98 was not covered by tests
}
}
}
38 changes: 26 additions & 12 deletions CoughSync/Dashboard/Dashboard.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,33 @@
import SpeziViews
import SwiftUI


struct Dashboard: View {
@Environment(Account.self) private var account: Account?
@Environment(CoughSyncStandard.self) private var standard
@Binding var presentingAccount: Bool

@State private var viewModel = CoughDetectionViewModel()
// Don't initialize viewModel right away
@State private var viewModel: CoughDetectionViewModel?
@State private var previousCoughCount: Int = 0

init(presentingAccount: Binding<Bool>) {

Check failure on line 32 in CoughSync/Dashboard/Dashboard.swift

View workflow job for this annotation

GitHub Actions / SwiftLint / SwiftLint

Type Contents Order Violation: An 'initializer' should not be placed amongst the type content(s) 'instance_property' (type_contents_order)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix this error.

self._presentingAccount = presentingAccount
}

var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: 20) {
coughSummaryCard()
coughStats()
Divider()
if let viewModel = viewModel {
VStack(spacing: 20) {
coughSummaryCard()
coughStats()
Divider()
}
.padding()
} else {
// Show a loading indicator or placeholder
ProgressView("Loading...")
}
.padding()
}
.navigationTitle("Summary")
.toolbar {
Expand All @@ -45,10 +55,14 @@
}
}
.onAppear {
previousCoughCount = viewModel.coughCount
// Initialize viewModel here when environment is available
viewModel = CoughDetectionViewModel(standard: standard)
}
.onAppear {
previousCoughCount = viewModel?.coughCount ?? 0
}
.onChange(of: viewModel.coughCount) { oldValue, _ in
previousCoughCount = oldValue
.onChange(of: viewModel?.coughCount) { oldValue, _ in
previousCoughCount = oldValue ?? 0
}
}
}
Expand All @@ -61,7 +75,7 @@
Text("Today")
.font(.headline)
.foregroundColor(.primary)
Text("\(viewModel.coughCount) ")
Text("\(viewModel?.coughCount ?? 0) ")
.font(.largeTitle)
.bold()
.foregroundColor(.blue)
Expand Down Expand Up @@ -112,7 +126,7 @@

@ViewBuilder
private func statusCircle() -> some View {
let change = viewModel.coughCount - previousCoughCount
let change = viewModel?.coughCount ?? 0 - previousCoughCount
let color: Color = change > 0 ? .red : (change < 0 ? .green : .blue)
let trendSymbol = change > 0 ? "↑" : (change < 0 ? "↓" : "–")

Expand Down
3 changes: 3 additions & 0 deletions CoughSync/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,9 @@
}
}
}
},
"Loading..." : {

},
"Next" : {
"localizations" : {
Expand Down
Loading