diff --git a/ai_image.info.yml b/ai_image.info.yml index 3952180a26a06aad0d00e16dad8ff563f725eb01..ce15ed82163ce5ce9a525eebea3d8f808313616d 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 ce379288d864e202587e367aa6ecc0723ebfbc85..2fcc887565e04472ceb0de1fe012edefe5cf43ab 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 ba93156464bc219a43b885e9c18713cdbe9daeec..6478b60f12e59a111bba900f0a426819ba73ab2f 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 0f8e02a927050757789c5b123b7292dd7a7b1e3b..4396abc8080ae2fb125e3ace700eafd46b3882f7 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,80 @@ 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 = [ +// "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, + "image_size" => "1024x1024", + "size" => "1024x1024", +// "width"=> 1024, +// "height"=> 1024, + "samples"=> "1", + "steps"=> null, + "sampler"=> 'None', + "num_inference_steps"=> "20", + "seed"=> 0, + "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 f1a86f1088460cb0b764f86cce4da14fd3c7bb8e..4872b313ad00fbb4680282a0b5f2a460da75afcd 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} */ @@ -84,24 +85,16 @@ 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 generation model'), '#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 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'] = [ @@ -126,11 +119,8 @@ 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']; + _ai_image_check_default_provider_and_model(); } /**