From 3c1afbbd6e0c64d9e7598f4027d8b4ca442255df Mon Sep 17 00:00:00 2001 From: Frederik Wouters <woutefr@cronos.be> Date: Wed, 29 Jan 2025 13:32:44 +0100 Subject: [PATCH 1/2] v1 --- ai_image.info.yml | 8 +- ai_image.services.yml | 2 +- src/Controller/AIImgController.php | 52 +++++++-- src/GetAIImage.php | 146 +++++++++++++------------ src/Plugin/CKEditor5Plugin/AiImage.php | 61 +++++------ 5 files changed, 146 insertions(+), 123 deletions(-) diff --git a/ai_image.info.yml b/ai_image.info.yml index 3952180..ce15ed8 100644 --- a/ai_image.info.yml +++ b/ai_image.info.yml @@ -1,7 +1,7 @@ -name: AI Images Generator (in Ckeditor) +name: AI Image Generator Ckeditor type: module -description: Creates an image by taking user input and communicating with Open AI and Stable Diffusion APIs. +description: Creates an image in your ckeditor by taking user input and communicating with AI providers. package: AI -core_version_requirement: ^9 || ^10 +core_version_requirement: ^9 || ^10 || ^11 dependencies: - - ai:ai + - drupal:ai diff --git a/ai_image.services.yml b/ai_image.services.yml index ce37928..2fcc887 100644 --- a/ai_image.services.yml +++ b/ai_image.services.yml @@ -8,4 +8,4 @@ services: - '@file.repository' - '@file_url_generator' - '@ai.provider' - + - '@module_handler' diff --git a/src/Controller/AIImgController.php b/src/Controller/AIImgController.php index ba93156..6478b60 100644 --- a/src/Controller/AIImgController.php +++ b/src/Controller/AIImgController.php @@ -2,14 +2,16 @@ namespace Drupal\ai_image\Controller; +use Drupal\ai\AiProviderPluginManager; use Drupal\Core\Controller\ControllerBase; use Drupal\key\KeyRepositoryInterface; use Drupal\ai_image\GetAIImage; +use PhpParser\Error; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\ai\Enum\Bundles; + /** * Returns responses for AIImg routes. */ @@ -29,6 +31,13 @@ class AIImgController extends ControllerBase { */ protected $aiImageGenerator; + /** + * The AI provider manager. + * + * @var \Drupal\ai\AiProviderPluginManager + */ + protected $aiProviderManager; + /** * Constructs an AIImgController object. * @@ -37,9 +46,10 @@ class AIImgController extends ControllerBase { * @param \Drupal\ai_image\GetAIImage $aiImageGenerator * The AI image generator service. */ - public function __construct(KeyRepositoryInterface $keyRepository, GetAIImage $aiImageGenerator) { + public function __construct(KeyRepositoryInterface $keyRepository, GetAIImage $aiImageGenerator, AiProviderPluginManager $aiProviderManager) { $this->keyRepository = $keyRepository; $this->aiImageGenerator = $aiImageGenerator; + $this->aiProviderManager = $aiProviderManager; } /** @@ -48,7 +58,8 @@ class AIImgController extends ControllerBase { public static function create(ContainerInterface $container) { return new static( $container->get('key.repository'), - $container->get('ai_image.get_image') + $container->get('ai_image.get_image'), + $container->get('ai.provider'), ); } @@ -69,20 +80,41 @@ class AIImgController extends ControllerBase { * Builds the response. */ public function getimage(Request $request): JsonResponse { + $imgurl = NULL; $data = json_decode($request->getContent()); $prompt = implode(', ', [$data->prompt, $data->options->prompt_extra]); - $provider_name = $data->options->source; - $generator = $this->aiImageGenerator; - $imgurl = $generator->generateImageInAiModule($provider_name, $prompt); - if (!$imgurl) { - $imgurl = '/modules/custom/ai_image/icons/error.jpg'; + $provider_model = $data->options->source; + $ai_model = ''; + $ai_provider = ''; + try { + if ($provider_model == '' || $provider_model == '000-AI-IMAGE-DEFAULT') { + if (empty($parts[0])) { + $default_model = $this->aiProviderManager->getSimpleDefaultProviderOptions('text_to_image'); + if ($default_model == "") { + throw new Error('no text-to_image_model selected and no default , can not render.'); + } + else { + $parts1 = explode('__', $default_model); + $ai_provider = $parts1[0]; + $ai_model = $parts1[1]; + } + } + } + else { + $parts = explode('__', $provider_model); + $ai_provider = $parts[0]; + $ai_model = $parts[1]; + } + $imgurl = $this->aiImageGenerator->getImage($ai_provider, $ai_model, $prompt); + } catch (Exception $exception) { + $path = \Drupal::service('extension.list.module')->getPath('ai_image'); + $imgurl = '/' . $path . '//icons/error.jpg'; } return new JsonResponse( [ 'text' => trim($imgurl), - ], + ] ); } - } diff --git a/src/GetAIImage.php b/src/GetAIImage.php index 0f8e02a..1e60376 100644 --- a/src/GetAIImage.php +++ b/src/GetAIImage.php @@ -2,7 +2,9 @@ namespace Drupal\ai_image; +use Drupal\ai\AiProviderPluginManager; use Drupal\ai\OperationType\TextToImage\TextToImageInput; +use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\State\StateInterface; @@ -51,6 +53,21 @@ class GetAIImage { */ protected $fileUrlGenerator; + /** + * The provider plugin manager. + * + * @var \Drupal\ai\AiProviderPluginManager + */ + protected $aiProviderManager; + + /** + * The module handler service. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** * Constructs a GetAIImage object. * @@ -65,92 +82,77 @@ class GetAIImage { * @param \Drupal\Core\UrlGeneratorInterface $fileUrlGenerator * The file URL generator service. */ - public function __construct(StateInterface $state, LoggerChannelFactoryInterface $loggerFactory, FileSystemInterface $fileSystem, FileRepositoryInterface $fileRepository, FileUrlGenerator $fileUrlGenerator) { + public function __construct(StateInterface $state, LoggerChannelFactoryInterface $loggerFactory, FileSystemInterface $fileSystem, FileRepositoryInterface $fileRepository, FileUrlGenerator $fileUrlGenerator, AiProviderPluginManager $ai_provider_manager,ModuleHandlerInterface $module_handler ) { $this->state = $state; $this->logger = $loggerFactory->get('ai_image'); $this->fileSystem = $fileSystem; $this->fileRepository = $fileRepository; $this->fileUrlGenerator = $fileUrlGenerator; + $this->aiProviderManager = $ai_provider_manager; + $this->moduleHandler = $module_handler; } /** - * Generate the image in the AI provider. + * {@inheritdoc} * - * @param $provider_name - * @param $prompt + * @param String $prompt + * The prompt string. + * @param String $api + * The image generation engine. + * @param String $api_key + * API secret key. * - * @return \Drupal\Core\GeneratedUrl|string - * @throws \Drupal\Core\Entity\EntityStorageException + * @return int + * The count of rows processed */ - public function generateImageInAiModule($provider_name, $prompt) { - $service = \Drupal::service('ai.provider'); - if ($provider_name == '000-AI-IMAGE-DEFAULT') { - $ai_config = \Drupal::service('config.factory')->get('ai.settings'); - $default_providers = $ai_config->get('default_providers') ?? []; - $ai_provider = $service->createInstance($default_providers['text_to_image']['provider_id']); - $default_model = $default_providers['text_to_image']['model_id']; - } - else { - $ai_provider = $service->createInstance($provider_name); - // TODO if no $default_model how to define this? via the ckeditor admin? - } - $config = [ - "n" => 1, - //"response_format" => "b64_json", - "response_format" => "url", - //"size" => "1792x1024", - "size" => "1024x1024", - "quality" => "standard", - "style" => "vivid", - ]; - $tags = ["tag_1", "tag_2"]; - try { - $ai_provider->setConfiguration($config); - $input = new TextToImageInput($prompt); - $response = $ai_provider->textToImage($input, $default_model, $tags); - $url = $this->saveAndGetImageUrl($response); - - if ($url) { - $this->state->set('recent_image', $url); - $this->state->set('recent_prompt', $prompt); - return $url; - } - else { - return FALSE; - } - } catch (Drupal\ai\Exception\AiUnsafePromptException $e) { - // TODO should maybe be notified in ckeditor? - return FALSE; - } + public function getImage(string $provider, string $model, string $prompt) { + return $this->getAiIMage($provider, $model, $prompt); } - /*** - * Generate a URL for this generated image. - * - * @param $response - * - * @return \Drupal\Core\GeneratedUrl|string - */ - private function saveAndGetImageUrl($response) { - $rand = time() . '-' . rand(0, 10000); - $file_name = $rand . ".png"; - $directory = 'public://ai_image_gen_images/'; - $file_path = $directory . $file_name; - - $file_system = $this->fileSystem; - $file_system->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS); - - $image_abstractions = $response->getNormalized(); - $images = []; - foreach ($image_abstractions as $image_abstraction) { - $images[] = $image_abstraction->getAsFileEntity($file_path); + private function getAiIMage($provider, $model, $prompt) { + $config = []; + if ($provider == 'openai') { + $config = [ + "n" => 1, + "response_format" => "url", + "size" => '1024x1024', + "quality" => "standard", + "style" => "vivid", + ]; } - if (isset($images[0])) { - $image_path = $images[0]->getFileUri(); - return $this->fileUrlGenerator - ->generate($image_path) - ->toString(); + if (str_contains($model,'stable-diffusion')) { + $config = [ + "response_format" => "url", + "negative_prompt"=> "((out of frame)), ((extra fingers)), mutated hands, ((poorly drawn hands)), ((poorly drawn face)), (((mutation))), (((deformed))), (((tiling))), ((naked)), ((tile)), ((fleshpile)), ((ugly)), (((abstract))), blurry, ((bad anatomy)), ((bad proportions)), ((extra limbs)), cloned face, (((skinny))), glitchy, ((extra breasts)), ((double torso)), ((extra arms)), ((extra hands)), ((mangled fingers)), ((missing breasts)), (missing lips), ((ugly face)), ((fat)), ((extra legs)), anime", + "cfg_scale" => null, + "width"=> "768", + "height"=> "768", + "samples"=> "1", + "steps"=> null, + "sampler"=> 'None', + "num_inference_steps"=> "20", + "seed"=> null, + "guidance_scale"=> 7.5, + "webhook"=> null, + "track_id"=> null, + "accept" => "image/jpeg", + "output_image_format" => 'JPG', + ]; } - return FALSE; + + // Allow overriding of the config passed in to the AI image generation. + $hook = 'ai_image_alter_config'; + $this->moduleHandler->invokeAllWith($hook, function (callable $hook, string $module) use (&$config, $model, $provider) { + $config = $hook(); + }); + + $ai_provider = $this->aiProviderManager->createInstance($provider); + $ai_provider->setConfiguration($config); + $input = new TextToImageInput($prompt); + // This gets an array of \Drupal\ai\OperationType\GenericType\ImageFile. + $normalized = $ai_provider->textToImage($input, $model, ["ai_image"])->getNormalized(); + $file = $normalized[0]->getAsFileEntity("public://", "generated_image.png"); + return $this->fileUrlGenerator->generateAbsoluteString($file->getFileUri()); } } + diff --git a/src/Plugin/CKEditor5Plugin/AiImage.php b/src/Plugin/CKEditor5Plugin/AiImage.php index f1a86f1..b968750 100644 --- a/src/Plugin/CKEditor5Plugin/AiImage.php +++ b/src/Plugin/CKEditor5Plugin/AiImage.php @@ -8,25 +8,28 @@ use Drupal\ai\AiProviderPluginManager; use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait; use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault; use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface; -use Drupal\ckeditor5\Plugin\CKEditor5PluginDefinition; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Logger\LoggerChannelFactoryInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountProxyInterface; use Drupal\editor\EditorInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * CKEditor 5 OpenAI Completion plugin configuration. */ -class AiImage extends CKEditor5PluginDefault implements ContainerFactoryPluginInterface, CKEditor5PluginConfigurableInterface { +class AiImage extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface, ContainerFactoryPluginInterface { use CKEditor5PluginConfigurableTrait; /** - * The AI Provider service. + * The provider plugin manager. * * @var \Drupal\ai\AiProviderPluginManager */ - protected $providerManager; + protected AiProviderPluginManager $aiProviderManager; /** @@ -36,34 +39,32 @@ class AiImage extends CKEditor5PluginDefault implements ContainerFactoryPluginIn */ const DEFAULT_CONFIGURATION = [ 'aiimage' => [ - 'source' => '000-AI-IMAGE-DEFAULT', + 'source' => 'openai', 'prompt_extra' => 'hyper-realistic, super detailed', ], ]; - public function __construct(array $configuration, - string $plugin_id, - CKEditor5PluginDefinition $plugin_definition, - AiProviderPluginManager $provider_manager) { + /** + * {@inheritdoc} + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, AiProviderPluginManager $ai_provider_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->providerManager = $provider_manager; + $this->setConfiguration($configuration); + $this->aiProviderManager = $ai_provider_manager; } /** * {@inheritdoc} */ - public static function create(ContainerInterface $container, - array $configuration, - $plugin_id, - $plugin_definition) { + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { return new static( $configuration, $plugin_id, $plugin_definition, - $container->get('ai.provider')); + $container->get('ai.provider'), + ); } - /** * {@inheritdoc} */ @@ -75,7 +76,6 @@ class AiImage extends CKEditor5PluginDefault implements ContainerFactoryPluginIn * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - $form['aiimage'] = [ '#title' => $this->t('AI Image'), '#type' => 'details', @@ -84,26 +84,19 @@ class AiImage extends CKEditor5PluginDefault implements ContainerFactoryPluginIn '#tree' => TRUE, ]; - $providers = []; - $options['000-AI-IMAGE-DEFAULT'] = 'Default provider (configured in AI default settings)'; - foreach ($this->providerManager->getDefinitions() as $id => $definition) { - $providers[$id] = $this->providerManager->createInstance($id); - } - - foreach ($providers as $provider) { - if ($provider->isUsable('text_to_image')) { - $options[$provider->getPluginId()] = $provider->getPluginDefinition()['label']; - } - } - + $options = $this->aiProviderManager->getSimpleProviderModelOptions('text_to_image'); + array_shift($options); + array_splice($options, 0, 1); $form['aiimage']['source'] = [ '#type' => 'select', - '#title' => $this->t('AI engine'), + '#title' => $this->t('AI provider'), '#options' => $options, - '#default_value' => $this->configuration['aiimage']['source'] ?? '000-AI-IMAGE-DEFAULT', - '#description' => $this->t('Select which model to use to generate images.'), + "#empty_option" => $this->t('-- Default from AI module (text_to_image) --'), + '#default_value' => $this->configuration['aiimage']['source'] ?? $this->aiProviderManager->getSimpleDefaultProviderOptions('text_to_image'), + '#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']), ]; + $form['aiimage']['prompt_extra'] = [ '#type' => 'textfield', '#title' => $this->t('Prompt extra'), @@ -126,10 +119,6 @@ class AiImage extends CKEditor5PluginDefault implements ContainerFactoryPluginIn public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { $values = $form_state->getValues(); $this->configuration['aiimage']['source'] = $values['aiimage']['source']; - if ('000-AI-IMAGE-DEFAULT' == $this->configuration['aiimage']['source']) { - // Make sure a default is selected. - _ai_image_check_default_provider_and_model(); - } $this->configuration['aiimage']['prompt_extra'] = $values['aiimage']['prompt_extra']; } -- GitLab From f899c982e7911594ab98508fa0f8c6afb483ba51 Mon Sep 17 00:00:00 2001 From: Frederik Wouters <woutefr@cronos.be> Date: Wed, 29 Jan 2025 15:14:40 +0100 Subject: [PATCH 2/2] tested with openai and Ifreworks (Stablediffustion) --- src/GetAIImage.php | 9 ++++++--- src/Plugin/CKEditor5Plugin/AiImage.php | 7 ++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/GetAIImage.php b/src/GetAIImage.php index 1e60376..4396abc 100644 --- a/src/GetAIImage.php +++ b/src/GetAIImage.php @@ -122,16 +122,19 @@ class GetAIImage { } if (str_contains($model,'stable-diffusion')) { $config = [ +// "prompt" => $prompt, "response_format" => "url", "negative_prompt"=> "((out of frame)), ((extra fingers)), mutated hands, ((poorly drawn hands)), ((poorly drawn face)), (((mutation))), (((deformed))), (((tiling))), ((naked)), ((tile)), ((fleshpile)), ((ugly)), (((abstract))), blurry, ((bad anatomy)), ((bad proportions)), ((extra limbs)), cloned face, (((skinny))), glitchy, ((extra breasts)), ((double torso)), ((extra arms)), ((extra hands)), ((mangled fingers)), ((missing breasts)), (missing lips), ((ugly face)), ((fat)), ((extra legs)), anime", "cfg_scale" => null, - "width"=> "768", - "height"=> "768", + "image_size" => "1024x1024", + "size" => "1024x1024", +// "width"=> 1024, +// "height"=> 1024, "samples"=> "1", "steps"=> null, "sampler"=> 'None', "num_inference_steps"=> "20", - "seed"=> null, + "seed"=> 0, "guidance_scale"=> 7.5, "webhook"=> null, "track_id"=> null, diff --git a/src/Plugin/CKEditor5Plugin/AiImage.php b/src/Plugin/CKEditor5Plugin/AiImage.php index b968750..4872b31 100644 --- a/src/Plugin/CKEditor5Plugin/AiImage.php +++ b/src/Plugin/CKEditor5Plugin/AiImage.php @@ -76,6 +76,7 @@ class AiImage extends CKEditor5PluginDefault implements CKEditor5PluginConfigura * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['aiimage'] = [ '#title' => $this->t('AI Image'), '#type' => 'details', @@ -89,14 +90,13 @@ class AiImage extends CKEditor5PluginDefault implements CKEditor5PluginConfigura array_splice($options, 0, 1); $form['aiimage']['source'] = [ '#type' => 'select', - '#title' => $this->t('AI provider'), + '#title' => $this->t('AI generation model'), '#options' => $options, "#empty_option" => $this->t('-- Default from AI module (text_to_image) --'), '#default_value' => $this->configuration['aiimage']['source'] ?? $this->aiProviderManager->getSimpleDefaultProviderOptions('text_to_image'), - '#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']), + '#description' => $this->t('Select which generation model to use for this plugin. See the <a href=":link">Provider overview</a> for details about each provider.', [':link' => '/admin/config/ai/providers']), ]; - $form['aiimage']['prompt_extra'] = [ '#type' => 'textfield', '#title' => $this->t('Prompt extra'), @@ -120,6 +120,7 @@ class AiImage extends CKEditor5PluginDefault implements CKEditor5PluginConfigura $values = $form_state->getValues(); $this->configuration['aiimage']['source'] = $values['aiimage']['source']; $this->configuration['aiimage']['prompt_extra'] = $values['aiimage']['prompt_extra']; + _ai_image_check_default_provider_and_model(); } /** -- GitLab