py-aidol 0.2.0__tar.gz → 0.4.0__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.
Files changed (30) hide show
  1. {py_aidol-0.2.0 → py_aidol-0.4.0}/PKG-INFO +24 -6
  2. {py_aidol-0.2.0 → py_aidol-0.4.0}/README.md +22 -5
  3. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/api/aidol.py +68 -73
  4. py_aidol-0.4.0/aidol/api/common.py +72 -0
  5. py_aidol-0.4.0/aidol/api/companion.py +286 -0
  6. py_aidol-0.4.0/aidol/api/lead.py +123 -0
  7. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/factories.py +8 -0
  8. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/protocols.py +26 -0
  9. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/repositories/aidol_lead.py +3 -6
  10. py_aidol-0.4.0/aidol/services/companion_service.py +96 -0
  11. py_aidol-0.4.0/aidol/services/image_generation_service.py +112 -0
  12. py_aidol-0.4.0/aidol/settings.py +37 -0
  13. {py_aidol-0.2.0 → py_aidol-0.4.0}/pyproject.toml +2 -1
  14. py_aidol-0.2.0/aidol/api/companion.py +0 -168
  15. py_aidol-0.2.0/aidol/services/image_generation_service.py +0 -145
  16. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/__init__.py +0 -0
  17. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/api/__init__.py +0 -0
  18. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/models/__init__.py +0 -0
  19. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/models/aidol.py +0 -0
  20. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/models/aidol_lead.py +0 -0
  21. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/models/companion.py +0 -0
  22. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/py.typed +0 -0
  23. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/repositories/__init__.py +0 -0
  24. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/repositories/aidol.py +0 -0
  25. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/repositories/companion.py +0 -0
  26. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/schemas/__init__.py +0 -0
  27. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/schemas/aidol.py +0 -0
  28. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/schemas/aidol_lead.py +0 -0
  29. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/schemas/companion.py +0 -0
  30. {py_aidol-0.2.0 → py_aidol-0.4.0}/aidol/services/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: py-aidol
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: Create and chat with your own AI idol group
5
5
  License: Apache-2.0
6
6
  Keywords: kpop,idol,aidol,ai-companion,chatbot,image-generation
@@ -14,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.11
14
14
  Classifier: Programming Language :: Python :: 3.12
15
15
  Requires-Dist: aioia-core (>=2.2.0,<3.0.0)
16
16
  Requires-Dist: fastapi (>=0.115.12,<0.116.0)
17
+ Requires-Dist: google-genai (>=1.60.0,<2.0.0)
17
18
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
18
19
  Requires-Dist: openai (>=1.0.0)
19
20
  Requires-Dist: pillow (>=10.0.0,<11.0.0)
@@ -33,7 +34,7 @@ AI 아이돌 그룹 생성 및 채팅 Python 패키지
33
34
  ## 주요 기능
34
35
 
35
36
  - AI 아이돌 그룹/멤버 CRUD
36
- - DALL-E 3 이미지 생성 (엠블럼, 프로필)
37
+ - Google Gemini 이미지 생성 (엠블럼, 프로필)
37
38
  - 텍스트 채팅 (페르소나 기반 응답)
38
39
  - Buppy 통합 Adapter 패턴
39
40
 
@@ -61,7 +62,7 @@ from aidol.factories import AIdolRepositoryFactory, CompanionRepositoryFactory
61
62
  # AIdol 라우터
62
63
  aidol_router = AIdolRouter(
63
64
  repository_factory=AIdolRepositoryFactory(),
64
- openai_settings=openai_settings,
65
+ google_settings=google_settings,
65
66
  image_storage=image_storage,
66
67
  )
67
68
 
@@ -86,11 +87,27 @@ make format
86
87
 
87
88
  ## 환경 변수
88
89
 
89
- ### 필수 (이미지 생성 )
90
+ ### 이미지 생성 인증 (선택, ADC 지원)
90
91
 
91
92
  | 변수 | 설명 |
92
93
  |------|------|
93
- | `OPENAI_API_KEY` | OpenAI API 키 |
94
+ | `GOOGLE_API_KEY` | Google API 키 (Google AI API) |
95
+ | `GOOGLE_CLOUD_PROJECT` | GCP 프로젝트 ID (Vertex AI) |
96
+
97
+ **인증 방법:**
98
+
99
+ **Option 1: Google AI API (API Key)**
100
+ ```bash
101
+ export GOOGLE_API_KEY=your-api-key
102
+ ```
103
+
104
+ **Option 2: Vertex AI (ADC)**
105
+ ```bash
106
+ export GOOGLE_CLOUD_PROJECT=your-project-id
107
+ gcloud auth application-default login # 로컬 개발
108
+ ```
109
+
110
+ > **참고**: Vertex AI 사용 시 `location=global`이 하드코딩되어 있습니다 (Gemini 이미지 생성 모델 요구사항).
94
111
 
95
112
  ### 선택
96
113
 
@@ -105,7 +122,8 @@ make format
105
122
 
106
123
  - aioia-core (공통 인프라)
107
124
  - FastAPI, SQLAlchemy, Pydantic
108
- - OpenAI (이미지 생성, 채팅)
125
+ - Google Generative AI (이미지 생성)
126
+ - OpenAI (채팅)
109
127
  - Pillow (이미지 처리)
110
128
 
111
129
  ## 라이선스
@@ -5,7 +5,7 @@ AI 아이돌 그룹 생성 및 채팅 Python 패키지
5
5
  ## 주요 기능
6
6
 
7
7
  - AI 아이돌 그룹/멤버 CRUD
8
- - DALL-E 3 이미지 생성 (엠블럼, 프로필)
8
+ - Google Gemini 이미지 생성 (엠블럼, 프로필)
9
9
  - 텍스트 채팅 (페르소나 기반 응답)
10
10
  - Buppy 통합 Adapter 패턴
11
11
 
@@ -33,7 +33,7 @@ from aidol.factories import AIdolRepositoryFactory, CompanionRepositoryFactory
33
33
  # AIdol 라우터
34
34
  aidol_router = AIdolRouter(
35
35
  repository_factory=AIdolRepositoryFactory(),
36
- openai_settings=openai_settings,
36
+ google_settings=google_settings,
37
37
  image_storage=image_storage,
38
38
  )
39
39
 
@@ -58,11 +58,27 @@ make format
58
58
 
59
59
  ## 환경 변수
60
60
 
61
- ### 필수 (이미지 생성 )
61
+ ### 이미지 생성 인증 (선택, ADC 지원)
62
62
 
63
63
  | 변수 | 설명 |
64
64
  |------|------|
65
- | `OPENAI_API_KEY` | OpenAI API 키 |
65
+ | `GOOGLE_API_KEY` | Google API 키 (Google AI API) |
66
+ | `GOOGLE_CLOUD_PROJECT` | GCP 프로젝트 ID (Vertex AI) |
67
+
68
+ **인증 방법:**
69
+
70
+ **Option 1: Google AI API (API Key)**
71
+ ```bash
72
+ export GOOGLE_API_KEY=your-api-key
73
+ ```
74
+
75
+ **Option 2: Vertex AI (ADC)**
76
+ ```bash
77
+ export GOOGLE_CLOUD_PROJECT=your-project-id
78
+ gcloud auth application-default login # 로컬 개발
79
+ ```
80
+
81
+ > **참고**: Vertex AI 사용 시 `location=global`이 하드코딩되어 있습니다 (Gemini 이미지 생성 모델 요구사항).
66
82
 
67
83
  ### 선택
68
84
 
@@ -77,7 +93,8 @@ make format
77
93
 
78
94
  - aioia-core (공통 인프라)
79
95
  - FastAPI, SQLAlchemy, Pydantic
80
- - OpenAI (이미지 생성, 채팅)
96
+ - Google Generative AI (이미지 생성)
97
+ - OpenAI (채팅)
81
98
  - Pillow (이미지 처리)
82
99
 
83
100
  ## 라이선스
@@ -1,3 +1,4 @@
1
+ # pylint: disable=duplicate-code
1
2
  """
2
3
  AIdol API router
3
4
 
@@ -8,32 +9,25 @@ Public access pattern: no authentication required.
8
9
  from aioia_core.auth import UserInfoProvider
9
10
  from aioia_core.errors import ErrorResponse
10
11
  from aioia_core.fastapi import BaseCrudRouter
11
- from aioia_core.settings import JWTSettings, OpenAIAPISettings
12
- from fastapi import APIRouter, Depends, HTTPException, status
12
+ from aioia_core.settings import JWTSettings
13
+ from fastapi import APIRouter, Depends, HTTPException, Response, status
13
14
  from pydantic import BaseModel
14
15
  from sqlalchemy.orm import sessionmaker
15
16
 
17
+ from aidol.api.common import register_image_generation_route
16
18
  from aidol.protocols import (
17
19
  AIdolRepositoryFactoryProtocol,
18
20
  AIdolRepositoryProtocol,
19
21
  ImageStorageProtocol,
20
22
  )
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
23
+ from aidol.schemas import AIdol, AIdolCreate, AIdolPublic, AIdolUpdate
24
+ from aidol.settings import GoogleGenAISettings
31
25
 
32
26
 
33
- class AIdolSingleItemResponse(BaseModel):
34
- """Single item response for AIdol (public)."""
27
+ class AIdolCreateResponse(BaseModel):
28
+ """Response for AIdol creation (only id)."""
35
29
 
36
- data: AIdolPublic
30
+ id: str
37
31
 
38
32
 
39
33
  class AIdolRouter(
@@ -48,49 +42,92 @@ class AIdolRouter(
48
42
 
49
43
  def __init__(
50
44
  self,
51
- openai_settings: OpenAIAPISettings,
45
+ google_settings: GoogleGenAISettings | None,
52
46
  image_storage: ImageStorageProtocol,
53
47
  **kwargs,
54
48
  ):
55
- super().__init__(**kwargs)
56
- self.openai_settings = openai_settings
49
+ self.google_settings = google_settings
57
50
  self.image_storage = image_storage
51
+ super().__init__(**kwargs)
58
52
 
59
53
  def _register_routes(self) -> None:
60
54
  """Register routes (public CRUD + image generation)"""
61
- self._register_image_generation_route()
55
+ # Register shared image generation route
56
+ register_image_generation_route(
57
+ router=self.router,
58
+ resource_name=self.resource_name,
59
+ google_settings=self.google_settings,
60
+ image_storage=self.image_storage,
61
+ )
62
+
62
63
  self._register_public_create_route()
63
64
  self._register_public_get_route()
65
+ self._register_public_update_route()
66
+
67
+ def _register_public_update_route(self) -> None:
68
+ """PATCH /{resource_name}/{id} - Update AIdol group (public)"""
69
+
70
+ @self.router.patch(
71
+ f"/{self.resource_name}/{{item_id}}",
72
+ response_model=AIdolPublic,
73
+ status_code=status.HTTP_200_OK,
74
+ summary="Update AIdol group",
75
+ description="Update AIdol group by ID (public endpoint). Returns updated AIdol data directly.",
76
+ responses={
77
+ 404: {"model": ErrorResponse, "description": "AIdol group not found"},
78
+ },
79
+ )
80
+ async def update_aidol(
81
+ item_id: str,
82
+ data: AIdolUpdate,
83
+ repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
84
+ ):
85
+ """Update AIdol group."""
86
+ # TODO: Verify ClaimToken if strict ownership is required (Sprint 1)
87
+ updated = repository.update(item_id, data)
88
+ if not updated:
89
+ raise HTTPException(
90
+ status_code=status.HTTP_404_NOT_FOUND,
91
+ detail="AIdol group not found",
92
+ )
93
+
94
+ # Return updated AIdol as public schema
95
+ return AIdolPublic(**updated.model_dump())
64
96
 
65
97
  def _register_public_create_route(self) -> None:
66
98
  """POST /{resource_name} - Create an AIdol group (public)"""
67
99
 
68
100
  @self.router.post(
69
101
  f"/{self.resource_name}",
70
- response_model=AIdolSingleItemResponse,
102
+ response_model=AIdolCreateResponse,
71
103
  status_code=status.HTTP_201_CREATED,
72
104
  summary="Create AIdol group",
73
- description="Create a new AIdol group (public endpoint)",
105
+ description="Create a new AIdol group (public endpoint). Returns only the created id.",
74
106
  )
75
107
  async def create_aidol(
76
108
  request: AIdolCreate,
109
+ response: Response,
77
110
  repository: AIdolRepositoryProtocol = Depends(self.get_repository_dep),
78
111
  ):
79
112
  """Create a new AIdol group."""
80
113
  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)
114
+
115
+ # Set ClaimToken header
116
+ if created.claim_token:
117
+ response.headers["ClaimToken"] = created.claim_token
118
+
119
+ # Return only id
120
+ return AIdolCreateResponse(id=created.id)
84
121
 
85
122
  def _register_public_get_route(self) -> None:
86
123
  """GET /{resource_name}/{id} - Get an AIdol group (public)"""
87
124
 
88
125
  @self.router.get(
89
126
  f"/{self.resource_name}/{{item_id}}",
90
- response_model=AIdolSingleItemResponse,
127
+ response_model=AIdolPublic,
91
128
  status_code=status.HTTP_200_OK,
92
129
  summary="Get AIdol group",
93
- description="Get AIdol group by ID (public endpoint)",
130
+ description="Get AIdol group by ID (public endpoint). Returns AIdol data directly.",
94
131
  responses={
95
132
  404: {"model": ErrorResponse, "description": "AIdol group not found"},
96
133
  },
@@ -101,54 +138,12 @@ class AIdolRouter(
101
138
  ):
102
139
  """Get AIdol group by ID."""
103
140
  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
- )
141
+ # Return AIdol as public schema
142
+ return AIdolPublic(**aidol.model_dump())
148
143
 
149
144
 
150
145
  def create_aidol_router(
151
- openai_settings: OpenAIAPISettings,
146
+ google_settings: GoogleGenAISettings | None,
152
147
  db_session_factory: sessionmaker,
153
148
  repository_factory: AIdolRepositoryFactoryProtocol,
154
149
  image_storage: ImageStorageProtocol,
@@ -161,7 +156,7 @@ def create_aidol_router(
161
156
  Create AIdol router with dependency injection.
162
157
 
163
158
  Args:
164
- openai_settings: OpenAI API settings for image generation
159
+ google_settings: Google API settings (uses ADC if api_key is None)
165
160
  db_session_factory: Database session factory
166
161
  repository_factory: Factory implementing AIdolRepositoryFactoryProtocol
167
162
  image_storage: Image storage for permanent URLs
@@ -174,7 +169,7 @@ def create_aidol_router(
174
169
  FastAPI APIRouter instance
175
170
  """
176
171
  router = AIdolRouter(
177
- openai_settings=openai_settings,
172
+ google_settings=google_settings,
178
173
  image_storage=image_storage,
179
174
  model_class=AIdol,
180
175
  create_schema=AIdolCreate,
@@ -0,0 +1,72 @@
1
+ """
2
+ Common API utilities.
3
+
4
+ Shared functions for registering common routes across different routers.
5
+ """
6
+
7
+ from aioia_core.errors import ErrorResponse
8
+ from fastapi import APIRouter, HTTPException, status
9
+
10
+ from aidol.protocols import ImageStorageProtocol
11
+ from aidol.schemas import (
12
+ ImageGenerationData,
13
+ ImageGenerationRequest,
14
+ ImageGenerationResponse,
15
+ )
16
+ from aidol.services import ImageGenerationService
17
+ from aidol.settings import GoogleGenAISettings
18
+
19
+
20
+ def register_image_generation_route(
21
+ router: APIRouter,
22
+ resource_name: str,
23
+ google_settings: GoogleGenAISettings | None,
24
+ image_storage: ImageStorageProtocol,
25
+ ) -> None:
26
+ """
27
+ Register image generation route to the given router.
28
+
29
+ Args:
30
+ router: FastAPI APIRouter instance
31
+ resource_name: Resource name for the route path
32
+ google_settings: Google API settings (API Key or Vertex AI with ADC)
33
+ image_storage: Image Storage instance
34
+ """
35
+
36
+ @router.post(
37
+ f"/{resource_name}/images",
38
+ response_model=ImageGenerationResponse,
39
+ status_code=status.HTTP_201_CREATED,
40
+ summary="Generate image",
41
+ description=f"Generate image for {resource_name}",
42
+ responses={
43
+ 500: {"model": ErrorResponse, "description": "Image generation failed"},
44
+ },
45
+ )
46
+ async def generate_image(request: ImageGenerationRequest):
47
+ """Generate image from prompt."""
48
+ # Generate and download image
49
+ service = ImageGenerationService(settings=google_settings)
50
+ image = service.generate_and_download_image(
51
+ prompt=request.prompt,
52
+ size="1024x1024",
53
+ quality="standard",
54
+ )
55
+
56
+ if image is None:
57
+ raise HTTPException(
58
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
59
+ detail="Image generation failed",
60
+ )
61
+
62
+ # Upload to permanent storage
63
+ image_url = image_storage.upload_image(image)
64
+
65
+ return ImageGenerationResponse(
66
+ data=ImageGenerationData(
67
+ image_url=image_url,
68
+ width=1024,
69
+ height=1024,
70
+ format="png",
71
+ )
72
+ )