diff --git a/README.md b/README.md
index 2a35658b..08e10f4f 100644
--- a/README.md
+++ b/README.md
@@ -2,14 +2,15 @@
This app brings a user interface to use the Nextcloud text processing feature.
-It allows users to launch text processing tasks, be notified when they finish and see the results.
+It allows users to launch AI tasks, be notified when they finish and see the results.
The assistant also appears in others apps like Text to easily process parts of a document.
### How to use it
-A new right header menu entry appears. Once clicked, the assistant is displayed and you can select and task type and
-set the input text you want to process.
+A new right header menu entry appears. Once clicked, the assistant is displayed and you can select and task type and
+set the input you want to process.
+The task might run immediately or be scheduled depending on the time estimation given by the AI provider.
Once a task is scheduled, it will run as a background job. When it is finished, you will receive a notification
from which the results can be displayed.
@@ -17,10 +18,13 @@ Other apps can integrate with the assistant. For example, Text will display an i
to directly select a task type to process this paragraph. Selecting a task this way will open the assistant with the task
being pre-selected and the input text set.
-### Text processing providers
+## Features
In the assistant, the list of available tasks depends on the available providers installed via other apps.
-This means you have complete freedom over which service/software will actually run your text processing tasks.
+This means you have complete freedom over which service/software will actually run your AI tasks.
+
+### Text processing
+
So far, the [Large language model](https://github.com/nextcloud/llm#readme)
and the [OpenAi/LocalAI integration](https://apps.nextcloud.com/apps/integration_openai) apps
include text processing providers to:
@@ -29,3 +33,16 @@ include text processing providers to:
* Generate a headline
* Get an answer from a free prompt
* Reformulate (OpenAi/LocalAi only)
+* Context writer: Generate text with a specified style. The style can be described or provided via an example text.
+
+### Text to image (Image generation)
+
+Known providers:
+* [OpenAi/LocalAI integration](https://apps.nextcloud.com/apps/integration_openai)
+* [Text2Image Stable Diffusion](https://apps.nextcloud.com/apps/text2image_stablediffusion)
+
+### Speech to text (Audio transcription)
+
+Known providers:
+* [OpenAi/LocalAI integration](https://apps.nextcloud.com/apps/integration_openai)
+* [Local Whisper Speech-To-Text](https://apps.nextcloud.com/apps/stt_whisper)
diff --git a/appinfo/info.xml b/appinfo/info.xml
index 8d70b9ae..6af10a60 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -6,14 +6,15 @@
1.0.5
agpl
diff --git a/appinfo/routes.php b/appinfo/routes.php
index e11714fe..324554f4 100644
--- a/appinfo/routes.php
+++ b/appinfo/routes.php
@@ -1,5 +1,9 @@
'(v1)',
+];
+
return [
'routes' => [
['name' => 'config#getConfigValue', 'url' => '/config', 'verb' => 'GET'],
@@ -7,23 +11,8 @@
['name' => 'config#setAdminConfig', 'url' => '/admin-config', 'verb' => 'PUT'],
['name' => 'assistant#getAssistantTaskResultPage', 'url' => '/task/view/{metaTaskId}', 'verb' => 'GET'],
- ['name' => 'assistant#getAssistantTask', 'url' => '/task/{metaTaskId}', 'verb' => 'GET'],
- ['name' => 'assistant#getUserTasks', 'url' => '/tasks', 'verb' => 'GET'],
- ['name' => 'assistant#runTextProcessingTask', 'url' => '/task/run', 'verb' => 'POST'],
- ['name' => 'assistant#scheduleTextProcessingTask', 'url' => '/task/schedule', 'verb' => 'POST'],
- ['name' => 'assistant#runOrScheduleTextProcessingTask', 'url' => '/task/run-or-schedule', 'verb' => 'POST'],
- ['name' => 'assistant#parseTextFromFile', 'url' => '/parse-file', 'verb' => 'POST'],
- ['name' => 'assistant#deleteTask', 'url' => '/task/{metaTaskId}', 'verb' => 'DELETE'],
- ['name' => 'assistant#cancelTask', 'url' => '/task/cancel/{metaTaskId}', 'verb' => 'PUT'],
-
- ['name' => 'Text2Image#processPrompt', 'url' => '/i/process_prompt', 'verb' => 'POST'],
- ['name' => 'Text2Image#getPromptHistory', 'url' => '/i/prompt_history', 'verb' => 'GET'],
+
['name' => 'Text2Image#showGenerationPage', 'url' => '/i/{imageGenId}', 'verb' => 'GET'],
- ['name' => 'Text2Image#getGenerationInfo', 'url' => '/i/info/{imageGenId}', 'verb' => 'GET'],
- ['name' => 'Text2Image#getImage', 'url' => '/i/{imageGenId}/{fileNameId}', 'verb' => 'GET'],
- ['name' => 'Text2Image#cancelGeneration', 'url' => '/i/cancel_generation', 'verb' => 'POST'],
- ['name' => 'Text2Image#setVisibilityOfImageFiles', 'url' => '/i/visibility/{imageGenId}', 'verb' => 'POST'],
- ['name' => 'Text2Image#notifyWhenReady', 'url' => '/i/notify/{imageGenId}', 'verb' => 'POST'],
['name' => 'FreePrompt#processPrompt', 'url' => '/f/process_prompt', 'verb' => 'POST'],
['name' => 'FreePrompt#getPromptHistory', 'url' => '/f/prompt_history', 'verb' => 'GET'],
@@ -31,7 +20,27 @@
['name' => 'FreePrompt#cancelGeneration', 'url' => '/f/cancel_generation', 'verb' => 'POST'],
['name' => 'SpeechToText#getResultPage', 'url' => '/stt/result-page/{metaTaskId}', 'verb' => 'GET'],
- ['name' => 'SpeechToText#transcribeAudio', 'url' => '/stt/transcribeAudio', 'verb' => 'POST'],
- ['name' => 'SpeechToText#transcribeFile', 'url' => '/stt/transcribeFile', 'verb' => 'POST'],
+ ],
+ 'ocs' => [
+ ['name' => 'assistantApi#getAvailableTaskTypes', 'url' => '/api/{apiVersion}/task-types', 'verb' => 'GET', 'requirements' => $requirements],
+ ['name' => 'assistantApi#getAssistantTask', 'url' => '/api/{apiVersion}/task/{metaTaskId}', 'verb' => 'GET', 'requirements' => $requirements],
+ ['name' => 'assistantApi#getUserTasks', 'url' => '/api/{apiVersion}/tasks', 'verb' => 'GET', 'requirements' => $requirements],
+ ['name' => 'assistantApi#runTextProcessingTask', 'url' => '/api/{apiVersion}/task/run', 'verb' => 'POST', 'requirements' => $requirements],
+ ['name' => 'assistantApi#scheduleTextProcessingTask', 'url' => '/api/{apiVersion}/task/schedule', 'verb' => 'POST', 'requirements' => $requirements],
+ ['name' => 'assistantApi#runOrScheduleTextProcessingTask', 'url' => '/api/{apiVersion}/task/run-or-schedule', 'verb' => 'POST', 'requirements' => $requirements],
+ ['name' => 'assistantApi#parseTextFromFile', 'url' => '/api/{apiVersion}/parse-file', 'verb' => 'POST', 'requirements' => $requirements],
+ ['name' => 'assistantApi#deleteTask', 'url' => '/api/{apiVersion}/task/{metaTaskId}', 'verb' => 'DELETE', 'requirements' => $requirements],
+ ['name' => 'assistantApi#cancelTask', 'url' => '/api/{apiVersion}/task/cancel/{metaTaskId}', 'verb' => 'PUT', 'requirements' => $requirements],
+
+ ['name' => 'Text2ImageApi#processPrompt', 'url' => '/api/{apiVersion}/i/process_prompt', 'verb' => 'POST', 'requirements' => $requirements],
+ ['name' => 'Text2ImageApi#getPromptHistory', 'url' => '/api/{apiVersion}/i/prompt_history', 'verb' => 'GET', 'requirements' => $requirements],
+ ['name' => 'Text2ImageApi#getGenerationInfo', 'url' => '/api/{apiVersion}/i/info/{imageGenId}', 'verb' => 'GET', 'requirements' => $requirements],
+ ['name' => 'Text2ImageApi#getImage', 'url' => '/api/{apiVersion}/i/{imageGenId}/{fileNameId}', 'verb' => 'GET', 'requirements' => $requirements],
+ ['name' => 'Text2ImageApi#cancelGeneration', 'url' => '/api/{apiVersion}/i/cancel_generation', 'verb' => 'POST', 'requirements' => $requirements],
+ ['name' => 'Text2ImageApi#setVisibilityOfImageFiles', 'url' => '/api/{apiVersion}/i/visibility/{imageGenId}', 'verb' => 'POST', 'requirements' => $requirements],
+ ['name' => 'Text2ImageApi#notifyWhenReady', 'url' => '/api/{apiVersion}/i/notify/{imageGenId}', 'verb' => 'POST', 'requirements' => $requirements],
+
+ ['name' => 'SpeechToTextApi#transcribeAudio', 'url' => '/api/{apiVersion}/stt/transcribeAudio', 'verb' => 'POST', 'requirements' => $requirements],
+ ['name' => 'SpeechToTextApi#transcribeFile', 'url' => '/api/{apiVersion}/stt/transcribeFile', 'verb' => 'POST', 'requirements' => $requirements],
],
];
diff --git a/lib/Controller/AssistantApiController.php b/lib/Controller/AssistantApiController.php
new file mode 100644
index 00000000..525d60e0
--- /dev/null
+++ b/lib/Controller/AssistantApiController.php
@@ -0,0 +1,195 @@
+assistantService->getAvailableTaskTypes();
+ return new DataResponse(['types' => $taskTypes]);
+ }
+
+ /**
+ * @param int $metaTaskId
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function deleteTask(int $metaTaskId): DataResponse {
+ if ($this->userId !== null) {
+ try {
+ $this->assistantService->deleteAssistantTask($this->userId, $metaTaskId);
+ return new DataResponse('');
+ } catch (\Exception $e) {
+ }
+ }
+
+ return new DataResponse('', Http::STATUS_NOT_FOUND);
+ }
+
+ /**
+ * @param int $metaTaskId
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function cancelTask(int $metaTaskId): DataResponse {
+ if ($this->userId !== null) {
+ try {
+ $this->assistantService->cancelAssistantTask($this->userId, $metaTaskId);
+ return new DataResponse('');
+ } catch (\Exception $e) {
+ }
+ }
+
+ return new DataResponse('', Http::STATUS_NOT_FOUND);
+ }
+
+ /**
+ * @param int $metaTaskId
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function getAssistantTask(int $metaTaskId): DataResponse {
+ if ($this->userId !== null) {
+ $task = $this->assistantService->getAssistantTask($this->userId, $metaTaskId);
+ if ($task !== null) {
+ return new DataResponse([
+ 'task' => $task->jsonSerializeCc(),
+ ]);
+ }
+ }
+ return new DataResponse('', Http::STATUS_NOT_FOUND);
+ }
+
+ #[NoAdminRequired]
+ public function getUserTasks(?string $taskType = null, ?int $category = null): DataResponse {
+ if ($this->userId !== null) {
+ try {
+ $tasks = $this->metaTaskMapper->getUserMetaTasks($this->userId, $taskType, $category);
+ $serializedTasks = array_map(static function (MetaTask $task) {
+ return $task->jsonSerializeCc();
+ }, $tasks);
+ return new DataResponse(['tasks' => $serializedTasks]);
+ } catch (Exception $e) {
+ return new DataResponse(['tasks' => []]);
+ }
+ }
+ return new DataResponse('', Http::STATUS_NOT_FOUND);
+ }
+
+ /**
+ * @param array $inputs
+ * @param string $type
+ * @param string $appId
+ * @param string $identifier
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function runTextProcessingTask(string $type, array $inputs, string $appId, string $identifier): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
+ }
+
+ try {
+ $task = $this->assistantService->runTextProcessingTask($type, $inputs, $appId, $this->userId, $identifier);
+ } catch (\Exception | \Throwable $e) {
+ return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
+ }
+ return new DataResponse([
+ 'task' => $task->jsonSerializeCc(),
+ ]);
+ }
+
+ /**
+ * @param array $inputs
+ * @param string $type
+ * @param string $appId
+ * @param string $identifier
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function scheduleTextProcessingTask(string $type, array $inputs, string $appId, string $identifier): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
+ }
+
+ try {
+ $task = $this->assistantService->scheduleTextProcessingTask($type, $inputs, $appId, $this->userId, $identifier);
+ } catch (\Exception | \Throwable $e) {
+ return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
+ }
+ return new DataResponse([
+ 'task' => $task->jsonSerializeCc(),
+ ]);
+ }
+
+ /**
+ * @param array $inputs
+ * @param string $type
+ * @param string $appId
+ * @param string $identifier
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function runOrScheduleTextProcessingTask(string $type, array $inputs, string $appId, string $identifier): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
+ }
+
+ try {
+ $task = $this->assistantService->runOrScheduleTextProcessingTask($type, $inputs, $appId, $this->userId, $identifier);
+ } catch (\Exception | \Throwable $e) {
+ return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
+ }
+ return new DataResponse([
+ 'task' => $task->jsonSerializeCc(),
+ ]);
+ }
+
+ /**
+ * Parse text from file (if parsing the file type is supported)
+ *
+ * @param string $filePath
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function parseTextFromFile(string $filePath): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
+ }
+
+ try {
+ $text = $this->assistantService->parseTextFromFile($filePath, $this->userId);
+ } catch (\Exception | \Throwable $e) {
+ return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
+ }
+ return new DataResponse([
+ 'parsedText' => $text,
+ ]);
+ }
+}
diff --git a/lib/Controller/AssistantController.php b/lib/Controller/AssistantController.php
index 398663cb..018dea1b 100644
--- a/lib/Controller/AssistantController.php
+++ b/lib/Controller/AssistantController.php
@@ -3,17 +3,13 @@
namespace OCA\TpAssistant\Controller;
use OCA\TpAssistant\AppInfo\Application;
-use OCA\TpAssistant\Db\MetaTask;
-use OCA\TpAssistant\Db\MetaTaskMapper;
use OCA\TpAssistant\Service\AssistantService;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
-use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
-use OCP\DB\Exception;
use OCP\IRequest;
class AssistantController extends Controller {
@@ -22,47 +18,12 @@ public function __construct(
string $appName,
IRequest $request,
private AssistantService $assistantService,
- private MetaTaskMapper $metaTaskMapper,
private IInitialState $initialStateService,
private ?string $userId,
) {
parent::__construct($appName, $request);
}
- /**
- * @param int $metaTaskId
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function deleteTask(int $metaTaskId): DataResponse {
- if ($this->userId !== null) {
- try {
- $this->assistantService->deleteAssistantTask($this->userId, $metaTaskId);
- return new DataResponse('');
- } catch (\Exception $e) {
- }
- }
-
- return new DataResponse('', Http::STATUS_NOT_FOUND);
- }
-
- /**
- * @param int $metaTaskId
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function cancelTask(int $metaTaskId): DataResponse {
- if ($this->userId !== null) {
- try {
- $this->assistantService->cancelAssistantTask($this->userId, $metaTaskId);
- return new DataResponse('');
- } catch (\Exception $e) {
- }
- }
-
- return new DataResponse('', Http::STATUS_NOT_FOUND);
- }
-
/**
* @param int $metaTaskId
* @return TemplateResponse
@@ -79,128 +40,4 @@ public function getAssistantTaskResultPage(int $metaTaskId): TemplateResponse {
}
return new TemplateResponse('', '403', [], TemplateResponse::RENDER_AS_ERROR, Http::STATUS_FORBIDDEN);
}
-
- /**
- * @param int $metaTaskId
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function getAssistantTask(int $metaTaskId): DataResponse {
- if ($this->userId !== null) {
- $task = $this->assistantService->getAssistantTask($this->userId, $metaTaskId);
- if ($task !== null) {
- return new DataResponse([
- 'task' => $task->jsonSerializeCc(),
- ]);
- }
- }
- return new DataResponse('', Http::STATUS_NOT_FOUND);
- }
-
- #[NoAdminRequired]
- public function getUserTasks(?string $taskType = null, ?int $category = null): DataResponse {
- if ($this->userId !== null) {
- try {
- $tasks = $this->metaTaskMapper->getUserMetaTasks($this->userId, $taskType, $category);
- $serializedTasks = array_map(static function (MetaTask $task) {
- return $task->jsonSerializeCc();
- }, $tasks);
- return new DataResponse(['tasks' => $serializedTasks]);
- } catch (Exception $e) {
- return new DataResponse(['tasks' => []]);
- }
- }
- return new DataResponse('', Http::STATUS_NOT_FOUND);
- }
-
- /**
- * @param array $inputs
- * @param string $type
- * @param string $appId
- * @param string $identifier
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function runTextProcessingTask(string $type, array $inputs, string $appId, string $identifier): DataResponse {
- if ($this->userId === null) {
- return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
- }
-
- try {
- $task = $this->assistantService->runTextProcessingTask($type, $inputs, $appId, $this->userId, $identifier);
- } catch (\Exception | \Throwable $e) {
- return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
- }
- return new DataResponse([
- 'task' => $task->jsonSerializeCc(),
- ]);
- }
-
- /**
- * @param array $inputs
- * @param string $type
- * @param string $appId
- * @param string $identifier
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function scheduleTextProcessingTask(string $type, array $inputs, string $appId, string $identifier): DataResponse {
- if ($this->userId === null) {
- return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
- }
-
- try {
- $task = $this->assistantService->scheduleTextProcessingTask($type, $inputs, $appId, $this->userId, $identifier);
- } catch (\Exception | \Throwable $e) {
- return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
- }
- return new DataResponse([
- 'task' => $task->jsonSerializeCc(),
- ]);
- }
-
- /**
- * @param array $inputs
- * @param string $type
- * @param string $appId
- * @param string $identifier
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function runOrScheduleTextProcessingTask(string $type, array $inputs, string $appId, string $identifier): DataResponse {
- if ($this->userId === null) {
- return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
- }
-
- try {
- $task = $this->assistantService->runOrScheduleTextProcessingTask($type, $inputs, $appId, $this->userId, $identifier);
- } catch (\Exception | \Throwable $e) {
- return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
- }
- return new DataResponse([
- 'task' => $task->jsonSerializeCc(),
- ]);
- }
-
- /**
- * Parse text from file (if parsing the file type is supported)
- *
- * @param string $filePath
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function parseTextFromFile(string $filePath): DataResponse {
- if ($this->userId === null) {
- return new DataResponse('Unknow user', Http::STATUS_BAD_REQUEST);
- }
-
- try {
- $text = $this->assistantService->parseTextFromFile($filePath, $this->userId);
- } catch (\Exception | \Throwable $e) {
- return new DataResponse($e->getMessage(), Http::STATUS_BAD_REQUEST);
- }
- return new DataResponse([
- 'parsedText' => $text,
- ]);
- }
}
diff --git a/lib/Controller/SpeechToTextApiController.php b/lib/Controller/SpeechToTextApiController.php
new file mode 100644
index 00000000..ea7ca42c
--- /dev/null
+++ b/lib/Controller/SpeechToTextApiController.php
@@ -0,0 +1,169 @@
+
+ *
+ * @author Anupam Kumar
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+namespace OCA\TpAssistant\Controller;
+
+use Exception;
+use InvalidArgumentException;
+use OCA\TpAssistant\AppInfo\Application;
+use OCA\TpAssistant\Service\SpeechToText\SpeechToTextService;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\Files\NotFoundException;
+use OCP\Files\NotPermittedException;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\PreConditionNotMetException;
+use Psr\Log\LoggerInterface;
+use RuntimeException;
+
+class SpeechToTextApiController extends OCSController {
+
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private SpeechToTextService $service,
+ private LoggerInterface $logger,
+ private IL10N $l10n,
+ private ?string $userId,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * @param int $id Transcript ID
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function getTranscript(int $id): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse('', Http::STATUS_UNAUTHORIZED);
+ }
+ try {
+ return new DataResponse($this->service->internalGetTask($this->userId, $id)->getOutput());
+ } catch (Exception $e) {
+ return new DataResponse($e->getMessage(), intval($e->getCode()));
+ }
+ }
+
+
+ /**
+ * @return DataResponse
+ * @throws NotPermittedException
+ */
+ #[NoAdminRequired]
+ public function transcribeAudio(): DataResponse {
+ $audioData = $this->request->getUploadedFile('audioData');
+
+ if ($audioData['error'] !== 0) {
+ return new DataResponse('Error in audio file upload: ' . $audioData['error'], Http::STATUS_BAD_REQUEST);
+ }
+
+ if (empty($audioData)) {
+ return new DataResponse('Invalid audio data received', Http::STATUS_BAD_REQUEST);
+ }
+
+ if ($audioData['type'] !== 'audio/mp3' && $audioData['type'] !== 'audio/mpeg') {
+ return new DataResponse('Audio file must be in MP3 format', Http::STATUS_BAD_REQUEST);
+ }
+
+ try {
+ $this->service->transcribeAudio($audioData['tmp_name'], $this->userId);
+ return new DataResponse('ok');
+ } catch (RuntimeException $e) {
+ $this->logger->error(
+ 'Runtime exception: ' . $e->getMessage(),
+ ['app' => Application::APP_ID]
+ );
+ return new DataResponse(
+ $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
+ Http::STATUS_INTERNAL_SERVER_ERROR
+ );
+ } catch (PreConditionNotMetException $e) {
+ $this->logger->error('No Speech-to-Text provider found: ' . $e->getMessage(), ['app' => Application::APP_ID]);
+ return new DataResponse(
+ $this->l10n->t('No Speech-to-Text provider found, install one from the app store to use this feature.'),
+ Http::STATUS_BAD_REQUEST
+ );
+ } catch (InvalidArgumentException $e) {
+ $this->logger->error('InvalidArgumentException: ' . $e->getMessage(), ['app' => Application::APP_ID]);
+ return new DataResponse(
+ $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
+ Http::STATUS_INTERNAL_SERVER_ERROR
+ );
+ }
+ }
+
+ /**
+ * @param string $path Nextcloud file path
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function transcribeFile(string $path): DataResponse {
+ if ($path === '') {
+ return new DataResponse('Empty file path received', Http::STATUS_BAD_REQUEST);
+ }
+
+ try {
+ $this->service->transcribeFile($path, $this->userId);
+ return new DataResponse('ok');
+ } catch (NotFoundException $e) {
+ $this->logger->error('Audio file not found: ' . $e->getMessage(), ['app' => Application::APP_ID]);
+ return new DataResponse(
+ $this->l10n->t('Audio file not found.'),
+ Http::STATUS_NOT_FOUND
+ );
+ } catch (RuntimeException $e) {
+ $this->logger->error(
+ 'Runtime exception: ' . $e->getMessage(),
+ ['app' => Application::APP_ID]
+ );
+ return new DataResponse(
+ $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
+ Http::STATUS_INTERNAL_SERVER_ERROR
+ );
+ } catch (NotPermittedException $e) {
+ $this->logger->error(
+ 'No permission to create recording file/directory: ' . $e->getMessage(),
+ ['app' => Application::APP_ID]
+ );
+ return new DataResponse(
+ $this->l10n->t('No permission to create recording file/directory, contact your sysadmin to resolve this issue.'),
+ Http::STATUS_INTERNAL_SERVER_ERROR
+ );
+ } catch (PreConditionNotMetException $e) {
+ $this->logger->error('No Speech-to-Text provider found: ' . $e->getMessage(), ['app' => Application::APP_ID]);
+ return new DataResponse(
+ $this->l10n->t('No Speech-to-Text provider found, install one from the app store to use this feature.'),
+ Http::STATUS_BAD_REQUEST
+ );
+ } catch (InvalidArgumentException $e) {
+ $this->logger->error('InvalidArgumentException: ' . $e->getMessage(), ['app' => Application::APP_ID]);
+ return new DataResponse(
+ $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
+ Http::STATUS_INTERNAL_SERVER_ERROR
+ );
+ }
+ }
+}
diff --git a/lib/Controller/SpeechToTextController.php b/lib/Controller/SpeechToTextController.php
index 03365162..3fae1da4 100644
--- a/lib/Controller/SpeechToTextController.php
+++ b/lib/Controller/SpeechToTextController.php
@@ -23,39 +23,24 @@
namespace OCA\TpAssistant\Controller;
use Exception;
-use InvalidArgumentException;
use OCA\TpAssistant\AppInfo\Application;
-use OCA\TpAssistant\Db\MetaTask;
-use OCA\TpAssistant\Db\MetaTaskMapper;
use OCA\TpAssistant\Service\SpeechToText\SpeechToTextService;
use OCP\AppFramework\Controller;
-use OCP\AppFramework\Db\DoesNotExistException;
-use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
-use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
-use OCP\Files\NotFoundException;
-use OCP\Files\NotPermittedException;
-use OCP\IL10N;
use OCP\IRequest;
-use OCP\PreConditionNotMetException;
-use Psr\Log\LoggerInterface;
-use RuntimeException;
class SpeechToTextController extends Controller {
public function __construct(
- string $appName,
- IRequest $request,
+ string $appName,
+ IRequest $request,
private SpeechToTextService $service,
- private LoggerInterface $logger,
- private IL10N $l10n,
- private IInitialState $initialState,
- private ?string $userId,
- private MetaTaskMapper $metaTaskMapper,
+ private IInitialState $initialState,
+ private ?string $userId,
) {
parent::__construct($appName, $request);
}
@@ -67,10 +52,13 @@ public function __construct(
#[NoAdminRequired]
#[NoCSRFRequired]
public function getResultPage(int $metaTaskId): TemplateResponse {
+ if ($this->userId === null) {
+ return new TemplateResponse('', '403', [], TemplateResponse::RENDER_AS_ERROR, Http::STATUS_FORBIDDEN);
+ }
$response = new TemplateResponse(Application::APP_ID, 'speechToTextResultPage');
try {
$initData = [
- 'task' => $this->internalGetTask($metaTaskId),
+ 'task' => $this->service->internalGetTask($this->userId, $metaTaskId),
];
} catch (Exception $e) {
$initData = [
@@ -83,146 +71,4 @@ public function getResultPage(int $metaTaskId): TemplateResponse {
$this->initialState->provideInitialState('plain-text-result', $initData);
return $response;
}
-
- /**
- * @param int $id Transcript ID
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function getTranscript(int $id): DataResponse {
- try {
- return new DataResponse($this->internalGetTask($id)->getOutput());
- } catch (Exception $e) {
- return new DataResponse($e->getMessage(), intval($e->getCode()));
- }
- }
-
- /**
- * Internal function to get transcription assistant tasks based on the assistant meta task id
- *
- * @param integer $id
- * @return MetaTask
- * @throws Exception
- */
- private function internalGetTask(int $id): MetaTask {
- try {
- $metaTask = $this->metaTaskMapper->getUserMetaTask($id, $this->userId);
-
- if($metaTask->getCategory() !== Application::TASK_CATEGORY_SPEECH_TO_TEXT) {
- throw new Exception('Task is not a speech to text task.', Http::STATUS_BAD_REQUEST);
- }
-
- return $metaTask;
- } catch (MultipleObjectsReturnedException $e) {
- $this->logger->error('Multiple tasks found for one id: ' . $e->getMessage(), ['app' => Application::APP_ID]);
- throw new Exception($this->l10n->t('Multiple tasks found'), Http::STATUS_BAD_REQUEST);
- } catch (DoesNotExistException $e) {
- throw new Exception($this->l10n->t('Transcript not found'), Http::STATUS_NOT_FOUND);
- } catch (Exception $e) {
- $this->logger->error('Error: ' . $e->getMessage(), ['app' => Application::APP_ID]);
- throw new Exception(
- $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
- Http::STATUS_INTERNAL_SERVER_ERROR,
- );
- }
- }
-
- /**
- * @return DataResponse
- * @throws NotPermittedException
- */
- #[NoAdminRequired]
- public function transcribeAudio(): DataResponse {
- $audioData = $this->request->getUploadedFile('audioData');
-
- if ($audioData['error'] !== 0) {
- return new DataResponse('Error in audio file upload: ' . $audioData['error'], Http::STATUS_BAD_REQUEST);
- }
-
- if (empty($audioData)) {
- return new DataResponse('Invalid audio data received', Http::STATUS_BAD_REQUEST);
- }
-
- if ($audioData['type'] !== 'audio/mp3' && $audioData['type'] !== 'audio/mpeg') {
- return new DataResponse('Audio file must be in MP3 format', Http::STATUS_BAD_REQUEST);
- }
-
- try {
- $this->service->transcribeAudio($audioData['tmp_name'], $this->userId);
- return new DataResponse('ok');
- } catch (RuntimeException $e) {
- $this->logger->error(
- 'Runtime exception: ' . $e->getMessage(),
- ['app' => Application::APP_ID]
- );
- return new DataResponse(
- $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
- Http::STATUS_INTERNAL_SERVER_ERROR
- );
- } catch (PreConditionNotMetException $e) {
- $this->logger->error('No Speech-to-Text provider found: ' . $e->getMessage(), ['app' => Application::APP_ID]);
- return new DataResponse(
- $this->l10n->t('No Speech-to-Text provider found, install one from the app store to use this feature.'),
- Http::STATUS_BAD_REQUEST
- );
- } catch (InvalidArgumentException $e) {
- $this->logger->error('InvalidArgumentException: ' . $e->getMessage(), ['app' => Application::APP_ID]);
- return new DataResponse(
- $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
- Http::STATUS_INTERNAL_SERVER_ERROR
- );
- }
- }
-
- /**
- * @param string $path Nextcloud file path
- * @return DataResponse
- */
- #[NoAdminRequired]
- public function transcribeFile(string $path): DataResponse {
- if ($path === '') {
- return new DataResponse('Empty file path received', Http::STATUS_BAD_REQUEST);
- }
-
- try {
- $this->service->transcribeFile($path, $this->userId);
- return new DataResponse('ok');
- } catch (NotFoundException $e) {
- $this->logger->error('Audio file not found: ' . $e->getMessage(), ['app' => Application::APP_ID]);
- return new DataResponse(
- $this->l10n->t('Audio file not found.'),
- Http::STATUS_NOT_FOUND
- );
- } catch (RuntimeException $e) {
- $this->logger->error(
- 'Runtime exception: ' . $e->getMessage(),
- ['app' => Application::APP_ID]
- );
- return new DataResponse(
- $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
- Http::STATUS_INTERNAL_SERVER_ERROR
- );
- } catch (NotPermittedException $e) {
- $this->logger->error(
- 'No permission to create recording file/directory: ' . $e->getMessage(),
- ['app' => Application::APP_ID]
- );
- return new DataResponse(
- $this->l10n->t('No permission to create recording file/directory, contact your sysadmin to resolve this issue.'),
- Http::STATUS_INTERNAL_SERVER_ERROR
- );
- } catch (PreConditionNotMetException $e) {
- $this->logger->error('No Speech-to-Text provider found: ' . $e->getMessage(), ['app' => Application::APP_ID]);
- return new DataResponse(
- $this->l10n->t('No Speech-to-Text provider found, install one from the app store to use this feature.'),
- Http::STATUS_BAD_REQUEST
- );
- } catch (InvalidArgumentException $e) {
- $this->logger->error('InvalidArgumentException: ' . $e->getMessage(), ['app' => Application::APP_ID]);
- return new DataResponse(
- $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
- Http::STATUS_INTERNAL_SERVER_ERROR
- );
- }
- }
}
diff --git a/lib/Controller/Text2ImageApiController.php b/lib/Controller/Text2ImageApiController.php
new file mode 100644
index 00000000..67daf89b
--- /dev/null
+++ b/lib/Controller/Text2ImageApiController.php
@@ -0,0 +1,220 @@
+
+// SPDX-License-Identifier: AGPL-3.0-or-later
+
+namespace OCA\TpAssistant\Controller;
+
+use Exception;
+use OCA\TpAssistant\Service\Text2Image\Text2ImageHelperService;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\AnonRateLimit;
+use OCP\AppFramework\Http\Attribute\BruteForceProtection;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
+use OCP\AppFramework\Http\Attribute\PublicPage;
+use OCP\AppFramework\Http\DataDisplayResponse;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\Db\Exception as DbException;
+
+use OCP\Files\NotPermittedException;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\TextToImage\Exception\TaskFailureException;
+
+class Text2ImageApiController extends OCSController {
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private Text2ImageHelperService $text2ImageHelperService,
+ private ?string $userId,
+ private IL10N $l10n,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * @param string $appId
+ * @param string $identifier
+ * @param string $prompt
+ * @param int $nResults
+ * @param bool $displayPrompt
+ * @param bool $notifyReadyIfScheduled
+ * @param bool $schedule
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function processPrompt(
+ string $appId, string $identifier, string $prompt, int $nResults = 1, bool $displayPrompt = false,
+ bool $notifyReadyIfScheduled = false, bool $schedule = false
+ ): DataResponse {
+ $nResults = min(10, max(1, $nResults));
+ try {
+ $result = $this->text2ImageHelperService->processPrompt(
+ $appId, $identifier, $prompt, $nResults, $displayPrompt, $this->userId, $notifyReadyIfScheduled, $schedule
+ );
+ } catch (Exception | TaskFailureException $e) {
+ return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
+ }
+
+ return new DataResponse($result);
+ }
+
+ /**
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ public function getPromptHistory(): DataResponse {
+
+ if ($this->userId === null) {
+ return new DataResponse(['error' => $this->l10n->t('Failed to get prompt history; unknown user')], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ try {
+ $response = $this->text2ImageHelperService->getPromptHistory($this->userId);
+ } catch (DbException $e) {
+ return new DataResponse(['error' => $this->l10n->t('Unknown error while retrieving prompt history.')], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ return new DataResponse($response);
+ }
+
+ /**
+ * @param string $imageGenId
+ * @param int $fileNameId
+ * @return DataDisplayResponse | DataResponse
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[PublicPage]
+ #[BruteForceProtection(action: 'imageGenId')]
+ public function getImage(string $imageGenId, int $fileNameId): DataDisplayResponse | DataResponse {
+ try {
+ $result = $this->text2ImageHelperService->getImage($imageGenId, $fileNameId);
+ } catch (Exception $e) {
+ $response = new DataResponse(['error' => $e->getMessage()], (int) $e->getCode());
+ if ($e->getCode() === Http::STATUS_BAD_REQUEST || $e->getCode() === Http::STATUS_UNAUTHORIZED) {
+ // Throttle brute force attempts
+ $response->throttle(['imageGenId' => $imageGenId, 'fileId' => $fileNameId, 'status' => $e->getCode()]);
+ }
+ return $response;
+ }
+
+ /*
+ if (isset($result['processing'])) {
+ return new DataResponse($result, Http::STATUS_OK);
+ }
+ */
+
+ $response = new DataDisplayResponse(
+ $result['image'] ?? '',
+ Http::STATUS_OK,
+ ['Content-Type' => $result['content-type'] ?? 'image/jpeg']
+ );
+ $response->cacheFor(60 * 60 * 24);
+ return $response;
+ }
+
+ /**
+ * @param string $imageGenId
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[PublicPage]
+ #[BruteForceProtection(action: 'imageGenId')]
+ public function getGenerationInfo(string $imageGenId): DataResponse {
+ try {
+ $result = $this->text2ImageHelperService->getGenerationInfo($imageGenId, $this->userId, true);
+ } catch (Exception $e) {
+ $response = new DataResponse(['error' => $e->getMessage()], (int) $e->getCode());
+ if ($e->getCode() === Http::STATUS_BAD_REQUEST || $e->getCode() === Http::STATUS_UNAUTHORIZED) {
+ // Throttle brute force attempts
+ $response->throttle(['imageGenId' => $imageGenId, 'status' => $e->getCode()]);
+ }
+ return $response;
+ }
+
+ return new DataResponse($result, Http::STATUS_OK);
+ }
+
+ /**
+ * @param string $imageGenId
+ * @param array $fileVisStatusArray
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[BruteForceProtection(action: 'imageGenId')]
+ public function setVisibilityOfImageFiles(string $imageGenId, array $fileVisStatusArray): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse(['error' => $this->l10n->t('Failed to set visibility of image files; unknown user')], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ if (count($fileVisStatusArray) < 1) {
+ return new DataResponse('File visibility array empty', Http::STATUS_BAD_REQUEST);
+ }
+
+ try {
+ $this->text2ImageHelperService->setVisibilityOfImageFiles($imageGenId, $fileVisStatusArray, $this->userId);
+ } catch (Exception $e) {
+ $response = new DataResponse(['error' => $e->getMessage()], (int) $e->getCode());
+ if($e->getCode() === Http::STATUS_BAD_REQUEST || $e->getCode() === Http::STATUS_UNAUTHORIZED) {
+ // Throttle brute force attempts
+ $response->throttle(['imageGenId' => $imageGenId, 'status' => $e->getCode()]);
+ }
+ return $response;
+ }
+
+ return new DataResponse('success', Http::STATUS_OK);
+ }
+
+ /**
+ * Notify when image generation is ready
+ *
+ * Does not need bruteforce protection since we respond with success anyways
+ * as we don't want to keep the front-end waiting.
+ * However, we still use rate limiting to prevent timing attacks.
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[AnonRateLimit(limit: 10, period: 60)]
+ public function notifyWhenReady(string $imageGenId): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse(['error' => $this->l10n->t('Failed to notify when ready; unknown user')], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ try {
+ $this->text2ImageHelperService->notifyWhenReady($imageGenId, $this->userId);
+ } catch (Exception $e) {
+ // Ignore
+ }
+ return new DataResponse('success', Http::STATUS_OK);
+ }
+
+ /**
+ * Cancel image generation
+ *
+ * Does not need bruteforce protection since we respond with success anyways
+ * (In theory bruteforce may be possible by a response timing attack but the attacker
+ * won't gain access to the generation since its deleted during the attack.)
+ *
+ * @param string $imageGenId
+ * @return DataResponse
+ * @throws NotPermittedException
+ */
+ #[NoAdminRequired]
+ #[NoCSRFRequired]
+ #[AnonRateLimit(limit: 10, period: 60)]
+ public function cancelGeneration(string $imageGenId): DataResponse {
+ if ($this->userId === null) {
+ return new DataResponse(['error' => $this->l10n->t('Failed to cancel generation; unknown user')], Http::STATUS_INTERNAL_SERVER_ERROR);
+ }
+
+ $this->text2ImageHelperService->cancelGeneration($imageGenId, $this->userId);
+ return new DataResponse('success', Http::STATUS_OK);
+ }
+}
diff --git a/lib/Controller/Text2ImageController.php b/lib/Controller/Text2ImageController.php
index 5fdb48cf..599ca53a 100644
--- a/lib/Controller/Text2ImageController.php
+++ b/lib/Controller/Text2ImageController.php
@@ -5,223 +5,24 @@
namespace OCA\TpAssistant\Controller;
-use Exception;
use OCA\TpAssistant\AppInfo\Application;
-use OCA\TpAssistant\Service\Text2Image\Text2ImageHelperService;
use OCP\AppFramework\Controller;
-use OCP\AppFramework\Http;
-use OCP\AppFramework\Http\Attribute\AnonRateLimit;
-use OCP\AppFramework\Http\Attribute\BruteForceProtection;
use OCP\AppFramework\Http\Attribute\NoAdminRequired;
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
use OCP\AppFramework\Http\Attribute\PublicPage;
-use OCP\AppFramework\Http\DataDisplayResponse;
-use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
-use OCP\Db\Exception as DbException;
-
-use OCP\Files\NotPermittedException;
-use OCP\IL10N;
use OCP\IRequest;
-use OCP\TextToImage\Exception\TaskFailureException;
class Text2ImageController extends Controller {
public function __construct(
string $appName,
IRequest $request,
- private Text2ImageHelperService $text2ImageHelperService,
private IInitialState $initialStateService,
- private ?string $userId,
- private IL10N $l10n,
) {
parent::__construct($appName, $request);
}
- /**
- * @param string $appId
- * @param string $identifier
- * @param string $prompt
- * @param int $nResults
- * @param bool $displayPrompt
- * @param bool $notifyReadyIfScheduled
- * @param bool $schedule
- * @return DataResponse
- */
- #[NoAdminRequired]
- #[NoCSRFRequired]
- public function processPrompt(
- string $appId, string $identifier, string $prompt, int $nResults = 1, bool $displayPrompt = false,
- bool $notifyReadyIfScheduled = false, bool $schedule = false
- ): DataResponse {
- $nResults = min(10, max(1, $nResults));
- try {
- $result = $this->text2ImageHelperService->processPrompt(
- $appId, $identifier, $prompt, $nResults, $displayPrompt, $this->userId, $notifyReadyIfScheduled, $schedule
- );
- } catch (Exception | TaskFailureException $e) {
- return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
- }
-
- return new DataResponse($result);
- }
-
- /**
- * @return DataResponse
- */
- #[NoAdminRequired]
- #[NoCSRFRequired]
- public function getPromptHistory(): DataResponse {
-
- if ($this->userId === null) {
- return new DataResponse(['error' => $this->l10n->t('Failed to get prompt history; unknown user')], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- try {
- $response = $this->text2ImageHelperService->getPromptHistory($this->userId);
- } catch (DbException $e) {
- return new DataResponse(['error' => $this->l10n->t('Unknown error while retrieving prompt history.')], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- return new DataResponse($response);
- }
-
- /**
- * @param string $imageGenId
- * @param int $fileNameId
- * @return DataDisplayResponse | DataResponse
- */
- #[NoAdminRequired]
- #[NoCSRFRequired]
- #[PublicPage]
- #[BruteForceProtection(action: 'imageGenId')]
- public function getImage(string $imageGenId, int $fileNameId): DataDisplayResponse | DataResponse {
- try {
- $result = $this->text2ImageHelperService->getImage($imageGenId, $fileNameId);
- } catch (Exception $e) {
- $response = new DataResponse(['error' => $e->getMessage()], (int) $e->getCode());
- if ($e->getCode() === Http::STATUS_BAD_REQUEST || $e->getCode() === Http::STATUS_UNAUTHORIZED) {
- // Throttle brute force attempts
- $response->throttle(['imageGenId' => $imageGenId, 'fileId' => $fileNameId, 'status' => $e->getCode()]);
- }
- return $response;
- }
-
- /*
- if (isset($result['processing'])) {
- return new DataResponse($result, Http::STATUS_OK);
- }
- */
-
- $response = new DataDisplayResponse(
- $result['image'] ?? '',
- Http::STATUS_OK,
- ['Content-Type' => $result['content-type'] ?? 'image/jpeg']
- );
- $response->cacheFor(60 * 60 * 24);
- return $response;
- }
-
- /**
- * @param string $imageGenId
- * @return DataResponse
- */
- #[NoAdminRequired]
- #[NoCSRFRequired]
- #[PublicPage]
- #[BruteForceProtection(action: 'imageGenId')]
- public function getGenerationInfo(string $imageGenId): DataResponse {
- try {
- $result = $this->text2ImageHelperService->getGenerationInfo($imageGenId, $this->userId, true);
- } catch (Exception $e) {
- $response = new DataResponse(['error' => $e->getMessage()], (int) $e->getCode());
- if ($e->getCode() === Http::STATUS_BAD_REQUEST || $e->getCode() === Http::STATUS_UNAUTHORIZED) {
- // Throttle brute force attempts
- $response->throttle(['imageGenId' => $imageGenId, 'status' => $e->getCode()]);
- }
- return $response;
- }
-
- return new DataResponse($result, Http::STATUS_OK);
- }
-
- /**
- * @param string $imageGenId
- * @param array $fileVisStatusArray
- * @return DataResponse
- */
- #[NoAdminRequired]
- #[NoCSRFRequired]
- #[BruteForceProtection(action: 'imageGenId')]
- public function setVisibilityOfImageFiles(string $imageGenId, array $fileVisStatusArray): DataResponse {
- if ($this->userId === null) {
- return new DataResponse(['error' => $this->l10n->t('Failed to set visibility of image files; unknown user')], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- if (count($fileVisStatusArray) < 1) {
- return new DataResponse('File visibility array empty', Http::STATUS_BAD_REQUEST);
- }
-
- try {
- $this->text2ImageHelperService->setVisibilityOfImageFiles($imageGenId, $fileVisStatusArray, $this->userId);
- } catch (Exception $e) {
- $response = new DataResponse(['error' => $e->getMessage()], (int) $e->getCode());
- if($e->getCode() === Http::STATUS_BAD_REQUEST || $e->getCode() === Http::STATUS_UNAUTHORIZED) {
- // Throttle brute force attempts
- $response->throttle(['imageGenId' => $imageGenId, 'status' => $e->getCode()]);
- }
- return $response;
- }
-
- return new DataResponse('success', Http::STATUS_OK);
- }
-
- /**
- * Notify when image generation is ready
- *
- * Does not need bruteforce protection since we respond with success anyways
- * as we don't want to keep the front-end waiting.
- * However, we still use rate limiting to prevent timing attacks.
- */
- #[NoAdminRequired]
- #[NoCSRFRequired]
- #[AnonRateLimit(limit: 10, period: 60)]
- public function notifyWhenReady(string $imageGenId): DataResponse {
- if ($this->userId === null) {
- return new DataResponse(['error' => $this->l10n->t('Failed to notify when ready; unknown user')], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- try {
- $this->text2ImageHelperService->notifyWhenReady($imageGenId, $this->userId);
- } catch (Exception $e) {
- // Ignore
- }
- return new DataResponse('success', Http::STATUS_OK);
- }
-
- /**
- * Cancel image generation
- *
- * Does not need bruteforce protection since we respond with success anyways
- * (In theory bruteforce may be possible by a response timing attack but the attacker
- * won't gain access to the generation since its deleted during the attack.)
- *
- * @param string $imageGenId
- * @return DataResponse
- * @throws NotPermittedException
- */
- #[NoAdminRequired]
- #[NoCSRFRequired]
- #[AnonRateLimit(limit: 10, period: 60)]
- public function cancelGeneration(string $imageGenId): DataResponse {
- if ($this->userId === null) {
- return new DataResponse(['error' => $this->l10n->t('Failed to cancel generation; unknown user')], Http::STATUS_INTERNAL_SERVER_ERROR);
- }
-
- $this->text2ImageHelperService->cancelGeneration($imageGenId, $this->userId);
- return new DataResponse('success', Http::STATUS_OK);
- }
-
/**
* Show visibility dialog
*
diff --git a/lib/Service/AssistantService.php b/lib/Service/AssistantService.php
index 68a63307..4ec949ec 100644
--- a/lib/Service/AssistantService.php
+++ b/lib/Service/AssistantService.php
@@ -21,11 +21,16 @@
use OCP\IL10N;
use OCP\Lock\LockedException;
use OCP\PreConditionNotMetException;
+use OCP\SpeechToText\ISpeechToTextManager;
use OCP\TextProcessing\FreePromptTaskType;
use OCP\TextProcessing\IManager as ITextProcessingManager;
+use OCP\TextProcessing\ITaskType;
use OCP\TextProcessing\Task as TextProcessingTask;
use Parsedown;
use PhpOffice\PhpWord\IOFactory;
+use Psr\Container\ContainerExceptionInterface;
+use Psr\Container\ContainerInterface;
+use Psr\Container\NotFoundExceptionInterface;
use Psr\Log\LoggerInterface;
use RuntimeException;
@@ -33,15 +38,72 @@ class AssistantService {
public function __construct(
private ITextProcessingManager $textProcessingManager,
+ private ISpeechToTextManager $speechToTextManager,
private Text2ImageHelperService $text2ImageHelperService,
private MetaTaskMapper $metaTaskMapper,
private LoggerInterface $logger,
private IRootFolder $storage,
private IJobList $jobList,
private IL10N $l10n,
+ private ContainerInterface $container,
) {
}
+ /**
+ * @return array
+ */
+ public function getAvailableTaskTypes(): array {
+ // text processing and copywriter
+ $typeClasses = $this->textProcessingManager->getAvailableTaskTypes();
+ $types = [];
+ /** @var string $typeClass */
+ foreach ($typeClasses as $typeClass) {
+ try {
+ /** @var ITaskType $object */
+ $object = $this->container->get($typeClass);
+ } catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
+ $this->logger->warning('Could not find ' . $typeClass, ['exception' => $e]);
+ continue;
+ }
+ if ($typeClass === FreePromptTaskType::class) {
+ $types[] = [
+ 'id' => $typeClass,
+ 'name' => $this->l10n->t('Generate text'),
+ 'description' => $this->l10n->t('Send a request to the Assistant, for example: write a first draft of a presentation, give me suggestions for a presentation, write a draft reply to my colleague.'),
+ ];
+ $types[] = [
+ 'id' => 'copywriter',
+ 'name' => $this->l10n->t('Context write'),
+ 'description' => $this->l10n->t('Writes text in a given style based on the provided source material.'),
+ ];
+ } else {
+ $types[] = [
+ 'id' => $typeClass,
+ 'name' => $object->getName(),
+ 'description' => $object->getDescription(),
+ ];
+ }
+ }
+
+ // STT
+ if ($this->speechToTextManager->hasProviders()) {
+ $types[] = [
+ 'id' => 'speech-to-text',
+ 'name' => $this->l10n->t('Transcribe'),
+ 'description' => $this->l10n->t('Transcribe audio to text'),
+ ];
+ }
+ // text2image
+ if ($this->text2ImageHelperService->hasProviders()) {
+ $types[] = [
+ 'id' => 'OCP\TextToImage\Task',
+ 'name' => $this->l10n->t('Generate image'),
+ 'description' => $this->l10n->t('Generate an image from a text'),
+ ];
+ }
+ return $types;
+ }
+
/**
* @param string $writingStyle
* @param string $sourceMaterial
diff --git a/lib/Service/SpeechToText/SpeechToTextService.php b/lib/Service/SpeechToText/SpeechToTextService.php
index 8494a2e6..99a6f3dc 100644
--- a/lib/Service/SpeechToText/SpeechToTextService.php
+++ b/lib/Service/SpeechToText/SpeechToTextService.php
@@ -23,17 +23,24 @@
namespace OCA\TpAssistant\Service\SpeechToText;
use DateTime;
+use Exception;
use InvalidArgumentException;
use OCA\TpAssistant\AppInfo\Application;
+use OCA\TpAssistant\Db\MetaTask;
use OCA\TpAssistant\Db\MetaTaskMapper;
+use OCP\AppFramework\Db\DoesNotExistException;
+use OCP\AppFramework\Db\MultipleObjectsReturnedException;
+use OCP\AppFramework\Http;
use OCP\Files\File;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\Files\NotFoundException;
use OCP\Files\NotPermittedException;
use OCP\IConfig;
+use OCP\IL10N;
use OCP\PreConditionNotMetException;
use OCP\SpeechToText\ISpeechToTextManager;
+use Psr\Log\LoggerInterface;
use RuntimeException;
class SpeechToTextService {
@@ -43,9 +50,42 @@ public function __construct(
private IRootFolder $rootFolder,
private IConfig $config,
private MetaTaskMapper $metaTaskMapper,
+ private IL10N $l10n,
+ private LoggerInterface $logger,
) {
}
+ /**
+ * Internal function to get transcription assistant tasks based on the assistant meta task id
+ *
+ * @param string $userId
+ * @param integer $metaTaskId
+ * @return MetaTask
+ * @throws Exception
+ */
+ public function internalGetTask(string $userId, int $metaTaskId): MetaTask {
+ try {
+ $metaTask = $this->metaTaskMapper->getUserMetaTask($metaTaskId, $userId);
+
+ if($metaTask->getCategory() !== Application::TASK_CATEGORY_SPEECH_TO_TEXT) {
+ throw new Exception('Task is not a speech to text task.', Http::STATUS_BAD_REQUEST);
+ }
+
+ return $metaTask;
+ } catch (MultipleObjectsReturnedException $e) {
+ $this->logger->error('Multiple tasks found for one id: ' . $e->getMessage(), ['app' => Application::APP_ID]);
+ throw new Exception($this->l10n->t('Multiple tasks found'), Http::STATUS_BAD_REQUEST);
+ } catch (DoesNotExistException $e) {
+ throw new Exception($this->l10n->t('Transcript not found'), Http::STATUS_NOT_FOUND);
+ } catch (Exception $e) {
+ $this->logger->error('Error: ' . $e->getMessage(), ['app' => Application::APP_ID]);
+ throw new Exception(
+ $this->l10n->t('Some internal error occurred. Contact your sysadmin for more info.'),
+ Http::STATUS_INTERNAL_SERVER_ERROR,
+ );
+ }
+ }
+
/**
* @param string $path
* @param string|null $userId
diff --git a/lib/Service/Text2Image/Text2ImageHelperService.php b/lib/Service/Text2Image/Text2ImageHelperService.php
index daa2cd31..2985d0d7 100644
--- a/lib/Service/Text2Image/Text2ImageHelperService.php
+++ b/lib/Service/Text2Image/Text2ImageHelperService.php
@@ -56,6 +56,10 @@ public function __construct(
) {
}
+ public function hasProviders(): bool {
+ return $this->textToImageManager->hasProviders();
+ }
+
/**
* Process a prompt using ImageProcessingProvider and return a link to the generated image(s)
*
diff --git a/src/assistant.js b/src/assistant.js
index 6048d50e..04acc06d 100644
--- a/src/assistant.js
+++ b/src/assistant.js
@@ -86,7 +86,7 @@ export async function openAssistantTextProcessingForm({
.then(async (response) => {
view.inputs = data.inputs
view.showScheduleConfirmation = true
- const task = response.data?.task
+ const task = response.data?.ocs?.data?.task
lastTask = task
useMetaTasks ? resolve(task) : resolve(await resolveMetaTaskToOcpTask(task))
})
@@ -115,7 +115,7 @@ export async function openAssistantTextProcessingForm({
: runOrScheduleTask
runOrScheduleFunction(appId, newTaskIdentifier, taskTypeId, inputs)
.then(async (response) => {
- const task = response.data?.task
+ const task = response.data?.ocs?.data?.task
lastTask = task
useMetaTasks ? resolve(task) : resolve(await resolveMetaTaskToOcpTask(task))
view.inputs = task.inputs
@@ -137,7 +137,7 @@ export async function openAssistantTextProcessingForm({
} else {
view.$destroy()
console.error('Assistant sync run error', error)
- showError(t('assistant', 'Assistant error') + ': ' + error?.response?.data)
+ showError(t('assistant', 'Assistant error'))
reject(new Error('Assistant sync run error'))
}
})
@@ -166,14 +166,14 @@ export async function openAssistantTextProcessingForm({
.then(async (response) => {
view.showSyncTaskRunning = false
view.showScheduleConfirmation = true
- const task = response.data?.task
+ const task = response.data?.ocs?.data?.task
lastTask = task
useMetaTasks ? resolve(task) : resolve(await resolveMetaTaskToOcpTask(task))
})
.catch(error => {
view.$destroy()
console.error('Assistant scheduling error', error)
- showError(t('assistant', 'Assistant error') + ': ' + error?.response?.data)
+ showError(t('assistant', 'Assistant error'))
reject(new Error('Assistant scheduling error'))
})
})
@@ -189,14 +189,14 @@ export async function openAssistantTextProcessingForm({
export async function runSttTask(inputs) {
const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios')
- const { generateUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
+ const { generateOcsUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
saveLastSelectedTaskType('speech-to-text')
if (inputs.sttMode === 'choose') {
- const url = generateUrl('/apps/assistant/stt/transcribeFile')
+ const url = generateOcsUrl('/apps/assistant/api/v1/stt/transcribeFile')
const params = { path: inputs.audioFilePath }
return axios.post(url, params)
} else {
- const url = generateUrl('/apps/assistant/stt/transcribeAudio')
+ const url = generateOcsUrl('/apps/assistant/api/v1/stt/transcribeAudio')
const formData = new FormData()
formData.append('audioData', inputs.audioData)
return axios.post(url, formData)
@@ -210,7 +210,7 @@ export function scheduleTtiTask(appId, identifier, taskType, inputs) {
export async function runOrScheduleTtiTask(appId, identifier, taskType, inputs, schedule = false) {
window.assistantAbortController = new AbortController()
const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios')
- const { generateUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
+ const { generateOcsUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
saveLastSelectedTaskType('OCP\\TextToImage\\Task')
const params = {
appId,
@@ -221,7 +221,7 @@ export async function runOrScheduleTtiTask(appId, identifier, taskType, inputs,
notifyReadyIfScheduled: true,
schedule,
}
- const url = generateUrl('/apps/assistant/i/process_prompt')
+ const url = generateOcsUrl('/apps/assistant/api/v1/i/process_prompt')
return axios.post(url, params, { signal: window.assistantAbortController.signal })
}
@@ -251,9 +251,9 @@ export async function cancelCurrentSyncTask() {
export async function runTask(appId, identifier, taskType, inputs) {
window.assistantAbortController = new AbortController()
const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios')
- const { generateUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
+ const { generateOcsUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
saveLastSelectedTaskType(taskType)
- const url = generateUrl('/apps/assistant/task/run')
+ const url = generateOcsUrl('/apps/assistant/api/v1/task/run')
const params = {
inputs,
type: taskType,
@@ -266,9 +266,9 @@ export async function runTask(appId, identifier, taskType, inputs) {
export async function runOrScheduleTask(appId, identifier, taskType, inputs) {
window.assistantAbortController = new AbortController()
const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios')
- const { generateUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
+ const { generateOcsUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
saveLastSelectedTaskType(taskType)
- const url = generateUrl('/apps/assistant/task/run-or-schedule')
+ const url = generateOcsUrl('/apps/assistant/api/v1/task/run-or-schedule')
const params = {
inputs,
type: taskType,
@@ -289,9 +289,9 @@ export async function runOrScheduleTask(appId, identifier, taskType, inputs) {
*/
export async function scheduleTask(appId, identifier, taskType, inputs) {
const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios')
- const { generateUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
+ const { generateOcsUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router')
saveLastSelectedTaskType(taskType)
- const url = generateUrl('/apps/assistant/task/schedule')
+ const url = generateOcsUrl('/apps/assistant/api/v1/task/schedule')
const params = {
inputs,
type: taskType,
@@ -353,12 +353,12 @@ export function handleNotification(event) {
*/
async function showAssistantTaskResult(taskId) {
const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios')
- const { generateUrl } = await import(/* webpackChunkName: "router-lazy" */'@nextcloud/router')
+ const { generateOcsUrl } = await import(/* webpackChunkName: "router-lazy" */'@nextcloud/router')
const { showError } = await import(/* webpackChunkName: "dialogs-lazy" */'@nextcloud/dialogs')
- const url = generateUrl('apps/assistant/task/{taskId}', { taskId })
+ const url = generateOcsUrl('/apps/assistant/api/v1/task/{taskId}', { taskId })
axios.get(url).then(response => {
- console.debug('showing results for task', response.data.task)
- openAssistantTaskResult(response.data.task, true)
+ console.debug('showing results for task', response.data?.ocs?.data?.task)
+ openAssistantTaskResult(response.data?.ocs?.data?.task, true)
}).catch(error => {
console.error(error)
showError(t('assistant', 'This task does not exist or has been cleaned up'))
@@ -400,8 +400,8 @@ export async function openAssistantPlainTextResult(metaTask) {
*/
export async function openAssistantImageResult(metaTask) {
// For now just open the image generation result on a new page:
- const { generateUrl } = await import(/* webpackChunkName: "router-lazy" */'@nextcloud/router')
- const url = generateUrl('apps/assistant/i/{genId}', { genId: metaTask.output })
+ const { generateOcsUrl } = await import(/* webpackChunkName: "router-lazy" */'@nextcloud/router')
+ const url = generateOcsUrl('/apps/assistant/api/v1/i/{genId}', { genId: metaTask.output })
window.open(url, '_blank')
}
@@ -459,7 +459,7 @@ export async function openAssistantTaskResult(task, useMetaTasks = false) {
scheduleTask(task.appId, task.identifier ?? '', data.selectedTaskTypeId, data.inputs)
.then((response) => {
view.showScheduleConfirmation = true
- console.debug('scheduled task', response.data?.task)
+ console.debug('scheduled task', response.data?.ocs?.data?.task)
})
.catch(error => {
view.$destroy()
@@ -485,8 +485,8 @@ export async function openAssistantTaskResult(task, useMetaTasks = false) {
: runOrScheduleTask
runOrScheduleFunction(task.appId, newTaskIdentifier, taskTypeId, inputs)
.then((response) => {
- // resolve(response.data?.task)
- const task = response.data?.task
+ // resolve(response.data?.ocs?.data?.task)
+ const task = response.data?.ocs?.data?.task
if (task.status === STATUS.successfull) {
view.output = task?.output
} else if (task.status === STATUS.scheduled) {
diff --git a/src/components/AssistantFormInputs.vue b/src/components/AssistantFormInputs.vue
index 1aa3b7b7..6a422038 100644
--- a/src/components/AssistantFormInputs.vue
+++ b/src/components/AssistantFormInputs.vue
@@ -100,7 +100,7 @@ import SpeechToTextInputForm from './SpeechToText/SpeechToTextInputForm.vue'
import Text2ImageInputForm from './Text2Image/Text2ImageInputForm.vue'
import { getFilePickerBuilder, showError } from '@nextcloud/dialogs'
-import { generateUrl } from '@nextcloud/router'
+import { generateOcsUrl } from '@nextcloud/router'
import axios from '@nextcloud/axios'
const VALID_MIME_TYPES = [
@@ -191,25 +191,26 @@ export default {
showError(t('assistant', 'No file selected'))
return
}
- const url = generateUrl('apps/assistant/parse-file')
+ const url = generateOcsUrl('/apps/assistant/api/v1/parse-file')
axios.post(url, {
filePath,
}).then((response) => {
- if (response.data?.parsedText === undefined) {
+ const data = response.data?.ocs?.data
+ if (data?.parsedText === undefined) {
showError(t('assistant', 'Unexpected response from text parser'))
return
}
switch (target) {
case 'sourceMaterial':
- this.sourceMaterial = response.data.parsedText
+ this.sourceMaterial = data.parsedText
this.onUpdateCopywriter()
break
case 'writingStyle':
- this.writingStyle = response.data.parsedText
+ this.writingStyle = data.parsedText
this.onUpdateCopywriter()
break
default:
- this.prompt = response.data.parsedText
+ this.prompt = data.parsedText
this.onUpdateMainInput()
}
}).catch((error) => {
diff --git a/src/components/AssistantTextProcessingForm.vue b/src/components/AssistantTextProcessingForm.vue
index 1d70c23c..4241521a 100644
--- a/src/components/AssistantTextProcessingForm.vue
+++ b/src/components/AssistantTextProcessingForm.vue
@@ -59,7 +59,8 @@