Skip to content

Commit

Permalink
MachineTranslation (#210)
Browse files Browse the repository at this point in the history
Signed-off-by: Andrey Borysenko <andrey18106x@gmail.com>
Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
Co-authored-by: Alexander Piskun <bigcat88@icloud.com>
  • Loading branch information
3 people authored Jan 14, 2024
1 parent 567eeff commit 319a543
Show file tree
Hide file tree
Showing 14 changed files with 839 additions and 4 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [1.4.7 - 2024-01-xx]
## [1.5.0 - 2024-01-xx]

### Added

- Added filesplugin batch actions implementation. #203
- Added MachineTranslation providers API. #210

### Changed

- Changed TextProcessing providers API flow to asynchronous. #208
- Changed SpeechToText providers API flow to asynchronous. #209

## [1.4.6 - 2024-01-05]

Expand Down
6 changes: 6 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,11 @@
['name' => 'textProcessing#unregisterProvider', 'url' => '/api/v1/ai_provider/text_processing', 'verb' => 'DELETE'],
['name' => 'textProcessing#getProvider', 'url' => '/api/v1/ai_provider/text_processing', 'verb' => 'GET'],
['name' => 'textProcessing#reportResult', 'url' => '/api/v1/ai_provider/text_processing', 'verb' => 'PUT'],

// Machine-Translation
['name' => 'Translation#registerProvider', 'url' => '/api/v1/ai_provider/translation', 'verb' => 'POST'],
['name' => 'Translation#unregisterProvider', 'url' => '/api/v1/ai_provider/translation', 'verb' => 'DELETE'],
['name' => 'Translation#getProvider', 'url' => '/api/v1/ai_provider/translation', 'verb' => 'GET'],
['name' => 'Translation#reportResult', 'url' => '/api/v1/ai_provider/translation', 'verb' => 'PUT'],
],
];
1 change: 1 addition & 0 deletions docs/tech_details/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ AppAPI Nextcloud APIs
talkbots
speechtotext
textprocessing
machinetranslation
other_ocs
79 changes: 79 additions & 0 deletions docs/tech_details/api/machinetranslation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
===================
Machine Translation
===================

AppAPI provides a Machine-Translation providers registration mechanism for ExApps.

.. note::

Available since Nextcloud 29.

Registering translation provider (OCS)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

OCS endpoint: ``POST /apps/app_api/api/v1/ai_provider/translation``

Request data
************

.. code-block:: json
{
"name": "unique_provider_name",
"display_name": "Provider Display Name",
"action_handler": "/handler_route_on_ex_app",
"from_languages": {
"en": "English",
"fr": "French",
},
"to_languages": {
"en": "English",
"fr": "French",
},
}
.. note::

``from_languages`` and ``to_languages`` are JSON object with language code as key and language name as value.


Response
********

On successful registration response with status code 200 is returned.

Unregistering translation provider (OCS)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

OCS endpoint: ``DELETE /apps/app_api/api/v1/ai_provider/translation``

Request data
************

.. code-block:: json
{
"name": "unique_provider_name",
}
Response
********

On successful unregister response with status code 200 is returned.


Report translation result (OCS)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

OCS endpoint: ``PUT /apps/app_api/api/v1/ai_provider/translation``

Request data
************

.. code-block:: json
{
"task_id": "queued_task_id",
"result": "translated_text",
"error": "error_message_if_any",
}
5 changes: 5 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use OCA\AppAPI\PublicCapabilities;
use OCA\AppAPI\Service\ProvidersAI\SpeechToTextService;
use OCA\AppAPI\Service\ProvidersAI\TextProcessingService;
use OCA\AppAPI\Service\ProvidersAI\TranslationService;
use OCA\AppAPI\Service\UI\TopMenuService;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
Expand Down Expand Up @@ -71,6 +72,10 @@ public function register(IRegistrationContext $context): void {
/** @var TextProcessingService $textProcessingService */
$textProcessingService = $container->get(TextProcessingService::class);
$textProcessingService->registerExAppTextProcessingProviders($context, $container->getServer());

/** @var TranslationService $translationService */
$translationService = $container->get(TranslationService::class);
$translationService->registerExAppTranslationProviders($context, $container->getServer());
} catch (NotFoundExceptionInterface|ContainerExceptionInterface) {
}
}
Expand Down
5 changes: 4 additions & 1 deletion lib/BackgroundJob/ProvidersAICleanUpJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use OCA\AppAPI\Db\SpeechToText\SpeechToTextProviderQueueMapper;
use OCA\AppAPI\Db\TextProcessing\TextProcessingProviderQueueMapper;
use OCA\AppAPI\Db\Translation\TranslationQueueMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\IJob;
use OCP\BackgroundJob\TimedJob;
Expand All @@ -16,9 +17,10 @@ class ProvidersAICleanUpJob extends TimedJob {
private const overdueTime = 7 * 24 * 60 * 60;

public function __construct(
ITimeFactory $time,
ITimeFactory $time,
private readonly TextProcessingProviderQueueMapper $mapperTextProcessing,
private readonly SpeechToTextProviderQueueMapper $mapperSpeechToText,
private readonly TranslationQueueMapper $mapperTranslation,
) {
parent::__construct($time);

Expand All @@ -32,6 +34,7 @@ protected function run($argument): void {
try {
$this->mapperTextProcessing->removeAllOlderThenThat(self::overdueTime);
$this->mapperSpeechToText->removeAllOlderThenThat(self::overdueTime);
$this->mapperTranslation->removeAllOlderThenThat(self::overdueTime);
} catch (Exception) {
}
}
Expand Down
126 changes: 126 additions & 0 deletions lib/Controller/TranslationController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Controller;

use OCA\AppAPI\AppInfo\Application;
use OCA\AppAPI\Attribute\AppAPIAuth;
use OCA\AppAPI\Db\Translation\TranslationQueueMapper;
use OCA\AppAPI\Service\ProvidersAI\TranslationService;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\OCSController;
use OCP\DB\Exception;
use OCP\IConfig;
use OCP\IRequest;

class TranslationController extends OCSController {
protected $request;

public function __construct(
IRequest $request,
private readonly IConfig $config,
private readonly TranslationService $translationService,
private readonly TranslationQueueMapper $mapper,
) {
parent::__construct(Application::APP_ID, $request);

$this->request = $request;
}

#[NoCSRFRequired]
#[PublicPage]
#[AppAPIAuth]
public function registerProvider(
string $name,
string $displayName,
array $fromLanguages,
array $toLanguages,
string $actionHandler,
): DataResponse {
$ncVersion = $this->config->getSystemValueString('version', '0.0.0');
if (version_compare($ncVersion, '29.0', '<')) {
return new DataResponse([], Http::STATUS_NOT_IMPLEMENTED);
}

$provider = $this->translationService->registerTranslationProvider(
$this->request->getHeader('EX-APP-ID'), $name, $displayName,
$fromLanguages, $toLanguages, $actionHandler
);

if ($provider === null) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}

return new DataResponse();
}

#[NoCSRFRequired]
#[PublicPage]
#[AppAPIAuth]
public function unregisterProvider(string $name): Response {
$ncVersion = $this->config->getSystemValueString('version', '0.0.0');
if (version_compare($ncVersion, '29.0', '<')) {
return new DataResponse([], Http::STATUS_NOT_IMPLEMENTED);
}

$unregistered = $this->translationService->unregisterTranslationProvider(
$this->request->getHeader('EX-APP-ID'), $name
);

if ($unregistered === null) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}

return new DataResponse();
}

#[NoCSRFRequired]
#[PublicPage]
#[AppAPIAuth]
public function getProvider(string $name): DataResponse {
$ncVersion = $this->config->getSystemValueString('version', '0.0.0');
if (version_compare($ncVersion, '29.0', '<')) {
return new DataResponse([], Http::STATUS_NOT_IMPLEMENTED);
}
$result = $this->translationService->getExAppTranslationProvider(
$this->request->getHeader('EX-APP-ID'), $name
);
if (!$result) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return new DataResponse($result, Http::STATUS_OK);
}

#[NoCSRFRequired]
#[PublicPage]
#[AppAPIAuth]
public function reportResult(int $taskId, string $result, string $error = ""): DataResponse {
$ncVersion = $this->config->getSystemValueString('version', '0.0.0');
if (version_compare($ncVersion, '29.0', '<')) {
return new DataResponse([], Http::STATUS_NOT_IMPLEMENTED);
}
try {
$taskResult = $this->mapper->getById($taskId);
} catch (DoesNotExistException) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
} catch (MultipleObjectsReturnedException | Exception) {
return new DataResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}
$taskResult->setResult($result);
$taskResult->setError($error);
$taskResult->setFinished(1);
try {
$this->mapper->update($taskResult);
} catch (Exception) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
return new DataResponse([], Http::STATUS_OK);
}
}
77 changes: 77 additions & 0 deletions lib/Db/Translation/TranslationProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Db\Translation;

use OCP\AppFramework\Db\Entity;

/**
* Class TranslationProvider
*
* @package OCA\AppAPI\Db\Translation
*
* @method string getAppid()
* @method string getName()
* @method string getDisplayName()
* @method array getFromLanguages()
* @method array getToLanguages()
* @method string getActionHandler()
* @method void setAppid(string $appid)
* @method void setName(string $name)
* @method void setDisplayName(string $displayName)
* @method void setFromLanguages(array $fromLanguages)
* @method void setToLanguages(array $toLanguages)
* @method void setActionHandler(string $actionHandler)
*/
class TranslationProvider extends Entity implements \JsonSerializable {
protected $appid;
protected $name;
protected $displayName;
protected $fromLanguages;
protected $toLanguages;
protected $actionHandler;

public function __construct(array $params = []) {
$this->addType('appid', 'string');
$this->addType('name', 'string');
$this->addType('displayName', 'string');
$this->addType('fromLanguages', 'json');
$this->addType('toLanguages', 'json');
$this->addType('actionHandler', 'string');

if (isset($params['id'])) {
$this->setId($params['id']);
}
if (isset($params['appid'])) {
$this->setAppid($params['appid']);
}
if (isset($params['name'])) {
$this->setName($params['name']);
}
if (isset($params['display_name'])) {
$this->setDisplayName($params['display_name']);
}
if (isset($params['from_languages'])) {
$this->setFromLanguages($params['from_languages']);
}
if (isset($params['to_languages'])) {
$this->setToLanguages($params['to_languages']);
}
if (isset($params['action_handler'])) {
$this->setActionHandler($params['action_handler']);
}
}

public function jsonSerialize(): array {
return [
'id' => $this->getId(),
'appid' => $this->getAppid(),
'name' => $this->getName(),
'display_name' => $this->getDisplayName(),
'from_languages' => $this->getFromLanguages(),
'to_languages' => $this->getToLanguages(),
'action_handler' => $this->getActionHandler(),
];
}
}
Loading

0 comments on commit 319a543

Please sign in to comment.