diff --git a/appinfo/routes.php b/appinfo/routes.php index 15033ba7..e11714fe 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -8,10 +8,13 @@ ['name' => 'assistant#getAssistantTaskResultPage', 'url' => '/task/view/{metaTaskId}', 'verb' => 'GET'], ['name' => 'assistant#getAssistantTask', 'url' => '/task/{metaTaskId}', 'verb' => 'GET'], + ['name' => 'assistant#getUserTasks', 'url' => '/tasks', 'verb' => 'GET'], ['name' => 'assistant#runTextProcessingTask', 'url' => '/task/run', 'verb' => 'POST'], ['name' => 'assistant#scheduleTextProcessingTask', 'url' => '/task/schedule', 'verb' => 'POST'], ['name' => 'assistant#runOrScheduleTextProcessingTask', 'url' => '/task/run-or-schedule', 'verb' => 'POST'], ['name' => 'assistant#parseTextFromFile', 'url' => '/parse-file', 'verb' => 'POST'], + ['name' => 'assistant#deleteTask', 'url' => '/task/{metaTaskId}', 'verb' => 'DELETE'], + ['name' => 'assistant#cancelTask', 'url' => '/task/cancel/{metaTaskId}', 'verb' => 'PUT'], ['name' => 'Text2Image#processPrompt', 'url' => '/i/process_prompt', 'verb' => 'POST'], ['name' => 'Text2Image#getPromptHistory', 'url' => '/i/prompt_history', 'verb' => 'GET'], diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 43af1ff6..fda929fb 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -26,6 +26,7 @@ use OCP\SpeechToText\Events\TranscriptionSuccessfulEvent; use OCP\TextProcessing\Events\TaskFailedEvent as TextTaskFailedEvent; use OCP\TextProcessing\Events\TaskSuccessfulEvent as TextTaskSuccessfulEvent; +use OCP\TextProcessing\Task as OCPTextprocessingTask; use OCP\TextToImage\Events\TaskFailedEvent as TextToImageTaskFailedEvent; use OCP\TextToImage\Events\TaskSuccessfulEvent as TextToImageTaskSuccessfulEvent; @@ -45,6 +46,19 @@ class Application extends App implements IBootstrap { public const STT_TASK_SUCCESSFUL = 1; public const STT_TASK_FAILED = -1; + public const STATUS_META_TASK_UNKNOWN = 0; + public const STATUS_META_TASK_SCHEDULED = 1; + public const STATUS_META_TASK_RUNNING = 2; + public const STATUS_META_TASK_SUCCESSFUL = 3; + public const STATUS_META_TASK_FAILED = 4; + public const TP_STATUS_TO_META_STATUS = [ + OCPTextprocessingTask::STATUS_UNKNOWN => self::STATUS_META_TASK_UNKNOWN, + OCPTextprocessingTask::STATUS_SCHEDULED => self::STATUS_META_TASK_SCHEDULED, + OCPTextprocessingTask::STATUS_RUNNING => self::STATUS_META_TASK_RUNNING, + OCPTextprocessingTask::STATUS_SUCCESSFUL => self::STATUS_META_TASK_SUCCESSFUL, + OCPTextprocessingTask::STATUS_FAILED => self::STATUS_META_TASK_FAILED, + ]; + public const TASK_CATEGORY_TEXT_GEN = 0; public const TASK_CATEGORY_TEXT_TO_IMAGE = 1; public const TASK_CATEGORY_SPEECH_TO_TEXT = 2; diff --git a/lib/Controller/AssistantController.php b/lib/Controller/AssistantController.php index fcb2fedd..398663cb 100644 --- a/lib/Controller/AssistantController.php +++ b/lib/Controller/AssistantController.php @@ -3,6 +3,8 @@ namespace OCA\TpAssistant\Controller; use OCA\TpAssistant\AppInfo\Application; +use OCA\TpAssistant\Db\MetaTask; +use OCA\TpAssistant\Db\MetaTaskMapper; use OCA\TpAssistant\Service\AssistantService; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; @@ -11,20 +13,56 @@ use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Services\IInitialState; +use OCP\DB\Exception; use OCP\IRequest; class AssistantController extends Controller { public function __construct( - string $appName, - IRequest $request, + string $appName, + IRequest $request, private AssistantService $assistantService, - private IInitialState $initialStateService, - private ?string $userId, + private MetaTaskMapper $metaTaskMapper, + private IInitialState $initialStateService, + private ?string $userId, ) { parent::__construct($appName, $request); } + /** + * @param int $metaTaskId + * @return DataResponse + */ + #[NoAdminRequired] + public function deleteTask(int $metaTaskId): DataResponse { + if ($this->userId !== null) { + try { + $this->assistantService->deleteAssistantTask($this->userId, $metaTaskId); + return new DataResponse(''); + } catch (\Exception $e) { + } + } + + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + + /** + * @param int $metaTaskId + * @return DataResponse + */ + #[NoAdminRequired] + public function cancelTask(int $metaTaskId): DataResponse { + if ($this->userId !== null) { + try { + $this->assistantService->cancelAssistantTask($this->userId, $metaTaskId); + return new DataResponse(''); + } catch (\Exception $e) { + } + } + + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + /** * @param int $metaTaskId * @return TemplateResponse @@ -59,6 +97,22 @@ public function getAssistantTask(int $metaTaskId): DataResponse { return new DataResponse('', Http::STATUS_NOT_FOUND); } + #[NoAdminRequired] + public function getUserTasks(?string $taskType = null, ?int $category = null): DataResponse { + if ($this->userId !== null) { + try { + $tasks = $this->metaTaskMapper->getUserMetaTasks($this->userId, $taskType, $category); + $serializedTasks = array_map(static function (MetaTask $task) { + return $task->jsonSerializeCc(); + }, $tasks); + return new DataResponse(['tasks' => $serializedTasks]); + } catch (Exception $e) { + return new DataResponse(['tasks' => []]); + } + } + return new DataResponse('', Http::STATUS_NOT_FOUND); + } + /** * @param array $inputs * @param string $type diff --git a/lib/Controller/SpeechToTextController.php b/lib/Controller/SpeechToTextController.php index ac8be40b..03365162 100644 --- a/lib/Controller/SpeechToTextController.php +++ b/lib/Controller/SpeechToTextController.php @@ -106,7 +106,7 @@ public function getTranscript(int $id): DataResponse { */ private function internalGetTask(int $id): MetaTask { try { - $metaTask = $this->metaTaskMapper->getMetaTaskOfUser($id, $this->userId); + $metaTask = $this->metaTaskMapper->getUserMetaTask($id, $this->userId); if($metaTask->getCategory() !== Application::TASK_CATEGORY_SPEECH_TO_TEXT) { throw new Exception('Task is not a speech to text task.', Http::STATUS_BAD_REQUEST); diff --git a/lib/Controller/Text2ImageController.php b/lib/Controller/Text2ImageController.php index 516613c5..5fdb48cf 100644 --- a/lib/Controller/Text2ImageController.php +++ b/lib/Controller/Text2ImageController.php @@ -113,11 +113,13 @@ public function getImage(string $imageGenId, int $fileNameId): DataDisplayRespon } */ - return new DataDisplayResponse( + $response = new DataDisplayResponse( $result['image'] ?? '', Http::STATUS_OK, ['Content-Type' => $result['content-type'] ?? 'image/jpeg'] ); + $response->cacheFor(60 * 60 * 24); + return $response; } /** diff --git a/lib/Db/MetaTaskMapper.php b/lib/Db/MetaTaskMapper.php index d846db57..3ca8a0a6 100644 --- a/lib/Db/MetaTaskMapper.php +++ b/lib/Db/MetaTaskMapper.php @@ -120,7 +120,7 @@ public function getMetaTasksByOcpTaskIdAndCategory(int $ocpTaskId, int $category * @throws Exception * @throws MultipleObjectsReturnedException */ - public function getMetaTaskOfUser(int $id, string $userId): MetaTask { + public function getUserMetaTask(int $id, string $userId): MetaTask { $qb = $this->db->getQueryBuilder(); $qb->select('*') @@ -136,10 +136,12 @@ public function getMetaTaskOfUser(int $id, string $userId): MetaTask { /** * @param string $userId + * @param string|null $taskType + * @param int|null $category * @return array * @throws Exception */ - public function getMetaTasksOfUser(string $userId): array { + public function getUserMetaTasks(string $userId, ?string $taskType = null, ?int $category = null): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') @@ -147,6 +149,17 @@ public function getMetaTasksOfUser(string $userId): array { ->where( $qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)) ); + if ($taskType !== null) { + $qb->andWhere( + $qb->expr()->eq('task_type', $qb->createNamedParameter($taskType, IQueryBuilder::PARAM_STR)) + ); + } + if ($category !== null) { + $qb->andWhere( + $qb->expr()->eq('category', $qb->createNamedParameter($category, IQueryBuilder::PARAM_INT)) + ); + } + $qb->orderBy('timestamp', 'DESC'); return $this->findEntities($qb); } diff --git a/lib/Db/Text2Image/ImageGeneration.php b/lib/Db/Text2Image/ImageGeneration.php index 1de5eb52..1f4de526 100644 --- a/lib/Db/Text2Image/ImageGeneration.php +++ b/lib/Db/Text2Image/ImageGeneration.php @@ -17,6 +17,10 @@ * @method \void setUserId(string $userId) * @method \int getTimestamp() * @method \void setTimestamp(int $timestamp) + * @method \boolean getIsGenerated() + * @method \void setIsGenerated(bool $isGenerated) + * @method \boolean getFailed() + * @method \void setFailed(bool $failed) * @method \boolean getNotifyReady() * @method \void setNotifyReady(bool $notifyReady) * @method \int getExpGenTime() @@ -67,19 +71,4 @@ public function jsonSerialize() { 'exp_gen_time' => $this->expGenTime, ]; } - - public function setIsGenerated(?bool $isGenerated): void { - $this->isGenerated = $isGenerated === true; - } - public function getIsGenerated(): bool { - return $this->isGenerated === true; - } - - public function setFailed(?bool $failed): void { - $this->failed = $failed === true; - } - - public function getFailed(): bool { - return $this->failed === true; - } } diff --git a/lib/Db/Text2Image/ImageGenerationMapper.php b/lib/Db/Text2Image/ImageGenerationMapper.php index 4a336939..adfeaf34 100644 --- a/lib/Db/Text2Image/ImageGenerationMapper.php +++ b/lib/Db/Text2Image/ImageGenerationMapper.php @@ -59,16 +59,39 @@ public function createImageGeneration( string $imageGenId, string $prompt = '', string $userId = '', ?int $expCompletionTime = null, bool $notifyReady = false ): ImageGeneration { - $imageGeneration = new ImageGeneration(); - $imageGeneration->setImageGenId($imageGenId); - $imageGeneration->setTimestamp((new DateTime())->getTimestamp()); - $imageGeneration->setPrompt($prompt); - $imageGeneration->setUserId($userId); - $imageGeneration->setIsGenerated(false); - $imageGeneration->setFailed(false); - $imageGeneration->setNotifyReady($notifyReady); - $imageGeneration->setExpGenTime($expCompletionTime ?? (new DateTime())->getTimestamp()); - return $this->insert($imageGeneration); + $nowTimestamp = (new DateTime())->getTimestamp(); + + $qb = $this->db->getQueryBuilder(); + $qb->insert($this->getTableName()) + ->values([ + 'image_gen_id' => $qb->createNamedParameter($imageGenId, IQueryBuilder::PARAM_STR), + 'timestamp' => $qb->createNamedParameter($nowTimestamp, IQueryBuilder::PARAM_INT), + 'prompt' => $qb->createNamedParameter($prompt, IQueryBuilder::PARAM_STR), + 'user_id' => $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR), + 'is_generated' => $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), + 'failed' => $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL), + 'notify_ready' => $qb->createNamedParameter($notifyReady, IQueryBuilder::PARAM_BOOL), + 'exp_gen_time' => $qb->createNamedParameter($expCompletionTime ?? $nowTimestamp, IQueryBuilder::PARAM_INT), + ]); + $qb->executeStatement(); + $qb->resetQueryParts(); + + return $this->getImageGenerationOfImageGenId($imageGenId); + + // TODO figure out why inserting an entity does not work on PostgreSQL and produces: + // An exception occurred while executing a query: SQLSTATE[22P02]: Invalid text representation: 7 ERROR: invalid input syntax for type boolean: \"\"" + // could there be a bug in the query generation? + + //$imageGeneration = new ImageGeneration(); + //$imageGeneration->setImageGenId($imageGenId); + //$imageGeneration->setTimestamp((new DateTime())->getTimestamp()); + //$imageGeneration->setPrompt($prompt); + //$imageGeneration->setUserId($userId); + //$imageGeneration->setIsGenerated(false); + //$imageGeneration->setFailed(false); + //$imageGeneration->setNotifyReady($notifyReady); + //$imageGeneration->setExpGenTime($expCompletionTime ?? (new DateTime())->getTimestamp()); + //return $this->insert($imageGeneration); } /** diff --git a/lib/Listener/SpeechToText/SpeechToTextResultListener.php b/lib/Listener/SpeechToText/SpeechToTextResultListener.php index 8c76e9a5..8b70c146 100644 --- a/lib/Listener/SpeechToText/SpeechToTextResultListener.php +++ b/lib/Listener/SpeechToText/SpeechToTextResultListener.php @@ -24,7 +24,7 @@ use OCA\TpAssistant\AppInfo\Application; use OCA\TpAssistant\Db\MetaTaskMapper; -use OCA\TpAssistant\Service\AssistantService; +use OCA\TpAssistant\Service\NotificationService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\SpeechToText\Events\AbstractTranscriptionEvent; @@ -39,7 +39,7 @@ class SpeechToTextResultListener implements IEventListener { public function __construct( private LoggerInterface $logger, private MetaTaskMapper $metaTaskMapper, - private AssistantService $assistantService, + private NotificationService $notificationService, ) { } @@ -72,17 +72,18 @@ public function handle(Event $event): void { // Update the meta task with the output and new status $assistantTask->setOutput($transcript); - $assistantTask->setStatus(Application::STT_TASK_SUCCESSFUL); + $assistantTask->setStatus(Application::STATUS_META_TASK_SUCCESSFUL); $assistantTask = $this->metaTaskMapper->update($assistantTask); try { - $this->assistantService->sendNotification($assistantTask, null, null, $transcript); + $this->notificationService->sendNotification($assistantTask, null, null, $transcript); } catch (\InvalidArgumentException $e) { $this->logger->error('Failed to dispatch notification for successful transcription: ' . $e->getMessage()); } } if ($event instanceof TranscriptionFailedEvent) { + $file = $event->getFile(); $this->logger->error('Transcript generation failed: ' . $event->getErrorMessage()); $metaTasks = $this->metaTaskMapper->getMetaTasksByOcpTaskIdAndCategory($file->getId(), Application::TASK_CATEGORY_SPEECH_TO_TEXT); @@ -99,16 +100,17 @@ public function handle(Event $event): void { } if ($assistantTask === null) { - $this->logger->error('No assistant task found for speech to text result'); + $this->logger->error('No assistant task found for speech to text result (task id: ' . $file->getId() . ')'); return; } // Update the meta task with the new status - $assistantTask->setStatus(Application::STT_TASK_FAILED); + $assistantTask->setStatus(Application::STATUS_META_TASK_FAILED); + $assistantTask->setOutput($event->getErrorMessage()); $assistantTask = $this->metaTaskMapper->update($assistantTask); try { - $this->assistantService->sendNotification($assistantTask); + $this->notificationService->sendNotification($assistantTask); } catch (\InvalidArgumentException $e) { $this->logger->error('Failed to dispatch notification for failed transcription: ' . $e->getMessage()); } diff --git a/lib/Listener/TaskFailedListener.php b/lib/Listener/TaskFailedListener.php index 058d76e6..ba664860 100644 --- a/lib/Listener/TaskFailedListener.php +++ b/lib/Listener/TaskFailedListener.php @@ -5,7 +5,7 @@ use OCA\TpAssistant\AppInfo\Application; use OCA\TpAssistant\Db\MetaTaskMapper; use OCA\TpAssistant\Event\BeforeAssistantNotificationEvent; -use OCA\TpAssistant\Service\AssistantService; +use OCA\TpAssistant\Service\NotificationService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; @@ -18,7 +18,7 @@ class TaskFailedListener implements IEventListener { public function __construct( - private AssistantService $assistantService, + private NotificationService $notificationService, private IEventDispatcher $eventDispatcher, private MetaTaskMapper $metaTaskMapper, ) { @@ -58,10 +58,10 @@ public function handle(Event $event): void { } // Update task status and output: - $assistantTask->setStatus($task->getStatus()); - $assistantTask->setOutput($task->getOutput()); + $assistantTask->setStatus(Application::TP_STATUS_TO_META_STATUS[$task->getStatus()]); + $assistantTask->setOutput($event->getErrorMessage()); $assistantTask = $this->metaTaskMapper->update($assistantTask); - $this->assistantService->sendNotification($assistantTask, $notificationTarget, $notificationActionLabel); + $this->notificationService->sendNotification($assistantTask, $notificationTarget, $notificationActionLabel); } } diff --git a/lib/Listener/TaskSuccessfulListener.php b/lib/Listener/TaskSuccessfulListener.php index 0bf72bc3..ff1cf219 100644 --- a/lib/Listener/TaskSuccessfulListener.php +++ b/lib/Listener/TaskSuccessfulListener.php @@ -5,7 +5,7 @@ use OCA\TpAssistant\AppInfo\Application; use OCA\TpAssistant\Db\MetaTaskMapper; use OCA\TpAssistant\Event\BeforeAssistantNotificationEvent; -use OCA\TpAssistant\Service\AssistantService; +use OCA\TpAssistant\Service\NotificationService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventDispatcher; @@ -18,7 +18,7 @@ class TaskSuccessfulListener implements IEventListener { public function __construct( - private AssistantService $assistantService, + private NotificationService $notificationService, private IEventDispatcher $eventDispatcher, private MetaTaskMapper $metaTaskMapper, ) { @@ -58,10 +58,10 @@ public function handle(Event $event): void { } // Update task status and output: - $assistantTask->setStatus($task->getStatus()); + $assistantTask->setStatus(Application::TP_STATUS_TO_META_STATUS[$task->getStatus()]); $assistantTask->setOutput($task->getOutput()); $assistantTask = $this->metaTaskMapper->update($assistantTask); - $this->assistantService->sendNotification($assistantTask, $notificationTarget, $notificationActionLabel); + $this->notificationService->sendNotification($assistantTask, $notificationTarget, $notificationActionLabel); } } diff --git a/lib/Listener/Text2Image/Text2ImageResultListener.php b/lib/Listener/Text2Image/Text2ImageResultListener.php index 2d42f7ab..5a83ca54 100644 --- a/lib/Listener/Text2Image/Text2ImageResultListener.php +++ b/lib/Listener/Text2Image/Text2ImageResultListener.php @@ -5,7 +5,7 @@ use OCA\TpAssistant\AppInfo\Application; use OCA\TpAssistant\Db\MetaTaskMapper; use OCA\TpAssistant\Db\Text2Image\ImageGenerationMapper; -use OCA\TpAssistant\Service\AssistantService; +use OCA\TpAssistant\Service\NotificationService; use OCA\TpAssistant\Service\Text2Image\Text2ImageHelperService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; @@ -24,10 +24,10 @@ class Text2ImageResultListener implements IEventListener { public function __construct( private Text2ImageHelperService $text2ImageService, - private ImageGenerationMapper $imageGenerationMapper, - private LoggerInterface $logger, - private AssistantService $assistantService, - private MetaTaskMapper $metaTaskMapper, + private ImageGenerationMapper $imageGenerationMapper, + private LoggerInterface $logger, + private NotificationService $notificationService, + private MetaTaskMapper $metaTaskMapper, ) { } @@ -57,7 +57,7 @@ public function handle(Event $event): void { $this->text2ImageService->storeImages($images, $imageGenId); - $assistantTask->setStatus(Task::STATUS_SUCCESSFUL); + $assistantTask->setStatus(Application::STATUS_META_TASK_SUCCESSFUL); $assistantTask = $this->metaTaskMapper->update($assistantTask); } @@ -66,17 +66,18 @@ public function handle(Event $event): void { $this->imageGenerationMapper->setFailed($imageGenId, true); // Update the assistant meta task status: - $assistantTask->setStatus(Task::STATUS_FAILED); + $assistantTask->setStatus(Application::STATUS_META_TASK_FAILED); + $assistantTask->setOutput($event->getErrorMessage()); $assistantTask = $this->metaTaskMapper->update($assistantTask); - $this->assistantService->sendNotification($assistantTask); + $this->notificationService->sendNotification($assistantTask); } // Only send the notification if the user enabled them for this task: try { $imageGeneration = $this->imageGenerationMapper->getImageGenerationOfImageGenId($imageGenId); if ($imageGeneration->getNotifyReady()) { - $this->assistantService->sendNotification($assistantTask); + $this->notificationService->sendNotification($assistantTask); } } catch (\OCP\Db\Exception | DoesNotExistException | MultipleObjectsReturnedException $e) { $this->logger->warning('Could not notify user of a generation (id:' . $imageGenId . ') being ready: ' . $e->getMessage()); diff --git a/lib/Service/AssistantService.php b/lib/Service/AssistantService.php index 2d524e6d..68a63307 100644 --- a/lib/Service/AssistantService.php +++ b/lib/Service/AssistantService.php @@ -4,27 +4,26 @@ require_once __DIR__ . '/../../vendor/autoload.php'; -use DateTime; +use OC\SpeechToText\TranscriptionJob; use OCA\TpAssistant\AppInfo\Application; use OCA\TpAssistant\Db\MetaTask; use OCA\TpAssistant\Db\MetaTaskMapper; -use OCA\TpAssistant\Db\Text2Image\ImageGenerationMapper; +use OCA\TpAssistant\Service\Text2Image\Text2ImageHelperService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; +use OCP\BackgroundJob\IJobList; use OCP\Common\Exception\NotFoundException; use OCP\DB\Exception; use OCP\Files\File; use OCP\Files\GenericFileException; use OCP\Files\IRootFolder; use OCP\Files\NotPermittedException; -use OCP\IURLGenerator; +use OCP\IL10N; use OCP\Lock\LockedException; -use OCP\Notification\IManager as INotificationManager; use OCP\PreConditionNotMetException; use OCP\TextProcessing\FreePromptTaskType; use OCP\TextProcessing\IManager as ITextProcessingManager; use OCP\TextProcessing\Task as TextProcessingTask; -use OCP\TextToImage\Task as TextToImageTask; use Parsedown; use PhpOffice\PhpWord\IOFactory; use Psr\Log\LoggerInterface; @@ -33,84 +32,16 @@ class AssistantService { public function __construct( - private INotificationManager $notificationManager, private ITextProcessingManager $textProcessingManager, - private MetaTaskMapper $metaTaskMapper, - private ImageGenerationMapper $imageGenerationMapper, - private LoggerInterface $logger, - private IRootFolder $storage, - private IURLGenerator $url, + private Text2ImageHelperService $text2ImageHelperService, + private MetaTaskMapper $metaTaskMapper, + private LoggerInterface $logger, + private IRootFolder $storage, + private IJobList $jobList, + private IL10N $l10n, ) { } - /** - * Send a success or failure task result notification - * - * @param MetaTask $task - * @param string|null $customTarget optional notification link target - * @param string|null $actionLabel optional label for the notification action button - * @param string|null $resultPreview - * @return void - */ - public function sendNotification(MetaTask $task, ?string $customTarget = null, ?string $actionLabel = null, ?string $resultPreview = null): void { - $manager = $this->notificationManager; - $notification = $manager->createNotification(); - - $params = [ - 'appId' => $task->getAppId(), - 'id' => $task->getId(), - 'inputs' => $task->getInputsAsArray(), - 'target' => $customTarget ?? $this->getDefaultTarget($task), - 'actionLabel' => $actionLabel, - 'result' => $resultPreview, - ]; - $params['taskTypeClass'] = $task->getTaskType(); - $params['taskCategory'] = $task->getCategory(); - - switch ($task->getCategory()) { - case Application::TASK_CATEGORY_TEXT_TO_IMAGE: - { - $taskSuccessful = $task->getStatus() === TextToImageTask::STATUS_SUCCESSFUL; - break; - } - case Application::TASK_CATEGORY_TEXT_GEN: - { - $taskSuccessful = $task->getStatus() === TextProcessingTask::STATUS_SUCCESSFUL; - break; - } - case Application::TASK_CATEGORY_SPEECH_TO_TEXT: - { - $taskSuccessful = $task->getStatus() === Application::STT_TASK_SUCCESSFUL; - break; - } - default: - { - $taskSuccessful = false; - break; - } - } - - $subject = $taskSuccessful - ? 'success' - : 'failure'; - - $objectType = $customTarget === null - ? 'task' - : 'task-with-custom-target'; - - $notification->setApp(Application::APP_ID) - ->setUser($task->getUserId()) - ->setDateTime(new DateTime()) - ->setObject($objectType, (string) ($task->getId() ?? 0)) - ->setSubject($subject, $params); - - $manager->notify($notification); - } - - private function getDefaultTarget(MetaTask $task): string { - return $this->url->linkToRouteAbsolute(Application::APP_ID . '.assistant.getAssistantTaskResultPage', ['metaTaskId' => $task->getId()]); - } - /** * @param string $writingStyle * @param string $sourceMaterial @@ -190,6 +121,77 @@ public function getAssistantTask(string $userId, int $metaTaskId): ?MetaTask { return $metaTask; } + /** + * @param string $userId + * @param int $metaTaskId + * @return void + * @throws Exception + */ + public function deleteAssistantTask(string $userId, int $metaTaskId): void { + $metaTask = $this->getAssistantTask($userId, $metaTaskId); + if ($metaTask !== null) { + $this->cancelOcpTaskOfMetaTask($userId, $metaTask); + $this->metaTaskMapper->delete($metaTask); + } + } + + /** + * @param string $userId + * @param int $metaTaskId + * @return void + * @throws Exception + */ + public function cancelAssistantTask(string $userId, int $metaTaskId): void { + $metaTask = $this->getAssistantTask($userId, $metaTaskId); + if ($metaTask !== null) { + // deal with underlying tasks + if ($metaTask->getStatus() === Application::STATUS_META_TASK_SCHEDULED) { + $this->cancelOcpTaskOfMetaTask($userId, $metaTask); + } + + $metaTask->setStatus(Application::STATUS_META_TASK_FAILED); + $metaTask->setOutput($this->l10n->t('Canceled by user')); + $this->metaTaskMapper->update($metaTask); + } + } + + private function cancelOcpTaskOfMetaTask(string $userId, MetaTask $metaTask): void { + if ($metaTask->getCategory() === Application::TASK_CATEGORY_TEXT_GEN) { + try { + $ocpTask = $this->textProcessingManager->getTask($metaTask->getOcpTaskId()); + $this->textProcessingManager->deleteTask($ocpTask); + } catch (NotFoundException $e) { + } + } elseif ($metaTask->getCategory() === Application::TASK_CATEGORY_TEXT_TO_IMAGE) { + $this->text2ImageHelperService->cancelGeneration($metaTask->getOutput(), $userId); + } elseif ($metaTask->getCategory() === Application::TASK_CATEGORY_SPEECH_TO_TEXT) { + // TODO implement task canceling in stt manager + $fileId = $metaTask->getOcpTaskId(); + $files = $this->storage->getById($fileId); + if (count($files) < 1) { + return; + } + $file = array_shift($files); + if (!$file instanceof File) { + return; + } + $owner = $file->getOwner(); + if ($owner === null) { + return; + } + $ownerId = $owner->getUID(); + $jobArguments = [ + 'fileId' => $fileId, + 'owner' => $ownerId, + 'userId' => $userId, + 'appId' => Application::APP_ID, + ]; + if ($this->jobList->has(TranscriptionJob::class, $jobArguments)) { + $this->jobList->remove(TranscriptionJob::class, $jobArguments); + } + } + } + /** * @param string $type * @param array $inputs diff --git a/lib/Service/NotificationService.php b/lib/Service/NotificationService.php new file mode 100644 index 00000000..cff326e4 --- /dev/null +++ b/lib/Service/NotificationService.php @@ -0,0 +1,65 @@ +notificationManager; + $notification = $manager->createNotification(); + + $params = [ + 'appId' => $metaTask->getAppId(), + 'id' => $metaTask->getId(), + 'inputs' => $metaTask->getInputsAsArray(), + 'target' => $customTarget ?? $this->getDefaultTarget($metaTask), + 'actionLabel' => $actionLabel, + 'result' => $resultPreview, + ]; + $params['taskTypeClass'] = $metaTask->getTaskType(); + $params['taskCategory'] = $metaTask->getCategory(); + + $taskSuccessful = $metaTask->getStatus() === Application::STATUS_META_TASK_SUCCESSFUL; + + $subject = $taskSuccessful + ? 'success' + : 'failure'; + + $objectType = $customTarget === null + ? 'task' + : 'task-with-custom-target'; + + $notification->setApp(Application::APP_ID) + ->setUser($metaTask->getUserId()) + ->setDateTime(new DateTime()) + ->setObject($objectType, (string) ($metaTask->getId() ?? 0)) + ->setSubject($subject, $params); + + $manager->notify($notification); + } + + private function getDefaultTarget(MetaTask $task): string { + return $this->url->linkToRouteAbsolute(Application::APP_ID . '.assistant.getAssistantTaskResultPage', ['metaTaskId' => $task->getId()]); + } +} diff --git a/lib/Service/SpeechToText/SpeechToTextService.php b/lib/Service/SpeechToText/SpeechToTextService.php index b7fa0d2f..8494a2e6 100644 --- a/lib/Service/SpeechToText/SpeechToTextService.php +++ b/lib/Service/SpeechToText/SpeechToTextService.php @@ -40,9 +40,9 @@ class SpeechToTextService { public function __construct( private ISpeechToTextManager $speechToTextManager, - private IRootFolder $rootFolder, - private IConfig $config, - private MetaTaskMapper $metaTaskMapper, + private IRootFolder $rootFolder, + private IConfig $config, + private MetaTaskMapper $metaTaskMapper, ) { } @@ -77,7 +77,7 @@ public function transcribeFile(string $path, ?string $userId): void { $audioFile->getId(), 'speech-to-text', Application::APP_ID, - Application::STT_TASK_SCHEDULED, + Application::STATUS_META_TASK_SCHEDULED, Application::TASK_CATEGORY_SPEECH_TO_TEXT); } @@ -106,7 +106,7 @@ public function transcribeAudio(string $tempFileLocation, ?string $userId): void $audioFile->getId(), 'speech-to-text', Application::APP_ID, - Application::STT_TASK_SCHEDULED, + Application::STATUS_META_TASK_SCHEDULED, Application::TASK_CATEGORY_SPEECH_TO_TEXT); } diff --git a/lib/Service/Text2Image/Text2ImageHelperService.php b/lib/Service/Text2Image/Text2ImageHelperService.php index c3783b65..daa2cd31 100644 --- a/lib/Service/Text2Image/Text2ImageHelperService.php +++ b/lib/Service/Text2Image/Text2ImageHelperService.php @@ -17,7 +17,7 @@ use OCA\TpAssistant\Db\Text2Image\PromptMapper; use OCA\TpAssistant\Db\Text2Image\StaleGenerationMapper; -use OCA\TpAssistant\Service\AssistantService; +use OCA\TpAssistant\Service\NotificationService; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\AppFramework\Http; @@ -42,17 +42,17 @@ class Text2ImageHelperService { private ?ISimpleFolder $imageDataFolder = null; public function __construct( - private LoggerInterface $logger, - private IManager $textToImageManager, - private PromptMapper $promptMapper, + private LoggerInterface $logger, + private IManager $textToImageManager, + private PromptMapper $promptMapper, private ImageGenerationMapper $imageGenerationMapper, - private ImageFileNameMapper $imageFileNameMapper, + private ImageFileNameMapper $imageFileNameMapper, private StaleGenerationMapper $staleGenerationMapper, - private IAppData $appData, - private IURLGenerator $urlGenerator, - private IL10N $l10n, - private AssistantService $assistantService, - private MetaTaskMapper $metaTaskMapper, + private IAppData $appData, + private IURLGenerator $urlGenerator, + private IL10N $l10n, + private NotificationService $notificationService, + private MetaTaskMapper $metaTaskMapper, ) { } @@ -588,7 +588,7 @@ public function notifyWhenReady(string $imageGenId, string $userId): void { // No need to update the output since it's already set $assistantTask = $this->metaTaskMapper->update($assistantTask); - $this->assistantService->sendNotification($assistantTask); + $this->notificationService->sendNotification($assistantTask); } } } diff --git a/psalm.xml b/psalm.xml index 5613fa39..812a7f51 100644 --- a/psalm.xml +++ b/psalm.xml @@ -49,5 +49,6 @@ + diff --git a/src/assistant.js b/src/assistant.js index 3c7b1e5f..6048d50e 100644 --- a/src/assistant.js +++ b/src/assistant.js @@ -10,10 +10,12 @@ export async function openAssistantForm(params) { return openAssistantTextProcessingForm(params) } +// TODO add param to lock on specific task type + /** * Creates an assistant modal and return a promise which provides the result * - * OCA.TpAssistant.openAssistantTextProcessingForm({ + * OCA.TPAssistant.openAssistantTextProcessingForm({ * appId: 'my_app_id', * identifier: 'my task identifier', * taskType: 'OCP\\TextProcessing\\FreePromptTaskType', @@ -95,23 +97,23 @@ export async function openAssistantTextProcessingForm({ reject(new Error('Assistant scheduling error')) }) }) - view.$on('sync-submit', (data) => { + const syncSubmit = (inputs, taskTypeId, newTaskIdentifier = '') => { view.loading = true view.showSyncTaskRunning = true - view.inputs = data.inputs - view.selectedTaskTypeId = data.selectedTaskTypeId - if (data.selectedTaskTypeId === 'speech-to-text') { - runSttTask(data.inputs).then(response => { + view.inputs = inputs + view.selectedTaskTypeId = taskTypeId + if (taskTypeId === 'speech-to-text') { + runSttTask(inputs).then(response => { view.showScheduleConfirmation = true view.loading = false view.showSyncTaskRunning = false }) return } - const runOrScheduleFunction = data.selectedTaskTypeId === 'OCP\\TextToImage\\Task' + const runOrScheduleFunction = taskTypeId === 'OCP\\TextToImage\\Task' ? runOrScheduleTtiTask : runOrScheduleTask - runOrScheduleFunction(appId, identifier, data.selectedTaskTypeId, data.inputs) + runOrScheduleFunction(appId, newTaskIdentifier, taskTypeId, inputs) .then(async (response) => { const task = response.data?.task lastTask = task @@ -141,6 +143,19 @@ export async function openAssistantTextProcessingForm({ }) .then(() => { }) + } + view.$on('sync-submit', (data) => { + syncSubmit(data.inputs, data.selectedTaskTypeId, identifier) + }) + view.$on('try-again', (task) => { + syncSubmit(task.inputs, task.taskType) + }) + view.$on('load-task', (task) => { + if (!view.loading) { + view.selectedTaskTypeId = task.taskType + view.inputs = task.inputs + view.output = task.status === STATUS.successfull ? task.output : null + } }) view.$on('cancel-sync-n-schedule', () => { cancelCurrentSyncTask() @@ -176,15 +191,15 @@ export async function runSttTask(inputs) { const { default: axios } = await import(/* webpackChunkName: "axios-lazy" */'@nextcloud/axios') const { generateUrl } = await import(/* webpackChunkName: "router-gen-lazy" */'@nextcloud/router') saveLastSelectedTaskType('speech-to-text') - if (inputs.audioData) { + if (inputs.sttMode === 'choose') { + const url = generateUrl('/apps/assistant/stt/transcribeFile') + const params = { path: inputs.audioFilePath } + return axios.post(url, params) + } else { const url = generateUrl('/apps/assistant/stt/transcribeAudio') const formData = new FormData() formData.append('audioData', inputs.audioData) return axios.post(url, formData) - } else { - const url = generateUrl('/apps/assistant/stt/transcribeFile') - const params = { path: this.audioFilePath } - return axios.post(url, params) } } @@ -452,23 +467,23 @@ export async function openAssistantTaskResult(task, useMetaTasks = false) { showError(t('assistant', 'Failed to schedule the task')) }) }) - view.$on('sync-submit', (data) => { + const syncSubmit = (inputs, taskTypeId, newTaskIdentifier = '') => { view.loading = true view.showSyncTaskRunning = true - view.inputs = data.inputs - view.selectedTaskTypeId = data.selectedTaskTypeId - if (data.selectedTaskTypeId === 'speech-to-text') { - runSttTask(data.inputs).then(response => { + view.inputs = inputs + view.selectedTaskTypeId = taskTypeId + if (taskTypeId === 'speech-to-text') { + runSttTask(inputs).then(response => { view.showScheduleConfirmation = true view.loading = false view.showSyncTaskRunning = false }) return } - const runOrScheduleFunction = data.selectedTaskTypeId === 'OCP\\TextToImage\\Task' + const runOrScheduleFunction = taskTypeId === 'OCP\\TextToImage\\Task' ? runOrScheduleTtiTask : runOrScheduleTask - runOrScheduleFunction(task.appId, task.identifier ?? '', data.selectedTaskTypeId, data.inputs) + runOrScheduleFunction(task.appId, newTaskIdentifier, taskTypeId, inputs) .then((response) => { // resolve(response.data?.task) const task = response.data?.task @@ -492,6 +507,19 @@ export async function openAssistantTaskResult(task, useMetaTasks = false) { }) .then(() => { }) + } + view.$on('sync-submit', (data) => { + syncSubmit(data.inputs, data.selectedTaskTypeId, task.identifier ?? '') + }) + view.$on('try-again', (task) => { + syncSubmit(task.inputs, task.taskType) + }) + view.$on('load-task', (task) => { + if (!view.loading) { + view.selectedTaskTypeId = task.taskType + view.inputs = task.inputs + view.output = task.status === STATUS.successfull ? task.output : null + } }) view.$on('cancel-sync-n-schedule', () => { cancelCurrentSyncTask() diff --git a/src/components/AssistantFormInputs.vue b/src/components/AssistantFormInputs.vue index d1c8cc74..1aa3b7b7 100644 --- a/src/components/AssistantFormInputs.vue +++ b/src/components/AssistantFormInputs.vue @@ -17,7 +17,10 @@ - {{ t('assistant','Choose File') }} + + {{ t('assistant','Choose file') }} - {{ t('assistant','Choose File') }} + + {{ t('assistant','Choose file') }} - {{ t('assistant','Choose File') }} + + {{ t('assistant','Choose file') }} + + diff --git a/src/components/TaskListItem.vue b/src/components/TaskListItem.vue new file mode 100644 index 00000000..7d36f7dd --- /dev/null +++ b/src/components/TaskListItem.vue @@ -0,0 +1,225 @@ + + + + + diff --git a/src/components/Text2Image/Text2ImageDisplay.vue b/src/components/Text2Image/Text2ImageDisplay.vue index a295d801..45d92ce0 100644 --- a/src/components/Text2Image/Text2ImageDisplay.vue +++ b/src/components/Text2Image/Text2ImageDisplay.vue @@ -156,6 +156,26 @@ export default { return generateUrl('/apps/assistant/i/{imageGenId}', { imageGenId: this.imageGenId }) }, }, + watch: { + imageGenId() { + this.prompt = '' + this.loadingImages = true + this.imgLoadedList = [] + this.timeUntilCompletion = null + this.failed = false + this.imageUrls = [] + this.isOwner = false + this.closed = false + this.fileVisStatusArray = [] + this.hoveredIndex = -1 + this.hovered = false + this.editModeEnabled = false + this.waitingInBg = false + + this.getImageGenInfo() + this.editModeEnabled = this.forceEditMode + }, + }, mounted() { this.getImageGenInfo() this.editModeEnabled = this.forceEditMode diff --git a/src/components/Text2Image/Text2ImageInlineDisplay.vue b/src/components/Text2Image/Text2ImageInlineDisplay.vue new file mode 100644 index 00000000..8ebf9f3d --- /dev/null +++ b/src/components/Text2Image/Text2ImageInlineDisplay.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/src/main.js b/src/main.js index 8ed46887..19a681cc 100644 --- a/src/main.js +++ b/src/main.js @@ -7,12 +7,12 @@ import { subscribe } from '@nextcloud/event-bus' import { loadState } from '@nextcloud/initial-state' /** - * - Expose OCA.TpAssistant.openTextProcessingModal to let apps use the assistant + * - Expose OCA.TPAssistant.openTextProcessingModal to let apps use the assistant * - Add a header right menu entry * - Listen to notification event */ function init() { - if (!OCA.TpAssistant) { + if (!OCA.TPAssistant) { /** * @namespace */ diff --git a/src/views/TaskResultPage.vue b/src/views/TaskResultPage.vue index 8a028eb3..5b510a86 100644 --- a/src/views/TaskResultPage.vue +++ b/src/views/TaskResultPage.vue @@ -15,11 +15,13 @@ v-else class="form" :inputs="task.inputs" - :output="task.output ?? ''" + :output="task.output" :selected-task-type-id="task.taskType" :loading="loading" @submit="onSubmit" - @sync-submit="onSyncSubmit" /> + @sync-submit="onSyncSubmit" + @try-again="onTryAgain" + @load-task="onLoadTask" /> @@ -110,21 +112,21 @@ export default { showError(t('assistant', 'Failed to schedule your task')) }) }, - onSyncSubmit(data) { + syncSubmit(inputs, taskTypeId, newTaskIdentifier = '') { this.showSyncTaskRunning = true - this.task.inputs = data.inputs - this.task.taskType = data.selectedTaskTypeId - if (data.selectedTaskTypeId === 'speech-to-text') { - runSttTask(data.inputs).then(response => { + this.task.inputs = inputs + this.task.taskType = taskTypeId + if (taskTypeId === 'speech-to-text') { + runSttTask(inputs).then(response => { this.showScheduleConfirmation = true this.showSyncTaskRunning = false }) return } - const runOrScheduleFunction = data.selectedTaskTypeId === 'OCP\\TextToImage\\Task' + const runOrScheduleFunction = taskTypeId === 'OCP\\TextToImage\\Task' ? runOrScheduleTtiTask : runOrScheduleTask - runOrScheduleFunction(this.task.appId, this.task.identifier, data.selectedTaskTypeId, data.inputs) + runOrScheduleFunction(this.task.appId, this.task.identifier, taskTypeId, inputs) .then((response) => { console.debug('Assistant SYNC result', response.data) const task = response.data?.task @@ -143,6 +145,20 @@ export default { .then(() => { }) }, + onSyncSubmit(data) { + this.syncSubmit(data.inputs, data.selectedTaskTypeId, this.task.identifier) + }, + onTryAgain(task) { + this.syncSubmit(task.inputs, task.taskType) + }, + onLoadTask(task) { + if (this.loading === false) { + this.task.taskType = task.taskType + this.task.inputs = task.inputs + this.task.status = task.status + this.task.output = task.status === STATUS.successfull ? task.output : null + } + }, }, } diff --git a/tests/stubs/oc_transcription.php b/tests/stubs/oc_transcription.php new file mode 100644 index 00000000..1e1ef641 --- /dev/null +++ b/tests/stubs/oc_transcription.php @@ -0,0 +1,11 @@ +