AI Image Studio, AI podcast Maker, AI product Marketing

This commit is contained in:
ajaysi
2025-11-28 14:33:52 +05:30
parent 77d7c0cde6
commit 49e2131715
122 changed files with 22311 additions and 4331 deletions

View File

@@ -51,7 +51,7 @@ def save_asset_to_library(
description: Optional[str] = None,
prompt: Optional[str] = None,
tags: Optional[list] = None,
metadata: Optional[Dict[str, Any]] = None,
asset_metadata: Optional[Dict[str, Any]] = None,
provider: Optional[str] = None,
model: Optional[str] = None,
cost: Optional[float] = None,
@@ -77,7 +77,7 @@ def save_asset_to_library(
description: Asset description (optional)
prompt: Generation prompt (optional)
tags: List of tags (optional)
metadata: Additional metadata (optional)
asset_metadata: Additional metadata (optional)
provider: AI provider used (optional)
model: Model used (optional)
cost: Generation cost (optional)
@@ -143,7 +143,7 @@ def save_asset_to_library(
description=description,
prompt=prompt,
tags=tags or [],
metadata=metadata or {},
asset_metadata=asset_metadata or {},
provider=provider,
model=model,
cost=cost,

View File

@@ -0,0 +1,246 @@
"""
File Storage Utility
Robust file storage helper for saving generated content assets.
"""
import os
import uuid
from pathlib import Path
from typing import Optional, Tuple
import logging
logger = logging.getLogger(__name__)
# Maximum filename length
MAX_FILENAME_LENGTH = 255
# Allowed characters in filenames (alphanumeric, dash, underscore, dot)
ALLOWED_FILENAME_CHARS = set('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.')
def sanitize_filename(filename: str, max_length: int = 100) -> str:
"""
Sanitize filename to be filesystem-safe.
Args:
filename: Original filename
max_length: Maximum length for filename
Returns:
Sanitized filename
"""
if not filename:
return f"file_{uuid.uuid4().hex[:8]}"
# Remove path separators and other dangerous characters
sanitized = "".join(c if c in ALLOWED_FILENAME_CHARS else '_' for c in filename)
# Remove leading/trailing dots and spaces
sanitized = sanitized.strip('. ')
# Ensure it's not empty
if not sanitized:
sanitized = f"file_{uuid.uuid4().hex[:8]}"
# Truncate if too long
if len(sanitized) > max_length:
name, ext = os.path.splitext(sanitized)
max_name_length = max_length - len(ext) - 1
sanitized = name[:max_name_length] + ext
return sanitized
def ensure_directory_exists(directory: Path) -> bool:
"""
Ensure directory exists, creating it if necessary.
Args:
directory: Path to directory
Returns:
True if directory exists or was created, False otherwise
"""
try:
directory.mkdir(parents=True, exist_ok=True)
return True
except Exception as e:
logger.error(f"Failed to create directory {directory}: {e}")
return False
def save_file_safely(
content: bytes,
directory: Path,
filename: str,
max_file_size: int = 100 * 1024 * 1024 # 100MB default
) -> Tuple[Optional[Path], Optional[str]]:
"""
Safely save file content to disk.
Args:
content: File content as bytes
directory: Directory to save file in
filename: Filename (will be sanitized)
max_file_size: Maximum allowed file size in bytes
Returns:
Tuple of (file_path, error_message). file_path is None on error.
"""
try:
# Validate file size
if len(content) > max_file_size:
return None, f"File size {len(content)} exceeds maximum {max_file_size}"
if len(content) == 0:
return None, "File content is empty"
# Ensure directory exists
if not ensure_directory_exists(directory):
return None, f"Failed to create directory: {directory}"
# Sanitize filename
safe_filename = sanitize_filename(filename)
# Construct full path
file_path = directory / safe_filename
# Check if file already exists (unlikely with UUID, but check anyway)
if file_path.exists():
# Add UUID to make it unique
name, ext = os.path.splitext(safe_filename)
safe_filename = f"{name}_{uuid.uuid4().hex[:8]}{ext}"
file_path = directory / safe_filename
# Write file atomically (write to temp file first, then rename)
temp_path = file_path.with_suffix(file_path.suffix + '.tmp')
try:
with open(temp_path, 'wb') as f:
f.write(content)
# Atomic rename
temp_path.replace(file_path)
logger.info(f"Successfully saved file: {file_path} ({len(content)} bytes)")
return file_path, None
except Exception as write_error:
# Clean up temp file if it exists
if temp_path.exists():
try:
temp_path.unlink()
except:
pass
raise write_error
except Exception as e:
logger.error(f"Error saving file: {e}", exc_info=True)
return None, str(e)
def generate_unique_filename(
prefix: str,
extension: str = ".png",
include_uuid: bool = True
) -> str:
"""
Generate a unique filename.
Args:
prefix: Filename prefix
extension: File extension (with or without dot)
include_uuid: Whether to include UUID in filename
Returns:
Unique filename
"""
if not extension.startswith('.'):
extension = '.' + extension
prefix = sanitize_filename(prefix, max_length=50)
if include_uuid:
unique_id = uuid.uuid4().hex[:8]
return f"{prefix}_{unique_id}{extension}"
else:
return f"{prefix}{extension}"
def save_text_file_safely(
content: str,
directory: Path,
filename: str,
encoding: str = 'utf-8',
max_file_size: int = 10 * 1024 * 1024 # 10MB default for text
) -> Tuple[Optional[Path], Optional[str]]:
"""
Safely save text content to disk.
Args:
content: Text content as string
directory: Directory to save file in
filename: Filename (will be sanitized)
encoding: Text encoding (default: utf-8)
max_file_size: Maximum allowed file size in bytes
Returns:
Tuple of (file_path, error_message). file_path is None on error.
"""
try:
# Validate content
if not content or not isinstance(content, str):
return None, "Content must be a non-empty string"
# Convert to bytes for size check
content_bytes = content.encode(encoding)
# Validate file size
if len(content_bytes) > max_file_size:
return None, f"File size {len(content_bytes)} exceeds maximum {max_file_size}"
# Ensure directory exists
if not ensure_directory_exists(directory):
return None, f"Failed to create directory: {directory}"
# Sanitize filename
safe_filename = sanitize_filename(filename)
# Ensure .txt extension if not present
if not safe_filename.endswith(('.txt', '.md', '.json')):
safe_filename = os.path.splitext(safe_filename)[0] + '.txt'
# Construct full path
file_path = directory / safe_filename
# Check if file already exists
if file_path.exists():
# Add UUID to make it unique
name, ext = os.path.splitext(safe_filename)
safe_filename = f"{name}_{uuid.uuid4().hex[:8]}{ext}"
file_path = directory / safe_filename
# Write file atomically (write to temp file first, then rename)
temp_path = file_path.with_suffix(file_path.suffix + '.tmp')
try:
with open(temp_path, 'w', encoding=encoding) as f:
f.write(content)
# Atomic rename
temp_path.replace(file_path)
logger.info(f"Successfully saved text file: {file_path} ({len(content_bytes)} bytes, {len(content)} chars)")
return file_path, None
except Exception as write_error:
# Clean up temp file if it exists
if temp_path.exists():
try:
temp_path.unlink()
except:
pass
raise write_error
except Exception as e:
logger.error(f"Error saving text file: {e}", exc_info=True)
return None, str(e)

View File

@@ -0,0 +1,133 @@
"""
Text Asset Tracker Utility
Helper utility for saving and tracking text content as files in the asset library.
"""
from typing import Dict, Any, Optional
from pathlib import Path
from sqlalchemy.orm import Session
from utils.asset_tracker import save_asset_to_library
from utils.file_storage import save_text_file_safely, generate_unique_filename, sanitize_filename
import logging
logger = logging.getLogger(__name__)
def save_and_track_text_content(
db: Session,
user_id: str,
content: str,
source_module: str,
title: str,
description: Optional[str] = None,
prompt: Optional[str] = None,
tags: Optional[list] = None,
asset_metadata: Optional[Dict[str, Any]] = None,
base_dir: Optional[Path] = None,
subdirectory: Optional[str] = None,
file_extension: str = ".txt"
) -> Optional[int]:
"""
Save text content to disk and track it in the asset library.
Args:
db: Database session
user_id: Clerk user ID
content: Text content to save
source_module: Source module name (e.g., "linkedin_writer", "facebook_writer")
title: Title for the asset
description: Description of the content
prompt: Original prompt used for generation
tags: List of tags for search/filtering
asset_metadata: Additional metadata
base_dir: Base directory for file storage (defaults to backend/{module}_text)
subdirectory: Optional subdirectory (e.g., "posts", "articles")
file_extension: File extension (.txt, .md, etc.)
Returns:
Asset ID if successful, None otherwise
"""
try:
if not content or not isinstance(content, str) or len(content.strip()) == 0:
logger.warning("Empty or invalid content provided")
return None
if not user_id or not isinstance(user_id, str):
logger.error("Invalid user_id provided")
return None
# Determine output directory
if base_dir is None:
# Default to backend/{module}_text
base_dir = Path(__file__).parent.parent
module_name = source_module.replace('_', '')
output_dir = base_dir / f"{module_name}_text"
else:
output_dir = base_dir
# Add subdirectory if specified
if subdirectory:
output_dir = output_dir / subdirectory
# Generate safe filename from title
safe_title = sanitize_filename(title, max_length=80)
filename = generate_unique_filename(
prefix=safe_title,
extension=file_extension,
include_uuid=True
)
# Save text file
file_path, save_error = save_text_file_safely(
content=content,
directory=output_dir,
filename=filename,
encoding='utf-8',
max_file_size=10 * 1024 * 1024 # 10MB for text
)
if not file_path or save_error:
logger.error(f"Failed to save text file: {save_error}")
return None
# Generate file URL
relative_path = file_path.relative_to(base_dir)
file_url = f"/api/text-assets/{relative_path.as_posix()}"
# Prepare metadata
final_metadata = asset_metadata or {}
final_metadata.update({
"status": "completed",
"character_count": len(content),
"word_count": len(content.split())
})
# Save to asset library
asset_id = save_asset_to_library(
db=db,
user_id=user_id,
asset_type="text",
source_module=source_module,
filename=filename,
file_url=file_url,
file_path=str(file_path),
file_size=len(content.encode('utf-8')),
mime_type="text/plain" if file_extension == ".txt" else "text/markdown",
title=title,
description=description or f"Generated {source_module.replace('_', ' ')} content",
prompt=prompt,
tags=tags or [source_module, "text"],
asset_metadata=final_metadata
)
if asset_id:
logger.info(f"✅ Text asset saved to library: ID={asset_id}, filename={filename}")
else:
logger.warning(f"Asset tracking returned None for {filename}")
return asset_id
except Exception as e:
logger.error(f"❌ Error saving and tracking text content: {str(e)}", exc_info=True)
return None