diff --git a/server/reflector/db/transcripts.py b/server/reflector/db/transcripts.py index fecf6641..9afed8b8 100644 --- a/server/reflector/db/transcripts.py +++ b/server/reflector/db/transcripts.py @@ -251,6 +251,23 @@ class Transcript(BaseModel): url += f"?token={token}" return url + def find_empty_speaker(self) -> int: + """ + Find an empty speaker seat + """ + speakers = set( + word.speaker + for topic in self.topics + for word in topic.words + if word.speaker is not None + ) + i = 0 + while True: + if i not in speakers: + return i + i += 1 + raise Exception("No empty speaker found") + class TranscriptController: async def get_all( diff --git a/server/reflector/processors/types.py b/server/reflector/processors/types.py index 93e565df..cedb23f9 100644 --- a/server/reflector/processors/types.py +++ b/server/reflector/processors/types.py @@ -57,6 +57,7 @@ class Word(BaseModel): class TranscriptSegment(BaseModel): text: str start: float + end: float speaker: int = 0 @@ -127,6 +128,7 @@ class Transcript(BaseModel): current_segment = TranscriptSegment( text=word.text, start=word.start, + end=word.end, speaker=word.speaker, ) continue @@ -138,6 +140,7 @@ class Transcript(BaseModel): current_segment = TranscriptSegment( text=word.text, start=word.start, + end=word.end, speaker=word.speaker, ) continue @@ -145,6 +148,7 @@ class Transcript(BaseModel): # if the word is the end of a sentence, and we have enough content, # add the word to the current segment and push it current_segment.text += word.text + current_segment.end = word.end have_punc = PUNC_RE.search(word.text) if have_punc and (len(current_segment.text) > MAX_SEGMENT_LENGTH): diff --git a/server/reflector/views/transcripts.py b/server/reflector/views/transcripts.py index 682e1576..abb72af4 100644 --- a/server/reflector/views/transcripts.py +++ b/server/reflector/views/transcripts.py @@ -122,6 +122,7 @@ class GetTranscriptTopic(BaseModel): title: str summary: str timestamp: float + duration: float | None transcript: str segments: list[GetTranscriptSegmentTopic] = [] @@ -131,6 +132,7 @@ class GetTranscriptTopic(BaseModel): # In previous version, words were missing # Just output a segment with speaker 0 text = topic.transcript + duration = None segments = [ GetTranscriptSegmentTopic( text=topic.transcript, @@ -142,6 +144,7 @@ class GetTranscriptTopic(BaseModel): # New versions include words transcript = ProcessorTranscript(words=topic.words) text = transcript.text + duration = transcript.duration segments = [ GetTranscriptSegmentTopic( text=segment.text, @@ -157,6 +160,7 @@ class GetTranscriptTopic(BaseModel): timestamp=topic.timestamp, transcript=text, segments=segments, + duration=duration, ) @@ -171,6 +175,44 @@ class GetTranscriptTopicWithWords(GetTranscriptTopic): return instance +class SpeakerWords(BaseModel): + speaker: int + words: list[Word] + + +class GetTranscriptTopicWithWordsPerSpeaker(GetTranscriptTopic): + words_per_speaker: list[SpeakerWords] = [] + + @classmethod + def from_transcript_topic(cls, topic: TranscriptTopic): + instance = super().from_transcript_topic(topic) + if topic.words: + words_per_speakers = [] + # group words by speaker + words = [] + for word in topic.words: + if words and words[-1].speaker != word.speaker: + words_per_speakers.append( + SpeakerWords( + speaker=words[-1].speaker, + words=words, + ) + ) + words = [] + words.append(word) + if words: + words_per_speakers.append( + SpeakerWords( + speaker=words[-1].speaker, + words=words, + ) + ) + + instance.words_per_speaker = words_per_speakers + + return instance + + @router.get("/transcripts/{transcript_id}", response_model=GetTranscript) async def transcript_get( transcript_id: str, @@ -247,3 +289,26 @@ async def transcript_get_topics_with_words( GetTranscriptTopicWithWords.from_transcript_topic(topic) for topic in transcript.topics ] + + +@router.get( + "/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker", + response_model=GetTranscriptTopicWithWordsPerSpeaker, +) +async def transcript_get_topics_with_words_per_speaker( + transcript_id: str, + topic_id: str, + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +): + user_id = user["sub"] if user else None + transcript = await transcripts_controller.get_by_id_for_http( + transcript_id, user_id=user_id + ) + + # get the topic from the transcript + topic = next((t for t in transcript.topics if t.id == topic_id), None) + if not topic: + raise HTTPException(status_code=404, detail="Topic not found") + + # convert to GetTranscriptTopicWithWordsPerSpeaker + return GetTranscriptTopicWithWordsPerSpeaker.from_transcript_topic(topic) diff --git a/server/reflector/views/transcripts_participants.py b/server/reflector/views/transcripts_participants.py index ddd5af4f..fd08405c 100644 --- a/server/reflector/views/transcripts_participants.py +++ b/server/reflector/views/transcripts_participants.py @@ -59,7 +59,7 @@ async def transcript_add_participant( ) # ensure the speaker is unique - if transcript.participants: + if participant.speaker is not None: for p in transcript.participants: if p.speaker == participant.speaker: raise HTTPException( diff --git a/server/reflector/views/transcripts_speaker.py b/server/reflector/views/transcripts_speaker.py index 20489aa0..0bddad5e 100644 --- a/server/reflector/views/transcripts_speaker.py +++ b/server/reflector/views/transcripts_speaker.py @@ -7,14 +7,15 @@ from typing import Annotated, Optional import reflector.auth as auth from fastapi import APIRouter, Depends, HTTPException -from pydantic import BaseModel +from pydantic import BaseModel, Field from reflector.db.transcripts import transcripts_controller router = APIRouter() class SpeakerAssignment(BaseModel): - speaker: int + speaker: Optional[int] = Field(None, ge=0) + participant: Optional[str] = Field(None) timestamp_from: float timestamp_to: float @@ -23,6 +24,11 @@ class SpeakerAssignmentStatus(BaseModel): status: str +class SpeakerMerge(BaseModel): + speaker_from: int + speaker_to: int + + @router.patch("/transcripts/{transcript_id}/speaker/assign") async def transcript_assign_speaker( transcript_id: str, @@ -37,6 +43,44 @@ async def transcript_assign_speaker( if not transcript: raise HTTPException(status_code=404, detail="Transcript not found") + if assignment.speaker is None and assignment.participant is None: + raise HTTPException( + status_code=400, + detail="Either speaker or participant must be provided", + ) + + if assignment.speaker is not None and assignment.participant is not None: + raise HTTPException( + status_code=400, + detail="Only one of speaker or participant must be provided", + ) + + # if it's a participant, search for it + if assignment.speaker is not None: + speaker = assignment.speaker + + elif assignment.participant is not None: + participant = next( + ( + participant + for participant in transcript.participants + if participant.id == assignment.participant + ), + None, + ) + if not participant: + raise HTTPException( + status_code=404, + detail="Participant not found", + ) + + # if the participant does not have a speaker, create one + if participant.speaker is None: + participant.speaker = transcript.find_empty_speaker() + await transcripts_controller.upsert_participant(transcript, participant) + + speaker = participant.speaker + # reassign speakers from words in the transcript ts_from = assignment.timestamp_from ts_to = assignment.timestamp_to @@ -45,7 +89,70 @@ async def transcript_assign_speaker( changed = False for word in topic.words: if ts_from <= word.start <= ts_to: - word.speaker = assignment.speaker + word.speaker = speaker + changed = True + if changed: + changed_topics.append(topic) + + # batch changes + for topic in changed_topics: + transcript.upsert_topic(topic) + await transcripts_controller.update( + transcript, + { + "topics": transcript.topics_dump(), + }, + ) + + return SpeakerAssignmentStatus(status="ok") + + +@router.patch("/transcripts/{transcript_id}/speaker/merge") +async def transcript_merge_speaker( + transcript_id: str, + merge: SpeakerMerge, + user: Annotated[Optional[auth.UserInfo], Depends(auth.current_user_optional)], +) -> SpeakerAssignmentStatus: + user_id = user["sub"] if user else None + transcript = await transcripts_controller.get_by_id_for_http( + transcript_id, user_id=user_id + ) + + if not transcript: + raise HTTPException(status_code=404, detail="Transcript not found") + + # ensure both speaker are not assigned to the 2 differents participants + participant_from = next( + ( + participant + for participant in transcript.participants + if participant.speaker == merge.speaker_from + ), + None, + ) + participant_to = next( + ( + participant + for participant in transcript.participants + if participant.speaker == merge.speaker_to + ), + None, + ) + if participant_from and participant_to: + raise HTTPException( + status_code=400, + detail="Both speakers are assigned to participants", + ) + + # reassign speakers from words in the transcript + speaker_from = merge.speaker_from + speaker_to = merge.speaker_to + changed_topics = [] + for topic in transcript.topics: + changed = False + for word in topic.words: + if word.speaker == speaker_from: + word.speaker = speaker_to changed = True if changed: changed_topics.append(topic) diff --git a/server/tests/test_transcripts_speaker.py b/server/tests/test_transcripts_speaker.py index 2bca0beb..e3e8034a 100644 --- a/server/tests/test_transcripts_speaker.py +++ b/server/tests/test_transcripts_speaker.py @@ -115,3 +115,287 @@ async def test_transcript_reassign_speaker(fake_transcript_with_topics): assert topics[0]["segments"][0]["speaker"] == 4 assert len(topics[1]["segments"]) == 1 assert topics[1]["segments"][0]["speaker"] == 4 + + +@pytest.mark.asyncio +async def test_transcript_merge_speaker(fake_transcript_with_topics): + from reflector.app import app + + transcript_id = fake_transcript_with_topics.id + + async with AsyncClient(app=app, base_url="http://test/v1") as ac: + # check the transcript exists + response = await ac.get(f"/transcripts/{transcript_id}") + assert response.status_code == 200 + + # check initial topics of the transcript + response = await ac.get(f"/transcripts/{transcript_id}/topics/with-words") + assert response.status_code == 200 + topics = response.json() + assert len(topics) == 2 + + # check through words + assert topics[0]["words"][0]["speaker"] == 0 + assert topics[0]["words"][1]["speaker"] == 0 + assert topics[1]["words"][0]["speaker"] == 0 + assert topics[1]["words"][1]["speaker"] == 0 + + # reassign speaker + response = await ac.patch( + f"/transcripts/{transcript_id}/speaker/assign", + json={ + "speaker": 1, + "timestamp_from": 0, + "timestamp_to": 1, + }, + ) + assert response.status_code == 200 + + # check topics again + response = await ac.get(f"/transcripts/{transcript_id}/topics/with-words") + assert response.status_code == 200 + topics = response.json() + assert len(topics) == 2 + + # check through words + assert topics[0]["words"][0]["speaker"] == 1 + assert topics[0]["words"][1]["speaker"] == 1 + assert topics[1]["words"][0]["speaker"] == 0 + assert topics[1]["words"][1]["speaker"] == 0 + + # merge speakers + response = await ac.patch( + f"/transcripts/{transcript_id}/speaker/merge", + json={ + "speaker_from": 1, + "speaker_to": 0, + }, + ) + assert response.status_code == 200 + + # check topics again + response = await ac.get(f"/transcripts/{transcript_id}/topics/with-words") + assert response.status_code == 200 + topics = response.json() + assert len(topics) == 2 + + # check through words + assert topics[0]["words"][0]["speaker"] == 0 + assert topics[0]["words"][1]["speaker"] == 0 + assert topics[1]["words"][0]["speaker"] == 0 + assert topics[1]["words"][1]["speaker"] == 0 + + +@pytest.mark.asyncio +async def test_transcript_reassign_with_participant(fake_transcript_with_topics): + from reflector.app import app + + transcript_id = fake_transcript_with_topics.id + + async with AsyncClient(app=app, base_url="http://test/v1") as ac: + # check the transcript exists + response = await ac.get(f"/transcripts/{transcript_id}") + assert response.status_code == 200 + transcript = response.json() + assert len(transcript["participants"]) == 0 + + # create 2 participants + response = await ac.post( + f"/transcripts/{transcript_id}/participants", + json={ + "name": "Participant 1", + }, + ) + assert response.status_code == 200 + participant1_id = response.json()["id"] + + response = await ac.post( + f"/transcripts/{transcript_id}/participants", + json={ + "name": "Participant 2", + }, + ) + assert response.status_code == 200 + participant2_id = response.json()["id"] + + # check participants speakers + response = await ac.get(f"/transcripts/{transcript_id}/participants") + assert response.status_code == 200 + participants = response.json() + assert len(participants) == 2 + assert participants[0]["name"] == "Participant 1" + assert participants[0]["speaker"] is None + assert participants[1]["name"] == "Participant 2" + assert participants[1]["speaker"] is None + + # check initial topics of the transcript + response = await ac.get(f"/transcripts/{transcript_id}/topics/with-words") + assert response.status_code == 200 + topics = response.json() + assert len(topics) == 2 + + # check through words + assert topics[0]["words"][0]["speaker"] == 0 + assert topics[0]["words"][1]["speaker"] == 0 + assert topics[1]["words"][0]["speaker"] == 0 + assert topics[1]["words"][1]["speaker"] == 0 + # check through segments + assert len(topics[0]["segments"]) == 1 + assert topics[0]["segments"][0]["speaker"] == 0 + assert len(topics[1]["segments"]) == 1 + assert topics[1]["segments"][0]["speaker"] == 0 + + # reassign speaker from a participant + response = await ac.patch( + f"/transcripts/{transcript_id}/speaker/assign", + json={ + "participant": participant1_id, + "timestamp_from": 0, + "timestamp_to": 1, + }, + ) + assert response.status_code == 200 + + # check participants if speaker has been assigned + # first participant should have 1, because it's not used yet. + response = await ac.get(f"/transcripts/{transcript_id}/participants") + assert response.status_code == 200 + participants = response.json() + assert len(participants) == 2 + assert participants[0]["name"] == "Participant 1" + assert participants[0]["speaker"] == 1 + assert participants[1]["name"] == "Participant 2" + assert participants[1]["speaker"] is None + + # check topics again + response = await ac.get(f"/transcripts/{transcript_id}/topics/with-words") + assert response.status_code == 200 + topics = response.json() + assert len(topics) == 2 + + # check through words + assert topics[0]["words"][0]["speaker"] == 1 + assert topics[0]["words"][1]["speaker"] == 1 + assert topics[1]["words"][0]["speaker"] == 0 + assert topics[1]["words"][1]["speaker"] == 0 + # check segments + assert len(topics[0]["segments"]) == 1 + assert topics[0]["segments"][0]["speaker"] == 1 + assert len(topics[1]["segments"]) == 1 + assert topics[1]["segments"][0]["speaker"] == 0 + + # reassign participant, middle of 2 topics + response = await ac.patch( + f"/transcripts/{transcript_id}/speaker/assign", + json={ + "participant": participant2_id, + "timestamp_from": 1, + "timestamp_to": 2.5, + }, + ) + assert response.status_code == 200 + + # check participants if speaker has been assigned + # first participant should have 1, because it's not used yet. + response = await ac.get(f"/transcripts/{transcript_id}/participants") + assert response.status_code == 200 + participants = response.json() + assert len(participants) == 2 + assert participants[0]["name"] == "Participant 1" + assert participants[0]["speaker"] == 1 + assert participants[1]["name"] == "Participant 2" + assert participants[1]["speaker"] == 2 + + # check topics again + response = await ac.get(f"/transcripts/{transcript_id}/topics/with-words") + assert response.status_code == 200 + topics = response.json() + assert len(topics) == 2 + + # check through words + assert topics[0]["words"][0]["speaker"] == 1 + assert topics[0]["words"][1]["speaker"] == 2 + assert topics[1]["words"][0]["speaker"] == 2 + assert topics[1]["words"][1]["speaker"] == 0 + # check segments + assert len(topics[0]["segments"]) == 2 + assert topics[0]["segments"][0]["speaker"] == 1 + assert topics[0]["segments"][1]["speaker"] == 2 + assert len(topics[1]["segments"]) == 2 + assert topics[1]["segments"][0]["speaker"] == 2 + assert topics[1]["segments"][1]["speaker"] == 0 + + # reassign speaker, everything + response = await ac.patch( + f"/transcripts/{transcript_id}/speaker/assign", + json={ + "participant": participant1_id, + "timestamp_from": 0, + "timestamp_to": 100, + }, + ) + assert response.status_code == 200 + + # check topics again + response = await ac.get(f"/transcripts/{transcript_id}/topics/with-words") + assert response.status_code == 200 + topics = response.json() + assert len(topics) == 2 + + # check through words + assert topics[0]["words"][0]["speaker"] == 1 + assert topics[0]["words"][1]["speaker"] == 1 + assert topics[1]["words"][0]["speaker"] == 1 + assert topics[1]["words"][1]["speaker"] == 1 + # check segments + assert len(topics[0]["segments"]) == 1 + assert topics[0]["segments"][0]["speaker"] == 1 + assert len(topics[1]["segments"]) == 1 + assert topics[1]["segments"][0]["speaker"] == 1 + + +@pytest.mark.asyncio +async def test_transcript_reassign_edge_cases(fake_transcript_with_topics): + from reflector.app import app + + transcript_id = fake_transcript_with_topics.id + + async with AsyncClient(app=app, base_url="http://test/v1") as ac: + # check the transcript exists + response = await ac.get(f"/transcripts/{transcript_id}") + assert response.status_code == 200 + transcript = response.json() + assert len(transcript["participants"]) == 0 + + # try reassign without any participant_id or speaker + response = await ac.patch( + f"/transcripts/{transcript_id}/speaker/assign", + json={ + "timestamp_from": 0, + "timestamp_to": 1, + }, + ) + assert response.status_code == 400 + + # try reassing with both participant_id and speaker + response = await ac.patch( + f"/transcripts/{transcript_id}/speaker/assign", + json={ + "participant": "123", + "speaker": 1, + "timestamp_from": 0, + "timestamp_to": 1, + }, + ) + assert response.status_code == 400 + + # try reassing with non-existing participant_id + response = await ac.patch( + f"/transcripts/{transcript_id}/speaker/assign", + json={ + "participant": "123", + "timestamp_from": 0, + "timestamp_to": 1, + }, + ) + assert response.status_code == 404 diff --git a/server/tests/test_transcripts_topics.py b/server/tests/test_transcripts_topics.py new file mode 100644 index 00000000..cd845b3f --- /dev/null +++ b/server/tests/test_transcripts_topics.py @@ -0,0 +1,26 @@ +import pytest +from httpx import AsyncClient + + +@pytest.mark.asyncio +async def test_transcript_topics(fake_transcript_with_topics): + from reflector.app import app + + transcript_id = fake_transcript_with_topics.id + + async with AsyncClient(app=app, base_url="http://test/v1") as ac: + # check the transcript exists + response = await ac.get(f"/transcripts/{transcript_id}/topics") + assert response.status_code == 200 + assert len(response.json()) == 2 + topic_id = response.json()[0]["id"] + + # get words per speakers + response = await ac.get( + f"/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker" + ) + assert response.status_code == 200 + data = response.json() + assert len(data["words_per_speaker"]) == 1 + assert data["words_per_speaker"][0]["speaker"] == 0 + assert len(data["words_per_speaker"][0]["words"]) == 2 diff --git a/www/app/api/.openapi-generator/FILES b/www/app/api/.openapi-generator/FILES index 2b965ee1..0ba155bf 100644 --- a/www/app/api/.openapi-generator/FILES +++ b/www/app/api/.openapi-generator/FILES @@ -9,12 +9,15 @@ models/GetTranscript.ts models/GetTranscriptSegmentTopic.ts models/GetTranscriptTopic.ts models/GetTranscriptTopicWithWords.ts +models/GetTranscriptTopicWithWordsPerSpeaker.ts models/HTTPValidationError.ts models/PageGetTranscript.ts models/Participant.ts models/RtcOffer.ts models/SpeakerAssignment.ts models/SpeakerAssignmentStatus.ts +models/SpeakerMerge.ts +models/SpeakerWords.ts models/TranscriptParticipant.ts models/UpdateParticipant.ts models/UpdateTranscript.ts diff --git a/www/app/api/apis/DefaultApi.ts b/www/app/api/apis/DefaultApi.ts index ded2fa2e..855f7cda 100644 --- a/www/app/api/apis/DefaultApi.ts +++ b/www/app/api/apis/DefaultApi.ts @@ -19,12 +19,14 @@ import type { CreateTranscript, DeletionStatus, GetTranscript, + GetTranscriptTopicWithWordsPerSpeaker, HTTPValidationError, PageGetTranscript, Participant, RtcOffer, SpeakerAssignment, SpeakerAssignmentStatus, + SpeakerMerge, UpdateParticipant, UpdateTranscript, } from "../models"; @@ -39,6 +41,8 @@ import { DeletionStatusToJSON, GetTranscriptFromJSON, GetTranscriptToJSON, + GetTranscriptTopicWithWordsPerSpeakerFromJSON, + GetTranscriptTopicWithWordsPerSpeakerToJSON, HTTPValidationErrorFromJSON, HTTPValidationErrorToJSON, PageGetTranscriptFromJSON, @@ -51,6 +55,8 @@ import { SpeakerAssignmentToJSON, SpeakerAssignmentStatusFromJSON, SpeakerAssignmentStatusToJSON, + SpeakerMergeFromJSON, + SpeakerMergeToJSON, UpdateParticipantFromJSON, UpdateParticipantToJSON, UpdateTranscriptFromJSON, @@ -106,10 +112,20 @@ export interface V1TranscriptGetTopicsWithWordsRequest { transcriptId: any; } +export interface V1TranscriptGetTopicsWithWordsPerSpeakerRequest { + transcriptId: any; + topicId: any; +} + export interface V1TranscriptGetWebsocketEventsRequest { transcriptId: any; } +export interface V1TranscriptMergeSpeakerRequest { + transcriptId: any; + speakerMerge: SpeakerMerge; +} + export interface V1TranscriptRecordWebrtcRequest { transcriptId: any; rtcOffer: RtcOffer; @@ -917,6 +933,82 @@ export class DefaultApi extends runtime.BaseAPI { return await response.value(); } + /** + * Transcript Get Topics With Words Per Speaker + */ + async v1TranscriptGetTopicsWithWordsPerSpeakerRaw( + requestParameters: V1TranscriptGetTopicsWithWordsPerSpeakerRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + if ( + requestParameters.transcriptId === null || + requestParameters.transcriptId === undefined + ) { + throw new runtime.RequiredError( + "transcriptId", + "Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptGetTopicsWithWordsPerSpeaker.", + ); + } + + if ( + requestParameters.topicId === null || + requestParameters.topicId === undefined + ) { + throw new runtime.RequiredError( + "topicId", + "Required parameter requestParameters.topicId was null or undefined when calling v1TranscriptGetTopicsWithWordsPerSpeaker.", + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + if (this.configuration && this.configuration.accessToken) { + // oauth required + headerParameters["Authorization"] = await this.configuration.accessToken( + "OAuth2AuthorizationCodeBearer", + [], + ); + } + + const response = await this.request( + { + path: `/v1/transcripts/{transcript_id}/topics/{topic_id}/words-per-speaker` + .replace( + `{${"transcript_id"}}`, + encodeURIComponent(String(requestParameters.transcriptId)), + ) + .replace( + `{${"topic_id"}}`, + encodeURIComponent(String(requestParameters.topicId)), + ), + method: "GET", + headers: headerParameters, + query: queryParameters, + }, + initOverrides, + ); + + return new runtime.JSONApiResponse(response, (jsonValue) => + GetTranscriptTopicWithWordsPerSpeakerFromJSON(jsonValue), + ); + } + + /** + * Transcript Get Topics With Words Per Speaker + */ + async v1TranscriptGetTopicsWithWordsPerSpeaker( + requestParameters: V1TranscriptGetTopicsWithWordsPerSpeakerRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.v1TranscriptGetTopicsWithWordsPerSpeakerRaw( + requestParameters, + initOverrides, + ); + return await response.value(); + } + /** * Transcript Get Websocket Events */ @@ -972,6 +1064,80 @@ export class DefaultApi extends runtime.BaseAPI { return await response.value(); } + /** + * Transcript Merge Speaker + */ + async v1TranscriptMergeSpeakerRaw( + requestParameters: V1TranscriptMergeSpeakerRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise> { + if ( + requestParameters.transcriptId === null || + requestParameters.transcriptId === undefined + ) { + throw new runtime.RequiredError( + "transcriptId", + "Required parameter requestParameters.transcriptId was null or undefined when calling v1TranscriptMergeSpeaker.", + ); + } + + if ( + requestParameters.speakerMerge === null || + requestParameters.speakerMerge === undefined + ) { + throw new runtime.RequiredError( + "speakerMerge", + "Required parameter requestParameters.speakerMerge was null or undefined when calling v1TranscriptMergeSpeaker.", + ); + } + + const queryParameters: any = {}; + + const headerParameters: runtime.HTTPHeaders = {}; + + headerParameters["Content-Type"] = "application/json"; + + if (this.configuration && this.configuration.accessToken) { + // oauth required + headerParameters["Authorization"] = await this.configuration.accessToken( + "OAuth2AuthorizationCodeBearer", + [], + ); + } + + const response = await this.request( + { + path: `/v1/transcripts/{transcript_id}/speaker/merge`.replace( + `{${"transcript_id"}}`, + encodeURIComponent(String(requestParameters.transcriptId)), + ), + method: "PATCH", + headers: headerParameters, + query: queryParameters, + body: SpeakerMergeToJSON(requestParameters.speakerMerge), + }, + initOverrides, + ); + + return new runtime.JSONApiResponse(response, (jsonValue) => + SpeakerAssignmentStatusFromJSON(jsonValue), + ); + } + + /** + * Transcript Merge Speaker + */ + async v1TranscriptMergeSpeaker( + requestParameters: V1TranscriptMergeSpeakerRequest, + initOverrides?: RequestInit | runtime.InitOverrideFunction, + ): Promise { + const response = await this.v1TranscriptMergeSpeakerRaw( + requestParameters, + initOverrides, + ); + return await response.value(); + } + /** * Transcript Record Webrtc */ diff --git a/www/app/api/models/GetTranscriptTopic.ts b/www/app/api/models/GetTranscriptTopic.ts index 460b8b39..10dcb102 100644 --- a/www/app/api/models/GetTranscriptTopic.ts +++ b/www/app/api/models/GetTranscriptTopic.ts @@ -43,6 +43,12 @@ export interface GetTranscriptTopic { * @memberof GetTranscriptTopic */ timestamp: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopic + */ + duration: any | null; /** * * @type {any} @@ -66,6 +72,7 @@ export function instanceOfGetTranscriptTopic(value: object): boolean { isInstance = isInstance && "title" in value; isInstance = isInstance && "summary" in value; isInstance = isInstance && "timestamp" in value; + isInstance = isInstance && "duration" in value; isInstance = isInstance && "transcript" in value; return isInstance; @@ -87,6 +94,7 @@ export function GetTranscriptTopicFromJSONTyped( title: json["title"], summary: json["summary"], timestamp: json["timestamp"], + duration: json["duration"], transcript: json["transcript"], segments: !exists(json, "segments") ? undefined : json["segments"], }; @@ -106,6 +114,7 @@ export function GetTranscriptTopicToJSON( title: value.title, summary: value.summary, timestamp: value.timestamp, + duration: value.duration, transcript: value.transcript, segments: value.segments, }; diff --git a/www/app/api/models/GetTranscriptTopicWithWords.ts b/www/app/api/models/GetTranscriptTopicWithWords.ts index 5948ea1c..7a5b21b4 100644 --- a/www/app/api/models/GetTranscriptTopicWithWords.ts +++ b/www/app/api/models/GetTranscriptTopicWithWords.ts @@ -43,6 +43,12 @@ export interface GetTranscriptTopicWithWords { * @memberof GetTranscriptTopicWithWords */ timestamp: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWords + */ + duration: any | null; /** * * @type {any} @@ -72,6 +78,7 @@ export function instanceOfGetTranscriptTopicWithWords(value: object): boolean { isInstance = isInstance && "title" in value; isInstance = isInstance && "summary" in value; isInstance = isInstance && "timestamp" in value; + isInstance = isInstance && "duration" in value; isInstance = isInstance && "transcript" in value; return isInstance; @@ -95,6 +102,7 @@ export function GetTranscriptTopicWithWordsFromJSONTyped( title: json["title"], summary: json["summary"], timestamp: json["timestamp"], + duration: json["duration"], transcript: json["transcript"], segments: !exists(json, "segments") ? undefined : json["segments"], words: !exists(json, "words") ? undefined : json["words"], @@ -115,6 +123,7 @@ export function GetTranscriptTopicWithWordsToJSON( title: value.title, summary: value.summary, timestamp: value.timestamp, + duration: value.duration, transcript: value.transcript, segments: value.segments, words: value.words, diff --git a/www/app/api/models/GetTranscriptTopicWithWordsPerSpeaker.ts b/www/app/api/models/GetTranscriptTopicWithWordsPerSpeaker.ts new file mode 100644 index 00000000..845192c5 --- /dev/null +++ b/www/app/api/models/GetTranscriptTopicWithWordsPerSpeaker.ts @@ -0,0 +1,135 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * FastAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from "../runtime"; +/** + * + * @export + * @interface GetTranscriptTopicWithWordsPerSpeaker + */ +export interface GetTranscriptTopicWithWordsPerSpeaker { + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWordsPerSpeaker + */ + id: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWordsPerSpeaker + */ + title: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWordsPerSpeaker + */ + summary: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWordsPerSpeaker + */ + timestamp: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWordsPerSpeaker + */ + duration: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWordsPerSpeaker + */ + transcript: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWordsPerSpeaker + */ + segments?: any | null; + /** + * + * @type {any} + * @memberof GetTranscriptTopicWithWordsPerSpeaker + */ + wordsPerSpeaker?: any | null; +} + +/** + * Check if a given object implements the GetTranscriptTopicWithWordsPerSpeaker interface. + */ +export function instanceOfGetTranscriptTopicWithWordsPerSpeaker( + value: object, +): boolean { + let isInstance = true; + isInstance = isInstance && "id" in value; + isInstance = isInstance && "title" in value; + isInstance = isInstance && "summary" in value; + isInstance = isInstance && "timestamp" in value; + isInstance = isInstance && "duration" in value; + isInstance = isInstance && "transcript" in value; + + return isInstance; +} + +export function GetTranscriptTopicWithWordsPerSpeakerFromJSON( + json: any, +): GetTranscriptTopicWithWordsPerSpeaker { + return GetTranscriptTopicWithWordsPerSpeakerFromJSONTyped(json, false); +} + +export function GetTranscriptTopicWithWordsPerSpeakerFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): GetTranscriptTopicWithWordsPerSpeaker { + if (json === undefined || json === null) { + return json; + } + return { + id: json["id"], + title: json["title"], + summary: json["summary"], + timestamp: json["timestamp"], + duration: json["duration"], + transcript: json["transcript"], + segments: !exists(json, "segments") ? undefined : json["segments"], + wordsPerSpeaker: !exists(json, "words_per_speaker") + ? undefined + : json["words_per_speaker"], + }; +} + +export function GetTranscriptTopicWithWordsPerSpeakerToJSON( + value?: GetTranscriptTopicWithWordsPerSpeaker | null, +): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + id: value.id, + title: value.title, + summary: value.summary, + timestamp: value.timestamp, + duration: value.duration, + transcript: value.transcript, + segments: value.segments, + words_per_speaker: value.wordsPerSpeaker, + }; +} diff --git a/www/app/api/models/SpeakerAssignment.ts b/www/app/api/models/SpeakerAssignment.ts index 8c9ab5d8..b541ba9a 100644 --- a/www/app/api/models/SpeakerAssignment.ts +++ b/www/app/api/models/SpeakerAssignment.ts @@ -24,7 +24,13 @@ export interface SpeakerAssignment { * @type {any} * @memberof SpeakerAssignment */ - speaker: any | null; + speaker?: any | null; + /** + * + * @type {any} + * @memberof SpeakerAssignment + */ + participant?: any | null; /** * * @type {any} @@ -44,7 +50,6 @@ export interface SpeakerAssignment { */ export function instanceOfSpeakerAssignment(value: object): boolean { let isInstance = true; - isInstance = isInstance && "speaker" in value; isInstance = isInstance && "timestampFrom" in value; isInstance = isInstance && "timestampTo" in value; @@ -63,7 +68,8 @@ export function SpeakerAssignmentFromJSONTyped( return json; } return { - speaker: json["speaker"], + speaker: !exists(json, "speaker") ? undefined : json["speaker"], + participant: !exists(json, "participant") ? undefined : json["participant"], timestampFrom: json["timestamp_from"], timestampTo: json["timestamp_to"], }; @@ -78,6 +84,7 @@ export function SpeakerAssignmentToJSON(value?: SpeakerAssignment | null): any { } return { speaker: value.speaker, + participant: value.participant, timestamp_from: value.timestampFrom, timestamp_to: value.timestampTo, }; diff --git a/www/app/api/models/SpeakerMerge.ts b/www/app/api/models/SpeakerMerge.ts new file mode 100644 index 00000000..809d32a1 --- /dev/null +++ b/www/app/api/models/SpeakerMerge.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * FastAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from "../runtime"; +/** + * + * @export + * @interface SpeakerMerge + */ +export interface SpeakerMerge { + /** + * + * @type {any} + * @memberof SpeakerMerge + */ + speakerFrom: any | null; + /** + * + * @type {any} + * @memberof SpeakerMerge + */ + speakerTo: any | null; +} + +/** + * Check if a given object implements the SpeakerMerge interface. + */ +export function instanceOfSpeakerMerge(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "speakerFrom" in value; + isInstance = isInstance && "speakerTo" in value; + + return isInstance; +} + +export function SpeakerMergeFromJSON(json: any): SpeakerMerge { + return SpeakerMergeFromJSONTyped(json, false); +} + +export function SpeakerMergeFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): SpeakerMerge { + if (json === undefined || json === null) { + return json; + } + return { + speakerFrom: json["speaker_from"], + speakerTo: json["speaker_to"], + }; +} + +export function SpeakerMergeToJSON(value?: SpeakerMerge | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + speaker_from: value.speakerFrom, + speaker_to: value.speakerTo, + }; +} diff --git a/www/app/api/models/SpeakerWords.ts b/www/app/api/models/SpeakerWords.ts new file mode 100644 index 00000000..52f8b162 --- /dev/null +++ b/www/app/api/models/SpeakerWords.ts @@ -0,0 +1,75 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * FastAPI + * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +import { exists, mapValues } from "../runtime"; +/** + * + * @export + * @interface SpeakerWords + */ +export interface SpeakerWords { + /** + * + * @type {any} + * @memberof SpeakerWords + */ + speaker: any | null; + /** + * + * @type {any} + * @memberof SpeakerWords + */ + words: any | null; +} + +/** + * Check if a given object implements the SpeakerWords interface. + */ +export function instanceOfSpeakerWords(value: object): boolean { + let isInstance = true; + isInstance = isInstance && "speaker" in value; + isInstance = isInstance && "words" in value; + + return isInstance; +} + +export function SpeakerWordsFromJSON(json: any): SpeakerWords { + return SpeakerWordsFromJSONTyped(json, false); +} + +export function SpeakerWordsFromJSONTyped( + json: any, + ignoreDiscriminator: boolean, +): SpeakerWords { + if (json === undefined || json === null) { + return json; + } + return { + speaker: json["speaker"], + words: json["words"], + }; +} + +export function SpeakerWordsToJSON(value?: SpeakerWords | null): any { + if (value === undefined) { + return undefined; + } + if (value === null) { + return null; + } + return { + speaker: value.speaker, + words: value.words, + }; +} diff --git a/www/app/api/models/index.ts b/www/app/api/models/index.ts index 49e4badf..6bb9444e 100644 --- a/www/app/api/models/index.ts +++ b/www/app/api/models/index.ts @@ -8,12 +8,15 @@ export * from "./GetTranscript"; export * from "./GetTranscriptSegmentTopic"; export * from "./GetTranscriptTopic"; export * from "./GetTranscriptTopicWithWords"; +export * from "./GetTranscriptTopicWithWordsPerSpeaker"; export * from "./HTTPValidationError"; export * from "./PageGetTranscript"; export * from "./Participant"; export * from "./RtcOffer"; export * from "./SpeakerAssignment"; export * from "./SpeakerAssignmentStatus"; +export * from "./SpeakerMerge"; +export * from "./SpeakerWords"; export * from "./TranscriptParticipant"; export * from "./UpdateParticipant"; export * from "./UpdateTranscript";