Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added emptyTemplates/ai/pitch.odp
Binary file not shown.
Binary file added emptyTemplates/ai/security.odp
Binary file not shown.
Binary file added emptyTemplates/ai/triangle.odp
Binary file not shown.
97 changes: 72 additions & 25 deletions lib/Service/SlideDeckService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
namespace OCA\Richdocuments\Service;

use OCA\Richdocuments\AppInfo\Application;
use OCA\Richdocuments\TaskProcessing\Presentation\LayoutType;
use OCA\Richdocuments\TaskProcessing\Presentation\Presentation;
use OCA\Richdocuments\TaskProcessing\Presentation\Slides\TitleContentSlide;
use OCA\Richdocuments\TaskProcessing\Presentation\Slides\TitleSlide;
use OCA\Richdocuments\TemplateManager;
use OCP\IConfig;
use OCP\TaskProcessing\Exception\Exception;
Expand All @@ -20,11 +24,40 @@

class SlideDeckService {
public const PROMPT = <<<EOF
Draft a presentation slide deck with headlines and a maximum of 5 bullet points per headline.
Use the following JSON structure for your whole output and output only the JSON array:
Draft a presentation with slides based on the following JSON.
Replace the title, subtitle, and content with your own.
If the content is an array of bullet point strings, replace them as necessary and always use at least four of them. Do not place any dot (.) or hyphen (-) before the bullet points.
Choose one of the following three presentation styles and replace the "presentationStyle" field with your choice: security, pitch, triangle
Security is dark and serious, pitch is light and playful, and triangle is light and geometric.
The slide titles should not contain more than three words.
Use the following JSON structure for your entire output.
Output 5 or more of the JSON objects, and use each of them at least once.
Output only the JSON array:

```
[{"headline": "Headline 1", "points": ["Bullet point 1", "Bullet point 2"]}, {"headline": "Headline 2", "points": ["Bullet point 1", "Bullet point 2"]}]
[
{
"layout": 0,
"title": "Presentation title",
"subtitle": "Presentation subtitle"
},
{
"layout": 1,
"title": "Slide title",
"content": "Paragraph or other longer text"
},
{
"layout": 1,
"title": "Slide title",
"content": [
"Bullet point one",
"Bullet point two",
"Bullet point three",
"Bullet point four"
]
},
{ "presentationStyle": "" },
]
```

Only output the JSON array. Do not wrap it with spaces, new lines or backticks (`).
Expand All @@ -45,19 +78,21 @@ public function generateSlideDeck(?string $userId, string $presentationText) {

$ooxml = $this->config->getAppValue(Application::APPNAME, 'doc_format', 'ooxml') === 'ooxml';
$format = $ooxml ? 'pptx' : 'odp';
$emptyPresentation = $this->getBlankPresentation($format);

try {
$parsedStructure = $this->parseModelJSON($rawModelOutput);
[$presentationStyle, $parsedStructure] = $this->parseModelJSON($rawModelOutput);
} catch (\JsonException) {
throw new RuntimeException('LLM generated faulty JSON data');
}

$emptyPresentation = $this->getPresentationTemplate($presentationStyle);

try {
$transformedPresentation = $this->remoteService->transformDocumentStructure(
'presentation.' . $format,
$emptyPresentation,
$parsedStructure
$parsedStructure,
$format
);

return $transformedPresentation;
Expand All @@ -81,37 +116,49 @@ private function parseModelJSON(string $jsonString): array {
flags: JSON_THROW_ON_ERROR
);

$slideCommands = [];
foreach ($modelJSON as $index => $slide) {
if (count($slideCommands) > 0) {
$slideCommands[] = [ 'JumpToSlide' => 'last' ];
$slideCommands[] = [ 'InsertMasterSlide' => 0 ];
} else {
$slideCommands[] = [ 'JumpToSlide' => $index];
$layoutTypes = array_column(LayoutType::cases(), 'value');
$presentation = new Presentation();

foreach ($modelJSON as $index => $slideJSON) {
if ($slideJSON['presentationStyle']) {
$presentation->setStyle($slideJSON['presentationStyle']);
continue;
}

$validLayout = array_key_exists($slideJSON['layout'], $layoutTypes);

if (!$validLayout) {
continue;
}

$slideCommands[] = [ 'ChangeLayoutByName' => 'AUTOLAYOUT_TITLE_CONTENT' ];
$slideCommands[] = [ 'SetText.0' => $slide['headline'] ];
$slideLayout = LayoutType::from($layoutTypes[$slideJSON['layout']]);

$editTextObjectCommands = [
[ 'SelectParagraph' => 0 ],
[ 'InsertText' => implode(PHP_EOL, $slide['points']) ],
];
$slide = match ($slideLayout) {
LayoutType::Title => new TitleSlide($index, $slideJSON['title'], $slideJSON['subtitle']),

LayoutType::TitleContent => new TitleContentSlide($index, $slideJSON['title'], $slideJSON['content']),

default => null,
};

if (is_null($slide)) {
continue;
}

$slideCommands[] = [ 'EditTextObject.1' => $editTextObjectCommands ];
$presentation->addSlide($slide);
}

return [ 'SlideCommands' => $slideCommands ];
return [$presentation->getStyle(), $presentation->getSlideCommands()];
}

/**
* Creates a blank presentation file in memory
* Creates a presentation file in memory
*
* @param string $format
* @param string $name
* @return resource
*/
private function getBlankPresentation(string $format) {
$emptyPresentationContent = $this->templateManager->getEmptyFileContent($format);
private function getPresentationTemplate(string $name = '') {
$emptyPresentationContent = $this->templateManager->getAITemplate($name);
$memoryStream = fopen('php://memory', 'r+');

if (!$memoryStream) {
Expand Down
12 changes: 12 additions & 0 deletions lib/TaskProcessing/Presentation/ISlide.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Richdocuments\TaskProcessing\Presentation;

interface ISlide {
public function getPosition(): int;
public function getSlideCommands(): array;
}
13 changes: 13 additions & 0 deletions lib/TaskProcessing/Presentation/LayoutType.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Richdocuments\TaskProcessing\Presentation;

enum LayoutType: string {
case Title = 'AUTOLAYOUT_TITLE';
case TitleContent = 'AUTOLAYOUT_TITLE_CONTENT';
case Title2Content = 'AUTOLAYOUT_TITLE_2CONTENT';
}
56 changes: 56 additions & 0 deletions lib/TaskProcessing/Presentation/Presentation.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Richdocuments\TaskProcessing\Presentation;

class Presentation {
/** @var ISlide[] $slides */
private $slides = [];

/** @var string $style */
private $style = 'security';

public function __construct() {
}

/**
* @param ISlide $slide Slide to be inserted into the presentation
*/
public function addSlide(ISlide $slide): void {
$this->slides[] = $slide;
}

/**
* @return ISlide[] Array of slides in the presentation
*/
public function getSlides(): array {
return $this->slides;
}

public function setStyle(string $style): void {
$this->style = $style;
}

public function getStyle(): string {
return $this->style;
}

/**
* @return array Slide commands to be passed to an external API
*/
public function getSlideCommands(): array {
$slideCommands = array_map(
function (ISlide $slide) {
return $slide->getSlideCommands();
},
$this->getSlides(),
);

$slideCommands = array_merge([], ...$slideCommands);

return [ 'SlideCommands' => $slideCommands ];
}
}
75 changes: 75 additions & 0 deletions lib/TaskProcessing/Presentation/Slides/TitleContentSlide.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Richdocuments\TaskProcessing\Presentation\Slides;

use OCA\Richdocuments\TaskProcessing\Presentation\ISlide;

class TitleContentSlide implements ISlide {
private int $position;
private string $title;
private string|array $content;

public function __construct(
int $position,
string $title,
string|array $content) {
$this->position = $position;
$this->title = $title;
$this->content = $content;
}

public function getTitle(): string {
return $this->title;
}

public function getContent(): string|array {
return $this->content;
}

public function getPosition(): int {
return $this->position;
}

public function getSlideCommands(): array {
$slideCommands = [];

if ($this->getPosition() > 1) {
$slideCommands[] = [ 'DuplicateSlide' => $this->getPosition() - 1 ];
}

$slideCommands[] = [ 'JumpToSlide' => $this->getPosition() ];

$slideCommands[] = [
'EditTextObject.0' => [
'SelectParagraph' => 0,
'InsertText' => $this->getTitle(),
]
];

if (is_array($this->getContent())) {
$slideCommands[] = [
'EditTextObject.1' => [
'SelectText' => [],
'UnoCommand' => '.uno:Cut',
'InsertText' => implode(PHP_EOL, array_map(function ($bulletPoint) {
return '• ' . $bulletPoint;
}, $this->getContent())),
]
];
} else {
$slideCommands[] = [
'EditTextObject.1' => [
'SelectText' => [],
'UnoCommand' => '.uno:Cut',
'InsertText' => $this->getContent(),
]
];
}

return $slideCommands;
}
}
56 changes: 56 additions & 0 deletions lib/TaskProcessing/Presentation/Slides/TitleSlide.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Richdocuments\TaskProcessing\Presentation\Slides;

use OCA\Richdocuments\TaskProcessing\Presentation\ISlide;

class TitleSlide implements ISlide {
private int $position;
private string $title;
private string $subtitle;

public function __construct(
int $position,
string $title,
string $subtitle) {
$this->position = $position;
$this->title = $title;
$this->subtitle = $subtitle;
}

public function getTitle(): string {
return $this->title;
}

public function getSubtitle(): string {
return $this->subtitle;
}

public function getPosition(): int {
return $this->position;
}

public function getSlideCommands(): array {
$slideCommands = [];

$slideCommands[] = [
'EditTextObject.0' => [
'SelectParagraph' => 0,
'InsertText' => $this->getTitle(),
]
];

$slideCommands[] = [
'EditTextObject.1' => [
'SelectParagraph' => 0,
'InsertText' => $this->getSubtitle(),
]
];

return $slideCommands;
}
}
25 changes: 21 additions & 4 deletions lib/TaskProcessing/SlideDeckGenerationProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,28 @@ public function process(?string $userId, array $input, callable $reportProgress)
throw new \RuntimeException('Invalid input, expected "text" key with string value');
}

$response = $this->slideDeckService->generateSlideDeck(
$userId,
$input['text'],
);
$response = $this->withRetry(function () use ($userId, $input) {
return $this->slideDeckService->generateSlideDeck(
$userId,
$input['text'],
);
});

return ['slide_deck' => $response];
}

private function withRetry(callable $action, $maxAttempts = 2) {
$attempt = 0;

while ($attempt < $maxAttempts) {
try {
$attempt += 1;
return $action();
} catch (\Exception $e) {
if ($attempt === $maxAttempts) {
throw $e;
}
}
}
}
}
Loading
Loading