1
0
Fork 0
mirror of https://github.com/DanielnetoDotCom/YouPHPTube synced 2025-10-03 01:39:24 +02:00

Implement AI image generation feature and enhance image handling in video library

This commit is contained in:
Daniel Neto 2025-06-25 10:52:14 -03:00
parent 0f6b38e7c7
commit 9b438e96e5
17 changed files with 403 additions and 79 deletions

View file

@ -154,7 +154,11 @@ $croppieFilesAdded = 1;
$('#library-btn<?php echo $uid; ?>').off('click');
$('#library-btn<?php echo $uid; ?>').on('click', function(ev) {
avideoModalIframe(webSiteRootURL + 'view/list-images.php?uid=<?php echo $uid; ?>');
var url = webSiteRootURL + 'view/list-images.php?uid=<?php echo $uid; ?>';
if (typeof mediaId == 'number' && !empty(mediaId)) {
url = addQueryStringParameter(url, 'videos_id', mediaId);
}
avideoModalIframe(url);
});

View file

@ -7485,6 +7485,80 @@ if (!class_exists('Video')) {
return $result;
}
static function saveImageInVideoLib($videos_id, $imageContent, $imageExt = 'png', $prefix = '')
{
global $global;
if (empty($videos_id) || empty($imageContent)) {
return false;
}
$video = Video::getVideoLight($videos_id);
if (empty($video)) {
return false;
}
$imageExt = preg_replace('/[^a-zA-Z0-9]/', '', $imageExt);
$imageName = $prefix .'_'. uniqid() . ".{$imageExt}";
$relativeDir = Video::getVideoLibRelativePath($videos_id);
$path = "{$global['systemRootPath']}{$relativeDir}{$imageName}";
if (_file_put_contents($path, $imageContent)) {
_error_log("Video::saveImageInVideoLib({$videos_id}, {$imageExt}) saved in {$path}");
return true;
} else {
_error_log("Video::saveImageInVideoLib({$videos_id}, {$imageExt}) could not save in {$path}");
return false;
}
}
static function getVideoLibRelativePath($videos_id)
{
if (empty($videos_id)) {
return false;
}
$video = Video::getVideoLight($videos_id);
if (empty($video)) {
return false;
}
$relativeDir = "videos/{$video['filename']}/images/";
return $relativeDir;
}
static function listAllImagesInVideoLib($videos_id)
{
global $global;
if (empty($videos_id)) {
return [];
}
$video = Video::getVideoLight($videos_id);
if (empty($video)) {
return [];
}
$relativeDir = Video::getVideoLibRelativePath($videos_id);
$path = "{$global['systemRootPath']}{$relativeDir}";
if (!is_dir($path)) {
return [];
}
$images = [];
$files = scandir($path);
foreach ($files as $file) {
if (preg_match('/\.(jpg|jpeg|png|gif)$/i', $file)) {
$images[] = [
'name' => $file,
'url' => "{$global['webSiteRootURL']}{$relativeDir}{$file}",
'path' => "{$path}{$file}"
];
}
}
return $images;
}
}
}
// Just to convert permalink into clean_title

View file

@ -16,6 +16,7 @@ class AI extends PluginAbstract
static $typeTranslation = 'translation';
static $typeTranscription = 'transcription';
static $typeBasic = 'basic';
static $typeImage = 'image';
static $typeShorts = 'shorts';
static $typeDubbing = 'dubbing';
@ -845,6 +846,10 @@ class AI extends PluginAbstract
_error_log('AI:asyncVideosId ' . basename(__FILE__) . ' line=' . __LINE__);
$obj = AI::getVideoDubbingMetadata($videos_id, @$_REQUEST['language']);
break;
case AI::$typeImage:
_error_log('AI:asyncVideosId typeImage ' . basename(__FILE__) . ' line=' . __LINE__);
$obj = AI::getVideoBasicMetadata($videos_id);
break;
default:
_error_log('AI:asyncVideosId ' . basename(__FILE__) . ' line=' . __LINE__);
$obj = new stdClass();

View file

@ -75,14 +75,55 @@ class Ai_responses extends ObjectYPT
return intval($this->videos_id);
}
function setPrice($price) {
function setPrice($price)
{
$this->price = floatval($price);
}
function getPrice() {
function getPrice()
{
return floatval($this->price);
}
static function getAllImageFromVideo($videos_id)
{
global $global;
$sql = "SELECT *
FROM ai_responses_json as arj
LEFT JOIN ai_responses as ar ON ar.id = arj.ai_responses_id
WHERE ar.videos_id = ? AND arj.ai_type = ?";
$sql .= self::getSqlFromPost('arj.');
// var_dump($sql, [$videos_id, AI::$typeImage]);
$res = sqlDAL::readSql($sql, 'is', [$videos_id, AI::$typeImage]);
$fullData = sqlDAL::fetchAllAssoc($res);
//var_dump($sql, $fullData);exit;
sqlDAL::close($res);
$rows = array();
if ($res != false) {
foreach ($fullData as $row) {
if (!empty($row['response'])) {
$row['response'] = _json_decode($row['response']);
}
//var_dump($row['response']->data[0]->url);
$row['url'] = '';
if (!empty($row['response']) && !empty($row['response']->data[0]->url)) {
$row['url'] = $row['response']->data[0]->url;
}
$rows[] = $row;
}
} else {
/**
*
* @var array $global
* @var object $global['mysqli']
*/
_error_log($sql . ' Error : (' . $global['mysqli']->errno . ') ' . $global['mysqli']->error);
}
return $rows;
}
static function getAllBasicFromVideo($videos_id)
{
global $global;
@ -100,14 +141,14 @@ class Ai_responses extends ObjectYPT
$rows = array();
if ($res != false) {
foreach ($fullData as $row) {
if(empty($row['videoTitles'])){
if (empty($row['videoTitles'])) {
$row['videoTitles'] = array();
}else if(is_string($row['videoTitles'])){
} else if (is_string($row['videoTitles'])) {
$row['videoTitles'] = json_decode($row['videoTitles']);
}
if(empty($row['keywords'])){
if (empty($row['keywords'])) {
$row['keywords'] = array();
}else if(is_string($row['keywords'])){
} else if (is_string($row['keywords'])) {
$row['keywords'] = json_decode($row['keywords']);
}
$rows[] = $row;
@ -211,14 +252,14 @@ class Ai_responses extends ObjectYPT
'text');
*/
foreach ($fullData as $row) {
if(empty($row['videoTitles'])){
if (empty($row['videoTitles'])) {
$row['videoTitles'] = array();
}else if(is_string($row['videoTitles'])){
} else if (is_string($row['videoTitles'])) {
$row['videoTitles'] = json_decode($row['videoTitles']);
}
if(empty($row['keywords'])){
if (empty($row['keywords'])) {
$row['keywords'] = array();
}else if(is_string($row['keywords'])){
} else if (is_string($row['keywords'])) {
$row['keywords'] = json_decode($row['keywords']);
}
foreach ($cleanKeys as $value) {
@ -265,20 +306,18 @@ class Ai_responses extends ObjectYPT
$fullData = sqlDAL::fetchAllAssoc($res);
sqlDAL::close($res);
return $fullData ;
return $fullData;
}
static function getValidTranscriptions($videos_id)
{
$rows = self::getTranscriptions($videos_id);
foreach ($rows as $row) {
if(!empty($row['text'])){
if (!empty($row['text'])) {
return $row;
}
}
return false;
}
@ -296,8 +335,7 @@ class Ai_responses extends ObjectYPT
$fullData = sqlDAL::fetchAllAssoc($res);
sqlDAL::close($res);
return $fullData ;
return $fullData;
}
static function hasTranscriptions($videos_id)
@ -309,7 +347,7 @@ class Ai_responses extends ObjectYPT
static function getTranscriptionText($videos_id)
{
$rows = self::getValidTranscriptions($videos_id);
if(!empty($rows)){
if (!empty($rows)) {
return $rows['text'];
}
return '';
@ -318,14 +356,15 @@ class Ai_responses extends ObjectYPT
static function getTranscriptionVtt($videos_id)
{
$rows = self::getValidTranscriptions($videos_id);
if(!empty($rows)){
if (!empty($rows)) {
//_error_log("AI::getTranscriptionVtt($videos_id) ".json_encode($rows['vtt']));
return $rows['vtt'];
}
return '';
}
static function getLatest($videos_id) {
static function getLatest($videos_id)
{
global $global;
$sql = "SELECT tr.*, mr.*, r.* FROM ai_responses r
LEFT JOIN ai_transcribe_responses tr ON tr.ai_responses_id = r.id

View file

@ -22,6 +22,11 @@
loadAIShorts();
}
break;
case '<?php echo AI::$typeImage; ?>':
if (typeof loadAIImage == 'function') {
loadAIImage();
}
break;
default:
break;
}

View file

@ -164,6 +164,12 @@ $_page = new Page(['Video Metatags']);
<?php echo __("Basic"); ?>
</a>
</li>
<li>
<a data-toggle="tab" href="#pimage">
<i class="fa-solid fa-image"></i>
<?php echo __("Image"); ?>
</a>
</li>
<li>
<a data-toggle="tab" href="#pShorts">
<i class="fa-solid fa-scissors"></i>
@ -208,6 +214,11 @@ $_page = new Page(['Video Metatags']);
include $global['systemRootPath'] . 'plugin/AI/tabs/basic.php';
?>
</div>
<div id="pimage" class="tab-pane fade">
<?php
include $global['systemRootPath'] . 'plugin/AI/tabs/image.php';
?>
</div>
<div id="pShorts" class="tab-pane fade">
<?php
include $global['systemRootPath'] . 'plugin/AI/tabs/shorts.php';

View file

@ -164,6 +164,26 @@ switch ($_REQUEST['type']) {
//$jsonDecoded->lines[] = __LINE__;
}
break;
case AI::$typeImage:
error_log('AI: ' . basename(__FILE__) . ' line=' . __LINE__);
if (!empty($_REQUEST['response'])) {
$o = new Ai_responses_json(0);
$o->setResponse($_REQUEST['response']);
$o->setAi_type(AI::$typeImage);
$o->setAi_responses_id($token->ai_responses_id);
if (!empty($_REQUEST['response']['data'][0]['url'])) {
$imageContent = file_get_contents($_REQUEST['response']['data'][0]['url']);
if (empty($imageContent)) {
_error_log('AI: ' . basename(__FILE__) . ' line=' . __LINE__ . ' Error fetching image content');
} else {
Video::saveImageInVideoLib($token->videos_id, $imageContent, 'png', 'ai');
}
}
$jsonDecoded->msg = $_REQUEST['msg'];
$jsonDecoded->Ai_responses_json = $o->save();
$jsonDecoded->error = empty($jsonDecoded->Ai_responses_json);
}
break;
default:
_error_log('AI: ' . basename(__FILE__) . ' line=' . __LINE__);

View file

@ -1,11 +1,24 @@
<div class="panel panel-default">
<div class="panel-heading">
<div class="alert alert-info">
<h4><strong>Enhance Your Video SEO with AI</strong></h4>
<p>We're excited to announce a new AI-driven feature to enhance your video SEO! This tool will automatically suggest optimized <strong>Titles, Casual Descriptions, Professional Descriptions, Meta Descriptions, Keywords, Summaries, Ratings, and Rating Justifications</strong> for your videos.</p>
<p>Our AI analyzes your video's existing title and description to generate these SEO elements. For even more precise and tailored suggestions, we recommend providing a <i class="fas fa-microphone-alt"></i> <?php echo __("Transcription"); ?> of your video. This additional information allows our AI to better understand and optimize your content for search engines, boosting your video's visibility and reach.</p>
<p>Start leveraging the power of AI to make your videos stand out in search results!</p>
<div class="alert alert-info" style="border-left: 5px solid #31708f; padding-left: 20px;">
<h4 class="text-primary" style="margin-top: 0;">
<strong><i class="fas fa-rocket"></i> Boost Your Video SEO with AI</strong>
</h4>
<p>
We're pleased to introduce an <strong>AI-powered SEO enhancement tool</strong> for your videos.
This new feature intelligently generates optimized:
<em>Titles, Descriptions (Casual & Professional), Meta Descriptions, Keywords, Summaries, Ratings, and Justifications</em>.
</p>
<p>
By analyzing your video's current title and description, our AI delivers tailored SEO suggestions
to maximize discoverability. For the most accurate results, we recommend including a
<i class="fas fa-microphone-alt"></i> <strong><?php echo __("Transcription"); ?></strong> of your video — enabling deeper content understanding.
</p>
<p>
<strong>Start using AI to elevate your content and stand out in search results.</strong>
</p>
</div>
<?php
echo AI::getProgressBarHTML("basic_{$videos_id}", '');
?>
@ -24,7 +37,7 @@
<button class="btn btn-success btn-block" onclick="generateAIIdeas()">
<i class="fa-solid fa-lightbulb"></i> <?php echo __('Generate Basic Ideas') ?>
<?php
if(!empty($priceForBasic)){
if (!empty($priceForBasic)) {
echo "<br><span class=\"label label-success\">{$priceForBasicText}</span>";
}
?>

View file

@ -0,0 +1,32 @@
<?php
require_once '../../../videos/configuration.php';
header('Content-Type: application/json');
$videos_id = getVideos_id();
if (empty($videos_id)) {
forbiddenPage('Videos ID is required');
}
if (!AVideoPlugin::isEnabledByName('AI')) {
forbiddenPage('AI plugin is disabled');
}
if(!AI::canUseAI()){
forbiddenPage('You cannot use AI');
}
if (!Video::canEdit($videos_id)) {
forbiddenPage('You cannot edit this video');
}
setRowCount(100);
$video = new Video('', '', $videos_id);
setDefaultSort('created', 'DESC');
$obj = new stdClass();
$obj->msg = '';
$obj->videos_id = $videos_id;
$obj->response = Ai_responses::getAllImageFromVideo($videos_id);
$obj->error = empty($obj->response) && !is_array($obj->response);
$obj->images = Video::listAllImagesInVideoLib($videos_id);
echo _json_encode($obj);

79
plugin/AI/tabs/image.php Normal file
View file

@ -0,0 +1,79 @@
<div class="panel panel-default">
<div class="panel-heading">
<div class="alert alert-info" style="border-left: 5px solid #31708f; padding-left: 20px;">
<p>
<strong><i class="fas fa-image"></i> AI-Generated Image Preview:</strong> We use your video's <strong>title</strong> and <strong>description</strong> to create a unique and visually engaging AI-generated image.
</p>
<p>
These images are designed to enhance your contents visual appeal ideal for use as thumbnails, background posters, or video covers.
</p>
<p>
For the best results, ensure your title and description clearly reflect the main subject or mood of the video.
</p>
</div>
<?php echo AI::getProgressBarHTML("image_{$videos_id}", ''); ?>
</div>
<div class="panel-body">
<div id="ai-images" class="row">
<!-- As imagens serão carregadas aqui -->
</div>
</div>
<div class="panel-footer">
<button class="btn btn-success btn-block" onclick="generateAIImages()">
<i class="fa fa-images"></i> <?php echo __('Generate Image') ?>
<?php
if (!empty($priceForBasic)) {
echo "<br><span class=\"label label-success\">{$priceForBasicText}</span>";
}
?>
</button>
</div>
</div>
<script>
async function generateAIImages() {
await createAISuggestions('<?php echo AI::$typeImage; ?>');
loadAIImage();
loadAIUsage();
}
function loadAIImage() {
modal.showPleaseWait();
$.ajax({
url: webSiteRootURL + 'plugin/AI/tabs/image.json.php',
data: {
videos_id: <?php echo $videos_id; ?>
},
type: 'post',
success: function(response) {
if (response.error) {
avideoAlertError(response.msg);
} else {
const container = $('#ai-images');
container.empty();
response.images.forEach(function(item) {
const imgURL = item.url;
const html = `
<div class="col-xs-12 col-sm-6 col-md-4 text-center" style="margin-bottom: 15px;">
<a href="${imgURL}" target="_blank">
<img src="${imgURL}" class="img img-responsive img-thumbnail" style="margin: 0 auto;"/>
</a>
</div>
`;
container.append(html);
});
}
modal.hidePleaseWait();
}
});
}
$(document).ready(function() {
loadAIImage();
});
</script>

View file

@ -31,9 +31,16 @@ $columnCallbackFunctions = ['text'];
<div class="col-sm-8">
<div class="panel panel-default">
<div class="panel-heading">
<div class="alert alert-info">
<p><strong>Note:</strong> To ensure accurate transcription, your videos should contain clear speech. Please be aware that videos without any spoken words, or those containing only sounds and instrumental music, cannot be transcribed by our AI system. Make sure your videos have audible and clear speech to take full advantage of this feature.</p>
<div class="alert alert-info" style="border-left: 5px solid #31708f; padding-left: 20px;">
<p>
<strong><i class="fas fa-info-circle"></i> Important:</strong> For accurate transcriptions, your videos must contain clear, audible speech.
Please note that videos with no spoken words or only instrumental music or sound effects cannot be processed by our AI transcription system.
</p>
<p>
Ensure that speech is present and understandable in your content to fully benefit from this feature.
</p>
</div>
<?php
echo AI::getProgressBarHTML("transcription_{$videos_id}", __('Automatic'));
foreach (AI::LANGS as $key => $value) {

View file

@ -40,6 +40,8 @@ foreach ($obj->response as $key => $value) {
}else if(!empty($value['ai_type'])){
if($value['ai_type'] === AI::$typeShorts){
$obj->response[$key]['type'] = __('Shorts');
}else if($value['ai_type'] === AI::$typeImage){
$obj->response[$key]['type'] = __('Image');
}else{
$obj->response[$key]['type'] = "ERROR: {$value['ai_type']} ";
}

View file

@ -168,7 +168,7 @@ $bodyClass = '';
if (!empty($_REQUEST['isClosed'])) {
$bodyClass = 'is-closed';
}
// this is to make sure will not play in fullscreen on ios
$global['overrideNative'] = 1;
//var_dump($liveVideo, $video['id'], $poster, $sources);exit;
?>

View file

@ -10,7 +10,13 @@ if (!User::isLogged()) {
}
$userId = User::getId();
$relativeDir = "videos/userPhoto/Live/user_{$userId}/";
if (!empty($_REQUEST['videos_id'])) {
$relativeDir = Video::getVideoLibRelativePath($_REQUEST['videos_id']);
} else {
$relativeDir = "videos/userPhoto/Live/user_{$userId}/";
}
$absoluteDir = realpath(__DIR__ . "/../{$relativeDir}");
if (!is_dir($absoluteDir)) {

View file

@ -6,11 +6,18 @@ if (!User::isLogged()) {
}
$userId = User::getId();
$videos_id = getVideos_id();
// List of relative directories (must end with slash)
$relativeDirs = [
if($videos_id){
$relativeDirs = [
Video::getVideoLibRelativePath($videos_id),
];
}else{
$relativeDirs = [
"videos/userPhoto/Live/user_{$userId}/",
];
];
}
$allowed_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
$images = [];
@ -19,7 +26,7 @@ foreach ($relativeDirs as $relativeDir) {
$absoluteDir = realpath(__DIR__ . "/../{$relativeDir}");
// Security check: must be valid and inside videos folder
if (!$absoluteDir || strpos($absoluteDir, realpath(__DIR__ . '/../videos/userPhoto/Live/')) !== 0) {
if (!$absoluteDir || strpos($absoluteDir, realpath(__DIR__ . '/../videos/')) !== 0) {
continue;
}

View file

@ -1,6 +1,7 @@
<?php
global $global, $config;
require_once __DIR__ . '/../videos/configuration.php';
$videos_id = getVideos_id();
$_page = new Page(array('List Categories'));
?>
<link href="<?php echo getURL('view/mini-upload-form/assets/css/style.css'); ?>" rel="stylesheet" />
@ -9,6 +10,7 @@ $_page = new Page(array('List Categories'));
.image-col {
margin-bottom: 15px;
}
.image-col img {
margin: 0 !important;
}
@ -31,6 +33,7 @@ $_page = new Page(array('List Categories'));
<div id="drop">
<a><?php echo __("Browse files"); ?></a>
<input type="file" name="upl" id="fileInput" multiple accept="image/*" />
<input type="hidden" name="videos_id" value="<?php echo $videos_id; ?>" />
</div>
<ul>
<!-- Upload progress shown here -->
@ -49,8 +52,15 @@ $_page = new Page(array('List Categories'));
<script src="<?php echo getURL('view/mini-upload-form/assets/js/jquery.fileupload.js'); ?>"></script>
<script>
var videos_id = <?php echo json_encode($videos_id ?: null); ?>;
function loadImages() {
$.getJSON(webSiteRootURL + 'view/list-images.json.php', function(images) {
const url = webSiteRootURL + 'view/list-images.json.php';
const query = videos_id ? {
videos_id: videos_id
} : {};
$.getJSON(url, query, function(images) {
$('#imageGrid').empty();
images.forEach(function(img) {
const col = $('<div class="col-xs-6 col-sm-4 col-md-3 text-center image-col"></div>');
@ -62,7 +72,8 @@ $_page = new Page(array('List Categories'));
avideoConfirm(__('Are you sure you want to delete this image?')).then(function(response) {
if (response) {
$.post(webSiteRootURL + 'view/list-images.delete.json.php', {
filename: img.filename
filename: img.filename,
videos_id: videos_id
}, function(response) {
if (!response.error) {
col.remove();
@ -81,7 +92,8 @@ $_page = new Page(array('List Categories'));
const uid = new URLSearchParams(window.location.search).get('uid');
window.parent.postMessage({
selectedImageURL: image.data('src'),
croppieUID: uid
croppieUID: uid,
videos_id: videos_id
}, '*');
});
@ -91,6 +103,7 @@ $_page = new Page(array('List Categories'));
});
}
$(document).ready(function() {
$('#drop a').click(function() {
$(this).parent().find('input').click();

View file

@ -13,7 +13,14 @@ if (!User::isLogged()) {
}
$userId = User::getId();
$relativeDir = "videos/userPhoto/Live/user_{$userId}/";
if (!empty($_REQUEST['videos_id'])) {
$relativeDir = Video::getVideoLibRelativePath($_REQUEST['videos_id']);
} else {
$relativeDir = "videos/userPhoto/Live/user_{$userId}/";
}
$absoluteDir = __DIR__ . '/../' . $relativeDir;
make_path($relativeDir);