feat(AI): add ai manager admin and create powerpoint with ai
This commit is contained in:
81
app/Services/Ai/OpenAiCompatibleTextGenerator.php
Normal file
81
app/Services/Ai/OpenAiCompatibleTextGenerator.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use App\Enums\TemplateKeyType;
|
||||
use App\Models\AiProvider;
|
||||
use App\Models\Template;
|
||||
use App\Models\TemplateKey;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use RuntimeException;
|
||||
|
||||
class OpenAiCompatibleTextGenerator
|
||||
{
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public function generateForTemplate(Template $template, string $topic, AiProvider $provider): array
|
||||
{
|
||||
$textKeys = $template->keys->where('type', TemplateKeyType::Text);
|
||||
|
||||
if ($textKeys->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$keysDescription = $textKeys
|
||||
->map(fn (TemplateKey $key): string => sprintf('- %s (%s)%s', $key->key, $key->label, $key->description ? ': '.$key->description : ''))
|
||||
->implode("\n");
|
||||
|
||||
$systemPrompt = <<<PROMPT
|
||||
شما دستیار تولید محتوای پاورپوینت هستید. بر اساس موضوعی که کاربر میدهد، برای هرکدام از کلیدهای زیر یک متن مناسب فارسی تولید کن.
|
||||
فقط یک JSON معتبر برگردان که کلیدهای آن دقیقاً و بدون هیچ تغییری (بدون تغییر حروف بزرگ/کوچک، بدون ترجمه، بدون حذف فاصله) برابر با نام کلیدهای زیر باشد. هیچ توضیح اضافهای برنگردان.
|
||||
|
||||
کلیدها:
|
||||
{$keysDescription}
|
||||
PROMPT;
|
||||
|
||||
$response = Http::withToken($provider->api_key)
|
||||
->timeout(60)
|
||||
->post(rtrim($provider->resolvedBaseUrl(), '/').'/chat/completions', [
|
||||
'model' => $provider->model,
|
||||
'response_format' => ['type' => 'json_object'],
|
||||
'messages' => [
|
||||
['role' => 'system', 'content' => $systemPrompt],
|
||||
['role' => 'user', 'content' => $topic],
|
||||
],
|
||||
]);
|
||||
|
||||
if ($response->failed()) {
|
||||
throw new RuntimeException('خطا در ارتباط با سرویس هوش مصنوعی: '.$response->body());
|
||||
}
|
||||
|
||||
$decoded = json_decode((string) $response->json('choices.0.message.content'), true);
|
||||
|
||||
if (! is_array($decoded)) {
|
||||
throw new RuntimeException('پاسخ هوش مصنوعی قابل تفسیر نبود.');
|
||||
}
|
||||
|
||||
$normalizedDecoded = [];
|
||||
|
||||
foreach ($decoded as $decodedKey => $decodedValue) {
|
||||
$normalizedDecoded[$this->normalizeKey((string) $decodedKey)] = $decodedValue;
|
||||
}
|
||||
|
||||
$values = [];
|
||||
|
||||
foreach ($textKeys as $key) {
|
||||
$normalizedKey = $this->normalizeKey($key->key);
|
||||
|
||||
if (array_key_exists($normalizedKey, $normalizedDecoded)) {
|
||||
$values[$key->key] = (string) $normalizedDecoded[$normalizedKey];
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
private function normalizeKey(string $key): string
|
||||
{
|
||||
return mb_strtolower(trim($key, "{} \t\n\r\0\x0B"));
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,11 @@ use PhpOffice\PhpPresentation\IOFactory;
|
||||
use PhpOffice\PhpPresentation\PhpPresentation;
|
||||
use PhpOffice\PhpPresentation\Shape\Drawing\File as FileDrawing;
|
||||
use PhpOffice\PhpPresentation\Shape\Drawing\Gd as GdDrawing;
|
||||
use PhpOffice\PhpPresentation\Shape\Group as GroupShape;
|
||||
use PhpOffice\PhpPresentation\Shape\RichText;
|
||||
use PhpOffice\PhpPresentation\Shape\RichText\BreakElement;
|
||||
use PhpOffice\PhpPresentation\Shape\RichText\Paragraph;
|
||||
use PhpOffice\PhpPresentation\Shape\Table as TableShape;
|
||||
use RuntimeException;
|
||||
|
||||
class TemplateFillService
|
||||
@@ -23,18 +27,26 @@ class TemplateFillService
|
||||
|
||||
$values = $generation->values ?? [];
|
||||
|
||||
$textValues = [];
|
||||
|
||||
foreach ($template->keys as $key) {
|
||||
if (! array_key_exists($key->key, $values)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
match ($key->type) {
|
||||
TemplateKeyType::Text => $this->fillText($presentation, $key->key, (string) $values[$key->key]),
|
||||
TemplateKeyType::Text => $textValues[$key->key] = (string) $values[$key->key],
|
||||
TemplateKeyType::Image => $this->fillImage($presentation, $key->shape_name, Storage::disk('local')->path($values[$key->key])),
|
||||
TemplateKeyType::Chart => throw new RuntimeException('پرکردن کلید از نوع چارت هنوز پشتیبانی نمیشود.'),
|
||||
};
|
||||
}
|
||||
|
||||
if ($textValues !== []) {
|
||||
foreach ($presentation->getAllSlides() as $slide) {
|
||||
$this->replaceInShapes($slide->getShapeCollection(), $textValues);
|
||||
}
|
||||
}
|
||||
|
||||
$outputRelativePath = 'templates/generations/'.$generation->uuid.'.pptx';
|
||||
$outputAbsolutePath = Storage::disk('local')->path($outputRelativePath);
|
||||
|
||||
@@ -46,27 +58,103 @@ class TemplateFillService
|
||||
return $outputRelativePath;
|
||||
}
|
||||
|
||||
private function fillText(PhpPresentation $presentation, string $key, string $value): void
|
||||
/**
|
||||
* @param iterable<mixed> $shapes
|
||||
* @param array<string, string> $textValues
|
||||
*/
|
||||
private function replaceInShapes(iterable $shapes, array $textValues): void
|
||||
{
|
||||
$placeholder = '{{'.$key.'}}';
|
||||
|
||||
foreach ($presentation->getAllSlides() as $slide) {
|
||||
foreach ($slide->getShapeCollection() as $shape) {
|
||||
if (! $shape instanceof RichText) {
|
||||
continue;
|
||||
foreach ($shapes as $shape) {
|
||||
if ($shape instanceof RichText) {
|
||||
foreach ($shape->getParagraphs() as $paragraph) {
|
||||
$this->replaceInParagraph($paragraph, $textValues);
|
||||
}
|
||||
|
||||
foreach ($shape->getParagraphs() as $paragraph) {
|
||||
foreach ($paragraph->getRichTextElements() as $element) {
|
||||
if (str_contains($element->getText(), $placeholder)) {
|
||||
$element->setText(str_replace($placeholder, $value, $element->getText()));
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($shape instanceof TableShape) {
|
||||
foreach ($shape->getRows() as $row) {
|
||||
foreach ($row->getCells() as $cell) {
|
||||
foreach ($cell->getParagraphs() as $paragraph) {
|
||||
$this->replaceInParagraph($paragraph, $textValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($shape instanceof GroupShape) {
|
||||
$this->replaceInShapes($shape->getShapeCollection(), $textValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $textValues
|
||||
*/
|
||||
private function replaceInParagraph(Paragraph $paragraph, array $textValues): void
|
||||
{
|
||||
$elements = $paragraph->getRichTextElements();
|
||||
|
||||
if ($elements === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$originalText = '';
|
||||
|
||||
foreach ($elements as $element) {
|
||||
$originalText .= $element->getText();
|
||||
}
|
||||
|
||||
if (! str_contains($originalText, '{')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lookup = [];
|
||||
|
||||
foreach ($textValues as $key => $value) {
|
||||
$lookup[$this->normalizeKey($key)] = $value;
|
||||
}
|
||||
|
||||
$newText = preg_replace_callback(
|
||||
'/\{\{?\s*([^{}]+?)\s*\}?\}/u',
|
||||
function (array $matches) use ($lookup): string {
|
||||
$normalizedKey = $this->normalizeKey($matches[1]);
|
||||
|
||||
return array_key_exists($normalizedKey, $lookup) ? $lookup[$normalizedKey] : $matches[0];
|
||||
},
|
||||
$originalText
|
||||
);
|
||||
|
||||
if ($newText === null || $newText === $originalText) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target = null;
|
||||
|
||||
foreach ($elements as $element) {
|
||||
if ($element instanceof BreakElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($target === null) {
|
||||
$target = $element;
|
||||
$element->setText($newText);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$element->setText('');
|
||||
}
|
||||
}
|
||||
|
||||
private function normalizeKey(string $key): string
|
||||
{
|
||||
return mb_strtolower(trim($key, "{} \t\n\r\0\x0B"));
|
||||
}
|
||||
|
||||
private function fillImage(PhpPresentation $presentation, ?string $shapeName, string $imagePath): void
|
||||
{
|
||||
if ($shapeName === null || $shapeName === '') {
|
||||
@@ -74,16 +162,30 @@ class TemplateFillService
|
||||
}
|
||||
|
||||
foreach ($presentation->getAllSlides() as $slide) {
|
||||
foreach ($slide->getShapeCollection() as $shape) {
|
||||
if ($shape->getName() !== $shapeName) {
|
||||
continue;
|
||||
}
|
||||
$this->replaceImageInShapes($slide->getShapeCollection(), $shapeName, $imagePath);
|
||||
}
|
||||
}
|
||||
|
||||
if ($shape instanceof GdDrawing) {
|
||||
$this->replaceGdImage($shape, $imagePath);
|
||||
} elseif ($shape instanceof FileDrawing) {
|
||||
$shape->setPath($imagePath);
|
||||
}
|
||||
/**
|
||||
* @param iterable<mixed> $shapes
|
||||
*/
|
||||
private function replaceImageInShapes(iterable $shapes, string $shapeName, string $imagePath): void
|
||||
{
|
||||
foreach ($shapes as $shape) {
|
||||
if ($shape instanceof GroupShape) {
|
||||
$this->replaceImageInShapes($shape->getShapeCollection(), $shapeName, $imagePath);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($shape->getName() !== $shapeName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($shape instanceof GdDrawing) {
|
||||
$this->replaceGdImage($shape, $imagePath);
|
||||
} elseif ($shape instanceof FileDrawing) {
|
||||
$shape->setPath($imagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user