From f6aba5b1fbdd421dd75345b893e4e297518ccbe2 Mon Sep 17 00:00:00 2001
From: Frederik Wouters <woutefr@cronos.be>
Date: Mon, 3 Mar 2025 09:43:32 +0100
Subject: [PATCH 1/8] first step of allowing skipping of moderations

---
 .../search_api_ai_search.backend.schema.yml   |  3 +++
 .../src/Base/EmbeddingStrategyPluginBase.php  | 22 +++++++++++++++++++
 .../EmbeddingStrategy/EmbeddingBase.php       | 18 +++++++++++----
 3 files changed, 39 insertions(+), 4 deletions(-)

diff --git a/modules/ai_search/config/schema/search_api_ai_search.backend.schema.yml b/modules/ai_search/config/schema/search_api_ai_search.backend.schema.yml
index da9b6a3e5..52b5c6ed2 100644
--- a/modules/ai_search/config/schema/search_api_ai_search.backend.schema.yml
+++ b/modules/ai_search/config/schema/search_api_ai_search.backend.schema.yml
@@ -50,6 +50,9 @@ plugin.plugin_configuration.search_api_backend.search_api_ai_search:
         contextual_content_max_percentage:
           type: string
           label: 'Maximum percentage of contextual content'
+        skip_moderation:
+          type: boolean
+          label: 'Whether to skip moderation'
     embedding_strategy_details:
       type: string
       label: 'Additional details for the embedding strategy'
diff --git a/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php b/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
index 0e90dde4c..5eabe6a27 100644
--- a/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
+++ b/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
@@ -45,6 +45,13 @@ abstract class EmbeddingStrategyPluginBase implements EmbeddingStrategyInterface
    */
   protected int $chunkSize;
 
+  /**
+   * If we should skip moderation (mostly NO!).
+   *
+   * @var bool
+   */
+  protected bool $skip_moderation;
+
   /**
    * The chunk minimum overlap.
    *
@@ -118,6 +125,12 @@ abstract class EmbeddingStrategyPluginBase implements EmbeddingStrategyInterface
     $this->textChunker->setModel($chat_model_id);
     /** @var \Drupal\ai\OperationType\Embeddings\EmbeddingsInterface $embeddingLlm */
     $this->embeddingLlm = $this->aiProviderManager->createInstance($this->providerId);
+    if (!empty($configuration['skip_moderation']) && is_numeric($configuration['skip_moderation'])) {
+      $this->skip_moderation = (bool) $configuration['skip_moderation'];
+    }
+    else {
+      $this->skip_moderation = FALSE;
+    }
     if (!empty($configuration['chunk_size']) && is_numeric($configuration['chunk_size'])) {
       $this->chunkSize = (int) $configuration['chunk_size'];
     }
@@ -179,6 +192,15 @@ abstract class EmbeddingStrategyPluginBase implements EmbeddingStrategyInterface
     if (empty($configuration)) {
       $configuration = $this->getDefaultConfigurationValues();
     }
+
+    $form['skip_moderation'] = [
+      '#title' => $this->t('Skip moderation for embeddings. (DO NOT CHECK THIS)'),
+      '#description' => $this->t('ONLY CHECK THIS IF YOU ARE 100% SURE WHAT YOU ARE DOING!'),
+      '#required' => TRUE,
+      '#type' => 'checkbox',
+      '#default_value' => $configuration['skip_moderation'] ?? FALSE,
+    ];
+
     $form['chunk_size'] = [
       '#title' => $this->t('Maximum chunk size allowed when breaking up larger content'),
       '#description' => $this->t('When the content is longer than this in tokens (which roughly equates to syllables when oversimplified), the content should be broken into smaller "Chunks". This setting defines how to segment or break up the larger text. When configuring Fields for this Index, the fields with the indexing option "Main Content" will be split into chunks no greater than this size. This includes any added "Contextual Content" as well as the "Title" to ensure an accurate vectorized representation of the content. More details are provided when configuring the Fields within your Index. Leave this blank to use the maximum size provided by the selected model.'),
diff --git a/modules/ai_search/src/Plugin/EmbeddingStrategy/EmbeddingBase.php b/modules/ai_search/src/Plugin/EmbeddingStrategy/EmbeddingBase.php
index 785958758..171003623 100644
--- a/modules/ai_search/src/Plugin/EmbeddingStrategy/EmbeddingBase.php
+++ b/modules/ai_search/src/Plugin/EmbeddingStrategy/EmbeddingBase.php
@@ -118,10 +118,14 @@ class EmbeddingBase extends EmbeddingStrategyPluginBase implements EmbeddingStra
       if ($chunk) {
         // Normalize the chunk before embedding it.
         $input = new EmbeddingsInput($chunk);
+        $tags = ['ai_search'];
+        if ($this->skip_moderation) {
+          $tags[] = 'skip_moderation';
+        }
         $raw_embeddings[] = $embedding_llm->embeddings(
           $input,
           $this->modelId,
-          ['ai_search'],
+          $tags,
         )->getNormalized();
       }
     }
@@ -313,7 +317,8 @@ class EmbeddingBase extends EmbeddingStrategyPluginBase implements EmbeddingStra
    */
   public function buildBaseMetadata(array $fields, IndexInterface $index): array {
     $metadata = [];
-    $index_config = $this->configFactory->get('ai_search.index.' . $index->id())->getRawData();
+    $index_config = $this->configFactory->get('ai_search.index.' . $index->id())
+      ->getRawData();
     $indexing_options = $index_config['indexing_options'];
     foreach ($fields as $field) {
 
@@ -346,7 +351,8 @@ class EmbeddingBase extends EmbeddingStrategyPluginBase implements EmbeddingStra
    *   The metadata to attach to the vector database record.
    */
   public function addContentToMetadata(array $metadata, string $content, IndexInterface $index): array {
-    $ai_search_index_config = $this->configFactory->get('ai_search.index.' . $index->id())->getRawData();
+    $ai_search_index_config = $this->configFactory->get('ai_search.index.' . $index->id())
+      ->getRawData();
     if (
       !isset($ai_search_index_config['exclude_chunk_from_metadata'])
       || !$ai_search_index_config['exclude_chunk_from_metadata']
@@ -422,7 +428,11 @@ class EmbeddingBase extends EmbeddingStrategyPluginBase implements EmbeddingStra
     // probably need to consider what field types the Vector Database supports
     // as metadata, but for now let's assume, strings, floats, integers, and
     // boolean values are fine for all.
-    if (in_array($field->getType(), ['date', 'boolean', 'integer']) && count($values) === 1) {
+    if (in_array($field->getType(), [
+        'date',
+        'boolean',
+        'integer',
+      ]) && count($values) === 1) {
       return (int) reset($values);
     }
     elseif (in_array($field->getType(), ['boolean']) && count($values) === 1) {
-- 
GitLab


From 4f26911c66364e8d1faf63d0fbfbf7d346d30ed1 Mon Sep 17 00:00:00 2001
From: Frederik Wouters <woutefr@cronos.be>
Date: Mon, 3 Mar 2025 12:23:47 +0100
Subject: [PATCH 2/8] phpcs

---
 .../ai_search/src/Base/EmbeddingStrategyPluginBase.php    | 6 +++---
 .../src/Plugin/EmbeddingStrategy/EmbeddingBase.php        | 8 ++------
 2 files changed, 5 insertions(+), 9 deletions(-)

diff --git a/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php b/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
index 5eabe6a27..3a266f439 100644
--- a/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
+++ b/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
@@ -50,7 +50,7 @@ abstract class EmbeddingStrategyPluginBase implements EmbeddingStrategyInterface
    *
    * @var bool
    */
-  protected bool $skip_moderation;
+  protected bool $skipModeration;
 
   /**
    * The chunk minimum overlap.
@@ -126,10 +126,10 @@ abstract class EmbeddingStrategyPluginBase implements EmbeddingStrategyInterface
     /** @var \Drupal\ai\OperationType\Embeddings\EmbeddingsInterface $embeddingLlm */
     $this->embeddingLlm = $this->aiProviderManager->createInstance($this->providerId);
     if (!empty($configuration['skip_moderation']) && is_numeric($configuration['skip_moderation'])) {
-      $this->skip_moderation = (bool) $configuration['skip_moderation'];
+      $this->skipModeration = (bool) $configuration['skip_moderation'];
     }
     else {
-      $this->skip_moderation = FALSE;
+      $this->skipModeration = FALSE;
     }
     if (!empty($configuration['chunk_size']) && is_numeric($configuration['chunk_size'])) {
       $this->chunkSize = (int) $configuration['chunk_size'];
diff --git a/modules/ai_search/src/Plugin/EmbeddingStrategy/EmbeddingBase.php b/modules/ai_search/src/Plugin/EmbeddingStrategy/EmbeddingBase.php
index 171003623..7645cfd5d 100644
--- a/modules/ai_search/src/Plugin/EmbeddingStrategy/EmbeddingBase.php
+++ b/modules/ai_search/src/Plugin/EmbeddingStrategy/EmbeddingBase.php
@@ -119,7 +119,7 @@ class EmbeddingBase extends EmbeddingStrategyPluginBase implements EmbeddingStra
         // Normalize the chunk before embedding it.
         $input = new EmbeddingsInput($chunk);
         $tags = ['ai_search'];
-        if ($this->skip_moderation) {
+        if ($this->skipModeration) {
           $tags[] = 'skip_moderation';
         }
         $raw_embeddings[] = $embedding_llm->embeddings(
@@ -428,11 +428,7 @@ class EmbeddingBase extends EmbeddingStrategyPluginBase implements EmbeddingStra
     // probably need to consider what field types the Vector Database supports
     // as metadata, but for now let's assume, strings, floats, integers, and
     // boolean values are fine for all.
-    if (in_array($field->getType(), [
-        'date',
-        'boolean',
-        'integer',
-      ]) && count($values) === 1) {
+    if (in_array($field->getType(), ['date', 'boolean', 'integer']) && count($values) === 1) {
       return (int) reset($values);
     }
     elseif (in_array($field->getType(), ['boolean']) && count($values) === 1) {
-- 
GitLab


From 8f987bcbc4ad9585758dfc76bfce76c745a0e3c1 Mon Sep 17 00:00:00 2001
From: Kevin Quillen <15399-kevinquillen@users.noreply.drupalcode.org>
Date: Mon, 3 Mar 2025 20:02:54 +0000
Subject: [PATCH 3/8] Add eca_content as a dependency of this module.

---
 modules/ai_eca/ai_eca.info.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/modules/ai_eca/ai_eca.info.yml b/modules/ai_eca/ai_eca.info.yml
index 0af3e6ab3..e71a56ed2 100644
--- a/modules/ai_eca/ai_eca.info.yml
+++ b/modules/ai_eca/ai_eca.info.yml
@@ -8,3 +8,4 @@ dependencies:
   - ai:ai
   - drupal:file
   - eca:eca
+  - eca_content:eca_content
-- 
GitLab


From ad402db441b2a4b6c358c1ae8e79f82a22b5b7d4 Mon Sep 17 00:00:00 2001
From: Marcus Johansson <me@marcusmailbox.com>
Date: Wed, 5 Mar 2025 10:46:43 +0100
Subject: [PATCH 4/8] Fix Video Automators bug

---
 .../AiAutomatorType/LlmVideoToImage.php       |  23 ++-
 .../AiAutomatorType/LlmVideoToVideo.php       |  27 +--
 .../src/PluginBaseClasses/VideoToText.php     | 162 +++++++++++++-----
 3 files changed, 149 insertions(+), 63 deletions(-)

diff --git a/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToImage.php b/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToImage.php
index 911d6a0a3..2ea9cb261 100644
--- a/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToImage.php
+++ b/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToImage.php
@@ -4,6 +4,7 @@ namespace Drupal\ai_automators\Plugin\AiAutomatorType;
 
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ai\OperationType\Chat\ChatInput;
@@ -135,7 +136,7 @@ class LlmVideoToImage extends VideoToText implements AiAutomatorTypeInterface {
           if (!isset($values[0][0]['timestamp'])) {
             throw new AiAutomatorResponseErrorException('Could not find any timestamp..');
           }
-          $this->createVideoRasterImages($automatorConfig, $entityWrapper->entity, $values[0]['value'][0]['timestamp']);
+          $this->createVideoRasterImages($automatorConfig, $entityWrapper->entity, $values[0][0]['timestamp']);
           $input = new ChatInput([
             new ChatMessage('user', $prompt, $this->images),
           ]);
@@ -154,7 +155,7 @@ class LlmVideoToImage extends VideoToText implements AiAutomatorTypeInterface {
    */
   public function verifyValue(ContentEntityInterface $entity, $value, FieldDefinitionInterface $fieldDefinition, array $automatorConfig) {
     // Should have start and end time.
-    if (!isset($value['timestamp'])) {
+    if (!isset($value[0]['timestamp'])) {
       return FALSE;
     }
     // Otherwise it is ok.
@@ -175,7 +176,7 @@ class LlmVideoToImage extends VideoToText implements AiAutomatorTypeInterface {
 
     // Get the actual file name and replace it with _cut.
     $fileName = pathinfo($realPath, PATHINFO_FILENAME);
-    $newFile = str_replace($fileName, $fileName . '_cut', $entity->{$baseField}->entity->getFileUri());
+    $newFile = str_replace($fileName, $fileName . '_cut', $entity->{$baseField}->entity->getFileUri()) . '.jpg';
 
     $tmpName = "";
     foreach ($values as $keys) {
@@ -186,16 +187,20 @@ class LlmVideoToImage extends VideoToText implements AiAutomatorTypeInterface {
         $tmpNames[] = $tmpName;
 
         $inputVideo = $this->video ?? $realPath;
-        $command = 'ffmpeg -y -nostdin -i "' . $inputVideo . '" -ss "' . $key['timestamp'] . '" -frames:v 1 ' . $tmpName;
 
-        exec($command, $status);
-        if ($status) {
-          throw new AiAutomatorResponseErrorException('Could not generate new videos.');
-        }
+        // Clean the values.
+        $timeStamp = $this->cleanTimestamp($key['timestamp']);
+        $tokens = [
+          'inputFile' => $inputVideo,
+          'timeStamp' => $timeStamp,
+          'outputFile' => $tmpName,
+        ];
+        $command = '-y -nostdin -i {inputFile} -ss {timeStamp} -frames:v 1 {outputFile}';
+        $this->runFfmpegCommand($command, $tokens, 'Could not generate new images.');
       }
 
       // Move the file to the correct place.
-      $fixedFile = $this->fileSystem->move($tmpName, $newFile);
+      $fixedFile = $this->fileSystem->copy($tmpName, $newFile, FileSystemInterface::EXISTS_RENAME);
 
       // Generate the new file entity.
       $file = File::create([
diff --git a/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToVideo.php b/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToVideo.php
index 630e02a01..3cef5a299 100644
--- a/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToVideo.php
+++ b/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToVideo.php
@@ -9,7 +9,6 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ai\OperationType\Chat\ChatInput;
 use Drupal\ai\OperationType\Chat\ChatMessage;
 use Drupal\ai_automators\Attribute\AiAutomatorType;
-use Drupal\ai_automators\Exceptions\AiAutomatorResponseErrorException;
 use Drupal\ai_automators\PluginBaseClasses\VideoToText;
 use Drupal\ai_automators\PluginInterfaces\AiAutomatorTypeInterface;
 use Drupal\file\Entity\File;
@@ -172,11 +171,14 @@ class LlmVideoToVideo extends VideoToText implements AiAutomatorTypeInterface {
         $tmpName = $this->fileSystem->tempnam($this->tmpDir, 'video') . '.mp4';
         $tmpNames[] = $tmpName;
 
-        $command = "ffmpeg -y -nostdin -i \"$realPath\" -ss {$key['start_time']} -to {$key['end_time']} -c:v libx264 -c:a aac -strict -2 $tmpName";
-        exec($command, $status);
-        if ($status) {
-          throw new AiAutomatorResponseErrorException('Could not generate new videos.');
-        }
+        $tokens = [
+          'startTime' => $this->cleanTimestamp($key['start_time']),
+          'endTime' => $this->cleanTimestamp($key['end_time']),
+          'realPath' => $realPath,
+          'tmpName' => $tmpName,
+        ];
+        $command = "-y -nostdin -i {realPath} -ss {startTime} -to {endTime} -c:v libx264 -c:a aac -strict -2 {tmpName}";
+        $this->runFfmpegCommand($command, $tokens, 'Could not cut out the video.');
       }
 
       // If we only have one video, we can just rename it.
@@ -189,14 +191,15 @@ class LlmVideoToVideo extends VideoToText implements AiAutomatorTypeInterface {
         // Generate list file.
         $text = '';
         foreach ($tmpNames as $tmpName) {
-          $text .= "file '$tmpName'\n";
+          $text .= "file $tmpName\n";
         }
         file_put_contents($this->tmpDir . 'list.txt', $text);
-        $command = "ffmpeg -y -nostdin -f concat -safe 0 -i {$this->tmpDir}list.txt -c:v libx264 -c:a aac -strict -2 $endFile";
-        exec($command, $status);
-        if ($status) {
-          throw new AiAutomatorResponseErrorException('Could not generate new videos.');
-        }
+        $tokens = [
+          'listFile' => $this->tmpDir . 'list.txt',
+          'endFile' => $endFile,
+        ];
+        $command = "-y -nostdin -f concat -safe 0 -i {listFile} -c:v libx264 -c:a aac -strict -2 {endFile}";
+        $this->runFfmpegCommand($command, $tokens, 'Could not mix the videos together.');
       }
       // Move the file to the correct place.
       $fixedFile = $this->fileSystem->move($endFile, $newFile);
diff --git a/modules/ai_automators/src/PluginBaseClasses/VideoToText.php b/modules/ai_automators/src/PluginBaseClasses/VideoToText.php
index 886877df1..081c0f509 100644
--- a/modules/ai_automators/src/PluginBaseClasses/VideoToText.php
+++ b/modules/ai_automators/src/PluginBaseClasses/VideoToText.php
@@ -17,6 +17,7 @@ use Drupal\ai\AiProviderPluginManager;
 use Drupal\ai\OperationType\Chat\ChatInput;
 use Drupal\ai\OperationType\Chat\ChatMessage;
 use Drupal\ai\OperationType\GenericType\AudioFile;
+use Drupal\ai\OperationType\GenericType\ImageFile;
 use Drupal\ai\OperationType\SpeechToText\SpeechToTextInput;
 use Drupal\ai\Service\AiProviderFormHelper;
 use Drupal\ai\Service\PromptJsonDecoder\PromptJsonDecoderInterface;
@@ -55,6 +56,11 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
    */
   public string $tmpDir;
 
+  /**
+   * The directory inside the subdirectory, to use for security.
+   */
+  public string $tmpSubDir = 'ai_automator';
+
   /**
    * The images.
    */
@@ -264,21 +270,24 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
    */
   public function screenshotFromTimestamp(File $video, $timeStamp, array $cropData = []) {
     $path = $video->getFileUri();
-    $realPath = $this->fileSystem->realpath($path);
-    $command = "ffmpeg -y -nostdin -ss $timeStamp -i \"$realPath\" -vframes 1 {$this->tmpDir}/screenshot.jpeg";
+    // Clean values before using them.
+    $command = "-y -nostdin -ss {timeStamp} -i {realPath} -vframes 1 {screenshotFile}";
+    $tokens = [
+      'screenshotFile' => $this->tmpDir . "screenshot.jpeg",
+      'timeStamp' => $this->cleanTimestamp($timeStamp),
+      'realPath' => $this->fileSystem->realpath($path),
+    ];
     // If we need to crop also.
     if (count($cropData)) {
       $realCropData = $this->normalizeCropData($video, $cropData);
-      $command = "ffmpeg -y -nostdin  -ss $timeStamp -i \"$realPath\" -vf \"crop={$realCropData[2]}:{$realCropData[3]}:{$realCropData[0]}:{$realCropData[1]}\" -vframes 1 {$this->tmpDir}/screenshot.jpeg";
-    }
-
-    exec($command, $status);
-    if ($status) {
-      throw new AiAutomatorRequestErrorException('Could not create video screenshot.');
+      $tokens['crop'] = 'crop=' . $realCropData[2] . ':' . $realCropData[3] . ':' . $realCropData[0] . ':' . $realCropData[1];
+      $command = "-y -nostdin  -ss {timeStamp} -i {realPath} -vf {crop} -vframes 1 {screenshotFile}";
     }
-    $newFile = str_replace($video->getFilename(), $video->getFilename() . '_cut', $path);
+    // Run the command.
+    $this->runFfmpegCommand($command, $tokens, 'Could not generate screenshot from video.');
+    $newFile = str_replace($video->getFilename(), $video->getFilename() . '_cut', $path) . '.jpg';
     $newFile = preg_replace('/\.(avi|mp4|mov|wmv|flv|mkv)$/', '.jpg', $newFile);
-    $fixedFile = $this->fileSystem->move("{$this->tmpDir}/screenshot.jpeg", $newFile);
+    $fixedFile = $this->fileSystem->move("{$this->tmpDir}screenshot.jpeg", $newFile);
     $file = File::create([
       'uri' => $fixedFile,
       'status' => 1,
@@ -301,8 +310,8 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
   public function normalizeCropData(File $video, $cropData) {
     $originalWidth = 640;
     // Get the width and height of the video with FFmpeg.
-    $realPath = $this->fileSystem->realpath($video->getFileUri());
-    $command = "ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 \"$realPath\"";
+    $realPathEscaped = escapeshellarg($this->fileSystem->realpath($video->getFileUri()));
+    $command = "ffprobe -v error -select_streams v:0 -show_entries stream=width,height -of csv=s=x:p=0 $realPathEscaped";
     $result = shell_exec($command);
     [$width] = explode('x', $result);
     $ratio = $width / $originalWidth;
@@ -310,6 +319,10 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
     foreach ($cropData as $key => $value) {
       $newCropData[$key] = round($value * $ratio);
     }
+    // There should only be 4 numeric values.
+    if (count($newCropData) != 4) {
+      throw new AiAutomatorRequestErrorException('The crop data is not in the correct format.');
+    }
     return $newCropData;
   }
 
@@ -355,11 +368,12 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
     // Get the actual file path on the server.
     $realPath = $this->fileSystem->realpath($video);
     // Let FFMPEG do its magic.
-    $command = "ffmpeg -y -nostdin  -i \"$realPath\" -c:a mp3 -b:a 64k {$this->tmpDir}/audio.mp3";
-    exec($command, $status);
-    if ($status) {
-      throw new AiAutomatorResponseErrorException('Could not generate audio from video.');
-    }
+    $command = "-y -nostdin  -i {realPath} -c:a mp3 -b:a 64k {file}";
+    $tokens = [
+      'file' => $this->tmpDir . "audio.mp3",
+      'realPath' => $realPath,
+    ];
+    $this->runFfmpegCommand($command, $tokens, 'Could not generate audio from video.');
     return '';
   }
 
@@ -370,51 +384,52 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
     // Use Whisper to transcribe and then get the segments.
     $input = [
       'model' => 'whisper-1',
-      'file' => fopen($this->tmpDir . '/audio.mp3', 'r'),
+      'file' => fopen($this->tmpDir . 'audio.mp3', 'r'),
       'response_format' => 'json',
     ];
     $instance = $this->aiPluginManager->createInstance($automatorConfig['ai_provider_audio']);
 
-    $input = new SpeechToTextInput(new AudioFile(file_get_contents($this->tmpDir . '/audio.mp3'), 'audio/mpeg', 'audio.mp3'));
+    $input = new SpeechToTextInput(new AudioFile(file_get_contents($this->tmpDir . 'audio.mp3'), 'audio/mpeg', 'audio.mp3'));
     $this->transcription = $instance->speechToText($input, $automatorConfig['ai_model_audio'])->getNormalized();
   }
 
   /**
    * Helper function to get the image raster images from the video.
    */
-  protected function createVideoRasterImages($automatorConfig, File $file, $timestamp = NULL) {
+  protected function createVideoRasterImages($automatorConfig, File $file, $timeStamp = NULL) {
     $this->images = [];
+    // Remove all the images.
     exec('rm ' . $this->tmpDir . '/*.jpeg');
     // Get the video file.
     $video = $file->getFileUri();
-    // Get the actual file path on the server.
-    $realPath = $this->fileSystem->realpath($video);
     // Let FFMPEG do its magic.
-    $command = "ffmpeg -y -nostdin  -i \"$realPath\" -vf \"select='gt(scene,0.1)',scale=640:-1,drawtext=fontsize=45:fontcolor=yellow:box=1:boxcolor=black:x=(W-tw)/2:y=H-th-10:text='%{pts\:hms}'\" -vsync vfr {$this->tmpDir}output_frame_%04d.jpeg";
+    $tokens = [
+      'tmpDir' => $this->tmpDir,
+      'realPath' => $this->fileSystem->realpath($video),
+      'thumbFile' => $this->tmpDir . 'output_frame_%04d.jpeg',
+      'rasterFile' => $this->tmpDir . 'raster-%04d.jpeg',
+      'videoFile' => $this->tmpDir . 'tmpVideo.mp4',
+    ];
+    $command = "-y -nostdin  -i {realPath} -vf \"select='gt(scene,0.1)',scale=640:-1,drawtext=fontsize=45:fontcolor=yellow:box=1:boxcolor=black:x=(W-tw)/2:y=H-th-10:text='%{pts\:hms}'\" -vsync vfr {thumbFile}";
     // If its timestamp, just get 0.5 seconds before and after.
-    if ($timestamp) {
-      $command = "ffmpeg -y -nostdin -ss " . $timestamp . " -i \"$realPath\" -t 3 -vf \"scale=640:-1,drawtext=fontsize=45:fontcolor=yellow:box=1:boxcolor=black:x=(W-tw)/2:y=H-th-10:text='%{pts\:hms}'\" -vsync vfr {$this->tmpDir}output_frame_%04d.jpeg";
+    $tokens['timeStamp'] = $this->cleanTimestamp($timeStamp);
+    if ($tokens['timeStamp']) {
+      $command = "-y -nostdin -ss {timeStamp} -i {realPath} -t 3 -vf \"scale=640:-1,drawtext=fontsize=45:fontcolor=yellow:box=1:boxcolor=black:x=(W-tw)/2:y=H-th-10:text='%{pts\:hms}'\" -vsync vfr {thumbFile}";
     }
+    $this->runFfmpegCommand($command, $tokens, 'Could not generate images from video.');
 
-    exec($command, $status);
-    // If it failed, give up.
-    if ($status) {
-      throw new AiAutomatorResponseErrorException('Could not create video thumbs.');
-    }
-    $rasterCommand = "ffmpeg -i {$this->tmpDir}/output_frame_%04d.jpeg -filter_complex \"scale=640:-1,tile=3x3:margin=10:padding=4:color=white\" {$this->tmpDir}/raster-%04d.jpeg";
-    exec($rasterCommand, $status);
-    // If it failed, give up.
-    if ($status) {
-      throw new AiAutomatorResponseErrorException('Could not create video raster.');
-    }
+    $rasterCommand = "-i {thumbFile} -filter_complex \"scale=640:-1,tile=3x3:margin=10:padding=4:color=white\" {rasterFile}";
+    $this->runFfmpegCommand($rasterCommand, $tokens, 'Could not create video raster.');
     $images = glob($this->tmpDir . 'raster-*.jpeg');
     foreach ($images as $uri) {
-      $this->images[] = 'data:image/jpeg;base64,' . base64_encode(file_get_contents($uri));
+      $image = new ImageFile();
+      $image->setFileFromUri($uri);
+      $this->images[] = $image;
     }
     // If timestamp also generate a temp video.
-    if ($timestamp) {
-      $command = "ffmpeg -y -nostdin -ss " . $timestamp . " -i \"$realPath\" -t 3 -c:v libx264 -qscale 0 {$this->tmpDir}tmpVideo.mp4";
-      exec($command, $status);
+    if ($tokens['timeStamp']) {
+      $command = "-y -nostdin -ss {timeStamp} -i {realPath} -t 3 -c:v libx264 -qscale 0 {videoFile}";
+      $this->runFfmpegCommand($command, $tokens, 'Could not generate video from video.');
       $this->video = "{$this->tmpDir}tmpVideo.mp4";
     }
   }
@@ -423,9 +438,9 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
    * Helper function to generate a temp directory.
    */
   protected function createTempDirectory() {
-    $this->tmpDir = $this->fileSystem->getTempDirectory() . '/' . mt_rand(10000, 99999) . '/';
+    $this->tmpDir = $this->fileSystem->getTempDirectory() . '/' . $this->tmpSubDir . '/' . mt_rand(10000, 99999) . '/';
     if (!file_exists($this->tmpDir)) {
-      $this->fileSystem->mkdir($this->tmpDir);
+      $this->fileSystem->mkdir($this->tmpDir, NULL, TRUE);
     }
   }
 
@@ -478,4 +493,67 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
     return substr($date->format('H:i:s.u'), 0, -3);
   }
 
+  /**
+   * Clean up timestamp.
+   *
+   * @param string|null $timeStamp
+   *   The timestamp.
+   *
+   * @return string
+   *   The cleaned up timestamp.
+   */
+  public function cleanTimestamp(?string $timeStamp = NULL) {
+    // If its null, just return it.
+    if (!$timeStamp) {
+      return NULL;
+    }
+    // Make sure it follows the h:i:s.ms, since we can't escape this.
+    if (!preg_match('/^(\d{1,2}:\d{2}:\d{2}\.\d{2,3})$/', $timeStamp)) {
+      throw new AiAutomatorRequestErrorException('The timestamp is not in the correct format.');
+    }
+    return $timeStamp;
+  }
+
+  /**
+   * Run FFMPEG command.
+   *
+   * @param string $command
+   *   The command to run.
+   * @param array $tokens
+   *   The tokens to replace.
+   * @param string $error_message
+   *   Error message to throw if it fails.
+   */
+  public function runFfmpegCommand($command, array $tokens, $error_message) {
+    $command = $this->prepareFfmpegCommand($command, $tokens);
+    exec($command, $status);
+    if ($status) {
+      throw new AiAutomatorResponseErrorException($error_message);
+    }
+  }
+
+  /**
+   * Prepare the FFMPEG command.
+   *
+   * @param string $command
+   *   The command to run.
+   * @param array $tokens
+   *   The tokens to replace.
+   *
+   * @return string
+   *   The prepared command.
+   */
+  public function prepareFfmpegCommand($command, array $tokens) {
+    foreach ($tokens as $token => $value) {
+      // Only escape if it is not empty.
+      if (empty($value)) {
+        continue;
+      }
+      $escaped_value = escapeshellarg($value);
+      $command = str_replace("{{$token}}", $escaped_value, $command);
+    }
+    // @todo Add full path to ffmpeg.
+    return "ffmpeg $command";
+  }
+
 }
-- 
GitLab


From 4d5cd947c02a9795fd5069c16460f968e30d499a Mon Sep 17 00:00:00 2001
From: Marcus Johansson <me@marcusmailbox.com>
Date: Wed, 5 Mar 2025 12:32:48 +0100
Subject: [PATCH 5/8] Use 10.3+ code for renaming files in video to image

---
 .../AiAutomatorType/LlmVideoToImage.php       |  4 +-
 .../src/PluginBaseClasses/VideoToText.php     | 38 +++++++++++++++----
 2 files changed, 32 insertions(+), 10 deletions(-)

diff --git a/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToImage.php b/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToImage.php
index 2ea9cb261..0c5af3672 100644
--- a/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToImage.php
+++ b/modules/ai_automators/src/Plugin/AiAutomatorType/LlmVideoToImage.php
@@ -4,7 +4,7 @@ namespace Drupal\ai_automators\Plugin\AiAutomatorType;
 
 use Drupal\Core\Entity\ContentEntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
-use Drupal\Core\File\FileSystemInterface;
+use Drupal\Core\File\FileExists;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Drupal\ai\OperationType\Chat\ChatInput;
@@ -200,7 +200,7 @@ class LlmVideoToImage extends VideoToText implements AiAutomatorTypeInterface {
       }
 
       // Move the file to the correct place.
-      $fixedFile = $this->fileSystem->copy($tmpName, $newFile, FileSystemInterface::EXISTS_RENAME);
+      $fixedFile = $this->fileSystem->copy($tmpName, $newFile, FileExists::Rename);
 
       // Generate the new file entity.
       $file = File::create([
diff --git a/modules/ai_automators/src/PluginBaseClasses/VideoToText.php b/modules/ai_automators/src/PluginBaseClasses/VideoToText.php
index 081c0f509..494763626 100644
--- a/modules/ai_automators/src/PluginBaseClasses/VideoToText.php
+++ b/modules/ai_automators/src/PluginBaseClasses/VideoToText.php
@@ -56,11 +56,6 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
    */
   public string $tmpDir;
 
-  /**
-   * The directory inside the subdirectory, to use for security.
-   */
-  public string $tmpSubDir = 'ai_automator';
-
   /**
    * The images.
    */
@@ -171,7 +166,7 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
    */
   public function __destruct() {
     if (!empty($this->tmpDir) && file_exists($this->tmpDir)) {
-      exec('rm -rf ' . $this->tmpDir);
+      $this->deleteFilesFromTmpDir('', TRUE);
     }
   }
 
@@ -399,7 +394,7 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
   protected function createVideoRasterImages($automatorConfig, File $file, $timeStamp = NULL) {
     $this->images = [];
     // Remove all the images.
-    exec('rm ' . $this->tmpDir . '/*.jpeg');
+    $this->deleteFilesFromTmpDir('jpeg');
     // Get the video file.
     $video = $file->getFileUri();
     // Let FFMPEG do its magic.
@@ -438,7 +433,7 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
    * Helper function to generate a temp directory.
    */
   protected function createTempDirectory() {
-    $this->tmpDir = $this->fileSystem->getTempDirectory() . '/' . $this->tmpSubDir . '/' . mt_rand(10000, 99999) . '/';
+    $this->tmpDir = $this->fileSystem->getTempDirectory() . '/ai_automator/' . mt_rand(10000, 99999) . '/';
     if (!file_exists($this->tmpDir)) {
       $this->fileSystem->mkdir($this->tmpDir, NULL, TRUE);
     }
@@ -556,4 +551,31 @@ class VideoToText extends RuleBase implements ContainerFactoryPluginInterface {
     return "ffmpeg $command";
   }
 
+  /**
+   * Delete files from the tmp dir.
+   *
+   * @param string $ext
+   *   The extension to delete.
+   * @param bool $remove_directory
+   *   If the directory should be removed.
+   */
+  public function deleteFilesFromTmpDir($ext = '', $remove_directory = FALSE) {
+    // Get the actual tmp directory, to make sure nothing was injected.
+    $tmpDir = $this->fileSystem->getTempDirectory() . '/ai_automator';
+
+    foreach (scandir($tmpDir) as $file) {
+      if ($file == '.' || $file == '..') {
+        continue;
+      }
+      if ($ext && pathinfo($file, PATHINFO_EXTENSION) != $ext) {
+        continue;
+      }
+      unlink($tmpDir . $file);
+    }
+
+    if ($remove_directory) {
+      rmdir($tmpDir);
+    }
+  }
+
 }
-- 
GitLab


From f87e87d0981af5b9dee9f63496e0f5ecdad3b832 Mon Sep 17 00:00:00 2001
From: Artem  Dmitriiev <a.dmitriiev@1xinternet.de>
Date: Tue, 11 Mar 2025 18:05:59 +0100
Subject: [PATCH 6/8] Issue #3512278 by a.dmitriiev: Spell Fix plugin has
 provider as required field

---
 .../src/Plugin/AiCKEditor/Completion.php            |  5 +++++
 .../src/Plugin/AiCKEditor/ReformatHtml.php          |  6 +++++-
 .../ai_ckeditor/src/Plugin/AiCKEditor/SpellFix.php  | 13 ++++++++++---
 .../ai_ckeditor/src/Plugin/AiCKEditor/Summarize.php |  6 +++++-
 modules/ai_ckeditor/src/Plugin/AiCKEditor/Tone.php  | 10 +++++++---
 .../ai_ckeditor/src/Plugin/AiCKEditor/Translate.php | 10 +++++++---
 6 files changed, 39 insertions(+), 11 deletions(-)

diff --git a/modules/ai_ckeditor/src/Plugin/AiCKEditor/Completion.php b/modules/ai_ckeditor/src/Plugin/AiCKEditor/Completion.php
index abff602df..89a73e1da 100644
--- a/modules/ai_ckeditor/src/Plugin/AiCKEditor/Completion.php
+++ b/modules/ai_ckeditor/src/Plugin/AiCKEditor/Completion.php
@@ -42,6 +42,11 @@ final class Completion extends AiCKEditorPluginBase {
       '#title' => $this->t('Completion pre prompt'),
       '#default_value' => $prompt_complete ?? '',
       '#description' => $this->t('This prompt will be prepended before the user prompt. This field may be left empty too.'),
+      '#states' => [
+        'required' => [
+          ':input[name="editor[settings][plugins][ai_ckeditor_ai][plugins][ai_ckeditor_completion][enabled]"]' => ['checked' => TRUE],
+        ],
+      ],
     ];
 
     return $form;
diff --git a/modules/ai_ckeditor/src/Plugin/AiCKEditor/ReformatHtml.php b/modules/ai_ckeditor/src/Plugin/AiCKEditor/ReformatHtml.php
index 4a788ef3b..ceb5a5ff2 100644
--- a/modules/ai_ckeditor/src/Plugin/AiCKEditor/ReformatHtml.php
+++ b/modules/ai_ckeditor/src/Plugin/AiCKEditor/ReformatHtml.php
@@ -49,9 +49,13 @@ final class ReformatHtml extends AiCKEditorPluginBase {
     $form['prompt'] = [
       '#type' => 'textarea',
       '#title' => $this->t('Reformat prompt'),
-      '#required' => TRUE,
       '#default_value' => $prompt_reformat,
       '#description' => $this->t('This prompt will be used to reformat the html.'),
+      '#states' => [
+        'required' => [
+          ':input[name="editor[settings][plugins][ai_ckeditor_ai][plugins][ai_ckeditor_reformat_html][enabled]"]' => ['checked' => TRUE],
+        ],
+      ],
     ];
 
     return $form;
diff --git a/modules/ai_ckeditor/src/Plugin/AiCKEditor/SpellFix.php b/modules/ai_ckeditor/src/Plugin/AiCKEditor/SpellFix.php
index b0799b2b7..f2ab0b0b8 100644
--- a/modules/ai_ckeditor/src/Plugin/AiCKEditor/SpellFix.php
+++ b/modules/ai_ckeditor/src/Plugin/AiCKEditor/SpellFix.php
@@ -35,11 +35,14 @@ final class SpellFix extends AiCKEditorPluginBase {
    * {@inheritdoc}
    */
   public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
+    $options = $this->aiProviderManager->getSimpleProviderModelOptions('chat');
+    array_shift($options);
+    array_splice($options, 0, 1);
     $form['provider'] = [
       '#type' => 'select',
       '#title' => $this->t('AI provider'),
-      '#options' => $this->aiProviderManager->getSimpleProviderModelOptions('chat'),
-      '#required' => TRUE,
+      '#options' => $options,
+      "#empty_option" => $this->t('-- Default from AI module (chat) --'),
       '#default_value' => $this->configuration['provider'] ?? $this->aiProviderManager->getSimpleDefaultProviderOptions('chat'),
       '#description' => $this->t('Select which provider to use for this plugin. See the <a href=":link">Provider overview</a> for details about each provider.', [':link' => '/admin/config/ai/providers']),
     ];
@@ -48,9 +51,13 @@ final class SpellFix extends AiCKEditorPluginBase {
     $form['prompt'] = [
       '#type' => 'textarea',
       '#title' => $this->t('Spelling fix prompt'),
-      '#required' => TRUE,
       '#default_value' => $prompt_fix_spelling,
       '#description' => $this->t('This prompt will be used to fix the spelling.'),
+      '#states' => [
+        'required' => [
+          ':input[name="editor[settings][plugins][ai_ckeditor_ai][plugins][ai_ckeditor_spellfix][enabled]"]' => ['checked' => TRUE],
+        ],
+      ],
     ];
 
     return $form;
diff --git a/modules/ai_ckeditor/src/Plugin/AiCKEditor/Summarize.php b/modules/ai_ckeditor/src/Plugin/AiCKEditor/Summarize.php
index 58449698f..a9aa055f1 100644
--- a/modules/ai_ckeditor/src/Plugin/AiCKEditor/Summarize.php
+++ b/modules/ai_ckeditor/src/Plugin/AiCKEditor/Summarize.php
@@ -48,9 +48,13 @@ final class Summarize extends AiCKEditorPluginBase {
     $form['prompt'] = [
       '#type' => 'textarea',
       '#title' => $this->t('Summarise prompt'),
-      '#required' => TRUE,
       '#default_value' => $prompt_summarise,
       '#description' => $this->t('This prompt will be used to summarise the text.'),
+      '#states' => [
+        'required' => [
+          ':input[name="editor[settings][plugins][ai_ckeditor_ai][plugins][ai_ckeditor_summarize][enabled]"]' => ['checked' => TRUE],
+        ],
+      ],
     ];
     return $form;
   }
diff --git a/modules/ai_ckeditor/src/Plugin/AiCKEditor/Tone.php b/modules/ai_ckeditor/src/Plugin/AiCKEditor/Tone.php
index 848b6be7c..14b0e67e8 100644
--- a/modules/ai_ckeditor/src/Plugin/AiCKEditor/Tone.php
+++ b/modules/ai_ckeditor/src/Plugin/AiCKEditor/Tone.php
@@ -88,9 +88,13 @@ final class Tone extends AiCKEditorPluginBase {
     $form['prompt'] = [
       '#type' => 'textarea',
       '#title' => $this->t('Change tone prompt'),
-      '#required' => TRUE,
       '#default_value' => $prompt_tone,
       '#description' => $this->t('This prompt will be used to change the tone of voice. {{ tone }} is the target tone of voice that is chosen.'),
+      '#states' => [
+        'required' => [
+          ':input[name="editor[settings][plugins][ai_ckeditor_ai][plugins][ai_ckeditor_tone][enabled]"]' => ['checked' => TRUE],
+        ],
+      ],
     ];
 
     return $form;
@@ -211,8 +215,8 @@ final class Tone extends AiCKEditorPluginBase {
       $prompts_config = $this->getConfigFactory()->get('ai_ckeditor.settings');
       $prompt = $prompts_config->get('prompts.tone');
       $prompt = str_replace('{{ tone }}', $term->label(), $prompt);
-      if ($this->configuration['use_description'] && !empty($term->description->value)) {
-        $prompt .= 'That tone can described as: ' . strip_tags($term->description->value);
+      if ($this->configuration['use_description'] && !empty($term->getDescription())) {
+        $prompt .= 'That tone can described as: ' . strip_tags($term->getDescription());
       }
       $prompt .= "\n\nThe text that we want to change is the following:\n" . $values["plugin_config"]["selected_text"];
       $response = new AjaxResponse();
diff --git a/modules/ai_ckeditor/src/Plugin/AiCKEditor/Translate.php b/modules/ai_ckeditor/src/Plugin/AiCKEditor/Translate.php
index 716fe1692..049cb12f5 100644
--- a/modules/ai_ckeditor/src/Plugin/AiCKEditor/Translate.php
+++ b/modules/ai_ckeditor/src/Plugin/AiCKEditor/Translate.php
@@ -88,9 +88,13 @@ final class Translate extends AiCKEditorPluginBase {
     $form['prompt'] = [
       '#type' => 'textarea',
       '#title' => $this->t('Change translation prompt'),
-      '#required' => TRUE,
       '#default_value' => $prompt_translate,
       '#description' => $this->t('This prompt will be used to translate the text. {{ tone }} is the target tone of voice that is chosen.'),
+      '#states' => [
+        'required' => [
+          ':input[name="editor[settings][plugins][ai_ckeditor_ai][plugins][ai_ckeditor_translate][enabled]"]' => ['checked' => TRUE],
+        ],
+      ],
     ];
 
     return $form;
@@ -204,8 +208,8 @@ final class Translate extends AiCKEditorPluginBase {
       $prompts_config = $this->getConfigFactory()->get('ai_ckeditor.settings');
       $prompt = $prompts_config->get('prompts.translate');
       $prompt = str_replace('{{ lang }}', $term->label(), $prompt);
-      if ($this->configuration['use_description'] && !empty($term->description->value)) {
-        $prompt .= 'Think about the following when translating it into ' . $term->label() . ': ' . strip_tags($term->description->value);
+      if ($this->configuration['use_description'] && !empty($term->getDescription())) {
+        $prompt .= 'Think about the following when translating it into ' . $term->label() . ': ' . strip_tags($term->getDescription());
       }
       $prompt .= "\n\nThe text that we want to translate is the following:\n" . $values["plugin_config"]["selected_text"];
       $response = new AjaxResponse();
-- 
GitLab


From 73c4536ba7cd86f0da5db6f0fd31060f52a57897 Mon Sep 17 00:00:00 2001
From: andrewbelcher <andrewbelcher@655282.no-reply.drupal.org>
Date: Wed, 12 Mar 2025 15:55:22 +0000
Subject: [PATCH 7/8] Issue #3512505 by andrewbelcher: Exit early in
 SetupAiProvider to avoid errors if a provider isn't available.

---
 src/Plugin/ConfigAction/SetupAiProvider.php | 20 +++++++++++++-------
 1 file changed, 13 insertions(+), 7 deletions(-)

diff --git a/src/Plugin/ConfigAction/SetupAiProvider.php b/src/Plugin/ConfigAction/SetupAiProvider.php
index 11d9f0f1b..eacad19f5 100644
--- a/src/Plugin/ConfigAction/SetupAiProvider.php
+++ b/src/Plugin/ConfigAction/SetupAiProvider.php
@@ -51,8 +51,18 @@ final class SetupAiProvider implements ConfigActionPluginInterface, ContainerFac
     assert(is_array($value));
     // Provider has to be set.
     assert(isset($value['provider']));
+
+    // If the value is empty, we can still try to get it from environment vars.
+    if ((empty($value['key_value']) || str_starts_with($value['key_value'], "\${")) && isset($value['env_var'])) {
+      $value['key_value'] = getenv($value['env_var']);
+    }
+
+    // Stop if we don't have a key for this provider.
+    if (empty($value['key_value'])) {
+      return;
+    }
+
     // Load the provider.
-    $provider = [];
     try {
       $provider = $this->aiProviderPluginManager->createInstance($value['provider']);
     }
@@ -63,12 +73,8 @@ final class SetupAiProvider implements ConfigActionPluginInterface, ContainerFac
     if ($provider->getSetupData()) {
       $setupData = $provider->getSetupData();
     }
-    // If the value is empty, we can still try to get it from environment vars.
-    if ((empty($value['key_value']) || str_starts_with($value['key_value'], "\${")) && isset($value['env_var'])) {
-      $value['key_value'] = getenv($value['env_var']);
-    }
-    if (isset($value['provider']) && !empty($value['key_value']) && !empty($setupData['key_config_name'])) {
-      // Create a key.
+    if (!empty($setupData['key_config_name'])) {
+      // Create a key and set against the provider config.
       $key = $this->createKeyFromApiKey($value['key_name'], $value['key_label'], $value['key_value']);
 
       $this->simpleConfigUpdate->apply($configName, [
-- 
GitLab


From 2c4c3c51e68c64217d1f310c842d355ee51b327f Mon Sep 17 00:00:00 2001
From: Frederik Wouters <woutefr@cronos.be>
Date: Fri, 14 Mar 2025 17:27:54 +0100
Subject: [PATCH 8/8] processed remarks

---
 .../src/Base/EmbeddingStrategyPluginBase.php         | 12 +++---------
 1 file changed, 3 insertions(+), 9 deletions(-)

diff --git a/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php b/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
index 3a266f439..e127dbb1f 100644
--- a/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
+++ b/modules/ai_search/src/Base/EmbeddingStrategyPluginBase.php
@@ -125,12 +125,7 @@ abstract class EmbeddingStrategyPluginBase implements EmbeddingStrategyInterface
     $this->textChunker->setModel($chat_model_id);
     /** @var \Drupal\ai\OperationType\Embeddings\EmbeddingsInterface $embeddingLlm */
     $this->embeddingLlm = $this->aiProviderManager->createInstance($this->providerId);
-    if (!empty($configuration['skip_moderation']) && is_numeric($configuration['skip_moderation'])) {
-      $this->skipModeration = (bool) $configuration['skip_moderation'];
-    }
-    else {
-      $this->skipModeration = FALSE;
-    }
+    $this->skipModeration = !empty($configuration['skip_moderation']);
     if (!empty($configuration['chunk_size']) && is_numeric($configuration['chunk_size'])) {
       $this->chunkSize = (int) $configuration['chunk_size'];
     }
@@ -194,9 +189,8 @@ abstract class EmbeddingStrategyPluginBase implements EmbeddingStrategyInterface
     }
 
     $form['skip_moderation'] = [
-      '#title' => $this->t('Skip moderation for embeddings. (DO NOT CHECK THIS)'),
-      '#description' => $this->t('ONLY CHECK THIS IF YOU ARE 100% SURE WHAT YOU ARE DOING!'),
-      '#required' => TRUE,
+      '#title' => $this->t('Skip moderation for embeddings (only for advanced use cases).'),
+      '#description' => $this->t('Only check this if you are 100% sure what you are doing.'),
       '#type' => 'checkbox',
       '#default_value' => $configuration['skip_moderation'] ?? FALSE,
     ];
-- 
GitLab