Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text2Speech Plugin #4629

Open
wants to merge 13 commits into
base: 1.11.x
Choose a base branch
from
125 changes: 114 additions & 11 deletions main/lp/lp_add_audio.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -48,7 +48,7 @@
'name' => $lp->getNameNoTags(),
];

$audioPreview = DocumentManager::generateAudioJavascript([]);
$audioPreview = DocumentManager::generateAudioJavascript();
$htmlHeadXtra[] = "<script>
$(function() {
$audioPreview
Expand Down Expand Up @@ -99,7 +99,7 @@
$audioFolderId = DocumentManager::get_document_id($courseInfo, $currentDir);

if (isset($_REQUEST['folder_id'])) {
$folderIdFromRequest = isset($_REQUEST['folder_id']) ? (int) $_REQUEST['folder_id'] : 0;
$folderIdFromRequest = (int) $_REQUEST['folder_id'];
$documentData = DocumentManager::get_document_data_by_id($folderIdFromRequest, $courseInfo['code']);
if ($documentData) {
$audioFolderId = $folderIdFromRequest;
Expand All @@ -111,6 +111,7 @@
}

$file = null;
$urlFile = '';
if (!empty($lp_item->audio)) {
$file = api_get_path(SYS_COURSE_PATH).$courseInfo['path'].'/document/'.$lp_item->audio;
$urlFile = api_get_path(WEB_COURSE_PATH).$courseInfo['path'].'/document/'.$lp_item->audio.'?'.api_get_cidreq();
Expand All @@ -132,10 +133,14 @@

$recordVoiceForm = '<h3 class="page-header">'.get_lang('RecordYourVoice').'</h3>';
$page .= '<div id="doc_form" class="col-md-8">';

$webLibraryPath = api_get_path(WEB_LIBRARY_PATH);
$webCodePath = api_get_path(WEB_CODE_PATH);

$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_JS_PATH).'rtc/RecordRTC.js"></script>';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_PATH).'wami-recorder/recorder.js"></script>';
$htmlHeadXtra[] = '<script src="'.api_get_path(WEB_LIBRARY_PATH).'wami-recorder/gui.js"></script>';
$htmlHeadXtra[] = '<script type="text/javascript" src="'.api_get_path(WEB_LIBRARY_PATH).'swfobject/swfobject.js"></script>';
$htmlHeadXtra[] = '<script src="'.$webLibraryPath.'wami-recorder/recorder.js"></script>';
$htmlHeadXtra[] = '<script src="'.$webLibraryPath.'wami-recorder/gui.js"></script>';
$htmlHeadXtra[] = '<script type="text/javascript" src="'.$webLibraryPath.'swfobject/swfobject.js"></script>';

$tpl = new Template(get_lang('Add'));
$tpl->assign('unique_file_id', api_get_unique_id());
Expand All @@ -161,7 +166,14 @@
Display::getMediaPlayer($file, ['url' => $urlFile]).
"</div>";
$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,
Expand All @@ -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,
Expand All @@ -196,6 +208,96 @@
$page .= $recordVoiceForm;
$page .= '<br>';
$page .= $form->returnForm();

$text2speechPlugin = Text2SpeechPlugin::create();

if ($text2speechPlugin->isEnabled(true)) {
$page .= '<div class="clearfix"></div>
<h3 class="page-header">
<small>'.get_lang('Or').'</small>
'.$text2speechPlugin->get_title().'
</h3>
<div class="row">
<div class="col-sm-8 col-sm-offset-2">
<p>
<button id="btn-tts" class="btn btn-default" type="button">
<span id="btn-tts__spinner" class="fa fa-spinner fa-spin" aria-hidden="true" style="display: none;"></span>
'.$text2speechPlugin->get_lang('GenerateAudioFromContent').'
</button>
</p>
<p id="tts-player" id="tts-player" style="display: none;">
<audio controls class="skip"></audio>
</p>
<p id="tts-warning" class="alert alert-warning" style="display: none;">
'.get_lang('ErrorOccurred').'
</p>
<p>
<button id="btn-save-tts" class="btn btn-primary" type="button" disabled>
<span id="btn-save-tts__spinner" class="fa fa-spinner fa-spin" aria-hidden="true" style="display: none;"></span>
'.get_lang('SaveRecordedAudio').'
</button>
</p>
</div>
</div>
<script>
$(function () {
var btnTts = $(\'#btn-tts\');
var btnTssSpiner = $(\'#btn-tts__spinner\');
var ttsPlayer = $(\'#tts-player\');
var ttsPlayerAudio = $(\'#tts-player audio\');
var ttsWarning = $(\'#tts-warning\');

var btnSaveTts = $(\'#btn-save-tts\');
var btnSaveTtsSpiner = $(\'#btn-save-tts__spinner\');

var audioSrc = \'\';

btnTts.on(\'click\', function (e) {
e.preventDefault();

ttsWarning.hide();
btnTts.prop(\'disabled\', true);
btnTssSpiner.show();

$
.ajax(_p.web_plugin + \'text2speech/convert.php?item_id='.$lp_item_id.'\')
.done(function (response) {
audioSrc = response;
ttsPlayer.show();
ttsPlayerAudio.prop(\'src\', audioSrc).mediaelementplayer();
btnSaveTts.prop(\'disabled\', false);
})
.fail(function () {
ttsPlayer.hide();
ttsWarning.show();
btnSaveTts.prop(\'disabled\', true);
})
.always(function () {
btnTssSpiner.hide();
btnTts.prop(\'disabled\', false);
});
});

btnSaveTts.on(\'click\', function () {
btnSaveTts.prop(\'disabled\', true);
btnSaveTtsSpiner.show();

$.ajax({
type: \'post\',
url: \''.api_get_self().'?action=add_audio&tts=1&id='.$lp_item_id.'&'.api_get_cidreq().'&lp_id='.$learnpath_id.'\',
data: {
file: audioSrc,
},
success: function () {
window.location.reload();
},
});
});
});
</script>
';
}

$page .= '<h3 class="page-header">
<small>'.get_lang('Or').'</small> '.get_lang('SelectAnAudioFileFromDocuments').'</h3>';

Expand Down Expand Up @@ -229,8 +331,9 @@
$page .= '<li class="doc_folder" style="margin-left: 36px;">'.get_lang('Audio').'</li>';
$page .= '<li class="doc_folder">';
$page .= '<ul class="lp_resource">'.$documentTree.'</ul>';
$page .= '</div>';
$page .= '</li>';
$page .= '</ul>';

$page .= '</div>';
$page .= '</div>';

Expand Down
19 changes: 19 additions & 0 deletions main/lp/lp_controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions plugin/ai_helper/tool/learnpath.php
Original file line number Diff line number Diff line change
Expand Up @@ -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']) {
Expand Down Expand Up @@ -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, '</head>')) {
$lpItemContent = preg_replace("|</head>|i", "\r\n$style\r\n\\0", $lpItemContent);
} else {
Expand Down
45 changes: 45 additions & 0 deletions plugin/text2speech/README.md
Original file line number Diff line number Diff line change
@@ -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:<LANGUAGE>```

(where <LANGUAGE> 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.
108 changes: 108 additions & 0 deletions plugin/text2speech/Text2SpeechPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<?php
/* For license terms, see /license.txt */

/**
* Description of Text2SpeechPlugin.
*
* @author Francis Gonzales <[email protected]>
*/
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 = '<p>'.$this->get_lang('plugin_comment').'</p>';

$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);
}
}
}
Loading