appkit-imagecreator 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,114 @@
1
+ __pycache__/
2
+ __pypackages__/
3
+ .cache
4
+ .coverage
5
+ .coverage.*
6
+ .dmypy.json
7
+ .DS_Store
8
+ .eggs/
9
+ .env
10
+ .env.backup
11
+ .env.docker
12
+ .hypothesis/
13
+ .idea/
14
+ .installed.cfg
15
+ .ipynb_checkpoints
16
+ .mypy_cache/
17
+ .nox/
18
+ .pdm.toml
19
+ .pybuilder/
20
+ .pyre/
21
+ .pytest_cache/
22
+ .Python
23
+ .python_packages
24
+ .pytype/
25
+ .ropeproject
26
+ .scrapy
27
+ .spyderproject
28
+ .spyproject
29
+ .states
30
+ .tox/
31
+ .venv
32
+ .venv.mac
33
+ .web
34
+ .webassets-cache
35
+ *.bak
36
+ *.cover
37
+ *.db
38
+ *.egg
39
+ *.egg-info/
40
+ *.kv-env.*
41
+ *.log
42
+ *.manifest
43
+ *.mo
44
+ *.pot
45
+ *.py,cover
46
+ *.py[cod]
47
+ *.sage.py
48
+ *.so
49
+ *.spec
50
+ *.terraform.lock.hcl
51
+ *.tfplan
52
+ *.tfstate
53
+ *.tfstate.*.backup
54
+ *.tfstate.backup
55
+ *.tfvars
56
+ **/.terraform/*
57
+ *$py.class
58
+ /site
59
+ /vectorstore/
60
+ aila-storage/
61
+ assets/external/
62
+ build/
63
+ celerybeat-schedule
64
+ celerybeat.pid
65
+ configuration/config.abaz009.yaml
66
+ configuration/config.bubb001.yaml
67
+ configuration/config.stie104.yaml
68
+ configuration/config.voro047.yaml
69
+ connector examples/sharepoint.json
70
+ cover/
71
+ coverage.xml
72
+ cython_debug/
73
+ db.sqlite3
74
+ db.sqlite3-journal
75
+ develop-eggs/
76
+ dist/
77
+ dmypy.json
78
+ docs/_build/
79
+ Documents/
80
+ downloads/
81
+ eggs/
82
+ env.bak/
83
+ env/
84
+ ENV/
85
+ htmlcov/
86
+ instance/
87
+ ipython_config.py
88
+ knowledge/migrate.py
89
+ lib/
90
+ lib64/
91
+ local_settings.py
92
+ local.settings.json
93
+ MANIFEST
94
+ nosetests.xml
95
+ out
96
+ parts/
97
+ pip-delete-this-directory.txt
98
+ pip-log.txt
99
+ Pipfile
100
+ profile_default/
101
+ sdist/
102
+ share/python-wheels/
103
+ sketchpad/
104
+ sketchpad/
105
+ stores/
106
+ target/
107
+ tests/mcp_test.py
108
+ tmp.txt
109
+ uploaded_files/
110
+ uploads/
111
+ var/
112
+ venv.bak/
113
+ venv/
114
+ wheels/
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: appkit-imagecreator
3
+ Version: 0.7.1
4
+ Summary: Add your description here
5
+ Author: Jens Rehpöhler
6
+ Requires-Python: >=3.13
7
+ Requires-Dist: appkit-commons
8
+ Requires-Dist: google-genai>=1.26.0
9
+ Requires-Dist: httpx>=0.28.1
10
+ Requires-Dist: openai>=2.3.0
File without changes
@@ -0,0 +1,26 @@
1
+ [project]
2
+ name = "appkit-imagecreator"
3
+ version = "0.7.1"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [{ name = "Jens Rehpöhler" }]
7
+ requires-python = ">=3.13"
8
+ dependencies = [
9
+ "google-genai>=1.26.0",
10
+ "httpx>=0.28.1",
11
+ "appkit-commons",
12
+ "openai>=2.3.0",
13
+ ]
14
+
15
+ [tool.setuptools.packages.find]
16
+ where = ["src"]
17
+
18
+ [tool.uv.sources]
19
+ appkit-commons = { workspace = true }
20
+
21
+ [tool.hatch.build.targets.wheel]
22
+ packages = ["src/appkit_imagecreator"]
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
@@ -0,0 +1,108 @@
1
+ import logging
2
+ from typing import Final
3
+
4
+ from appkit_commons.configuration.configuration import ReflexConfig
5
+ from appkit_commons.registry import service_registry
6
+ from appkit_imagecreator.backend.generators import (
7
+ GoogleImageGenerator,
8
+ )
9
+ from appkit_imagecreator.backend.generators.openai import OpenAIImageGenerator
10
+ from appkit_imagecreator.backend.models import ImageGenerator
11
+ from appkit_imagecreator.configuration import ImageGeneratorConfig
12
+ from rxconfig import config
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class ImageGeneratorRegistry:
18
+ """Registry of image generators.
19
+
20
+ Maintains a collection of configured image generators that can be retrieved by ID.
21
+ """
22
+
23
+ def __init__(self):
24
+ self.config = service_registry().get(ImageGeneratorConfig)
25
+ self.reflex_config = service_registry().get(ReflexConfig)
26
+ self._generators: dict[str, ImageGenerator] = {}
27
+ self._initialize_default_generators()
28
+
29
+ logger.debug("reflex config: %s", self.reflex_config)
30
+ logger.debug("image generator config: %s", self.config)
31
+
32
+ def _initialize_default_generators(self) -> None:
33
+ """Initialize the registry with default generators."""
34
+
35
+ if self.reflex_config.single_port:
36
+ backend_server = f"{self.reflex_config.deploy_url}"
37
+ else:
38
+ backend_server = f"{self.reflex_config.deploy_url}:{config.backend_port}"
39
+
40
+ self.register(
41
+ OpenAIImageGenerator(
42
+ api_key=self.config.openai_api_key.get_secret_value(),
43
+ base_url=self.config.openai_base_url,
44
+ backend_server=backend_server,
45
+ )
46
+ )
47
+ self.register(
48
+ OpenAIImageGenerator(
49
+ api_key=self.config.openai_api_key.get_secret_value(),
50
+ base_url=self.config.openai_base_url,
51
+ backend_server=backend_server,
52
+ model="FLUX-1.1-pro",
53
+ label="Blackforest Labs FLUX 1.1-pro",
54
+ id="FLUX-1.1-pro",
55
+ )
56
+ )
57
+ self.register(
58
+ GoogleImageGenerator(
59
+ api_key=self.config.google_api_key.get_secret_value(),
60
+ backend_server=backend_server,
61
+ )
62
+ )
63
+ self.register(
64
+ GoogleImageGenerator(
65
+ api_key=self.config.google_api_key.get_secret_value(),
66
+ backend_server=backend_server,
67
+ model="imagen-3.0-generate-002",
68
+ label="Google Imagen 3",
69
+ id="imagen-3",
70
+ )
71
+ )
72
+
73
+ def register(self, generator: ImageGenerator) -> None:
74
+ """Register a new generator in the registry."""
75
+ self._generators[generator.id] = generator
76
+
77
+ def get(
78
+ self,
79
+ generator_id: str,
80
+ ) -> ImageGenerator:
81
+ """Get a generator by ID.
82
+
83
+ If api_key or backend_server are provided, they will override the
84
+ default values.
85
+ """
86
+ if generator_id not in self._generators:
87
+ raise ValueError(f"Unknown generator ID: {generator_id}")
88
+
89
+ return self._generators[generator_id]
90
+
91
+ def list_generators(self) -> list[dict[str, str]]:
92
+ """List all available generators with their IDs and labels."""
93
+ return [{"id": gen.id, "label": gen.label} for gen in self._generators.values()]
94
+
95
+ def get_generator_ids(self) -> list[str]:
96
+ """Get the IDs of all registered generators."""
97
+ return list(self._generators.keys())
98
+
99
+ def get_default_generator(self) -> ImageGenerator:
100
+ """Get the default generator."""
101
+ if not self._generators:
102
+ raise ValueError("No generators registered.")
103
+
104
+ return next(iter(self._generators.values()))
105
+
106
+
107
+ # Create a global instance of the registry
108
+ generator_registry: Final = ImageGeneratorRegistry()
@@ -0,0 +1,11 @@
1
+ from appkit_imagecreator.backend.generators.black_forest_labs import (
2
+ BlackForestLabsImageGenerator,
3
+ )
4
+ from appkit_imagecreator.backend.generators.openai import OpenAIImageGenerator
5
+ from appkit_imagecreator.backend.generators.google import GoogleImageGenerator
6
+
7
+ __ALL__ = [
8
+ "BlackForestLabsImageGenerator",
9
+ "OpenAIImageGenerator",
10
+ "GoogleImageGenerator",
11
+ ]
@@ -0,0 +1,117 @@
1
+ import asyncio
2
+ import logging
3
+
4
+ import httpx
5
+
6
+ from appkit_imagecreator.backend.models import (
7
+ GenerationInput,
8
+ ImageGenerator,
9
+ ImageGeneratorResponse,
10
+ ImageResponseState,
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class BlackForestLabsImageGenerator(ImageGenerator):
17
+ """Generator for the Together AI API (Flux Schnell model)."""
18
+
19
+ def __init__(
20
+ self,
21
+ api_key: str,
22
+ label: str = "Flux.1 Kontext [Pro]",
23
+ id: str = "flux-kontext-pro", # noqa: A002
24
+ model: str = "flux-kontext-pro",
25
+ backend_server: str | None = None,
26
+ ) -> None:
27
+ super().__init__(
28
+ id=id,
29
+ label=label,
30
+ model=model,
31
+ api_key=api_key,
32
+ backend_server=backend_server,
33
+ )
34
+
35
+ async def _perform_generation(
36
+ self, input_data: GenerationInput
37
+ ) -> ImageGeneratorResponse:
38
+ prompt = self._format_prompt(input_data.prompt, input_data.negative_prompt)
39
+
40
+ api_url = f"https://api.bfl.ai/v1/{self.model}"
41
+ headers = {
42
+ "accept": "application/json",
43
+ "x-key": self.api_key,
44
+ "Content-Type": "application/json",
45
+ }
46
+ payload = {
47
+ "prompt": prompt,
48
+ "aspect_ratio": self._aspect_ratio(input_data.width, input_data.height),
49
+ "seed": input_data.seed,
50
+ "prompt_upsampling": input_data.enhance_prompt,
51
+ "safety_tolerance": 6,
52
+ }
53
+
54
+ error_msg = None
55
+ image_url = None
56
+
57
+ try:
58
+ async with httpx.AsyncClient() as client:
59
+ response = await client.post(api_url, headers=headers, json=payload)
60
+ response.raise_for_status() # Raise an exception for bad status codes
61
+ request_data = response.json()
62
+
63
+ polling_url = request_data.get("polling_url")
64
+ polling_headers = {
65
+ "accept": "application/json",
66
+ "x-key": self.api_key,
67
+ }
68
+
69
+ while True:
70
+ await asyncio.sleep(1.5) # Use asyncio.sleep for async context
71
+ poll_response = await client.get(
72
+ polling_url, headers=polling_headers
73
+ )
74
+ poll_response.raise_for_status()
75
+ result = poll_response.json()
76
+ status = result.get("status")
77
+
78
+ if status == "Ready":
79
+ image_url = result.get("result", {}).get("sample")
80
+ if not image_url:
81
+ error_msg = (
82
+ "Bild-URL wurde im 'Ready'-Status nicht gefunden."
83
+ )
84
+ break
85
+ if status not in ["Pending", "Processing", "Queued"]:
86
+ error_msg = f"Ein Fehler oder ein unerwarteter Status ist aufgetreten: {result}" # noqa: E501
87
+ break
88
+ except httpx.HTTPStatusError as e:
89
+ error_msg = (
90
+ f"HTTP-Fehler aufgetreten: {e.response.status_code} - {e.response.text}"
91
+ )
92
+ logger.error(error_msg)
93
+ except httpx.RequestError as e:
94
+ error_msg = f"Anfragefehler aufgetreten: {e!s}"
95
+ logger.error(error_msg)
96
+ except Exception as e:
97
+ error_msg = f"Ein unerwarteter Fehler ist aufgetreten: {e!s}"
98
+ logger.exception("Unerwarteter Fehler während der Bildgenerierung")
99
+
100
+ if error_msg or not image_url:
101
+ final_error_message = (
102
+ error_msg or "Zu dem generierten Bild wurde keine URL erstellt."
103
+ )
104
+ logger.error(
105
+ "Image generation failed: %s",
106
+ final_error_message,
107
+ )
108
+ return ImageGeneratorResponse(
109
+ state=ImageResponseState.FAILED,
110
+ images=[],
111
+ error=final_error_message,
112
+ )
113
+
114
+ return ImageGeneratorResponse(
115
+ state=ImageResponseState.SUCCEEDED,
116
+ images=[image_url],
117
+ )
@@ -0,0 +1,84 @@
1
+ import logging
2
+ from typing import Final
3
+
4
+ from google import genai
5
+
6
+ from appkit_imagecreator.backend.models import (
7
+ GenerationInput,
8
+ ImageGenerator,
9
+ ImageGeneratorResponse,
10
+ ImageResponseState,
11
+ )
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ TMP_IMG_FILE: Final[str] = "imagen-image"
16
+
17
+
18
+ class GoogleImageGenerator(ImageGenerator):
19
+ """Generator for the Google Imagen API."""
20
+
21
+ def __init__(
22
+ self,
23
+ api_key: str,
24
+ label: str = "Google Imagen 4",
25
+ id: str = "imagen-4", # noqa: A002
26
+ model: str = "imagen-4.0-generate-preview-06-06",
27
+ backend_server: str | None = None,
28
+ ) -> None:
29
+ super().__init__(
30
+ id=id,
31
+ label=label,
32
+ model=model,
33
+ api_key=api_key,
34
+ backend_server=backend_server,
35
+ )
36
+ self.client = genai.Client(api_key=self.api_key)
37
+
38
+ def _enhance_prompt(self, prompt: str) -> str:
39
+ response = self.client.models.generate_content(
40
+ model="gemini-2.0-flash-001",
41
+ contents=(
42
+ "You are an image generation assistant specialized in "
43
+ "optimizing user prompts. Ensure content "
44
+ "compliance rules are followed. Do not ask followup "
45
+ "questions, just generate the plain, raw, optimized prompt "
46
+ "withoud any additional text, headlines or questions."
47
+ f"Enhance this prompt for image generation: {prompt}"
48
+ ),
49
+ )
50
+
51
+ prompt = response.text.strip()
52
+ logger.debug("Enhanced prompt for image generation: %s", prompt)
53
+ return prompt
54
+
55
+ async def _perform_generation(
56
+ self, input_data: GenerationInput
57
+ ) -> ImageGeneratorResponse:
58
+ prompt = self._format_prompt(input_data.prompt, input_data.negative_prompt)
59
+
60
+ if input_data.enhance_prompt:
61
+ prompt = self._enhance_prompt(prompt)
62
+
63
+ response = self.client.models.generate_images(
64
+ model=self.model,
65
+ prompt=prompt,
66
+ config=genai.types.GenerateImagesConfig(
67
+ number_of_images=input_data.n,
68
+ aspect_ratio=self._aspect_ratio(input_data.width, input_data.height),
69
+ ),
70
+ )
71
+
72
+ self.clean_tmp_path(TMP_IMG_FILE)
73
+ output_format = "jpeg"
74
+ images = []
75
+
76
+ for img in response.generated_images:
77
+ image_url = await self._save_image_to_tmp_and_get_url(
78
+ image_bytes=img.image.image_bytes,
79
+ tmp_file_prefix=TMP_IMG_FILE,
80
+ output_format=output_format,
81
+ )
82
+ images.append(image_url)
83
+
84
+ return ImageGeneratorResponse(state=ImageResponseState.SUCCEEDED, images=images)
@@ -0,0 +1,119 @@
1
+ import base64
2
+ import logging
3
+ from typing import Final
4
+
5
+ from openai import AsyncAzureOpenAI
6
+
7
+ from appkit_imagecreator.backend.models import (
8
+ GenerationInput,
9
+ ImageGenerator,
10
+ ImageGeneratorResponse,
11
+ ImageResponseState,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ TMP_IMG_FILE: Final[str] = "gpt-image"
17
+
18
+
19
+ class OpenAIImageGenerator(ImageGenerator):
20
+ """Generator for the OpenAI DALL-E API."""
21
+
22
+ def __init__(
23
+ self,
24
+ api_key: str,
25
+ id: str = "gpt-image-1", # noqa: A002
26
+ label: str = "OpenAI GPT-Image-1",
27
+ model: str = "gpt-image-1",
28
+ backend_server: str | None = None,
29
+ base_url: str | None = None,
30
+ ) -> None:
31
+ super().__init__(
32
+ id=id,
33
+ label=label,
34
+ model=model,
35
+ api_key=api_key,
36
+ backend_server=backend_server,
37
+ )
38
+ # self.client = AsyncOpenAI(api_key=self.api_key)
39
+
40
+ self.client = AsyncAzureOpenAI(
41
+ api_version="2025-04-01-preview",
42
+ azure_endpoint=base_url,
43
+ api_key=api_key,
44
+ )
45
+
46
+ async def _enhance_prompt(self, prompt: str) -> str:
47
+ response = await self.client.chat.completions.create(
48
+ model="gpt-4.1-mini",
49
+ stream=False,
50
+ messages=[
51
+ {
52
+ "role": "system",
53
+ "content": (
54
+ "You are an image generation assistant specialized in "
55
+ "optimizing user prompts. Ensure content "
56
+ "compliance rules are followed. Do not ask followup "
57
+ "questions, just generate the optimized prompt."
58
+ ),
59
+ },
60
+ {
61
+ "role": "user",
62
+ "content": f"Enhance this prompt for image generation: {prompt}",
63
+ },
64
+ ],
65
+ )
66
+
67
+ result = response.choices[0].message.content.strip()
68
+ if not result:
69
+ result = prompt
70
+
71
+ logger.debug("Enhanced prompt for image generation: %s", result)
72
+ return result
73
+
74
+ async def _perform_generation(
75
+ self, input_data: GenerationInput
76
+ ) -> ImageGeneratorResponse:
77
+ output_format = "jpeg"
78
+ prompt = self._format_prompt(input_data.prompt, input_data.negative_prompt)
79
+
80
+ if input_data.enhance_prompt:
81
+ prompt = await self._enhance_prompt(prompt)
82
+
83
+ response = await self.client.images.generate(
84
+ model=self.model,
85
+ prompt=prompt,
86
+ n=input_data.n,
87
+ moderation="low",
88
+ output_format=output_format,
89
+ output_compression=95,
90
+ )
91
+
92
+ self.clean_tmp_path(TMP_IMG_FILE)
93
+
94
+ images = []
95
+ for img in response.data:
96
+ if img.url:
97
+ images.append(img.url)
98
+ elif img.b64_json:
99
+ image_bytes = base64.b64decode(img.b64_json)
100
+ image_url = await self._save_image_to_tmp_and_get_url(
101
+ image_bytes=image_bytes,
102
+ tmp_file_prefix=TMP_IMG_FILE,
103
+ output_format=output_format,
104
+ )
105
+ images.append(image_url)
106
+ else:
107
+ logger.warning("Image data from OpenAI is neither b64_json nor a URL.")
108
+
109
+ if not images:
110
+ logger.error(
111
+ "No images were successfully processed or retrieved from OpenAI."
112
+ )
113
+ return ImageGeneratorResponse(
114
+ state=ImageResponseState.FAILED,
115
+ images=[],
116
+ error="Es wurden keine Bilder generiert oder von der API abgerufen.",
117
+ )
118
+
119
+ return ImageGeneratorResponse(state=ImageResponseState.SUCCEEDED, images=images)