elevenlabs_field-1.0.0-beta7/src/Plugin/Field/FieldWidget/ElevenLabsGenerationWidget.php

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

namespace Drupal\elevenlabs_field\Plugin\Field\FieldWidget;

use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\NestedArray;
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\elevenlabs_field\ElevenLabsApiService;
use Drupal\file\Plugin\Field\FieldWidget\FileWidget;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\ConstraintViolationInterface;

/**
 * Defines the 'elevenlabs' field widget.
 *
 * @FieldWidget(
 *   id = "elevenlabs",
 *   label = @Translation("ElevenLabs"),
 *   field_types = {"elevenlabs", "file"},
 * )
 */
class ElevenLabsGenerationWidget extends FileWidget {

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

  /**
   * Values and defaults.
   */
  public array $elevenData = [
    'text' => '',
    'speaker' => '',
    'start_time' => 0,
    'playing_time' => 0,
    'target_id' => NULL,
    'should_change' => 0,
    'model_id' => 'auto',
    'stability' => 50,
    'similarity_boost' => 50,
    'style_exaggeration' => 50,
    'speaker_boost' => FALSE,
    'history_item_id' => FALSE,
  ];

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

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

  /**
   * ElevenLabs API.
   */
  protected ElevenLabsApiService $elevenLabsApi;

  /**
   * {@inheritDoc}
   */
  public function __construct(
    $plugin_id,
    $plugin_definition,
    FieldDefinitionInterface $field_definition,
    array $settings,
    array $third_party_settings,
    ElementInfoManagerInterface $elementInfo,
    ElevenLabsApiService $elevenLabsApi
  ) {
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $elementInfo);
    $this->elevenLabsApi = $elevenLabsApi;
    $this->voices = $this->elevenLabsApi->getVoices();
    $this->models = $this->elevenLabsApi->getModels();
  }

  /**
   * {@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('elevenlabs_field.api_service'),
    );
  }

  /**
   * {@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) {
    $settings = $this->getSettings();
    $fieldName = $this->fieldDefinition->getName();
    $widgetState = static::getWidgetState($form['#parents'], $fieldName, $formState);
    foreach ($this->elevenData 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'],
    ];

    $element['speaker'] = [
      '#type' => 'select',
      '#title' => $this->t('Speaker'),
      '#options' => $this->getVoicesOptionsArray(),
      '#default_value' => $widgetState['audios'][$delta]['speaker'],
    ];

    $element['playing_time'] = [
      '#type' => 'hidden',
      '#title' => $this->t('Playing Time (ms)'),
      '#default_value' => $widgetState['audios'][$delta]['playing_time'],
    ];

    if (!empty($widgetState['audios'][$delta]['target_id'])) {
      $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,
      '#submit' => [[$this, 'generateSubmit']],
      '#attributes' => [
        'data-delta' => $delta,
      ],
      '#weight' => 3,
      '#access' => TRUE,
    ];

    $element['start_time'] = [
      '#type' => $settings['show_start_time'] ? 'number' : 'hidden',
      '#title' => $this->t('Start Time (ms)'),
      '#description' => $this->t('The start time in relations to the last speaker. Can be negative if they should speak over each other. Leave empty if it should just start directly after.'),
      '#default_value' => $widgetState['audios'][$delta]['start_time'],
    ];

    $element['model_id'] = [
      '#prefix' => $settings['show_advanced'] ? '<details><summary>' . $this->t('Advanced') . '</summary>' : '',
      '#weight' => 6,
      '#type' => $settings['show_advanced'] ? 'select' : 'hidden',
      '#title' => $this->t('Models'),
      '#options' => $this->getModelsOptionsArray(),
      '#default_value' => $widgetState['audios'][$delta]['model_id'],
      '#description' => $this->t('The base model to use. If you choose automatic it will choose the newest model that is covered by the entities language or fallback to the newest model.'),
    ];

    $element['stability'] = [
      '#type' => $settings['show_advanced'] ? 'number' : 'hidden',
      '#weight' => 7,
      '#title' => $this->t('Stability'),
      '#min' => 0,
      '#max' => 100,
      '#default_value' => $widgetState['audios'][$delta]['stability'],
      '#description' => $this->t('A value 0-100. Read more what this does <a href=":link" target="_blank">here</a>', [
        ':link' => 'https://docs.elevenlabs.io/speech-synthesis/voice-settings#stability',
      ]),
    ];

    $element['similarity_boost'] = [
      '#type' => $settings['show_advanced'] ? 'number' : 'hidden',
      '#weight' => 8,
      '#title' => $this->t('Similarity Boost'),
      '#min' => 0,
      '#max' => 100,
      '#default_value' => $widgetState['audios'][$delta]['similarity_boost'],
      '#description' => $this->t('A value 0-100. Read more what this does <a href=":link" target="_blank">here</a>', [
        ':link' => 'https://docs.elevenlabs.io/speech-synthesis/voice-settings#similarity',
      ]),
    ];

    $element['style_exaggeration'] = [
      '#type' => $settings['show_advanced'] ? 'number' : 'hidden',
      '#weight' => 9,
      '#title' => $this->t('Style Exaggeration'),
      '#min' => 0,
      '#max' => 100,
      '#default_value' => $widgetState['audios'][$delta]['style_exaggeration'],
      '#description' => $this->t(
        'A value 0-100. Does not exist on all models. From Docs: <em>High values are recommended if the style of the speech should be exaggerated compared to the uploaded audio. Higher values can lead to more instability in the generated speech. Setting this to 0.0 will greatly increase generation speed and is the default setting.</em>'
      ),
    ];

    $element['speaker_boost'] = [
      '#type' => $settings['show_advanced'] ? 'checkbox' : 'hidden',
      '#weight' => 10,
      '#title' => $this->t('Speaker Boost'),
      '#default_value' => $widgetState['audios'][$delta]['speaker_boost'] ? 1 : 0,
      '#description' => $this->t(
        'Does not exist on all models. From Docs: <em>Boost the similarity of the synthesized speech and the voice at the cost of some generation speed.</em>'
      ),
      '#suffix' => $settings['show_advanced'] ? '</details>' : '',
    ];

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

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

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

    $element['#theme_wrappers'] = ['container', 'form_element'];
    $element['#attributes']['class'][] = 'elevenlabs-elements';
    $element['#attached']['library'][] = 'elevenlabs_field/elevenlabs';

    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['speaker'] === '') {
        $values[$delta]['speaker'] = NULL;
      }
      if ($value['playing_time'] === '') {
        $values[$delta]['playing_time'] = NULL;
      }
    }
    return $values;
  }

  /**
   * Callback to generate the form again.
   */
  public function generateForm(&$form, FormStateInterface $formState) {
    $trigger = $formState->getTriggeringElement();
    $elements = NestedArray::getValue($form, array_slice($trigger['#array_parents'], 0, -2));
    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)) {
      $key = $delta--;
      $generator = \Drupal::service('elevenlabs_field.generator_service');
      $model = $generator->validateModel($data[$key]['model_id'], $formState->getFormObject()->getEntity());
      $elData = $generator->generateFile($data[$key]['text'], $data[$key]['speaker'], $this->fieldDefinition, $model, [
        'stability' => ($data[$key]['stability'] / 100),
        'similarity_boost' => ($data[$key]['similarity_boost'] / 100),
        'style' => ($data[$key]['style_exaggeration'] / 100),
        'use_speaker_boost' => $data[$key]['speaker_boost'],
      ]);
      if (!empty($elData['file'])) {
        // Update both widget state and form state.
        $widgetState['audios'][$key]['target_id'] = $elData['file']->id();
        $widgetState['audios'][$key]['history_item_id'] = $elData['history_item_id'];
        $data[$key]['target_id'] = $elData['file']->id();
        $data[$key]['history_item_id'] = $elData['history_item_id'];
        $data[$key]['should_change'] = FALSE;
        // Set old to used.
        $oldData = $formState->getValue($fieldName);
        $fid = $oldData[$key]['target_id'] ?? NULL;
        if ($fid) {
          $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);
    }
  }

  /**
   * Get the models.
   *
   * @return array
   *   The models.
   */
  private function getModelsOptionsArray() {
    $options[''] = $this->t('- Select a model -');
    $options['auto'] = $this->t('Automatic');
    foreach ($this->models as $model) {
      $options[$model['model_id']] = $model['name'];
    }
    return $options;
  }

  /**
   * Get the voices.
   *
   * @return array
   *   The voices.
   */
  private function getVoicesOptionsArray() {
    $options[''] = $this->t('- Select a voice -');
    foreach ($this->voices['voices'] as $voice) {
      $options[$voice['voice_id']] = $voice['name'];
    }
    return $options;
  }

}

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

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