Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
645d336
added imagen upscale
kbaljeet-byte Jan 19, 2026
840d29b
chore: resolve merge conflicts with develop branch
kbaljeet-byte Jan 20, 2026
0ba430a
DM migration added, small fixes in place
kbaljeet-byte Jan 29, 2026
27467cc
chore: resolve merge conflicts with develop branch
kbaljeet-byte Jan 29, 2026
8b9bbcf
feat: Implement image thumbnail generation for generated media items …
MauroCominotti Jan 29, 2026
2a904e3
refactor: comment out the video duration on video component
MauroCominotti Jan 29, 2026
fa93196
Merge branch 'feature/imagen-upscale' of github.com:GoogleCloudPlatfo…
MauroCominotti Jan 29, 2026
ffe146f
fix: rollback changes to match develop branch
MauroCominotti Jan 30, 2026
769e4d1
fix: rollback changes to match develop branch
MauroCominotti Jan 30, 2026
da74890
Merge branch 'develop' of github.com:GoogleCloudPlatform/gcc-creative…
MauroCominotti Jan 30, 2026
76254f5
refactor: Remove upscale image dialog component, associated job track…
MauroCominotti Jan 30, 2026
e36a18d
feat: refactor image upscale functionality from cropper dialog to the…
MauroCominotti Jan 30, 2026
ee792c7
feat: Implement enhance input image and image preservation factor opt…
MauroCominotti Jan 30, 2026
9345105
feat: Implement thumbnail generation and generation time tracking for…
MauroCominotti Jan 30, 2026
09bf103
Merge branch 'develop' of github.com:GoogleCloudPlatform/gcc-creative…
MauroCominotti Jan 30, 2026
8865afc
feat: Add Google LLC copyright headers to all files
MauroCominotti Jan 30, 2026
8f5e418
feat: Add support for audio asset uploads and dynamic aspect ratio ha…
MauroCominotti Jan 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions backend/alembic/versions/0bd50a4bf20c_add_original_gcs_uris.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""add_original_gcs_uris

Revision ID: 0bd50a4bf20c
Revises: 9393a3d298c6
Create Date: 2026-01-29 12:53:26.493393

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '0bd50a4bf20c'
down_revision: Union[str, None] = '9393a3d298c6'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)

media_columns = [c['name'] for c in inspector.get_columns('media_items')]
if 'original_gcs_uris' not in media_columns:
op.add_column('media_items', sa.Column('original_gcs_uris', sa.ARRAY(sa.String()), nullable=True))

source_columns = [c['name'] for c in inspector.get_columns('source_assets')]
if 'original_gcs_uri' not in source_columns:
op.add_column('source_assets', sa.Column('original_gcs_uri', sa.String(), nullable=True))


def downgrade() -> None:
op.drop_column('source_assets', 'original_gcs_uri')
op.drop_column('media_items', 'original_gcs_uris')

14 changes: 14 additions & 0 deletions backend/alembic/versions/9393a3d298c6_add_workflow_runs_table.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""add_workflow_runs_table

Revision ID: 9393a3d298c6
Expand Down
47 changes: 0 additions & 47 deletions backend/src/auth/firebase_client_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,6 @@ def __init__(self):
)
raise RuntimeError(f"Failed to initialize Firebase Admin SDK: {e}")

db_name = config_service.FIREBASE_DB
logger.info(f"Connecting to Firestore database: '{db_name}'")

def check_adc_authentication(self):
"""
Checks if Application Default Credentials (ADC) are valid by making a
Expand Down Expand Up @@ -128,47 +125,3 @@ def check_adc_authentication(self):


firebase_client = FirebaseClient()



def create_firebase_user(email: str, password: str):
try:
user_record = auth.create_user(email=email, password=password)
return user_record
except auth.EmailAlreadyExistsError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered with Firebase.",
)
except Exception as e:
logger.error(
f"Error creating Firebase user {email}: {e}", exc_info=True
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Could not create user in Firebase.", # Avoid leaking raw error details to client
)


def verify_firebase_token(id_token: str):
try:
decoded_token = auth.verify_id_token(id_token)
return decoded_token
except auth.ExpiredIdTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Firebase ID token has expired.",
)
except auth.InvalidIdTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Firebase ID token.",
)
except Exception as e:
logger.error(
f"Unexpected error verifying Firebase token: {e}", exc_info=True
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Could not process token.",
)
7 changes: 3 additions & 4 deletions backend/src/brand_guidelines/brand_guideline_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
GenerateUploadUrlResponseDto,
)
from src.workspaces.repository.workspace_repository import WorkspaceRepository
from src.workspaces.workspace_auth_guard import workspace_auth_service
from src.workspaces.workspace_auth_guard import WorkspaceAuth
from src.users.user_model import UserModel, UserRoleEnum

MAX_UPLOAD_SIZE_BYTES = 500 * 1024 * 1024 # 500 MB
Expand All @@ -62,18 +62,17 @@ async def generate_upload_url(
request_dto: GenerateUploadUrlDto,
current_user: UserModel = Depends(get_current_user),
service: BrandGuidelineService = Depends(),
workspace_repo: WorkspaceRepository = Depends(),
workspace_auth: WorkspaceAuth = Depends(),
):
"""
Generates a secure, short-lived URL that the client can use to upload a
brand guideline PDF directly to Google Cloud Storage.
"""
# If a workspace ID is provided, ensure the user has access to it.
if request_dto.workspace_id:
await workspace_auth_service.authorize(
await workspace_auth.authorize(
workspace_id=request_dto.workspace_id,
user=current_user,
workspace_repo=workspace_repo,
)

if not request_dto.content_type == "application/pdf":
Expand Down
1 change: 1 addition & 0 deletions backend/src/common/base_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class GenerationModelEnum(str, Enum):
"""Enum representing the available generation models."""

# Image-Specific Models
IMAGEN_4_UPSCALE_PREVIEW = "imagen-4.0-upscale-preview"
IMAGEN_4_001 = "imagen-4.0-generate-001"
IMAGEN_4_ULTRA = "imagen-4.0-ultra-generate-001"
IMAGEN_4_ULTRA_PREVIEW = "imagen-4.0-ultra-generate-preview-06-06"
Expand Down
83 changes: 83 additions & 0 deletions backend/src/common/media_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,98 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import io
import json
import logging
import os
import pathlib
import subprocess
from typing import List, Tuple

from PIL import Image as PILImage

from src.common.storage_service import GcsService

logger = logging.getLogger(__name__)




def generate_image_thumbnail_bytes(image_bytes: bytes, mime_type: str) -> bytes | None:
"""
Generates a thumbnail from image bytes using PIL.

Args:
image_bytes: The raw bytes of the image.
mime_type: The mime type of the image (e.g., 'image/png', 'image/jpeg').

Returns:
The raw bytes of the generated thumbnail, or None if it fails.
"""
try:
with PILImage.open(io.BytesIO(image_bytes)) as img:
# Convert to RGB if RGBA and saving as JPEG
if mime_type == "image/jpeg" and img.mode == "RGBA":
img = img.convert("RGB")

img.thumbnail((512, 512))
output = io.BytesIO()

format_to_save = "PNG" if mime_type == "image/png" else "JPEG"
img.save(output, format=format_to_save, optimize=True)
return output.getvalue()
except Exception as e:
logger.error(f"Error generating image thumbnail: {e}")
return None


def generate_image_thumbnail_from_gcs(
gcs_service: GcsService,
gcs_uri: str,
mime_type: str
) -> str | None:
"""
Generates a thumbnail for the given GCS URI and uploads it.

Args:
gcs_service: The GcsService instance to use for download/upload.
gcs_uri: The GCS URI of the source image.
mime_type: The mime type of the image.

Returns:
The GCS URI of the generated thumbnail, or None if it fails.
"""
try:
image_bytes = gcs_service.download_bytes_from_gcs(gcs_uri)
if not image_bytes:
return None

thumbnail_bytes = generate_image_thumbnail_bytes(image_bytes, mime_type)
if not thumbnail_bytes:
return None

if not gcs_uri.startswith("gs://"):
return None

# gs://bucket/blob_name
parts = gcs_uri.split("/", 3) # gs:, , bucket, blob_name
if len(parts) < 4:
return None
blob_name = parts[3]

path = pathlib.Path(blob_name)
# Use simple string manipulation to avoid path issues on different OS if needed, pathlib is generally fine.
new_blob_name = str(path.parent / f"{path.stem}_thumbnail{path.suffix}")
if path.parent == pathlib.Path("."):
new_blob_name = f"{path.stem}_thumbnail{path.suffix}"

return gcs_service.upload_bytes_to_gcs(thumbnail_bytes, new_blob_name, mime_type)

except Exception as e:
logger.error(f"Thumbnail generation failed: {e}")
return None


def generate_thumbnail(video_path: str) -> str | None:
"""
Generates a thumbnail from a video file using ffmpeg.
Expand Down
11 changes: 10 additions & 1 deletion backend/src/common/schema/media_item_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ class MediaItem(Base):
num_media: Mapped[Optional[int]] = mapped_column(Integer, nullable=True)
generation_time: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
error_message: Mapped[Optional[str]] = mapped_column(String, nullable=True)
thumbnail_uris: Mapped[List[str]] = mapped_column(ARRAY(String), default=[])

# Enums
aspect_ratio: Mapped[AspectRatioEnum] = mapped_column(String, nullable=False)
Expand All @@ -164,10 +165,10 @@ class MediaItem(Base):
source_media_items: Mapped[Optional[List[dict]]] = mapped_column(JSONB, nullable=True)

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

# Video specific
duration_seconds: Mapped[Optional[float]] = mapped_column(Float, nullable=True)
thumbnail_uris: Mapped[List[str]] = mapped_column(ARRAY(String), default=[])
comment: Mapped[Optional[str]] = mapped_column(String, nullable=True)

# Image specific
Expand Down Expand Up @@ -253,6 +254,14 @@ class MediaItemModel(BaseDocument):
description="A list of public URLs for the media to be displayed (e.g., video or image).",
),
]
original_gcs_uris: Annotated[
Optional[List[str]],
Field(
default=None,
min_length=0,
description="A list of public URLs (original / non-upscaled) for the media to be displayed (e.g., video or image).",
),
]

# Video specific
duration_seconds: Optional[float] = None
Expand Down
2 changes: 1 addition & 1 deletion backend/src/common/storage_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,4 @@ def store_to_gcs(
logger.error(
f"Failed to download '{destination_blob_name}' from GCS: {e}"
)
return None
return None
3 changes: 0 additions & 3 deletions backend/src/config/config_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,6 @@ class ConfigService(BaseSettings):
GEMINI_MODEL_ID: str = "gemini-2.5-pro"
GEMINI_AUDIO_ANALYSIS_MODEL_ID: str = "gemini-2.5-pro"

# --- Collections ---
FIREBASE_DB: str = "cstudio-development"

# --- Database Configuration ---
INSTANCE_CONNECTION_NAME: str = ""
DB_USER: str = "postgres"
Expand Down
1 change: 1 addition & 0 deletions backend/src/galleries/dto/gallery_response_dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ class MediaItemResponse(MediaItemModel):
"""

presigned_urls: List[str] = []
original_presigned_urls: Optional[List[str]] = None
presigned_thumbnail_urls: Optional[List[str]] = []
enriched_source_assets: Optional[List[SourceAssetLinkResponse]] = None
enriched_source_media_items: Optional[List[SourceMediaItemLinkResponse]] = (
Expand Down
7 changes: 3 additions & 4 deletions backend/src/galleries/gallery_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from src.galleries.gallery_service import GalleryService
from src.users.user_model import UserModel, UserRoleEnum
from src.workspaces.repository.workspace_repository import WorkspaceRepository
from src.workspaces.workspace_auth_guard import workspace_auth_service
from src.workspaces.workspace_auth_guard import WorkspaceAuth

router = APIRouter(
prefix="/api/gallery",
Expand All @@ -48,7 +48,7 @@ async def search_gallery_items(
search_dto: GallerySearchDto,
current_user: UserModel = Depends(get_current_user),
service: GalleryService = Depends(),
workspace_repo: WorkspaceRepository = Depends(),
workspace_auth: WorkspaceAuth = Depends(),
):
"""
Performs a paginated search for media items within a specific workspace.
Expand All @@ -58,10 +58,9 @@ async def search_gallery_items(
"""
# This dependency call acts as a gatekeeper. If the user is not authorized
# for the workspace_id inside search_dto, it will raise an exception.
await workspace_auth_service.authorize(
await workspace_auth.authorize(
workspace_id=search_dto.workspace_id,
user=current_user,
workspace_repo=workspace_repo,
)

return await service.get_paginated_gallery(
Expand Down
Loading