Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
44697d4
refactor: Extract check for live transcriptions to its own method
danxuliu Jan 8, 2026
c321fc4
feat: Add capability for live translations in calls
danxuliu Jan 8, 2026
1829757
fix: Do not get live_transcription capabilities from the capabilities
danxuliu Jan 8, 2026
cf5a8e8
feat: Add endpoints to get and set the live translation languages
danxuliu Jan 8, 2026
a1c1ec9
feat: Provide a default target language with the translation languages
danxuliu Jan 13, 2026
27e1e62
feat: Add target language for live translations to the user settings
danxuliu Jan 13, 2026
06a7f2f
feat: Add target language to settings store
danxuliu Jan 13, 2026
776387c
feat: Add translation languages to live transcription store
danxuliu Jan 13, 2026
0e9b3ba
refactor: Extract promise to its own variable to simplify logic
danxuliu Jan 13, 2026
a10ae03
fix: Log exception when getting live transcription languages
danxuliu Jan 13, 2026
e13188a
feat: Get language metadata also from target languages
danxuliu Jan 13, 2026
549a0d6
feat: Add selector for the target language to app settings
danxuliu Jan 13, 2026
99d3e2f
feat: Add live transcription target language to the call view store
danxuliu Jan 13, 2026
656d89b
refactor: Return whether live transcriptions could be enabled or not
danxuliu Jan 13, 2026
8da2256
feat: Convert live transcription button to split button
danxuliu Jan 13, 2026
923bc5f
feat: Show language names in the button actions
danxuliu Jan 13, 2026
72f9178
feat: Remember translation state after disabling transcriptions
danxuliu Jan 13, 2026
b932745
refactor: Invert label logic
danxuliu Jan 15, 2026
0e15bd7
feat: Add button for live translations to the collapsed menu
danxuliu Jan 15, 2026
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
2 changes: 2 additions & 0 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,3 +203,5 @@
* `federated-shared-items` - Whether shared items endpoints can be called in a federated conversation
* `config => chat => style` (local) - User selected chat style (split or unified for now)
* `scheduled-messages` (local) - Whether a user can schedule messages
* `config => call => live-translation` - Whether live translation is supported in calls
* `config => call => live-transcription-target-language-id` (local) - User defined string value with the id of the target language to use for live translations
53 changes: 51 additions & 2 deletions lib/Capabilities.php
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ class Capabilities implements IPublicCapability {
'can-upload-background',
'start-without-media',
'blur-virtual-background',
'live-transcription-target-language-id',
],
'chat' => [
'read-privacy',
Expand Down Expand Up @@ -256,8 +257,8 @@ public function getCapabilities(): array {
'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(),
'live-transcription' => $this->isLiveTranscriptionSupported(),
'live-translation' => $this->isLiveTranslationSupported(),
],
'chat' => [
'max-length' => ChatManager::MAX_CHAT_LENGTH,
Expand Down Expand Up @@ -370,11 +371,59 @@ public function getCapabilities(): array {
$capabilities['features'][] = 'call-end-to-end-encryption';
}

if ($user instanceof IUser) {
$capabilities['config']['call']['live-transcription-target-language-id'] = $this->talkConfig->getLiveTranscriptionTargetLanguageId($user->getUID());
} else {
$capabilities['config']['call']['live-transcription-target-language-id'] = $this->talkConfig->getLiveTranscriptionTargetLanguageId();
}

return [
'spreed' => $capabilities,
];
}

protected function isLiveTranscriptionSupported(): bool {
return $this->talkConfig->getSignalingMode() === Config::SIGNALING_EXTERNAL
&& $this->liveTranscriptionService->isLiveTranscriptionAppEnabled();
}

protected function isLiveTranslationSupported(): bool {
if (!$this->isLiveTranscriptionSupported()) {
return false;
}

// FIXME Getting the capabilities from the live_transcription app causes
// the Nextcloud capabilities to be requested, so it enters in a loop.
// For now checking whether text2text tasks are supported or not is
// directly done here instead (but that does not guarantee that
// translations are supported, as an old live_transcription app might be
// being used).
// $this->getLiveTranslationSupportedFromExAppCapabilities();

$supportedTaskTypeIds = $this->taskProcessingManager->getAvailableTaskTypeIds();

return in_array(TextToTextTranslate::ID, $supportedTaskTypeIds, true);
}

protected function getLiveTranslationSupportedFromExAppCapabilities(): bool {
$cacheKey = 'is_live_translation_supported';

$isLiveTranslationSupported = $this->talkCache->get($cacheKey);
if (is_bool($isLiveTranslationSupported)) {
return $isLiveTranslationSupported;
}

try {
$isLiveTranslationSupported = $this->liveTranscriptionService->isLiveTranslationSupported();
} catch (\Exception $e) {
$isLiveTranslationSupported = false;
}

$this->talkCache->set($cacheKey, $isLiveTranslationSupported, 300);

return $isLiveTranslationSupported;
}

/**
* @return list<string>
*/
Expand Down
9 changes: 9 additions & 0 deletions lib/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,15 @@ public function getRecordingFolder(string $userId): string {
);
}

public function getLiveTranscriptionTargetLanguageId(?string $userId = null): string {
return $this->config->getUserValue(
$userId,
'spreed',
UserPreference::LIVE_TRANSCRIPTION_TARGET_LANGUAGE_ID,
''
);
}

public function isDisabledForUser(IUser $user): bool {
$allowedGroups = $this->getAllowedTalkGroupIds();
if (empty($allowedGroups)) {
Expand Down
79 changes: 79 additions & 0 deletions lib/Controller/LiveTranscriptionController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace OCA\Talk\Controller;

use OCA\Talk\Exceptions\LiveTranscriptionAppNotEnabledException;
use OCA\Talk\Exceptions\LiveTranslationNotSupportedException;
use OCA\Talk\Middleware\Attribute\RequireCallEnabled;
use OCA\Talk\Middleware\Attribute\RequireModeratorOrNoLobby;
use OCA\Talk\Middleware\Attribute\RequireModeratorParticipant;
Expand Down Expand Up @@ -125,6 +126,38 @@ public function getAvailableLanguages(): DataResponse {
return new DataResponse($languages);
}

/**
* Get available languages for live translations
*
* The returned array provides a list of origin languages
* ("originLanguages") and a list of target languages ("targetLanguages").
* Any origin language can be translated to any target language.
*
* The origin language list can contain "detect_language" as a special value
* indicating auto-detection support.
*
* @return DataResponse<Http::STATUS_OK, array{originLanguages: array<string, TalkLiveTranscriptionLanguage>, targetLanguages: array<string, TalkLiveTranscriptionLanguage>, defaultTargetLanguageId: string}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'app'|'translations'}, array{}>
*
* 200: Available languages got successfully
* 400: The external app "live_transcription" is not available or
* translations are not supported.
*/
#[PublicPage]
#[ApiRoute(verb: 'GET', url: '/api/{apiVersion}/live-transcription/translation-languages', requirements: [
'apiVersion' => '(v1)',
])]
public function getAvailableTranslationLanguages(): DataResponse {
try {
$languages = $this->liveTranscriptionService->getAvailableTranslationLanguages();
} catch (LiveTranscriptionAppNotEnabledException $e) {
return new DataResponse(['error' => 'app'], Http::STATUS_BAD_REQUEST);
} catch (LiveTranslationNotSupportedException $e) {
return new DataResponse(['error' => 'translations'], Http::STATUS_BAD_REQUEST);
}

return new DataResponse($languages);
}

/**
* Set language for live transcriptions
*
Expand All @@ -150,4 +183,50 @@ public function setLanguage(string $languageId): DataResponse {

return new DataResponse(null);
}

/**
* Set target language for live translations
*
* Each participant can set the language in which they want to receive the
* translations.
*
* Setting the target language is possible only during a call and
* immediately enables the translations. Translations can be disabled by
* sending a null value as the language id.
*
* @param string $targetLanguageId the ID of the language to set
* @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: 'app'|'translations'|'in-call'}, array{}>
*
* 200: Target language set successfully
* 400: The external app "live_transcription" is not available or
* translations are not supported.
* 400: The participant is not in the call.
*/
#[PublicPage]
#[RequireCallEnabled]
#[RequireModeratorOrNoLobby]
#[RequireParticipant]
#[ApiRoute(verb: 'POST', url: '/api/{apiVersion}/live-transcription/{token}/target-language', requirements: [
'apiVersion' => '(v1)',
'token' => '[a-z0-9]{4,30}',
])]
public function setTargetLanguage(?string $targetLanguageId): DataResponse {
if ($this->room->getCallFlag() === Participant::FLAG_DISCONNECTED) {
return new DataResponse(['error' => 'in-call'], Http::STATUS_BAD_REQUEST);
}

if (!$this->participant->getSession() || $this->participant->getSession()->getInCall() === Participant::FLAG_DISCONNECTED) {
return new DataResponse(['error' => 'in-call'], Http::STATUS_BAD_REQUEST);
}

try {
$this->liveTranscriptionService->setTargetLanguage($this->room, $this->participant, $targetLanguageId);
} catch (LiveTranscriptionAppNotEnabledException $e) {
return new DataResponse(['error' => 'app'], Http::STATUS_BAD_REQUEST);
} catch (LiveTranslationNotSupportedException $e) {
return new DataResponse(['error' => 'translations'], Http::STATUS_BAD_REQUEST);
}

return new DataResponse(null);
}
}
12 changes: 12 additions & 0 deletions lib/Exceptions/LiveTranslationNotSupportedException.php
Original file line number Diff line number Diff line change
@@ -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 LiveTranslationNotSupportedException extends \Exception {
}
2 changes: 2 additions & 0 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,8 @@
* blur-virtual-background: bool,
* end-to-end-encryption: bool,
* live-transcription: bool,
* live-translation: bool,
* live-transcription-target-language-id: string,
* },
* chat: array{
* max-length: int,
Expand Down
Loading
Loading