{"id":36371781,"url":"https://github.com/in2code-de/texter","last_synced_at":"2026-01-11T14:01:46.233Z","repository":{"id":327735104,"uuid":"1110019960","full_name":"in2code-de/texter","owner":"in2code-de","description":"Using AI to generate texts in TYPO3 backend","archived":false,"fork":false,"pushed_at":"2026-01-03T16:56:53.000Z","size":4144,"stargazers_count":2,"open_issues_count":1,"forks_count":1,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-07T03:19:32.787Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"PHP","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/in2code-de.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-12-04T15:49:17.000Z","updated_at":"2026-01-03T16:56:56.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/in2code-de/texter","commit_stats":null,"previous_names":["in2code-de/texter"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/in2code-de/texter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Ftexter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Ftexter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Ftexter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Ftexter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/in2code-de","download_url":"https://codeload.github.com/in2code-de/texter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Ftexter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28306985,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-11T11:18:18.743Z","status":"ssl_error","status_checked_at":"2026-01-11T11:07:56.842Z","response_time":60,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-11T14:01:04.552Z","updated_at":"2026-01-11T14:01:46.180Z","avatar_url":"https://github.com/in2code-de.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Texter - AI generated texts in TYPO3 with Google Gemini\n\n## Table of Contents\n\n- [Introduction](#introduction)\n- [Google Gemini API](#google-gemini-api)\n- [Installation](#installation)\n- [Custom LLM Integration (like ChatGPT, Claude, Mistral, etc.)](#custom-llm-integration-like-chatgpt-claude-mistral-etc)\n- [Changelog and breaking changes](#changelog-and-breaking-changes)\n- [Contribution with ddev](#contribution-with-ddev)\n\n## Introduction\n\nAdd AI integration to TYPO3 backend. We simply added a CKEditor plugin to generate texts from AI (Gemini). \n\nExample integration into TYPO3 backend.\n\nExample Video\n![documentation_video_rte.gif](Documentation/Images/documentation_video_rte.gif)\nBetter quality: https://www.youtube.com/watch?v=yPFrigLah3o\n\nVideo image #1\n![documentation_screenshot_rte1.png](Documentation/Images/documentation_screenshot_rte1.png)\n\nVideo image #2\n![documentation_screenshot_rte2.png](Documentation/Images/documentation_screenshot_rte2.png)\n\nVideo image #3\n![documentation_screenshot_rte3.png](Documentation/Images/documentation_screenshot_rte3.png)\n\nVideo image #4\n![documentation_screenshot_rte4.png](Documentation/Images/documentation_screenshot_rte4.png)\n\n## Google Gemini API\n\n- To use the extension, you need a **Google Gemini API** key. You can register for one\n  at https://aistudio.google.com/app/api-keys.\n- Alternatively, you can implement your own LLM provider (see [Custom LLM Integration](#custom-llm-integration-like-chatgpt-claude-mistral-etc) below).\n\n## Installation\n\n### With composer\n\n```\ncomposer req in2code/texter\n```\n\n### Main configuration\n\nAfter that, you have to set some initial configuration in Extension Manager configuration:\n\n| Title         | Default value | Description                                                                                                                                          |\n|---------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------|\n| promptPrefix  | -             | Prefix text that should be always added to the prompt at the beginning                                                                               |\n| apiKey        | -             | Google Gemini API key. You can let this value empty and simply use ENV_VAR \"GOOGLE_API_KEY\" instead if you want to use CI pipelines for this setting |\n\nNote: It's recommended to use ENV vars for in2code/imager instead of saving the API-Key in Extension Manager configuration\n\n```\nGOOGLE_API_KEY=your_api_key_from_google\n```\n\n### RTE configuration\n\nPer default, in2code/texter sets a default RTE configuration via Page TSConfig:\n\n```\nRTE.default.preset = texter\n```\n\nIf you want to overrule this default setting, you can require in2code/texter in your sitepackage (to ensure that your\nextension is loaded after texter) and define a different default preset.\nCheck file [Texter.yaml](Configuration/RTE/Texter.yaml) for an example how to add texter to your RTE configuration.\n\n**Hint** You can also use texter for selected RTE fields in backend. Example Page TSConfig:\n\n```\nRTE.config.tt_content.bodytext.preset = texter\nRTE.config.tx_news_domain_model_news.bodytext.preset = texter\n```\n\n## Custom LLM Integration (like ChatGPT, Claude, Mistral, etc.)\n\nTexter uses a factory pattern to allow custom LLM providers. By default, it uses Google Gemini,\nbut you can easily integrate other AI services (OpenAI, Claude, local models, etc.).\n\n### Implementing a Custom LLM Repository\n\n1. Create a custom repository class implementing `RepositoryInterface` - see example for OpenAI ChatGPT:\n\n```php\n\u003c?php\n\ndeclare(strict_types=1);\n\nnamespace Vendor\\MyExtension\\Domain\\Repository\\Llm;\n\nuse In2code\\Texter\\Domain\\Repository\\Llm\\AbstractRepository;\nuse In2code\\Texter\\Domain\\Repository\\Llm\\RepositoryInterface;\nuse In2code\\Texter\\Domain\\Service\\ConversationHistory;\nuse In2code\\Texter\\Exception\\ApiException;\nuse In2code\\Texter\\Exception\\ConfigurationException;\nuse TYPO3\\CMS\\Core\\Http\\RequestFactory;\n\nclass ChatGptRepository extends AbstractRepository implements RepositoryInterface\n{\n    private string $apiKey = '';\n    private string $apiUrl = 'https://api.openai.com/v1/chat/completions';\n\n    public function __construct(\n        protected RequestFactory $requestFactory,\n        protected ConversationHistory $conversationHistory,\n    ) {\n        parent::__construct($requestFactory, $conversationHistory);\n        $this-\u003eapiKey = getenv('OPENAI_API_KEY') ?: '';\n    }\n\n    public function checkApiKey(): void\n    {\n        if ($this-\u003eapiKey === '') {\n            throw new ConfigurationException('OpenAI API key not configured', 1735646000);\n        }\n    }\n\n    public function getApiUrl(): string\n    {\n        return $this-\u003eapiUrl;\n    }\n\n    public function getText(string $prompt, string $pageId = '0'): string\n    {\n        $this-\u003echeckApiKey();\n        $history = $this-\u003econversationHistory-\u003egetHistory($pageId);\n        $this-\u003econversationHistory-\u003eaddUserMessage($history, $this-\u003eextendPrompt($prompt));\n        $response = $this-\u003econnectToChatGpt($history);\n        $this-\u003econversationHistory-\u003eaddModelResponse($history, $response);\n        $this-\u003econversationHistory-\u003esaveHistory($history, $pageId);\n        return $response;\n    }\n\n    protected function connectToChatGpt(array $conversationHistory): string\n    {\n        // Convert Gemini format to ChatGPT format\n        $messages = $this-\u003econvertHistoryToChatGptFormat($conversationHistory);\n\n        $payload = [\n            'model' =\u003e 'gpt-4o',\n            'messages' =\u003e $messages,\n            'temperature' =\u003e 0.7,\n            'max_tokens' =\u003e 8192,\n        ];\n\n        $options = [\n            'headers' =\u003e [\n                'Authorization' =\u003e 'Bearer ' . $this-\u003eapiKey,\n                'Content-Type' =\u003e 'application/json',\n            ],\n            'body' =\u003e json_encode($payload),\n        ];\n\n        $response = $this-\u003erequestFactory-\u003erequest($this-\u003egetApiUrl(), $this-\u003erequestMethod, $options);\n\n        if ($response-\u003egetStatusCode() !== 200) {\n            throw new ApiException(\n                'Failed to generate text with ChatGPT: ' . $response-\u003egetBody()-\u003egetContents(),\n                1735646001\n            );\n        }\n\n        $responseData = json_decode($response-\u003egetBody()-\u003egetContents(), true);\n\n        if (isset($responseData['choices'][0]['message']['content']) === false) {\n            throw new ApiException('Invalid ChatGPT API response structure', 1735646002);\n        }\n\n        return $responseData['choices'][0]['message']['content'];\n    }\n\n    protected function convertHistoryToChatGptFormat(array $geminiHistory): array\n    {\n        $messages = [];\n        foreach ($geminiHistory as $entry) {\n            $role = $entry['role'] === 'model' ? 'assistant' : $entry['role'];\n            $content = $entry['parts'][0]['text'] ?? '';\n            $messages[] = [\n                'role' =\u003e $role,\n                'content' =\u003e $content,\n            ];\n        }\n        return $messages;\n    }\n}\n```\n\n2. Register your custom repository in `ext_localconf.php`:\n\n```php\n\u003c?php\ndefined('TYPO3') || die();\n\n// Register custom LLM repository\n$GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['texter']['llmRepositoryClass']\n    = \\Vendor\\MyExtension\\Domain\\Repository\\Llm\\ChatGptRepository::class;\n```\n\n3. Set your API key as environment variable:\n\n```\nOPENAI_API_KEY=your_openai_api_key_here\n```\n\n**Hint**: Don't forget to register your Repository in your Services.yaml and flush caches after making these changes.\n\n## Changelog and breaking changes\n\n| Version | Date       | State   | Description                                                 |\n|---------|------------|---------|-------------------------------------------------------------|\n| 2.0.0   | 2025-01-01 | Feature | Add the possibility to extend texter with other AI services |\n| 1.0.0   | 2025-12-06 | Task    | Initial release of in2code/texter                           |\n\n\n\n## Contribution with ddev\n\nThis repository provides a [DDEV](https://ddev.readthedocs.io)-backed development environment. If DDEV is installed, simply run the following\ncommands to quickly set up a local environment with example usages:\n\n* `ddev start`\n* `ddev initialize`\n\n**Backend Login:**\n```\nUsername: admin\nPassword: admin\n```\n\n**Installation hint:**\n\n1. Install ddev before, see: https://ddev.readthedocs.io/en/stable/#installation\n2. Install git-lfs before, see: https://git-lfs.github.com/\n3. You can place .ddev/.env with the google API key\n\n```\nGOOGLE_API_KEY=your_api_key_from_google\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fin2code-de%2Ftexter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fin2code-de%2Ftexter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fin2code-de%2Ftexter/lists"}