Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 12 additions & 0 deletions app/lib/backend/http/api/users.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,18 @@ Future<bool> deletePerson(String personId) async {
return response.statusCode == 204;
}

Future<bool> deletePersonSpeechSample(String personId, int sampleIndex) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/users/people/$personId/speech-samples/$sampleIndex',
headers: {},
method: 'DELETE',
body: '',
);
if (response == null) return false;
debugPrint('deletePersonSpeechSample response: ${response.body}');
return response.statusCode == 200;
}

Future<String> getFollowUpQuestion({String conversationId = '0'}) async {
var response = await makeApiCall(
url: '${Env.apiBaseUrl}v1/joan/$conversationId/followup-question',
Expand Down
22 changes: 6 additions & 16 deletions app/lib/pages/settings/people.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:omi/providers/people_provider.dart';
import 'package:omi/providers/connectivity_provider.dart';
import 'package:omi/widgets/dialog.dart';
import 'package:omi/widgets/extensions/functions.dart';
import 'package:just_audio/just_audio.dart';
import 'package:omi/utils/l10n_extensions.dart';
import 'package:provider/provider.dart';

Expand Down Expand Up @@ -161,7 +160,7 @@ class _UserPeoplePageState extends State<_UserPeoplePage> {
);
}

Future<void> _confirmDeleteSample(int peopleIdx, Person person, String url, PeopleProvider provider) async {
Future<void> _confirmDeleteSample(int peopleIdx, Person person, int sampleIdx, PeopleProvider provider) async {
final connectivityProvider = Provider.of<ConnectivityProvider>(context, listen: false);
if (!connectivityProvider.isConnected) {
ConnectivityProvider.showNoInternetDialog(context);
Expand All @@ -180,7 +179,7 @@ class _UserPeoplePageState extends State<_UserPeoplePage> {
);

if (confirmed == true) {
provider.deletePersonSample(peopleIdx, url);
await provider.deletePersonSample(peopleIdx, sampleIdx);
}
}

Expand Down Expand Up @@ -297,20 +296,11 @@ class _UserPeoplePageState extends State<_UserPeoplePage> {
),
onPressed: () => provider.playPause(index, j, sample),
),
title: Text(index == 0
title: Text(j == 0
? context.l10n.speechProfile
: context.l10n.sampleNumber(index)),
onTap: () => _confirmDeleteSample(index, person, sample, provider),
subtitle: FutureBuilder<Duration?>(
future: AudioPlayer().setUrl(sample),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(context.l10n.secondsCount(snapshot.data!.inSeconds));
} else {
return Text(context.l10n.loadingDuration);
}
},
),
: context.l10n.sampleNumber(j)),
onTap: () => _confirmDeleteSample(index, person, j, provider),
subtitle: Text('Tap to delete'),
)),
],
),
Expand Down
25 changes: 10 additions & 15 deletions app/lib/providers/people_provider.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:omi/backend/http/api/speech_profile.dart';
import 'package:omi/backend/http/api/users.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/backend/schema/person.dart';
Expand Down Expand Up @@ -106,21 +105,17 @@ class PeopleProvider extends BaseProvider {
notifyListeners();
}

String _getFileNameFromUrl(String url) {
Uri uri = Uri.parse(url);
String fileName = uri.pathSegments.last;
return fileName.split('.').first;
}
Future<void> deletePersonSample(int personIdx, int sampleIdx) async {
String personId = people[personIdx].id;

void deletePersonSample(int personIdx, String url) {
String name = _getFileNameFromUrl(url);
var parts = name.split('_segment_');
String conversationId = parts[0];
int segmentIdx = int.parse(parts[1]);
deleteProfileSample(conversationId, segmentIdx, personId: people[personIdx].id);
people[personIdx].speechSamples!.remove(url);
SharedPreferencesUtil().replaceCachedPerson(people[personIdx]);
notifyListeners();
bool success = await deletePersonSpeechSample(personId, sampleIdx);
if (success) {
people[personIdx].speechSamples!.removeAt(sampleIdx);
SharedPreferencesUtil().replaceCachedPerson(people[personIdx]);
notifyListeners();
} else {
debugPrint('Failed to delete speech sample at index: $sampleIdx');
}
}

void deletePersonProvider(Person person) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ env:
value: "http://34.172.155.20:80/v1/vad"
- name: HOSTED_SPEECH_PROFILE_API_URL
value: "http://34.172.155.20:80/v1/speaker-identification"
- name: HOSTED_SPEAKER_EMBEDDING_API_URL
value: "http://34.172.155.20:80"
- name: PINECONE_API_KEY
valueFrom:
secretKeyRef:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ env:
value: "http://172.16.128.101:8080/v1/vad"
- name: HOSTED_SPEECH_PROFILE_API_URL
value: "http://172.16.128.101:8080/v1/speaker-identification"
- name: HOSTED_SPEAKER_EMBEDDING_API_URL
value: "http://diarizer.omi.me:80"
- name: PINECONE_API_KEY
valueFrom:
secretKeyRef:
Expand Down
2 changes: 2 additions & 0 deletions backend/charts/pusher/dev_omi_pusher_values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ env:
value: "http://34.172.155.20:80/v1/vad"
- name: HOSTED_SPEECH_PROFILE_API_URL
value: "http://34.172.155.20:80/v1/speaker-identification"
- name: HOSTED_SPEAKER_EMBEDDING_API_URL
value: "http://34.172.155.20:80"
- name: PINECONE_API_KEY
valueFrom:
secretKeyRef:
Expand Down
2 changes: 2 additions & 0 deletions backend/charts/pusher/prod_omi_pusher_values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ env:
value: "http://vad.omi.me:80/v1/vad"
- name: HOSTED_SPEECH_PROFILE_API_URL
value: "http://vad.omi.me:80/v1/speaker-identification"
- name: HOSTED_SPEAKER_EMBEDDING_API_URL
value: "http://diarizer.omi.me:80"
- name: PINECONE_API_KEY
valueFrom:
secretKeyRef:
Expand Down
121 changes: 121 additions & 0 deletions backend/database/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,127 @@ def delete_person(uid: str, person_id: str):
person_ref.delete()


def add_person_speech_sample(uid: str, person_id: str, sample_path: str, max_samples: int = 5) -> bool:
"""
Append speech sample path to person's speech_samples list.
Limits to max_samples to prevent unlimited growth.

Args:
uid: User ID
person_id: Person ID
sample_path: GCS path to the speech sample
max_samples: Maximum number of samples to keep (default 5)

Returns:
True if sample was added, False if limit reached
"""
person_ref = db.collection('users').document(uid).collection('people').document(person_id)
person_doc = person_ref.get()

if not person_doc.exists:
return False

person_data = person_doc.to_dict()
current_samples = person_data.get('speech_samples', [])

# Check if we've hit the limit
if len(current_samples) >= max_samples:
return False

person_ref.update(
{
'speech_samples': firestore.ArrayUnion([sample_path]),
'updated_at': datetime.now(timezone.utc),
}
)
return True


def get_person_speech_samples_count(uid: str, person_id: str) -> int:
"""Get the count of speech samples for a person."""
person_ref = db.collection('users').document(uid).collection('people').document(person_id)
person_doc = person_ref.get()

if not person_doc.exists:
return 0

person_data = person_doc.to_dict()
return len(person_data.get('speech_samples', []))


def remove_person_speech_sample(uid: str, person_id: str, sample_path: str) -> bool:
"""
Remove a speech sample path from person's speech_samples list.

Args:
uid: User ID
person_id: Person ID
sample_path: GCS path to remove

Returns:
True if removed, False if person not found
"""
person_ref = db.collection('users').document(uid).collection('people').document(person_id)
person_doc = person_ref.get()

if not person_doc.exists:
return False

person_ref.update({
'speech_samples': firestore.ArrayRemove([sample_path]),
'updated_at': datetime.now(timezone.utc),
})
return True


def set_person_speaker_embedding(uid: str, person_id: str, embedding: list) -> bool:
"""
Store speaker embedding for a person.

Args:
uid: User ID
person_id: Person ID
embedding: List of floats representing the speaker embedding

Returns:
True if stored successfully, False if person not found
"""
person_ref = db.collection('users').document(uid).collection('people').document(person_id)
person_doc = person_ref.get()

if not person_doc.exists:
return False

person_ref.update(
{
'speaker_embedding': embedding,
'updated_at': datetime.now(timezone.utc),
}
)
return True


def get_person_speaker_embedding(uid: str, person_id: str) -> Optional[list]:
"""
Get speaker embedding for a person.

Args:
uid: User ID
person_id: Person ID

Returns:
List of floats representing the embedding, or None if not found
"""
person_ref = db.collection('users').document(uid).collection('people').document(person_id)
person_doc = person_ref.get()

if not person_doc.exists:
return None

person_data = person_doc.to_dict()
return person_data.get('speaker_embedding')


def delete_user_data(uid: str):
user_ref = db.collection('users').document(uid)
if not user_ref.get().exists:
Expand Down
13 changes: 13 additions & 0 deletions backend/routers/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
from utils.conversations.process_conversation import process_conversation, retrieve_in_progress_conversation
from utils.conversations.search import search_conversations
from utils.llm.conversation_processing import generate_summary_with_prompt
from utils.speaker_identification import extract_speaker_samples
from utils.other import endpoints as auth
from utils.other.storage import get_conversation_recording_if_exists
from utils.app_integrations import trigger_external_integrations
Expand Down Expand Up @@ -495,6 +496,7 @@ def set_assignee_conversation_segment(
def assign_segments_bulk(
conversation_id: str,
data: BulkAssignSegmentsRequest,
background_tasks: BackgroundTasks,
uid: str = Depends(auth.get_current_user_uid),
):
conversation = _get_valid_conversation_by_id(uid, conversation_id)
Expand All @@ -521,6 +523,17 @@ def assign_segments_bulk(
conversations_db.update_conversation_segments(
uid, conversation_id, [segment.dict() for segment in conversation.transcript_segments]
)

# Trigger speaker sample extraction when assigning to a person
if data.assign_type == 'person_id' and value:
background_tasks.add_task(
extract_speaker_samples,
uid=uid,
person_id=value,
conversation_id=conversation_id,
segment_ids=data.segment_ids,
)

return conversation


Expand Down
Loading