%PDF- %PDF-
Direktori : /www/varak.net/nextcloud.varak.net/apps/app_api/lib/Service/ProvidersAI/ |
Current File : //www/varak.net/nextcloud.varak.net/apps/app_api/lib/Service/ProvidersAI/TranslationService.php |
<?php declare(strict_types=1); namespace OCA\AppAPI\Service\ProvidersAI; use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\Db\Translation\TranslationProvider; use OCA\AppAPI\Db\Translation\TranslationProviderMapper; use OCA\AppAPI\Db\Translation\TranslationQueue; use OCA\AppAPI\Db\Translation\TranslationQueueMapper; use OCA\AppAPI\PublicFunctions; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Db\MultipleObjectsReturnedException; use OCP\DB\Exception; use OCP\ICache; use OCP\ICacheFactory; use OCP\IServerContainer; use OCP\Translation\IDetectLanguageProvider; use OCP\Translation\ITranslationProviderWithId; use OCP\Translation\ITranslationProviderWithUserId; use OCP\Translation\LanguageTuple; use Psr\Log\LoggerInterface; class TranslationService { private ?ICache $cache = null; public function __construct( ICacheFactory $cacheFactory, private readonly TranslationProviderMapper $mapper, private readonly LoggerInterface $logger, ) { if ($cacheFactory->isAvailable()) { $this->cache = $cacheFactory->createDistributed(Application::APP_ID . '/ex_translation_providers'); } } public function registerTranslationProvider( string $appId, string $name, string $displayName, array $fromLanguages, array $toLanguages, string $actionHandler, string $actionDetectLang, ): ?TranslationProvider { try { $translationProvider = $this->mapper->findByAppidName($appId, $name); } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception) { $translationProvider = null; } try { $newTranslationProvider = new TranslationProvider([ 'appid' => $appId, 'name' => $name, 'display_name' => $displayName, 'from_languages' => $fromLanguages, 'to_languages' => $toLanguages, 'action_handler' => ltrim($actionHandler, '/'), 'action_detect_lang' => ltrim($actionDetectLang, '/'), ]); if ($translationProvider !== null) { $newTranslationProvider->setId($translationProvider->getId()); } $translationProvider = $this->mapper->insertOrUpdate($newTranslationProvider); $this->resetCacheEnabled(); } catch (Exception $e) { $this->logger->error( sprintf('Failed to register ExApp %s TranslationProvider %s. Error: %s', $appId, $name, $e->getMessage()), ['exception' => $e] ); return null; } return $translationProvider; } public function unregisterTranslationProvider(string $appId, string $name): ?TranslationProvider { try { $translationProvider = $this->getExAppTranslationProvider($appId, $name); if ($translationProvider !== null) { $this->mapper->delete($translationProvider); $this->resetCacheEnabled(); return $translationProvider; } } catch (Exception $e) { $this->logger->error(sprintf('Failed to unregister ExApp %s TranslationProvider %s. Error: %s', $appId, $name, $e->getMessage()), ['exception' => $e]); } return null; } /** * Get list of registered Translation providers (only for enabled ExApps) * * @return TranslationProvider[] */ public function getRegisteredTranslationProviders(): array { try { $cacheKey = '/ex_translation_providers'; $records = $this->cache?->get($cacheKey); if ($records === null) { $records = $this->mapper->findAllEnabled(); $this->cache?->set($cacheKey, $records); } return array_map(function ($record) { return new TranslationProvider($record); }, $records); } catch (Exception) { return []; } } public function getExAppTranslationProvider(string $appId, string $name): ?TranslationProvider { foreach ($this->getRegisteredTranslationProviders() as $provider) { if (($provider->getAppid() === $appId) && ($provider->getName() === $name)) { return $provider; } } try { return $this->mapper->findByAppIdName($appId, $name); } catch (DoesNotExistException|MultipleObjectsReturnedException|Exception) { return null; } } public function unregisterExAppTranslationProviders(string $appId): int { try { $result = $this->mapper->removeAllByAppId($appId); } catch (Exception) { $result = -1; } $this->resetCacheEnabled(); return $result; } public function resetCacheEnabled(): void { $this->cache?->remove('/ex_translation_providers'); } /** * Register ExApp anonymous providers implementations of ITranslationProviderWithId and ITranslationProviderWithUserId * so that they can be used as regular providers in DI container. */ public function registerExAppTranslationProviders(IRegistrationContext &$context, IServerContainer $serverContainer): void { $exAppsProviders = $this->getRegisteredTranslationProviders(); foreach ($exAppsProviders as $exAppProvider) { $class = '\\OCA\\AppAPI\\' . $exAppProvider->getAppid() . '\\' . $exAppProvider->getName(); // IDetectLanguageProvider implementation is optional if ExApp has action_detect_lang if ($exAppProvider->getActionDetectLang() !== '') { $provider = $this->getAnonymousExAppIDetectLanguageProvider($exAppProvider, $serverContainer, $class); } else { $provider = $this->getAnonymousExAppProvider($exAppProvider, $serverContainer, $class); } $context->registerService($class, function () use ($provider) { return $provider; }); $context->registerTranslationProvider($class); } } /** * @psalm-suppress UndefinedClass, MissingDependency, InvalidReturnStatement, InvalidReturnType */ private function getAnonymousExAppProvider(TranslationProvider $provider, IServerContainer $serverContainer, string $class): ?ITranslationProviderWithId { return new class($provider, $serverContainer, $class) implements ITranslationProviderWithId, ITranslationProviderWithUserId { public function __construct( TranslationProvider $provider, IServerContainer $serverContainer, string $class, ) { $this->provider = $provider; $this->serverContainer = $serverContainer; $this->class = $class; } use TranslationProviderWithIdAndUserId; }; } /** * @psalm-suppress UndefinedClass, MissingDependency, InvalidReturnStatement, InvalidReturnType */ private function getAnonymousExAppIDetectLanguageProvider(TranslationProvider $provider, IServerContainer $serverContainer, string $class): ?IDetectLanguageProvider { return new class($provider, $serverContainer, $class) implements ITranslationProviderWithId, ITranslationProviderWithUserId, IDetectLanguageProvider { public function __construct( TranslationProvider $provider, IServerContainer $serverContainer, string $class, ) { $this->provider = $provider; $this->serverContainer = $serverContainer; $this->class = $class; } use TranslationProviderWithIdAndUserId; public function detectLanguage(string $text): ?string { /** @var PublicFunctions $service */ $service = $this->serverContainer->get(PublicFunctions::class); $logger = $this->serverContainer->get(LoggerInterface::class); $route = $this->provider->getActionDetectLang(); if ($route === '') { return null; // ExApp does not support language detection } $response = $service->exAppRequest($this->provider->getAppid(), $route, $this->userId, params: [ 'text' => $text, ], ); $response = json_decode($response->getBody(), true); $logger->debug('Detect language response ' . json_encode($response)); if (isset($response['error'])) { throw new \Exception(sprintf('Failed to detect language for text: %s. Error: %s', $text, $response['error'])); } return $response['detected_lang'] ?? null; } }; } } trait TranslationProviderWithIdAndUserId { private ?string $userId; private IServerContainer $serverContainer; private TranslationProvider $provider; private string $class; public function getId(): string { return $this->class; } public function getName(): string { return $this->provider->getDisplayName(); } public function getAvailableLanguages(): array { // $fromLanguages and $toLanguages are JSON objects with lang_code => lang_label paris { "language_code": "language_label" } $fromLanguages = $this->provider->getFromLanguages(); $toLanguages = $this->provider->getToLanguages(); // Convert JSON objects to array of all possible LanguageTuple pairs $availableLanguages = []; foreach ($fromLanguages as $fromLanguageCode => $fromLanguageLabel) { foreach ($toLanguages as $toLanguageCode => $toLanguageLabel) { if ($fromLanguageCode === $toLanguageCode) { continue; } $availableLanguages[] = LanguageTuple::fromArray([ 'from' => $fromLanguageCode, 'fromLabel' => $fromLanguageLabel, 'to' => $toLanguageCode, 'toLabel' => $toLanguageLabel, ]); } } return $availableLanguages; } public function translate(?string $fromLanguage, string $toLanguage, string $text, float $maxExecutionTime = 0): string { /** @var PublicFunctions $service */ $service = $this->serverContainer->get(PublicFunctions::class); /** @var TranslationQueueMapper $mapper */ $mapper = $this->serverContainer->get(TranslationQueueMapper::class); $route = $this->provider->getActionHandler(); $queueRecord = $mapper->insert(new TranslationQueue(['created_time' => time()])); $taskId = $queueRecord->getId(); $response = $service->exAppRequest($this->provider->getAppid(), $route, $this->userId, params: [ 'from_language' => $fromLanguage, 'to_language' => $toLanguage, 'text' => $text, 'task_id' => $taskId, 'max_execution_time' => $maxExecutionTime, ], options: [ 'timeout' => $maxExecutionTime, ], ); if (is_array($response)) { $mapper->delete($mapper->getById($taskId)); throw new \Exception(sprintf('Failed to process translation task: %s:%s:%s-%s. Error: %s', $this->provider->getAppid(), $this->provider->getName(), $fromLanguage, $toLanguage, $response['error'] )); } do { $taskResults = $mapper->getById($taskId); usleep(300000); // 0.3s } while ($taskResults->getFinished() === 0); $mapper->delete($taskResults); if (!empty($taskResults->getError())) { throw new \Exception(sprintf('Translation task returned error: %s:%s:%s-%s. Error: %s', $this->provider->getAppid(), $this->provider->getName(), $fromLanguage, $toLanguage, $taskResults->getError(), )); } return $taskResults->getResult(); } public function setUserId(?string $userId): void { $this->userId = $userId; } }