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;
}
}
