1
0
cms11/app/Console/Commands/Bundle/Describe.php

393 lines
12 KiB
PHP

<?php
namespace App\Console\Commands\Bundle;
use App\Classes\AttachmentsManager;
use App\Classes\Bundle;
use App\Console\Commands\Bundle\Traits\ReadsBundles;
use App\Console\Commands\Bundle\Traits\SelectsDisks;
use App\ImageFilters\ForAI;
use App\Services\AI\AIHandler;
use App\Services\AI\Facades\AI;
use App\Services\Translator;
use Carbon\CarbonInterval;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Process;
use Intervention\Image\Laravel\Facades\Image;
use Text_LanguageDetect;
use function Laravel\Prompts\progress;
use function Laravel\Prompts\select;
use function Laravel\Prompts\textarea;
class Describe extends Command
{
use ReadsBundles;
use SelectsDisks;
/**
* The console command description.
*
* @var string
*/
protected $description = 'Add description to bundles';
public function __construct()
{
$this->signature = 'bundle:describe
{ --r|recursive : Also upgrade sub-bundles }
{ --source-disk= : Use specified content disk - Defaults to <info>' . env('CONTENT_DISK') . '</info> }
{ path? : Path to a specific bundle to upgrade - Default to <info>/</info> }
';
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
$this->selectDisk()
->selectBundles()
->perform();
}
private function formatNanoseconds(int $nanoseconds): string
{
// Conversion des nanosecondes en secondes
$seconds = $nanoseconds / 1e9;
// Création d'un intervalle de temps à partir des secondes
$interval = CarbonInterval::seconds($seconds)->cascade();
// Formatage de l'intervalle de temps en une chaîne lisible
return $interval->forHumans();
}
private function perform()
{
progress('Updating bundles...', $this->bundles, function (Bundle $bundle, $progress) {
$this->handleBundle($bundle, $progress);
});
}
private function handleBundle(Bundle $bundle, $progress)
{
$allowedSections = [
'/blog/',
'/critiques/',
];
if (!in_array($bundle->getSection()->getPath(), $allowedSections)) {
return;
}
$this->describeContent($bundle, $progress);
$this->describeAttachments($bundle, $progress);
}
private function describeContent(Bundle $bundle, $progress)
{
$bundle->load();
if ($bundle->metadata()->has('description')) {
return;
}
$content = $bundle->markdown()->get();
if (empty($content)) {
return;
}
$isSelected = false;
while (!$isSelected) {
$result = $this->askAiToSummarizeBundle($bundle);
switch ($result) {
case 'fresh':
$text = textarea(
label: sprintf('Custom description for "%s"', $bundle->getArticleTitle()),
required: true
);
$bundle->metadata()->set('description', [
'text' => $text,
]);
$bundle->metadata()->save();
$isSelected = true;
break;
case 'regenerate':
break;
case 'skip':
$isSelected = true;
break;
case 'skip_forever':
$bundle->metadata()->set('description', '');
$bundle->metadata()->save();
$isSelected = true;
break;
default:
$ld = new Text_LanguageDetect();
$ld->setNameMode(2);
$text = $result['response'];
$lang = $ld->detectSimple($text);
$currentLocale = app()->getLocale();
$currentLanguage = substr($currentLocale, 0, 2);
if ($currentLanguage !== $lang) {
$text = Translator::translate($text, $lang, $currentLanguage);
}
$newText = textarea(
label: sprintf('Confirm description for "%s"', $bundle->getArticleTitle()),
default: $text,
required: true
);
$bundle->metadata()->set('description', [
'text' => $newText,
'generator' => $result['handler'],
'model' => $result['model'],
'edited' => $text !== $newText,
]);
$bundle->metadata()->save();
$isSelected = true;
break;
}
}
}
private function describeAttachments(Bundle $bundle, $progress)
{
foreach ($bundle->attachments(AttachmentsManager::Images)->manager()->get('files') ?? [] as $ref => $data) {
if (array_key_exists('alt', $data)) {
continue;
}
$this->describeAttachment($bundle, $ref, $data);
}
}
private function describeAttachment(Bundle $bundle, string $ref, array $data)
{
$manager = $bundle->attachments(AttachmentsManager::Images);
$fullPath = $manager->getAttachmentFullPath($ref);
$content = $this->sourceDisk->get($fullPath);
$content = Image::read($content)->modify(new ForAI())->toJpeg();
$base64 = base64_encode($content);
$isSelected = false;
while (!$isSelected) {
$result = $this->askAiToDescribeAttachment($fullPath, $base64);
switch ($result) {
case 'fresh':
$text = textarea(
label: sprintf('Custom description for %s', $fullPath),
required: true
);
$manager->manager()->set(sprintf('files.%s.alt', $ref), [
'text' => $text,
]);
$manager->manager()->save();
$isSelected = true;
break;
case 'regenerate':
break;
case 'skip':
$isSelected = true;
break;
case 'skip_forever':
$manager->manager()->set(sprintf('files.%s.alt', $ref), '');
$manager->manager()->save();
$isSelected = true;
break;
default:
$ld = new Text_LanguageDetect();
$ld->setNameMode(2);
$text = $result['response'];
$lang = $ld->detectSimple($text);
$currentLocale = app()->getLocale();
$currentLanguage = substr($currentLocale, 0, 2);
if ($currentLanguage !== $lang) {
$text = Translator::translate($text, $lang, $currentLanguage);
}
$newText = textarea(
label: sprintf('Confirm description for %s', $fullPath),
default: $text,
required: true
);
$manager->manager()->set(sprintf('files.%s.alt', $ref), [
'text' => $newText,
'generator' => $result['handler'],
'model' => $result['model'],
'edited' => $newText !== $text,
]);
$manager->manager()->save();
$isSelected = true;
break;
}
}
}
private function askAiToSummarizeBundle(Bundle $bundle)
{
$prompt = 'Summarize the content of the following Markdown text:
```markdown
# ' . $bundle->getArticleTitle() . '
' . $bundle->markdown()->get() . '
```
The goal is to create a summary that captures the spirit and key points of the page. Use concise phrases, but ensure they reflect the tone and content of the Markdown text.
The summary should be written in English or French and not exceed 255 characters. It will be used in the `<meta description>` tag of the page to help search engines understand the content of the file.
Do not include any header to the answer.
';
$results = AI::providers([
AI::make('open-webui')->forService(AIHandler::SERVICE_SUMMARIZE_CONTENT)->withPrompt($prompt),
])->run();
$propositions = [];
$index = 1;
foreach ($results as $handler => $models) {
foreach ($models as $model => $result) {
$this->newLine(1);
$parts = [
sprintf('[<info>%d</info>] Proposed by <comment>%s</comment> ', $index, $handler),
sprintf('and using the <comment>%s</comment> model', $model),
];
if (!empty($result['total_duration'])) {
$parts[] = sprintf(' (done in <comment>%s</comment>)', $this->formatNanoseconds($result['total_duration']));
}
$this->line(implode('', $parts));
$response = trim($result['response'] ?? 'Error');
$this->line($response);
$propositions[$index] = [
'handler' => $handler,
'model' => $model,
'response' => $response,
];
$index++;
}
}
$options = [
'fresh' => 'Let me write my own',
'regenerate' => 'Regenerate suggestions',
'skip' => 'Skip',
'skip_forever' => 'Never ask again for this file',
];
foreach (array_keys($propositions) as $index) {
$options[strval($index)] = $index;
}
$selection = select(
sprintf('Which description do you prefer for "%s" ?', $bundle->getArticleTitle()),
$options,
'regenerate',
10
);
if (is_numeric($selection)) {
return $propositions[$selection];
}
return $selection;
}
private function askAiToDescribeAttachment(string $fullPath, string $base64)
{
$results = AI::providers([
AI::make('open-webui')->forService(AIHandler::SERVICE_DESCRIBE_IMAGE)->withFiles([$base64]),
])->run();
$propositions = [];
$index = 1;
foreach ($results as $handler => $models) {
foreach ($models as $model => $result) {
$this->newLine(1);
$parts = [
sprintf('[<info>%d</info>] Proposed by <comment>%s</comment> ', $index, $handler),
sprintf('and using the <comment>%s</comment> model', $model),
];
if (!empty($result['total_duration'])) {
$parts[] = sprintf(' (done in <comment>%s</comment>)', $this->formatNanoseconds($result['total_duration']));
}
$this->line(implode('', $parts));
$response = trim($result['response'] ?? 'Error');
$this->line($response);
$propositions[$index] = [
'handler' => $handler,
'model' => $model,
'response' => $response,
];
$index++;
}
}
$options = [
'fresh' => 'Let me write my own',
'regenerate' => 'Regenerate suggestions',
'skip' => 'Skip',
'skip_forever' => 'Never ask again for this file',
];
foreach (array_keys($propositions) as $index) {
$options[strval($index)] = $index;
}
$result = Process::tty()->run(sprintf('lsix /content%s', $fullPath));
echo $result->output();
$selection = select(
sprintf('Which description do you prefer for %s ?', $fullPath),
$options,
'regenerate',
10
);
if (is_numeric($selection)) {
return $propositions[$selection];
}
return $selection;
}
}