diff options
34 files changed, 2746 insertions, 8 deletions
diff --git a/apps/settings/lib/Controller/AISettingsController.php b/apps/settings/lib/Controller/AISettingsController.php index 7f016d79c25..8db8fa8b5bb 100644 --- a/apps/settings/lib/Controller/AISettingsController.php +++ b/apps/settings/lib/Controller/AISettingsController.php @@ -57,7 +57,7 @@ class AISettingsController extends Controller { * @return DataResponse */ public function update($settings) { - $keys = ['ai.stt_provider', 'ai.textprocessing_provider_preferences', 'ai.translation_provider_preferences']; + $keys = ['ai.stt_provider', 'ai.textprocessing_provider_preferences', 'ai.translation_provider_preferences', 'ai.text2image_provider']; foreach ($keys as $key) { if (!isset($settings[$key])) { continue; diff --git a/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php b/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php index eb1983690a5..0af82a74c5e 100644 --- a/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php +++ b/apps/settings/lib/Settings/Admin/ArtificialIntelligence.php @@ -48,6 +48,7 @@ class ArtificialIntelligence implements IDelegatedSettings { private ISpeechToTextManager $sttManager, private IManager $textProcessingManager, private ContainerInterface $container, + private \OCP\TextToImage\IManager $text2imageManager, ) { } @@ -101,15 +102,25 @@ class ArtificialIntelligence implements IDelegatedSettings { ]; } + $text2imageProviders = []; + foreach ($this->text2imageManager->getProviders() as $provider) { + $text2imageProviders[] = [ + 'id' => $provider->getId(), + 'name' => $provider->getName(), + ]; + } + $this->initialState->provideInitialState('ai-stt-providers', $sttProviders); $this->initialState->provideInitialState('ai-translation-providers', $translationProviders); $this->initialState->provideInitialState('ai-text-processing-providers', $textProcessingProviders); $this->initialState->provideInitialState('ai-text-processing-task-types', $textProcessingTaskTypes); + $this->initialState->provideInitialState('ai-text2image-providers', $text2imageProviders); $settings = [ 'ai.stt_provider' => count($sttProviders) > 0 ? $sttProviders[0]['class'] : null, 'ai.textprocessing_provider_preferences' => $textProcessingSettings, 'ai.translation_provider_preferences' => $translationPreferences, + 'ai.text2image_provider' => count($text2imageProviders) > 0 ? $text2imageProviders[0]['id'] : null, ]; foreach ($settings as $key => $defaultValue) { $value = $defaultValue; diff --git a/apps/settings/src/components/AdminAI.vue b/apps/settings/src/components/AdminAI.vue index 6a8b73d81f3..6a3f30451e9 100644 --- a/apps/settings/src/components/AdminAI.vue +++ b/apps/settings/src/components/AdminAI.vue @@ -36,6 +36,24 @@ </NcCheckboxRadioSwitch> </template> </NcSettingsSection> + <NcSettingsSection :name="t('settings', 'Image generation')" + :description="t('settings', 'Image generation can be implemented by different apps. Here you can set which app should be used.')"> + <template v-for="provider in text2imageProviders"> + <NcCheckboxRadioSwitch :key="provider.id" + :checked.sync="settings['ai.text2image_provider']" + :value="provider.id" + name="text2image_provider" + type="radio" + @update:checked="saveChanges"> + {{ provider.name }} + </NcCheckboxRadioSwitch> + </template> + <template v-if="!hasText2ImageProviders"> + <NcCheckboxRadioSwitch disabled type="radio"> + {{ t('settings', 'None of your currently installed apps provide image generation functionality') }} + </NcCheckboxRadioSwitch> + </template> + </NcSettingsSection> <NcSettingsSection :name="t('settings', 'Text processing')" :description="t('settings', 'Text processing tasks can be implemented by different apps. Here you can set which app should be used for which task.')"> <template v-for="type in tpTaskTypes"> @@ -88,7 +106,7 @@ export default { DragVerticalIcon, ArrowDownIcon, ArrowUpIcon, - NcButton + NcButton, }, data() { return { @@ -100,6 +118,7 @@ export default { translationProviders: loadState('settings', 'ai-translation-providers'), textProcessingProviders: loadState('settings', 'ai-text-processing-providers'), textProcessingTaskTypes: loadState('settings', 'ai-text-processing-task-types'), + text2imageProviders: loadState('settings', 'ai-text2image-providers'), settings: loadState('settings', 'ai-settings'), } }, @@ -113,13 +132,16 @@ export default { tpTaskTypes() { return Object.keys(this.settings['ai.textprocessing_provider_preferences']).filter(type => !!this.getTaskType(type)) }, + hasText2ImageProviders() { + return this.text2imageProviders.length > 0 + }, }, methods: { moveUp(i) { this.settings['ai.translation_provider_preferences'].splice( Math.min(i - 1, 0), 0, - ...this.settings['ai.translation_provider_preferences'].splice(i, 1) + ...this.settings['ai.translation_provider_preferences'].splice(i, 1), ) this.saveChanges() }, @@ -127,7 +149,7 @@ export default { this.settings['ai.translation_provider_preferences'].splice( i + 1, 0, - ...this.settings['ai.translation_provider_preferences'].splice(i, 1) + ...this.settings['ai.translation_provider_preferences'].splice(i, 1), ) this.saveChanges() }, diff --git a/apps/testing/composer/composer/autoload_classmap.php b/apps/testing/composer/composer/autoload_classmap.php index d176ec2784d..c75faee7ed2 100644 --- a/apps/testing/composer/composer/autoload_classmap.php +++ b/apps/testing/composer/composer/autoload_classmap.php @@ -13,6 +13,7 @@ return array( 'OCA\\Testing\\Controller\\LockingController' => $baseDir . '/../lib/Controller/LockingController.php', 'OCA\\Testing\\Controller\\RateLimitTestController' => $baseDir . '/../lib/Controller/RateLimitTestController.php', 'OCA\\Testing\\Locking\\FakeDBLockingProvider' => $baseDir . '/../lib/Locking/FakeDBLockingProvider.php', + 'OCA\\Testing\\Provider\\FakeText2ImageProvider' => $baseDir . '/../lib/Provider/FakeText2ImageProvider.php', 'OCA\\Testing\\Provider\\FakeTextProcessingProvider' => $baseDir . '/../lib/Provider/FakeTextProcessingProvider.php', 'OCA\\Testing\\Provider\\FakeTranslationProvider' => $baseDir . '/../lib/Provider/FakeTranslationProvider.php', ); diff --git a/apps/testing/composer/composer/autoload_static.php b/apps/testing/composer/composer/autoload_static.php index 76909190cb2..d26cdbab8d5 100644 --- a/apps/testing/composer/composer/autoload_static.php +++ b/apps/testing/composer/composer/autoload_static.php @@ -28,6 +28,7 @@ class ComposerStaticInitTesting 'OCA\\Testing\\Controller\\LockingController' => __DIR__ . '/..' . '/../lib/Controller/LockingController.php', 'OCA\\Testing\\Controller\\RateLimitTestController' => __DIR__ . '/..' . '/../lib/Controller/RateLimitTestController.php', 'OCA\\Testing\\Locking\\FakeDBLockingProvider' => __DIR__ . '/..' . '/../lib/Locking/FakeDBLockingProvider.php', + 'OCA\\Testing\\Provider\\FakeText2ImageProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeText2ImageProvider.php', 'OCA\\Testing\\Provider\\FakeTextProcessingProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeTextProcessingProvider.php', 'OCA\\Testing\\Provider\\FakeTranslationProvider' => __DIR__ . '/..' . '/../lib/Provider/FakeTranslationProvider.php', ); diff --git a/apps/testing/img/logo.png b/apps/testing/img/logo.png Binary files differnew file mode 100644 index 00000000000..df32e1c7eab --- /dev/null +++ b/apps/testing/img/logo.png diff --git a/apps/testing/lib/AppInfo/Application.php b/apps/testing/lib/AppInfo/Application.php index 98c211acaab..a92caeb245a 100644 --- a/apps/testing/lib/AppInfo/Application.php +++ b/apps/testing/lib/AppInfo/Application.php @@ -25,6 +25,7 @@ namespace OCA\Testing\AppInfo; use OCA\Testing\AlternativeHomeUserBackend; +use OCA\Testing\Provider\FakeText2ImageProvider; use OCA\Testing\Provider\FakeTranslationProvider; use OCA\Testing\Provider\FakeTextProcessingProvider; use OCP\AppFramework\App; @@ -40,6 +41,7 @@ class Application extends App implements IBootstrap { public function register(IRegistrationContext $context): void { $context->registerTranslationProvider(FakeTranslationProvider::class); $context->registerTextProcessingProvider(FakeTextProcessingProvider::class); + $context->registerTextToImageProvider(FakeText2ImageProvider::class); } public function boot(IBootContext $context): void { diff --git a/apps/testing/lib/Provider/FakeText2ImageProvider.php b/apps/testing/lib/Provider/FakeText2ImageProvider.php new file mode 100644 index 00000000000..a3728894a2c --- /dev/null +++ b/apps/testing/lib/Provider/FakeText2ImageProvider.php @@ -0,0 +1,48 @@ +<?php +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + +namespace OCA\Testing\Provider; + +use OCP\TextToImage\IProvider; + +class FakeText2ImageProvider implements IProvider { + + public function getName(): string { + return 'Fake Text2Image provider'; + } + + public function generate(string $prompt, array $resources): void { + foreach ($resources as $resource) { + $read = fopen(__DIR__ . '/../../img/logo.png', 'r'); + stream_copy_to_stream($read, $resource); + fclose($read); + } + } + + public function getExpectedRuntime(): int { + return 1; + } + + public function getId(): string { + return 'testing-fake-text2image-provider'; + } +} diff --git a/core/Controller/TextToImageApiController.php b/core/Controller/TextToImageApiController.php new file mode 100644 index 00000000000..13ce4658d59 --- /dev/null +++ b/core/Controller/TextToImageApiController.php @@ -0,0 +1,246 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + + +namespace OC\Core\Controller; + +use OC\Files\AppData\AppData; +use OCA\Core\ResponseDefinitions; +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\PublicPage; +use OCP\AppFramework\Http\Attribute\UserRateLimit; +use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\DB\Exception; +use OCP\Files\NotFoundException; +use OCP\IL10N; +use OCP\IRequest; +use OCP\TextToImage\Exception\TaskFailureException; +use OCP\TextToImage\Exception\TaskNotFoundException; +use OCP\TextToImage\Task; +use OCP\TextToImage\IManager; +use OCP\PreConditionNotMetException; + +/** + * @psalm-import-type CoreTextToImageTask from ResponseDefinitions + */ +class TextToImageApiController extends \OCP\AppFramework\OCSController { + public function __construct( + string $appName, + IRequest $request, + private IManager $textToImageManager, + private IL10N $l, + private ?string $userId, + private AppData $appData, + ) { + parent::__construct($appName, $request); + } + + /** + * Check whether this feature is available + * + * @return DataResponse<Http::STATUS_OK, array{isAvailable: bool}, array{}> + * + * 200: Returns availability status + */ + #[PublicPage] + public function isAvailable(): DataResponse { + return new DataResponse([ + 'isAvailable' => $this->textToImageManager->hasProviders(), + ]); + } + + /** + * This endpoint allows scheduling a text to image task + * + * @param string $input Input text + * @param string $appId ID of the app that will execute the task + * @param string $identifier An arbitrary identifier for the task + * @param int $numberOfImages The number of images to generate + * + * @return DataResponse<Http::STATUS_OK, array{task: CoreTextToImageTask}, array{}>|DataResponse<Http::STATUS_PRECONDITION_FAILED|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Task scheduled successfully + * 412: Scheduling task is not possible + */ + #[PublicPage] + #[UserRateLimit(limit: 20, period: 120)] + #[AnonRateLimit(limit: 5, period: 120)] + public function schedule(string $input, string $appId, string $identifier = '', int $numberOfImages = 8): DataResponse { + $task = new Task($input, $appId, $numberOfImages, $this->userId, $identifier); + try { + try { + $this->textToImageManager->runOrScheduleTask($task); + } catch (TaskFailureException) { + // Task status was already updated by the manager, nothing to do here + } + + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (PreConditionNotMetException) { + return new DataResponse(['message' => $this->l->t('No text to image provider is available')], Http::STATUS_PRECONDITION_FAILED); + } catch (Exception) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * This endpoint allows checking the status and results of a task. + * Tasks are removed 1 week after receiving their last update. + * + * @param int $id The id of the task + * + * @return DataResponse<Http::STATUS_OK, array{task: CoreTextToImageTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Task returned + * 404: Task not found + */ + #[PublicPage] + #[BruteForceProtection(action: 'text2image')] + public function getTask(int $id): DataResponse { + try { + $task = $this->textToImageManager->getUserTask($id, $this->userId); + + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (TaskNotFoundException) { + $res = new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); + $res->throttle(['action' => 'text2image']); + return $res; + } catch (\RuntimeException) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + /** + * This endpoint allows downloading the resulting image of a task + * + * @param int $id The id of the task + * @param int $index The index of the image to retrieve + * + * @return FileDisplayResponse<Http::STATUS_OK, array{'Content-Type': string}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Image returned + * 404: Task or image not found + */ + #[PublicPage] + #[BruteForceProtection(action: 'text2image')] + public function getImage(int $id, int $index): DataResponse|FileDisplayResponse { + try { + $task = $this->textToImageManager->getUserTask($id, $this->userId); + try { + $folder = $this->appData->getFolder('text2image'); + } catch(NotFoundException) { + $res = new DataResponse(['message' => $this->l->t('Image not found')], Http::STATUS_NOT_FOUND); + $res->throttle(['action' => 'text2image']); + return $res; + } + $file = $folder->getFolder((string) $task->getId())->getFile((string) $index); + $info = getimagesizefromstring($file->getContent()); + + return new FileDisplayResponse($file, Http::STATUS_OK, ['Content-Type' => image_type_to_mime_type($info[2])]); + } catch (TaskNotFoundException) { + $res = new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); + $res->throttle(['action' => 'text2image']); + return $res; + } catch (\RuntimeException) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } catch (NotFoundException) { + $res = new DataResponse(['message' => $this->l->t('Image not found')], Http::STATUS_NOT_FOUND); + $res->throttle(['action' => 'text2image']); + return $res; + } + } + + /** + * This endpoint allows to delete a scheduled task for a user + * + * @param int $id The id of the task + * + * @return DataResponse<Http::STATUS_OK, array{task: CoreTextToImageTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Task returned + * 404: Task not found + */ + #[NoAdminRequired] + #[BruteForceProtection(action: 'text2image')] + public function deleteTask(int $id): DataResponse { + try { + $task = $this->textToImageManager->getUserTask($id, $this->userId); + + $this->textToImageManager->deleteTask($task); + + $json = $task->jsonSerialize(); + + return new DataResponse([ + 'task' => $json, + ]); + } catch (TaskNotFoundException) { + $res = new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND); + $res->throttle(['action' => 'text2image']); + return $res; + } catch (\RuntimeException) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } + + + /** + * This endpoint returns a list of tasks of a user that are related + * with a specific appId and optionally with an identifier + * + * @param string $appId ID of the app + * @param string|null $identifier An arbitrary identifier for the task + * @return DataResponse<Http::STATUS_OK, array{tasks: CoreTextToImageTask[]}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}> + * + * 200: Task list returned + */ + #[NoAdminRequired] + #[AnonRateLimit(limit: 5, period: 120)] + public function listTasksByApp(string $appId, ?string $identifier = null): DataResponse { + try { + $tasks = $this->textToImageManager->getUserTasksByApp($this->userId, $appId, $identifier); + /** @var CoreTextToImageTask[] $json */ + $json = array_map(static function (Task $task) { + return $task->jsonSerialize(); + }, $tasks); + + return new DataResponse([ + 'tasks' => $json, + ]); + } catch (\RuntimeException) { + return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR); + } + } +} diff --git a/core/Migrations/Version28000Date20230906104802.php b/core/Migrations/Version28000Date20230906104802.php new file mode 100644 index 00000000000..06b41ca3ace --- /dev/null +++ b/core/Migrations/Version28000Date20230906104802.php @@ -0,0 +1,99 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OC\Core\Migrations; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Introduce text2image_tasks table + */ +class Version28000Date20230906104802 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): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + if (!$schema->hasTable('text2image_tasks')) { + $table = $schema->createTable('text2image_tasks'); + + $table->addColumn('id', Types::BIGINT, [ + 'notnull' => true, + 'length' => 64, + 'autoincrement' => true, + ]); + $table->addColumn('input', Types::TEXT, [ + 'notnull' => true, + ]); + $table->addColumn('status', Types::INTEGER, [ + 'notnull' => false, + 'length' => 6, + 'default' => 0, + ]); + $table->addColumn('number_of_images', Types::INTEGER, [ + 'notnull' => true, + 'default' => 1, + ]); + $table->addColumn('user_id', Types::STRING, [ + 'notnull' => false, + 'length' => 64, + ]); + $table->addColumn('app_id', Types::STRING, [ + 'notnull' => true, + 'length' => 32, + 'default' => '', + ]); + $table->addColumn('identifier', Types::STRING, [ + 'notnull' => false, + 'length' => 255, + 'default' => '', + ]); + $table->addColumn('last_updated', Types::DATETIME, [ + 'notnull' => false, + ]); + $table->addColumn('completion_expected_at', Types::DATETIME, [ + 'notnull' => false, + ]); + + $table->setPrimaryKey(['id'], 't2i_tasks_id_index'); + $table->addIndex(['last_updated'], 't2i_tasks_updated'); + $table->addIndex(['status'], 't2i_tasks_status'); + $table->addIndex(['user_id', 'app_id', 'identifier'], 't2i_tasks_uid_appid_ident'); + + return $schema; + } + + return null; + } +} diff --git a/core/ResponseDefinitions.php b/core/ResponseDefinitions.php index 7e2bc643ce5..ca3f117051c 100644 --- a/core/ResponseDefinitions.php +++ b/core/ResponseDefinitions.php @@ -144,6 +144,17 @@ namespace OCA\Core; * output: ?string, * identifier: string, * } + * + * @psalm-type CoreTextToImageTask = array{ + * id: ?int, + * status: 0|1|2|3|4, + * userId: ?string, + * appId: string, + * input: string, + * identifier: ?string, + * numberOfImages: int, + * completionExpectedAt: ?int, + * } */ class ResponseDefinitions { } diff --git a/core/openapi.json b/core/openapi.json index a9c810a790a..7cb48b58f0a 100644 --- a/core/openapi.json +++ b/core/openapi.json @@ -450,6 +450,53 @@ } } }, + "TextToImageTask": { + "type": "object", + "required": [ + "id", + "status", + "userId", + "appId", + "input", + "identifier", + "numberOfImages", + "completionExpectedAt" + ], + "properties": { + "id": { + "type": "integer", + "format": "int64", + "nullable": true + }, + "status": { + "type": "integer", + "format": "int64" + }, + "userId": { + "type": "string", + "nullable": true + }, + "appId": { + "type": "string" + }, + "input": { + "type": "string" + }, + "identifier": { + "type": "string", + "nullable": true + }, + "numberOfImages": { + "type": "integer", + "format": "int64" + }, + "completionExpectedAt": { + "type": "integer", + "format": "int64", + "nullable": true + } + } + }, "UnifiedSearchProvider": { "type": "object", "required": [ @@ -5130,6 +5177,835 @@ } } }, + "/ocs/v2.php/text2image/is_available": { + "get": { + "operationId": "text_to_image_api-is-available", + "summary": "Check whether this feature is available", + "tags": [ + "text_to_image_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Returns availability status", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "isAvailable" + ], + "properties": { + "isAvailable": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/text2image/schedule": { + "post": { + "operationId": "text_to_image_api-schedule", + "summary": "This endpoint allows scheduling a text to image task", + "tags": [ + "text_to_image_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "input", + "in": "query", + "description": "Input text", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "appId", + "in": "query", + "description": "ID of the app that will execute the task", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "identifier", + "in": "query", + "description": "An arbitrary identifier for the task", + "schema": { + "type": "string", + "default": "" + } + }, + { + "name": "numberOfImages", + "in": "query", + "description": "The number of images to generate", + "schema": { + "type": "integer", + "format": "int64", + "default": 8 + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Task scheduled successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "task" + ], + "properties": { + "task": { + "$ref": "#/components/schemas/TextToImageTask" + } + } + } + } + } + } + } + } + } + }, + "412": { + "description": "Scheduling task is not possible", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/text2image/task/{id}": { + "get": { + "operationId": "text_to_image_api-get-task", + "summary": "This endpoint allows checking the status and results of a task. Tasks are removed 1 week after receiving their last update.", + "tags": [ + "text_to_image_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the task", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Task returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "task" + ], + "properties": { + "task": { + "$ref": "#/components/schemas/TextToImageTask" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Task not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + }, + "delete": { + "operationId": "text_to_image_api-delete-task", + "summary": "This endpoint allows to delete a scheduled task for a user", + "tags": [ + "text_to_image_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the task", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Task returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "task" + ], + "properties": { + "task": { + "$ref": "#/components/schemas/TextToImageTask" + } + } + } + } + } + } + } + } + } + }, + "404": { + "description": "Task not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/text2image/task/{id}/image/{index}": { + "get": { + "operationId": "text_to_image_api-get-image", + "summary": "This endpoint allows downloading the resulting image of a task", + "tags": [ + "text_to_image_api" + ], + "security": [ + {}, + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "id", + "in": "path", + "description": "The id of the task", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "index", + "in": "path", + "description": "The index of the image to retrieve", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Image returned", + "content": { + "*/*": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "404": { + "description": "Task or image not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "/ocs/v2.php/text2image/tasks/app/{appId}": { + "get": { + "operationId": "text_to_image_api-list-tasks-by-app", + "summary": "This endpoint returns a list of tasks of a user that are related with a specific appId and optionally with an identifier", + "tags": [ + "text_to_image_api" + ], + "security": [ + { + "bearer_auth": [] + }, + { + "basic_auth": [] + } + ], + "parameters": [ + { + "name": "identifier", + "in": "query", + "description": "An arbitrary identifier for the task", + "schema": { + "type": "string", + "nullable": true + } + }, + { + "name": "appId", + "in": "path", + "description": "ID of the app", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "OCS-APIRequest", + "in": "header", + "description": "Required to be true for the API request to pass", + "required": true, + "schema": { + "type": "boolean", + "default": true + } + } + ], + "responses": { + "200": { + "description": "Task list returned", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "tasks" + ], + "properties": { + "tasks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TextToImageTask" + } + } + } + } + } + } + } + } + } + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object", + "required": [ + "ocs" + ], + "properties": { + "ocs": { + "type": "object", + "required": [ + "meta", + "data" + ], + "properties": { + "meta": { + "$ref": "#/components/schemas/OCSMeta" + }, + "data": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "type": "string" + } + } + } + } + } + } + } + } + } + } + } + } + }, "/status.php": { "get": { "operationId": "get-status", diff --git a/core/routes.php b/core/routes.php index fcb5a15cf01..fe1fe6fcd75 100644 --- a/core/routes.php +++ b/core/routes.php @@ -155,6 +155,13 @@ $application->registerRoutes($this, [ ['root' => '/textprocessing', 'name' => 'TextProcessingApi#getTask', 'url' => '/task/{id}', 'verb' => 'GET'], ['root' => '/textprocessing', 'name' => 'TextProcessingApi#deleteTask', 'url' => '/task/{id}', 'verb' => 'DELETE'], ['root' => '/textprocessing', 'name' => 'TextProcessingApi#listTasksByApp', 'url' => '/tasks/app/{appId}', 'verb' => 'GET'], + + ['root' => '/text2image', 'name' => 'TextToImageApi#isAvailable', 'url' => '/is_available', 'verb' => 'GET'], + ['root' => '/text2image', 'name' => 'TextToImageApi#schedule', 'url' => '/schedule', 'verb' => 'POST'], + ['root' => '/text2image', 'name' => 'TextToImageApi#getTask', 'url' => '/task/{id}', 'verb' => 'GET'], + ['root' => '/text2image', 'name' => 'TextToImageApi#getImage', 'url' => '/task/{id}/image/{index}', 'verb' => 'GET'], + ['root' => '/text2image', 'name' => 'TextToImageApi#deleteTask', 'url' => '/task/{id}', 'verb' => 'DELETE'], + ['root' => '/text2image', 'name' => 'TextToImageApi#listTasksByApp', 'url' => '/tasks/app/{appId}', 'verb' => 'GET'], ], ]); diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 548d930e758..a1a42dd595d 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -672,6 +672,15 @@ return array( 'OCP\\TextProcessing\\SummaryTaskType' => $baseDir . '/lib/public/TextProcessing/SummaryTaskType.php', 'OCP\\TextProcessing\\Task' => $baseDir . '/lib/public/TextProcessing/Task.php', 'OCP\\TextProcessing\\TopicsTaskType' => $baseDir . '/lib/public/TextProcessing/TopicsTaskType.php', + 'OCP\\TextToImage\\Events\\AbstractTextToImageEvent' => $baseDir . '/lib/public/TextToImage/Events/AbstractTextToImageEvent.php', + 'OCP\\TextToImage\\Events\\TaskFailedEvent' => $baseDir . '/lib/public/TextToImage/Events/TaskFailedEvent.php', + 'OCP\\TextToImage\\Events\\TaskSuccessfulEvent' => $baseDir . '/lib/public/TextToImage/Events/TaskSuccessfulEvent.php', + 'OCP\\TextToImage\\Exception\\TaskFailureException' => $baseDir . '/lib/public/TextToImage/Exception/TaskFailureException.php', + 'OCP\\TextToImage\\Exception\\TaskNotFoundException' => $baseDir . '/lib/public/TextToImage/Exception/TaskNotFoundException.php', + 'OCP\\TextToImage\\Exception\\TextToImageException' => $baseDir . '/lib/public/TextToImage/Exception/TextToImageException.php', + 'OCP\\TextToImage\\IManager' => $baseDir . '/lib/public/TextToImage/IManager.php', + 'OCP\\TextToImage\\IProvider' => $baseDir . '/lib/public/TextToImage/IProvider.php', + 'OCP\\TextToImage\\Task' => $baseDir . '/lib/public/TextToImage/Task.php', 'OCP\\Translation\\CouldNotTranslateException' => $baseDir . '/lib/public/Translation/CouldNotTranslateException.php', 'OCP\\Translation\\IDetectLanguageProvider' => $baseDir . '/lib/public/Translation/IDetectLanguageProvider.php', 'OCP\\Translation\\ITranslationManager' => $baseDir . '/lib/public/Translation/ITranslationManager.php', @@ -1102,6 +1111,7 @@ return array( 'OC\\Core\\Controller\\SearchController' => $baseDir . '/core/Controller/SearchController.php', 'OC\\Core\\Controller\\SetupController' => $baseDir . '/core/Controller/SetupController.php', 'OC\\Core\\Controller\\TextProcessingApiController' => $baseDir . '/core/Controller/TextProcessingApiController.php', + 'OC\\Core\\Controller\\TextToImageApiController' => $baseDir . '/core/Controller/TextToImageApiController.php', 'OC\\Core\\Controller\\TranslationApiController' => $baseDir . '/core/Controller/TranslationApiController.php', 'OC\\Core\\Controller\\TwoFactorChallengeController' => $baseDir . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UnifiedSearchController' => $baseDir . '/core/Controller/UnifiedSearchController.php', @@ -1183,6 +1193,7 @@ return array( 'OC\\Core\\Migrations\\Version28000Date20230616104802' => $baseDir . '/core/Migrations/Version28000Date20230616104802.php', 'OC\\Core\\Migrations\\Version28000Date20230728104802' => $baseDir . '/core/Migrations/Version28000Date20230728104802.php', 'OC\\Core\\Migrations\\Version28000Date20230803221055' => $baseDir . '/core/Migrations/Version28000Date20230803221055.php', + 'OC\\Core\\Migrations\\Version28000Date20230906104802' => $baseDir . '/core/Migrations/Version28000Date20230906104802.php', 'OC\\Core\\Notification\\CoreNotifier' => $baseDir . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => $baseDir . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => $baseDir . '/lib/private/DB/Adapter.php', @@ -1719,6 +1730,11 @@ return array( 'OC\\TextProcessing\\Manager' => $baseDir . '/lib/private/TextProcessing/Manager.php', 'OC\\TextProcessing\\RemoveOldTasksBackgroundJob' => $baseDir . '/lib/private/TextProcessing/RemoveOldTasksBackgroundJob.php', 'OC\\TextProcessing\\TaskBackgroundJob' => $baseDir . '/lib/private/TextProcessing/TaskBackgroundJob.php', + 'OC\\TextToImage\\Db\\Task' => $baseDir . '/lib/private/TextToImage/Db/Task.php', + 'OC\\TextToImage\\Db\\TaskMapper' => $baseDir . '/lib/private/TextToImage/Db/TaskMapper.php', + 'OC\\TextToImage\\Manager' => $baseDir . '/lib/private/TextToImage/Manager.php', + 'OC\\TextToImage\\RemoveOldTasksBackgroundJob' => $baseDir . '/lib/private/TextToImage/RemoveOldTasksBackgroundJob.php', + 'OC\\TextToImage\\TaskBackgroundJob' => $baseDir . '/lib/private/TextToImage/TaskBackgroundJob.php', 'OC\\Translation\\TranslationManager' => $baseDir . '/lib/private/Translation/TranslationManager.php', 'OC\\URLGenerator' => $baseDir . '/lib/private/URLGenerator.php', 'OC\\Updater' => $baseDir . '/lib/private/Updater.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 4af4beb4867..421e9b775b5 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -705,6 +705,15 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\TextProcessing\\SummaryTaskType' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/SummaryTaskType.php', 'OCP\\TextProcessing\\Task' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/Task.php', 'OCP\\TextProcessing\\TopicsTaskType' => __DIR__ . '/../../..' . '/lib/public/TextProcessing/TopicsTaskType.php', + 'OCP\\TextToImage\\Events\\AbstractTextToImageEvent' => __DIR__ . '/../../..' . '/lib/public/TextToImage/Events/AbstractTextToImageEvent.php', + 'OCP\\TextToImage\\Events\\TaskFailedEvent' => __DIR__ . '/../../..' . '/lib/public/TextToImage/Events/TaskFailedEvent.php', + 'OCP\\TextToImage\\Events\\TaskSuccessfulEvent' => __DIR__ . '/../../..' . '/lib/public/TextToImage/Events/TaskSuccessfulEvent.php', + 'OCP\\TextToImage\\Exception\\TaskFailureException' => __DIR__ . '/../../..' . '/lib/public/TextToImage/Exception/TaskFailureException.php', + 'OCP\\TextToImage\\Exception\\TaskNotFoundException' => __DIR__ . '/../../..' . '/lib/public/TextToImage/Exception/TaskNotFoundException.php', + 'OCP\\TextToImage\\Exception\\TextToImageException' => __DIR__ . '/../../..' . '/lib/public/TextToImage/Exception/TextToImageException.php', + 'OCP\\TextToImage\\IManager' => __DIR__ . '/../../..' . '/lib/public/TextToImage/IManager.php', + 'OCP\\TextToImage\\IProvider' => __DIR__ . '/../../..' . '/lib/public/TextToImage/IProvider.php', + 'OCP\\TextToImage\\Task' => __DIR__ . '/../../..' . '/lib/public/TextToImage/Task.php', 'OCP\\Translation\\CouldNotTranslateException' => __DIR__ . '/../../..' . '/lib/public/Translation/CouldNotTranslateException.php', 'OCP\\Translation\\IDetectLanguageProvider' => __DIR__ . '/../../..' . '/lib/public/Translation/IDetectLanguageProvider.php', 'OCP\\Translation\\ITranslationManager' => __DIR__ . '/../../..' . '/lib/public/Translation/ITranslationManager.php', @@ -1135,6 +1144,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Controller\\SearchController' => __DIR__ . '/../../..' . '/core/Controller/SearchController.php', 'OC\\Core\\Controller\\SetupController' => __DIR__ . '/../../..' . '/core/Controller/SetupController.php', 'OC\\Core\\Controller\\TextProcessingApiController' => __DIR__ . '/../../..' . '/core/Controller/TextProcessingApiController.php', + 'OC\\Core\\Controller\\TextToImageApiController' => __DIR__ . '/../../..' . '/core/Controller/TextToImageApiController.php', 'OC\\Core\\Controller\\TranslationApiController' => __DIR__ . '/../../..' . '/core/Controller/TranslationApiController.php', 'OC\\Core\\Controller\\TwoFactorChallengeController' => __DIR__ . '/../../..' . '/core/Controller/TwoFactorChallengeController.php', 'OC\\Core\\Controller\\UnifiedSearchController' => __DIR__ . '/../../..' . '/core/Controller/UnifiedSearchController.php', @@ -1216,6 +1226,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Core\\Migrations\\Version28000Date20230616104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230616104802.php', 'OC\\Core\\Migrations\\Version28000Date20230728104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230728104802.php', 'OC\\Core\\Migrations\\Version28000Date20230803221055' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230803221055.php', + 'OC\\Core\\Migrations\\Version28000Date20230906104802' => __DIR__ . '/../../..' . '/core/Migrations/Version28000Date20230906104802.php', 'OC\\Core\\Notification\\CoreNotifier' => __DIR__ . '/../../..' . '/core/Notification/CoreNotifier.php', 'OC\\Core\\Service\\LoginFlowV2Service' => __DIR__ . '/../../..' . '/core/Service/LoginFlowV2Service.php', 'OC\\DB\\Adapter' => __DIR__ . '/../../..' . '/lib/private/DB/Adapter.php', @@ -1752,6 +1763,11 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\TextProcessing\\Manager' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/Manager.php', 'OC\\TextProcessing\\RemoveOldTasksBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/RemoveOldTasksBackgroundJob.php', 'OC\\TextProcessing\\TaskBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/TextProcessing/TaskBackgroundJob.php', + 'OC\\TextToImage\\Db\\Task' => __DIR__ . '/../../..' . '/lib/private/TextToImage/Db/Task.php', + 'OC\\TextToImage\\Db\\TaskMapper' => __DIR__ . '/../../..' . '/lib/private/TextToImage/Db/TaskMapper.php', + 'OC\\TextToImage\\Manager' => __DIR__ . '/../../..' . '/lib/private/TextToImage/Manager.php', + 'OC\\TextToImage\\RemoveOldTasksBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/TextToImage/RemoveOldTasksBackgroundJob.php', + 'OC\\TextToImage\\TaskBackgroundJob' => __DIR__ . '/../../..' . '/lib/private/TextToImage/TaskBackgroundJob.php', 'OC\\Translation\\TranslationManager' => __DIR__ . '/../../..' . '/lib/private/Translation/TranslationManager.php', 'OC\\URLGenerator' => __DIR__ . '/../../..' . '/lib/private/URLGenerator.php', 'OC\\Updater' => __DIR__ . '/../../..' . '/lib/private/Updater.php', diff --git a/lib/private/AppFramework/Bootstrap/RegistrationContext.php b/lib/private/AppFramework/Bootstrap/RegistrationContext.php index d4588527006..5ff2dcd7969 100644 --- a/lib/private/AppFramework/Bootstrap/RegistrationContext.php +++ b/lib/private/AppFramework/Bootstrap/RegistrationContext.php @@ -138,6 +138,12 @@ class RegistrationContext { /** @var ServiceRegistration<IReferenceProvider>[] */ private array $referenceProviders = []; + /** @var ServiceRegistration<\OCP\TextToImage\IProvider>[] */ + private $textToImageProviders = []; + + + + /** @var ParameterRegistration[] */ private $sensitiveMethods = []; @@ -273,6 +279,13 @@ class RegistrationContext { ); } + public function registerTextToImageProvider(string $providerClass): void { + $this->context->registerTextToImageProvider( + $this->appId, + $providerClass + ); + } + public function registerTemplateProvider(string $providerClass): void { $this->context->registerTemplateProvider( $this->appId, @@ -450,6 +463,10 @@ class RegistrationContext { $this->textProcessingProviders[] = new ServiceRegistration($appId, $class); } + public function registerTextToImageProvider(string $appId, string $class): void { + $this->textToImageProviders[] = new ServiceRegistration($appId, $class); + } + public function registerTemplateProvider(string $appId, string $class): void { $this->templateProviders[] = new ServiceRegistration($appId, $class); } @@ -740,6 +757,13 @@ class RegistrationContext { } /** + * @return ServiceRegistration<\OCP\TextToImage\IProvider>[] + */ + public function getTextToImageProviders(): array { + return $this->textToImageProviders; + } + + /** * @return ServiceRegistration<ICustomTemplateProvider>[] */ public function getTemplateProviders(): array { diff --git a/lib/private/Files/Node/Folder.php b/lib/private/Files/Node/Folder.php index ccd10da9d0c..c7462572fed 100644 --- a/lib/private/Files/Node/Folder.php +++ b/lib/private/Files/Node/Folder.php @@ -177,7 +177,7 @@ class Folder extends Node implements \OCP\Files\Folder { * @throws \OCP\Files\NotPermittedException */ public function newFile($path, $content = null) { - if (empty($path)) { + if ($path === '') { throw new NotPermittedException('Could not create as provided path is empty'); } if ($this->checkPermissions(\OCP\Constants::PERMISSION_CREATE)) { diff --git a/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php b/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php index 94ae39f2183..00badbb726d 100644 --- a/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php +++ b/lib/private/Repair/AddRemoveOldTasksBackgroundJob.php @@ -25,7 +25,8 @@ declare(strict_types=1); */ namespace OC\Repair; -use OC\TextProcessing\RemoveOldTasksBackgroundJob; +use OC\TextProcessing\RemoveOldTasksBackgroundJob as RemoveOldTextProcessingTasksBackgroundJob; +use OC\TextToImage\RemoveOldTasksBackgroundJob as RemoveOldTextToImageTasksBackgroundJob; use OCP\BackgroundJob\IJobList; use OCP\Migration\IOutput; use OCP\Migration\IRepairStep; @@ -38,10 +39,11 @@ class AddRemoveOldTasksBackgroundJob implements IRepairStep { } public function getName(): string { - return 'Add language model tasks cleanup job'; + return 'Add AI tasks cleanup job'; } public function run(IOutput $output) { - $this->jobList->add(RemoveOldTasksBackgroundJob::class); + $this->jobList->add(RemoveOldTextProcessingTasksBackgroundJob::class); + $this->jobList->add(RemoveOldTextToImageTasksBackgroundJob::class); } } diff --git a/lib/private/Server.php b/lib/private/Server.php index 37b7669f624..77791c49e8b 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -1428,6 +1428,8 @@ class Server extends ServerContainer implements IServerContainer { $this->registerAlias(\OCP\TextProcessing\IManager::class, \OC\TextProcessing\Manager::class); + $this->registerAlias(\OCP\TextToImage\IManager::class, \OC\TextToImage\Manager::class); + $this->registerAlias(ILimiter::class, Limiter::class); $this->registerAlias(IPhoneNumberUtil::class, PhoneNumberUtil::class); diff --git a/lib/private/TextToImage/Db/Task.php b/lib/private/TextToImage/Db/Task.php new file mode 100644 index 00000000000..96dd6e4e165 --- /dev/null +++ b/lib/private/TextToImage/Db/Task.php @@ -0,0 +1,117 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + +namespace OC\TextToImage\Db; + +use DateTime; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\TextToImage\Task as OCPTask; + +/** + * @method setLastUpdated(DateTime $lastUpdated) + * @method DateTime getLastUpdated() + * @method setInput(string $type) + * @method string getInput() + * @method setResultPath(string $resultPath) + * @method string getResultPath() + * @method setStatus(int $type) + * @method int getStatus() + * @method setUserId(?string $userId) + * @method string|null getUserId() + * @method setAppId(string $type) + * @method string getAppId() + * @method setIdentifier(string $identifier) + * @method string|null getIdentifier() + * @method setNumberOfImages(int $numberOfImages) + * @method int getNumberOfImages() + * @method setCompletionExpectedAt(DateTime $at) + * @method DateTime getCompletionExpectedAt() + */ +class Task extends Entity { + protected $lastUpdated; + protected $type; + protected $input; + protected $status; + protected $userId; + protected $appId; + protected $identifier; + protected $numberOfImages; + protected $completionExpectedAt; + + /** + * @var string[] + */ + public static array $columns = ['id', 'last_updated', 'input', 'status', 'user_id', 'app_id', 'identifier', 'number_of_images', 'completion_expected_at']; + + /** + * @var string[] + */ + public static array $fields = ['id', 'lastUpdated', 'input', 'status', 'userId', 'appId', 'identifier', 'numberOfImages', 'completionExpectedAt']; + + + public function __construct() { + // add types in constructor + $this->addType('id', 'integer'); + $this->addType('lastUpdated', 'datetime'); + $this->addType('input', 'string'); + $this->addType('status', 'integer'); + $this->addType('userId', 'string'); + $this->addType('appId', 'string'); + $this->addType('identifier', 'string'); + $this->addType('numberOfImages', 'integer'); + $this->addType('completionExpectedAt', 'datetime'); + } + + public function toRow(): array { + return array_combine(self::$columns, array_map(function ($field) { + return $this->{'get'.ucfirst($field)}(); + }, self::$fields)); + } + + public static function fromPublicTask(OCPTask $task): Task { + /** @var Task $dbTask */ + $dbTask = Task::fromParams([ + 'id' => $task->getId(), + 'lastUpdated' => \OCP\Server::get(ITimeFactory::class)->getDateTime(), + 'status' => $task->getStatus(), + 'numberOfImages' => $task->getNumberOfImages(), + 'input' => $task->getInput(), + 'userId' => $task->getUserId(), + 'appId' => $task->getAppId(), + 'identifier' => $task->getIdentifier(), + 'completionExpectedAt' => $task->getCompletionExpectedAt(), + ]); + return $dbTask; + } + + public function toPublicTask(): OCPTask { + $task = new OCPTask($this->getInput(), $this->getAppId(), $this->getNumberOfImages(), $this->getuserId(), $this->getIdentifier()); + $task->setId($this->getId()); + $task->setStatus($this->getStatus()); + $task->setCompletionExpectedAt($this->getCompletionExpectedAt()); + return $task; + } +} diff --git a/lib/private/TextToImage/Db/TaskMapper.php b/lib/private/TextToImage/Db/TaskMapper.php new file mode 100644 index 00000000000..68fdd8f40de --- /dev/null +++ b/lib/private/TextToImage/Db/TaskMapper.php @@ -0,0 +1,127 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + +namespace OC\TextToImage\Db; + +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\Entity; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\AppFramework\Db\QBMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\DB\Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; + +/** + * @extends QBMapper<Task> + */ +class TaskMapper extends QBMapper { + public function __construct( + IDBConnection $db, + private ITimeFactory $timeFactory, + ) { + parent::__construct($db, 'text2image_tasks', Task::class); + } + + /** + * @param int $id + * @return Task + * @throws Exception + * @throws DoesNotExistException + * @throws MultipleObjectsReturnedException + */ + public function find(int $id): Task { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); + return $this->findEntity($qb); + } + + /** + * @param int $id + * @param string|null $userId + * @return Task + * @throws DoesNotExistException + * @throws Exception + * @throws MultipleObjectsReturnedException + */ + public function findByIdAndUser(int $id, ?string $userId): Task { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->eq('id', $qb->createPositionalParameter($id))); + if ($userId === null) { + $qb->andWhere($qb->expr()->isNull('user_id')); + } else { + $qb->andWhere($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))); + } + return $this->findEntity($qb); + } + + /** + * @param string $userId + * @param string $appId + * @param string|null $identifier + * @return array + * @throws Exception + */ + public function findUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array { + $qb = $this->db->getQueryBuilder(); + $qb->select(Task::$columns) + ->from($this->tableName) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId))) + ->andWhere($qb->expr()->eq('app_id', $qb->createPositionalParameter($appId))); + if ($identifier !== null) { + $qb->andWhere($qb->expr()->eq('identifier', $qb->createPositionalParameter($identifier))); + } + return $this->findEntities($qb); + } + + /** + * @param int $timeout + * @return Task[] the deleted tasks + * @throws Exception + */ + public function deleteOlderThan(int $timeout): array { + $datetime = $this->timeFactory->getDateTime(); + $datetime->sub(new \DateInterval('PT'.$timeout.'S')); + $qb = $this->db->getQueryBuilder(); + $qb->select('*') + ->from($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($datetime, IQueryBuilder::PARAM_DATE))); + $deletedTasks = $this->findEntities($qb); + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->tableName) + ->where($qb->expr()->lt('last_updated', $qb->createPositionalParameter($datetime, IQueryBuilder::PARAM_DATE))); + $qb->executeStatement(); + return $deletedTasks; + } + + public function update(Entity $entity): Entity { + $entity->setLastUpdated($this->timeFactory->getDateTime()); + return parent::update($entity); + } +} diff --git a/lib/private/TextToImage/Manager.php b/lib/private/TextToImage/Manager.php new file mode 100644 index 00000000000..1553fed7545 --- /dev/null +++ b/lib/private/TextToImage/Manager.php @@ -0,0 +1,334 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + +namespace OC\TextToImage; + +use OC\AppFramework\Bootstrap\Coordinator; +use OC\TextToImage\Db\Task as DbTask; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IConfig; +use OCP\TextToImage\Exception\TaskFailureException; +use OCP\TextToImage\Exception\TaskNotFoundException; +use OCP\TextToImage\IManager; +use OCP\TextToImage\Task; +use OC\TextToImage\Db\TaskMapper; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\BackgroundJob\IJobList; +use OCP\DB\Exception; +use OCP\IServerContainer; +use OCP\TextToImage\IProvider; +use OCP\PreConditionNotMetException; +use Psr\Log\LoggerInterface; +use RuntimeException; +use Throwable; + +class Manager implements IManager { + /** @var ?IProvider[] */ + private ?array $providers = null; + private IAppData $appData; + + public function __construct( + private IServerContainer $serverContainer, + private Coordinator $coordinator, + private LoggerInterface $logger, + private IJobList $jobList, + private TaskMapper $taskMapper, + private IConfig $config, + IAppDataFactory $appDataFactory, + ) { + $this->appData = $appDataFactory->get('core'); + } + + /** + * @inerhitDocs + */ + public function getProviders(): array { + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + return []; + } + + if ($this->providers !== null) { + return $this->providers; + } + + $this->providers = []; + + foreach ($context->getTextToImageProviders() as $providerServiceRegistration) { + $class = $providerServiceRegistration->getService(); + try { + $this->providers[$class] = $this->serverContainer->get($class); + } catch (Throwable $e) { + $this->logger->error('Failed to load Text to image provider ' . $class, [ + 'exception' => $e, + ]); + } + } + + return $this->providers; + } + + /** + * @inheritDoc + */ + public function hasProviders(): bool { + $context = $this->coordinator->getRegistrationContext(); + if ($context === null) { + return false; + } + return count($context->getTextToImageProviders()) > 0; + } + + /** + * @inheritDoc + */ + public function runTask(Task $task): void { + $this->logger->debug('Running TextToImage Task'); + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No text to image provider is installed that can handle this task'); + } + $providers = $this->getProviders(); + + $json = $this->config->getAppValue('core', 'ai.text2image_provider', ''); + if ($json !== '') { + try { + $className = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + $provider = current(array_filter($providers, fn ($provider) => $provider::class === $className)); + if ($provider !== false) { + $providers = [$provider]; + } + } catch (\JsonException $e) { + $this->logger->warning('Failed to decode Text2Image setting `ai.text2image_provider`', ['exception' => $e]); + } + } + + foreach ($providers as $provider) { + $this->logger->debug('Trying to run Text2Image provider '.$provider::class); + try { + $task->setStatus(Task::STATUS_RUNNING); + if ($task->getId() === null) { + $this->logger->debug('Inserting Text2Image task into DB'); + $taskEntity = $this->taskMapper->insert(DbTask::fromPublicTask($task)); + $task->setId($taskEntity->getId()); + } else { + $this->logger->debug('Updating Text2Image task in DB'); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + } + try { + $folder = $this->appData->getFolder('text2image'); + } catch(NotFoundException) { + $this->logger->debug('Creating folder in appdata for Text2Image results'); + $folder = $this->appData->newFolder('text2image'); + } + try { + $folder = $folder->getFolder((string) $task->getId()); + } catch(NotFoundException) { + $this->logger->debug('Creating new folder in appdata Text2Image results folder'); + $folder = $folder->newFolder((string) $task->getId()); + } + $this->logger->debug('Creating result files for Text2Image task'); + $resources = []; + $files = []; + for ($i = 0; $i < $task->getNumberOfImages(); $i++) { + $file = $folder->newFile((string) $i); + $files[] = $file; + $resource = $file->write(); + if ($resource !== false && $resource !== true && is_resource($resource)) { + $resources[] = $resource; + } else { + throw new RuntimeException('Text2Image generation using provider "' . $provider->getName() . '" failed: Couldn\'t open file to write.'); + } + } + $this->logger->debug('Calling Text2Image provider\'s generate method'); + $provider->generate($task->getInput(), $resources); + for ($i = 0; $i < $task->getNumberOfImages(); $i++) { + if (is_resource($resources[$i])) { + // If $resource hasn't been closed yet, we'll do that here + fclose($resources[$i]); + } + } + $task->setStatus(Task::STATUS_SUCCESSFUL); + $this->logger->debug('Updating Text2Image task in DB'); + $this->taskMapper->update(DbTask::fromPublicTask($task)); + return; + } catch (\RuntimeException|\Throwable $e) { + for ($i = 0; $i < $task->getNumberOfImages(); $i++) { + if (isset($files, $files[$i])) { + try { + $files[$i]->delete(); + } catch(NotPermittedException $e) { + $this->logger->warning('Failed to clean up Text2Image result file after error', ['exception' => $e]); + } + } + } + + $this->logger->info('Text2Image generation using provider "' . $provider->getName() . '" failed', ['exception' => $e]); + $task->setStatus(Task::STATUS_FAILED); + try { + $this->taskMapper->update(DbTask::fromPublicTask($task)); + } catch (Exception $e) { + $this->logger->warning('Failed to update database after Text2Image error', ['exception' => $e]); + } + throw new TaskFailureException('Text2Image generation using provider "' . $provider->getName() . '" failed: ' . $e->getMessage(), 0, $e); + } + } + + $task->setStatus(Task::STATUS_FAILED); + try { + $this->taskMapper->update(DbTask::fromPublicTask($task)); + } catch (Exception $e) { + $this->logger->warning('Failed to update database after Text2Image error', ['exception' => $e]); + } + throw new TaskFailureException('Could not run task'); + } + + /** + * @inheritDoc + */ + public function scheduleTask(Task $task): void { + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No text to image provider is installed that can handle this task'); + } + $this->logger->debug('Scheduling Text2Image Task'); + $task->setStatus(Task::STATUS_SCHEDULED); + $taskEntity = DbTask::fromPublicTask($task); + $this->taskMapper->insert($taskEntity); + $task->setId($taskEntity->getId()); + $this->jobList->add(TaskBackgroundJob::class, [ + 'taskId' => $task->getId() + ]); + } + + /** + * @inheritDoc + */ + public function runOrScheduleTask(Task $task) : void { + if (!$this->hasProviders()) { + throw new PreConditionNotMetException('No text to image provider is installed that can handle this task'); + } + $providers = $this->getProviders(); + + $json = $this->config->getAppValue('core', 'ai.text2image_provider', ''); + if ($json !== '') { + try { + $id = json_decode($json, true, 512, JSON_THROW_ON_ERROR); + $provider = current(array_filter($providers, fn ($provider) => $provider->getId() === $id)); + if ($provider !== false) { + $providers = [$provider]; + } + } catch (\JsonException $e) { + $this->logger->warning('Failed to decode Text2Image setting `ai.text2image_provider`', ['exception' => $e]); + } + } + $maxExecutionTime = (int) ini_get('max_execution_time'); + // Offload the tttttttask to a background job if the expected runtime of the likely provider is longer than 80% of our max execution time + if ($providers[0]->getExpectedRuntime() > $maxExecutionTime * 0.8) { + $this->scheduleTask($task); + return; + } + $this->runTask($task); + } + + /** + * @inheritDoc + */ + public function deleteTask(Task $task): void { + $taskEntity = DbTask::fromPublicTask($task); + $this->taskMapper->delete($taskEntity); + $this->jobList->remove(TaskBackgroundJob::class, [ + 'taskId' => $task->getId() + ]); + } + + /** + * Get a task from its id + * + * @param int $id The id of the task + * @return Task + * @throws RuntimeException If the query failed + * @throws TaskNotFoundException If the task could not be found + */ + public function getTask(int $id): Task { + try { + $taskEntity = $this->taskMapper->find($id); + return $taskEntity->toPublicTask(); + } catch (DoesNotExistException $e) { + throw new TaskNotFoundException('Could not find task with the provided id'); + } catch (MultipleObjectsReturnedException $e) { + throw new RuntimeException('Could not uniquely identify task with given id', 0, $e); + } catch (Exception $e) { + throw new RuntimeException('Failure while trying to find task by id: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get a task from its user id and task id + * If userId is null, this can only get a task that was scheduled anonymously + * + * @param int $id The id of the task + * @param string|null $userId The user id that scheduled the task + * @return Task + * @throws RuntimeException If the query failed + * @throws TaskNotFoundException If the task could not be found + */ + public function getUserTask(int $id, ?string $userId): Task { + try { + $taskEntity = $this->taskMapper->findByIdAndUser($id, $userId); + return $taskEntity->toPublicTask(); + } catch (DoesNotExistException $e) { + throw new TaskNotFoundException('Could not find task with the provided id and user id'); + } catch (MultipleObjectsReturnedException $e) { + throw new RuntimeException('Could not uniquely identify task with given id and user id', 0, $e); + } catch (Exception $e) { + throw new RuntimeException('Failure while trying to find task by id and user id: ' . $e->getMessage(), 0, $e); + } + } + + /** + * Get a list of tasks scheduled by a specific user for a specific app + * and optionally with a specific identifier. + * This cannot be used to get anonymously scheduled tasks + * + * @param string $userId + * @param string $appId + * @param string|null $identifier + * @return Task[] + * @throws RuntimeException + */ + public function getUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array { + try { + $taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $identifier); + return array_map(static function (DbTask $taskEntity) { + return $taskEntity->toPublicTask(); + }, $taskEntities); + } catch (Exception $e) { + throw new RuntimeException('Failure while trying to find tasks by appId and identifier: ' . $e->getMessage(), 0, $e); + } + } +} diff --git a/lib/private/TextToImage/RemoveOldTasksBackgroundJob.php b/lib/private/TextToImage/RemoveOldTasksBackgroundJob.php new file mode 100644 index 00000000000..2ecebc241bf --- /dev/null +++ b/lib/private/TextToImage/RemoveOldTasksBackgroundJob.php @@ -0,0 +1,78 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + + +namespace OC\TextToImage; + +use OC\TextToImage\Db\TaskMapper; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\TimedJob; +use OCP\DB\Exception; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\IAppData; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use Psr\Log\LoggerInterface; + +class RemoveOldTasksBackgroundJob extends TimedJob { + public const MAX_TASK_AGE_SECONDS = 60 * 50 * 24 * 7; // 1 week + + private IAppData $appData; + + public function __construct( + ITimeFactory $timeFactory, + private TaskMapper $taskMapper, + private LoggerInterface $logger, + IAppDataFactory $appDataFactory, + ) { + parent::__construct($timeFactory); + $this->appData = $appDataFactory->get('core'); + $this->setInterval(60 * 60 * 24); + } + + /** + * @param mixed $argument + * @inheritDoc + */ + protected function run($argument) { + try { + $deletedTasks = $this->taskMapper->deleteOlderThan(self::MAX_TASK_AGE_SECONDS); + $folder = $this->appData->getFolder('text2image'); + foreach ($deletedTasks as $deletedTask) { + try { + $folder->getFolder((string)$deletedTask->getId())->delete(); + } catch (NotFoundException) { + // noop + } catch (NotPermittedException $e) { + $this->logger->warning('Failed to delete stale text to image task files', ['exception' => $e]); + } + } + } catch (Exception $e) { + $this->logger->warning('Failed to delete stale text to image tasks', ['exception' => $e]); + } catch(NotFoundException) { + // noop + } + } +} diff --git a/lib/private/TextToImage/TaskBackgroundJob.php b/lib/private/TextToImage/TaskBackgroundJob.php new file mode 100644 index 00000000000..ac5cd6b59b5 --- /dev/null +++ b/lib/private/TextToImage/TaskBackgroundJob.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + + +namespace OC\TextToImage; + +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\QueuedJob; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\TextToImage\Events\TaskFailedEvent; +use OCP\TextToImage\Events\TaskSuccessfulEvent; +use OCP\TextToImage\IManager; + +class TaskBackgroundJob extends QueuedJob { + public function __construct( + ITimeFactory $timeFactory, + private IManager $text2imageManager, + private IEventDispatcher $eventDispatcher, + ) { + parent::__construct($timeFactory); + // We want to avoid overloading the machine with these jobs + // so we only allow running one job at a time + $this->setAllowParallelRuns(false); + } + + /** + * @param array{taskId: int} $argument + * @inheritDoc + */ + protected function run($argument) { + $taskId = $argument['taskId']; + $task = $this->text2imageManager->getTask($taskId); + try { + $this->text2imageManager->runTask($task); + $event = new TaskSuccessfulEvent($task); + } catch (\Throwable $e) { + $event = new TaskFailedEvent($task, $e->getMessage()); + } + $this->eventDispatcher->dispatchTyped($event); + } +} diff --git a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php index 2398c5ef931..3153b556a5c 100644 --- a/lib/public/AppFramework/Bootstrap/IRegistrationContext.php +++ b/lib/public/AppFramework/Bootstrap/IRegistrationContext.php @@ -38,6 +38,7 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Template\ICustomTemplateProvider; use OCP\IContainer; use OCP\TextProcessing\IProvider as ITextProcessingProvider; +use OCP\TextToImage\IProvider as ITextToImageProvider; use OCP\Notification\INotifier; use OCP\Preview\IProviderV2; use OCP\SpeechToText\ISpeechToTextProvider; @@ -231,6 +232,16 @@ interface IRegistrationContext { public function registerTextProcessingProvider(string $providerClass): void; /** + * Register a custom text2image provider class that provides the possibility to generate images + * through the OCP\TextToImage APIs + * + * @param string $providerClass + * @psalm-param class-string<ITextToImageProvider> $providerClass + * @since 28.0.0 + */ + public function registerTextToImageProvider(string $providerClass): void; + + /** * Register a custom template provider class that is able to inject custom templates * in addition to the user defined ones * diff --git a/lib/public/TextToImage/Events/AbstractTextToImageEvent.php b/lib/public/TextToImage/Events/AbstractTextToImageEvent.php new file mode 100644 index 00000000000..56c68195602 --- /dev/null +++ b/lib/public/TextToImage/Events/AbstractTextToImageEvent.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\TextToImage\Events; + +use OCP\EventDispatcher\Event; +use OCP\TextToImage\Task; + +/** + * @since 28.0.0 + */ +abstract class AbstractTextToImageEvent extends Event { + /** + * @since 28.0.0 + */ + public function __construct( + private Task $task + ) { + parent::__construct(); + } + + /** + * @return Task + * @since 28.0.0 + */ + public function getTask(): Task { + return $this->task; + } +} diff --git a/lib/public/TextToImage/Events/TaskFailedEvent.php b/lib/public/TextToImage/Events/TaskFailedEvent.php new file mode 100644 index 00000000000..0d91b3a4f67 --- /dev/null +++ b/lib/public/TextToImage/Events/TaskFailedEvent.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\TextToImage\Events; + +use OCP\TextToImage\Task; + +/** + * @since 28.0.0 + */ +class TaskFailedEvent extends AbstractTextToImageEvent { + /** + * @param Task $task + * @param string $errorMessage + * @since 28.0.0 + */ + public function __construct( + Task $task, + private string $errorMessage, + ) { + parent::__construct($task); + } + + /** + * @return string + * @since 28.0.0 + */ + public function getErrorMessage(): string { + return $this->errorMessage; + } +} diff --git a/lib/public/TextToImage/Events/TaskSuccessfulEvent.php b/lib/public/TextToImage/Events/TaskSuccessfulEvent.php new file mode 100644 index 00000000000..15d263c0ff0 --- /dev/null +++ b/lib/public/TextToImage/Events/TaskSuccessfulEvent.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\TextToImage\Events; + +/** + * @since 28.0.0 + */ +class TaskSuccessfulEvent extends AbstractTextToImageEvent { +} diff --git a/lib/public/TextToImage/Exception/TaskFailureException.php b/lib/public/TextToImage/Exception/TaskFailureException.php new file mode 100644 index 00000000000..a640fdff2e8 --- /dev/null +++ b/lib/public/TextToImage/Exception/TaskFailureException.php @@ -0,0 +1,31 @@ +<?php + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\TextToImage\Exception; + +/** + * @since 28.0.0 + */ +class TaskFailureException extends TextToImageException { +} diff --git a/lib/public/TextToImage/Exception/TaskNotFoundException.php b/lib/public/TextToImage/Exception/TaskNotFoundException.php new file mode 100644 index 00000000000..bd713fe3905 --- /dev/null +++ b/lib/public/TextToImage/Exception/TaskNotFoundException.php @@ -0,0 +1,31 @@ +<?php + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\TextToImage\Exception; + +/** + * @since 28.0.0 + */ +class TaskNotFoundException extends TextToImageException { +} diff --git a/lib/public/TextToImage/Exception/TextToImageException.php b/lib/public/TextToImage/Exception/TextToImageException.php new file mode 100644 index 00000000000..3d4fd192c94 --- /dev/null +++ b/lib/public/TextToImage/Exception/TextToImageException.php @@ -0,0 +1,31 @@ +<?php + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + * + */ + +namespace OCP\TextToImage\Exception; + +/** + * @since 28.0.0 + */ +class TextToImageException extends \Exception { +} diff --git a/lib/public/TextToImage/IManager.php b/lib/public/TextToImage/IManager.php new file mode 100644 index 00000000000..30b88217690 --- /dev/null +++ b/lib/public/TextToImage/IManager.php @@ -0,0 +1,116 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + + +namespace OCP\TextToImage; + +use OCP\DB\Exception; +use OCP\PreConditionNotMetException; +use OCP\TextToImage\Exception\TaskFailureException; +use OCP\TextToImage\Exception\TaskNotFoundException; +use RuntimeException; + +/** + * API surface for apps interacting with and making use of TextToImage providers + * without knowing which providers are installed + * @since 28.0.0 + */ +interface IManager { + /** + * @since 28.0.0 + */ + public function hasProviders(): bool; + + /** + * @since 28.0.0 + * @return IProvider[] + */ + public function getProviders(): array; + + /** + * @param Task $task The task to run + * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called + * @throws TaskFailureException If something else failed. When this is thrown task status was already set to failure. + * @since 28.0.0 + */ + public function runTask(Task $task): void; + + /** + * Will schedule a TextToImage process in the background. The result will become available + * with the \OCP\TextToImage\TaskSuccessfulEvent + * If inference fails a \OCP\TextToImage\Events\TaskFailedEvent will be dispatched instead + * + * @param Task $task The task to schedule + * @throws PreConditionNotMetException If no provider was registered but this method was still called + * @throws Exception If there was a problem inserting the task into the database + * @since 28.0.0 + */ + public function scheduleTask(Task $task) : void; + + /** + * @throws Exception if there was a problem inserting the task into the database + * @throws PreConditionNotMetException if no provider is registered + * @throws TaskFailureException If the task run failed + * @since 28.0.0 + */ + public function runOrScheduleTask(Task $task) : void; + + /** + * Delete a task that has been scheduled before + * + * @param Task $task The task to delete + * @since 28.0.0 + */ + public function deleteTask(Task $task): void; + + /** + * @param int $id The id of the task + * @return Task + * @throws RuntimeException If the query failed + * @throws TaskNotFoundException If the task could not be found + * @since 28.0.0 + */ + public function getTask(int $id): Task; + + /** + * @param int $id The id of the task + * @param string|null $userId The user id that scheduled the task + * @return Task + * @throws RuntimeException If the query failed + * @throws TaskNotFoundException If the task could not be found + * @since 28.0.0 + */ + public function getUserTask(int $id, ?string $userId): Task; + + /** + * @param ?string $userId + * @param string $appId + * @param string|null $identifier + * @return Task[] + * @since 28.0.0 + * @throws RuntimeException If the query failed + */ + public function getUserTasksByApp(?string $userId, string $appId, ?string $identifier = null): array; +} diff --git a/lib/public/TextToImage/IProvider.php b/lib/public/TextToImage/IProvider.php new file mode 100644 index 00000000000..9f2ff51f599 --- /dev/null +++ b/lib/public/TextToImage/IProvider.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + +namespace OCP\TextToImage; + +use RuntimeException; + +/** + * This is the interface that is implemented by apps that + * implement a text to image provider + * @since 28.0.0 + */ +interface IProvider { + /** + * An arbitrary unique text string identifying this provider + * @since 28.0.0 + */ + public function getId(): string; + + /** + * The localized name of this provider + * @since 28.0.0 + */ + public function getName(): string; + + /** + * Processes a text + * + * @param string $prompt The input text + * @param resource[] $resources The file resources to write the images to + * @return void + * @since 28.0.0 + * @throws RuntimeException If the text could not be processed + */ + public function generate(string $prompt, array $resources): void; + + /** + * The expected runtime for one task with this provider in seconds + * @since 28.0.0 + */ + public function getExpectedRuntime(): int; +} diff --git a/lib/public/TextToImage/Task.php b/lib/public/TextToImage/Task.php new file mode 100644 index 00000000000..e610af6aa96 --- /dev/null +++ b/lib/public/TextToImage/Task.php @@ -0,0 +1,212 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2023 Marcel Klehr <mklehr@gmx.net> + * + * @author Marcel Klehr <mklehr@gmx.net> + * + * @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 <http://www.gnu.org/licenses/>. + */ + +namespace OCP\TextToImage; + +use DateTime; +use OCP\Files\AppData\IAppDataFactory; +use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; +use OCP\IImage; +use OCP\Image; + +/** + * This is a text to image task + * + * @since 28.0.0 + */ +final class Task implements \JsonSerializable { + protected ?int $id = null; + + protected ?DateTime $completionExpectedAt = null; + + /** + * @since 28.0.0 + */ + public const STATUS_FAILED = 4; + /** + * @since 28.0.0 + */ + public const STATUS_SUCCESSFUL = 3; + /** + * @since 28.0.0 + */ + public const STATUS_RUNNING = 2; + /** + * @since 28.0.0 + */ + public const STATUS_SCHEDULED = 1; + /** + * @since 28.0.0 + */ + public const STATUS_UNKNOWN = 0; + + /** + * @psalm-var self::STATUS_* + */ + protected int $status = self::STATUS_UNKNOWN; + + /** + * @param string $input + * @param string $appId + * @param int $numberOfImages + * @param string|null $userId + * @param null|string $identifier An arbitrary identifier for this task. max length: 255 chars + * @since 28.0.0 + */ + final public function __construct( + protected string $input, + protected string $appId, + protected int $numberOfImages, + protected ?string $userId, + protected ?string $identifier = '', + ) { + } + + /** + * @return IImage[]|null + * @since 28.0.0 + */ + final public function getOutputImages(): ?array { + $appData = \OCP\Server::get(IAppDataFactory::class)->get('core'); + try { + $folder = $appData->getFolder('text2image')->getFolder((string)$this->getId()); + $images = []; + for ($i = 0; $i < $this->getNumberOfImages(); $i++) { + $image = new Image(); + $image->loadFromFileHandle($folder->getFile((string) $i)->read()); + $images[] = $image; + } + return $images; + } catch (NotFoundException|NotPermittedException) { + return null; + } + } + + /** + * @return int + * @since 28.0.0 + */ + final public function getNumberOfImages(): int { + return $this->numberOfImages; + } + + /** + * @psalm-return self::STATUS_* + * @since 28.0.0 + */ + final public function getStatus(): int { + return $this->status; + } + + /** + * @psalm-param self::STATUS_* $status + * @since 28.0.0 + */ + final public function setStatus(int $status): void { + $this->status = $status; + } + + /** + * @param ?DateTime $at + * @since 28.0.0 + */ + final public function setCompletionExpectedAt(?DateTime $at): void { + $this->completionExpectedAt = $at; + } + + /** + * @return ?DateTime + * @since 28.0.0 + */ + final public function getCompletionExpectedAt(): ?DateTime { + return $this->completionExpectedAt; + } + + /** + * @return int|null + * @since 28.0.0 + */ + final public function getId(): ?int { + return $this->id; + } + + /** + * @param int|null $id + * @since 28.0.0 + */ + final public function setId(?int $id): void { + $this->id = $id; + } + + /** + * @return string + * @since 28.0.0 + */ + final public function getInput(): string { + return $this->input; + } + + /** + * @return string + * @since 28.0.0 + */ + final public function getAppId(): string { + return $this->appId; + } + + /** + * @return null|string + * @since 28.0.0 + */ + final public function getIdentifier(): ?string { + return $this->identifier; + } + + /** + * @return string|null + * @since 28.0.0 + */ + final public function getUserId(): ?string { + return $this->userId; + } + + /** + * @psalm-return array{id: ?int, status: self::STATUS_*, userId: ?string, appId: string, input: string, identifier: ?string, numberOfImages: int, completionExpectedAt: ?int} + * @since 28.0.0 + */ + public function jsonSerialize(): array { + return [ + 'id' => $this->getId(), + 'status' => $this->getStatus(), + 'userId' => $this->getUserId(), + 'appId' => $this->getAppId(), + 'numberOfImages' => $this->getNumberOfImages(), + 'input' => $this->getInput(), + 'identifier' => $this->getIdentifier(), + 'completionExpectedAt' => $this->getCompletionExpectedAt()->getTimestamp(), + ]; + } +} |