{"id":37004896,"url":"https://github.com/in2code-de/alternative","last_synced_at":"2026-01-14T00:39:11.338Z","repository":{"id":327845188,"uuid":"1109470402","full_name":"in2code-de/alternative","owner":"in2code-de","description":"Automatically set alternative texts for images","archived":false,"fork":false,"pushed_at":"2026-01-03T16:55:31.000Z","size":369,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-05T01:25:04.139Z","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-03T21:11:44.000Z","updated_at":"2026-01-03T16:55:35.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/in2code-de/alternative","commit_stats":null,"previous_names":["in2code-de/alternative"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/in2code-de/alternative","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Falternative","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Falternative/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Falternative/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Falternative/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/in2code-de","download_url":"https://codeload.github.com/in2code-de/alternative/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/in2code-de%2Falternative/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28406520,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T21:51:37.118Z","status":"ssl_error","status_checked_at":"2026-01-13T21:45:14.585Z","response_time":56,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: 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-14T00:39:11.200Z","updated_at":"2026-01-14T00:39:11.316Z","avatar_url":"https://github.com/in2code-de.png","language":"PHP","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Alternative - AI generated metatags for images in TYPO3 with Google Gemini\n\n## Introduction\n\nThis TYPO3 extension allows setting alternative texts, title labels and a description for images in a filestorage.\nThis can be done via file list backend module or via command on the CLI.\n\nExample metadata labels from AI:\n![documentation_example1.png](Documentation/Images/documentation_example1.png)\n\nExample backend integration:\n![documentation_example2.png](Documentation/Images/documentation_example2.png)\n\nExample CLI command:\n![documentation_example3.png](Documentation/Images/documentation_example3.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```\ncomposer req in2code/alternative\n```\n\nAfter that, you have to set some initial configuration in Extension Manager configuration:\n\n| Title                | Default value | Description                                                                                                                                          |\n|----------------------|---------------|------------------------------------------------------------------------------------------------------------------------------------------------------|\n| setAlternative       | 1             | Toggle function: Set a value for alternative text                                                                                                    |\n| setTitle             | 1             | Toggle function: Set a value for image title                                                                                                         |\n| setDescription       | 1             | Toggle function: Set a value for a description                                                                                                       |\n| showButtonInFileList | 1             | Show or hide button in backend module file list                                                                                                      |\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| limitToLanguages     | -             | If set, limit to this language identifiers only. Use a commaseparated list of numbers                                                                |\n\nNote: It's recommended to use ENV vars for in2code/alternative instead of saving the API-Key in Extension Manager configuration\n\n```\nGOOGLE_API_KEY=your_api_key_from_google\n```\n\n## CLI commands\n\n```\n# Set metadata for all image files in storage 1\n./vendor/bin/typo3 alternative:set \"1:/\"\n\n# Set metadata for all image files in a subfoler in storage 1 (maybe \"fileadmin/in2code/folder/\")\n./vendor/bin/typo3 alternative:set \"1:/in2code/folder/\"\n\n# Enforce to set metadata for all image files in storage 1\n./vendor/bin/typo3 alternative:set \"1:/\" 1\n```\n\n## Custom LLM Integration (like ChatGPT, Claude, Mistral, etc.)\n\nAlternative 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\\Alternative\\Domain\\Repository\\Llm\\AbstractRepository;\nuse In2code\\Alternative\\Domain\\Repository\\Llm\\RepositoryInterface;\nuse In2code\\Alternative\\Exception\\ApiException;\nuse In2code\\Alternative\\Exception\\ConfigurationException;\nuse TYPO3\\CMS\\Core\\Http\\RequestFactory;\nuse TYPO3\\CMS\\Core\\Resource\\File;\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    ) {\n        parent::__construct($requestFactory);\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 analyzeImage(File $file, string $languageCode): array\n    {\n        $this-\u003echeckApiKey();\n        $this-\u003esetLanguageCode($languageCode);\n        $imageData = base64_encode($file-\u003egetContents());\n        return $this-\u003egenerateMetadataWithChatGpt($imageData, $file-\u003egetMimeType());\n    }\n\n    protected function generateMetadataWithChatGpt(string $imageData, string $mimeType): array\n    {\n        $payload = [\n            'model' =\u003e 'gpt-4o',\n            'messages' =\u003e [\n                [\n                    'role' =\u003e 'user',\n                    'content' =\u003e [\n                        ['type' =\u003e 'text', 'text' =\u003e $this-\u003egetPrompt()],\n                        [\n                            'type' =\u003e 'image_url',\n                            'image_url' =\u003e [\n                                'url' =\u003e 'data:' . $mimeType . ';base64,' . $imageData,\n                            ],\n                        ],\n                    ],\n                ],\n            ],\n            'temperature' =\u003e 0.1,\n            'max_tokens' =\u003e 500,\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 analyze image with ChatGPT: ' . $response-\u003egetBody()-\u003egetContents(),\n                1735646001\n            );\n        }\n\n        $responseData = json_decode($response-\u003egetBody()-\u003egetContents(), true);\n        return $this-\u003eparseResponse($responseData);\n    }\n\n    protected function parseResponse(array $responseData): array\n    {\n        if (isset($responseData['choices'][0]['message']['content']) === false) {\n            throw new ApiException('Invalid ChatGPT API response structure', 1735646002);\n        }\n\n        $text = $responseData['choices'][0]['message']['content'];\n\n        // Extract JSON from markdown code blocks if present\n        if (preg_match('/```json\\s*(\\{.*?\\})\\s*```/s', $text, $matches)) {\n            $text = $matches[1];\n        } elseif (preg_match('/```\\s*(\\{.*?\\})\\s*```/s', $text, $matches)) {\n            $text = $matches[1];\n        }\n\n        $data = json_decode($text, true);\n        if ($data === null) {\n            throw new ApiException('Could not parse ChatGPT response as JSON', 1735646003);\n        }\n\n        return [\n            'title' =\u003e $data['title'] ?? '',\n            'description' =\u003e $data['description'] ?? '',\n            'alternativeText' =\u003e $data['alternativeText'] ?? '',\n        ];\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']['alternative']['llmRepositoryClass']\n    = \\Vendor\\MyExtension\\Domain\\Repository\\Llm\\ChatGptRepository::class;\n```\n\n**Hint**: Don't forget to register your Repository in your Services.yaml and flush caches\n\n## Changelog and breaking changes\n\n| Version | Date       | State   | Description                                                                                      |\n|---------|------------|---------|--------------------------------------------------------------------------------------------------|\n| 2.0.0   | 2025-12-31 | Feature | Prepare overruling of the GeminiRepository                                                       |\n| 1.2.0   | 2025-12-07 | Feature | Support TYPO3 14, use better titles and icons, decrease length for metadata, skip missing images |\n| 1.1.0   | 2025-12-04 | Feature | Add ddev as local environment, prevent syntax error in PHP 8.2                                   |\n| 1.0.0   | 2025-12-03 | Task    | Initial release of in2code/alternative                                                           |\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. Add the API key in `.ddev/.env` like\n\n```\nGOOGLE_API_KEY=your_api_key_from_google\n```","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fin2code-de%2Falternative","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fin2code-de%2Falternative","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fin2code-de%2Falternative/lists"}