diff --git a/main/lp/lp_add_audio.php b/main/lp/lp_add_audio.php index 2d2c7b37827..ba58d7f73b5 100755 --- a/main/lp/lp_add_audio.php +++ b/main/lp/lp_add_audio.php @@ -15,9 +15,9 @@ $isStudentView = api_is_student_view_active(); $learnpath_id = (int) $_REQUEST['lp_id']; $lp_item_id = isset($_GET['id']) ? (int) $_GET['id'] : null; -$submit = isset($_POST['submit_button']) ? $_POST['submit_button'] : null; -$type = isset($_GET['type']) ? $_GET['type'] : null; -$action = isset($_GET['action']) ? $_GET['action'] : null; +$submit = $_POST['submit_button'] ?? null; +$type = $_GET['type'] ?? null; +$action = $_GET['action'] ?? null; $courseInfo = api_get_course_info(); if (!$is_allowed_to_edit || $isStudentView) { @@ -48,7 +48,7 @@ 'name' => $lp->getNameNoTags(), ]; -$audioPreview = DocumentManager::generateAudioJavascript([]); +$audioPreview = DocumentManager::generateAudioJavascript(); $htmlHeadXtra[] = "'; -$htmlHeadXtra[] = ''; -$htmlHeadXtra[] = ''; -$htmlHeadXtra[] = ''; +$htmlHeadXtra[] = ''; +$htmlHeadXtra[] = ''; +$htmlHeadXtra[] = ''; $tpl = new Template(get_lang('Add')); $tpl->assign('unique_file_id', api_get_unique_id()); @@ -161,7 +166,14 @@ Display::getMediaPlayer($file, ['url' => $urlFile]). ""; $form->addElement('label', get_lang('Listen'), $audioPlayer); - $url = api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?lp_id='.$lp->get_id().'&action=add_audio&id='.$lp_item_id.'&delete_file=1&'.api_get_cidreq(); + $url = $webCodePath.'lp/lp_controller.php?&' + .http_build_query([ + 'lp_id' => $lp->get_id(), + 'action' => 'add_audio', + 'id' => $lp_item_id, + 'delete_file' => 1, + ]) + .'&'.api_get_cidreq(); $form->addElement( 'label', null, @@ -184,7 +196,7 @@ api_get_session_id(), false, '', - api_get_path(WEB_CODE_PATH).'lp/lp_controller.php?action=add_audio&lp_id='.$lp->get_id().'&id='.$lp_item_id, + $webCodePath.'lp/lp_controller.php?action=add_audio&lp_id='.$lp->get_id().'&id='.$lp_item_id, false, true, $audioFolderId, @@ -196,6 +208,96 @@ $page .= $recordVoiceForm; $page .= '
'; $page .= $form->returnForm(); + +$text2speechPlugin = Text2SpeechPlugin::create(); + +if ($text2speechPlugin->isEnabled(true)) { + $page .= '
+ +
+
+

+ +

+ + +

+ +

+
+
+ + '; +} + $page .= ''; @@ -229,8 +331,9 @@ $page .= '
  • '.get_lang('Audio').'
  • '; $page .= '
  • '; $page .= ''; -$page .= ''; +$page .= '
  • '; $page .= ''; + $page .= ''; $page .= ''; diff --git a/main/lp/lp_controller.php b/main/lp/lp_controller.php index 7281a08ab04..824845e40aa 100755 --- a/main/lp/lp_controller.php +++ b/main/lp/lp_controller.php @@ -711,6 +711,25 @@ function(reponse) { exit; } + if (Text2SpeechPlugin::create()->isEnabled(true) + && isset($_GET['tts']) && 1 === (int) $_GET['tts'] + ) { + $audioPath = api_get_path(SYS_UPLOAD_PATH).'plugins/text2speech/'.basename($_POST['file']); + + $fileInfo = new SplFileInfo($audioPath); + + if ($fileInfo->isReadable()) { + $_FILES['file'] = [ + 'name' => $fileInfo->getFilename(), + 'type' => 'audio/'.$fileInfo->getExtension(), + 'tmp_name' => $fileInfo->getRealPath(), + 'error' => UPLOAD_ERR_OK, + 'size' => $fileInfo->getSize(), + 'copy_file' => true, + ]; + } + } + // Upload audio if (isset($_FILES['file']) && !empty($_FILES['file'])) { // Updating the lp.modified_on diff --git a/plugin/ai_helper/tool/learnpath.php b/plugin/ai_helper/tool/learnpath.php index 78a0f9b04cf..db15032e7d6 100644 --- a/plugin/ai_helper/tool/learnpath.php +++ b/plugin/ai_helper/tool/learnpath.php @@ -35,6 +35,7 @@ $messageGetItems = 'Generate the table of contents of a course in "%s" in %d or less chapters on the topic of "%s" in a list separated with comma, without chapter number. Do not include a conclusion chapter.'; $prompt = sprintf($messageGetItems, $courseLanguage, $chaptersCount, $topic); + $resultText = $plugin->openAiGetCompletionText($prompt, 'learnpath'); if (isset($resultText['error']) && true === $resultText['error']) { @@ -65,6 +66,7 @@ $promptItem = sprintf($messageGetItemContent, $topic, $courseLanguage, $wordsCount, $title); $resultContentText = $plugin->openAiGetCompletionText($promptItem, 'learnpath'); $lpItemContent = (!empty($resultContentText) ? trim($resultContentText) : ''); + if (false !== stripos($lpItemContent, '')) { $lpItemContent = preg_replace("||i", "\r\n$style\r\n\\0", $lpItemContent); } else { diff --git a/plugin/text2speech/README.md b/plugin/text2speech/README.md new file mode 100755 index 00000000000..c466b70b92e --- /dev/null +++ b/plugin/text2speech/README.md @@ -0,0 +1,45 @@ +Text2Speech +=========== + +Version 0.1 + +This plugin adds the possibility (once setup with 3rd party account) to add +speech to learning paths by converting text in the learning paths to audio +files attached to each learning path item. + +This plugin requires the *installation and configuration* of the TTS software +and data from Mozilla, which might be deterring to most users (sorry about +that). Please refer to https://github.com/mozilla/TTS/wiki on how to download, +install and configure your own TTS server. + +It also requires the "AI Helper" plugin to be installed, enabled and properly +configured, as it connects to the learning path auto-generation feature to add +audio to it. + +Once your TTS server is available, get a URL to connect to it, install and +enable the plugin, give it an API key (if any), a host (could be localhost) +and enable the plugin in the learning paths, then create a new learning +path using the AI Helper plugin in the learning path tool. You should now +get additional speech for every document in your learning path. + +## Use a Mozilla TTS server + +To mount your TTS server, you can use the Docker image from +[synesthesiam/docker-mozillatts](https://github.com/synesthesiam/docker-mozillatts). +Clone the repository and then run + +```$ docker run -it -p 5002:5002 synesthesiam/mozillatts:``` + +(where is one of the supported languages (en, es, fr, de) for this image. If no language is given, +U.S. English is used). This image will serve the necessary API to configure in the plugin. + +## Configuring the plugin + +The plugin configuration asks for an API key which is *not* necessary if using a local Docker container. +The TTS URL field, in the case of the Docker container described above, should simply point to `http://localhost:5002/`. Requests sent by Chamilo will be visible in the Docker container console, if left open. +This plugin and the suggested TTS model only allow for very small character strings to be translated, as documented here: https://github.com/synesthesiam/docker-mozillatts/issues/3. + +## Using the plugin + +The plugin, once enabled and properly configured, will add an audio creation block in the learning path edition screen, when clicking the audio speaker icon just under any document item of the learning path. The block is identified by "Text to Speech". Click the button to generate the audio, check if the quality is satisfying, then save the audio. +When student open this learning path item, the audio will play. diff --git a/plugin/text2speech/Text2SpeechPlugin.php b/plugin/text2speech/Text2SpeechPlugin.php new file mode 100644 index 00000000000..3f84dc9d5f2 --- /dev/null +++ b/plugin/text2speech/Text2SpeechPlugin.php @@ -0,0 +1,108 @@ + + */ +class Text2SpeechPlugin extends Plugin +{ + public const MOZILLATTS_API = 'mozillatts'; + public const PATH_TO_SAVE_FILES = __DIR__.'/../../app/upload/plugins/text2speech/'; + + protected function __construct() + { + $version = '0.1'; + $author = 'Francis Gonzales'; + + $message = '

    '.$this->get_lang('plugin_comment').'

    '; + + $settings = [ + $message => 'html', + 'tool_enable' => 'boolean', + 'api_name' => [ + 'type' => 'select', + 'options' => $this->getApiList(), + ], + 'api_key' => 'text', + 'url' => 'text', + 'tool_lp_enable' => 'boolean', + ]; + + parent::__construct($version, $author, $settings); + } + + /** + * Get the list of apis availables. + * + * @return array + */ + public function getApiList() + { + return [ + self::MOZILLATTS_API => 'MozillaTTS', + ]; + } + + /** + * Get the completion text from openai. + * + * @return string + */ + public function convert(string $text) + { + $path = '/app/upload/plugins/text2speech/'; + switch ($this->get('api_name')) { + case self::MOZILLATTS_API: + require_once __DIR__.'/src/mozillatts/MozillaTTS.php'; + + $mozillaTTS = new MozillaTTS($this->get('url'), $this->get('api_key'), self::PATH_TO_SAVE_FILES); + $path .= $mozillaTTS->convert($text); + break; + } + + return $path; + } + + /** + * Get the plugin directory name. + */ + public function get_name(): string + { + return 'text2speech'; + } + + /** + * Get the class instance. + * + * @staticvar Text2SpeechPlugin $result + */ + public static function create(): Text2SpeechPlugin + { + static $result = null; + + return $result ?: $result = new self(); + } + + /** + * Install the plugin. create folder to save files. + */ + public function install() + { + if (!file_exists(self::PATH_TO_SAVE_FILES)) { + mkdir(self::PATH_TO_SAVE_FILES); + } + } + + /** + * Unistall plugin. Clear the folder. + */ + public function uninstall() + { + if (file_exists(self::PATH_TO_SAVE_FILES)) { + array_map('unlink', glob(self::PATH_TO_SAVE_FILES.'/*.*')); + rmdir(self::PATH_TO_SAVE_FILES); + } + } +} diff --git a/plugin/text2speech/convert.php b/plugin/text2speech/convert.php new file mode 100644 index 00000000000..44179d67934 --- /dev/null +++ b/plugin/text2speech/convert.php @@ -0,0 +1,70 @@ +isEnabled(true) + || !$isAllowedToEdit + ) { + throw new Exception(); + } + + $textToConvert = ''; + + if ($httpRequest->query->has('text')) { + $textToConvert = $httpRequest->query->get('text'); + } elseif ($httpRequest->query->has('item_id')) { + $itemId = $httpRequest->query->getInt('item_id'); + + $item = $em->find(CLpItem::class, $itemId); + + if (!$item) { + throw new Exception(); + } + + $course = api_get_course_entity($item->getCId()); + $documentRepo = $em->getRepository(CDocument::class); + + $document = $documentRepo->findOneBy([ + 'cId' => $course->getId(), + 'iid' => $item->getPath(), + ]); + + if (!$document) { + throw new Exception(); + } + + $textToConvert = file_get_contents( + api_get_path(SYS_COURSE_PATH).$course->getDirectory().'/document/'.$document->getPath() + ); + $textToConvert = strip_tags($textToConvert); + } + + if (empty($textToConvert)) { + throw new Exception(); + } + + $path = $plugin->convert($textToConvert); + + $httpResponse->setContent($path); +} catch (Exception $exception) { + $httpResponse->setStatusCode(HttpResponse::HTTP_BAD_REQUEST); +} + +$httpResponse->send(); diff --git a/plugin/text2speech/install.php b/plugin/text2speech/install.php new file mode 100755 index 00000000000..408e9f86eb2 --- /dev/null +++ b/plugin/text2speech/install.php @@ -0,0 +1,16 @@ +install(); diff --git a/plugin/text2speech/lang/english.php b/plugin/text2speech/lang/english.php new file mode 100755 index 00000000000..81ac14e3bcf --- /dev/null +++ b/plugin/text2speech/lang/english.php @@ -0,0 +1,13 @@ +get_info(); diff --git a/plugin/text2speech/src/IProvider.php b/plugin/text2speech/src/IProvider.php new file mode 100644 index 00000000000..7312fbbe288 --- /dev/null +++ b/plugin/text2speech/src/IProvider.php @@ -0,0 +1,8 @@ +url = $url; + $this->apiKey = $apiKey; + $this->filePath = $filePath; + } + + public function convert(string $text): string + { + return $this->request($text); + } + + private function request(string $data): string + { + $filename = uniqid().'.wav'; + $filePath = $this->filePath.$filename; +// $resource = fopen(realpath($filePath), 'w'); + + $client = new GuzzleHttp\Client(); + $client->get($this->url.'?api_key='.urlencode($this->apiKey). + '&text='.str_replace('%0A', '+', urlencode($data)), [ + 'headers' => [ + 'Cache-Control' => 'no-cache', + 'Content-Type' => 'audio/wav', + ], + 'sink' => $filePath, + ]); + + return $filename; + } +} diff --git a/plugin/text2speech/uninstall.php b/plugin/text2speech/uninstall.php new file mode 100755 index 00000000000..df666a1d5dc --- /dev/null +++ b/plugin/text2speech/uninstall.php @@ -0,0 +1,16 @@ +uninstall();