Skip to content

Commit fa93196

Browse files
Merge branch 'feature/imagen-upscale' of github.com:GoogleCloudPlatform/gcc-creative-studio into feature/gallery-enhancements-and-fixes
2 parents 2a904e3 + 27467cc commit fa93196

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+2345
-745
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""add_original_gcs_uris
2+
3+
Revision ID: 0bd50a4bf20c
4+
Revises: 9393a3d298c6
5+
Create Date: 2026-01-29 12:53:26.493393
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '0bd50a4bf20c'
16+
down_revision: Union[str, None] = '9393a3d298c6'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
bind = op.get_bind()
23+
inspector = sa.inspect(bind)
24+
25+
media_columns = [c['name'] for c in inspector.get_columns('media_items')]
26+
if 'original_gcs_uris' not in media_columns:
27+
op.add_column('media_items', sa.Column('original_gcs_uris', sa.ARRAY(sa.String()), nullable=True))
28+
29+
source_columns = [c['name'] for c in inspector.get_columns('source_assets')]
30+
if 'original_gcs_uri' not in source_columns:
31+
op.add_column('source_assets', sa.Column('original_gcs_uri', sa.String(), nullable=True))
32+
33+
34+
def downgrade() -> None:
35+
op.drop_column('source_assets', 'original_gcs_uri')
36+
op.drop_column('media_items', 'original_gcs_uris')
37+

backend/main.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
setup_logging()
1919

2020
import logging
21+
import os
22+
from dotenv import load_dotenv
2123
from concurrent.futures import ThreadPoolExecutor
2224
from contextlib import asynccontextmanager
2325
from os import getenv
@@ -54,15 +56,15 @@
5456

5557
# Get a logger instance for use in this file. It will inherit the root setup.
5658
logger = logging.getLogger(__name__)
57-
59+
load_dotenv()
5860

5961
def configure_cors(app):
6062
"""Configures CORS middleware based on the environment."""
61-
environment = getenv("ENVIRONMENT")
63+
environment = os.getenv("ENVIRONMENT")
6264
allowed_origins = []
6365

6466
if environment == "production":
65-
frontend_url = getenv("FRONTEND_URL")
67+
frontend_url = os.getenv("FRONTEND_URL")
6668
if not frontend_url:
6769
raise ValueError(
6870
"FRONTEND_URL environment variable not set in production"

backend/src/brand_guidelines/brand_guideline_controller.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
GenerateUploadUrlResponseDto,
3737
)
3838
from src.workspaces.repository.workspace_repository import WorkspaceRepository
39-
from src.workspaces.workspace_auth_guard import workspace_auth_service
39+
from src.workspaces.workspace_auth_guard import WorkspaceAuth
4040
from src.users.user_model import UserModel, UserRoleEnum
4141

4242
MAX_UPLOAD_SIZE_BYTES = 500 * 1024 * 1024 # 500 MB
@@ -62,18 +62,17 @@ async def generate_upload_url(
6262
request_dto: GenerateUploadUrlDto,
6363
current_user: UserModel = Depends(get_current_user),
6464
service: BrandGuidelineService = Depends(),
65-
workspace_repo: WorkspaceRepository = Depends(),
65+
workspace_auth: WorkspaceAuth = Depends(),
6666
):
6767
"""
6868
Generates a secure, short-lived URL that the client can use to upload a
6969
brand guideline PDF directly to Google Cloud Storage.
7070
"""
7171
# If a workspace ID is provided, ensure the user has access to it.
7272
if request_dto.workspace_id:
73-
await workspace_auth_service.authorize(
73+
await workspace_auth.authorize(
7474
workspace_id=request_dto.workspace_id,
7575
user=current_user,
76-
workspace_repo=workspace_repo,
7776
)
7877

7978
if not request_dto.content_type == "application/pdf":

backend/src/common/base_dto.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class GenerationModelEnum(str, Enum):
4343
"""Enum representing the available generation models."""
4444

4545
# Image-Specific Models
46+
IMAGEN_4_UPSCALE_PREVIEW = "imagen-4.0-upscale-preview"
4647
IMAGEN_4_001 = "imagen-4.0-generate-001"
4748
IMAGEN_4_ULTRA = "imagen-4.0-ultra-generate-001"
4849
IMAGEN_4_ULTRA_PREVIEW = "imagen-4.0-ultra-generate-preview-06-06"

backend/src/common/schema/media_item_model.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ class MediaItem(Base):
165165
source_media_items: Mapped[Optional[List[dict]]] = mapped_column(JSONB, nullable=True)
166166

167167
gcs_uris: Mapped[List[str]] = mapped_column(ARRAY(String), default=[])
168+
original_gcs_uris: Mapped[Optional[List[str]]] = mapped_column(ARRAY(String), nullable=True, default=[])
168169

169170
# Video specific
170171
duration_seconds: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
@@ -253,6 +254,14 @@ class MediaItemModel(BaseDocument):
253254
description="A list of public URLs for the media to be displayed (e.g., video or image).",
254255
),
255256
]
257+
original_gcs_uris: Annotated[
258+
Optional[List[str]],
259+
Field(
260+
default=None,
261+
min_length=0,
262+
description="A list of public URLs (original / non-upscaled) for the media to be displayed (e.g., video or image).",
263+
),
264+
]
256265

257266
# Video specific
258267
duration_seconds: Optional[float] = None

backend/src/common/storage_service.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -220,8 +220,28 @@ def store_to_gcs(
220220
f"Blob '{destination_blob_name}' not found in bucket '{self.bucket_name}'."
221221
)
222222
return None
223-
except exceptions.GoogleAPICallError as e:
224-
logger.error(
225-
f"Failed to download '{destination_blob_name}' from GCS: {e}"
226-
)
227-
return None
223+
224+
def check_file_exists(self, gcs_uri: str) -> bool:
225+
"""
226+
Checks if a file exists in GCS.
227+
228+
Args:
229+
gcs_uri: The full GCS URI (e.g., "gs://bucket-name/path/to/blob").
230+
231+
Returns:
232+
True if the file exists, False otherwise.
233+
"""
234+
try:
235+
if not gcs_uri:
236+
return False
237+
if not gcs_uri.startswith("gs://"):
238+
logger.warning(f"Invalid GCS URI format: {gcs_uri}")
239+
return False
240+
241+
bucket_name, blob_name = gcs_uri.replace("gs://", "").split("/", 1)
242+
bucket = self.client.bucket(bucket_name)
243+
blob = bucket.blob(blob_name)
244+
return blob.exists()
245+
except Exception as e:
246+
logger.error(f"Error checking file existence for {gcs_uri}: {e}")
247+
return False

backend/src/config/config_service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,4 +155,4 @@ def IMAGE_BUCKET(self) -> str:
155155

156156

157157
# Create a single, cached instance of the settings to be used throughout the app.
158-
config_service = ConfigService()
158+
config_service = ConfigService()

backend/src/galleries/dto/gallery_response_dto.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class MediaItemResponse(MediaItemModel):
4949
"""
5050

5151
presigned_urls: List[str] = []
52+
original_presigned_urls: Optional[List[str]] = None
5253
presigned_thumbnail_urls: Optional[List[str]] = []
5354
enriched_source_assets: Optional[List[SourceAssetLinkResponse]] = None
5455
enriched_source_media_items: Optional[List[SourceMediaItemLinkResponse]] = (

backend/src/galleries/gallery_controller.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from src.galleries.gallery_service import GalleryService
2222
from src.users.user_model import UserModel, UserRoleEnum
2323
from src.workspaces.repository.workspace_repository import WorkspaceRepository
24-
from src.workspaces.workspace_auth_guard import workspace_auth_service
24+
from src.workspaces.workspace_auth_guard import WorkspaceAuth
2525

2626
router = APIRouter(
2727
prefix="/api/gallery",
@@ -48,7 +48,7 @@ async def search_gallery_items(
4848
search_dto: GallerySearchDto,
4949
current_user: UserModel = Depends(get_current_user),
5050
service: GalleryService = Depends(),
51-
workspace_repo: WorkspaceRepository = Depends(),
51+
workspace_auth: WorkspaceAuth = Depends(),
5252
):
5353
"""
5454
Performs a paginated search for media items within a specific workspace.
@@ -58,10 +58,9 @@ async def search_gallery_items(
5858
"""
5959
# This dependency call acts as a gatekeeper. If the user is not authorized
6060
# for the workspace_id inside search_dto, it will raise an exception.
61-
await workspace_auth_service.authorize(
61+
await workspace_auth.authorize(
6262
workspace_id=search_dto.workspace_id,
6363
user=current_user,
64-
workspace_repo=workspace_repo,
6564
)
6665

6766
return await service.get_paginated_gallery(

backend/src/galleries/gallery_service.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,22 @@
3232
SourceAssetLinkResponse,
3333
SourceMediaItemLinkResponse,
3434
)
35+
from src.common.base_dto import (
36+
AspectRatioEnum,
37+
GenerationModelEnum,
38+
MimeTypeEnum,
39+
)
3540
from src.galleries.dto.gallery_search_dto import GallerySearchDto
41+
from src.images.dto.upscale_imagen_dto import UpscaleImagenDto
42+
from src.images.imagen_service import ImagenService
3643
from src.images.repository.media_item_repository import MediaRepository
3744
from src.source_assets.repository.source_asset_repository import (
3845
SourceAssetRepository,
3946
)
4047
from src.users.user_model import UserModel, UserRoleEnum
4148
from src.workspaces.repository.workspace_repository import WorkspaceRepository
4249
from src.workspaces.schema.workspace_model import WorkspaceScopeEnum
43-
from src.workspaces.workspace_auth_guard import workspace_auth_service
50+
from src.workspaces.workspace_auth_guard import WorkspaceAuth
4451

4552
logger = logging.getLogger(__name__)
4653

@@ -56,12 +63,16 @@ def __init__(
5663
source_asset_repo: SourceAssetRepository = Depends(),
5764
workspace_repo: WorkspaceRepository = Depends(),
5865
iam_signer_credentials: IamSignerCredentials = Depends(),
66+
workspace_auth: WorkspaceAuth = Depends(),
67+
imagen_service: ImagenService = Depends(),
5968
):
6069
"""Initializes the service with its dependencies."""
6170
self.media_repo = media_repo
6271
self.source_asset_repo = source_asset_repo
6372
self.workspace_repo = workspace_repo
6473
self.iam_signer_credentials = iam_signer_credentials
74+
self.workspace_auth = workspace_auth
75+
self.imagen_service = imagen_service
6576

6677
async def _enrich_source_asset_link(
6778
self, link: SourceAssetLink
@@ -70,7 +81,7 @@ async def _enrich_source_asset_link(
7081
Fetches the source asset document and generates a presigned URL for it.
7182
"""
7283
asset_doc = await self.source_asset_repo.get_by_id(link.asset_id)
73-
84+
7485
if not asset_doc:
7586
return None
7687

@@ -171,6 +182,16 @@ async def _create_gallery_response(
171182
if uri
172183
]
173184

185+
# 1.5 Create tasks for original media URLs
186+
all_original_gcs_uris = item.original_gcs_uris or []
187+
original_url_tasks = [
188+
asyncio.to_thread(
189+
self.iam_signer_credentials.generate_presigned_url, uri
190+
)
191+
for uri in all_original_gcs_uris
192+
if uri
193+
]
194+
174195
# 2. Create tasks for thumbnail URLs
175196
thumbnail_tasks = [
176197
asyncio.to_thread(
@@ -199,11 +220,13 @@ async def _create_gallery_response(
199220
# 5. Gather all results concurrently
200221
(
201222
presigned_urls,
223+
original_presigned_urls,
202224
presigned_thumbnail_urls,
203225
enriched_source_assets_with_nones,
204226
enriched_source_media_items_with_nones,
205227
) = await asyncio.gather(
206228
asyncio.gather(*main_url_tasks),
229+
asyncio.gather(*original_url_tasks),
207230
asyncio.gather(*thumbnail_tasks),
208231
asyncio.gather(*source_asset_tasks),
209232
asyncio.gather(*source_media_item_tasks),
@@ -220,6 +243,7 @@ async def _create_gallery_response(
220243
return MediaItemResponse(
221244
**item.model_dump(exclude={"source_assets"}),
222245
presigned_urls=presigned_urls,
246+
original_presigned_urls=original_presigned_urls,
223247
presigned_thumbnail_urls=presigned_thumbnail_urls,
224248
enriched_source_assets=enriched_source_assets or None,
225249
enriched_source_media_items=enriched_source_media_items or None,
@@ -259,7 +283,7 @@ async def get_paginated_gallery(
259283
)
260284

261285
async def get_media_by_id(
262-
self, item_id: int, current_user: UserModel
286+
self, item_id: str, current_user: UserModel
263287
) -> Optional[MediaItemResponse]:
264288
"""
265289
Retrieves a single media item, performs an authorization check,
@@ -282,10 +306,13 @@ async def get_media_by_id(
282306
)
283307

284308
# Use the centralized authorization logic
285-
await workspace_auth_service.authorize(
309+
await self.workspace_auth.authorize(
286310
workspace_id=item.workspace_id,
287311
user=current_user,
288-
workspace_repo=self.workspace_repo,
289312
)
290313

291314
return await self._create_gallery_response(item)
315+
316+
317+
318+

0 commit comments

Comments
 (0)