From 4c32176793d0e3c479c3594d91fc8af8d2c85b67 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 15 Jun 2026 11:32:15 +0200 Subject: [PATCH 1/9] feat(audio-translation): add audio translation task type and provider, factorize translation logic in the translate service Signed-off-by: Julien Veyssier --- lib/AppInfo/Application.php | 13 +- lib/Service/TranslateService.php | 113 ++++++++++ .../AudioToAudioTranslateProvider.php | 210 ++++++++++++++++++ .../AudioToAudioTranslateTaskType.php | 87 ++++++++ lib/TaskProcessing/TranslateProvider.php | 114 ++-------- 5 files changed, 434 insertions(+), 103 deletions(-) create mode 100644 lib/TaskProcessing/AudioToAudioTranslateProvider.php create mode 100644 lib/TaskProcessing/AudioToAudioTranslateTaskType.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index bd8a8cad..6c3cc47c 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; @@ -103,11 +105,18 @@ 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'; + // 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) { + $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/Service/TranslateService.php b/lib/Service/TranslateService.php index f26df103..19011eb7 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,83 @@ 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 . ': '; + } + + foreach ($chunks as $chunk) { + $progress += $increase; + $cacheKey = $sourceLanguageCode . '/' . $targetLanguageCode . '/' . md5($chunk); + + $cache = $this->cacheFactory->createDistributed('integration_openai'); + 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(['output' => $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(['output' => $translation]); + } + $cache->set($cacheKey, $decodedCompletion['translation']); + continue; + } + return $translation; + } } diff --git a/lib/TaskProcessing/AudioToAudioTranslateProvider.php b/lib/TaskProcessing/AudioToAudioTranslateProvider.php new file mode 100644 index 00000000..4577315c --- /dev/null +++ b/lib/TaskProcessing/AudioToAudioTranslateProvider.php @@ -0,0 +1,210 @@ +openAiAPIService->getServiceName(Application::SERVICE_TYPE_STT); + } + + public function getTaskTypeId(): string { + return AudioToAudioTranslateTaskType::ID; + } + + public function getExpectedRuntime(): int { + return 60; + } + + public function getInputShapeEnumValues(): array { + $coreL = $this->l10nFactory->getLanguages(); + $languages = array_merge($coreL['commonLanguages'], $coreL['otherLanguages']); + $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 []; + } + + public function getOptionalInputShapeEnumValues(): array { + return []; + } + + public function getOptionalInputShapeDefaults(): array { + return []; + } + + public function getOutputShapeEnumValues(): array { + return []; + } + + public function getOptionalOutputShape(): array { + return []; + } + + 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, + ); + } + + $reportProgress(0.3); + + // 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 { + $translatedText = $this->translateService->translate( + $transcription, $input['origin_language'], $input['target_language'], + $completionModel, $maxTokens, $userId, + ); + + $reportOutput([ + 'translated_text' => $translatedText, + ]); + + 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; + if ($includeWatermark) { + if ($userId !== null) { + $user = $this->userManager->getExistingUser($userId); + $lang = $this->l10nFactory->getUserLanguage($user); + $l = $this->l10nFactory->get(Application::APP_ID, $lang); + $ttsPrompt .= "\n\n" . $l->t('This was generated using Artificial Intelligence.'); + } else { + $ttsPrompt .= "\n\n" . $this->l->t('This was generated using Artificial Intelligence.'); + } + } + $ttsModel = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_model_id', Application::DEFAULT_SPEECH_MODEL_ID, lazy: true) ?: Application::DEFAULT_SPEECH_MODEL_ID; + $voice = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_voice', Application::DEFAULT_SPEECH_VOICE, lazy: true) ?: Application::DEFAULT_SPEECH_VOICE; + $speed = 1; + 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 [ + 'audio_output' => $translatedAudio, + 'text_output' => $translatedText, + ]; + } +} 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/TranslateProvider.php b/lib/TaskProcessing/TranslateProvider.php index c82fe528..d07b5c6e 100644 --- a/lib/TaskProcessing/TranslateProvider.php +++ b/lib/TaskProcessing/TranslateProvider.php @@ -11,7 +11,6 @@ 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; @@ -30,36 +29,13 @@ 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 +120,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,82 +141,24 @@ 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 { + $translation = $this->translateService->translate( + $inputText, $input['origin_language'] ?? '', $input['target_language'] ?? '', + $model, $maxTokens, $userId, $reportProgress, + $preferStreaming, $reportOutput, + ); $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( From 18e3819bd88edf99184525520e7b5ccbf4a44b28 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 15 Jun 2026 11:37:52 +0200 Subject: [PATCH 2/9] adjust tests Signed-off-by: Julien Veyssier --- lib/TaskProcessing/TranslateProvider.php | 4 ---- psalm.xml | 2 +- tests/unit/Providers/OpenAiProviderTest.php | 19 ++++++++++++++----- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/TaskProcessing/TranslateProvider.php b/lib/TaskProcessing/TranslateProvider.php index d07b5c6e..061adf5c 100644 --- a/lib/TaskProcessing/TranslateProvider.php +++ b/lib/TaskProcessing/TranslateProvider.php @@ -14,7 +14,6 @@ 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; @@ -25,7 +24,6 @@ use OCP\TaskProcessing\ShapeEnumValue; use OCP\TaskProcessing\SynchronousProviderOptions; use OCP\TaskProcessing\TaskTypes\TextToTextTranslate; -use Psr\Log\LoggerInterface; class TranslateProvider implements IProvider, ISynchronousOptionsAwareProvider { @@ -33,8 +31,6 @@ public function __construct( private OpenAiAPIService $openAiAPIService, private OpenAiSettingsService $openAiSettingsService, private IL10N $l, - private ICacheFactory $cacheFactory, - private LoggerInterface $logger, private TranslateService $translateService, private ?string $userId, ) { 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/tests/unit/Providers/OpenAiProviderTest.php b/tests/unit/Providers/OpenAiProviderTest.php index e458fdfb..08214ae2 100644 --- a/tests/unit/Providers/OpenAiProviderTest.php +++ b/tests/unit/Providers/OpenAiProviderTest.php @@ -19,6 +19,7 @@ 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\ChangeToneProvider; use OCA\OpenAi\TaskProcessing\EmojiProvider; @@ -53,6 +54,7 @@ class OpenAiProviderTest extends TestCase { private OpenAiSettingsService $openAiSettingsService; private ChunkService $chunkService; private StreamingService $streamingService; + private TranslateService $translateService; /** * @var MockObject|IClient */ @@ -99,6 +101,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'); } @@ -627,9 +638,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 +678,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); From e5eebc7718d9d3237fe936440c22fed28dc62f09 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 15 Jun 2026 11:47:23 +0200 Subject: [PATCH 3/9] update composer config.platform, update compose dependencies Signed-off-by: Julien Veyssier --- composer.json | 2 +- composer.lock | 16 +- vendor-bin/php-cs-fixer/composer.lock | 40 ++-- vendor-bin/php-scoper/composer.lock | 136 ++++++----- vendor-bin/phpunit/composer.lock | 2 +- vendor-bin/psalm/composer.lock | 322 ++++++++++++++------------ 6 files changed, 270 insertions(+), 248 deletions(-) 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/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" } From 1963f7d2f2cc2fbe34c8f5c3d96e943c69826994 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 15 Jun 2026 11:47:51 +0200 Subject: [PATCH 4/9] run cs:fix Signed-off-by: Julien Veyssier --- lib/AppInfo/Application.php | 2 -- lib/Controller/ConfigController.php | 1 - lib/Db/QuotaRuleMapper.php | 1 - lib/Migration/Version030102Date20241003155512.php | 1 + lib/Migration/Version030103Date20241009172829.php | 1 + lib/Notification/Notifier.php | 1 - lib/Service/OpenAiSettingsService.php | 2 -- lib/TaskProcessing/AnalyzeImagesProvider.php | 2 -- lib/TaskProcessing/AudioToAudioChatProvider.php | 2 -- lib/TaskProcessing/AudioToAudioTranslateProvider.php | 1 - lib/TaskProcessing/ContextWriteProvider.php | 1 - lib/TaskProcessing/ProofreadProvider.php | 1 - lib/TaskProcessing/TextToSpeechProvider.php | 2 -- lib/TaskProcessing/TranslateProvider.php | 1 - scoper.inc.php | 1 - tests/bootstrap.php | 1 - .../stubs/ocp_task_processing_isynchronous_options_provider.php | 1 + .../stubs/ocp_task_processing_synchronous_provider_options.php | 1 + tests/unit/Providers/OpenAiProviderTest.php | 2 -- tests/unit/Quota/QuotaTest.php | 1 - tests/unit/Service/ServiceOverrideTest.php | 1 - 21 files changed, 4 insertions(+), 23 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 6c3cc47c..1bff8fab 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -31,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; @@ -77,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'; 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/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 index 4577315c..32341a55 100644 --- a/lib/TaskProcessing/AudioToAudioTranslateProvider.php +++ b/lib/TaskProcessing/AudioToAudioTranslateProvider.php @@ -77,7 +77,6 @@ public function getInputShapeDefaults(): array { ]; } - public function getOptionalInputShape(): array { return []; } 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 061adf5c..8ecd3703 100644 --- a/lib/TaskProcessing/TranslateProvider.php +++ b/lib/TaskProcessing/TranslateProvider.php @@ -155,7 +155,6 @@ public function process( throw new ProcessingException("Empty translation result from {$fromLanguage} to {$toLanguage}"); } return ['output' => trim($translation)]; - } catch (Exception $e) { throw new ProcessingException( "Failed to translate from {$fromLanguage} to {$toLanguage}: {$e->getMessage()}", 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 08214ae2..c36aecd3 100644 --- a/tests/unit/Providers/OpenAiProviderTest.php +++ b/tests/unit/Providers/OpenAiProviderTest.php @@ -364,7 +364,6 @@ public function testEmojiProvider(): void { $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); } - public function testHeadlineProvider(): void { $headlineProvider = new HeadlineProvider( $this->openAiApiService, @@ -495,7 +494,6 @@ public function testChangeToneProvider(): void { $this->quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1); } - public function testSummaryProvider(): void { $summaryProvider = new SummaryProvider( $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) { From 3f55524094725df3353ca57d8e3308feb7910b2e Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 15 Jun 2026 11:54:47 +0200 Subject: [PATCH 5/9] include audio input transcription as optional output Signed-off-by: Julien Veyssier --- .../AudioToAudioTranslateProvider.php | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/lib/TaskProcessing/AudioToAudioTranslateProvider.php b/lib/TaskProcessing/AudioToAudioTranslateProvider.php index 32341a55..a6aeac8c 100644 --- a/lib/TaskProcessing/AudioToAudioTranslateProvider.php +++ b/lib/TaskProcessing/AudioToAudioTranslateProvider.php @@ -20,9 +20,11 @@ use OCP\IL10N; use OCP\IUserManager; use OCP\L10N\IFactory; +use OCP\TaskProcessing\EShapeType; use OCP\TaskProcessing\Exception\ProcessingException; use OCP\TaskProcessing\IProvider; use OCP\TaskProcessing\ISynchronousOptionsAwareProvider; +use OCP\TaskProcessing\ShapeDescriptor; use OCP\TaskProcessing\ShapeEnumValue; use OCP\TaskProcessing\SynchronousProviderOptions; use Psr\Log\LoggerInterface; @@ -94,7 +96,13 @@ public function getOutputShapeEnumValues(): array { } public function getOptionalOutputShape(): array { - return []; + return [ + 'text_input' => new ShapeDescriptor( + $this->l->t('Audio transcription'), + $this->l->t('The transcribed audio input'), + EShapeType::Text, + ), + ]; } public function getOptionalOutputShapeEnumValues(): array { @@ -135,6 +143,12 @@ public function process( $reportProgress(0.3); + if ($preferStreaming) { + $reportOutput([ + 'text_input' => $transcription, + ]); + } + // 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) @@ -147,9 +161,12 @@ public function process( $completionModel, $maxTokens, $userId, ); - $reportOutput([ - 'translated_text' => $translatedText, - ]); + if ($preferStreaming) { + $reportOutput([ + 'text_input' => $transcription, + 'translated_text' => $translatedText, + ]); + } if (empty($translatedText)) { throw new ProcessingException("Empty translation result from {$input['origin_language']} to {$input['target_language']}"); From b77c2a9b4400c235aba48b3231b98550e6b6cb25 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 15 Jun 2026 12:08:56 +0200 Subject: [PATCH 6/9] include optional inputs: voice, model and speed Signed-off-by: Julien Veyssier --- .../AudioToAudioTranslateProvider.php | 62 +++++++++++++++++-- 1 file changed, 57 insertions(+), 5 deletions(-) diff --git a/lib/TaskProcessing/AudioToAudioTranslateProvider.php b/lib/TaskProcessing/AudioToAudioTranslateProvider.php index a6aeac8c..84d3f538 100644 --- a/lib/TaskProcessing/AudioToAudioTranslateProvider.php +++ b/lib/TaskProcessing/AudioToAudioTranslateProvider.php @@ -41,6 +41,7 @@ public function __construct( private IL10N $l, private IAppConfig $appConfig, private IUserManager $userManager, + private ?string $userId, ) { } @@ -80,15 +81,45 @@ public function getInputShapeDefaults(): array { } public function getOptionalInputShape(): array { - return []; + 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 { - return []; + $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 { - return []; + $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 { @@ -193,9 +224,29 @@ public function process( $ttsPrompt .= "\n\n" . $this->l->t('This was generated using Artificial Intelligence.'); } } - $ttsModel = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_model_id', Application::DEFAULT_SPEECH_MODEL_ID, lazy: true) ?: Application::DEFAULT_SPEECH_MODEL_ID; - $voice = $this->appConfig->getValueString(Application::APP_ID, 'default_speech_voice', Application::DEFAULT_SPEECH_VOICE, lazy: true) ?: Application::DEFAULT_SPEECH_VOICE; + if (isset($input['model']) && is_string($input['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, @@ -219,6 +270,7 @@ public function process( // Translation return [ + 'text_input' => $transcription, 'audio_output' => $translatedAudio, 'text_output' => $translatedText, ]; From e785e272341b1b08b33325174bce84c01f2d0324 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 15 Jun 2026 12:17:51 +0200 Subject: [PATCH 7/9] watermark the audio transcript so the audio and text outputs are watermarked in the target language Signed-off-by: Julien Veyssier --- .../AudioToAudioTranslateProvider.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/TaskProcessing/AudioToAudioTranslateProvider.php b/lib/TaskProcessing/AudioToAudioTranslateProvider.php index 84d3f538..8f6430f1 100644 --- a/lib/TaskProcessing/AudioToAudioTranslateProvider.php +++ b/lib/TaskProcessing/AudioToAudioTranslateProvider.php @@ -171,6 +171,16 @@ public function process( $e, ); } + if ($includeWatermark) { + if ($userId !== null) { + $user = $this->userManager->getExistingUser($userId); + $lang = $this->l10nFactory->getUserLanguage($user); + $l = $this->l10nFactory->get(Application::APP_ID, $lang); + $transcription .= "\n\n" . $l->t('This was generated using Artificial Intelligence.'); + } else { + $transcription .= "\n\n" . $this->l->t('This was generated using Artificial Intelligence.'); + } + } $reportProgress(0.3); @@ -214,16 +224,6 @@ public function process( // TTS $ttsPrompt = $translatedText; - if ($includeWatermark) { - if ($userId !== null) { - $user = $this->userManager->getExistingUser($userId); - $lang = $this->l10nFactory->getUserLanguage($user); - $l = $this->l10nFactory->get(Application::APP_ID, $lang); - $ttsPrompt .= "\n\n" . $l->t('This was generated using Artificial Intelligence.'); - } else { - $ttsPrompt .= "\n\n" . $this->l->t('This was generated using Artificial Intelligence.'); - } - } if (isset($input['model']) && is_string($input['model'])) { $ttsModel = $input['tts_model']; } else { @@ -256,7 +256,7 @@ public function process( $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']; + $translatedAudio = $apiResponse['body']; } catch (\Exception $e) { $this->logger->warning('Text to speech generation failed with: ' . $e->getMessage(), ['exception' => $e]); throw new ProcessingException( From acf646f5962ee80039bcb189facb8922fd15d349 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 15 Jun 2026 15:58:18 +0200 Subject: [PATCH 8/9] fix mistakes in AudioToAudioTranslateProvider use TranslateService::getStaticLanguages() to get the input shape enum values watermark the audio directly keep the watermark suffix in the language of the user who scheduled register audio translation provider if tts is enabled fix reporting translation output fix cache being created inside a loop cleanup Signed-off-by: Julien Veyssier --- lib/AppInfo/Application.php | 3 +- lib/Service/TranslateService.php | 9 ++--- lib/Service/WatermarkingService.php | 2 +- .../AudioToAudioTranslateProvider.php | 38 ++++++++++++------- lib/TaskProcessing/TranslateProvider.php | 7 +++- 5 files changed, 37 insertions(+), 22 deletions(-) diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index 1bff8fab..79ae26ca 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -105,12 +105,13 @@ public function register(IRegistrationContext $context): void { $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 ($translationProviderEnabled) { $context->registerTaskProcessingProvider(TranslateProvider::class); } - if ($translationProviderEnabled && $sttProviderEnabled) { + if ($translationProviderEnabled && $sttProviderEnabled && $ttsProviderEnabled) { $context->registerTaskProcessingTaskType(AudioToAudioTranslateTaskType::class); $context->registerTaskProcessingProvider(AudioToAudioTranslateProvider::class); } diff --git a/lib/Service/TranslateService.php b/lib/Service/TranslateService.php index 19011eb7..ecdb2d6a 100644 --- a/lib/Service/TranslateService.php +++ b/lib/Service/TranslateService.php @@ -30,7 +30,7 @@ class TranslateService { 'description' => 'The translated text', ], ], - 'required' => [ 'translation' ], + 'required' => ['translation'], 'additionalProperties' => false, ], ], @@ -86,11 +86,11 @@ public function translate( $promptStart = 'Translate the following text to ' . $toLanguage . ': '; } + $cache = $this->cacheFactory->createDistributed('integration_openai'); foreach ($chunks as $chunk) { $progress += $increase; $cacheKey = $sourceLanguageCode . '/' . $targetLanguageCode . '/' . md5($chunk); - $cache = $this->cacheFactory->createDistributed('integration_openai'); if ($cached = $cache->get($cacheKey)) { $this->logger->debug('Using cached translation', ['cached' => $cached, 'cacheKey' => $cacheKey]); $translation .= $cached; @@ -98,7 +98,7 @@ public function translate( $reportProgress($progress); } if ($preferStreaming && $reportOutput !== null) { - $reportOutput(['output' => $translation]); + $reportOutput($translation); } continue; } @@ -136,10 +136,9 @@ public function translate( } $translation .= $decodedCompletion['translation']; if ($preferStreaming && $reportOutput !== null) { - $reportOutput(['output' => $translation]); + $reportOutput($translation); } $cache->set($cacheKey, $decodedCompletion['translation']); - continue; } 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/AudioToAudioTranslateProvider.php b/lib/TaskProcessing/AudioToAudioTranslateProvider.php index 8f6430f1..954c58d9 100644 --- a/lib/TaskProcessing/AudioToAudioTranslateProvider.php +++ b/lib/TaskProcessing/AudioToAudioTranslateProvider.php @@ -62,8 +62,7 @@ public function getExpectedRuntime(): int { } public function getInputShapeEnumValues(): array { - $coreL = $this->l10nFactory->getLanguages(); - $languages = array_merge($coreL['commonLanguages'], $coreL['otherLanguages']); + $languages = TranslateService::getStaticLanguages(); $languageEnumValues = array_map(static function (array $language) { return new ShapeEnumValue($language['name'], $language['code']); }, $languages); @@ -171,14 +170,18 @@ public function process( $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); - $transcription .= "\n\n" . $l->t('This was generated using Artificial Intelligence.'); + $watermarkSuffix = "\n\n" . $l->t('This was generated using Artificial Intelligence.'); } else { - $transcription .= "\n\n" . $this->l->t('This was generated using Artificial Intelligence.'); + $watermarkSuffix = "\n\n" . $this->l->t('This was generated using Artificial Intelligence.'); } } @@ -186,7 +189,7 @@ public function process( if ($preferStreaming) { $reportOutput([ - 'text_input' => $transcription, + 'text_input' => $transcription . $watermarkSuffix, ]); } @@ -197,15 +200,22 @@ public function process( $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, + $completionModel, $maxTokens, $userId, null, + $preferStreaming, $reportTranslationOutput, ); if ($preferStreaming) { $reportOutput([ - 'text_input' => $transcription, - 'translated_text' => $translatedText, + 'text_input' => $transcription . $watermarkSuffix, + 'text_output' => $translatedText . $watermarkSuffix, ]); } @@ -223,8 +233,8 @@ public function process( $reportProgress(0.6); // TTS - $ttsPrompt = $translatedText; - if (isset($input['model']) && is_string($input['model'])) { + $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; @@ -256,8 +266,8 @@ public function process( $this->logger->warning('Text to speech generation failed: no speech returned'); throw new ProcessingException('Text to speech generation failed: no speech returned'); } - $translatedAudio = $apiResponse['body']; - } catch (\Exception $e) { + $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(), @@ -270,9 +280,9 @@ public function process( // Translation return [ - 'text_input' => $transcription, + 'text_input' => $transcription . $watermarkSuffix, 'audio_output' => $translatedAudio, - 'text_output' => $translatedText, + 'text_output' => $translatedText . $watermarkSuffix, ]; } } diff --git a/lib/TaskProcessing/TranslateProvider.php b/lib/TaskProcessing/TranslateProvider.php index 8ecd3703..deeaf98f 100644 --- a/lib/TaskProcessing/TranslateProvider.php +++ b/lib/TaskProcessing/TranslateProvider.php @@ -142,10 +142,15 @@ public function process( $toLanguage = $coreLanguages[$input['target_language']] ?? $input['target_language']; 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, $reportOutput, + $preferStreaming, $reportTranslationOutput, ); $endTime = time(); From d01c43f66960f916d454c8dc4c4a9e302ae09ab9 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Tue, 16 Jun 2026 12:47:48 +0200 Subject: [PATCH 9/9] add basic test for AudioToAudioTranslateProvider Signed-off-by: Julien Veyssier --- tests/unit/Providers/OpenAiProviderTest.php | 114 ++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/tests/unit/Providers/OpenAiProviderTest.php b/tests/unit/Providers/OpenAiProviderTest.php index c36aecd3..ed7018a2 100644 --- a/tests/unit/Providers/OpenAiProviderTest.php +++ b/tests/unit/Providers/OpenAiProviderTest.php @@ -21,6 +21,7 @@ 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; @@ -711,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,