py-aidol 0.1.0__py3-none-any.whl
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.
- aidol/__init__.py +7 -0
- aidol/api/__init__.py +13 -0
- aidol/api/aidol.py +189 -0
- aidol/api/companion.py +168 -0
- aidol/factories.py +24 -0
- aidol/models/__init__.py +8 -0
- aidol/models/aidol.py +24 -0
- aidol/models/companion.py +27 -0
- aidol/protocols.py +100 -0
- aidol/py.typed +0 -0
- aidol/repositories/__init__.py +11 -0
- aidol/repositories/aidol.py +57 -0
- aidol/repositories/companion.py +57 -0
- aidol/schemas/__init__.py +37 -0
- aidol/schemas/aidol.py +119 -0
- aidol/schemas/companion.py +93 -0
- aidol/services/__init__.py +9 -0
- aidol/services/image_generation_service.py +145 -0
- py_aidol-0.1.0.dist-info/METADATA +114 -0
- py_aidol-0.1.0.dist-info/RECORD +21 -0
- py_aidol-0.1.0.dist-info/WHEEL +4 -0
aidol/__init__.py
ADDED
aidol/api/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol API routers
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from aidol.api.aidol import AIdolRouter, create_aidol_router
|
|
6
|
+
from aidol.api.companion import CompanionRouter, create_companion_router
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"AIdolRouter",
|
|
10
|
+
"CompanionRouter",
|
|
11
|
+
"create_aidol_router",
|
|
12
|
+
"create_companion_router",
|
|
13
|
+
]
|
aidol/api/aidol.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol API router
|
|
3
|
+
|
|
4
|
+
Public endpoints for AIdol group creation and retrieval.
|
|
5
|
+
Public access pattern: no authentication required.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from aioia_core.auth import UserInfoProvider
|
|
9
|
+
from aioia_core.errors import ErrorResponse
|
|
10
|
+
from aioia_core.fastapi import BaseCrudRouter
|
|
11
|
+
from aioia_core.settings import JWTSettings, OpenAIAPISettings
|
|
12
|
+
from fastapi import APIRouter, Depends, HTTPException, status
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from sqlalchemy.orm import sessionmaker
|
|
15
|
+
|
|
16
|
+
from aidol.protocols import (
|
|
17
|
+
AIdolRepositoryFactoryProtocol,
|
|
18
|
+
AIdolRepositoryProtocol,
|
|
19
|
+
ImageStorageProtocol,
|
|
20
|
+
)
|
|
21
|
+
from aidol.schemas import (
|
|
22
|
+
AIdol,
|
|
23
|
+
AIdolCreate,
|
|
24
|
+
AIdolPublic,
|
|
25
|
+
AIdolUpdate,
|
|
26
|
+
ImageGenerationData,
|
|
27
|
+
ImageGenerationRequest,
|
|
28
|
+
ImageGenerationResponse,
|
|
29
|
+
)
|
|
30
|
+
from aidol.services import ImageGenerationService
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class AIdolSingleItemResponse(BaseModel):
|
|
34
|
+
"""Single item response for AIdol (public)."""
|
|
35
|
+
|
|
36
|
+
data: AIdolPublic
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AIdolRouter(
|
|
40
|
+
BaseCrudRouter[AIdol, AIdolCreate, AIdolUpdate, AIdolRepositoryProtocol]
|
|
41
|
+
):
|
|
42
|
+
"""
|
|
43
|
+
AIdol router with public endpoints.
|
|
44
|
+
|
|
45
|
+
Public CRUD pattern: no authentication required.
|
|
46
|
+
Returns AIdolPublic (excludes claim_token) for all responses.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
openai_settings: OpenAIAPISettings,
|
|
52
|
+
image_storage: ImageStorageProtocol,
|
|
53
|
+
**kwargs,
|
|
54
|
+
):
|
|
55
|
+
super().__init__(**kwargs)
|
|
56
|
+
self.openai_settings = openai_settings
|
|
57
|
+
self.image_storage = image_storage
|
|
58
|
+
|
|
59
|
+
def _register_routes(self) -> None:
|
|
60
|
+
"""Register routes (public CRUD + image generation)"""
|
|
61
|
+
self._register_image_generation_route()
|
|
62
|
+
self._register_public_create_route()
|
|
63
|
+
self._register_public_get_route()
|
|
64
|
+
|
|
65
|
+
def _register_public_create_route(self) -> None:
|
|
66
|
+
"""POST /{resource_name} - Create an AIdol group (public)"""
|
|
67
|
+
|
|
68
|
+
@self.router.post(
|
|
69
|
+
f"/{self.resource_name}",
|
|
70
|
+
response_model=AIdolSingleItemResponse,
|
|
71
|
+
status_code=status.HTTP_201_CREATED,
|
|
72
|
+
summary="Create AIdol group",
|
|
73
|
+
description="Create a new AIdol group (public endpoint)",
|
|
74
|
+
)
|
|
75
|
+
async def create_aidol(
|
|
76
|
+
request: AIdolCreate,
|
|
77
|
+
repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
|
|
78
|
+
):
|
|
79
|
+
"""Create a new AIdol group."""
|
|
80
|
+
created = repository.create(request)
|
|
81
|
+
# Convert to Public schema (exclude claim_token)
|
|
82
|
+
public_aidol = AIdolPublic(**created.model_dump())
|
|
83
|
+
return AIdolSingleItemResponse(data=public_aidol)
|
|
84
|
+
|
|
85
|
+
def _register_public_get_route(self) -> None:
|
|
86
|
+
"""GET /{resource_name}/{id} - Get an AIdol group (public)"""
|
|
87
|
+
|
|
88
|
+
@self.router.get(
|
|
89
|
+
f"/{self.resource_name}/{{item_id}}",
|
|
90
|
+
response_model=AIdolSingleItemResponse,
|
|
91
|
+
status_code=status.HTTP_200_OK,
|
|
92
|
+
summary="Get AIdol group",
|
|
93
|
+
description="Get AIdol group by ID (public endpoint)",
|
|
94
|
+
responses={
|
|
95
|
+
404: {"model": ErrorResponse, "description": "AIdol group not found"},
|
|
96
|
+
},
|
|
97
|
+
)
|
|
98
|
+
async def get_aidol(
|
|
99
|
+
item_id: str,
|
|
100
|
+
repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
|
|
101
|
+
):
|
|
102
|
+
"""Get AIdol group by ID."""
|
|
103
|
+
aidol = self._get_item_or_404(repository, item_id)
|
|
104
|
+
# Convert to Public schema (exclude claim_token)
|
|
105
|
+
public_aidol = AIdolPublic(**aidol.model_dump())
|
|
106
|
+
return AIdolSingleItemResponse(data=public_aidol)
|
|
107
|
+
|
|
108
|
+
def _register_image_generation_route(self) -> None:
|
|
109
|
+
"""POST /{resource_name}/images - Generate image for AIdol or Companion"""
|
|
110
|
+
|
|
111
|
+
@self.router.post(
|
|
112
|
+
f"/{self.resource_name}/images",
|
|
113
|
+
response_model=ImageGenerationResponse,
|
|
114
|
+
status_code=status.HTTP_201_CREATED,
|
|
115
|
+
summary="Generate image",
|
|
116
|
+
description="Generate image for AIdol emblem or Companion profile",
|
|
117
|
+
responses={
|
|
118
|
+
500: {"model": ErrorResponse, "description": "Image generation failed"},
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
async def generate_image(request: ImageGenerationRequest):
|
|
122
|
+
"""Generate image from prompt."""
|
|
123
|
+
# Generate and download image (TTS pattern: service returns data)
|
|
124
|
+
service = ImageGenerationService(self.openai_settings)
|
|
125
|
+
image = service.generate_and_download_image(
|
|
126
|
+
prompt=request.prompt,
|
|
127
|
+
size="1024x1024",
|
|
128
|
+
quality="standard",
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if image is None:
|
|
132
|
+
raise HTTPException(
|
|
133
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
134
|
+
detail="Image generation failed",
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Upload to permanent storage (TTS pattern: API layer orchestrates)
|
|
138
|
+
image_url = self.image_storage.upload_image(image)
|
|
139
|
+
|
|
140
|
+
return ImageGenerationResponse(
|
|
141
|
+
data=ImageGenerationData(
|
|
142
|
+
image_url=image_url,
|
|
143
|
+
width=1024,
|
|
144
|
+
height=1024,
|
|
145
|
+
format="png",
|
|
146
|
+
)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def create_aidol_router(
|
|
151
|
+
openai_settings: OpenAIAPISettings,
|
|
152
|
+
db_session_factory: sessionmaker,
|
|
153
|
+
repository_factory: AIdolRepositoryFactoryProtocol,
|
|
154
|
+
image_storage: ImageStorageProtocol,
|
|
155
|
+
jwt_settings: JWTSettings | None = None,
|
|
156
|
+
user_info_provider: UserInfoProvider | None = None,
|
|
157
|
+
resource_name: str = "aidols",
|
|
158
|
+
tags: list[str] | None = None,
|
|
159
|
+
) -> APIRouter:
|
|
160
|
+
"""
|
|
161
|
+
Create AIdol router with dependency injection.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
openai_settings: OpenAI API settings for image generation
|
|
165
|
+
db_session_factory: Database session factory
|
|
166
|
+
repository_factory: Factory implementing AIdolRepositoryFactoryProtocol
|
|
167
|
+
image_storage: Image storage for permanent URLs
|
|
168
|
+
jwt_settings: Optional JWT settings for authentication
|
|
169
|
+
user_info_provider: Optional user info provider
|
|
170
|
+
resource_name: Resource name for routes (default: "aidols")
|
|
171
|
+
tags: Optional OpenAPI tags
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
FastAPI APIRouter instance
|
|
175
|
+
"""
|
|
176
|
+
router = AIdolRouter(
|
|
177
|
+
openai_settings=openai_settings,
|
|
178
|
+
image_storage=image_storage,
|
|
179
|
+
model_class=AIdol,
|
|
180
|
+
create_schema=AIdolCreate,
|
|
181
|
+
update_schema=AIdolUpdate,
|
|
182
|
+
db_session_factory=db_session_factory,
|
|
183
|
+
repository_factory=repository_factory,
|
|
184
|
+
user_info_provider=user_info_provider,
|
|
185
|
+
jwt_secret_key=jwt_settings.secret_key if jwt_settings else None,
|
|
186
|
+
resource_name=resource_name,
|
|
187
|
+
tags=tags or ["AIdol"],
|
|
188
|
+
)
|
|
189
|
+
return router.get_router()
|
aidol/api/companion.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Companion API router
|
|
3
|
+
|
|
4
|
+
Public endpoints for Companion creation and retrieval.
|
|
5
|
+
Public access pattern: no authentication required.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from aioia_core.auth import UserInfoProvider
|
|
9
|
+
from aioia_core.errors import ErrorResponse
|
|
10
|
+
from aioia_core.fastapi import BaseCrudRouter
|
|
11
|
+
from aioia_core.settings import JWTSettings
|
|
12
|
+
from fastapi import APIRouter, Depends, Query, status
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from sqlalchemy.orm import sessionmaker
|
|
15
|
+
|
|
16
|
+
from aidol.protocols import (
|
|
17
|
+
CompanionRepositoryFactoryProtocol,
|
|
18
|
+
CompanionRepositoryProtocol,
|
|
19
|
+
)
|
|
20
|
+
from aidol.schemas import Companion, CompanionCreate, CompanionPublic, CompanionUpdate
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CompanionSingleItemResponse(BaseModel):
|
|
24
|
+
"""Single item response for Companion (public)."""
|
|
25
|
+
|
|
26
|
+
data: CompanionPublic
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CompanionPaginatedResponse(BaseModel):
|
|
30
|
+
"""Paginated response for Companion (public)."""
|
|
31
|
+
|
|
32
|
+
data: list[CompanionPublic]
|
|
33
|
+
total: int
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class CompanionRouter(
|
|
37
|
+
BaseCrudRouter[
|
|
38
|
+
Companion, CompanionCreate, CompanionUpdate, CompanionRepositoryProtocol
|
|
39
|
+
]
|
|
40
|
+
):
|
|
41
|
+
"""
|
|
42
|
+
Companion router with public endpoints.
|
|
43
|
+
|
|
44
|
+
Public CRUD pattern: no authentication required.
|
|
45
|
+
Returns CompanionPublic (excludes system_prompt) for all responses.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def _register_routes(self) -> None:
|
|
49
|
+
"""Register routes (public CRUD)"""
|
|
50
|
+
self._register_public_list_route()
|
|
51
|
+
self._register_public_create_route()
|
|
52
|
+
self._register_public_get_route()
|
|
53
|
+
|
|
54
|
+
def _register_public_list_route(self) -> None:
|
|
55
|
+
"""GET /{resource_name} - List Companions (public)"""
|
|
56
|
+
|
|
57
|
+
@self.router.get(
|
|
58
|
+
f"/{self.resource_name}",
|
|
59
|
+
response_model=CompanionPaginatedResponse,
|
|
60
|
+
status_code=status.HTTP_200_OK,
|
|
61
|
+
summary="List Companions",
|
|
62
|
+
description="List all Companions with optional filtering (public endpoint)",
|
|
63
|
+
)
|
|
64
|
+
async def list_companions(
|
|
65
|
+
current: int = Query(1, ge=1, description="Current page number"),
|
|
66
|
+
page_size: int = Query(10, ge=1, le=100, description="Items per page"),
|
|
67
|
+
sort_param: str | None = Query(
|
|
68
|
+
None,
|
|
69
|
+
alias="sort",
|
|
70
|
+
description='Sorting criteria in JSON format. Example: [["createdAt","desc"]]',
|
|
71
|
+
),
|
|
72
|
+
filters_param: str | None = Query(
|
|
73
|
+
None,
|
|
74
|
+
alias="filters",
|
|
75
|
+
description="Filter conditions (JSON format)",
|
|
76
|
+
),
|
|
77
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
78
|
+
):
|
|
79
|
+
"""List Companions with pagination, sorting, and filtering."""
|
|
80
|
+
sort_list, filter_list = self._parse_query_params(sort_param, filters_param)
|
|
81
|
+
items, total = repository.get_all(
|
|
82
|
+
current=current,
|
|
83
|
+
page_size=page_size,
|
|
84
|
+
sort=sort_list,
|
|
85
|
+
filters=filter_list,
|
|
86
|
+
)
|
|
87
|
+
# Convert to Public schema (exclude system_prompt)
|
|
88
|
+
public_items = [CompanionPublic(**c.model_dump()) for c in items]
|
|
89
|
+
return CompanionPaginatedResponse(data=public_items, total=total)
|
|
90
|
+
|
|
91
|
+
def _register_public_create_route(self) -> None:
|
|
92
|
+
"""POST /{resource_name} - Create a Companion (public)"""
|
|
93
|
+
|
|
94
|
+
@self.router.post(
|
|
95
|
+
f"/{self.resource_name}",
|
|
96
|
+
response_model=CompanionSingleItemResponse,
|
|
97
|
+
status_code=status.HTTP_201_CREATED,
|
|
98
|
+
summary="Create Companion",
|
|
99
|
+
description="Create a new Companion (public endpoint)",
|
|
100
|
+
)
|
|
101
|
+
async def create_companion(
|
|
102
|
+
request: CompanionCreate,
|
|
103
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
104
|
+
):
|
|
105
|
+
"""Create a new Companion."""
|
|
106
|
+
created = repository.create(request)
|
|
107
|
+
# Convert to Public schema (exclude system_prompt)
|
|
108
|
+
public_companion = CompanionPublic(**created.model_dump())
|
|
109
|
+
return CompanionSingleItemResponse(data=public_companion)
|
|
110
|
+
|
|
111
|
+
def _register_public_get_route(self) -> None:
|
|
112
|
+
"""GET /{resource_name}/{id} - Get a Companion (public)"""
|
|
113
|
+
|
|
114
|
+
@self.router.get(
|
|
115
|
+
f"/{self.resource_name}/{{item_id}}",
|
|
116
|
+
response_model=CompanionSingleItemResponse,
|
|
117
|
+
status_code=status.HTTP_200_OK,
|
|
118
|
+
summary="Get Companion",
|
|
119
|
+
description="Get Companion by ID (public endpoint)",
|
|
120
|
+
responses={
|
|
121
|
+
404: {"model": ErrorResponse, "description": "Companion not found"},
|
|
122
|
+
},
|
|
123
|
+
)
|
|
124
|
+
async def get_companion(
|
|
125
|
+
item_id: str,
|
|
126
|
+
repository: CompanionRepositoryProtocol = Depends(self.get_repository_dep),
|
|
127
|
+
):
|
|
128
|
+
"""Get Companion by ID."""
|
|
129
|
+
companion = self._get_item_or_404(repository, item_id)
|
|
130
|
+
# Convert to Public schema (exclude system_prompt)
|
|
131
|
+
public_companion = CompanionPublic(**companion.model_dump())
|
|
132
|
+
return CompanionSingleItemResponse(data=public_companion)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def create_companion_router(
|
|
136
|
+
db_session_factory: sessionmaker,
|
|
137
|
+
repository_factory: CompanionRepositoryFactoryProtocol,
|
|
138
|
+
jwt_settings: JWTSettings | None = None,
|
|
139
|
+
user_info_provider: UserInfoProvider | None = None,
|
|
140
|
+
resource_name: str = "companions",
|
|
141
|
+
tags: list[str] | None = None,
|
|
142
|
+
) -> APIRouter:
|
|
143
|
+
"""
|
|
144
|
+
Create Companion router with dependency injection.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
db_session_factory: Database session factory
|
|
148
|
+
repository_factory: Factory implementing CompanionRepositoryFactoryProtocol
|
|
149
|
+
jwt_settings: Optional JWT settings for authentication
|
|
150
|
+
user_info_provider: Optional user info provider
|
|
151
|
+
resource_name: Resource name for routes (default: "companions")
|
|
152
|
+
tags: Optional OpenAPI tags
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
FastAPI APIRouter instance
|
|
156
|
+
"""
|
|
157
|
+
router = CompanionRouter(
|
|
158
|
+
model_class=Companion,
|
|
159
|
+
create_schema=CompanionCreate,
|
|
160
|
+
update_schema=CompanionUpdate,
|
|
161
|
+
db_session_factory=db_session_factory,
|
|
162
|
+
repository_factory=repository_factory,
|
|
163
|
+
user_info_provider=user_info_provider,
|
|
164
|
+
jwt_secret_key=jwt_settings.secret_key if jwt_settings else None,
|
|
165
|
+
resource_name=resource_name,
|
|
166
|
+
tags=tags or ["Companion"],
|
|
167
|
+
)
|
|
168
|
+
return router.get_router()
|
aidol/factories.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol repository factories
|
|
3
|
+
|
|
4
|
+
Uses BaseRepositoryFactory for BaseCrudRouter compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from aioia_core.factories import BaseRepositoryFactory
|
|
8
|
+
|
|
9
|
+
from aidol.repositories.aidol import AIdolRepository
|
|
10
|
+
from aidol.repositories.companion import CompanionRepository
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AIdolRepositoryFactory(BaseRepositoryFactory[AIdolRepository]):
|
|
14
|
+
"""Factory for creating AIdol repositories."""
|
|
15
|
+
|
|
16
|
+
def __init__(self):
|
|
17
|
+
super().__init__(repository_class=AIdolRepository)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CompanionRepositoryFactory(BaseRepositoryFactory[CompanionRepository]):
|
|
21
|
+
"""Factory for creating Companion repositories."""
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
super().__init__(repository_class=CompanionRepository)
|
aidol/models/__init__.py
ADDED
aidol/models/aidol.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol database model
|
|
3
|
+
|
|
4
|
+
Uses aioia_core.models.BaseModel which provides:
|
|
5
|
+
- id: Mapped[str] (primary key, UUID default)
|
|
6
|
+
- created_at: Mapped[datetime]
|
|
7
|
+
- updated_at: Mapped[datetime]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from aioia_core.models import BaseModel
|
|
11
|
+
from sqlalchemy import String
|
|
12
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DBAIdol(BaseModel):
|
|
16
|
+
"""AIdol (group) database model"""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "aidols"
|
|
19
|
+
|
|
20
|
+
# id, created_at, updated_at inherited from BaseModel
|
|
21
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
22
|
+
concept: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
23
|
+
profile_image_url: Mapped[str] = mapped_column(String, nullable=False)
|
|
24
|
+
claim_token: Mapped[str | None] = mapped_column(String(36), nullable=True)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Companion database model
|
|
3
|
+
|
|
4
|
+
Uses aioia_core.models.BaseModel which provides:
|
|
5
|
+
- id: Mapped[str] (primary key, UUID default)
|
|
6
|
+
- created_at: Mapped[datetime]
|
|
7
|
+
- updated_at: Mapped[datetime]
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from aioia_core.models import BaseModel
|
|
11
|
+
from sqlalchemy import ForeignKey, Index, String, Text
|
|
12
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class DBCompanion(BaseModel):
|
|
16
|
+
"""Companion (member) database model"""
|
|
17
|
+
|
|
18
|
+
__tablename__ = "companions"
|
|
19
|
+
|
|
20
|
+
# id, created_at, updated_at inherited from BaseModel
|
|
21
|
+
aidol_id: Mapped[str | None] = mapped_column(ForeignKey("aidols.id"), nullable=True)
|
|
22
|
+
name: Mapped[str] = mapped_column(String, nullable=False)
|
|
23
|
+
biography: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
24
|
+
profile_picture_url: Mapped[str | None] = mapped_column(String, nullable=True)
|
|
25
|
+
system_prompt: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
26
|
+
|
|
27
|
+
__table_args__ = (Index("ix_companions_aidol_id", "aidol_id"),)
|
aidol/protocols.py
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol repository protocols for type-safe dependency injection.
|
|
3
|
+
|
|
4
|
+
Defines the interface that AIdol Router expects from repositories.
|
|
5
|
+
Platform-specific integrators implement these protocols via adapters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# pylint: disable=unnecessary-ellipsis
|
|
9
|
+
|
|
10
|
+
from typing import Protocol
|
|
11
|
+
|
|
12
|
+
import PIL.Image
|
|
13
|
+
from aioia_core import CrudRepositoryProtocol
|
|
14
|
+
from sqlalchemy.orm import Session
|
|
15
|
+
|
|
16
|
+
from aidol.schemas import (
|
|
17
|
+
AIdol,
|
|
18
|
+
AIdolCreate,
|
|
19
|
+
AIdolUpdate,
|
|
20
|
+
Companion,
|
|
21
|
+
CompanionCreate,
|
|
22
|
+
CompanionUpdate,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class AIdolRepositoryProtocol(
|
|
27
|
+
CrudRepositoryProtocol[AIdol, AIdolCreate, AIdolUpdate], Protocol
|
|
28
|
+
):
|
|
29
|
+
"""Protocol defining AIdol repository expectations.
|
|
30
|
+
|
|
31
|
+
This protocol enables type-safe dependency injection by defining
|
|
32
|
+
the exact interface that AIdolRouter uses. Inherits CRUD operations
|
|
33
|
+
from CrudRepositoryProtocol.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AIdolRepositoryFactoryProtocol(Protocol):
|
|
38
|
+
"""Protocol for factory that creates AIdolRepositoryProtocol instances.
|
|
39
|
+
|
|
40
|
+
Implementations:
|
|
41
|
+
- aidol.factories.AIdolRepositoryFactory (standalone)
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def create_repository(
|
|
45
|
+
self, db_session: Session | None = None
|
|
46
|
+
) -> AIdolRepositoryProtocol:
|
|
47
|
+
"""Create a repository instance.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
db_session: Optional database session.
|
|
51
|
+
"""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class CompanionRepositoryProtocol(
|
|
56
|
+
CrudRepositoryProtocol[Companion, CompanionCreate, CompanionUpdate], Protocol
|
|
57
|
+
):
|
|
58
|
+
"""Protocol defining Companion repository expectations.
|
|
59
|
+
|
|
60
|
+
This protocol enables type-safe dependency injection by defining
|
|
61
|
+
the exact interface that CompanionRouter uses. Inherits CRUD operations
|
|
62
|
+
from CrudRepositoryProtocol.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CompanionRepositoryFactoryProtocol(Protocol):
|
|
67
|
+
"""Protocol for factory that creates CompanionRepositoryProtocol instances.
|
|
68
|
+
|
|
69
|
+
Implementations:
|
|
70
|
+
- aidol.factories.CompanionRepositoryFactory (standalone)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def create_repository(
|
|
74
|
+
self, db_session: Session | None = None
|
|
75
|
+
) -> CompanionRepositoryProtocol:
|
|
76
|
+
"""Create a repository instance.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
db_session: Optional database session.
|
|
80
|
+
"""
|
|
81
|
+
...
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ImageStorageProtocol(Protocol):
|
|
85
|
+
"""Protocol for image storage operations.
|
|
86
|
+
|
|
87
|
+
Enables dependency injection of storage implementations.
|
|
88
|
+
Platform-specific adapters implement this protocol.
|
|
89
|
+
|
|
90
|
+
Implementations:
|
|
91
|
+
- ImageStorageAdapter (platform integration via StorageService)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
def upload_image(self, image: PIL.Image.Image) -> str:
|
|
95
|
+
"""Upload an image and return the permanent URL.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
image: PIL Image object to upload.
|
|
99
|
+
"""
|
|
100
|
+
...
|
aidol/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# pylint: disable=duplicate-code
|
|
2
|
+
# TODO: Extract common AIdol converters to shared module
|
|
3
|
+
# (duplicated in managers/database_aidol_manager.py)
|
|
4
|
+
"""
|
|
5
|
+
AIdol repository
|
|
6
|
+
|
|
7
|
+
Implements BaseRepository pattern for BaseCrudRouter compatibility.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from datetime import timezone
|
|
11
|
+
|
|
12
|
+
from aioia_core.repositories import BaseRepository
|
|
13
|
+
from sqlalchemy.orm import Session
|
|
14
|
+
|
|
15
|
+
from aidol.models import DBAIdol
|
|
16
|
+
from aidol.schemas import AIdol, AIdolCreate, AIdolUpdate
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _convert_db_aidol_to_model(db_aidol: DBAIdol) -> AIdol:
|
|
20
|
+
"""Convert DB AIdol to Pydantic model.
|
|
21
|
+
|
|
22
|
+
Includes claim_token for internal use (Service layer).
|
|
23
|
+
Router should convert to AIdolPublic for API responses.
|
|
24
|
+
"""
|
|
25
|
+
return AIdol(
|
|
26
|
+
id=db_aidol.id,
|
|
27
|
+
name=db_aidol.name,
|
|
28
|
+
concept=db_aidol.concept,
|
|
29
|
+
profile_image_url=db_aidol.profile_image_url,
|
|
30
|
+
claim_token=db_aidol.claim_token,
|
|
31
|
+
created_at=db_aidol.created_at.replace(tzinfo=timezone.utc),
|
|
32
|
+
updated_at=db_aidol.updated_at.replace(tzinfo=timezone.utc),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _convert_aidol_create_to_db(schema: AIdolCreate) -> dict:
|
|
37
|
+
"""Convert AIdolCreate schema to DB model data dict.
|
|
38
|
+
|
|
39
|
+
Includes claim_token for ownership verification.
|
|
40
|
+
"""
|
|
41
|
+
return schema.model_dump(exclude_unset=True)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AIdolRepository(BaseRepository[AIdol, DBAIdol, AIdolCreate, AIdolUpdate]):
|
|
45
|
+
"""
|
|
46
|
+
Database-backed AIdol repository.
|
|
47
|
+
|
|
48
|
+
Extends BaseRepository for CRUD operations compatible with BaseCrudRouter.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, db_session: Session):
|
|
52
|
+
super().__init__(
|
|
53
|
+
db_session=db_session,
|
|
54
|
+
db_model=DBAIdol,
|
|
55
|
+
convert_to_model=_convert_db_aidol_to_model,
|
|
56
|
+
convert_to_db_model=_convert_aidol_create_to_db,
|
|
57
|
+
)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Companion repository
|
|
3
|
+
|
|
4
|
+
Implements BaseRepository pattern for BaseCrudRouter compatibility.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import timezone
|
|
8
|
+
|
|
9
|
+
from aioia_core.repositories import BaseRepository
|
|
10
|
+
from sqlalchemy.orm import Session
|
|
11
|
+
|
|
12
|
+
from aidol.models import DBCompanion
|
|
13
|
+
from aidol.schemas import Companion, CompanionCreate, CompanionUpdate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _convert_db_companion_to_model(db_companion: DBCompanion) -> Companion:
|
|
17
|
+
"""Convert DB Companion to Pydantic model.
|
|
18
|
+
|
|
19
|
+
Includes system_prompt for internal use (Service layer).
|
|
20
|
+
Router should convert to CompanionPublic for API responses.
|
|
21
|
+
"""
|
|
22
|
+
return Companion(
|
|
23
|
+
id=db_companion.id,
|
|
24
|
+
aidol_id=db_companion.aidol_id,
|
|
25
|
+
name=db_companion.name,
|
|
26
|
+
biography=db_companion.biography,
|
|
27
|
+
profile_picture_url=db_companion.profile_picture_url,
|
|
28
|
+
system_prompt=db_companion.system_prompt,
|
|
29
|
+
created_at=db_companion.created_at.replace(tzinfo=timezone.utc),
|
|
30
|
+
updated_at=db_companion.updated_at.replace(tzinfo=timezone.utc),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _convert_companion_create_to_db(schema: CompanionCreate) -> dict:
|
|
35
|
+
"""Convert CompanionCreate schema to DB model data dict.
|
|
36
|
+
|
|
37
|
+
Includes system_prompt for AI configuration.
|
|
38
|
+
"""
|
|
39
|
+
return schema.model_dump(exclude_unset=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CompanionRepository(
|
|
43
|
+
BaseRepository[Companion, DBCompanion, CompanionCreate, CompanionUpdate]
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Database-backed Companion repository.
|
|
47
|
+
|
|
48
|
+
Extends BaseRepository for CRUD operations compatible with BaseCrudRouter.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self, db_session: Session):
|
|
52
|
+
super().__init__(
|
|
53
|
+
db_session=db_session,
|
|
54
|
+
db_model=DBCompanion,
|
|
55
|
+
convert_to_model=_convert_db_companion_to_model,
|
|
56
|
+
convert_to_db_model=_convert_companion_create_to_db,
|
|
57
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol Pydantic schemas
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from aidol.schemas.aidol import (
|
|
6
|
+
AIdol,
|
|
7
|
+
AIdolBase,
|
|
8
|
+
AIdolCreate,
|
|
9
|
+
AIdolPublic,
|
|
10
|
+
AIdolUpdate,
|
|
11
|
+
ImageGenerationData,
|
|
12
|
+
ImageGenerationRequest,
|
|
13
|
+
ImageGenerationResponse,
|
|
14
|
+
)
|
|
15
|
+
from aidol.schemas.companion import (
|
|
16
|
+
Companion,
|
|
17
|
+
CompanionBase,
|
|
18
|
+
CompanionCreate,
|
|
19
|
+
CompanionPublic,
|
|
20
|
+
CompanionUpdate,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"AIdol",
|
|
25
|
+
"AIdolBase",
|
|
26
|
+
"AIdolCreate",
|
|
27
|
+
"AIdolPublic",
|
|
28
|
+
"AIdolUpdate",
|
|
29
|
+
"ImageGenerationData",
|
|
30
|
+
"ImageGenerationRequest",
|
|
31
|
+
"ImageGenerationResponse",
|
|
32
|
+
"Companion",
|
|
33
|
+
"CompanionBase",
|
|
34
|
+
"CompanionCreate",
|
|
35
|
+
"CompanionPublic",
|
|
36
|
+
"CompanionUpdate",
|
|
37
|
+
]
|
aidol/schemas/aidol.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AIdol (group) schemas
|
|
3
|
+
|
|
4
|
+
Schema hierarchy:
|
|
5
|
+
- AIdolBase: Mutable fields (used in Create/Update)
|
|
6
|
+
- AIdolCreate: Base + optional claim_token
|
|
7
|
+
- AIdolUpdate: Base fields only
|
|
8
|
+
- AIdol: Response with all fields including claim_token (internal use)
|
|
9
|
+
- AIdolPublic: Response without sensitive fields (API use)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from humps import camelize
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AIdolBase(BaseModel):
|
|
19
|
+
"""Base AIdol model with mutable fields.
|
|
20
|
+
|
|
21
|
+
Contains only fields that can be modified after creation.
|
|
22
|
+
Excludes claim_token (immutable, set at creation only).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
|
|
26
|
+
|
|
27
|
+
name: str = Field(..., description="AIdol group name")
|
|
28
|
+
concept: str | None = Field(default=None, description="Group concept or theme")
|
|
29
|
+
profile_image_url: str = Field(..., description="Profile image URL")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AIdolCreate(AIdolBase):
|
|
33
|
+
"""Schema for creating an AIdol group (no id).
|
|
34
|
+
|
|
35
|
+
claim_token is optional for anonymous ownership verification.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
claim_token: str | None = Field(
|
|
39
|
+
default=None,
|
|
40
|
+
description="Optional client-generated UUID for ownership verification",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AIdolUpdate(BaseModel):
|
|
45
|
+
"""Schema for updating an AIdol group (all fields optional)."""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
|
|
48
|
+
|
|
49
|
+
name: str | None = Field(default=None, description="AIdol group name")
|
|
50
|
+
concept: str | None = Field(default=None, description="Group concept or theme")
|
|
51
|
+
profile_image_url: str | None = Field(default=None, description="Profile image URL")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class AIdol(AIdolBase):
|
|
55
|
+
"""AIdol response schema with id and timestamps.
|
|
56
|
+
|
|
57
|
+
Includes optional claim_token for ownership verification.
|
|
58
|
+
Use AIdolPublic for API responses to exclude sensitive fields.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
model_config = ConfigDict(
|
|
62
|
+
populate_by_name=True, from_attributes=True, alias_generator=camelize
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
id: str = Field(..., description="AIdol group ID")
|
|
66
|
+
claim_token: str | None = Field(
|
|
67
|
+
default=None, description="Optional ownership token (sensitive, internal use)"
|
|
68
|
+
)
|
|
69
|
+
created_at: datetime = Field(..., description="Creation timestamp")
|
|
70
|
+
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class AIdolPublic(AIdolBase):
|
|
74
|
+
"""Public AIdol response schema without sensitive fields.
|
|
75
|
+
|
|
76
|
+
Excludes claim_token for API responses.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(
|
|
80
|
+
populate_by_name=True, from_attributes=True, alias_generator=camelize
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
id: str = Field(..., description="AIdol group ID")
|
|
84
|
+
created_at: datetime = Field(..., description="Creation timestamp")
|
|
85
|
+
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Image Generation
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class ImageGenerationRequest(BaseModel):
|
|
94
|
+
"""Request schema for image generation."""
|
|
95
|
+
|
|
96
|
+
model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
|
|
97
|
+
|
|
98
|
+
prompt: str = Field(
|
|
99
|
+
...,
|
|
100
|
+
max_length=200,
|
|
101
|
+
description="Text description for image generation (max 200 chars)",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ImageGenerationData(BaseModel):
|
|
106
|
+
"""Image generation result data."""
|
|
107
|
+
|
|
108
|
+
model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
|
|
109
|
+
|
|
110
|
+
image_url: str = Field(..., description="Generated image URL")
|
|
111
|
+
width: int = Field(..., description="Image width in pixels")
|
|
112
|
+
height: int = Field(..., description="Image height in pixels")
|
|
113
|
+
format: str = Field(..., description="Image format (e.g., png, jpg)")
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class ImageGenerationResponse(BaseModel):
|
|
117
|
+
"""Response schema for image generation."""
|
|
118
|
+
|
|
119
|
+
data: ImageGenerationData
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Companion (member) schemas
|
|
3
|
+
|
|
4
|
+
Schema hierarchy:
|
|
5
|
+
- CompanionBase: Mutable fields (used in Create/Update)
|
|
6
|
+
- CompanionCreate: Base + system_prompt (mutable, but sensitive)
|
|
7
|
+
- CompanionUpdate: Base + system_prompt (mutable, but sensitive)
|
|
8
|
+
- Companion: Response with all fields including system_prompt (internal use)
|
|
9
|
+
- CompanionPublic: Response without sensitive fields (API use)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
from humps import camelize
|
|
15
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CompanionBase(BaseModel):
|
|
19
|
+
"""Base companion model with common mutable fields.
|
|
20
|
+
|
|
21
|
+
Contains fields that can be modified after creation.
|
|
22
|
+
Excludes system_prompt (sensitive, requires explicit inclusion).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
|
|
26
|
+
|
|
27
|
+
aidol_id: str | None = Field(default=None, description="AIdol group ID")
|
|
28
|
+
name: str = Field(..., description="Companion name")
|
|
29
|
+
biography: str | None = Field(default=None, description="Companion biography")
|
|
30
|
+
profile_picture_url: str | None = Field(
|
|
31
|
+
default=None, description="Profile picture URL"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class CompanionCreate(CompanionBase):
|
|
36
|
+
"""Schema for creating a companion (no id).
|
|
37
|
+
|
|
38
|
+
Includes system_prompt for creation (excluded from response for security).
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
system_prompt: str | None = Field(
|
|
42
|
+
default=None, description="AI system prompt (not exposed in responses)"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class CompanionUpdate(BaseModel):
|
|
47
|
+
"""Schema for updating a companion (all fields optional)."""
|
|
48
|
+
|
|
49
|
+
model_config = ConfigDict(populate_by_name=True, alias_generator=camelize)
|
|
50
|
+
|
|
51
|
+
aidol_id: str | None = Field(default=None, description="AIdol group ID")
|
|
52
|
+
name: str | None = Field(default=None, description="Companion name")
|
|
53
|
+
biography: str | None = Field(default=None, description="Companion biography")
|
|
54
|
+
profile_picture_url: str | None = Field(
|
|
55
|
+
default=None, description="Profile picture URL"
|
|
56
|
+
)
|
|
57
|
+
system_prompt: str | None = Field(
|
|
58
|
+
default=None, description="AI system prompt (not exposed in responses)"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class Companion(CompanionBase):
|
|
63
|
+
"""Companion response schema with id and timestamps.
|
|
64
|
+
|
|
65
|
+
Includes system_prompt for internal use (Service layer).
|
|
66
|
+
Use CompanionPublic for API responses to exclude sensitive fields.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
model_config = ConfigDict(
|
|
70
|
+
populate_by_name=True, from_attributes=True, alias_generator=camelize
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
id: str = Field(..., description="Companion ID")
|
|
74
|
+
system_prompt: str | None = Field(
|
|
75
|
+
default=None, description="AI system prompt (sensitive, internal use only)"
|
|
76
|
+
)
|
|
77
|
+
created_at: datetime = Field(..., description="Creation timestamp")
|
|
78
|
+
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CompanionPublic(CompanionBase):
|
|
82
|
+
"""Public companion response schema without sensitive fields.
|
|
83
|
+
|
|
84
|
+
Excludes system_prompt for API responses.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
model_config = ConfigDict(
|
|
88
|
+
populate_by_name=True, from_attributes=True, alias_generator=camelize
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
id: str = Field(..., description="Companion ID")
|
|
92
|
+
created_at: datetime = Field(..., description="Creation timestamp")
|
|
93
|
+
updated_at: datetime = Field(..., description="Last update timestamp")
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Image generation service for AIdol
|
|
3
|
+
|
|
4
|
+
Generates images using OpenAI DALL-E 3 for AIdol emblems and Companion profiles.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from io import BytesIO
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
import openai
|
|
14
|
+
import PIL.Image
|
|
15
|
+
from aioia_core.settings import OpenAIAPISettings
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ImageGenerationResponse:
|
|
22
|
+
"""Structured response from the Image Generation service"""
|
|
23
|
+
|
|
24
|
+
url: str
|
|
25
|
+
revised_prompt: str | None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ImageGenerationService:
|
|
29
|
+
"""Service for generating images using OpenAI DALL-E 3"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, openai_settings: OpenAIAPISettings):
|
|
32
|
+
"""
|
|
33
|
+
Initialize the Image Generation service with OpenAI settings.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
openai_settings: OpenAI settings containing required API key
|
|
37
|
+
"""
|
|
38
|
+
self.settings = openai_settings
|
|
39
|
+
self.client = openai.OpenAI(api_key=self.settings.api_key)
|
|
40
|
+
|
|
41
|
+
def generate_image(
|
|
42
|
+
self,
|
|
43
|
+
prompt: str,
|
|
44
|
+
size: Literal[
|
|
45
|
+
"1024x1024",
|
|
46
|
+
"1792x1024",
|
|
47
|
+
"1024x1792",
|
|
48
|
+
] = "1024x1024",
|
|
49
|
+
quality: Literal["standard", "hd"] = "standard",
|
|
50
|
+
) -> ImageGenerationResponse | None:
|
|
51
|
+
"""
|
|
52
|
+
Generate an image from a text prompt using OpenAI DALL-E 3.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
prompt: Text description of the image to generate
|
|
56
|
+
size: Image size (default: "1024x1024")
|
|
57
|
+
quality: Image quality "standard" or "hd" (default: "standard")
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
An ImageGenerationResponse object containing the image URL and revised prompt,
|
|
61
|
+
or None if generation fails.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
openai.OpenAIError: If OpenAI API call fails.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
logger.info("Generating image with OpenAI DALL-E 3...")
|
|
68
|
+
response = self.client.images.generate(
|
|
69
|
+
model="dall-e-3",
|
|
70
|
+
prompt=prompt,
|
|
71
|
+
size=size,
|
|
72
|
+
quality=quality,
|
|
73
|
+
n=1,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if not response.data or len(response.data) == 0:
|
|
77
|
+
logger.error("No image data returned from OpenAI")
|
|
78
|
+
return None
|
|
79
|
+
|
|
80
|
+
image_data = response.data[0]
|
|
81
|
+
url = image_data.url
|
|
82
|
+
|
|
83
|
+
if not url:
|
|
84
|
+
logger.error("No URL found in image response")
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
revised_prompt = image_data.revised_prompt
|
|
88
|
+
|
|
89
|
+
logger.info("Successfully generated image: %s", url[:100])
|
|
90
|
+
return ImageGenerationResponse(
|
|
91
|
+
url=url,
|
|
92
|
+
revised_prompt=revised_prompt,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
except openai.OpenAIError as e:
|
|
96
|
+
logger.error("OpenAI API error: %s", e)
|
|
97
|
+
raise
|
|
98
|
+
|
|
99
|
+
def _download_image(self, url: str) -> PIL.Image.Image:
|
|
100
|
+
"""Download image from URL and return as PIL Image.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
url: URL of the image to download.
|
|
104
|
+
|
|
105
|
+
Raises:
|
|
106
|
+
httpx.HTTPError: If download fails.
|
|
107
|
+
"""
|
|
108
|
+
with httpx.Client(timeout=30.0) as client:
|
|
109
|
+
response = client.get(url)
|
|
110
|
+
response.raise_for_status()
|
|
111
|
+
return PIL.Image.open(BytesIO(response.content))
|
|
112
|
+
|
|
113
|
+
def generate_and_download_image(
|
|
114
|
+
self,
|
|
115
|
+
prompt: str,
|
|
116
|
+
size: Literal[
|
|
117
|
+
"1024x1024",
|
|
118
|
+
"1792x1024",
|
|
119
|
+
"1024x1792",
|
|
120
|
+
] = "1024x1024",
|
|
121
|
+
quality: Literal["standard", "hd"] = "standard",
|
|
122
|
+
) -> PIL.Image.Image | None:
|
|
123
|
+
"""Generate an image and download as PIL Image.
|
|
124
|
+
|
|
125
|
+
DALL-E returns temporary URLs that expire in 1-2 hours.
|
|
126
|
+
Use this method to download the image immediately after generation.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
prompt: Text description of the image to generate.
|
|
130
|
+
size: Image size (default: "1024x1024").
|
|
131
|
+
quality: Image quality "standard" or "hd" (default: "standard").
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
PIL Image object, or None if generation fails.
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
openai.OpenAIError: If OpenAI API call fails.
|
|
138
|
+
httpx.HTTPError: If image download fails.
|
|
139
|
+
"""
|
|
140
|
+
result = self.generate_image(prompt, size, quality)
|
|
141
|
+
if result is None:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
logger.info("Downloading image from DALL-E temporary URL...")
|
|
145
|
+
return self._download_image(result.url)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: py-aidol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Create and chat with your own AI idol group
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Keywords: kpop,idol,aidol,ai-companion,chatbot,image-generation
|
|
7
|
+
Author: AIoIA, Inc.
|
|
8
|
+
Author-email: devops@aioia.ai
|
|
9
|
+
Requires-Python: >=3.10,<3.13
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Requires-Dist: aioia-core (>=2.2.0,<3.0.0)
|
|
16
|
+
Requires-Dist: fastapi (>=0.115.12,<0.116.0)
|
|
17
|
+
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
18
|
+
Requires-Dist: openai (>=1.0.0)
|
|
19
|
+
Requires-Dist: pillow (>=10.0.0,<11.0.0)
|
|
20
|
+
Requires-Dist: psycopg2-binary (>=2.9.9,<3.0.0)
|
|
21
|
+
Requires-Dist: pydantic-settings (>=2.1.0,<3.0.0)
|
|
22
|
+
Requires-Dist: pydantic[email] (>=2.5.3,<3.0.0)
|
|
23
|
+
Requires-Dist: pyhumps (>=3.8.0,<4.0.0)
|
|
24
|
+
Requires-Dist: python-dotenv (>=1.0.0,<2.0.0)
|
|
25
|
+
Requires-Dist: sqlalchemy (>=2.0.25,<3.0.0)
|
|
26
|
+
Requires-Dist: sqlalchemy-mixins (>=2.0.5,<3.0.0)
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# AIdol Backend
|
|
30
|
+
|
|
31
|
+
AI 아이돌 그룹 생성 및 채팅 Python 패키지
|
|
32
|
+
|
|
33
|
+
## 주요 기능
|
|
34
|
+
|
|
35
|
+
- AI 아이돌 그룹/멤버 CRUD
|
|
36
|
+
- DALL-E 3 이미지 생성 (엠블럼, 프로필)
|
|
37
|
+
- 텍스트 채팅 (페르소나 기반 응답)
|
|
38
|
+
- Buppy 통합 Adapter 패턴
|
|
39
|
+
|
|
40
|
+
## 설치
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
cd backend
|
|
44
|
+
poetry install
|
|
45
|
+
poetry run uvicorn main:app --reload
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
API 문서:
|
|
49
|
+
- Swagger UI: http://localhost:8000/docs
|
|
50
|
+
- ReDoc: http://localhost:8000/redoc
|
|
51
|
+
|
|
52
|
+
## 사용법
|
|
53
|
+
|
|
54
|
+
### FastAPI 통합
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
from aidol.api.aidol import AIdolRouter
|
|
58
|
+
from aidol.api.companion import CompanionRouter
|
|
59
|
+
from aidol.factories import AIdolRepositoryFactory, CompanionRepositoryFactory
|
|
60
|
+
|
|
61
|
+
# AIdol 라우터
|
|
62
|
+
aidol_router = AIdolRouter(
|
|
63
|
+
repository_factory=AIdolRepositoryFactory(),
|
|
64
|
+
openai_settings=openai_settings,
|
|
65
|
+
image_storage=image_storage,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Companion 라우터
|
|
69
|
+
companion_router = CompanionRouter(
|
|
70
|
+
repository_factory=CompanionRepositoryFactory(),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
app.include_router(aidol_router.router, prefix="/api/aidol")
|
|
74
|
+
app.include_router(companion_router.router, prefix="/api/aidol")
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 개발
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
poetry install
|
|
81
|
+
make lint
|
|
82
|
+
make type-check
|
|
83
|
+
make unit-test
|
|
84
|
+
make format
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 환경 변수
|
|
88
|
+
|
|
89
|
+
### 필수 (이미지 생성 시)
|
|
90
|
+
|
|
91
|
+
| 변수 | 설명 |
|
|
92
|
+
|------|------|
|
|
93
|
+
| `OPENAI_API_KEY` | OpenAI API 키 |
|
|
94
|
+
|
|
95
|
+
### 선택
|
|
96
|
+
|
|
97
|
+
| 변수 | 기본값 | 설명 |
|
|
98
|
+
|------|--------|------|
|
|
99
|
+
| `AIDOL_OPENAI_MODEL` | `gpt-4o-mini` | 채팅 응답 LLM 모델 |
|
|
100
|
+
|
|
101
|
+
> **참고**: 데이터베이스, 모델 등 추가 설정은 기본값으로 로컬 개발 가능합니다.
|
|
102
|
+
> 변경이 필요한 경우 `aidol/` 내 Settings 클래스를 참고하세요.
|
|
103
|
+
|
|
104
|
+
## 의존성
|
|
105
|
+
|
|
106
|
+
- aioia-core (공통 인프라)
|
|
107
|
+
- FastAPI, SQLAlchemy, Pydantic
|
|
108
|
+
- OpenAI (이미지 생성, 채팅)
|
|
109
|
+
- Pillow (이미지 처리)
|
|
110
|
+
|
|
111
|
+
## 라이선스
|
|
112
|
+
|
|
113
|
+
Apache 2.0
|
|
114
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
aidol/__init__.py,sha256=iMN-aij1k6vElJt0sZQT4QYJjvoD27Q9vtZkQA0TY9c,141
|
|
2
|
+
aidol/api/__init__.py,sha256=skD_w82nT0v1hdKK9BBOycNERexIr8F1BmSmSono4Jk,276
|
|
3
|
+
aidol/api/aidol.py,sha256=DQzwF41bnfw8JpcrZlV3XDkvuH5AszyYCJwOmcRxTKg,6684
|
|
4
|
+
aidol/api/companion.py,sha256=QBsPgSjBp1USunB6Jth6fdWLwBzsI6KIq6XQZu9OifQ,6287
|
|
5
|
+
aidol/factories.py,sha256=HYkJN9qu1fZOgLW6hk3t0Ixh41mVB4tzzmOQQcso9tA,698
|
|
6
|
+
aidol/models/__init__.py,sha256=nlmLezsKx1xGdJkKANkOj3QCZQVPniXkYwQBsj8Ockk,155
|
|
7
|
+
aidol/models/aidol.py,sha256=0t1OJiJBnmhpRCYczmsr1aYD8pMZqE3wKaUWIQiymKM,750
|
|
8
|
+
aidol/models/companion.py,sha256=F64BejN3ZWP4EyNDBiV0s63dofsAgMVZJi55JV0b5gk,954
|
|
9
|
+
aidol/protocols.py,sha256=Zc9LWUcdHJlerI649v3maXbgXARiXrBiRd2cxNLnSDE,2667
|
|
10
|
+
aidol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
aidol/repositories/__init__.py,sha256=hJbn-Pw0iGMMIYkUbEqLOy-gK_0LqCJLBba21pmZ0Pw,207
|
|
12
|
+
aidol/repositories/aidol.py,sha256=zwgXB823EZsrYYoJn2S05VD1bcuhSlcwnlVCJoD09p0,1744
|
|
13
|
+
aidol/repositories/companion.py,sha256=xzAHrdCexT4cXFPTg3yCmWoqou8edeZO0A22ozMieFg,1775
|
|
14
|
+
aidol/schemas/__init__.py,sha256=dakC_xvl4akjGRSOJdFnB1k6_RbNplGnPzEwI_7wwKA,661
|
|
15
|
+
aidol/schemas/aidol.py,sha256=aTS7hx4BGUvLDMgqo3CGVY_867GO0vnAeJhRFntGG1g,3834
|
|
16
|
+
aidol/schemas/companion.py,sha256=SCjvNETAZII256o0myCp6q1wzp999JIO99q8cKf8Yo0,3224
|
|
17
|
+
aidol/services/__init__.py,sha256=3vdT_CtUfeDWbsPn7Xnp41sajovcl2nCvpZ8KNFPHYM,144
|
|
18
|
+
aidol/services/image_generation_service.py,sha256=naqOxe5jsSTs9__Nj3gwBtOQPkfWvgqVV4z6XkA6DGM,4359
|
|
19
|
+
py_aidol-0.1.0.dist-info/METADATA,sha256=_c0y1xNNGOGHmKIeSfvCqWmYLPuGfDypFHg7jn8uVoM,2880
|
|
20
|
+
py_aidol-0.1.0.dist-info/WHEEL,sha256=3ny-bZhpXrU6vSQ1UPG34FoxZBp3lVcvK0LkgUz6VLk,88
|
|
21
|
+
py_aidol-0.1.0.dist-info/RECORD,,
|