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.
- appkit_imagecreator-0.7.1/.gitignore +114 -0
- appkit_imagecreator-0.7.1/PKG-INFO +10 -0
- appkit_imagecreator-0.7.1/README.md +0 -0
- appkit_imagecreator-0.7.1/pyproject.toml +26 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/__init__.py +0 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/backend/generator_registry.py +108 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/backend/generators/__init__.py +11 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/backend/generators/black_forest_labs.py +117 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/backend/generators/google.py +84 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/backend/generators/openai.py +119 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/backend/models.py +155 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/components/canvas.py +131 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/components/options_ui.py +520 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/components/react_zoom.py +23 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/components/sidebar.py +50 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/components/styles_preset.py +34 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/configuration.py +50 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/pages.py +54 -0
- appkit_imagecreator-0.7.1/src/appkit_imagecreator/states.py +249 -0
|
@@ -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"
|
|
File without changes
|
|
@@ -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)
|