Skip to content

Commit 12b022f

Browse files
authored
Fix video experience (#9)
* feat: enhance video upload functionality with folder support and improve tag generation logic * feat: implement folder context for improved folder management and refresh functionality * feat: enhance image filename generation and normalization for improved file handling * refactor: update video and image handling to use unique names for deletion and improve toast notifications * refactor: remove unnecessary logging statements in image and video processing for cleaner code
1 parent be4b04c commit 12b022f

File tree

16 files changed

+351
-369
lines changed

16 files changed

+351
-369
lines changed

backend/api/endpoints/images.py

Lines changed: 126 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import tempfile
1212
import os
1313
import uuid
14+
from pathlib import Path
1415

1516
from backend.models.images import (
1617
ImageGenerationRequest,
@@ -47,6 +48,83 @@
4748
logger = logging.getLogger(__name__)
4849

4950

51+
def normalize_filename(filename: str) -> str:
52+
"""
53+
Normalize a filename to be safe for file systems.
54+
55+
Args:
56+
filename: The filename to normalize
57+
58+
Returns:
59+
A normalized filename safe for most file systems
60+
"""
61+
if not filename:
62+
return filename
63+
64+
# Use pathlib to handle the filename safely
65+
path = Path(filename)
66+
67+
# Get the stem (filename without extension) and suffix (extension)
68+
stem = path.stem
69+
suffix = path.suffix
70+
71+
# Remove or replace invalid characters for most filesystems
72+
# Keep alphanumeric, hyphens, underscores, and dots
73+
stem = re.sub(r'[^a-zA-Z0-9_\-.]', '_', stem)
74+
75+
# Remove multiple consecutive underscores
76+
stem = re.sub(r'_+', '_', stem)
77+
78+
# Remove leading/trailing underscores and dots
79+
stem = stem.strip('_.')
80+
81+
# Ensure the filename isn't empty
82+
if not stem:
83+
stem = "generated_image"
84+
85+
# Reconstruct the filename
86+
normalized = f"{stem}{suffix}" if suffix else stem
87+
88+
# Ensure the filename isn't too long (most filesystems support 255 chars)
89+
if len(normalized) > 200: # Leave some room for additional suffixes
90+
# Truncate the stem but keep the extension
91+
max_stem_length = 200 - len(suffix)
92+
stem = stem[:max_stem_length]
93+
normalized = f"{stem}{suffix}" if suffix else stem
94+
95+
return normalized
96+
97+
98+
async def generate_filename_for_prompt(prompt: str, extension: str = None) -> str:
99+
"""
100+
Generate a filename using the existing filename generation endpoint.
101+
102+
Args:
103+
prompt: The prompt used for image generation
104+
extension: File extension (e.g., '.png', '.jpg')
105+
106+
Returns:
107+
Generated filename or None if generation fails
108+
"""
109+
try:
110+
# Create request for filename generation
111+
filename_request = ImageFilenameGenerateRequest(
112+
prompt=prompt,
113+
extension=extension
114+
)
115+
116+
# Call the filename generation function directly
117+
filename_response = generate_image_filename(filename_request)
118+
119+
# Normalize the generated filename
120+
generated_filename = normalize_filename(filename_response.filename)
121+
122+
return generated_filename
123+
124+
except Exception as e:
125+
return None
126+
127+
50128
@router.post("/generate", response_model=ImageGenerationResponse)
51129
async def generate_image(request: ImageGenerationRequest):
52130
"""Generate an image based on the provided prompt and settings"""
@@ -74,9 +152,6 @@ async def generate_image(request: ImageGenerationRequest):
74152
if request.user:
75153
params["user"] = request.user
76154

77-
logger.info(
78-
f"Generating image with gpt-image-1, quality: {request.quality}, size: {request.size}")
79-
80155
# Generate image
81156
response = dalle_client.generate_image(**params)
82157

@@ -99,10 +174,6 @@ async def generate_image(request: ImageGenerationRequest):
99174
input_tokens_details=input_tokens_details
100175
)
101176

102-
# Log token usage for cost tracking
103-
logger.info(
104-
f"Token usage - Total: {token_usage.total_tokens}, Input: {token_usage.input_tokens}, Output: {token_usage.output_tokens}")
105-
106177
return ImageGenerationResponse(
107178
success=True,
108179
message="Refer to the imgen_model_response for details",
@@ -145,19 +216,12 @@ async def edit_image(request: ImageEditRequest):
145216
if request.user:
146217
params["user"] = request.user
147218

148-
# Log information about multiple images if applicable
219+
# Check if organization is verified when using multiple images
149220
if isinstance(request.image, list):
150221
image_count = len(request.image)
151-
logger.info(
152-
f"Editing with {image_count} reference images using gpt-image-1, quality: {request.quality}, size: {request.size}")
153-
154-
# Check if organization is verified when using multiple images
155222
if image_count > 1 and not settings.OPENAI_ORG_VERIFIED:
156223
logger.warning(
157224
"Using multiple reference images requires organization verification")
158-
else:
159-
logger.info(
160-
f"Editing single image using gpt-image-1, quality: {request.quality}, size: {request.size}")
161225

162226
# Perform image editing
163227
response = dalle_client.edit_image(**params)
@@ -209,10 +273,6 @@ async def edit_image_upload(
209273
):
210274
"""Edit input images uploaded via multipart form data"""
211275
try:
212-
# Log request info
213-
logger.info(
214-
f"Received {len(image)} image(s) for editing with prompt: {prompt}")
215-
216276
# Validate file size for all images
217277
max_file_size_mb = settings.GPT_IMAGE_MAX_FILE_SIZE_MB
218278
temp_files = []
@@ -477,10 +537,32 @@ async def save_generated_images(
477537
# Reset file pointer
478538
img_file.seek(0)
479539

480-
# Create filename
481-
quality_suffix = f"_{request.quality}" if request.model == "gpt-image-1" and hasattr(
482-
request, "quality") else ""
483-
filename = f"generated_image_{idx+1}{quality_suffix}.{img_format.lower()}"
540+
# Generate intelligent filename using the existing endpoint
541+
if request.prompt:
542+
filename = await generate_filename_for_prompt(
543+
request.prompt,
544+
f".{img_format.lower()}"
545+
)
546+
547+
# Add index suffix for multiple images
548+
if filename and len(images_data) > 1:
549+
# Insert index before the extension
550+
path = Path(filename)
551+
stem = path.stem
552+
suffix = path.suffix
553+
filename = f"{stem}_{idx+1}{suffix}"
554+
logger.info(
555+
f"Using generated filename with index: {filename}")
556+
elif filename:
557+
logger.info(f"Using generated filename: {filename}")
558+
559+
# Fallback to default naming if filename generation fails
560+
if not filename:
561+
quality_suffix = f"_{request.quality}" if request.model == "gpt-image-1" and hasattr(
562+
request, "quality") else ""
563+
filename = f"generated_image_{idx+1}{quality_suffix}.{img_format.lower()}"
564+
filename = normalize_filename(filename)
565+
logger.info(f"Using fallback filename: {filename}")
484566

485567
elif "url" in img_data:
486568
# Download image from URL
@@ -507,10 +589,27 @@ async def save_generated_images(
507589
# Reset file pointer
508590
img_file.seek(0)
509591

510-
# Create filename
511-
quality_suffix = f"_{request.quality}" if request.model == "gpt-image-1" and hasattr(
512-
request, "quality") else ""
513-
filename = f"generated_image_{idx+1}{quality_suffix}.{ext}"
592+
# Generate intelligent filename using the existing endpoint
593+
if request.prompt:
594+
filename = await generate_filename_for_prompt(
595+
request.prompt,
596+
f".{ext}"
597+
)
598+
599+
# Add index suffix for multiple images
600+
if filename and len(images_data) > 1:
601+
# Insert index before the extension
602+
path = Path(filename)
603+
stem = path.stem
604+
suffix = path.suffix
605+
filename = f"{stem}_{idx+1}{suffix}"
606+
607+
# Fallback to default naming if filename generation fails
608+
if not filename:
609+
quality_suffix = f"_{request.quality}" if request.model == "gpt-image-1" and hasattr(
610+
request, "quality") else ""
611+
filename = f"generated_image_{idx+1}{quality_suffix}.{ext}"
612+
filename = normalize_filename(filename)
514613
else:
515614
logger.warning(
516615
f"Unsupported image data format for image {idx+1}")
@@ -627,7 +726,6 @@ def analyze_image(req: ImageAnalyzeRequest):
627726
file_path += f"?{image_sas_token}"
628727

629728
# Download the image from the URL
630-
logger.info(f"Downloading image for analysis from: {file_path}")
631729
response = requests.get(file_path, timeout=30)
632730
if response.status_code != 200:
633731
raise HTTPException(
@@ -640,7 +738,6 @@ def analyze_image(req: ImageAnalyzeRequest):
640738

641739
# Option 2: Process from base64 string
642740
elif req.base64_image:
643-
logger.info("Processing image from base64 data")
644741
try:
645742
# Decode base64 to binary
646743
image_content = base64.b64decode(req.base64_image)
@@ -658,8 +755,6 @@ def analyze_image(req: ImageAnalyzeRequest):
658755
has_transparency = img.mode == 'RGBA' and 'A' in img.getbands()
659756

660757
if has_transparency:
661-
logger.info(
662-
"Image has transparency, converting for analysis")
663758
# Create a white background
664759
background = Image.new(
665760
'RGBA', img.size, (255, 255, 255, 255))
@@ -678,8 +773,6 @@ def analyze_image(req: ImageAnalyzeRequest):
678773
# This is optional but can help with very large images
679774
width, height = img.size
680775
if width > 1500 or height > 1500:
681-
logger.info(
682-
f"Image is large ({width}x{height}), resizing for analysis")
683776
# Calculate new dimensions
684777
max_dimension = 1500
685778
if width > height:
@@ -713,7 +806,6 @@ def analyze_image(req: ImageAnalyzeRequest):
713806
image_base64 = re.sub(r"^data:image/.+;base64,", "", image_base64)
714807

715808
# analyze the image using the LLM
716-
logger.info("Sending image to LLM for analysis")
717809
image_analyzer = ImageAnalyzer(llm_client, settings.LLM_DEPLOYMENT)
718810
insights = image_analyzer.image_chat(
719811
image_base64, analyze_image_system_message)
@@ -774,17 +866,12 @@ def protect_image_prompt(req: ImagePromptBrandProtectionRequest):
774866
try:
775867
if req.brands_to_protect:
776868
if req.protection_mode == "replace":
777-
logger.info(
778-
f"Replace competitor brands of: {req.brands_to_protect}")
779869
system_message = brand_protect_replace_msg.format(
780870
brands=req.brands_to_protect)
781871
elif req.protection_mode == "neutralize":
782-
logger.info(
783-
f"Neutralize competitor brands of: {req.brands_to_protect}")
784872
system_message = brand_protect_neutralize_msg.format(
785873
brands=req.brands_to_protect)
786874
else:
787-
logger.info(f"No brand protection specified.")
788875
return ImagePromptBrandProtectionResponse(enhanced_prompt=req.original_prompt)
789876

790877
# Ensure LLM client is available

backend/api/endpoints/videos.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -280,12 +280,28 @@ def create_video_generation_with_analysis(req: VideoGenerationWithAnalysisReques
280280
from backend.core.azure_storage import AzureBlobStorageService
281281
from azure.storage.blob import ContentSettings
282282

283-
# Generate the final filename for gallery
284-
final_filename = f"{req.prompt.replace(' ', '_')}_{generation_id}.mp4"
285-
286283
# Create Azure storage service
287284
azure_service = AzureBlobStorageService()
288285

286+
# Generate the base filename
287+
base_filename = f"{req.prompt.replace(' ', '_')}_{generation_id}.mp4"
288+
289+
# Extract folder path from request metadata and normalize it
290+
folder_path = req.metadata.get(
291+
'folder') if req.metadata else None
292+
final_filename = base_filename
293+
294+
if folder_path and folder_path != 'root':
295+
# Use Azure service's normalize_folder_path method for consistency
296+
normalized_folder = azure_service.normalize_folder_path(
297+
folder_path)
298+
final_filename = f"{normalized_folder}{base_filename}"
299+
logger.info(
300+
f"Uploading video to folder: {normalized_folder}")
301+
else:
302+
logger.info(
303+
"Uploading video to root directory")
304+
289305
# Upload to Azure Blob Storage
290306
container_client = azure_service.blob_service_client.get_container_client(
291307
"videos")
@@ -305,6 +321,11 @@ def create_video_generation_with_analysis(req: VideoGenerationWithAnalysisReques
305321
"upload_date": datetime.now().isoformat()
306322
}
307323

324+
# Add folder path to metadata if specified
325+
if folder_path and folder_path != 'root':
326+
upload_metadata["folder_path"] = azure_service.normalize_folder_path(
327+
folder_path)
328+
308329
# Read the file and upload with metadata
309330
with open(downloaded_path, 'rb') as video_file:
310331
blob_client.upload_blob(
@@ -318,6 +339,9 @@ def create_video_generation_with_analysis(req: VideoGenerationWithAnalysisReques
318339
blob_url = blob_client.url
319340
logger.info(
320341
f"Uploaded video to gallery: {blob_url}")
342+
if folder_path and folder_path != 'root':
343+
logger.info(
344+
f"Video uploaded to folder '{folder_path}' with normalized path '{azure_service.normalize_folder_path(folder_path)}'")
321345

322346
except Exception as upload_error:
323347
logger.warning(

0 commit comments

Comments
 (0)