Browse Source
Merge pull request #40326 from nextcloud/enh/text-to-image-api
Merge pull request #40326 from nextcloud/enh/text-to-image-api
Implement TextToImage OCP APIpull/41135/head
committed by
GitHub
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 2746 additions and 8 deletions
-
2apps/settings/lib/Controller/AISettingsController.php
-
11apps/settings/lib/Settings/Admin/ArtificialIntelligence.php
-
28apps/settings/src/components/AdminAI.vue
-
1apps/testing/composer/composer/autoload_classmap.php
-
1apps/testing/composer/composer/autoload_static.php
-
BINapps/testing/img/logo.png
-
2apps/testing/lib/AppInfo/Application.php
-
48apps/testing/lib/Provider/FakeText2ImageProvider.php
-
246core/Controller/TextToImageApiController.php
-
99core/Migrations/Version28000Date20230906104802.php
-
11core/ResponseDefinitions.php
-
876core/openapi.json
-
7core/routes.php
-
16lib/composer/composer/autoload_classmap.php
-
16lib/composer/composer/autoload_static.php
-
24lib/private/AppFramework/Bootstrap/RegistrationContext.php
-
2lib/private/Files/Node/Folder.php
-
8lib/private/Repair/AddRemoveOldTasksBackgroundJob.php
-
2lib/private/Server.php
-
117lib/private/TextToImage/Db/Task.php
-
127lib/private/TextToImage/Db/TaskMapper.php
-
334lib/private/TextToImage/Manager.php
-
78lib/private/TextToImage/RemoveOldTasksBackgroundJob.php
-
63lib/private/TextToImage/TaskBackgroundJob.php
-
11lib/public/AppFramework/Bootstrap/IRegistrationContext.php
-
52lib/public/TextToImage/Events/AbstractTextToImageEvent.php
-
54lib/public/TextToImage/Events/TaskFailedEvent.php
-
33lib/public/TextToImage/Events/TaskSuccessfulEvent.php
-
31lib/public/TextToImage/Exception/TaskFailureException.php
-
31lib/public/TextToImage/Exception/TaskNotFoundException.php
-
31lib/public/TextToImage/Exception/TextToImageException.php
-
116lib/public/TextToImage/IManager.php
-
64lib/public/TextToImage/IProvider.php
-
212lib/public/TextToImage/Task.php
After Width: 252 | Height: 120 | Size: 3.5 KiB |
@ -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'; |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
|||
} |
@ -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
|
|||
} |
|||
} |
|||
} |
@ -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); |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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; |
|||
} |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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 { |
|||
} |
@ -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; |
|||
} |
@ -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; |
|||
} |
@ -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(), |
|||
]; |
|||
} |
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue