mirror of
https://github.com/nextcloud/spreed.git
synced 2025-12-18 05:20:50 +01:00
feat: Add capability for live transcriptions in calls
Live transcriptions is an optional feature that is only available if the external app "live_transcription" is available. Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This commit is contained in:
parent
d8f1d929a0
commit
2ce8e7d1b7
24 changed files with 158 additions and 8 deletions
|
|
@ -196,3 +196,4 @@
|
|||
|
||||
## 22
|
||||
* `threads` - Whether the chat supports threads
|
||||
* `config => call => live-transcription` - Whether live transcription is supported in calls
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ declare(strict_types=1);
|
|||
namespace OCA\Talk;
|
||||
|
||||
use OCA\Talk\Chat\ChatManager;
|
||||
use OCA\Talk\Service\LiveTranscriptionService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\Capabilities\IPublicCapability;
|
||||
|
|
@ -208,6 +209,7 @@ class Capabilities implements IPublicCapability {
|
|||
protected IAppManager $appManager,
|
||||
protected ITranslationManager $translationManager,
|
||||
protected ITaskProcessingManager $taskProcessingManager,
|
||||
protected LiveTranscriptionService $liveTranscriptionService,
|
||||
ICacheFactory $cacheFactory,
|
||||
) {
|
||||
$this->talkCache = $cacheFactory->createLocal('talk::');
|
||||
|
|
@ -249,6 +251,8 @@ class Capabilities implements IPublicCapability {
|
|||
'max-duration' => $this->appConfig->getAppValueInt('max_call_duration'),
|
||||
'blur-virtual-background' => $this->talkConfig->getBlurVirtualBackground($user?->getUID()),
|
||||
'end-to-end-encryption' => $this->talkConfig->isCallEndToEndEncryptionEnabled(),
|
||||
'live-transcription' => $this->talkConfig->getSignalingMode() === Config::SIGNALING_EXTERNAL
|
||||
&& $this->liveTranscriptionService->isLiveTranscriptionAppEnabled(),
|
||||
],
|
||||
'chat' => [
|
||||
'max-length' => ChatManager::MAX_CHAT_LENGTH,
|
||||
|
|
|
|||
12
lib/Exceptions/LiveTranscriptionAppAPIException.php
Normal file
12
lib/Exceptions/LiveTranscriptionAppAPIException.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Exceptions;
|
||||
|
||||
class LiveTranscriptionAppAPIException extends \Exception {
|
||||
}
|
||||
|
|
@ -493,6 +493,7 @@ namespace OCA\Talk;
|
|||
* max-duration: int,
|
||||
* blur-virtual-background: bool,
|
||||
* end-to-end-encryption: bool,
|
||||
* live-transcription: bool,
|
||||
* },
|
||||
* chat: array{
|
||||
* max-length: int,
|
||||
|
|
|
|||
58
lib/Service/LiveTranscriptionService.php
Normal file
58
lib/Service/LiveTranscriptionService.php
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Talk\Service;
|
||||
|
||||
use OCA\AppAPI\PublicFunctions;
|
||||
use OCA\Talk\Exceptions\LiveTranscriptionAppAPIException;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Server;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
class LiveTranscriptionService {
|
||||
|
||||
public function __construct(
|
||||
private IAppManager $appManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public function isLiveTranscriptionAppEnabled(): bool {
|
||||
try {
|
||||
$appApiPublicFunctions = $this->getAppApiPublicFunctions();
|
||||
} catch (LiveTranscriptionAppAPIException $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$exApp = $appApiPublicFunctions->getExApp('live_transcription');
|
||||
if ($exApp === null || !$exApp['enabled']) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws LiveTranscriptionAppAPIException if app_api is not enabled or the
|
||||
* public functions could not be
|
||||
* got.
|
||||
*/
|
||||
private function getAppApiPublicFunctions(): object {
|
||||
if (!$this->appManager->isEnabledForUser('app_api')) {
|
||||
throw new LiveTranscriptionAppAPIException('app-api');
|
||||
}
|
||||
|
||||
try {
|
||||
$appApiPublicFunctions = Server::get(PublicFunctions::class);
|
||||
} catch (ContainerExceptionInterface|NotFoundExceptionInterface $e) {
|
||||
throw new LiveTranscriptionAppAPIException('app-api-functions');
|
||||
}
|
||||
|
||||
return $appApiPublicFunctions;
|
||||
}
|
||||
}
|
||||
|
|
@ -155,7 +155,8 @@
|
|||
"start-without-media",
|
||||
"max-duration",
|
||||
"blur-virtual-background",
|
||||
"end-to-end-encryption"
|
||||
"end-to-end-encryption",
|
||||
"live-transcription"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -215,6 +216,9 @@
|
|||
},
|
||||
"end-to-end-encryption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"live-transcription": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@
|
|||
"start-without-media",
|
||||
"max-duration",
|
||||
"blur-virtual-background",
|
||||
"end-to-end-encryption"
|
||||
"end-to-end-encryption",
|
||||
"live-transcription"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -148,6 +149,9 @@
|
|||
},
|
||||
"end-to-end-encryption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"live-transcription": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@
|
|||
"start-without-media",
|
||||
"max-duration",
|
||||
"blur-virtual-background",
|
||||
"end-to-end-encryption"
|
||||
"end-to-end-encryption",
|
||||
"live-transcription"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -148,6 +149,9 @@
|
|||
},
|
||||
"end-to-end-encryption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"live-transcription": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"start-without-media",
|
||||
"max-duration",
|
||||
"blur-virtual-background",
|
||||
"end-to-end-encryption"
|
||||
"end-to-end-encryption",
|
||||
"live-transcription"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -191,6 +192,9 @@
|
|||
},
|
||||
"end-to-end-encryption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"live-transcription": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@
|
|||
"start-without-media",
|
||||
"max-duration",
|
||||
"blur-virtual-background",
|
||||
"end-to-end-encryption"
|
||||
"end-to-end-encryption",
|
||||
"live-transcription"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -148,6 +149,9 @@
|
|||
},
|
||||
"end-to-end-encryption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"live-transcription": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -131,7 +131,8 @@
|
|||
"start-without-media",
|
||||
"max-duration",
|
||||
"blur-virtual-background",
|
||||
"end-to-end-encryption"
|
||||
"end-to-end-encryption",
|
||||
"live-transcription"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -191,6 +192,9 @@
|
|||
},
|
||||
"end-to-end-encryption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"live-transcription": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -289,7 +289,8 @@
|
|||
"start-without-media",
|
||||
"max-duration",
|
||||
"blur-virtual-background",
|
||||
"end-to-end-encryption"
|
||||
"end-to-end-encryption",
|
||||
"live-transcription"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -349,6 +350,9 @@
|
|||
},
|
||||
"end-to-end-encryption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"live-transcription": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -248,7 +248,8 @@
|
|||
"start-without-media",
|
||||
"max-duration",
|
||||
"blur-virtual-background",
|
||||
"end-to-end-encryption"
|
||||
"end-to-end-encryption",
|
||||
"live-transcription"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
|
|
@ -308,6 +309,9 @@
|
|||
},
|
||||
"end-to-end-encryption": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"live-transcription": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@
|
|||
<referencedClass name="GuzzleHttp\Exception\ServerException" />
|
||||
<referencedClass name="GuzzleHttp\Exception\ConnectException" />
|
||||
<referencedClass name="OC" />
|
||||
<referencedClass name="OCA\AppAPI\PublicFunctions" />
|
||||
<referencedClass name="OCA\Circles\Api\v1\Circles" />
|
||||
<referencedClass name="OCA\Circles\CirclesManager" />
|
||||
<referencedClass name="OCA\Circles\Events\AddingCircleMemberEvent" />
|
||||
|
|
@ -61,6 +62,7 @@
|
|||
<referencedClass name="Doctrine\DBAL\Schema\Table" />
|
||||
<referencedClass name="OC\DB\ConnectionAdapter" />
|
||||
<referencedClass name="OC\User\NoUserException" />
|
||||
<referencedClass name="OCA\AppAPI\PublicFunctions" />
|
||||
<referencedClass name="OCA\Circles\CirclesManager" />
|
||||
<referencedClass name="OCA\Circles\Model\Circle" />
|
||||
<referencedClass name="OCA\Circles\Model\Member" />
|
||||
|
|
|
|||
|
|
@ -150,6 +150,7 @@ export const mockedCapabilities: Capabilities = {
|
|||
'max-duration': 0,
|
||||
'blur-virtual-background': false,
|
||||
'end-to-end-encryption': false,
|
||||
'live-transcription': false,
|
||||
},
|
||||
chat: {
|
||||
'max-length': 32000,
|
||||
|
|
|
|||
|
|
@ -236,6 +236,7 @@ export type components = {
|
|||
"max-duration": number;
|
||||
"blur-virtual-background": boolean;
|
||||
"end-to-end-encryption": boolean;
|
||||
"live-transcription": boolean;
|
||||
};
|
||||
chat: {
|
||||
/** Format: int64 */
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ export type components = {
|
|||
"max-duration": number;
|
||||
"blur-virtual-background": boolean;
|
||||
"end-to-end-encryption": boolean;
|
||||
"live-transcription": boolean;
|
||||
};
|
||||
chat: {
|
||||
/** Format: int64 */
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export type components = {
|
|||
"max-duration": number;
|
||||
"blur-virtual-background": boolean;
|
||||
"end-to-end-encryption": boolean;
|
||||
"live-transcription": boolean;
|
||||
};
|
||||
chat: {
|
||||
/** Format: int64 */
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ export type components = {
|
|||
"max-duration": number;
|
||||
"blur-virtual-background": boolean;
|
||||
"end-to-end-encryption": boolean;
|
||||
"live-transcription": boolean;
|
||||
};
|
||||
chat: {
|
||||
/** Format: int64 */
|
||||
|
|
|
|||
|
|
@ -74,6 +74,7 @@ export type components = {
|
|||
"max-duration": number;
|
||||
"blur-virtual-background": boolean;
|
||||
"end-to-end-encryption": boolean;
|
||||
"live-transcription": boolean;
|
||||
};
|
||||
chat: {
|
||||
/** Format: int64 */
|
||||
|
|
|
|||
|
|
@ -182,6 +182,7 @@ export type components = {
|
|||
"max-duration": number;
|
||||
"blur-virtual-background": boolean;
|
||||
"end-to-end-encryption": boolean;
|
||||
"live-transcription": boolean;
|
||||
};
|
||||
chat: {
|
||||
/** Format: int64 */
|
||||
|
|
|
|||
|
|
@ -2276,6 +2276,7 @@ export type components = {
|
|||
"max-duration": number;
|
||||
"blur-virtual-background": boolean;
|
||||
"end-to-end-encryption": boolean;
|
||||
"live-transcription": boolean;
|
||||
};
|
||||
chat: {
|
||||
/** Format: int64 */
|
||||
|
|
|
|||
|
|
@ -1754,6 +1754,7 @@ export type components = {
|
|||
"max-duration": number;
|
||||
"blur-virtual-background": boolean;
|
||||
"end-to-end-encryption": boolean;
|
||||
"live-transcription": boolean;
|
||||
};
|
||||
chat: {
|
||||
/** Format: int64 */
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ use OCA\Talk\Chat\CommentsManager;
|
|||
use OCA\Talk\Config;
|
||||
use OCA\Talk\Participant;
|
||||
use OCA\Talk\Room;
|
||||
use OCA\Talk\Service\LiveTranscriptionService;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\Capabilities\IPublicCapability;
|
||||
|
|
@ -39,6 +40,7 @@ class CapabilitiesTest extends TestCase {
|
|||
protected IAppManager&MockObject $appManager;
|
||||
protected ITranslationManager&MockObject $translationManager;
|
||||
protected ITaskProcessingManager&MockObject $taskProcessingManager;
|
||||
protected LiveTranscriptionService&MockObject $liveTranscriptionService;
|
||||
protected ICacheFactory&MockObject $cacheFactory;
|
||||
protected ICache&MockObject $talkCache;
|
||||
|
||||
|
|
@ -52,6 +54,7 @@ class CapabilitiesTest extends TestCase {
|
|||
$this->appManager = $this->createMock(IAppManager::class);
|
||||
$this->translationManager = $this->createMock(ITranslationManager::class);
|
||||
$this->taskProcessingManager = $this->createMock(ITaskProcessingManager::class);
|
||||
$this->liveTranscriptionService = $this->createMock(LiveTranscriptionService::class);
|
||||
$this->cacheFactory = $this->createMock(ICacheFactory::class);
|
||||
$this->talkCache = $this->createMock(ICache::class);
|
||||
|
||||
|
|
@ -79,6 +82,7 @@ class CapabilitiesTest extends TestCase {
|
|||
$this->appManager,
|
||||
$this->translationManager,
|
||||
$this->taskProcessingManager,
|
||||
$this->liveTranscriptionService,
|
||||
$this->cacheFactory,
|
||||
);
|
||||
}
|
||||
|
|
@ -148,6 +152,7 @@ class CapabilitiesTest extends TestCase {
|
|||
'max-duration' => 0,
|
||||
'blur-virtual-background' => false,
|
||||
'end-to-end-encryption' => false,
|
||||
'live-transcription' => false,
|
||||
'predefined-backgrounds' => [
|
||||
'1_office.jpg',
|
||||
'2_home.jpg',
|
||||
|
|
@ -319,6 +324,7 @@ class CapabilitiesTest extends TestCase {
|
|||
'max-duration' => 0,
|
||||
'blur-virtual-background' => false,
|
||||
'end-to-end-encryption' => false,
|
||||
'live-transcription' => false,
|
||||
'predefined-backgrounds' => [
|
||||
'1_office.jpg',
|
||||
'2_home.jpg',
|
||||
|
|
@ -465,6 +471,31 @@ class CapabilitiesTest extends TestCase {
|
|||
$this->assertEquals($data['spreed']['config']['call']['recording'], $enabled);
|
||||
}
|
||||
|
||||
public static function dataTestConfigCallLiveTranscription(): array {
|
||||
return [
|
||||
[Config::SIGNALING_EXTERNAL, true, true],
|
||||
[Config::SIGNALING_EXTERNAL, false, false],
|
||||
[Config::SIGNALING_INTERNAL, true, false],
|
||||
[Config::SIGNALING_INTERNAL, false, false],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('dataTestConfigCallLiveTranscription')]
|
||||
public function testConfigCallLiveTranscription(string $signalingMode, bool $liveTranscriptionAppEnabled, bool $expectedEnabled): void {
|
||||
$capabilities = $this->getCapabilities();
|
||||
|
||||
$this->talkConfig->expects($this->any())
|
||||
->method('getSignalingMode')
|
||||
->willReturn($signalingMode);
|
||||
|
||||
$this->liveTranscriptionService->expects($this->any())
|
||||
->method('isLiveTranscriptionAppEnabled')
|
||||
->willReturn($liveTranscriptionAppEnabled);
|
||||
|
||||
$data = $capabilities->getCapabilities();
|
||||
$this->assertEquals($data['spreed']['config']['call']['live-transcription'], $expectedEnabled);
|
||||
}
|
||||
|
||||
public function testCapabilitiesTranslations(): void {
|
||||
$capabilities = $this->getCapabilities();
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue