Skip to content

Commit

Permalink
Speech-To-Text API implementation with custom ProviderID using anonym…
Browse files Browse the repository at this point in the history
…ous classes (#184)

Speech-To-Text API implementation using dynamic registration with
anonymous classes and custom provider ID
(https://github.com/nextcloud/server/blob/master/lib/public/SpeechToText/ISpeechToTextProviderWithId.php)

---------

Signed-off-by: Andrey Borysenko <[email protected]>
Co-authored-by: Alexander Piskun <[email protected]>
  • Loading branch information
andrey18106 and bigcat88 authored Dec 31, 2023
1 parent f77a908 commit 08ecfeb
Show file tree
Hide file tree
Showing 12 changed files with 549 additions and 4 deletions.
5 changes: 5 additions & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,10 @@
['name' => 'OCSUi#setExAppStyle', 'url' => '/api/v1/ui/style', 'verb' => 'POST'],
['name' => 'OCSUi#deleteExAppStyle', 'url' => '/api/v1/ui/style', 'verb' => 'DELETE'],
['name' => 'OCSUi#getExAppStyle', 'url' => '/api/v1/ui/style', 'verb' => 'GET'],

// Speech-To-Text
['name' => 'speechToText#registerProvider', 'url' => '/api/v1/ai_provider/speech_to_text', 'verb' => 'POST'],
['name' => 'speechToText#unregisterProvider', 'url' => '/api/v1/ai_provider/speech_to_text', 'verb' => 'DELETE'],
['name' => 'speechToText#getProvider', 'url' => '/api/v1/ai_provider/speech_to_text', 'verb' => 'GET'],
],
];
1 change: 1 addition & 0 deletions docs/tech_details/ApiScopes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ The following API groups are currently supported:
* ``33`` WEATHER_STATUS
* ``50`` TALK
* ``60`` TALK_BOT
* ``61`` AI_PROVIDERS
* ``110`` ACTIVITIES
* ``120`` NOTES

Expand Down
1 change: 1 addition & 0 deletions docs/tech_details/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ AppAPI Nextcloud APIs
topmenu
notifications
talkbots
speechtotext
other_ocs
47 changes: 47 additions & 0 deletions docs/tech_details/api/speechtotext.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
==============
Speech-To-Text
==============

AppAPI provides a Speech-To-Text (STT) provider registration API for the ExApps.

Registering ExApp STT provider (OCS)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

OCS endpoint: ``POST /apps/app_api/api/v1/provider/speech_to_text``

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

.. code-block:: json
{
"name": "unique_provider_name",
"display_name": "Provider Display Name",
"action_handler": "/handler_route_on_ex_app",
}
Response
********

On successful registration response with status code 200 is returned.

Unregistering ExApp STT provider (OCS)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

OCS endpoint: ``DELETE /apps/app_api/api/v1/provider/speech_to_text``

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

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

On successful unregister response with status code 200 is returned.
10 changes: 10 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use OCA\AppAPI\Notifications\ExAppNotifier;
use OCA\AppAPI\Profiler\AppAPIDataCollector;
use OCA\AppAPI\PublicCapabilities;
use OCA\AppAPI\Service\SpeechToTextService;
use OCA\AppAPI\Service\UI\TopMenuService;
use OCA\DAV\Events\SabrePluginAuthInitEvent;
use OCA\Files\Event\LoadAdditionalScriptsEvent;
Expand Down Expand Up @@ -58,6 +59,15 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerNotifierService(ExAppNotifier::class);
$context->registerNotifierService(ExAppAdminNotifier::class);

// Dynamic anonymous providers registration
$container = $this->getContainer();
try {
/** @var SpeechToTextService $speechToTextService */
$speechToTextService = $container->get(SpeechToTextService::class);
$speechToTextService->registerExAppSpeechToTextProviders($context, $container->getServer());
} catch (NotFoundExceptionInterface|ContainerExceptionInterface) {
}
}

public function boot(IBootContext $context): void {
Expand Down
78 changes: 78 additions & 0 deletions lib/Controller/SpeechToTextController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Controller;

use OCA\AppAPI\AppInfo\Application;
use OCA\AppAPI\Attribute\AppAPIAuth;
use OCA\AppAPI\Service\SpeechToTextService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IConfig;
use OCP\IRequest;

class SpeechToTextController extends OCSController {
protected $request;

public function __construct(
IRequest $request,
private readonly SpeechToTextService $speechToTextService,
private readonly IConfig $config,
) {
parent::__construct(Application::APP_ID, $request);

$this->request = $request;
}

#[NoCSRFRequired]
#[PublicPage]
#[AppAPIAuth]
public function registerProvider(string $name, string $displayName, 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->speechToTextService->registerSpeechToTextProvider(
$this->request->getHeader('EX-APP-ID'), $name, $displayName, $actionHandler);
if ($provider === null) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
return new DataResponse();
}

#[NoCSRFRequired]
#[PublicPage]
#[AppAPIAuth]
public function unregisterProvider(string $name): DataResponse {
$ncVersion = $this->config->getSystemValueString('version', '0.0.0');
if (version_compare($ncVersion, '29.0', '<')) {
return new DataResponse([], Http::STATUS_NOT_IMPLEMENTED);
}
$unregistered = $this->speechToTextService->unregisterSpeechToTextProvider(
$this->request->getHeader('EX-APP-ID'), $name);
if ($unregistered === null) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return new DataResponse();
}

#[AppAPIAuth]
#[PublicPage]
#[NoCSRFRequired]
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->speechToTextService->getExAppSpeechToTextProvider(
$this->request->getHeader('EX-APP-ID'), $name);
if (!$result) {
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
return new DataResponse($result, Http::STATUS_OK);
}
}
61 changes: 61 additions & 0 deletions lib/Db/SpeechToText/SpeechToTextProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Db\SpeechToText;

use OCP\AppFramework\Db\Entity;

/**
* Class ExAppSpeechToTextProvider
*
* @package OCA\AppAPI\Db\SpeechToText
*
* @method string getAppid()
* @method string getName()
* @method string getDisplayName()
* @method string getActionHandler()
* @method void setAppid(string $appid)
* @method void setName(string $name)
* @method void setDisplayName(string $displayName)
* @method void setActionHandler(string $actionHandler)
*/
class SpeechToTextProvider extends Entity implements \JsonSerializable {
protected $appid;
protected $name;
protected $displayName;
protected $actionHandler;

public function __construct(array $params = []) {
$this->addType('appid', 'string');
$this->addType('name', 'string');
$this->addType('displayName', 'string');
$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['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(),
'action_handler' => $this->getActionHandler(),
];
}
}
72 changes: 72 additions & 0 deletions lib/Db/SpeechToText/SpeechToTextProviderMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Db\SpeechToText;

use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;

/**
* @template-extends QBMapper<SpeechToTextProvider>
*/
class SpeechToTextProviderMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'ex_speech_to_text');
}

/**
* @throws Exception
*/
public function findAllEnabled(): array {
$qb = $this->db->getQueryBuilder();
$result = $qb->select(
'ex_speech_to_text.appid',
'ex_speech_to_text.name',
'ex_speech_to_text.display_name',
'ex_speech_to_text.action_handler',
)
->from($this->tableName, 'ex_speech_to_text')
->innerJoin('ex_speech_to_text', 'ex_apps', 'exa', 'exa.appid = ex_speech_to_text.appid')
->where(
$qb->expr()->eq('exa.enabled', $qb->createNamedParameter(1, IQueryBuilder::PARAM_INT))
)
->executeQuery();
return $result->fetchAll();
}

/**
* @param string $appId
* @param string $name
*
* @return SpeechToTextProvider
* @throws Exception
* @throws MultipleObjectsReturnedException
*
* @throws DoesNotExistException
*/
public function findByAppidName(string $appId, string $name): SpeechToTextProvider {
$qb = $this->db->getQueryBuilder();
return $this->findEntity($qb->select('*')
->from($this->tableName)
->where($qb->expr()->eq('appid', $qb->createNamedParameter($appId), IQueryBuilder::PARAM_STR))
->andWhere($qb->expr()->eq('name', $qb->createNamedParameter($name), IQueryBuilder::PARAM_STR))
);
}

/**
* @throws Exception
*/
public function removeAllByAppId(string $appId): int {
$qb = $this->db->getQueryBuilder();
$qb->delete($this->tableName)
->where(
$qb->expr()->eq('appid', $qb->createNamedParameter($appId, IQueryBuilder::PARAM_STR))
);
return $qb->executeStatement();
}
}
55 changes: 55 additions & 0 deletions lib/Migration/Version1005Date202312271744.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

declare(strict_types=1);

namespace OCA\AppAPI\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version1005Date202312271744 extends SimpleMigrationStep {
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();

if (!$schema->hasTable('ex_speech_to_text')) {
$table = $schema->createTable('ex_speech_to_text');

$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
]);
$table->addColumn('appid', Types::STRING, [
'notnull' => true,
'length' => 32,
]);
$table->addColumn('name', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('display_name', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('action_handler', Types::STRING, [
'notnull' => true,
'length' => 410,
]);

$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['appid', 'name'], 'speech_to_text__idx');
}

return $schema;
}
}
Loading

0 comments on commit 08ecfeb

Please sign in to comment.