ai_audio_field-1.0.0-rc1/src/Plugin/Field/FieldWidget/AiAudioFieldWidget.php

src/Plugin/Field/FieldWidget/AiAudioFieldWidget.php
<?php

namespace Drupal\ai_audio_field\Plugin\Field\FieldWidget;

use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Field\Attribute\FieldWidget;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\ai\AiProviderPluginManager;
use Drupal\ai\OperationType\TextToSpeech\TextToSpeechInput;
use Drupal\ai\Service\AiProviderFormHelper;
use Drupal\file\Plugin\Field\FieldWidget\FileWidget;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;

/**
 * Defines the 'ai audio field' field widget.
 */
#[FieldWidget(
  id: 'ai_audio_field_widget',
  label: new TranslatableMarkup('AI Audio Field Form'),
  field_types: ['ai_audio_file', 'file'],
)]
class AiAudioFieldWidget extends FileWidget {

  /**
   * Wrapper id.
   */
  public string $fieldWrapperId = '';

  /**
   * Values and defaults.
   */
  public array $fieldData = [
    'text' => '',
    'speaker' => '',
    'start_time' => 0,
    'playing_time' => 0,
    'target_id' => NULL,
    'provider' => '',
    'model' => '',
    'configuration' => '',
  ];

  /**
   * ElevenLabs voices.
   */
  public array $voices = [];

  /**
   * ElevenLabs models.
   */
  public array $models = [];

  /**
   * {@inheritDoc}
   */
  public function __construct(
    $plugin_id,
    $plugin_definition,
    FieldDefinitionInterface $field_definition,
    array $settings,
    array $third_party_settings,
    ElementInfoManagerInterface $elementInfo,
    protected AiProviderPluginManager $providerManager,
  ) {
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfo);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $plugin_id,
      $plugin_definition,
      $configuration['field_definition'],
      $configuration['settings'],
      $configuration['third_party_settings'],
      $container->get('element_info'),
      $container->get('ai.provider'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return [
      'show_start_time' => FALSE,
      'show_advanced' => FALSE,
    ] + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {

    $settings = $this->getSettings();

    $element['show_start_time'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show Start Time'),
      '#default_value' => $settings['show_start_time'],
    ];

    $element['show_advanced'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show Advanced Fields'),
      '#default_value' => $settings['show_advanced'],
    ];

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    $settings = $this->getSettings();
    $summary[] = $this->t("Show Start Time: @show_start_time<br>Show Advanced Fields: @show_advanced", [
      '@show_start_time' => $settings['show_start_time'] ? 'Yes' : 'No',
      '@show_advanced' => $settings['show_advanced'] ? 'Yes' : 'No',
    ]);
    return $summary;
  }

  /**
   * {@inheritDoc}
   */
  public function formMultipleElements(FieldItemListInterface $items, array &$form, FormStateInterface $formState) {
    $field_name = $this->fieldDefinition->getName();
    $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality();
    $parents = $form['#parents'];

    $id_prefix = implode('-', array_merge($parents, [$field_name]));
    $this->fieldWrapperId = Html::getUniqueId($id_prefix . '-add-more-wrapper');

    // Determine the number of widgets to display.
    $field_state = static::getWidgetState($parents, $field_name, $formState);
    switch ($cardinality) {
      case FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED:
        $max = $field_state['items_count'];
        $is_multiple = TRUE;
        break;

      default:
        $max = $cardinality - 1;
        $is_multiple = ($cardinality > 1);
        break;
    }

    $title = $this->fieldDefinition->getLabel();
    $description = $this->getFilteredDescription();

    $elements = [];

    for ($delta = 0; $delta <= $max; $delta++) {
      // Add a new empty item if it doesn't exist yet at this delta.
      if (!isset($items[$delta])) {
        $items->appendItem();
      }

      // For multiple fields, title and description are handled by the wrapping
      // table.
      if ($is_multiple) {
        $element = [
          '#title' => $this->t('@title (value @number)', [
            '@title' => $title,
            '@number' => $delta + 1,
          ]),
          '#title_display' => 'invisible',
          '#description' => '',
        ];
      }
      else {
        $element = [
          '#title' => $title,
          '#title_display' => 'before',
          '#description' => $description,
        ];
      }

      $element = $this->formSingleElement($items, $delta, $element, $form, $formState);

      if ($element) {
        // Input field for the delta (drag-n-drop reordering).
        if ($is_multiple) {
          // We name the element '_weight' to avoid clashing with elements
          // defined by widget.
          $element['_weight'] = [
            '#type' => 'weight',
            '#title' => $this->t('Weight for row @number', ['@number' => $delta + 1]),
            '#title_display' => 'invisible',
            // Note: this 'delta' is the FAPI #type 'weight' element's property.
            '#delta' => $max,
            '#default_value' => $items[$delta]->_weight ?: $delta,
            '#weight' => 100,
          ];
        }

        $elements[$delta] = $element;
      }
    }

    if ($elements) {
      $elements += [
        '#theme' => 'field_multiple_value_form',
        '#field_name' => $field_name,
        '#cardinality' => $cardinality,
        '#cardinality_multiple' => $this->fieldDefinition->getFieldStorageDefinition()->isMultiple(),
        '#required' => $this->fieldDefinition->isRequired(),
        '#title' => $title,
        '#description' => $description,
        '#max_delta' => $max,
      ];

      $elements['#prefix'] = '<div id="' . $this->fieldWrapperId . '">';
      $elements['#suffix'] = '</div>';
      // Add 'add more' button, if not working with a programmed form.
      if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED && !$formState->isProgrammed()) {
        $elements['add_more'] = [
          '#type' => 'submit',
          '#name' => strtr($id_prefix, '-', '_') . '_add_more',
          '#value' => $this->t('Add another item'),
          '#attributes' => ['class' => ['field-add-more-submit']],
          '#limit_validation_errors' => [array_merge($parents, [$field_name])],
          '#submit' => [[static::class, 'addMoreSubmit']],
          '#ajax' => [
            'callback' => [static::class, 'addMoreAjax'],
            'wrapper' => $this->fieldWrapperId,
            'effect' => 'fade',
          ],
        ];
      }
    }

    return $elements;
  }

  /**
   * {@inheritdoc}
   */
  public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $formState) {
    $fieldName = $this->fieldDefinition->getName();
    $widgetState = static::getWidgetState($form['#parents'], $fieldName, $formState);
    foreach ($this->fieldData as $id => $default) {
      if (!isset($widgetState['audios'][$delta][$id])) {
        $widgetState['audios'][$delta][$id] = $items[$delta]->{$id} ?? $default;
      }
    }
    static::setWidgetState($form['#parents'], $fieldName, $formState, $widgetState);

    // Set an update wrapper.
    $element['text'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Text'),
      '#default_value' => $widgetState['audios'][$delta]['text'],
    ];

    // Get all providers for text to speech.
    $providers = $this->providerManager->getProvidersForOperationType('text_to_speech');
    $defaults = $this->providerManager->getDefaultProviderForOperationType('text_to_speech');
    $provider_id = $defaults['provider_id'] ?? '';
    if ($widgetState['audios'][$delta]['text']) {
      $provider_id = $widgetState['audios'][$delta]['provider'];
    }

    $options = [];
    foreach ($providers as $id => $provider_definition) {
      $options[$id] = $provider_definition['label'];
    }
    $element['provider'] = [
      '#type' => 'select',
      '#required' => TRUE,
      '#title' => $this->t('Provider'),
      '#options' => $options,
      '#default_value' => $provider_id,
    ];

    // If there is set a provider, get the voices.
    $model_options = [];
    if (!empty($provider_id)) {
      $provider_instance = $this->providerManager->createInstance($provider_id);
      $model_options = $provider_instance->getConfiguredModels('text_to_speech', []);
    }

    $model_id = $defaults['model_id'] ?? '';
    if ($widgetState['audios'][$delta]['text']) {
      $model_id = $widgetState['audios'][$delta]['model'];
    }
    $configuration = Json::decode($widgetState['audios'][$delta]['configuration']) ?? [];
    $element['model'] = [
      '#type' => 'select',
      '#required' => TRUE,
      '#title' => $this->t('Model'),
      '#options' => $model_options,
      '#default_value' => $model_id,
    ];

    if ($model_id) {
      $schema = $provider_instance->getAvailableConfiguration('text_to_speech', $model_id);
      $tmp_form = [];
      $this->generateFormElements('ai', $tmp_form, AiProviderFormHelper::FORM_CONFIGURATION_FULL, $schema, $configuration);
      foreach ($tmp_form as $key => $value) {
        $element[$key] = $value;
      }
    }

    if (!empty($widgetState['audios'][$delta]['target_id'])) {
      /** @var \Drupal\file\Entity\File $file */
      $file = \Drupal::entityTypeManager()->getStorage('file')->load($widgetState['audios'][$delta]['target_id']);
      $element['play'] = [
        '#type' => 'inline_template',
        '#template' => '{{ somecontent|raw }}',
        '#context' => [
          'somecontent' => '<div><audio controls><source src="' . $file->createFileUrl(TRUE) . '" type="audio/mpeg"></div>',
        ],
        '#weight' => 2,
      ];
    }

    $element['generate_' . $delta] = [
      '#type' => 'submit',
      '#value' => !empty($widgetState['audios'][$delta]['target_id']) ? $this->t('Regenerate audio') : $this->t('Generate audio'),
      '#ajax' => [
        'callback' => [$this, 'generateForm'],
        'event' => 'click',
        'wrapper' => $this->fieldWrapperId,
      ],
      '#name' => 'generate_' . $delta,
      '#attributes' => [
        'data-delta' => $delta,
      ],
      '#weight' => 3,
      '#access' => TRUE,
    ];

    $element['target_id'] = [
      '#type' => 'hidden',
      '#default_value' => $widgetState['audios'][$delta]['target_id'],
      '#value' => $widgetState['audios'][$delta]['target_id'],
    ];

    $element['#theme_wrappers'] = ['container', 'form_element'];

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function errorElement(array $element, ConstraintViolationInterface $violation, array $form, FormStateInterface $form_state) {
    return isset($violation->arrayPropertyPath[0]) ? $element[$violation->arrayPropertyPath[0]] : $element;
  }

  /**
   * {@inheritdoc}
   */
  public function massageFormValues(array $values, array $form, FormStateInterface $form_state) {
    foreach ($values as $delta => $value) {
      if ($value['text'] === '') {
        $values[$delta]['text'] = NULL;
      }
      if ($value['provider'] === '') {
        $values[$delta]['provider'] = NULL;
      }
      if ($value['model'] === '') {
        $values[$delta]['model'] = NULL;
      }
      if (!empty($value['ai'])) {
        $values[$delta]['configuration'] = Json::encode($value['ai']);
      }
    }
    return $values;
  }

  /**
   * Callback to generate the form again.
   */
  public function generateForm(&$form, FormStateInterface $formState) {
    $this->generateSubmit($form, $formState);
    $trigger = $formState->getTriggeringElement();
    $field = array_slice($trigger['#array_parents'], 0, -2);
    $elements = NestedArray::getValue($form, array_slice($trigger['#array_parents'], 0, -2));
    foreach ($elements as $key => $value) {
      if (is_int($key)) {
        $target_id = $formState->getValue($field[0])[$key]['target_id'];
        $elements[$key]['target_id']['#value'] = $target_id;
        if ($target_id) {
          /** @var \Drupal\file\Entity\File $file */
          $file = \Drupal::entityTypeManager()->getStorage('file')->load($target_id);
          // A randomized string to avoid caching.
          $randomized = md5(uniqid());
          $elements[$key]['play'] = [
            '#type' => 'inline_template',
            '#template' => '{{ somecontent|raw }}',
            '#context' => [
              'somecontent' => '<div><audio controls><source src="' . $file->createFileUrl(TRUE) . '?rand=' . $randomized . '" type="audio/mpeg"></div>',
            ],
            '#weight' => 2,
          ];
        }
      }
    }
    $formState->setRebuild(TRUE);
    return $elements;
  }

  /**
   * Callback to generate audio.
   */
  public function generateSubmit(&$form, FormStateInterface $formState) {
    $trigger = $formState->getTriggeringElement();
    $element = NestedArray::getValue($form, array_slice($trigger['#array_parents'], 0, -2));
    $parents = $element['#field_parents'];
    $fieldName = $element['#field_name'];
    $widgetState = static::getWidgetState($parents, $fieldName, $formState);
    $data = $formState->getValue($fieldName);
    $delta = $trigger['#attributes']['data-delta'];
    if (is_numeric($delta)) {
      $oldData = $formState->getValue($fieldName);
      $key = $delta--;
      $fid = $oldData[$key]['target_id'] ?? NULL;
      $provider = $this->providerManager->createInstance($data[$key]['provider']);
      $configuration = [];
      foreach ($data[$key]['ai'] as $config_key => $value) {
        $real_key = str_replace('ai_configuration_', '', $config_key);
        $configuration[$real_key] = $value;
      }
      $input = new TextToSpeechInput($data[$key]['text']);
      $output = $provider->textToSpeech($input, $data[$key]['model'], $configuration);
      $file = $output->getNormalized()[0]->getAsFileEntity('public://', 'test.mp3');
      if (!empty($file)) {
        // Update both widget state and form state.
        $widgetState['audios'][$key]['target_id'] = $file->id();
        $data[$key]['target_id'] = $file->id();
        $data[$key]['should_change'] = FALSE;
        if ($fid) {
          /** @var \Drupal\file\Entity\File $oldFile */
          $oldFile = \Drupal::entityTypeManager()->getStorage('file')->load($fid);
          if ($oldFile) {
            $oldFile->set('status', 0);
            $oldFile->save();
          }
        }
        $formState->setValue($fieldName, $data);
      }
      $formState->setRebuild(TRUE);
      static::setWidgetState($parents, $fieldName, $formState, $widgetState);
    }
  }

  /**
   * Helper function to generate form elements from schema.
   *
   * @param string $prefix
   *   Prefix for the form elements.
   * @param array $form
   *   The form.
   * @param int $config_level
   *   The config level to return.
   * @param array $schema
   *   Configuration schema of the provider.
   * @param array $values
   *   Values of the configuration.
   */
  private function generateFormElements(string $prefix, array &$form, int $config_level, array $schema, array $values): void {
    // If there isn't a configuration or shouldn't be, return.
    if (empty($schema) || $config_level == AiProviderFormHelper::FORM_CONFIGURATION_NONE) {
      return;
    }
    foreach ($schema as $key => $definition) {
      // We skip it if it's not required and we only want required.
      if ($config_level == AiProviderFormHelper::FORM_CONFIGURATION_REQUIRED && empty($definition['required'])) {
        continue;
      }
      $set_key = $key;
      $form[$prefix][$set_key]['#type'] = $this->mapSchemaTypeToFormType($definition);
      $form[$prefix][$set_key]['#required'] = $definition['required'] ?? FALSE;
      $form[$prefix][$set_key]['#title'] = $definition['label'] ?? $key;
      $form[$prefix][$set_key]['#description'] = $definition['description'] ?? '';
      $form[$prefix][$set_key]['#default_value'] = $values[$set_key] ?? $definition['default'] ?? NULL;
      if (isset($definition['constraints'])) {
        foreach ($definition['constraints'] as $form_key => $value) {
          if ($form_key == 'options') {
            $options = array_combine($value, $value);
            if (empty($definition['required'])) {
              $options = ['' => 'Select an option'] + $options;
            }
            $form[$prefix][$set_key]['#options'] = $options;
            continue;
          }
          $form[$prefix][$set_key]['#' . $form_key] = $value;
        }
      }
    }
  }

  /**
   * Maps schema data types to form element types.
   *
   * @param array $definition
   *   Data type of a configuration value.
   *
   * @return string
   *   Type of widget.
   */
  public function mapSchemaTypeToFormType(array $definition): string {
    // Check first for settings constraints.
    if (isset($definition['constraints']['options'])) {
      return 'select';
    }
    switch ($definition['type']) {
      case 'boolean':
        return 'checkbox';

      case 'int':
      case 'float':
        return 'textfield';

      case 'string_long':
        return 'textarea';

      case 'string':
      default:
        return 'textfield';
    }
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc