feat: Implement AI Watermarking

to comply with EU AI Act

Signed-off-by: Marcel Klehr <mklehr@gmx.net>
This commit is contained in:
Marcel Klehr 2025-11-27 10:08:31 +01:00
parent d3bcc030aa
commit 0c248a14c6
4 changed files with 29 additions and 12 deletions

View file

@ -10,6 +10,7 @@ use League\CommonMark\GithubFlavoredMarkdownConverter;
use OCA\Richdocuments\AppInfo\Application;
use OCA\Richdocuments\TaskProcessing\TextToDocumentProvider;
use OCA\Richdocuments\TaskProcessing\TextToSpreadsheetProvider;
use OCP\IL10N;
use OCP\TaskProcessing\Exception\Exception;
use OCP\TaskProcessing\Exception\PreConditionNotMetException;
use OCP\TaskProcessing\Exception\UnauthorizedException;
@ -37,15 +38,17 @@ EOF;
public function __construct(
private IManager $taskProcessingManager,
private RemoteService $remoteService,
private IL10N $l10n,
) {
}
public function generateTextDocument(?string $userId, string $description, string $targetFormat = TextToDocumentProvider::DEFAULT_TARGET_FORMAT) {
public function generateTextDocument(?string $userId, string $description, string $targetFormat = TextToDocumentProvider::DEFAULT_TARGET_FORMAT, bool $includeWatermark = true) {
$prompt = self::TEXT_PROMPT;
$taskInput = $prompt . "\n\n" . $description;
$markdownContent = $this->runTextToTextTask($taskInput, $userId);
$converter = new GithubFlavoredMarkdownConverter();
$htmlContent = $converter->convert($markdownContent)->getContent();
$markdownContentWithAiNote = $includeWatermark ? $markdownContent . "\n\n" . $this->l10n->t('This document was generated using Artificial Intelligence') : $markdownContent;
$htmlContent = $converter->convert($markdownContentWithAiNote)->getContent();
$htmlStream = $this->stringToStream($htmlContent);
$docxContent = $this->remoteService->convertTo('document.html', $htmlStream, $targetFormat);

View file

@ -13,6 +13,7 @@ use OCA\Richdocuments\TaskProcessing\Presentation\Slides\TitleContentSlide;
use OCA\Richdocuments\TaskProcessing\Presentation\Slides\TitleSlide;
use OCA\Richdocuments\TemplateManager;
use OCP\IConfig;
use OCP\IL10N;
use OCP\TaskProcessing\Exception\Exception;
use OCP\TaskProcessing\Exception\PreConditionNotMetException;
use OCP\TaskProcessing\Exception\UnauthorizedException;
@ -70,17 +71,18 @@ EOF;
private TemplateManager $templateManager,
private RemoteService $remoteService,
private IConfig $config,
private IL10N $l10n,
) {
}
public function generateSlideDeck(?string $userId, string $presentationText) {
public function generateSlideDeck(?string $userId, string $presentationText, bool $includeWatermark = true) {
$rawModelOutput = $this->runLLMQuery($userId, $presentationText);
$ooxml = $this->config->getAppValue(Application::APPNAME, 'doc_format', 'ooxml') === 'ooxml';
$format = $ooxml ? 'pptx' : 'odp';
try {
[$presentationStyle, $parsedStructure] = $this->parseModelJSON($rawModelOutput);
[$presentationStyle, $parsedStructure] = $this->parseModelJSON($rawModelOutput, $includeWatermark);
} catch (\JsonException) {
throw new RuntimeException('LLM generated faulty JSON data');
}
@ -108,7 +110,7 @@ EOF;
* @param string $jsonString
* @return array
*/
private function parseModelJSON(string $jsonString): array {
private function parseModelJSON(string $jsonString, bool $includeWatermark = true): array {
$jsonString = trim($jsonString, "` \n\r\t\v\0");
$modelJSON = json_decode(
$jsonString,
@ -148,9 +150,19 @@ EOF;
$presentation->addSlide($slide);
}
if ($includeWatermark) {
// Add a final slide with a note that this was generated by AI
$this->addAiComment(isset($index) ? $index + 1 : 0, $presentation);
}
return [$presentation->getStyle(), $presentation->getSlideCommands()];
}
private function addAiComment(int $index, Presentation $presentation) : void {
$slide = new TitleContentSlide($index, '', $this->l10n->t('Generated using Artificial Intelligence'));
$presentation->addSlide($slide);
}
/**
* Creates a presentation file in memory
*

View file

@ -11,9 +11,9 @@ namespace OCA\Richdocuments\TaskProcessing;
use OCA\Richdocuments\AppInfo\Application;
use OCA\Richdocuments\Service\SlideDeckService;
use OCP\IL10N;
use OCP\TaskProcessing\ISynchronousProvider;
use OCP\TaskProcessing\ISynchronousWatermarkingProvider;
class SlideDeckGenerationProvider implements ISynchronousProvider {
class SlideDeckGenerationProvider implements ISynchronousWatermarkingProvider {
public function __construct(
private SlideDeckService $slideDeckService,
@ -71,7 +71,7 @@ class SlideDeckGenerationProvider implements ISynchronousProvider {
/**
* @inheritDoc
*/
public function process(?string $userId, array $input, callable $reportProgress): array {
public function process(?string $userId, array $input, callable $reportProgress, bool $includeWatermark = true): array {
if ($userId === null) {
throw new \RuntimeException('User ID is required to process the prompt.');
}
@ -80,10 +80,11 @@ class SlideDeckGenerationProvider implements ISynchronousProvider {
throw new \RuntimeException('Invalid input, expected "text" key with string value');
}
$response = $this->withRetry(function () use ($userId, $input) {
$response = $this->withRetry(function () use ($userId, $input, $includeWatermark) {
return $this->slideDeckService->generateSlideDeck(
$userId,
$input['text'],
$includeWatermark,
);
});

View file

@ -12,11 +12,11 @@ use OCA\Richdocuments\AppInfo\Application;
use OCA\Richdocuments\Service\DocumentGenerationService;
use OCP\IL10N;
use OCP\TaskProcessing\EShapeType;
use OCP\TaskProcessing\ISynchronousProvider;
use OCP\TaskProcessing\ISynchronousWatermarkingProvider;
use OCP\TaskProcessing\ShapeDescriptor;
use OCP\TaskProcessing\ShapeEnumValue;
class TextToDocumentProvider implements ISynchronousProvider {
class TextToDocumentProvider implements ISynchronousWatermarkingProvider {
public const DEFAULT_TARGET_FORMAT = 'docx';
public function __construct(
@ -89,7 +89,7 @@ class TextToDocumentProvider implements ISynchronousProvider {
/**
* @inheritDoc
*/
public function process(?string $userId, array $input, callable $reportProgress): array {
public function process(?string $userId, array $input, callable $reportProgress, bool $includeWatermark = true): array {
if ($userId === null) {
throw new \RuntimeException('User ID is required to process the prompt.');
}
@ -108,6 +108,7 @@ class TextToDocumentProvider implements ISynchronousProvider {
$userId,
$input['text'],
$targetFormat,
$includeWatermark
);
return [