diff --git a/composer.json b/composer.json index 64a2209d..96efbc92 100644 --- a/composer.json +++ b/composer.json @@ -48,7 +48,7 @@ "optimize-autoloader": true, "classmap-authoritative": false, "platform": { - "php": "8.2" + "php": "8.3" } }, "extra": { diff --git a/composer.lock b/composer.lock index 1503d4e5..f7fd2f6b 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "7ccd91568a2cb120d5788be1bfe2af07", + "content-hash": "db059d652920a0c6cd9c7afda5199a69", "packages": [ { "name": "bamarni/composer-bin-plugin", @@ -200,16 +200,16 @@ "source": { "type": "git", "url": "https://github.com/nextcloud-deps/ocp.git", - "reference": "20e8343005856c8b1a5fc2b12ceae86f57c533f7" + "reference": "40842eb82f4972ef0fe465e9c03832275bb85533" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/20e8343005856c8b1a5fc2b12ceae86f57c533f7", - "reference": "20e8343005856c8b1a5fc2b12ceae86f57c533f7", + "url": "https://api.github.com/repos/nextcloud-deps/ocp/zipball/40842eb82f4972ef0fe465e9c03832275bb85533", + "reference": "40842eb82f4972ef0fe465e9c03832275bb85533", "shasum": "" }, "require": { - "php": "~8.1 || ~8.2 || ~8.3 || ~8.4 || ~8.5", + "php": "~8.3 || ~8.4 || ~8.5", "psr/clock": "^1.0", "psr/container": "^2.0.2", "psr/event-dispatcher": "^1.0", @@ -220,7 +220,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "34.0.0-dev" + "dev-master": "35.0.0-dev" } }, "notification-url": "https://packagist.org/downloads/", @@ -242,7 +242,7 @@ "issues": "https://github.com/nextcloud-deps/ocp/issues", "source": "https://github.com/nextcloud-deps/ocp/tree/master" }, - "time": "2026-05-14T02:06:02+00:00" + "time": "2026-06-13T09:58:31+00:00" }, { "name": "psr/clock", @@ -563,7 +563,7 @@ }, "platform-dev": {}, "platform-overrides": { - "php": "8.2" + "php": "8.3" }, "plugin-api-version": "2.9.0" } diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index bd8a8cad..79ae26ca 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -11,6 +11,8 @@ use OCA\OpenAi\Notification\Notifier; use OCA\OpenAi\OldProcessing\Translation\TranslationProvider as OldTranslationProvider; use OCA\OpenAi\TaskProcessing\AudioToAudioChatProvider; +use OCA\OpenAi\TaskProcessing\AudioToAudioTranslateProvider; +use OCA\OpenAi\TaskProcessing\AudioToAudioTranslateTaskType; use OCA\OpenAi\TaskProcessing\AudioToTextEnhancedProvider; use OCA\OpenAi\TaskProcessing\AudioToTextProvider; use OCA\OpenAi\TaskProcessing\ChangeToneProvider; @@ -29,7 +31,6 @@ use OCA\OpenAi\TaskProcessing\TranslateProvider; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; - use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\IAppConfig; @@ -75,7 +76,6 @@ class Application extends App implements IBootstrap { self::QUOTA_TYPE_IMAGE => 0, // 0 = unlimited self::QUOTA_TYPE_TRANSCRIPTION => 0, // 0 = unlimited self::QUOTA_TYPE_SPEECH => 0, // 0 = unlimited - ]; public const MODELS_CACHE_KEY = 'models'; @@ -103,11 +103,19 @@ public function register(IRegistrationContext $context): void { $context->registerTranslationProvider(OldTranslationProvider::class); } + $translationProviderEnabled = $this->appConfig->getValueString(Application::APP_ID, 'translation_provider_enabled', '1') === '1'; + $sttProviderEnabled = $this->appConfig->getValueString(Application::APP_ID, 'stt_provider_enabled', '1') === '1'; + $ttsProviderEnabled = $this->appConfig->getValueString(Application::APP_ID, 'tts_provider_enabled', '1') === '1'; + // Task processing - if ($this->appConfig->getValueString(Application::APP_ID, 'translation_provider_enabled', '1') === '1') { + if ($translationProviderEnabled) { $context->registerTaskProcessingProvider(TranslateProvider::class); } - if ($this->appConfig->getValueString(Application::APP_ID, 'stt_provider_enabled', '1') === '1') { + if ($translationProviderEnabled && $sttProviderEnabled && $ttsProviderEnabled) { + $context->registerTaskProcessingTaskType(AudioToAudioTranslateTaskType::class); + $context->registerTaskProcessingProvider(AudioToAudioTranslateProvider::class); + } + if ($sttProviderEnabled) { $context->registerTaskProcessingProvider(AudioToTextProvider::class); if (class_exists('OCP\\TaskProcessing\\TaskTypes\\TextToTextReformatParagraphs')) { $context->registerTaskProcessingProvider(AudioToTextEnhancedProvider::class); diff --git a/lib/Controller/ConfigController.php b/lib/Controller/ConfigController.php index 3c40d6a3..17c6d053 100644 --- a/lib/Controller/ConfigController.php +++ b/lib/Controller/ConfigController.php @@ -13,7 +13,6 @@ use OCP\AppFramework\Controller; use OCP\AppFramework\Http; use OCP\AppFramework\Http\Attribute\NoAdminRequired; - use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired; use OCP\AppFramework\Http\DataResponse; use OCP\IRequest; diff --git a/lib/Db/QuotaRuleMapper.php b/lib/Db/QuotaRuleMapper.php index 883e62b1..d0835af5 100644 --- a/lib/Db/QuotaRuleMapper.php +++ b/lib/Db/QuotaRuleMapper.php @@ -64,7 +64,6 @@ public function getRule(int $quotaType, string $userId, array $groups): QuotaRul $qb->expr()->eq('u.entity_type', $qb->createNamedParameter(EntityType::GROUP->value, IQueryBuilder::PARAM_INT)), $qb->expr()->in('u.entity_id', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY)) ), - ) )->orderBy('r.priority', 'ASC') ->setMaxResults(1); diff --git a/lib/Migration/Version030102Date20241003155512.php b/lib/Migration/Version030102Date20241003155512.php index 1ac8e59b..35a6fabc 100644 --- a/lib/Migration/Version030102Date20241003155512.php +++ b/lib/Migration/Version030102Date20241003155512.php @@ -6,6 +6,7 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\OpenAi\Migration; use Closure; diff --git a/lib/Migration/Version030103Date20241009172829.php b/lib/Migration/Version030103Date20241009172829.php index 866b06ff..3bcc311c 100644 --- a/lib/Migration/Version030103Date20241009172829.php +++ b/lib/Migration/Version030103Date20241009172829.php @@ -6,6 +6,7 @@ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\OpenAi\Migration; use Closure; diff --git a/lib/Notification/Notifier.php b/lib/Notification/Notifier.php index faaf8d49..7b27ab5a 100644 --- a/lib/Notification/Notifier.php +++ b/lib/Notification/Notifier.php @@ -12,7 +12,6 @@ use OCP\L10N\IFactory; use OCP\Notification\IAction; use OCP\Notification\INotification; - use OCP\Notification\INotifier; use OCP\Notification\UnknownNotificationException; diff --git a/lib/Service/OpenAiSettingsService.php b/lib/Service/OpenAiSettingsService.php index 26603ee8..30661239 100644 --- a/lib/Service/OpenAiSettingsService.php +++ b/lib/Service/OpenAiSettingsService.php @@ -81,7 +81,6 @@ class OpenAiSettingsService { 'stt_language' => 'string', ]; - public function __construct( private IConfig $config, private IAppConfig $appConfig, @@ -633,7 +632,6 @@ public function getUserConfig(string $userId): array { 'use_basic_auth' => $this->getUseBasicAuth(), 'is_custom_service' => $isCustomService, 'stt_language' => $this->getUserSTTLanguage($userId) - ]; } diff --git a/lib/Service/TranslateService.php b/lib/Service/TranslateService.php index f26df103..ecdb2d6a 100644 --- a/lib/Service/TranslateService.php +++ b/lib/Service/TranslateService.php @@ -10,8 +10,42 @@ namespace OCA\OpenAi\Service; use OCA\OpenAi\AppInfo\Application; +use OCP\ICacheFactory; +use Psr\Log\LoggerInterface; class TranslateService { + public const SYSTEM_PROMPT = 'You are a translations expert that ONLY outputs a valid JSON with the translated text in the following format: { "translation": "" } .'; + public const JSON_RESPONSE_FORMAT = [ + 'response_format' => [ + 'type' => 'json_schema', + 'json_schema' => [ + 'name' => 'TranslationResponse', + 'description' => 'A JSON object containing the translated text', + 'strict' => true, + 'schema' => [ + 'type' => 'object', + 'properties' => [ + 'translation' => [ + 'type' => 'string', + 'description' => 'The translated text', + ], + ], + 'required' => ['translation'], + 'additionalProperties' => false, + ], + ], + ], + ]; + + public function __construct( + private OpenAiSettingsService $openAiSettingsService, + private LoggerInterface $logger, + private OpenAiAPIService $openAiAPIService, + private ChunkService $chunkService, + private ICacheFactory $cacheFactory, + ) { + } + /** * @return array */ @@ -30,4 +64,82 @@ public static function getStaticLanguages(): array { public static function getCoreLanguagesByCode(): array { return array_column(Application::LANGUAGE_CODES_AND_ENDONYMS, 1, 0); } + + public function translate( + string $inputText, string $sourceLanguageCode, string $targetLanguageCode, string $model, ?int $maxTokens, + ?string $userId, ?callable $reportProgress = null, bool $preferStreaming = false, ?callable $reportOutput = null, + ): string { + $chunks = $this->chunkService->chunkSplitPrompt($inputText, true, $maxTokens); + $translation = ''; + $increase = 1.0 / (float)count($chunks); + $progress = 0.0; + + $coreLanguages = self::getCoreLanguagesByCode(); + + $fromLanguage = $sourceLanguageCode; + $toLanguage = $coreLanguages[$targetLanguageCode] ?? $targetLanguageCode; + + if ($sourceLanguageCode !== 'detect_language') { + $fromLanguage = $coreLanguages[$sourceLanguageCode] ?? $sourceLanguageCode; + $promptStart = 'Translate the following text from ' . $fromLanguage . ' to ' . $toLanguage . ': '; + } else { + $promptStart = 'Translate the following text to ' . $toLanguage . ': '; + } + + $cache = $this->cacheFactory->createDistributed('integration_openai'); + foreach ($chunks as $chunk) { + $progress += $increase; + $cacheKey = $sourceLanguageCode . '/' . $targetLanguageCode . '/' . md5($chunk); + + if ($cached = $cache->get($cacheKey)) { + $this->logger->debug('Using cached translation', ['cached' => $cached, 'cacheKey' => $cacheKey]); + $translation .= $cached; + if ($reportProgress !== null) { + $reportProgress($progress); + } + if ($preferStreaming && $reportOutput !== null) { + $reportOutput($translation); + } + continue; + } + $prompt = $promptStart . PHP_EOL . PHP_EOL . $chunk; + + if ($this->openAiAPIService->isUsingOpenAi() || $this->openAiSettingsService->getChatEndpointEnabled()) { + $completionsObj = $this->openAiAPIService->createChatCompletion( + $userId, $model, $prompt, TranslateService::SYSTEM_PROMPT, null, 1, $maxTokens, TranslateService::JSON_RESPONSE_FORMAT + ); + $completions = $completionsObj['messages']; + } else { + $completions = $this->openAiAPIService->createCompletion( + $userId, $prompt . PHP_EOL . TranslateService::SYSTEM_PROMPT . PHP_EOL . PHP_EOL, 1, $model, $maxTokens + ); + } + + if ($reportProgress !== null) { + $reportProgress($progress); + } + + if (count($completions) === 0) { + $this->logger->error('Empty translation response received for chunk'); + continue; + } + + $completion = array_pop($completions); + $decodedCompletion = json_decode($completion, true); + if ( + !isset($decodedCompletion['translation']) + || !is_string($decodedCompletion['translation']) + || empty($decodedCompletion['translation']) + ) { + $this->logger->error('Invalid translation response received for chunk', ['response' => $completion]); + continue; + } + $translation .= $decodedCompletion['translation']; + if ($preferStreaming && $reportOutput !== null) { + $reportOutput($translation); + } + $cache->set($cacheKey, $decodedCompletion['translation']); + } + return $translation; + } } diff --git a/lib/Service/WatermarkingService.php b/lib/Service/WatermarkingService.php index b5a1756c..5520e9da 100644 --- a/lib/Service/WatermarkingService.php +++ b/lib/Service/WatermarkingService.php @@ -123,7 +123,7 @@ public function markAudio(string $audio): string { return $newAudio; } catch (\Throwable $e) { - $this->logger->warning('Could not add AI watermark to AI generated image', ['exception' => $e]); + $this->logger->warning('Could not add AI watermark to AI generated audio', ['exception' => $e]); return $audio; } } diff --git a/lib/TaskProcessing/AnalyzeImagesProvider.php b/lib/TaskProcessing/AnalyzeImagesProvider.php index bc4bdc48..34e1dd38 100644 --- a/lib/TaskProcessing/AnalyzeImagesProvider.php +++ b/lib/TaskProcessing/AnalyzeImagesProvider.php @@ -60,7 +60,6 @@ public function getInputShapeDefaults(): array { return []; } - public function getOptionalInputShape(): array { return [ 'max_tokens' => new ShapeDescriptor( @@ -160,7 +159,6 @@ public function process( ]); } - if (!isset($input['input']) || !is_string($input['input'])) { throw new RuntimeException('Invalid prompt'); } diff --git a/lib/TaskProcessing/AudioToAudioChatProvider.php b/lib/TaskProcessing/AudioToAudioChatProvider.php index 5c5fac24..1a933a78 100644 --- a/lib/TaskProcessing/AudioToAudioChatProvider.php +++ b/lib/TaskProcessing/AudioToAudioChatProvider.php @@ -69,7 +69,6 @@ public function getInputShapeDefaults(): array { return []; } - public function getOptionalInputShape(): array { $isUsingOpenAi = $this->openAiAPIService->isUsingOpenAi(); $ois = [ @@ -199,7 +198,6 @@ public function process(?string $userId, array $input, callable $reportProgress) : $this->openAiSettingsService->getAdminDefaultCompletionModelId(); } - if (isset($input['voice']) && is_string($input['voice'])) { $outputVoice = $input['voice']; } else { diff --git a/lib/TaskProcessing/AudioToAudioTranslateProvider.php b/lib/TaskProcessing/AudioToAudioTranslateProvider.php new file mode 100644 index 00000000..954c58d9 --- /dev/null +++ b/lib/TaskProcessing/AudioToAudioTranslateProvider.php @@ -0,0 +1,288 @@ +openAiAPIService->getServiceName(Application::SERVICE_TYPE_STT); + } + + public function getTaskTypeId(): string { + return AudioToAudioTranslateTaskType::ID; + } + + public function getExpectedRuntime(): int { + return 60; + } + + public function getInputShapeEnumValues(): array { + $languages = TranslateService::getStaticLanguages(); + $languageEnumValues = array_map(static function (array $language) { + return new ShapeEnumValue($language['name'], $language['code']); + }, $languages); + $detectLanguageEnumValue = new ShapeEnumValue($this->l->t('Detect language'), 'detect_language'); + return [ + 'origin_language' => array_merge([$detectLanguageEnumValue], $languageEnumValues), + 'target_language' => $languageEnumValues, + ]; + } + + public function getInputShapeDefaults(): array { + return [ + 'origin_language' => 'detect_language', + ]; + } + + public function getOptionalInputShape(): array { + return [ + 'tts_voice' => new ShapeDescriptor( + $this->l->t('Voice'), + $this->l->t('The voice to use'), + EShapeType::Enum + ), + 'tts_model' => new ShapeDescriptor( + $this->l->t('Model'), + $this->l->t('The model used to generate the speech'), + EShapeType::Enum + ), + 'tts_speed' => new ShapeDescriptor( + $this->l->t('Speed'), + $this->openAiAPIService->isUsingOpenAi(Application::SERVICE_TYPE_TTS) + ? $this->l->t('Speech speed modifier (Valid values: 0.25-4)') + : $this->l->t('Speech speed modifier'), + EShapeType::Number + ), + ]; + } + + public function getOptionalInputShapeEnumValues(): array { + $voices = json_decode($this->appConfig->getValueString(Application::APP_ID, 'tts_voices', lazy: true)) ?: Application::DEFAULT_SPEECH_VOICES; + return [ + 'tts_voice' => array_map(function ($v) { + return new ShapeEnumValue($v, $v); + }, $voices), + 'tts_model' => $this->openAiAPIService->getModelEnumValues($this->userId, Application::SERVICE_TYPE_TTS), + ]; + } + + public function getOptionalInputShapeDefaults(): array { + $adminVoice = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_voice', lazy: true) ?: Application::DEFAULT_SPEECH_VOICE; + $adminModel = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_model_id', lazy: true) ?: Application::DEFAULT_SPEECH_MODEL_ID; + return [ + 'tts_voice' => $adminVoice, + 'tts_model' => $adminModel, + 'tts_speed' => 1, + ]; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShape(): array { + return [ + 'text_input' => new ShapeDescriptor( + $this->l->t('Audio transcription'), + $this->l->t('The transcribed audio input'), + EShapeType::Text, + ), + ]; + } + + public function getOptionalOutputShapeEnumValues(): array { + return []; + } + + public function process( + ?string $userId, array $input, callable $reportProgress, SynchronousProviderOptions $options = new SynchronousProviderOptions(), + ): array { + $includeWatermark = $options->getIncludeWatermarks(); + $reportOutput = $options->getReportIntermediateOutput(); + $preferStreaming = $options->getPreferStreaming(); + + if (!isset($input['input']) || !$input['input'] instanceof File || !$input['input']->isReadable()) { + throw new ProcessingException('Invalid input file'); + } + $inputFile = $input['input']; + + if (!isset($input['origin_language']) || !is_string($input['origin_language'])) { + throw new ProcessingException('Invalid origin_language input'); + } + if (!isset($input['target_language']) || !is_string($input['target_language'])) { + throw new ProcessingException('Invalid target_language input'); + } + + // STT + $sttModel = $this->appConfig->getValueString(Application::APP_ID, 'default_stt_model_id', Application::DEFAULT_MODEL_ID, lazy: true) ?: Application::DEFAULT_MODEL_ID; + try { + $transcription = $this->openAiAPIService->transcribeFile($userId, $inputFile, false, $sttModel, $input['origin_language']); + } catch (Exception $e) { + $this->logger->warning('Transcription failed with: ' . $e->getMessage(), ['exception' => $e]); + throw new ProcessingException( + 'Transcription failed with: ' . $e->getMessage(), + $e->getCode(), + $e, + ); + } + if (empty(trim($transcription))) { + throw new ProcessingException("Empty transcription result from {$input['origin_language']} to {$input['target_language']}"); + } + $watermarkSuffix = ''; + if ($includeWatermark) { + if ($userId !== null) { + $user = $this->userManager->getExistingUser($userId); + $lang = $this->l10nFactory->getUserLanguage($user); + $l = $this->l10nFactory->get(Application::APP_ID, $lang); + $watermarkSuffix = "\n\n" . $l->t('This was generated using Artificial Intelligence.'); + } else { + $watermarkSuffix = "\n\n" . $this->l->t('This was generated using Artificial Intelligence.'); + } + } + + $reportProgress(0.3); + + if ($preferStreaming) { + $reportOutput([ + 'text_input' => $transcription . $watermarkSuffix, + ]); + } + + // translate + $completionModel = $this->openAiAPIService->isUsingOpenAi() + ? ($this->appConfig->getValueString(Application::APP_ID, 'default_completion_model_id', Application::DEFAULT_MODEL_ID, lazy: true) ?: Application::DEFAULT_MODEL_ID) + : $this->appConfig->getValueString(Application::APP_ID, 'default_completion_model_id', lazy: true); + $maxTokens = $this->openAiSettingsService->getMaxTokens(); + + try { + $reportTranslationOutput = function (string $translationOutput) use ($reportOutput, $transcription, $watermarkSuffix) { + $reportOutput([ + 'text_input' => $transcription . $watermarkSuffix, + 'text_output' => $translationOutput, + ]); + }; + $translatedText = $this->translateService->translate( + $transcription, $input['origin_language'], $input['target_language'], + $completionModel, $maxTokens, $userId, null, + $preferStreaming, $reportTranslationOutput, + ); + + if ($preferStreaming) { + $reportOutput([ + 'text_input' => $transcription . $watermarkSuffix, + 'text_output' => $translatedText . $watermarkSuffix, + ]); + } + + if (empty($translatedText)) { + throw new ProcessingException("Empty translation result from {$input['origin_language']} to {$input['target_language']}"); + } + } catch (Exception $e) { + throw new ProcessingException( + "Failed to translate from {$input['origin_language']} to {$input['target_language']}: {$e->getMessage()}", + $e->getCode(), + $e, + ); + } + + $reportProgress(0.6); + + // TTS + $ttsPrompt = $translatedText . $watermarkSuffix; + if (isset($input['tts_model']) && is_string($input['tts_model'])) { + $ttsModel = $input['tts_model']; + } else { + $ttsModel = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_model_id', Application::DEFAULT_SPEECH_MODEL_ID, lazy: true) ?: Application::DEFAULT_SPEECH_MODEL_ID; + } + if (isset($input['tts_voice']) && is_string($input['tts_voice'])) { + $voice = $input['tts_voice']; + } else { + $voice = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_voice', Application::DEFAULT_SPEECH_VOICE, lazy: true) ?: Application::DEFAULT_SPEECH_VOICE; + } + + $speed = 1; + if (isset($input['tts_speed']) && is_numeric($input['tts_speed'])) { + $speed = $input['tts_speed']; + if ($this->openAiAPIService->isUsingOpenAi(Application::SERVICE_TYPE_TTS)) { + if ($speed > 4) { + $speed = 4; + } elseif ($speed < 0.25) { + $speed = 0.25; + } + } + } + + try { + $apiResponse = $this->openAiAPIService->requestSpeechCreation( + $userId, $ttsPrompt, $ttsModel, $voice, $speed, + ); + + if (!isset($apiResponse['body'])) { + $this->logger->warning('Text to speech generation failed: no speech returned'); + throw new ProcessingException('Text to speech generation failed: no speech returned'); + } + $translatedAudio = $includeWatermark ? $this->watermarkingService->markAudio($apiResponse['body']) : $apiResponse['body']; + } catch (Exception $e) { + $this->logger->warning('Text to speech generation failed with: ' . $e->getMessage(), ['exception' => $e]); + throw new ProcessingException( + 'Text to speech generation failed with: ' . $e->getMessage(), + $e->getCode(), + $e, + ); + } + + $reportProgress(1.0); + + // Translation + return [ + 'text_input' => $transcription . $watermarkSuffix, + 'audio_output' => $translatedAudio, + 'text_output' => $translatedText . $watermarkSuffix, + ]; + } +} diff --git a/lib/TaskProcessing/AudioToAudioTranslateTaskType.php b/lib/TaskProcessing/AudioToAudioTranslateTaskType.php new file mode 100644 index 00000000..fa7e4c19 --- /dev/null +++ b/lib/TaskProcessing/AudioToAudioTranslateTaskType.php @@ -0,0 +1,87 @@ +l->t('Translate audio'); + } + + /** + * @inheritDoc + */ + public function getDescription(): string { + return $this->l->t('Translate the input voice'); + } + + /** + * @return string + */ + public function getId(): string { + return self::ID; + } + + /** + * @return ShapeDescriptor[] + */ + public function getInputShape(): array { + return [ + 'input' => new ShapeDescriptor( + $this->l->t('Input audio'), + $this->l->t('The audio to translate'), + EShapeType::Audio, + ), + 'origin_language' => new ShapeDescriptor( + $this->l->t('Origin language'), + $this->l->t('The language of the origin audio'), + EShapeType::Enum, + ), + 'target_language' => new ShapeDescriptor( + $this->l->t('Target language'), + $this->l->t('The desired language to translate the origin audio in'), + EShapeType::Enum, + ), + ]; + } + + /** + * @return ShapeDescriptor[] + */ + public function getOutputShape(): array { + return [ + 'text_output' => new ShapeDescriptor( + $this->l->t('Text output'), + $this->l->t('The text translation'), + EShapeType::Text, + ), + 'audio_output' => new ShapeDescriptor( + $this->l->t('Audio output'), + $this->l->t('The audio translation'), + EShapeType::Audio, + ), + ]; + } +} diff --git a/lib/TaskProcessing/ContextWriteProvider.php b/lib/TaskProcessing/ContextWriteProvider.php index 8b2fd5a2..77c11480 100644 --- a/lib/TaskProcessing/ContextWriteProvider.php +++ b/lib/TaskProcessing/ContextWriteProvider.php @@ -184,6 +184,5 @@ public function process( $endTime = time(); $this->openAiAPIService->updateExpTextProcessingTime($endTime - $startTime); return ['output' => $result]; - } } diff --git a/lib/TaskProcessing/ProofreadProvider.php b/lib/TaskProcessing/ProofreadProvider.php index b035d434..7b6d9490 100644 --- a/lib/TaskProcessing/ProofreadProvider.php +++ b/lib/TaskProcessing/ProofreadProvider.php @@ -165,6 +165,5 @@ public function process(?string $userId, array $input, callable $reportProgress) $endTime = time(); $this->openAiAPIService->updateExpTextProcessingTime($endTime - $startTime); return ['output' => $result]; - } } diff --git a/lib/TaskProcessing/TextToSpeechProvider.php b/lib/TaskProcessing/TextToSpeechProvider.php index 16dd7416..ff448d5a 100644 --- a/lib/TaskProcessing/TextToSpeechProvider.php +++ b/lib/TaskProcessing/TextToSpeechProvider.php @@ -60,7 +60,6 @@ public function getInputShapeDefaults(): array { return []; } - public function getOptionalInputShape(): array { return [ 'voice' => new ShapeDescriptor( @@ -133,7 +132,6 @@ public function process(?string $userId, array $input, callable $reportProgress, $model = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_model_id', Application::DEFAULT_SPEECH_MODEL_ID, lazy: true) ?: Application::DEFAULT_SPEECH_MODEL_ID; } - if (isset($input['voice']) && is_string($input['voice'])) { $voice = $input['voice']; } else { diff --git a/lib/TaskProcessing/TranslateProvider.php b/lib/TaskProcessing/TranslateProvider.php index c82fe528..deeaf98f 100644 --- a/lib/TaskProcessing/TranslateProvider.php +++ b/lib/TaskProcessing/TranslateProvider.php @@ -11,11 +11,9 @@ use Exception; use OCA\OpenAi\AppInfo\Application; -use OCA\OpenAi\Service\ChunkService; use OCA\OpenAi\Service\OpenAiAPIService; use OCA\OpenAi\Service\OpenAiSettingsService; use OCA\OpenAi\Service\TranslateService; -use OCP\ICacheFactory; use OCP\IL10N; use OCP\TaskProcessing\EShapeType; use OCP\TaskProcessing\Exception\ProcessingException; @@ -26,40 +24,14 @@ use OCP\TaskProcessing\ShapeEnumValue; use OCP\TaskProcessing\SynchronousProviderOptions; use OCP\TaskProcessing\TaskTypes\TextToTextTranslate; -use Psr\Log\LoggerInterface; class TranslateProvider implements IProvider, ISynchronousOptionsAwareProvider { - public const SYSTEM_PROMPT = 'You are a translations expert that ONLY outputs a valid JSON with the translated text in the following format: { "translation": "" } .'; - public const JSON_RESPONSE_FORMAT = [ - 'response_format' => [ - 'type' => 'json_schema', - 'json_schema' => [ - 'name' => 'TranslationResponse', - 'description' => 'A JSON object containing the translated text', - 'strict' => true, - 'schema' => [ - 'type' => 'object', - 'properties' => [ - 'translation' => [ - 'type' => 'string', - 'description' => 'The translated text', - ], - ], - 'required' => [ 'translation' ], - 'additionalProperties' => false, - ], - ], - ], - ]; - public function __construct( private OpenAiAPIService $openAiAPIService, private OpenAiSettingsService $openAiSettingsService, private IL10N $l, - private ICacheFactory $cacheFactory, - private LoggerInterface $logger, - private ChunkService $chunkService, + private TranslateService $translateService, private ?string $userId, ) { } @@ -144,13 +116,7 @@ public function process( ): array { $reportOutput = $options->getReportIntermediateOutput(); $preferStreaming = $options->getPreferStreaming(); - /* - foreach (range(1, 20) as $i) { - $reportProgress($i / 100 * 5); - error_log('aa ' . ($i / 100 * 5)); - sleep(1); - } - */ + $startTime = time(); if (isset($input['model']) && is_string($input['model'])) { $model = $input['model']; @@ -171,83 +137,29 @@ public function process( $maxTokens = $input['max_tokens']; } - $chunks = $this->chunkService->chunkSplitPrompt($inputText, true, $maxTokens); - $result = ''; - $increase = 1.0 / (float)count($chunks); - $progress = 0.0; - try { - $coreLanguages = TranslateService::getCoreLanguagesByCode(); - - $fromLanguage = $input['origin_language']; - $toLanguage = $coreLanguages[$input['target_language']] ?? $input['target_language']; - - if ($input['origin_language'] !== 'detect_language') { - $fromLanguage = $coreLanguages[$input['origin_language']] ?? $input['origin_language']; - $promptStart = 'Translate the following text from ' . $fromLanguage . ' to ' . $toLanguage . ': '; - } else { - $promptStart = 'Translate the following text to ' . $toLanguage . ': '; - } - - foreach ($chunks as $chunk) { - $progress += $increase; - $cacheKey = ($input['origin_language'] ?? '') . '/' . $input['target_language'] . '/' . md5($chunk); + $coreLanguages = TranslateService::getCoreLanguagesByCode(); + $fromLanguage = $input['origin_language']; + $toLanguage = $coreLanguages[$input['target_language']] ?? $input['target_language']; - $cache = $this->cacheFactory->createDistributed('integration_openai'); - if ($cached = $cache->get($cacheKey)) { - $this->logger->debug('Using cached translation', ['cached' => $cached, 'cacheKey' => $cacheKey]); - $result .= $cached; - $reportProgress($progress); - if ($preferStreaming) { - $reportOutput(['output' => $result]); - } - continue; - } - $prompt = $promptStart . PHP_EOL . PHP_EOL . $chunk; - - if ($this->openAiAPIService->isUsingOpenAi() || $this->openAiSettingsService->getChatEndpointEnabled()) { - $completionsObj = $this->openAiAPIService->createChatCompletion( - $userId, $model, $prompt, self::SYSTEM_PROMPT, null, 1, $maxTokens, self::JSON_RESPONSE_FORMAT - ); - $completions = $completionsObj['messages']; - } else { - $completions = $this->openAiAPIService->createCompletion( - $userId, $prompt . PHP_EOL . self::SYSTEM_PROMPT . PHP_EOL . PHP_EOL, 1, $model, $maxTokens - ); - } - - $reportProgress($progress); - - if (count($completions) === 0) { - $this->logger->error('Empty translation response received for chunk'); - continue; - } - - $completion = array_pop($completions); - $decodedCompletion = json_decode($completion, true); - if ( - !isset($decodedCompletion['translation']) - || !is_string($decodedCompletion['translation']) - || empty($decodedCompletion['translation']) - ) { - $this->logger->error('Invalid translation response received for chunk', ['response' => $completion]); - continue; - } - $result .= $decodedCompletion['translation']; - if ($preferStreaming) { - $reportOutput(['output' => $result]); - } - $cache->set($cacheKey, $decodedCompletion['translation']); - continue; - } + try { + $reportTranslationOutput = function (string $translationOutput) use ($reportOutput) { + $reportOutput([ + 'output' => $translationOutput, + ]); + }; + $translation = $this->translateService->translate( + $inputText, $input['origin_language'] ?? '', $input['target_language'] ?? '', + $model, $maxTokens, $userId, $reportProgress, + $preferStreaming, $reportTranslationOutput, + ); $endTime = time(); $this->openAiAPIService->updateExpTextProcessingTime($endTime - $startTime); - if (empty(trim($result))) { + if (empty(trim($translation))) { throw new ProcessingException("Empty translation result from {$fromLanguage} to {$toLanguage}"); } - return ['output' => trim($result)]; - + return ['output' => trim($translation)]; } catch (Exception $e) { throw new ProcessingException( "Failed to translate from {$fromLanguage} to {$toLanguage}: {$e->getMessage()}", diff --git a/psalm.xml b/psalm.xml index 297e8a84..8f57e3d5 100644 --- a/psalm.xml +++ b/psalm.xml @@ -10,7 +10,7 @@ findUnusedCode="false" resolveFromConfigFile="true" ensureOverrideAttribute="false" - phpVersion="8.2" + phpVersion="8.3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="https://getpsalm.org/schema/config" xsi:schemaLocation="https://getpsalm.org/schema/config vendor-bin/psalm/vendor/vimeo/psalm/config.xsd" diff --git a/scoper.inc.php b/scoper.inc.php index 252d5675..02aadc3a 100644 --- a/scoper.inc.php +++ b/scoper.inc.php @@ -5,7 +5,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ - declare(strict_types=1); // scoper.inc.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php index b586c0b9..7c660904 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -7,7 +7,6 @@ declare(strict_types=1); - use OCP\App\IAppManager; use OCP\Server; diff --git a/tests/stubs/ocp_task_processing_isynchronous_options_provider.php b/tests/stubs/ocp_task_processing_isynchronous_options_provider.php index 1ab8dd77..7fb14c27 100644 --- a/tests/stubs/ocp_task_processing_isynchronous_options_provider.php +++ b/tests/stubs/ocp_task_processing_isynchronous_options_provider.php @@ -12,6 +12,7 @@ * implement a task processing provider * @since 35.0.0 */ + namespace OCP\TaskProcessing; interface ISynchronousOptionsAwareProvider extends IProvider, ISynchronousProvider { diff --git a/tests/stubs/ocp_task_processing_synchronous_provider_options.php b/tests/stubs/ocp_task_processing_synchronous_provider_options.php index 9fc2aa6c..244186e5 100644 --- a/tests/stubs/ocp_task_processing_synchronous_provider_options.php +++ b/tests/stubs/ocp_task_processing_synchronous_provider_options.php @@ -4,6 +4,7 @@ * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCP\TaskProcessing; /** diff --git a/tests/unit/Providers/OpenAiProviderTest.php b/tests/unit/Providers/OpenAiProviderTest.php index e458fdfb..ed7018a2 100644 --- a/tests/unit/Providers/OpenAiProviderTest.php +++ b/tests/unit/Providers/OpenAiProviderTest.php @@ -19,7 +19,9 @@ use OCA\OpenAi\Service\OpenAiSettingsService; use OCA\OpenAi\Service\QuotaRuleService; use OCA\OpenAi\Service\StreamingService; +use OCA\OpenAi\Service\TranslateService; use OCA\OpenAi\Service\WatermarkingService; +use OCA\OpenAi\TaskProcessing\AudioToAudioTranslateProvider; use OCA\OpenAi\TaskProcessing\ChangeToneProvider; use OCA\OpenAi\TaskProcessing\EmojiProvider; use OCA\OpenAi\TaskProcessing\HeadlineProvider; @@ -53,6 +55,7 @@ class OpenAiProviderTest extends TestCase { private OpenAiSettingsService $openAiSettingsService; private ChunkService $chunkService; private StreamingService $streamingService; + private TranslateService $translateService; /** * @var MockObject|IClient */ @@ -99,6 +102,15 @@ protected function setUp(): void { true, ); + $this->translateService = \OCP\Server::get(TranslateService::class); + $this->translateService = new TranslateService( + $this->openAiSettingsService, + \OCP\Server::get(\Psr\Log\LoggerInterface::class), + $this->openAiApiService, + $this->chunkService, + \OCP\Server::get(ICacheFactory::class), + ); + $this->openAiSettingsService->setUserApiKey(self::TEST_USER1, 'This is a PHPUnit test API key'); } @@ -353,7 +365,6 @@ public function testEmojiProvider(): void { $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); } - public function testHeadlineProvider(): void { $headlineProvider = new HeadlineProvider( $this->openAiApiService, @@ -484,7 +495,6 @@ public function testChangeToneProvider(): void { $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); } - public function testSummaryProvider(): void { $summaryProvider = new SummaryProvider( $this->openAiApiService, @@ -627,9 +637,7 @@ public function testTranslationProvider(): void { $this->openAiApiService, $this->openAiSettingsService, $this->createMock(\OCP\IL10N::class), - $this->createMock(\OCP\ICacheFactory::class), - $this->createMock(\Psr\Log\LoggerInterface::class), - $this->chunkService, + $this->translateService, self::TEST_USER1, ); @@ -669,14 +677,14 @@ public function testTranslationProvider(): void { $options['body'] = json_encode([ 'model' => Application::DEFAULT_COMPLETION_MODEL_ID, 'messages' => [ - ['role' => 'system', 'content' => $translationProvider::SYSTEM_PROMPT], + ['role' => 'system', 'content' => TranslateService::SYSTEM_PROMPT], ['role' => 'user', 'content' => $prompt], ], 'n' => $n, 'stream' => false, 'max_completion_tokens' => Application::DEFAULT_MAX_NUM_OF_TOKENS, 'user' => self::TEST_USER1, - ...$translationProvider::JSON_RESPONSE_FORMAT, + ...TranslateService::JSON_RESPONSE_FORMAT, ]); $iResponse = $this->createMock(\OCP\Http\Client\IResponse::class); @@ -704,6 +712,119 @@ public function testTranslationProvider(): void { $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); } + public function testAudioToAudioTranslateProvider(): void { + $l10n = $this->createMock(\OCP\IL10N::class); + $l10n->method('t')->willReturnCallback(fn ($text) => $text); + + $l10nFactory = $this->createMock(\OCP\L10N\IFactory::class); + $l10nFactory->method('getUserLanguage')->willReturn('en'); + $l10nFactory->method('get')->willReturn($l10n); + + $userManager = \OCP\Server::get(\OCP\IUserManager::class); + + $audioToAudioTranslateProvider = new AudioToAudioTranslateProvider( + $this->openAiApiService, + $this->translateService, + $this->openAiSettingsService, + \OCP\Server::get(WatermarkingService::class), + $this->createMock(\Psr\Log\LoggerInterface::class), + $l10nFactory, + $l10n, + \OCP\Server::get(IAppConfig::class), + $userManager, + self::TEST_USER1, + ); + + $inputSpeech = file_get_contents(__DIR__ . '/../../res/speech.mp3'); + if (!$inputSpeech) { + throw new \RuntimeException('Could not read test resource `speech.mp3`'); + } + + $file = $this->createMock(\OCP\Files\File::class); + $file->method('isReadable')->willReturn(true); + $file->method('getContent')->willReturn($inputSpeech); + + $transcribedText = 'Hello world'; + $translatedText = 'Bonjour le monde'; + $fromLang = 'en'; + $toLang = 'fr'; + + $sttResponse = json_encode(['text' => $transcribedText]); + $translationAiContent = ['translation' => $translatedText]; + $translationResponse = '{ + "id": "chatcmpl-123", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4.1-mini", + "choices": [{ + "index": 0, + "message": { + "role": "assistant", + "content": ' . json_encode(json_encode($translationAiContent)) . ' + }, + "finish_reason": "stop" + }], + "usage": { + "prompt_tokens": 9, + "completion_tokens": 12, + "total_tokens": 21 + } + }'; + $ttsResponse = $inputSpeech; + + $sttUrl = self::OPENAI_API_BASE . 'audio/transcriptions'; + $translationUrl = self::OPENAI_API_BASE . 'chat/completions'; + $ttsUrl = self::OPENAI_API_BASE . 'audio/speech'; + + $sttMockResponse = $this->createMock(\OCP\Http\Client\IResponse::class); + $sttMockResponse->method('getBody')->willReturn($sttResponse); + $sttMockResponse->method('getStatusCode')->willReturn(200); + $sttMockResponse->method('getHeader')->with('Content-Type')->willReturn('application/json'); + + $translationMockResponse = $this->createMock(\OCP\Http\Client\IResponse::class); + $translationMockResponse->method('getBody')->willReturn($translationResponse); + $translationMockResponse->method('getStatusCode')->willReturn(200); + $translationMockResponse->method('getHeader')->with('Content-Type')->willReturn('application/json'); + + $ttsMockResponse = $this->createMock(\OCP\Http\Client\IResponse::class); + $ttsMockResponse->method('getBody')->willReturn($ttsResponse); + $ttsMockResponse->method('getStatusCode')->willReturn(200); + $ttsMockResponse->method('getHeader')->with('Content-Type')->willReturn('audio/mpeg'); + + $this->iClient->expects($this->exactly(3)) + ->method('post') + ->willReturnCallback(function ($url, $options) use ($sttUrl, $translationUrl, $ttsUrl, $sttMockResponse, $translationMockResponse, $ttsMockResponse) { + if ($url === $sttUrl) { + return $sttMockResponse; + } elseif ($url === $translationUrl) { + return $translationMockResponse; + } elseif ($url === $ttsUrl) { + return $ttsMockResponse; + } + throw new \RuntimeException('Unexpected URL: ' . $url); + }); + + $result = $audioToAudioTranslateProvider->process( + self::TEST_USER1, + [ + 'input' => $file, + 'origin_language' => $fromLang, + 'target_language' => $toLang, + ], + fn () => null, + new SynchronousProviderOptions(includeWatermarks: false, preferStreaming: false), + ); + + $this->assertArrayHasKey('text_input', $result); + $this->assertArrayHasKey('text_output', $result); + $this->assertArrayHasKey('audio_output', $result); + $this->assertEquals($transcribedText, $result['text_input']); + $this->assertEquals($translatedText, $result['text_output']); + $this->assertEquals($ttsResponse, $result['audio_output']); + + $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); + } + public function testTextToSpeechProvider(): void { $TTSProvider = new TextToSpeechProvider( $this->openAiApiService, diff --git a/tests/unit/Quota/QuotaTest.php b/tests/unit/Quota/QuotaTest.php index 68b8a0ef..f4c9cb48 100644 --- a/tests/unit/Quota/QuotaTest.php +++ b/tests/unit/Quota/QuotaTest.php @@ -77,7 +77,6 @@ protected function setUp(): void { $this->quotaRuleService = \OCP\Server::get(QuotaRuleService::class); - $this->openAiApiService = new OpenAiAPIService( \OCP\Server::get(LoggerInterface::class), $this->createMock(IL10N::class), diff --git a/tests/unit/Service/ServiceOverrideTest.php b/tests/unit/Service/ServiceOverrideTest.php index 1c0cc54e..9fc7fa72 100644 --- a/tests/unit/Service/ServiceOverrideTest.php +++ b/tests/unit/Service/ServiceOverrideTest.php @@ -225,7 +225,6 @@ public function testAudioToTextProvider(): void { $file = $this->createMock(\OCP\Files\File::class); - $inputSpeech = file_get_contents(__DIR__ . '/../../res/speech.mp3'); if (!$inputSpeech) { diff --git a/vendor-bin/php-cs-fixer/composer.lock b/vendor-bin/php-cs-fixer/composer.lock index 20e47668..007c7819 100644 --- a/vendor-bin/php-cs-fixer/composer.lock +++ b/vendor-bin/php-cs-fixer/composer.lock @@ -9,16 +9,16 @@ "packages-dev": [ { "name": "kubawerlos/php-cs-fixer-custom-fixers", - "version": "v3.36.0", + "version": "v3.37.2", "source": { "type": "git", "url": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers.git", - "reference": "e1f97f6463f0b2a22e0dd320948a04132ff9c501" + "reference": "678df979ce743466b42ddb6eea46b3f4c9a7bade" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/e1f97f6463f0b2a22e0dd320948a04132ff9c501", - "reference": "e1f97f6463f0b2a22e0dd320948a04132ff9c501", + "url": "https://api.github.com/repos/kubawerlos/php-cs-fixer-custom-fixers/zipball/678df979ce743466b42ddb6eea46b3f4c9a7bade", + "reference": "678df979ce743466b42ddb6eea46b3f4c9a7bade", "shasum": "" }, "require": { @@ -28,7 +28,7 @@ "php": "^7.4 || ^8.0" }, "require-dev": { - "phpunit/phpunit": "^9.6.24 || ^10.5.51 || ^11.5.44" + "phpunit/phpunit": "^9.6.34 || ^10.5.63 || ^11.5.55" }, "type": "library", "autoload": { @@ -49,7 +49,7 @@ "description": "A set of custom fixers for PHP CS Fixer", "support": { "issues": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/issues", - "source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.36.0" + "source": "https://github.com/kubawerlos/php-cs-fixer-custom-fixers/tree/v3.37.2" }, "funding": [ { @@ -57,20 +57,20 @@ "type": "github" } ], - "time": "2026-01-31T07:02:11+00:00" + "time": "2026-05-12T16:22:19+00:00" }, { "name": "nextcloud/coding-standard", - "version": "v1.4.0", + "version": "v1.5.0", "source": { "type": "git", "url": "https://github.com/nextcloud/coding-standard.git", - "reference": "8e06808c1423e9208d63d1bd205b9a38bd400011" + "reference": "80547a93236fbb9c783e05f0f0899043851b0dba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nextcloud/coding-standard/zipball/8e06808c1423e9208d63d1bd205b9a38bd400011", - "reference": "8e06808c1423e9208d63d1bd205b9a38bd400011", + "url": "https://api.github.com/repos/nextcloud/coding-standard/zipball/80547a93236fbb9c783e05f0f0899043851b0dba", + "reference": "80547a93236fbb9c783e05f0f0899043851b0dba", "shasum": "" }, "require": { @@ -100,22 +100,22 @@ ], "support": { "issues": "https://github.com/nextcloud/coding-standard/issues", - "source": "https://github.com/nextcloud/coding-standard/tree/v1.4.0" + "source": "https://github.com/nextcloud/coding-standard/tree/v1.5.0" }, - "time": "2025-06-19T12:27:27+00:00" + "time": "2026-05-19T18:30:09+00:00" }, { "name": "php-cs-fixer/shim", - "version": "v3.94.2", + "version": "v3.95.7", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/shim.git", - "reference": "80fd29f44a736136a2f05bae5464816a444b91d1" + "reference": "e4b5e6a96eaa05ae6519bd3a21ae06c92d712dd0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/80fd29f44a736136a2f05bae5464816a444b91d1", - "reference": "80fd29f44a736136a2f05bae5464816a444b91d1", + "url": "https://api.github.com/repos/PHP-CS-Fixer/shim/zipball/e4b5e6a96eaa05ae6519bd3a21ae06c92d712dd0", + "reference": "e4b5e6a96eaa05ae6519bd3a21ae06c92d712dd0", "shasum": "" }, "require": { @@ -152,9 +152,9 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/PHP-CS-Fixer/shim/issues", - "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.94.2" + "source": "https://github.com/PHP-CS-Fixer/shim/tree/v3.95.7" }, - "time": "2026-02-20T16:14:17+00:00" + "time": "2026-06-13T17:52:27+00:00" } ], "aliases": [], @@ -166,5 +166,5 @@ "php": "^8.1 || ^8.2 || ^8.3 || ^8.4" }, "platform-dev": {}, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/vendor-bin/php-scoper/composer.lock b/vendor-bin/php-scoper/composer.lock index 16122195..eda39488 100644 --- a/vendor-bin/php-scoper/composer.lock +++ b/vendor-bin/php-scoper/composer.lock @@ -491,16 +491,16 @@ }, { "name": "symfony/console", - "version": "v6.4.34", + "version": "v6.4.41", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad" + "reference": "d21b17ed158e79180fac3895ff751707970eeb57" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/7b1f1c37eff5910ddda2831345467e593a5120ad", - "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad", + "url": "https://api.github.com/repos/symfony/console/zipball/d21b17ed158e79180fac3895ff751707970eeb57", + "reference": "d21b17ed158e79180fac3895ff751707970eeb57", "shasum": "" }, "require": { @@ -565,7 +565,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.34" + "source": "https://github.com/symfony/console/tree/v6.4.41" }, "funding": [ { @@ -585,20 +585,20 @@ "type": "tidelift" } ], - "time": "2026-02-23T15:42:15+00:00" + "time": "2026-05-24T08:48:41+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -611,7 +611,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -636,7 +636,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -647,25 +647,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/event-dispatcher-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher-contracts.git", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", - "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", "shasum": "" }, "require": { @@ -679,7 +683,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -712,7 +716,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" }, "funding": [ { @@ -723,25 +727,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-01-05T13:30:16+00:00" }, { "name": "symfony/filesystem", - "version": "v6.4.34", + "version": "v6.4.39", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3" + "reference": "c507b077756b4e3e09adbbe7975fac81cd3722ca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/01ffe0411b842f93c571e5c391f289c3fdd498c3", - "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c507b077756b4e3e09adbbe7975fac81cd3722ca", + "reference": "c507b077756b4e3e09adbbe7975fac81cd3722ca", "shasum": "" }, "require": { @@ -778,7 +786,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.34" + "source": "https://github.com/symfony/filesystem/tree/v6.4.39" }, "funding": [ { @@ -798,7 +806,7 @@ "type": "tidelift" } ], - "time": "2026-02-24T17:51:06+00:00" + "time": "2026-05-07T13:11:42+00:00" }, { "name": "symfony/finder", @@ -870,16 +878,16 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -929,7 +937,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -949,20 +957,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -1011,7 +1019,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -1031,20 +1039,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -1096,7 +1104,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -1116,20 +1124,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -1181,7 +1189,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -1201,20 +1209,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -1232,7 +1240,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -1268,7 +1276,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -1288,20 +1296,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v6.4.34", + "version": "v6.4.39", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432" + "reference": "62e3c927de664edadb5bef260987eb047a17a113" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/2adaf4106f2ef4c67271971bde6d3fe0a6936432", - "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432", + "url": "https://api.github.com/repos/symfony/string/zipball/62e3c927de664edadb5bef260987eb047a17a113", + "reference": "62e3c927de664edadb5bef260987eb047a17a113", "shasum": "" }, "require": { @@ -1357,7 +1365,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.34" + "source": "https://github.com/symfony/string/tree/v6.4.39" }, "funding": [ { @@ -1377,7 +1385,7 @@ "type": "tidelift" } ], - "time": "2026-02-08T20:44:54+00:00" + "time": "2026-05-12T11:44:19+00:00" }, { "name": "thecodingmachine/safe", @@ -1588,5 +1596,5 @@ "platform-overrides": { "php": "8.1" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/vendor-bin/phpunit/composer.lock b/vendor-bin/phpunit/composer.lock index 56d89915..a8f555ef 100644 --- a/vendor-bin/phpunit/composer.lock +++ b/vendor-bin/phpunit/composer.lock @@ -1817,5 +1817,5 @@ "platform-overrides": { "php": "8.2" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/vendor-bin/psalm/composer.lock b/vendor-bin/psalm/composer.lock index 7fc263ba..ffa9b728 100644 --- a/vendor-bin/psalm/composer.lock +++ b/vendor-bin/psalm/composer.lock @@ -319,16 +319,16 @@ }, { "name": "amphp/parallel", - "version": "v2.3.3", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/amphp/parallel.git", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60" + "reference": "37f5b2754fadc229c00f9416bd68fb8d04529a81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/parallel/zipball/296b521137a54d3a02425b464e5aee4c93db2c60", - "reference": "296b521137a54d3a02425b464e5aee4c93db2c60", + "url": "https://api.github.com/repos/amphp/parallel/zipball/37f5b2754fadc229c00f9416bd68fb8d04529a81", + "reference": "37f5b2754fadc229c00f9416bd68fb8d04529a81", "shasum": "" }, "require": { @@ -348,7 +348,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -391,7 +391,7 @@ ], "support": { "issues": "https://github.com/amphp/parallel/issues", - "source": "https://github.com/amphp/parallel/tree/v2.3.3" + "source": "https://github.com/amphp/parallel/tree/v2.4.0" }, "funding": [ { @@ -399,7 +399,7 @@ "type": "github" } ], - "time": "2025-11-15T06:23:42+00:00" + "time": "2026-05-16T16:54:01+00:00" }, { "name": "amphp/parser", @@ -465,16 +465,16 @@ }, { "name": "amphp/pipeline", - "version": "v1.2.3", + "version": "v1.2.4", "source": { "type": "git", "url": "https://github.com/amphp/pipeline.git", - "reference": "7b52598c2e9105ebcddf247fc523161581930367" + "reference": "a044733e080940d1483f56caff0c412ad6982776" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/pipeline/zipball/7b52598c2e9105ebcddf247fc523161581930367", - "reference": "7b52598c2e9105ebcddf247fc523161581930367", + "url": "https://api.github.com/repos/amphp/pipeline/zipball/a044733e080940d1483f56caff0c412ad6982776", + "reference": "a044733e080940d1483f56caff0c412ad6982776", "shasum": "" }, "require": { @@ -486,7 +486,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.18" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -520,7 +520,7 @@ ], "support": { "issues": "https://github.com/amphp/pipeline/issues", - "source": "https://github.com/amphp/pipeline/tree/v1.2.3" + "source": "https://github.com/amphp/pipeline/tree/v1.2.4" }, "funding": [ { @@ -528,20 +528,20 @@ "type": "github" } ], - "time": "2025-03-16T16:33:53+00:00" + "time": "2026-05-06T05:37:57+00:00" }, { "name": "amphp/process", - "version": "v2.0.3", + "version": "v2.1.0", "source": { "type": "git", "url": "https://github.com/amphp/process.git", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d" + "reference": "583959df17d00304ad7b0b32285373f985935643" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/process/zipball/52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", - "reference": "52e08c09dec7511d5fbc1fb00d3e4e79fc77d58d", + "url": "https://api.github.com/repos/amphp/process/zipball/583959df17d00304ad7b0b32285373f985935643", + "reference": "583959df17d00304ad7b0b32285373f985935643", "shasum": "" }, "require": { @@ -555,7 +555,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.4" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -588,7 +588,7 @@ "homepage": "https://amphp.org/process", "support": { "issues": "https://github.com/amphp/process/issues", - "source": "https://github.com/amphp/process/tree/v2.0.3" + "source": "https://github.com/amphp/process/tree/v2.1.0" }, "funding": [ { @@ -596,28 +596,31 @@ "type": "github" } ], - "time": "2024-04-19T03:13:44+00:00" + "time": "2026-05-31T15:11:55+00:00" }, { "name": "amphp/serialization", - "version": "v1.0.0", + "version": "v1.1.0", "source": { "type": "git", "url": "https://github.com/amphp/serialization.git", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1" + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/serialization/zipball/693e77b2fb0b266c3c7d622317f881de44ae94a1", - "reference": "693e77b2fb0b266c3c7d622317f881de44ae94a1", + "url": "https://api.github.com/repos/amphp/serialization/zipball/fdf2834d78cebb0205fb2672676c1b1eb84371f0", + "reference": "fdf2834d78cebb0205fb2672676c1b1eb84371f0", "shasum": "" }, "require": { - "php": ">=7.1" + "php": ">=7.4" }, "require-dev": { - "amphp/php-cs-fixer-config": "dev-master", - "phpunit/phpunit": "^9 || ^8 || ^7" + "amphp/php-cs-fixer-config": "^2", + "ext-json": "*", + "ext-zlib": "*", + "phpunit/phpunit": "^9", + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -652,22 +655,28 @@ ], "support": { "issues": "https://github.com/amphp/serialization/issues", - "source": "https://github.com/amphp/serialization/tree/master" + "source": "https://github.com/amphp/serialization/tree/v1.1.0" }, - "time": "2020-03-25T21:39:07+00:00" + "funding": [ + { + "url": "https://github.com/amphp", + "type": "github" + } + ], + "time": "2026-04-05T15:59:53+00:00" }, { "name": "amphp/socket", - "version": "v2.3.1", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/amphp/socket.git", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1" + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/58e0422221825b79681b72c50c47a930be7bf1e1", - "reference": "58e0422221825b79681b72c50c47a930be7bf1e1", + "url": "https://api.github.com/repos/amphp/socket/zipball/dadb63c5d3179fd83803e29dfeac27350e619314", + "reference": "dadb63c5d3179fd83803e29dfeac27350e619314", "shasum": "" }, "require": { @@ -676,17 +685,17 @@ "amphp/dns": "^2", "ext-openssl": "*", "kelunik/certificate": "^1.1", - "league/uri": "^6.5 | ^7", - "league/uri-interfaces": "^2.3 | ^7", + "league/uri": "^7", + "league/uri-interfaces": "^7", "php": ">=8.1", - "revolt/event-loop": "^1 || ^0.2" + "revolt/event-loop": "^1" }, "require-dev": { "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "amphp/process": "^2", "phpunit/phpunit": "^9", - "psalm/phar": "5.20" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -730,7 +739,7 @@ ], "support": { "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.3.1" + "source": "https://github.com/amphp/socket/tree/v2.4.0" }, "funding": [ { @@ -738,7 +747,7 @@ "type": "github" } ], - "time": "2024-04-21T14:33:03+00:00" + "time": "2026-04-19T15:09:56+00:00" }, { "name": "amphp/sync", @@ -817,28 +826,29 @@ }, { "name": "composer/pcre", - "version": "3.3.2", + "version": "3.4.0", "source": { "type": "git", "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", + "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", "shasum": "" }, "require": { "php": "^7.4 || ^8.0" }, "conflict": { - "phpstan/phpstan": "<1.11.10" + "phpstan/phpstan": "<2.2.2" }, "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^9" }, "type": "library", "extra": { @@ -876,7 +886,7 @@ ], "support": { "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" + "source": "https://github.com/composer/pcre/tree/3.4.0" }, "funding": [ { @@ -886,13 +896,9 @@ { "url": "https://github.com/composer", "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" } ], - "time": "2024-11-12T16:29:46+00:00" + "time": "2026-06-07T11:47:49+00:00" }, { "name": "composer/semver", @@ -1395,20 +1401,20 @@ }, { "name": "league/uri", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri.git", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76" + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri/zipball/4436c6ec8d458e4244448b069cc572d088230b76", - "reference": "4436c6ec8d458e4244448b069cc572d088230b76", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", "shasum": "" }, "require": { - "league/uri-interfaces": "^7.8", + "league/uri-interfaces": "^7.8.1", "php": "^8.1", "psr/http-factory": "^1" }, @@ -1481,7 +1487,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri/tree/7.8.0" + "source": "https://github.com/thephpleague/uri/tree/7.8.1" }, "funding": [ { @@ -1489,20 +1495,20 @@ "type": "github" } ], - "time": "2026-01-14T17:24:56+00:00" + "time": "2026-03-15T20:22:25+00:00" }, { "name": "league/uri-interfaces", - "version": "7.8.0", + "version": "7.8.1", "source": { "type": "git", "url": "https://github.com/thephpleague/uri-interfaces.git", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4" + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/c5c5cd056110fc8afaba29fa6b72a43ced42acd4", - "reference": "c5c5cd056110fc8afaba29fa6b72a43ced42acd4", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", "shasum": "" }, "require": { @@ -1565,7 +1571,7 @@ "docs": "https://uri.thephpleague.com", "forum": "https://thephpleague.slack.com", "issues": "https://github.com/thephpleague/uri-src/issues", - "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.0" + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" }, "funding": [ { @@ -1573,7 +1579,7 @@ "type": "github" } ], - "time": "2026-01-15T06:54:53+00:00" + "time": "2026-03-08T20:05:35+00:00" }, { "name": "netresearch/jsonmapper", @@ -1739,16 +1745,16 @@ }, { "name": "phpdocumentor/reflection-docblock", - "version": "6.0.2", + "version": "6.0.3", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf" + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/897b5986ece6b4f9d8413fea345c7d49c757d6bf", - "reference": "897b5986ece6b4f9d8413fea345c7d49c757d6bf", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582", + "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582", "shasum": "" }, "require": { @@ -1798,9 +1804,9 @@ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", "support": { "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", - "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.2" + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3" }, - "time": "2026-03-01T18:43:49+00:00" + "time": "2026-03-18T20:49:53+00:00" }, { "name": "phpdocumentor/type-resolver", @@ -2120,16 +2126,16 @@ }, { "name": "revolt/event-loop", - "version": "v1.0.8", + "version": "v1.0.9", "source": { "type": "git", "url": "https://github.com/revoltphp/event-loop.git", - "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c" + "reference": "44061cf513e53c6200372fc935ac42271566295d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/b6fc06dce8e9b523c9946138fa5e62181934f91c", - "reference": "b6fc06dce8e9b523c9946138fa5e62181934f91c", + "url": "https://api.github.com/repos/revoltphp/event-loop/zipball/44061cf513e53c6200372fc935ac42271566295d", + "reference": "44061cf513e53c6200372fc935ac42271566295d", "shasum": "" }, "require": { @@ -2139,7 +2145,7 @@ "ext-json": "*", "jetbrains/phpstorm-stubs": "^2019.3", "phpunit/phpunit": "^9", - "psalm/phar": "^5.15" + "psalm/phar": "6.16.*" }, "type": "library", "extra": { @@ -2186,9 +2192,9 @@ ], "support": { "issues": "https://github.com/revoltphp/event-loop/issues", - "source": "https://github.com/revoltphp/event-loop/tree/v1.0.8" + "source": "https://github.com/revoltphp/event-loop/tree/v1.0.9" }, - "time": "2025-08-27T21:33:23+00:00" + "time": "2026-05-16T17:55:38+00:00" }, { "name": "sebastian/diff", @@ -2327,16 +2333,16 @@ }, { "name": "symfony/console", - "version": "v7.4.6", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "6d643a93b47398599124022eb24d97c153c12f27" + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/6d643a93b47398599124022eb24d97c153c12f27", - "reference": "6d643a93b47398599124022eb24d97c153c12f27", + "url": "https://api.github.com/repos/symfony/console/zipball/85095d2573eaefaf35e40b9513a9bf09f72cd217", + "reference": "85095d2573eaefaf35e40b9513a9bf09f72cd217", "shasum": "" }, "require": { @@ -2401,7 +2407,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v7.4.6" + "source": "https://github.com/symfony/console/tree/v7.4.13" }, "funding": [ { @@ -2421,20 +2427,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T17:02:47+00:00" + "time": "2026-05-24T08:56:14+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", "shasum": "" }, "require": { @@ -2447,7 +2453,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -2472,7 +2478,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" }, "funding": [ { @@ -2483,25 +2489,29 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" } ], - "time": "2024-09-25T14:21:43+00:00" + "time": "2026-04-13T15:52:40+00:00" }, { "name": "symfony/filesystem", - "version": "v7.4.6", + "version": "v7.4.11", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e" + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e", - "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/d721ea61b4a5fba8c5b6e7c1feda19efea144b50", + "reference": "d721ea61b4a5fba8c5b6e7c1feda19efea144b50", "shasum": "" }, "require": { @@ -2538,7 +2548,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v7.4.6" + "source": "https://github.com/symfony/filesystem/tree/v7.4.11" }, "funding": [ { @@ -2558,20 +2568,20 @@ "type": "tidelift" } ], - "time": "2026-02-25T16:50:00+00:00" + "time": "2026-05-11T16:38:44+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.33.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", - "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", "shasum": "" }, "require": { @@ -2621,7 +2631,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -2641,20 +2651,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-04-10T16:19:22+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70" + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/380872130d3a5dd3ace2f4010d95125fde5d5c70", - "reference": "380872130d3a5dd3ace2f4010d95125fde5d5c70", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", "shasum": "" }, "require": { @@ -2703,7 +2713,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" }, "funding": [ { @@ -2723,20 +2733,20 @@ "type": "tidelift" } ], - "time": "2025-06-27T09:58:17+00:00" + "time": "2026-05-26T05:58:03+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.33.0", + "version": "v1.38.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "3833d7255cc303546435cb650316bff708a1c75c" + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", - "reference": "3833d7255cc303546435cb650316bff708a1c75c", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", "shasum": "" }, "require": { @@ -2788,7 +2798,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" }, "funding": [ { @@ -2808,20 +2818,20 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2026-05-25T13:48:31+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.33.0", + "version": "v1.38.2", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", "shasum": "" }, "require": { @@ -2873,7 +2883,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" }, "funding": [ { @@ -2893,20 +2903,20 @@ "type": "tidelift" } ], - "time": "2024-12-23T08:48:59+00:00" + "time": "2026-05-27T06:59:30+00:00" }, { "name": "symfony/polyfill-php84", - "version": "v1.33.0", + "version": "v1.38.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php84.git", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191" + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/d8ced4d875142b6a7426000426b8abc631d6b191", - "reference": "d8ced4d875142b6a7426000426b8abc631d6b191", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", "shasum": "" }, "require": { @@ -2953,7 +2963,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php84/tree/v1.33.0" + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" }, "funding": [ { @@ -2973,20 +2983,20 @@ "type": "tidelift" } ], - "time": "2025-06-24T13:30:11+00:00" + "time": "2026-05-26T12:51:13+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.6.1", + "version": "v3.7.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43" + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/45112560a3ba2d715666a509a0bc9521d10b6c43", - "reference": "45112560a3ba2d715666a509a0bc9521d10b6c43", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", "shasum": "" }, "require": { @@ -3004,7 +3014,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "3.7-dev" } }, "autoload": { @@ -3040,7 +3050,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.6.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" }, "funding": [ { @@ -3060,20 +3070,20 @@ "type": "tidelift" } ], - "time": "2025-07-15T11:30:57+00:00" + "time": "2026-03-28T09:44:51+00:00" }, { "name": "symfony/string", - "version": "v7.4.6", + "version": "v7.4.13", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "9f209231affa85aa930a5e46e6eb03381424b30b" + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/9f209231affa85aa930a5e46e6eb03381424b30b", - "reference": "9f209231affa85aa930a5e46e6eb03381424b30b", + "url": "https://api.github.com/repos/symfony/string/zipball/961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", + "reference": "961683010db3b27ec6ebcd7308e6e1ee8fa7ffde", "shasum": "" }, "require": { @@ -3131,7 +3141,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v7.4.6" + "source": "https://github.com/symfony/string/tree/v7.4.13" }, "funding": [ { @@ -3151,20 +3161,20 @@ "type": "tidelift" } ], - "time": "2026-02-09T09:33:46+00:00" + "time": "2026-05-23T15:23:29+00:00" }, { "name": "vimeo/psalm", - "version": "6.15.1", + "version": "6.16.1", "source": { "type": "git", "url": "https://github.com/vimeo/psalm.git", - "reference": "28dc127af1b5aecd52314f6f645bafc10d0e11f9" + "reference": "f1f5de594dc76faf8784e02d3dc4716c91c6f6ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vimeo/psalm/zipball/28dc127af1b5aecd52314f6f645bafc10d0e11f9", - "reference": "28dc127af1b5aecd52314f6f645bafc10d0e11f9", + "url": "https://api.github.com/repos/vimeo/psalm/zipball/f1f5de594dc76faf8784e02d3dc4716c91c6f6ac", + "reference": "f1f5de594dc76faf8784e02d3dc4716c91c6f6ac", "shasum": "" }, "require": { @@ -3269,20 +3279,20 @@ "issues": "https://github.com/vimeo/psalm/issues", "source": "https://github.com/vimeo/psalm" }, - "time": "2026-02-07T19:27:16+00:00" + "time": "2026-03-19T10:56:09+00:00" }, { "name": "webmozart/assert", - "version": "2.1.6", + "version": "2.4.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8" + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/ff31ad6efc62e66e518fbab1cde3453d389bcdc8", - "reference": "ff31ad6efc62e66e518fbab1cde3453d389bcdc8", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155", + "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155", "shasum": "" }, "require": { @@ -3298,7 +3308,11 @@ }, "type": "library", "extra": { + "psalm": { + "pluginClass": "Webmozart\\Assert\\PsalmPlugin" + }, "branch-alias": { + "dev-master": "2.0-dev", "dev-feature/2-0": "2.0-dev" } }, @@ -3329,9 +3343,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/2.1.6" + "source": "https://github.com/webmozarts/assert/tree/2.4.0" }, - "time": "2026-02-27T10:28:38+00:00" + "time": "2026-05-20T13:07:01+00:00" } ], "aliases": [], @@ -3346,5 +3360,5 @@ "platform-overrides": { "php": "8.2.27" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" }