elevenlabs_field-1.0.0-beta7/src/Plugin/Field/FieldType/ElevenLabsGenerationItem.php
src/Plugin/Field/FieldType/ElevenLabsGenerationItem.php
<?php namespace Drupal\elevenlabs_field\Plugin\Field\FieldType; use Drupal\Component\Utility\Random; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StreamWrapper\StreamWrapperInterface; use Drupal\Core\TypedData\DataDefinition; use Drupal\file\Plugin\Field\FieldType\FileItem; /** * Defines the 'elevenlabs' field type. * * @FieldType( * id = "elevenlabs", * label = @Translation("ElevenLabs"), * category = @Translation("Media"), * default_widget = "elevenlabs", * default_formatter = "elevenlabs_default", * column_groups = { * "file" = { * "label" = @Translation("File"), * "columns" = { * "target_id" * }, * "require_all_groups_for_translation" = TRUE * }, * "text" = { * "label" = @Translation("Alt"), * "translatable" = FALSE * }, * "speaker" = { * "label" = @Translation("Title"), * "translatable" = FALSE * }, * "playing_time" = { * "label" = @Translation("Alt"), * "translatable" = FALSE * }, * "start_time" = { * "label" = @Translation("Title"), * "translatable" = FALSE * }, * "history_item_id" = { * "label" = @Translation("Alt"), * "translatable" = FALSE * }, * "model_id" = { * "label" = @Translation("Title"), * "translatable" = FALSE * }, * "stability" = { * "label" = @Translation("Alt"), * "translatable" = FALSE * }, * "similarity_boost" = { * "label" = @Translation("Title"), * "translatable" = FALSE * }, * "style_exaggeration" = { * "label" = @Translation("Title"), * "translatable" = FALSE * }, * "speaker_boost" = { * "label" = @Translation("Title"), * "translatable" = FALSE * }, * }, * list_class = "\Drupal\elevenlabs_field\Plugin\Field\FieldType\ElevenLabsFieldItemList", * constraints = {"ReferenceAccess" = {}, "FileValidation" = {}} * ) */ class ElevenLabsGenerationItem extends FileItem { /** * {@inheritdoc} */ public static function mainPropertyName() { return 'target_id'; } /** * {@inheritdoc} */ public static function defaultStorageSettings() { $storageSettings = parent::defaultStorageSettings(); $storageSettings['target_type'] = 'file'; unset($storageSettings['display_field']); unset($storageSettings['display_default']); return $storageSettings; } /** * {@inheritdoc} */ public static function defaultFieldSettings() { $settings = parent::defaultFieldSettings(); $settings['save_audio'] = ''; $settings['concatenate'] = FALSE; $settings['batch'] = FALSE; $settings['file_extensions'] = 'mp3'; $settings['file_name'] = 'elevenlabs'; $settings['concatenate_advanced']['intro'] = FALSE; $settings['concatenate_advanced']['intro_details']['intro_field'] = ''; $settings['concatenate_advanced']['intro_details']['intro_file'] = 0; $settings['concatenate_advanced']['intro_details']['intro_volume'] = 100; $settings['concatenate_advanced']['outro'] = FALSE; $settings['concatenate_advanced']['outro_details']['outro_field'] = ''; $settings['concatenate_advanced']['outro_details']['outro_file'] = 0; $settings['concatenate_advanced']['outro_details']['outro_volume'] = 100; $settings['concatenate_advanced']['soundscape'] = FALSE; $settings['concatenate_advanced']['soundscape_details']['soundscape_field'] = ''; $settings['concatenate_advanced']['soundscape_details']['soundscape_file'] = 0; $settings['concatenate_advanced']['soundscape_details']['soundscape_cut_time'] = 0; $settings['concatenate_advanced']['soundscape_details']['soundscape_volume'] = 60; $settings['concatenate_advanced']['soundscape_details']['soundscape_start_wait'] = 3000; $settings['concatenate_advanced']['soundscape_details']['soundscape_fade_in'] = 0; $settings['concatenate_advanced']['soundscape_details']['soundscape_prolong'] = 3000; $settings['concatenate_advanced']['soundscape_details']['soundscape_fade_out'] = 3000; return $settings; } /** * {@inheritdoc} */ public function isEmpty() { if (empty($this->text)) { return TRUE; } return FALSE; } /** * {@inheritDoc} */ public function hasNewEntity() { return !$this->isEmpty() && $this->target_id === NULL; } /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { $properties = parent::propertyDefinitions($field_definition); unset($properties['display']); unset($properties['description']); $properties['text'] = DataDefinition::create('string') ->setLabel(t('Text')); $properties['speaker'] = DataDefinition::create('string') ->setLabel(t('Speaker')); $properties['playing_time'] = DataDefinition::create('integer') ->setLabel(t('Playing Time')); $properties['start_time'] = DataDefinition::create('integer') ->setLabel(t('Start Time')); $properties['history_item_id'] = DataDefinition::create('string') ->setLabel(t('History Item ID')); $properties['model_id'] = DataDefinition::create('string') ->setLabel(t('Model ID')); $properties['stability'] = DataDefinition::create('integer') ->setLabel(t('Stability')); $properties['similarity_boost'] = DataDefinition::create('integer') ->setLabel(t('Similarity Boost')); $properties['style_exaggeration'] = DataDefinition::create('integer') ->setLabel(t('Style Exaggeration')); $properties['speaker_boost'] = DataDefinition::create('integer') ->setLabel(t('Speaker Boost')); return $properties; } /** * {@inheritdoc} */ public function getValue() { if ($this->target_id) { parent::getValue(); $file = \Drupal::entityTypeManager()->getStorage('file')->load($this->target_id); $this->set('entity', $file); return [ 'target_id' => $this->target_id, 'text' => $this->text, 'speaker' => $this->speaker, 'playing_time' => $this->playing_time, 'start_time' => $this->start_time, 'history_item_id' => $this->history_item_id, 'model_id' => $this->model_id, 'stability' => $this->stability, 'similarity_boost' => $this->similarity_boost, 'style_exaggeration' => $this->style_exaggeration, 'speaker_boost' => $this->speaker_boost, ]; } } /** * {@inheritdoc} */ public function preSave() { $settings = $this->getSettings(); // Get original values to calculate if we need to generate new audio. $elevenLabsMap = []; // Only possible when there is an update. if (!empty($this->getEntity()->original)) { foreach ($this->getEntity()->original->get($this->getFieldDefinition()->getName())->getValue() as $value) { // Only count started processes. if (!empty($value['history_item_id'])) { $elevenLabsMap[$value['history_item_id']] = [ 'text' => $value['text'], 'speaker' => $value['speaker'], 'target_id' => $value['target_id'], ]; } } } // If no elevenlabs id exists, generate audio. $generateAudio = FALSE; // Recopy to audio fields if something changed. if (empty($this->history_item_id)) { $generateAudio = TRUE; } // If the text or speaker changed, but not the file. elseif ( isset($elevenLabsMap[$this->history_item_id]) && ($this->text != $elevenLabsMap[$this->history_item_id]['text'] || $this->speaker != $elevenLabsMap[$this->history_item_id]['speaker']) ) { $generateAudio = TRUE; } if ($generateAudio && !empty($this->text)) { // Batch jobs gets a temporary id. if ($settings['batch'] && $this->getName() != 0) { $this->set('target_id', 0); } else { $generator = \Drupal::service('elevenlabs_field.generator_service'); $model = $generator->validateModel($this->model_id, $this->getEntity()); $data = $generator->generateFile($this->text, $this->speaker, $this->getFieldDefinition(), $model, [ 'stability' => ($this->stability / 100), 'similarity_boost' => ($this->similarity_boost / 100), 'style' => ($this->style_exaggeration / 100), 'use_speaker_boost' => $this->speaker_boost ? TRUE : FALSE, ]); if (!empty($data['file'])) { // Remove old data if exists. if ($this->target_id) { $generator->removeFile($this->target_id); } $this->entity = $data['file']; $this->set('target_id', $data['file']->id()); $this->set('history_item_id', $data['history_item_id']); } } } elseif ($this->target_id) { $file = \Drupal::entityTypeManager()->getStorage('file')->load($this->target_id); $this->set('entity', $file); } if ($settings['save_audio']) { _elevenlabs_field_needs_copying($this->getEntity(), [ 'type' => $settings['concatenate'] ? 'concatenate' : 'save', 'field' => $settings['save_audio'], 'batch' => $settings['batch'], 'original_field' => $this->getFieldDefinition()->getName(), ]); } } /** * {@inheritdoc} */ public static function schema(FieldStorageDefinitionInterface $field_definition) { $columns = [ 'text' => [ 'type' => 'text', 'size' => 'big', ], 'speaker' => [ 'type' => 'varchar', 'length' => 255, ], 'playing_time' => [ 'type' => 'int', 'size' => 'normal', ], 'start_time' => [ 'type' => 'int', 'size' => 'normal', ], 'history_item_id' => [ 'type' => 'varchar', 'length' => 255, ], 'model_id' => [ 'type' => 'varchar', 'length' => 255, ], 'target_id' => [ 'description' => 'The ID of the file entity.', 'type' => 'int', 'unsigned' => TRUE, ], 'stability' => [ 'type' => 'int', 'size' => 'normal', ], 'similarity_boost' => [ 'type' => 'int', 'size' => 'normal', ], 'style_exaggeration' => [ 'type' => 'int', 'size' => 'normal', ], 'speaker_boost' => [ 'type' => 'int', 'size' => 'normal', ], ]; $schema = [ 'columns' => $columns, 'indexes' => [ 'history_item_id' => ['history_item_id'], 'target_id' => ['target_id'], ], 'foreign keys' => [ 'target_id' => [ 'table' => 'file_managed', 'columns' => ['target_id' => 'fid'], ], ], ]; return $schema; } /** * {@inheritdoc} */ public static function generateSampleValue(FieldDefinitionInterface $field_definition) { $random = new Random(); $values['text'] = $random->paragraphs(5); $values['speaker'] = '21m00Tcm4TlvDq8ikWAM'; $values['history_item_id'] = '21m00Tcm4TlvDq8ikWAM'; $values['playing_time'] = mt_rand(-1000, 1000); $values['start_time'] = mt_rand(-1000, 1000); $values['model_id'] = 'eleven_monolingual_v1'; $dirname = 'public://elevenlabs'; \Drupal::service('file_system')->prepareDirectory($dirname, FileSystemInterface::CREATE_DIRECTORY); $destination = $dirname . '/' . $random->name(10, TRUE) . '.mp4'; $data = $random->paragraphs(3); /** @var \Drupal\file\FileRepositoryInterface $file_repository */ $file_repository = \Drupal::service('file.repository'); $file = $file_repository->writeData($data, $destination, FileSystemInterface::EXISTS_ERROR); $values['target_id'] = $file->id(); return $values; } /** * {@inheritdoc} */ public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data) { $element = []; // We need the field-level 'default_image' setting, and $this->getSettings() // will only provide the instance-level one, so we need to explicitly fetch // the field. $settings = $this->getFieldDefinition()->getFieldStorageDefinition()->getSettings(); $scheme_options = \Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE); $element['uri_scheme'] = [ '#type' => 'radios', '#title' => $this->t('Upload destination'), '#options' => $scheme_options, '#default_value' => $settings['uri_scheme'], '#description' => $this->t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'), ]; return $element; } /** * {@inheritdoc} */ public function fieldSettingsForm(array $form, FormStateInterface $form_state) { $element = []; $settings = $this->getSettings(); $element['file_directory'] = [ '#type' => 'textfield', '#title' => $this->t('File directory'), '#default_value' => $settings['file_directory'], '#description' => $this->t('Optional subdirectory within the upload destination where files will be stored. Do not include preceding or trailing slashes.'), '#weight' => -1, ]; $element['file_name'] = [ '#type' => 'textfield', '#title' => $this->t('File name'), '#default_value' => $settings['file_name'], '#description' => $this->t('Optional file name for the mp3 files created.'), '#weight' => 0, ]; $element['save_audio'] = [ '#type' => 'select', '#title' => $this->t('Re-Save in Audio field'), '#default_value' => $settings['save_audio'], '#description' => $this->t('This will be the field to re-save the audio to. Only fields that allow mp3 and the same amount of fields or more will work.'), '#options' => $this->getAudioFields($form), ]; $element['batch'] = [ '#type' => 'checkbox', '#title' => $this->t('Enable Batching'), '#description' => $this->t('On saving this would batch each audio generation. Do this if you have timeouts.'), '#default_value' => $settings['batch'], ]; $element['concatenate'] = [ '#type' => 'checkbox', '#title' => $this->t('Concatenate File in FFmpeg'), '#description' => $this->t('If FFmpeg is available on the server, you can concatenate a long discussion into one file.'), '#default_value' => $settings['concatenate'], '#attributes' => [ 'disabled' => $this->checkFfmpeg(), ], '#states' => [ 'visible' => [ ':input[name="settings[save_audio]"]' => [ "!value" => "", ], ], ], ]; $element['concatenate_advanced'] = [ '#type' => 'details', '#title' => $this->t('Concatenate Settings'), '#open' => TRUE, '#states' => [ 'visible' => [ ':input[name="settings[concatenate]"]' => [ 'checked' => TRUE, ], ], ], ]; $element['concatenate_advanced']['intro'] = [ '#type' => 'checkbox', '#title' => $this->t('Intro'), '#description' => $this->t('Add an intro to the created file.'), '#default_value' => $settings['concatenate_advanced']['intro'], ]; $element['concatenate_advanced']['intro_details'] = [ '#type' => 'details', '#title' => $this->t('Intro Details'), '#open' => TRUE, '#states' => [ 'visible' => [ ':input[name="settings[concatenate_advanced][intro]"]' => [ 'checked' => TRUE, ], ], ], ]; $element['concatenate_advanced']['intro_details']['intro_file'] = [ '#type' => 'managed_file', '#title' => $this->t('Intro Music'), '#description' => $this->t('Intro before the main text.'), '#default_value' => $settings['concatenate_advanced']['intro_details']['intro_file'] ?? NULL, '#upload_location' => 'public://intro/', '#element_validate' => [ '\Drupal\file\Element\ManagedFile::validateManagedFile', ], '#upload_validators' => $this->getUploadValidators(), ]; $element['concatenate_advanced']['intro_details']['intro_field'] = [ '#type' => 'select', '#title' => $this->t('Use Content Field'), '#default_value' => $settings['concatenate_advanced']['intro_details']['intro_field'], '#description' => $this->t('This will be the field with the Intro.'), '#options' => $this->getAudioFields($form, '-- No Soudscape Field --'), ]; $element['concatenate_advanced']['intro_details']['intro_volume'] = [ '#type' => 'number', '#title' => $this->t('Intro Volume'), '#min' => 1, '#max' => 100, '#description' => $this->t('The volume to play the intro in.'), '#default_value' => $settings['concatenate_advanced']['intro_details']['intro_volume'], ]; $element['concatenate_advanced']['outro'] = [ '#type' => 'checkbox', '#title' => $this->t('Outro'), '#description' => $this->t('Add an outro to the created file.'), '#default_value' => $settings['concatenate_advanced']['outro'], ]; $element['concatenate_advanced']['outro_details'] = [ '#type' => 'details', '#title' => $this->t('Outro Details'), '#open' => TRUE, '#states' => [ 'visible' => [ ':input[name="settings[concatenate_advanced][outro]"]' => [ 'checked' => TRUE, ], ], ], ]; $element['concatenate_advanced']['outro_details']['outro_file'] = [ '#type' => 'managed_file', '#title' => $this->t('Outro Music'), '#description' => $this->t('Outro after the main text.'), '#default_value' => $settings['concatenate_advanced']['outro_details']['outro_file'] ?? NULL, '#upload_location' => 'public://outro/', '#element_validate' => [ '\Drupal\file\Element\ManagedFile::validateManagedFile', ], '#upload_validators' => $this->getUploadValidators(), ]; $element['concatenate_advanced']['outro_details']['outro_field'] = [ '#type' => 'select', '#title' => $this->t('Use Content Field'), '#default_value' => $settings['concatenate_advanced']['outro_details']['outro_field'], '#description' => $this->t('This will be the field with the outro.'), '#options' => $this->getAudioFields($form, '-- No Soudscape Field --'), ]; $element['concatenate_advanced']['outro_details']['outro_volume'] = [ '#type' => 'number', '#title' => $this->t('Outro Volume'), '#min' => 1, '#max' => 100, '#description' => $this->t('The volume to play the outr in.'), '#default_value' => $settings['concatenate_advanced']['outro_details']['outro_volume'], ]; $element['concatenate_advanced']['soundscape'] = [ '#type' => 'checkbox', '#title' => $this->t('Soundscape'), '#description' => $this->t('Add a soundscape under the talking file.'), '#default_value' => $settings['concatenate_advanced']['soundscape'], ]; $element['concatenate_advanced']['soundscape_details'] = [ '#type' => 'details', '#title' => $this->t('Soundscape Details'), '#open' => TRUE, '#states' => [ 'visible' => [ ':input[name="settings[concatenate_advanced][soundscape]"]' => [ 'checked' => TRUE, ], ], ], ]; $element['concatenate_advanced']['soundscape_details']['soundscape_file'] = [ '#type' => 'managed_file', '#title' => $this->t('Soundscape Music'), '#description' => $this->t('Soundscape under the main text.'), '#default_value' => $settings['concatenate_advanced']['soundscape_details']['soundscape_file'] ?? NULL, '#upload_location' => 'public://soundscape/', '#element_validate' => [ '\Drupal\file\Element\ManagedFile::validateManagedFile', ], '#upload_validators' => $this->getUploadValidators(), ]; $element['concatenate_advanced']['soundscape_details']['soundscape_field'] = [ '#type' => 'select', '#title' => $this->t('Use Content Field'), '#default_value' => $settings['concatenate_advanced']['soundscape_details']['soundscape_field'], '#description' => $this->t('This will be the field with the soundscape.'), '#options' => $this->getAudioFields($form, '-- No Soudscape Field --'), ]; $element['concatenate_advanced']['soundscape_details']['soundscape_volume'] = [ '#type' => 'number', '#title' => $this->t('Soundscape Volume'), '#min' => 1, '#max' => 100, '#description' => $this->t('The volume to play the soundscape in.'), '#default_value' => $settings['concatenate_advanced']['soundscape_details']['soundscape_volume'], ]; $element['concatenate_advanced']['soundscape_details']['soundscape_cut_time'] = [ '#type' => 'number', '#title' => $this->t('Soundscape Playing Length'), '#description' => $this->t('The amount of soundscape to play in total. 0 mean the entire thing.'), '#default_value' => $settings['concatenate_advanced']['soundscape_details']['soundscape_cut_time'], ]; $element['concatenate_advanced']['soundscape_details']['soundscape_start_wait'] = [ '#type' => 'number', '#title' => $this->t('Soundscape Start Padding'), '#description' => $this->t('The amount of soundscape to play before the talk starts.'), '#default_value' => $settings['concatenate_advanced']['soundscape_details']['soundscape_start_wait'], ]; $element['concatenate_advanced']['soundscape_details']['soundscape_fade_in'] = [ '#type' => 'number', '#title' => $this->t('Soundscape Fade In'), '#description' => $this->t('The length of a fade in on the soundscape in milliseconds. 0 means no fade in.'), '#default_value' => $settings['concatenate_advanced']['soundscape_details']['soundscape_fade_in'], ]; $element['concatenate_advanced']['soundscape_details']['soundscape_prolong'] = [ '#type' => 'number', '#title' => $this->t('Soundscape Prolonging'), '#description' => $this->t('The amount of soundscape to play after the talk stops in milliseconds.'), '#default_value' => $settings['concatenate_advanced']['soundscape_details']['soundscape_prolong'], ]; $element['concatenate_advanced']['soundscape_details']['soundscape_fade_out'] = [ '#type' => 'number', '#title' => $this->t('Soundscape Fade Out'), '#description' => $this->t('The length of a fade out on the soundscape in milliseconds. 0 means no fade out.'), '#default_value' => $settings['concatenate_advanced']['soundscape_details']['soundscape_fade_out'], ]; return $element; } /** * Checks if FFMpeg is available on the server. * * @return bool * True or false. */ private function checkFfmpeg() { $command = (PHP_OS == 'WINNT') ? 'where ffmpeg' : 'which ffmpeg'; $result = shell_exec($command); return !$result; } /** * Get the file field to use. * * @param array $form * The form. * @param string $message * The no message in the fields. * * @return array * Options for the audio field generation. */ private function getAudioFields(array $form, $message = '-- No audio generation --') { $options[''] = '-- No audio generation --'; /** @var \Drupal\Core\Field\FieldDefinitionInterface */ foreach ($form['#entity']->getFieldDefinitions() as $fieldDefinition) { if ($fieldDefinition->getType() == 'file' && ($fieldDefinition->getFieldStorageDefinition()->getCardinality() == -1 || $fieldDefinition->getFieldStorageDefinition()->getCardinality() > $this->getFieldDefinition()->getFieldStorageDefinition()->getCardinality())) { $config = $fieldDefinition->getConfig($form['#entity']->bundle())->getSettings(); $extensions = preg_split('/ (,| ) /', $config['file_extensions']); if (in_array('mp3', $extensions)) { $options[$fieldDefinition->getName()] = $fieldDefinition->getLabel(); } } } return $options; } }