From 4c2d477ea9b718a454688f03a0fe914ff2506b7e Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Mon, 18 Nov 2024 20:26:11 +0100 Subject: [PATCH] feat(AI-call-summary): Automatically summarize call transcript Signed-off-by: Joas Schilling --- docs/settings.md | 3 +- lib/AppInfo/Application.php | 8 +- lib/Controller/RecordingController.php | 5 +- lib/Notification/Notifier.php | 10 +- lib/Recording/Listener.php | 59 +++--- lib/Service/RecordingService.php | 177 +++++++++++++++--- .../features/bootstrap/FeatureContext.php | 1 + .../features/callapi/recording.feature | 87 ++++++++- tests/php/Service/RecordingServiceTest.php | 8 +- 9 files changed, 284 insertions(+), 74 deletions(-) diff --git a/docs/settings.md b/docs/settings.md index ad2543d8412..dc2cfe836c1 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -103,7 +103,8 @@ Legend: | `calls_start_without_media` | string
`yes` or `no` | `no` | Yes | | Whether participants start with enabled or disabled audio and video by default | | `breakout_rooms` | string
`yes` or `no` | `yes` | Yes | | Whether or not breakout rooms are allowed (Will only prevent creating new breakout rooms. Existing conversations are not modified.) | | `call_recording` | string
`yes` or `no` | `yes` | Yes | | Enable call recording | -| `call_recording_transcription` | string
`yes` or `no` | `no` | No | | Whether call recordings should automatically be transcripted when a transcription provider is enabled. | +| `call_recording_summary` | string
`yes` or `no` | `no` | No | | Whether call recordings should automatically be summarized when a transcription and summary provider is enabled. | +| `call_recording_transcription` | string
`yes` or `no` | `no` | No | | Whether call recordings should automatically be transcribed when a transcription provider is enabled. | | `sip_dialout` | string
`yes` or `no` | `no` | Yes | | SIP dial-out is allowed when a SIP bridge is configured | | `federation_enabled` | string
`yes` or `no` | `no` | Yes | | 🏗️ *Work in progress:* Whether or not federation with this instance is allowed | | `federation_incoming_enabled` | string
`1` or `0` | `1` | Yes | | 🏗️ *Work in progress:* Whether users of this instance can be invited to federated conversations | diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 2a54aca97bc..38499f63e67 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -148,8 +148,8 @@ use OCP\Share\Events\BeforeShareCreatedEvent; use OCP\Share\Events\ShareCreatedEvent; use OCP\Share\Events\VerifyMountPointEvent; -use OCP\SpeechToText\Events\TranscriptionFailedEvent; -use OCP\SpeechToText\Events\TranscriptionSuccessfulEvent; +use OCP\TaskProcessing\Events\TaskFailedEvent; +use OCP\TaskProcessing\Events\TaskSuccessfulEvent; use OCP\User\Events\BeforeUserLoggedOutEvent; use OCP\User\Events\UserChangedEvent; use OCP\User\Events\UserDeletedEvent; @@ -273,8 +273,8 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(RoomDeletedEvent::class, RecordingListener::class); $context->registerEventListener(CallEndedEvent::class, RecordingListener::class); $context->registerEventListener(CallEndedForEveryoneEvent::class, RecordingListener::class); - $context->registerEventListener(TranscriptionSuccessfulEvent::class, RecordingListener::class); - $context->registerEventListener(TranscriptionFailedEvent::class, RecordingListener::class); + $context->registerEventListener(TaskSuccessfulEvent::class, RecordingListener::class); + $context->registerEventListener(TaskFailedEvent::class, RecordingListener::class); // Federation listeners $context->registerEventListener(BeforeRoomDeletedEvent::class, TalkV1BeforeRoomDeletedListener::class); diff --git a/lib/Controller/RecordingController.php b/lib/Controller/RecordingController.php index f42caa1c535..665ba5faa0f 100644 --- a/lib/Controller/RecordingController.php +++ b/lib/Controller/RecordingController.php @@ -418,7 +418,8 @@ public function notificationDismiss(int $timestamp): DataResponse { $this->recordingService->notificationDismiss( $this->getRoom(), $this->participant, - $timestamp + $timestamp, + null, // FIXME we would/should extend the URL, but the iOS app is crafting it manually atm due to OS limitations ); } catch (InvalidArgumentException $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); @@ -446,7 +447,7 @@ public function shareToChat(int $fileId, int $timestamp): DataResponse { $this->getRoom(), $this->participant, $fileId, - $timestamp + $timestamp, ); } catch (InvalidArgumentException $e) { return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST); diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index f6ff48d2cf9..4e49f78ecfa 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -240,7 +240,7 @@ public function prepare(INotification $notification, string $languageCode): INot ->setLink($this->url->linkToRouteAbsolute('spreed.Page.showCall', ['token' => $room->getToken()])); $subject = $notification->getSubject(); - if ($subject === 'record_file_stored' || $subject === 'transcript_file_stored' || $subject === 'transcript_failed') { + if ($subject === 'record_file_stored' || $subject === 'transcript_file_stored' || $subject === 'transcript_failed' || $subject === 'summary_file_stored' || $subject === 'summary_failed') { return $this->parseStoredRecording($notification, $room, $participant, $l); } if ($subject === 'record_file_store_fail') { @@ -363,9 +363,15 @@ protected function parseStoredRecording( } elseif ($notification->getSubject() === 'transcript_file_stored') { $subject = $l->t('Transcript now available'); $message = $l->t('The transcript for the call in {call} was uploaded to {file}.'); - } else { + } elseif ($notification->getSubject() === 'transcript_failed') { $subject = $l->t('Failed to transcript call recording'); $message = $l->t('The server failed to transcript the recording at {file} for the call in {call}. Please reach out to the administration.'); + } elseif ($notification->getSubject() === 'summary_file_stored') { + $subject = $l->t('Call summary now available'); + $message = $l->t('The summary for the call in {call} was uploaded to {file}.'); + } else { + $subject = $l->t('Failed to summarize call recording'); + $message = $l->t('The server failed to summarize the recording at {file} for the call in {call}. Please reach out to the administration.'); } $notification diff --git a/lib/Recording/Listener.php b/lib/Recording/Listener.php index 75650197ce1..b384cafb4fe 100644 --- a/lib/Recording/Listener.php +++ b/lib/Recording/Listener.php @@ -20,11 +20,11 @@ use OCA\Talk\Service\RecordingService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; -use OCP\Files\File; use OCP\Files\IRootFolder; -use OCP\SpeechToText\Events\AbstractTranscriptionEvent; -use OCP\SpeechToText\Events\TranscriptionFailedEvent; -use OCP\SpeechToText\Events\TranscriptionSuccessfulEvent; +use OCP\TaskProcessing\Events\AbstractTaskProcessingEvent; +use OCP\TaskProcessing\Events\TaskFailedEvent; +use OCP\TaskProcessing\Events\TaskSuccessfulEvent; +use Psr\Log\LoggerInterface; /** * @template-implements IEventListener @@ -34,12 +34,17 @@ public function __construct( protected RecordingService $recordingService, protected ConsentService $consentService, protected IRootFolder $rootFolder, + protected LoggerInterface $logger, ) { } public function handle(Event $event): void { - if ($event instanceof AbstractTranscriptionEvent) { - $this->handleTranscriptionEvents($event); + if ($event instanceof AbstractTaskProcessingEvent) { + try { + $this->handleTranscriptionEvents($event); + } catch (\Throwable $e) { + $this->logger->error('An error occurred while processing recording AI follow-up task', ['exception' => $e]); + } return; } @@ -54,40 +59,40 @@ public function handle(Event $event): void { }; } - public function handleTranscriptionEvents(AbstractTranscriptionEvent $event): void { - if ($event->getAppId() !== Application::APP_ID) { + public function handleTranscriptionEvents(AbstractTaskProcessingEvent $event): void { + $task = $event->getTask(); + if ($task->getAppId() !== Application::APP_ID) { return; } - if ($event instanceof TranscriptionSuccessfulEvent) { - $this->successfulTranscript($event->getUserId(), $event->getFile(), $event->getTranscript()); - } elseif ($event instanceof TranscriptionFailedEvent) { - $this->failedTranscript($event->getUserId(), $event->getFile()); - } - } - - protected function successfulTranscript(?string $owner, ?File $fileNode, string $transcript): void { - if (!$fileNode instanceof File) { + // 'call/transcription/' . $room->getToken() + $customId = $task->getCustomId(); + if (str_starts_with($customId, 'call/transcription/')) { + $aiType = 'transcript'; + $roomToken = substr($customId, strlen('call/transcription/')); + + $fileId = (int)($task->getInput()['input'] ?? null); + } elseif (str_starts_with($customId, 'call/summary/')) { + $aiType = 'summary'; + [$roomToken, $fileId] = explode('/', substr($customId, strlen('call/summary/'))); + $fileId = (int)$fileId; + } else { return; } - if ($owner === null) { + if ($fileId === 0) { return; } - $this->recordingService->storeTranscript($owner, $fileNode, $transcript); - } - - protected function failedTranscript(?string $owner, ?File $fileNode): void { - if (!$fileNode instanceof File) { + if ($task->getUserId() === null) { return; } - if ($owner === null) { - return; + if ($event instanceof TaskSuccessfulEvent) { + $this->recordingService->storeTranscript($task->getUserId(), $roomToken, $fileId, $task->getOutput()['output'] ?? '', $aiType); + } elseif ($event instanceof TaskFailedEvent) { + $this->recordingService->notifyAboutFailedTranscript($task->getUserId(), $roomToken, $fileId, $aiType); } - - $this->recordingService->notifyAboutFailedTranscript($owner, $fileNode); } protected function roomDeleted(RoomDeletedEvent $event): void { diff --git a/lib/Service/RecordingService.php b/lib/Service/RecordingService.php index f0ab59f0da0..448e31e01da 100644 --- a/lib/Service/RecordingService.php +++ b/lib/Service/RecordingService.php @@ -29,10 +29,13 @@ use OCP\Files\NotPermittedException; use OCP\IConfig; use OCP\Notification\IManager; -use OCP\PreConditionNotMetException; use OCP\Share\IManager as ShareManager; use OCP\Share\IShare; -use OCP\SpeechToText\ISpeechToTextManager; +use OCP\TaskProcessing\Exception\Exception; +use OCP\TaskProcessing\IManager as ITaskProcessingManager; +use OCP\TaskProcessing\Task; +use OCP\TaskProcessing\TaskTypes\AudioToText; +use OCP\TaskProcessing\TaskTypes\TextToTextSummary; use Psr\Log\LoggerInterface; class RecordingService { @@ -73,7 +76,7 @@ public function __construct( protected ChatManager $chatManager, protected LoggerInterface $logger, protected BackendNotifier $backendNotifier, - protected ISpeechToTextManager $speechToTextManager, + protected ITaskProcessingManager $taskProcessingManager, ) { } @@ -139,42 +142,120 @@ public function store(Room $room, string $owner, array $file): void { throw new InvalidArgumentException('owner_permission'); } - if ($this->serverConfig->getAppValue('spreed', 'call_recording_transcription', 'no') === 'no') { + $shouldTranscribe = $this->serverConfig->getAppValue('spreed', 'call_recording_transcription', 'no') === 'yes'; + $shouldSummarize = $this->serverConfig->getAppValue('spreed', 'call_recording_summary', 'no') === 'yes'; + if (!$shouldTranscribe && !$shouldSummarize) { + $this->logger->debug('Skipping transcription and summary of call recording, as both are disabled'); return; } + $supportedTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); + if (!isset($supportedTaskTypes[AudioToText::ID])) { + $this->logger->error('Can not transcribe call recording as no Audio2Text task provider is available'); + return; + } + + $task = new Task( + AudioToText::ID, + ['input' => $fileNode->getId()], + Application::APP_ID, + $owner, + 'call/transcription/' . $room->getToken(), + ); + try { - $this->speechToTextManager->scheduleFileTranscription($fileNode, $owner, Application::APP_ID); - $this->logger->debug('Scheduled transcription of call recording'); - } catch (PreConditionNotMetException $e) { - // No Speech-to-text provider installed - $this->logger->debug('Could not generate transcript of call recording', ['exception' => $e]); - } catch (InvalidArgumentException $e) { - $this->logger->warning('Could not generate transcript of call recording', ['exception' => $e]); + $this->taskProcessingManager->scheduleTask($task); + $this->logger->debug('Scheduled call recording transcript'); + } catch (Exception $e) { + $this->logger->error('An error occurred while trying to transcribe the call recording', ['exception' => $e]); } } - public function storeTranscript(string $owner, File $recording, string $transcript): void { + /** + * @param 'transcript'|'summary' $aiTask + */ + public function storeTranscript(string $owner, string $roomToken, int $recordingFileId, string $output, string $aiTask): void { + $userFolder = $this->rootFolder->getUserFolder($owner); + $recordingNodes = $userFolder->getById($recordingFileId); + + if (empty($recordingNodes)) { + $this->logger->warning("Could not save recording $aiTask as the recording could not be found", [ + 'owner' => $owner, + 'roomToken' => $roomToken, + 'recordingFileId' => $recordingFileId, + ]); + throw new InvalidArgumentException('owner_participant'); + } + $recording = array_pop($recordingNodes); $recordingFolder = $recording->getParent(); - $roomToken = $recordingFolder->getName(); + + if ($recordingFolder->getName() !== $roomToken) { + $this->logger->warning("Could not determinate conversation when trying to store $aiTask of call recording, as folder name did not match customId conversation token"); + throw new InvalidArgumentException('owner_participant'); + } try { $room = $this->roomManager->getRoomForUserByToken($roomToken, $owner); $participant = $this->participantService->getParticipant($room, $owner); } catch (ParticipantNotFoundException) { - $this->logger->warning('Could not determinate conversation when trying to store transcription of call recording'); + $this->logger->warning("Could not determinate conversation when trying to store $aiTask of call recording"); throw new InvalidArgumentException('owner_participant'); } - $transcriptFileName = pathinfo($recording->getName(), PATHINFO_FILENAME) . '.md'; + $shouldTranscribe = $this->serverConfig->getAppValue('spreed', 'call_recording_transcription', 'no') === 'yes'; + $shouldSummarize = $this->serverConfig->getAppValue('spreed', 'call_recording_summary', 'no') === 'yes'; + + if ($aiTask === 'transcript') { + $transcriptFileName = pathinfo($recording->getName(), PATHINFO_FILENAME) . '.md'; + if (!$shouldTranscribe) { + $this->logger->debug('Skipping saving of transcript for call recording as it is disabled'); + } + } else { + $transcriptFileName = pathinfo($recording->getName(), PATHINFO_FILENAME) . ' - ' . $aiTask . '.md'; + } + + if (($shouldTranscribe && $aiTask === 'transcript') + || ($shouldSummarize && $aiTask === 'summary')) { + try { + $fileNode = $recordingFolder->newFile($transcriptFileName, $output); + $this->notifyStoredTranscript($room, $participant, $fileNode, $aiTask); + } catch (NoUserException) { + throw new InvalidArgumentException('owner_invalid'); + } catch (NotPermittedException) { + throw new InvalidArgumentException('owner_permission'); + } + } + + if (!$shouldSummarize) { + // If summary is off skip scheduling it + $this->logger->debug('Skipping scheduling summary of call recording as it is disabled'); + return; + } + + if ($aiTask === 'summary') { + // After saving the summary there is nothing more to do + return; + } + + $supportedTaskTypes = $this->taskProcessingManager->getAvailableTaskTypes(); + if (!isset($supportedTaskTypes[TextToTextSummary::ID])) { + $this->logger->error('Can not summarize call recording as no TextToTextSummary task provider is available'); + return; + } + + $task = new Task( + TextToTextSummary::ID, + ['input' => $output], + Application::APP_ID, + $owner, + 'call/summary/' . $room->getToken() . '/' . $recordingFileId, + ); try { - $fileNode = $recordingFolder->newFile($transcriptFileName, $transcript); - $this->notifyStoredTranscript($room, $participant, $fileNode); - } catch (NoUserException) { - throw new InvalidArgumentException('owner_invalid'); - } catch (NotPermittedException) { - throw new InvalidArgumentException('owner_permission'); + $this->taskProcessingManager->scheduleTask($task); + $this->logger->debug('Scheduled call recording summary'); + } catch (Exception $e) { + $this->logger->error('An error occurred while trying to summarize the call recording', ['exception' => $e]); } } @@ -207,15 +288,31 @@ public function notifyAboutFailedStore(Room $room): void { $this->notificationManager->notify($notification); } - public function notifyAboutFailedTranscript(string $owner, File $recording): void { + public function notifyAboutFailedTranscript(string $owner, string $roomToken, int $recordingFileId, string $aiType): void { + $userFolder = $this->rootFolder->getUserFolder($owner); + $recordingNodes = $userFolder->getById($recordingFileId); + + if (empty($recordingNodes)) { + $this->logger->warning("Could not trying to notify about failed $aiType as the recording could not be found", [ + 'owner' => $owner, + 'roomToken' => $roomToken, + 'recordingFileId' => $recordingFileId, + ]); + throw new InvalidArgumentException('owner_participant'); + } + $recording = array_pop($recordingNodes); $recordingFolder = $recording->getParent(); - $roomToken = $recordingFolder->getName(); + + if ($recordingFolder->getName() !== $roomToken) { + $this->logger->warning("Could not determinate conversation when trying to notify about failed $aiType, as folder name did not match customId conversation token"); + throw new InvalidArgumentException('owner_participant'); + } try { $room = $this->roomManager->getRoomForUserByToken($roomToken, $owner); $participant = $this->participantService->getParticipant($room, $owner); } catch (ParticipantNotFoundException) { - $this->logger->warning('Could not determinate conversation when trying to notify about failed transcription of call recording'); + $this->logger->warning("Could not determinate conversation when trying to notify about failed $aiType of call recording"); throw new InvalidArgumentException('owner_participant'); } @@ -228,7 +325,7 @@ public function notifyAboutFailedTranscript(string $owner, File $recording): voi ->setDateTime($this->timeFactory->getDateTime()) ->setObject('recording', $room->getToken()) ->setUser($attendee->getActorId()) - ->setSubject('transcript_failed', [ + ->setSubject($aiType === 'transcript' ? 'transcript_failed' : 'summary_failed', [ 'objectId' => $recording->getId(), ]); $this->notificationManager->notify($notification); @@ -342,7 +439,10 @@ public function notifyStoredRecording(Room $room, Participant $participant, File } - public function notifyStoredTranscript(Room $room, Participant $participant, File $file): void { + /** + * @param 'transcript'|'summary' $aiType + */ + public function notifyStoredTranscript(Room $room, Participant $participant, File $file, string $aiType): void { $attendee = $participant->getAttendee(); $notification = $this->notificationManager->createNotification(); @@ -352,20 +452,26 @@ public function notifyStoredTranscript(Room $room, Participant $participant, Fil ->setDateTime($this->timeFactory->getDateTime()) ->setObject('recording', $room->getToken()) ->setUser($attendee->getActorId()) - ->setSubject('transcript_file_stored', [ + ->setSubject($aiType === 'transcript' ? 'transcript_file_stored' : 'summary_file_stored', [ 'objectId' => $file->getId(), ]); $this->notificationManager->notify($notification); } - public function notificationDismiss(Room $room, Participant $participant, int $timestamp): void { + public function notificationDismiss(Room $room, Participant $participant, int $timestamp, ?string $notificationSubject): void { $notification = $this->notificationManager->createNotification(); $notification->setApp('spreed') ->setObject('recording', $room->getToken()) ->setDateTime($this->timeFactory->getDateTime('@' . $timestamp)) ->setUser($participant->getAttendee()->getActorId()); - foreach (['record_file_stored', 'transcript_file_stored'] as $subject) { + if ($notificationSubject === null) { + $subjects = ['record_file_stored', 'transcript_file_stored', 'summary_file_stored']; + } else { + $subjects = [$notificationSubject]; + } + + foreach ($subjects as $subject) { $notification->setSubject($subject); $this->notificationManager->markProcessed($notification); } @@ -383,8 +489,8 @@ public function shareToChat(Room $room, Participant $participant, int $fileId, i $userFolder = $this->rootFolder->getUserFolder( $participant->getAttendee()->getActorId() ); - /** @var \OCP\Files\File[] */ $files = $userFolder->getById($fileId); + /** @var \OCP\Files\File $file */ $file = array_shift($files); } catch (\Throwable $th) { throw new InvalidArgumentException('file'); @@ -401,6 +507,15 @@ public function shareToChat(Room $room, Participant $participant, int $fileId, i ->setSharedWith($room->getToken()) ->setPermissions(\OCP\Constants::PERMISSION_READ); + $removeNotification = null; + if (!str_ends_with($file->getName(), '.md')) { + $removeNotification = 'record_file_stored'; + } elseif (!str_ends_with($file->getName(), ' - summary.md')) { + $removeNotification = 'transcript_file_stored'; + } elseif (str_ends_with($file->getName(), ' - summary.md')) { + $removeNotification = 'summary_file_stored'; + } + $share = $this->shareManager->createShare($share); $message = json_encode([ @@ -427,6 +542,6 @@ public function shareToChat(Room $room, Participant $participant, int $fileId, i $this->logger->error($e->getMessage(), ['exception' => $e]); throw new InvalidArgumentException('system'); } - $this->notificationDismiss($room, $participant, $timestamp); + $this->notificationDismiss($room, $participant, $timestamp, $removeNotification); } } diff --git a/tests/integration/features/bootstrap/FeatureContext.php b/tests/integration/features/bootstrap/FeatureContext.php index 3b894166848..7907c70dcf7 100644 --- a/tests/integration/features/bootstrap/FeatureContext.php +++ b/tests/integration/features/bootstrap/FeatureContext.php @@ -3943,6 +3943,7 @@ public function enableTestingApp(): void { $this->taskProcessingProviderPreference[$this->currentServer] = $this->lastStdOut; $preferences = json_decode($this->lastStdOut ?: '[]', true, flags: JSON_THROW_ON_ERROR); $preferences['core:text2text:summary'] = 'testing-text2text-summary'; + $preferences['core:audio2text'] = 'testing-audio2text'; $this->runOcc(['config:app:set', 'core', 'ai.taskprocessing_provider_preferences', '--value', json_encode($preferences)]); $this->setCurrentUser($currentUser); diff --git a/tests/integration/features/callapi/recording.feature b/tests/integration/features/callapi/recording.feature index 66d548b7d3f..f3081cca87f 100644 --- a/tests/integration/features/callapi/recording.feature +++ b/tests/integration/features/callapi/recording.feature @@ -448,8 +448,10 @@ Feature: callapi/recording | 2 | room1 | 0 | Scenario: Store recording with success and create transcript + Given Fake summary task provider is enabled Given the following spreed app config is set | call_recording_transcription | yes | + | call_recording_summary | yes | Given user "participant1" creates room "room1" (v4) | roomType | 2 | | roomName | room1 | @@ -461,9 +463,10 @@ Feature: callapi/recording And user "participant1" is participant of the following unordered rooms (v4) | type | name | callRecording | | 2 | room1 | 0 | - When run "OC\SpeechToText\TranscriptionJob" background jobs + And repeating run "OC\TaskProcessing\SynchronousBackgroundJob" background jobs Then user "participant1" has the following notifications | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Call summary now available | The summary for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call - summary.md. | | spreed | recording | room1 | Transcript now available | The transcript for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call.md. | | spreed | recording | room1 | Call recording now available | The recording for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call.ogg. | When user "participant1" shares file from the last notification to room "room1" with 200 (v1) @@ -475,7 +478,12 @@ Feature: callapi/recording | room1 | users | participant1 | participant1-displayname | record-audio | {file} | "IGNORE" | Then user "participant1" has the following notifications | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Call summary now available | The summary for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call - summary.md. | | spreed | recording | room1 | Transcript now available | The transcript for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call.md. | + When user "participant1" shares file from the last notification to room "room1" with 200 (v1) + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Call summary now available | The summary for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call - summary.md. | When user "participant1" shares file from the first notification to room "room1" with 200 (v1) Then user "participant1" has the following notifications | app | object_type | object_id | subject | message | @@ -486,15 +494,19 @@ Feature: callapi/recording | room | actorType | actorId | actorDisplayName | messageType | message | messageParameters | | room1 | users | participant1 | participant1-displayname | record-audio | {file} | "IGNORE" | | room1 | users | participant1 | participant1-displayname | record-audio | {file} | "IGNORE" | + | room1 | users | participant1 | participant1-displayname | record-audio | {file} | "IGNORE" | Scenario: Store recording with success but fail to transcript + Given Fake summary task provider is enabled Given the following spreed app config is set | call_recording_transcription | yes | + | call_recording_summary | yes | + Given the following testing app config is set + | fail-testing-audio2text | yes | Given user "participant1" creates room "room1" (v4) | roomType | 2 | | roomName | room1 | And user "participant1" joins room "room1" with 200 (v4) - # "leave" is used here as the file name makes the fake transcript provider fail When user "participant1" store recording file "/img/leave_call.ogg" in room "room1" with 200 (v1) Then user "participant1" has the following notifications | app | object_type | object_id | subject | message | @@ -502,12 +514,81 @@ Feature: callapi/recording And user "participant1" is participant of the following unordered rooms (v4) | type | name | callRecording | | 2 | room1 | 0 | - When run "OC\SpeechToText\TranscriptionJob" background jobs + And repeating run "OC\TaskProcessing\SynchronousBackgroundJob" background jobs Then user "participant1" has the following notifications | app | object_type | object_id | subject | message | | spreed | recording | room1 | Failed to transcript call recording | The server failed to transcript the recording at /Talk/Recording/ROOM(room1)/leave_call.ogg for the call in room1. Please reach out to the administration. | | spreed | recording | room1 | Call recording now available | The recording for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/leave_call.ogg. | + Scenario: Store recording and transcript with success but fail to summarize + Given Fake summary task provider is enabled + Given the following spreed app config is set + | call_recording_transcription | yes | + | call_recording_summary | yes | + Given the following testing app config is set + | fail-testing-text2text-summary | yes | + Given user "participant1" creates room "room1" (v4) + | roomType | 2 | + | roomName | room1 | + And user "participant1" joins room "room1" with 200 (v4) + When user "participant1" store recording file "/img/leave_call.ogg" in room "room1" with 200 (v1) + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Call recording now available | The recording for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/leave_call.ogg. | + And user "participant1" is participant of the following unordered rooms (v4) + | type | name | callRecording | + | 2 | room1 | 0 | + And repeating run "OC\TaskProcessing\SynchronousBackgroundJob" background jobs + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Failed to summarize call recording | The server failed to summarize the recording at /Talk/Recording/ROOM(room1)/leave_call.ogg for the call in room1. Please reach out to the administration. | + | spreed | recording | room1 | Transcript now available | The transcript for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/leave_call.md. | + | spreed | recording | room1 | Call recording now available | The recording for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/leave_call.ogg. | + + Scenario: Store recording and transcript with success but summarize is off + Given Fake summary task provider is enabled + Given the following spreed app config is set + | call_recording_transcription | yes | + | call_recording_summary | no | + Given user "participant1" creates room "room1" (v4) + | roomType | 2 | + | roomName | room1 | + And user "participant1" joins room "room1" with 200 (v4) + When user "participant1" store recording file "/img/leave_call.ogg" in room "room1" with 200 (v1) + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Call recording now available | The recording for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/leave_call.ogg. | + And user "participant1" is participant of the following unordered rooms (v4) + | type | name | callRecording | + | 2 | room1 | 0 | + And repeating run "OC\TaskProcessing\SynchronousBackgroundJob" background jobs + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Transcript now available | The transcript for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/leave_call.md. | + | spreed | recording | room1 | Call recording now available | The recording for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/leave_call.ogg. | + + Scenario: Store recording and summarize with success but transcript is off + Given Fake summary task provider is enabled + Given the following spreed app config is set + | call_recording_transcription | no | + | call_recording_summary | yes | + Given user "participant1" creates room "room1" (v4) + | roomType | 2 | + | roomName | room1 | + And user "participant1" joins room "room1" with 200 (v4) + When user "participant1" store recording file "/img/join_call.ogg" in room "room1" with 200 (v1) + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Call recording now available | The recording for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call.ogg. | + And user "participant1" is participant of the following unordered rooms (v4) + | type | name | callRecording | + | 2 | room1 | 0 | + And repeating run "OC\TaskProcessing\SynchronousBackgroundJob" background jobs + Then user "participant1" has the following notifications + | app | object_type | object_id | subject | message | + | spreed | recording | room1 | Call summary now available | The summary for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call - summary.md. | + | spreed | recording | room1 | Call recording now available | The recording for the call in room1 was uploaded to /Talk/Recording/ROOM(room1)/join_call.ogg. | + Scenario: Store recording with failure exceeding the upload_max_filesize Given user "participant1" creates room "room1" (v4) | roomType | 2 | diff --git a/tests/php/Service/RecordingServiceTest.php b/tests/php/Service/RecordingServiceTest.php index 4fe8c62c351..1503a0ac2fa 100644 --- a/tests/php/Service/RecordingServiceTest.php +++ b/tests/php/Service/RecordingServiceTest.php @@ -35,7 +35,7 @@ function is_uploaded_file($filename) { use OCP\IConfig; use OCP\Notification\IManager; use OCP\Share\IManager as ShareManager; -use OCP\SpeechToText\ISpeechToTextManager; +use OCP\TaskProcessing\IManager as ITaskProcessingManager; use PHPUnit\Framework\MockObject\MockObject; use Psr\Log\LoggerInterface; use Test\TestCase; @@ -55,7 +55,7 @@ class RecordingServiceTest extends TestCase { protected ChatManager&MockObject $chatManager; protected LoggerInterface&MockObject $logger; protected BackendNotifier&MockObject $backendNotifier; - protected ISpeechToTextManager&MockObject $speechToTextManager; + protected ITaskProcessingManager&MockObject $taskProcessingManager; protected RecordingService $recordingService; public function setUp(): void { @@ -75,7 +75,7 @@ public function setUp(): void { $this->chatManager = $this->createMock(ChatManager::class); $this->logger = $this->createMock(LoggerInterface::class); $this->backendNotifier = $this->createMock(BackendNotifier::class); - $this->speechToTextManager = $this->createMock(ISpeechToTextManager::class); + $this->taskProcessingManager = $this->createMock(ITaskProcessingManager::class); $this->recordingService = new RecordingService( $this->mimeTypeDetector, @@ -92,7 +92,7 @@ public function setUp(): void { $this->chatManager, $this->logger, $this->backendNotifier, - $this->speechToTextManager, + $this->taskProcessingManager, ); }