/dev/null || true
local image_file="${web_dir}${web_image_name}"
# Also save a copy in temp for debugging
local temp_image_file="${TEMP_DIR}/${blog_id}_image_${image_index}.jpg"
log_info "[$image_index/$images_to_generate] Generating: ${prompt:0:60}..."
if generate_single_image "$prompt" "$temp_image_file" "$blog_title" "content"; then
# Copy to web directory
cp "$temp_image_file" "$image_file" 2>/dev/null || true
# Create the correct URL
local web_image_path="${IMAGE_GENERATION_BASE_URL}${web_image_name}"
generated_image_urls+=("$web_image_path")
((successful_images++))
else
log_warning "Failed to generate image $image_index"
((failed_images++))
fi
# Rate limiting - respect API limits
if [[ $i -lt $((images_to_generate - 1)) ]]; then
sleep 3
fi
done
# OPTIMIZATION: Create mappings for all placeholders (reuse images if needed)
log_info "🔧 Creating optimized image mappings for $placeholder_count placeholders..."
for ((p=1; p<=placeholder_count; p++)); do
if [[ ${#generated_image_urls[@]} -eq 1 ]]; then
# Single image mode: use the same image for all placeholders
local image_url="${generated_image_urls[0]}"
echo "{{IMAGE_PROMPT_${p}}}=${image_url}" >> "${TEMP_DIR}/${blog_id}_image_mapping.txt"
log_info "Placeholder $p → ${image_url} (single image optimization)"
else
# Multiple images mode: cycle through available images
local image_index=$(( (p - 1) % ${#generated_image_urls[@]} ))
local image_url="${generated_image_urls[$image_index]}"
echo "{{IMAGE_PROMPT_${p}}}=${image_url}" >> "${TEMP_DIR}/${blog_id}_image_mapping.txt"
log_info "Placeholder $p → ${image_url} (cycling through images)"
fi
done
log_success "Image generation complete: $successful_images successful, $failed_images failed"
# Track cost for image generation
track_image_generation "$successful_images"
if [[ $successful_images -eq 0 ]]; then
log_error "No images were generated successfully"
return 1
fi
return 0
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# FEATURED AND HERO IMAGE GENERATION
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
generate_featured_image() {
local content_file="$1"
local blog_id="$2"
if [[ "$IS_FEATURED" != "true" ]]; then
return 0
fi
# Skip generation if not requested
if [[ "$IS_FEATURED" != "true" ]]; then
return 0
fi
log_step "Generating featured image with ${FEATURED_IMAGE_ASPECT_RATIO} aspect ratio..."
# Extract blog title for featured image prompt
local blog_title=$(jq -r '.title' "$content_file")
# Use the image prompt after content images (CONTENT_TOTAL + 1)
local featured_prompt_index=$((CONTENT_TOTAL + 1))
local featured_prompt=$(jq -r ".image_prompts[$((featured_prompt_index - 1))]" "$content_file")
if [[ "$featured_prompt" == "null" || -z "$featured_prompt" ]]; then
log_warning "Featured image prompt not found at index $featured_prompt_index, using fallback"
featured_prompt="Professional featured image for blog post: ${blog_title}. High-quality, engaging, suitable for blog header"
else
log_info "Using featured image prompt from index $featured_prompt_index: ${featured_prompt:0:50}..."
fi
local featured_image_name="${blog_id}_featured.jpg"
local featured_image_file="${IMAGE_SAVE_PATH}${featured_image_name}"
if generate_single_image "$featured_prompt" "$featured_image_file" "$blog_title" "featured"; then
local featured_url="${IMAGE_GENERATION_BASE_URL}${featured_image_name}"
echo "$featured_url" > "${TEMP_DIR}/${blog_id}_featured.txt"
log_success "Featured image generated: $featured_url (${FEATURED_IMAGE_ASPECT_RATIO})"
return 0
else
log_error "Failed to generate featured image"
return 1
fi
}
generate_hero_image() {
local content_file="$1"
local blog_id="$2"
# Skip generation if not requested
if [[ "$IS_HERO" != "true" ]]; then
return 0
fi
# Determine hero image type and configuration from CLI/env
local hero_type="hero_middle"
local aspect_ratio="${HERO_IMAGE_MIDDLE_ASPECT_RATIO}"
if [[ "$HERO_TYPE" == "middle_post" ]]; then
hero_type="hero_middle"
aspect_ratio="${HERO_IMAGE_MIDDLE_ASPECT_RATIO}"
log_step "Generating hero image for middle post (${aspect_ratio} aspect ratio)..."
else
hero_type="hero_side"
aspect_ratio="${HERO_IMAGE_SIDE_ASPECT_RATIO}"
log_step "Generating hero image for side post (${aspect_ratio} aspect ratio)..."
fi
# Extract blog title for hero image prompt
local blog_title=$(jq -r '.title' "$content_file")
# Calculate hero image prompt index (after content and featured images)
local hero_prompt_index=$((CONTENT_TOTAL + 1))
[[ "$IS_FEATURED" == "true" ]] && ((hero_prompt_index++))
local hero_prompt=$(jq -r ".image_prompts[$((hero_prompt_index - 1))]" "$content_file")
if [[ "$hero_prompt" == "null" || -z "$hero_prompt" ]]; then
log_warning "Hero image prompt not found at index $hero_prompt_index, using fallback"
hero_prompt="Professional hero image for blog post: ${blog_title}. Eye-catching, high-impact, suitable for hero section"
else
log_info "Using hero image prompt from index $hero_prompt_index: ${hero_prompt:0:50}..."
fi
local hero_image_name="${blog_id}_hero.jpg"
local hero_image_file="${IMAGE_SAVE_PATH}${hero_image_name}"
if generate_single_image "$hero_prompt" "$hero_image_file" "$blog_title" "$hero_type"; then
local hero_url="${IMAGE_GENERATION_BASE_URL}${hero_image_name}"
echo "$hero_url" > "${TEMP_DIR}/${blog_id}_hero.txt"
log_success "Hero image generated: $hero_url (${aspect_ratio})"
return 0
else
log_error "Failed to generate hero image"
return 1
fi
}
generate_slider_image() {
local content_file="$1"
local blog_id="$2"
# Skip generation if not requested
if [[ "$IS_SLIDER_POST" != "true" ]]; then
return 0
fi
log_step "Generating slider post image (${SLIDER_IMAGE_ASPECT_RATIO} aspect ratio)..."
# Extract blog title for slider image prompt
local blog_title=$(jq -r '.title' "$content_file")
# Calculate slider image prompt index (after content, featured, and hero images)
local slider_prompt_index=$((CONTENT_TOTAL + 1))
[[ "$IS_FEATURED" == "true" ]] && ((slider_prompt_index++))
[[ "$IS_HERO" == "true" ]] && ((slider_prompt_index++))
local slider_prompt=$(jq -r ".image_prompts[$((slider_prompt_index - 1))]" "$content_file")
if [[ "$slider_prompt" == "null" || -z "$slider_prompt" ]]; then
log_warning "Slider image prompt not found at index $slider_prompt_index, using fallback"
slider_prompt="Professional slider image for blog post: ${blog_title}. Eye-catching, suitable for homepage slider or carousel display"
else
log_info "Using slider image prompt from index $slider_prompt_index: ${slider_prompt:0:50}..."
fi
local slider_image_name="${blog_id}_slider_post.jpg"
local slider_image_file="${IMAGE_SAVE_PATH}${slider_image_name}"
if generate_single_image "$slider_prompt" "$slider_image_file" "$blog_title" "slider"; then
local slider_url="${IMAGE_GENERATION_BASE_URL}${slider_image_name}"
echo "$slider_url" > "${TEMP_DIR}/${blog_id}_slider_post.txt"
log_success "Slider post image generated: $slider_url (${SLIDER_IMAGE_ASPECT_RATIO})"
return 0
else
log_error "Failed to generate slider post image"
return 1
fi
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# IMAGE PLACEHOLDER REPLACEMENT
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
replace_image_placeholders() {
local content_file="$1"
local blog_id="$2"
local mapping_file="${TEMP_DIR}/${blog_id}_image_mapping.txt"
log_step "Replacing image placeholders in content..."
if [[ ! -f "$mapping_file" ]]; then
log_error "Image mapping file not found: $mapping_file"
return 1
fi
# Read the HTML content
local html_content=$(jq -r '.content' "$content_file")
# Replace each placeholder with proper HTML img tag
while IFS='=' read -r placeholder url; do
if [[ -n "$placeholder" ]] && [[ -n "$url" ]]; then
# Create proper HTML img tag with styling
local img_tag="
"
log_info "Replacing $placeholder with HTML img tag"
html_content="${html_content//$placeholder/$img_tag}"
fi
done < "$mapping_file"
# Update the content in JSON
local updated_json=$(jq --arg new_content "$html_content" '.content = $new_content' "$content_file")
echo "$updated_json" > "$content_file"
log_success "All image placeholders replaced"
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# PREPARE IMAGES FOR BLOG POST - OPTIMIZED VERSION
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
prepare_blog_images() {
local blog_id="$1"
local mapping_file="${TEMP_DIR}/${blog_id}_image_mapping.txt"
log_step "Preparing optimized image assignments from content images..."
if [[ ! -f "$mapping_file" ]]; then
log_error "No image mapping found"
return 1
fi
# Read all content images from mapping file (if it exists and has content)
local content_images=()
if [[ -f "$mapping_file" ]] && [[ -s "$mapping_file" ]]; then
while IFS='=' read -r placeholder url; do
if [[ -n "$url" ]]; then
content_images+=("$url")
fi
done < "$mapping_file"
fi
local total_content_images=${#content_images[@]}
if [[ $total_content_images -eq 0 ]]; then
log_info "🚫 NO CONTENT IMAGES: No content images available for assignment"
# Create empty files for consistency
touch "${TEMP_DIR}/${blog_id}_thumbnail.txt"
echo "[]" > "${TEMP_DIR}/${blog_id}_slider.json"
# Handle special images that were generated separately
if [[ -f "${TEMP_DIR}/${blog_id}_featured.txt" ]]; then
local featured_url=$(cat "${TEMP_DIR}/${blog_id}_featured.txt")
log_success "Featured image: $featured_url"
fi
if [[ -f "${TEMP_DIR}/${blog_id}_hero.txt" ]]; then
local hero_url=$(cat "${TEMP_DIR}/${blog_id}_hero.txt")
log_success "Hero image: $hero_url"
fi
if [[ -f "${TEMP_DIR}/${blog_id}_slider_post.txt" ]]; then
local slider_post_url=$(cat "${TEMP_DIR}/${blog_id}_slider_post.txt")
log_success "Slider post image: $slider_post_url"
fi
log_info "Image assignment completed (no content images)"
return 0
fi
log_info "Total content images available: $total_content_images"
log_info "CONTENT_TOTAL setting: $CONTENT_TOTAL"
# Handle image assignment based on available content images
if [[ $total_content_images -eq 0 ]]; then
log_info "🚫 NO CONTENT IMAGES: No images to assign"
# Create empty files for consistency
touch "${TEMP_DIR}/${blog_id}_thumbnail.txt"
echo "[]" > "${TEMP_DIR}/${blog_id}_slider.json"
return 0
elif [[ $total_content_images -eq 1 ]] || [[ $CONTENT_TOTAL -eq 1 ]]; then
# Single image mode: Use the same image for ALL image fields
local single_image="${content_images[0]}"
log_info "🎯 SINGLE IMAGE MODE: Using same image for all fields"
# Assign same image to all fields
echo "$single_image" > "${TEMP_DIR}/${blog_id}_thumbnail.txt"
# Only use content image for fields that don't have specifically generated images
if [[ "$IS_FEATURED" == "true" ]] && [[ ! -f "${TEMP_DIR}/${blog_id}_featured.txt" ]]; then
echo "$single_image" > "${TEMP_DIR}/${blog_id}_featured.txt"
log_success "Featured image (optimized): $single_image"
fi
if [[ "$IS_HERO" == "true" ]] && [[ ! -f "${TEMP_DIR}/${blog_id}_hero.txt" ]]; then
echo "$single_image" > "${TEMP_DIR}/${blog_id}_hero.txt"
log_success "Hero image (optimized): $single_image"
fi
if [[ "$IS_SLIDER_POST" == "true" ]] && [[ ! -f "${TEMP_DIR}/${blog_id}_slider_post.txt" ]]; then
echo "$single_image" > "${TEMP_DIR}/${blog_id}_slider_post.txt"
log_success "Slider post image (optimized): $single_image"
fi
# Slider images array - only include images if slider-total > 0
if [[ "$SLIDER_TOTAL" -gt 0 ]]; then
jq -n --arg img "$single_image" '[$img]' > "${TEMP_DIR}/${blog_id}_slider.json"
else
echo "[]" > "${TEMP_DIR}/${blog_id}_slider.json"
fi
log_success "✅ OPTIMIZED: All image fields use the same image: $single_image"
else
# Multiple images mode: Randomly assign images from available set
log_info "🎲 MULTIPLE IMAGE MODE: Randomly assigning from $total_content_images images"
# Function to get random image from content images
get_random_image() {
local random_index=$((RANDOM % total_content_images))
echo "${content_images[$random_index]}"
}
# Assign thumbnail (always use first image for consistency)
local thumbnail_image="${content_images[0]}"
echo "$thumbnail_image" > "${TEMP_DIR}/${blog_id}_thumbnail.txt"
# Randomly assign special images if they don't already exist
if [[ "$IS_FEATURED" == "true" ]] && [[ ! -f "${TEMP_DIR}/${blog_id}_featured.txt" ]]; then
local featured_image=$(get_random_image)
echo "$featured_image" > "${TEMP_DIR}/${blog_id}_featured.txt"
log_success "Featured image (random): $featured_image"
fi
if [[ "$IS_HERO" == "true" ]] && [[ ! -f "${TEMP_DIR}/${blog_id}_hero.txt" ]]; then
local hero_image=$(get_random_image)
echo "$hero_image" > "${TEMP_DIR}/${blog_id}_hero.txt"
log_success "Hero image (random): $hero_image"
fi
if [[ "$IS_SLIDER_POST" == "true" ]] && [[ ! -f "${TEMP_DIR}/${blog_id}_slider_post.txt" ]]; then
local slider_post_image=$(get_random_image)
echo "$slider_post_image" > "${TEMP_DIR}/${blog_id}_slider_post.txt"
log_success "Slider post image (random): $slider_post_image"
fi
# Slider images array - only include content images if slider-total > 0
if [[ "$SLIDER_TOTAL" -gt 0 ]]; then
local all_slider_images=()
for img_url in "${content_images[@]}"; do
all_slider_images+=("$img_url")
done
# Create proper JSON array using jq
if [[ ${#all_slider_images[@]} -gt 0 ]]; then
printf '%s\n' "${all_slider_images[@]}" | jq -R . | jq -s . > "${TEMP_DIR}/${blog_id}_slider.json"
else
echo "[]" > "${TEMP_DIR}/${blog_id}_slider.json"
fi
else
echo "[]" > "${TEMP_DIR}/${blog_id}_slider.json"
fi
log_success "✅ OPTIMIZED: Random assignment completed for $total_content_images images"
fi
# Log final assignments
local thumbnail_image=$(cat "${TEMP_DIR}/${blog_id}_thumbnail.txt" 2>/dev/null || echo "")
log_success "Thumbnail: $thumbnail_image"
# Log special images if they exist
local featured_file="${TEMP_DIR}/${blog_id}_featured.txt"
if [[ -f "$featured_file" ]]; then
local featured_url=$(cat "$featured_file")
if [[ -n "$featured_url" ]]; then
log_success "Featured image: $featured_url"
fi
fi
local hero_file="${TEMP_DIR}/${blog_id}_hero.txt"
if [[ -f "$hero_file" ]]; then
local hero_url=$(cat "$hero_file")
if [[ -n "$hero_url" ]]; then
log_success "Hero image: $hero_url"
fi
fi
local slider_post_file="${TEMP_DIR}/${blog_id}_slider_post.txt"
if [[ -f "$slider_post_file" ]]; then
local slider_post_url=$(cat "$slider_post_file")
if [[ -n "$slider_post_url" ]]; then
log_success "Slider post image: $slider_post_url"
fi
fi
local total_slider_images=$(jq length "${TEMP_DIR}/${blog_id}_slider.json" 2>/dev/null || echo "0")
log_success "Slider images: $total_slider_images total images (content images only)"
log_info "Image optimization completed successfully!"
return 0
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# BUILD FINAL JSON FOR API SUBMISSION
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
build_final_json() {
local content_file="$1"
local blog_id="$2"
local category_id="$3"
local language_id="$4"
local serial_number="$5"
local is_primary="${6:-true}" # true for main post, false for translations
local output_json="${TEMP_DIR}/${blog_id}_final.json"
log_step "Building final JSON for API submission..."
# Read content components
local title=$(jq -r '.title' "$content_file")
local content=$(jq -r '.content' "$content_file")
local meta_keywords=$(jq -r '.meta_keywords' "$content_file")
local meta_description=$(jq -r '.meta_description' "$content_file")
# Read image URLs
local thumbnail_image=$(cat "${TEMP_DIR}/${blog_id}_thumbnail.txt" 2>/dev/null || echo "")
local featured_image=$(cat "${TEMP_DIR}/${blog_id}_featured.txt" 2>/dev/null || echo "")
local hero_image=$(cat "${TEMP_DIR}/${blog_id}_hero.txt" 2>/dev/null || echo "")
local slider_post_image=$(cat "${TEMP_DIR}/${blog_id}_slider_post.txt" 2>/dev/null || echo "")
# Read and validate slider_images JSON
local slider_images="[]"
if [[ -f "${TEMP_DIR}/${blog_id}_slider.json" ]]; then
local slider_file_content=$(cat "${TEMP_DIR}/${blog_id}_slider.json")
# Validate that it's proper JSON
if echo "$slider_file_content" | jq empty 2>/dev/null; then
slider_images="$slider_file_content"
else
log_warning "âš ï¸ Invalid slider.json detected, using empty array"
slider_images="[]"
fi
fi
# Escape content for JSON
local escaped_content=$(echo "$content" | jq -Rs .)
# Handle optional serial number
local serial_json=""
if [[ -n "$serial_number" ]]; then
serial_json="\"serial_number\": ${serial_number},"
fi
# Handle optional created_at date
local created_at_json=""
if [[ -n "$CREATED_AT" ]]; then
# Convert to API-compatible format using cross-platform function
local api_date=$(convert_date_format "$CREATED_AT")
if [[ -n "$api_date" ]]; then
created_at_json="\"created_at\": \"${api_date}\","
log_info "✅ Custom date will be used: $api_date"
else
log_warning "âš ï¸ Invalid date format '$CREATED_AT', using current date"
fi
fi
# Build JSON structure based on whether this is primary post or translation
if [[ "$is_primary" == "true" ]]; then
# Primary post - use old structure for compatibility
local json_obj='{}'
# Add fields one by one with error checking
json_obj=$(echo "$json_obj" | jq --arg title "$title" '. + {"title": $title}') || { log_error "Failed to add title"; return 1; }
json_obj=$(echo "$json_obj" | jq --arg author "$DEFAULT_AUTHOR" '. + {"author": $author}') || { log_error "Failed to add author"; return 1; }
json_obj=$(echo "$json_obj" | jq --arg content "$content" '. + {"content": $content}') || { log_error "Failed to add content"; return 1; }
# Use --arg instead of --argjson for numeric values to avoid JSON parsing issues
json_obj=$(echo "$json_obj" | jq --arg category_id "$category_id" '. + {"category_id": ($category_id | tonumber)}') || { log_error "Failed to add category_id"; return 1; }
json_obj=$(echo "$json_obj" | jq --arg language_id "$language_id" '. + {"language_id": ($language_id | tonumber)}') || { log_error "Failed to add language_id"; return 1; }
# Add optional serial number
if [[ -n "$serial_number" ]]; then
json_obj=$(echo "$json_obj" | jq --arg serial_number "$serial_number" '. + {"serial_number": ($serial_number | tonumber)}')
fi
# Convert hero image type to frontend format (middle -> middle_post, side -> side_post)
local hero_image_type_for_api="$HERO_IMAGE_TYPE"
if [[ "$HERO_IMAGE_TYPE" == "middle" ]]; then
hero_image_type_for_api="middle_post"
elif [[ "$HERO_IMAGE_TYPE" == "side" ]]; then
hero_image_type_for_api="side_post"
fi
# Add image-related fields (only for primary post)
# Convert string booleans to actual JSON booleans
local is_featured_bool="false"
local is_hero_post_bool="false"
local is_slider_bool="false"
[[ "$IS_FEATURED" == "true" ]] && is_featured_bool="true"
[[ "$IS_HERO" == "true" ]] && is_hero_post_bool="true"
[[ "$IS_SLIDER_POST" == "true" ]] && is_slider_bool="true"
json_obj=$(echo "$json_obj" | jq --argjson is_featured "$is_featured_bool" '. + {"is_featured": $is_featured}')
json_obj=$(echo "$json_obj" | jq --arg featured_post_image "$featured_image" '. + {"featured_post_image": $featured_post_image}')
json_obj=$(echo "$json_obj" | jq --argjson is_hero_post "$is_hero_post_bool" '. + {"is_hero_post": $is_hero_post}')
json_obj=$(echo "$json_obj" | jq --arg hero_post_image "$hero_image" '. + {"hero_post_image": $hero_post_image}')
json_obj=$(echo "$json_obj" | jq --arg hero_image_type "$hero_image_type_for_api" '. + {"hero_image_type": $hero_image_type}')
json_obj=$(echo "$json_obj" | jq --argjson is_slider "$is_slider_bool" '. + {"is_slider": $is_slider}')
json_obj=$(echo "$json_obj" | jq --arg slider_post_image "$slider_post_image" '. + {"slider_post_image": $slider_post_image}')
json_obj=$(echo "$json_obj" | jq --arg thumbnail_image "$thumbnail_image" '. + {"thumbnail_image": $thumbnail_image}')
json_obj=$(echo "$json_obj" | jq --argjson slider_images "$slider_images" '. + {"slider_images": $slider_images}')
# Add optional created_at
if [[ -n "$CREATED_AT" ]]; then
local api_date=$(convert_date_format "$CREATED_AT")
if [[ -n "$api_date" ]]; then
json_obj=$(echo "$json_obj" | jq --arg created_at "$api_date" '. + {"created_at": $created_at}')
log_info "✅ Custom creation date set: $api_date"
else
log_warning "âš ï¸ Invalid date format '$CREATED_AT', skipping custom date"
fi
fi
# Add meta fields
json_obj=$(echo "$json_obj" | jq --arg meta_keywords "$meta_keywords" '. + {"meta_keywords": $meta_keywords}')
json_obj=$(echo "$json_obj" | jq --arg meta_description "$meta_description" '. + {"meta_description": $meta_description}')
else
# Translation - only post_content fields
local json_obj='{}'
json_obj=$(echo "$json_obj" | jq --arg language_id "$language_id" '. + {"language_id": ($language_id | tonumber)}')
json_obj=$(echo "$json_obj" | jq --arg post_category_id "$category_id" '. + {"post_category_id": ($post_category_id | tonumber)}')
json_obj=$(echo "$json_obj" | jq --arg title "$title" '. + {"title": $title}')
json_obj=$(echo "$json_obj" | jq --arg author "$DEFAULT_AUTHOR" '. + {"author": $author}')
json_obj=$(echo "$json_obj" | jq --arg content "$content" '. + {"content": $content}')
json_obj=$(echo "$json_obj" | jq --arg meta_keywords "$meta_keywords" '. + {"meta_keywords": $meta_keywords}')
json_obj=$(echo "$json_obj" | jq --arg meta_description "$meta_description" '. + {"meta_description": $meta_description}')
fi
# Check if json_obj is empty or invalid
if [[ -z "$json_obj" ]]; then
log_error "🚨 JSON object is empty - build process failed"
return 1
fi
# Write final JSON
echo "$json_obj" > "$output_json"
# Validate final JSON
if ! jq empty "$output_json" 2>/dev/null; then
log_error "Final JSON is invalid"
return 1
fi
log_success "Final JSON created: $output_json"
echo "$output_json"
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# SUBMIT BLOG TO API
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
submit_blog_post() {
local json_file="$1"
local blog_id="$2"
log_step "Submitting blog post to API..."
if [[ ! -f "$json_file" ]]; then
log_error "JSON file not found: $json_file"
return 1
fi
# Submit to API
local response=$(curl -s -w "\n%{http_code}" -X POST "$BLOG_API_URL" \
-H "X-API-Token: ${BLOG_API_TOKEN}" \
-H "Content-Type: application/json" \
-d @"$json_file")
local http_code=$(echo "$response" | tail -n1)
local response_body=$(echo "$response" | sed '$d')
# Save response
echo "$response_body" > "${OUTPUT_DIR}/${blog_id}_api_response.json"
if [[ "$http_code" == "200" ]] || [[ "$http_code" == "201" ]]; then
log_success "Blog post submitted successfully! (HTTP $http_code)"
log_success "Response saved to: ${OUTPUT_DIR}/${blog_id}_api_response.json"
# Extract post ID if available
local post_id=$(echo "$response_body" | jq -r '.id // .post_id // empty' 2>/dev/null)
if [[ -n "$post_id" ]]; then
log_success "Post ID: $post_id"
fi
# Return the response body for further processing
echo "$response_body"
return 0
else
log_error "API submission failed with HTTP code: $http_code"
log_error "Response: $response_body"
return 1
fi
}
submit_blog_translation() {
local json_file="$1"
log_step "Submitting blog translation to API..."
if [[ ! -f "$json_file" ]]; then
log_error "Translation JSON file not found: $json_file"
return 1
fi
# Submit to post-contents endpoint using same base URL as main API
local base_url=$(echo "$BLOG_API_URL" | sed 's|/api/v1/posts||')
local translation_endpoint="${base_url}/api/v1/post-contents"
local response=$(curl -s -w "HTTPSTATUS:%{http_code}" -X POST \
-H "X-API-Token: ${BLOG_API_TOKEN}" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d @"$json_file" \
"$translation_endpoint")
local response_body=$(echo "$response" | sed -E 's/HTTPSTATUS\:[0-9]{3}$//')
local http_status=$(echo "$response" | tr -d '\n' | sed -E 's/.*HTTPSTATUS:([0-9]{3})$/\1/')
# Save response for debugging
local response_file="${json_file%.json}_api_response.json"
echo "$response_body" > "$response_file"
if [[ "$http_status" == "201" ]] || [[ "$http_status" == "200" ]]; then
log_success "Translation submitted successfully! (HTTP $http_status)"
log_success "Response saved to: $response_file"
echo "$response_body"
return 0
else
log_error "Translation submission failed (HTTP $http_status)"
log_error "Response: $response_body"
return 1
fi
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# MULTILINGUAL BLOG GENERATION WORKFLOW
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
process_multilingual_blog() {
local topic="$1"
local category_id="$2"
local primary_language_id="$3"
local languages_json="$4"
local serial_number="$5"
local blog_id="blog_$(date +%Y%m%d_%H%M%S)_$$"
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
log_info " Starting multilingual blog generation for: '$topic'"
log_info " Primary Language ID: $primary_language_id"
log_info " Additional Languages: $(echo "$languages_json" | jq -r '.[].name' | tr '\n' ',' | sed 's/,$//')"
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
log_info "Blog ID: $blog_id"
# Step 1: Generate primary English content
log_step "[1/7] Generating primary blog content (English)..."
local content_file=$(generate_blog_content "$topic" "$blog_id")
if [[ ! -f "$content_file" ]]; then
log_error "Primary content generation failed - file not found: $content_file"
return 1
fi
# Extract and display blog title
local blog_title=$(jq -r '.title' "$content_file")
log_success "Blog title: '$blog_title'"
# Step 2: Generate images (only once for primary post)
log_step "[2/7] Generating content images with Imagen 3..."
if ! generate_all_images "$content_file" "$blog_id"; then
log_error "Image generation failed"
return 1
fi
# Step 2a-2c: Generate special images if requested
if [[ "$IS_FEATURED" == "true" ]]; then
log_step "[2a/7] Generating featured image..."
if ! generate_featured_image "$content_file" "$blog_id"; then
log_warning "Featured image generation failed, continuing..."
fi
fi
if [[ "$IS_HERO" == "true" ]]; then
log_step "[2b/7] Generating hero image..."
if ! generate_hero_image "$content_file" "$blog_id"; then
log_warning "Hero image generation failed, continuing..."
fi
fi
if [[ "$IS_SLIDER_POST" == "true" ]]; then
log_step "[2c/7] Generating slider post image..."
if ! generate_slider_image "$content_file" "$blog_id"; then
log_warning "Slider post image generation failed, continuing..."
fi
fi
# Step 3: Replace image placeholders in primary content
log_step "[3/7] Replacing image placeholders in primary content..."
if ! replace_image_placeholders "$content_file" "$blog_id"; then
log_error "Failed to replace image placeholders"
return 1
fi
# Step 4: Prepare thumbnail and slider images
log_step "[4/7] Preparing thumbnail and slider images..."
if ! prepare_blog_images "$blog_id"; then
log_error "Failed to prepare blog images"
return 1
fi
# Step 5: Generate translations
local translation_files=()
if [[ "$languages_json" != "[]" ]]; then
log_step "[5/7] Generating translations..."
local lang_count=$(echo "$languages_json" | jq length)
local current_lang=1
while read -r lang_name lang_id lang_category_id; do
log_info "[$current_lang/$lang_count] Translating to $lang_name (ID: $lang_id, Category: $lang_category_id)..."
local translated_file=$(generate_multilingual_content "$content_file" "$blog_id" "$lang_name" "$lang_id")
if [[ -f "$translated_file" ]]; then
# Replace image placeholders in translated content
if replace_image_placeholders "$translated_file" "$blog_id"; then
translation_files+=("$translated_file:$lang_id:$lang_category_id")
log_success "Translation completed for $lang_name"
else
log_warning "Failed to replace image placeholders for $lang_name"
fi
else
log_warning "Translation failed for $lang_name"
fi
((current_lang++))
done < <(echo "$languages_json" | jq -r '.[] | "\(.name) \(.id) \(.category_id)"')
else
log_info "[5/7] No additional languages specified, skipping translations..."
fi
# Step 6: Build and submit primary post
log_step "[6/7] Building and submitting primary post..."
local primary_json=$(build_final_json "$content_file" "$blog_id" "$category_id" "$primary_language_id" "$serial_number" "true")
if [[ ! -f "${TEMP_DIR}/${blog_id}_final.json" ]]; then
log_error "Failed to build primary JSON"
return 1
fi
# Submit primary post
local primary_response=$(submit_blog_post "${TEMP_DIR}/${blog_id}_final.json" "$blog_id")
if [[ $? -ne 0 ]]; then
log_error "Failed to submit primary post"
return 1
fi
# Extract post_id from response for translations
local post_id=$(echo "$primary_response" | jq -r '.data.id // empty')
if [[ -z "$post_id" ]]; then
log_error "Could not extract post_id from API response"
return 1
fi
log_success "Primary post created with ID: $post_id"
# Step 7: Submit translations
if [[ ${#translation_files[@]} -gt 0 ]]; then
log_step "[7/7] Submitting translations..."
local translation_count=1
for translation_entry in "${translation_files[@]}"; do
# Parse translation_file:lang_id:category_id
local translation_file=$(echo "$translation_entry" | cut -d':' -f1)
local lang_id=$(echo "$translation_entry" | cut -d':' -f2)
local lang_category_id=$(echo "$translation_entry" | cut -d':' -f3)
log_info "[$translation_count/${#translation_files[@]}] Submitting translation for language ID: $lang_id (Category: $lang_category_id)..."
# Build translation JSON with correct category ID
local translation_json=$(build_final_json "$translation_file" "$blog_id" "$lang_category_id" "$lang_id" "$serial_number" "false")
local translation_json_file="${TEMP_DIR}/${blog_id}_translation_${lang_id}.json"
# Add post_id to translation
local final_translation_json=$(jq --argjson post_id "$post_id" '. + {"post_id": $post_id}' "${TEMP_DIR}/${blog_id}_final.json")
echo "$final_translation_json" > "$translation_json_file"
# Submit translation
if submit_blog_translation "$translation_json_file"; then
log_success "Translation submitted for language ID: $lang_id"
else
log_warning "Failed to submit translation for language ID: $lang_id"
fi
((translation_count++))
done
else
log_info "[7/7] No translations to submit..."
fi
# Copy final files to generated_blogs directory
cp "${TEMP_DIR}/${blog_id}_final.json" "${OUTPUT_DIR}/${blog_id}_final.json"
# Generate cost report
calculate_costs "$blog_id" "$blog_title" "$post_id"
log_success "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
log_success " ✓ Multilingual blog generation completed successfully!"
log_success " ✓ Blog ID: $blog_id"
log_success " ✓ Primary Post ID: $post_id"
log_success " ✓ Title: $blog_title"
log_success " ✓ Languages: $((${#translation_files[@]} + 1)) total"
log_success " ✓ Output: ${OUTPUT_DIR}/${blog_id}_final.json"
log_success "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
return 0
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# SINGLE BLOG GENERATION WORKFLOW
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
process_single_blog() {
local topic="$1"
local category_id="$2"
local language_id="$3"
local serial_number="$4"
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
log_info " Starting blog generation for: '$topic'"
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
# Generate unique blog ID
local blog_id=$(generate_blog_id)
log_info "Blog ID: $blog_id"
# Step 1: Generate blog content with Gemini
log_step "[1/6] Generating blog content with ${TEXT_MODEL}..."
local content_file=$(generate_blog_content "$topic" "$blog_id")
local generation_result=$?
# Check if content file exists (more reliable than return code)
local expected_file="${TEMP_DIR}/${blog_id}_content.json"
if [[ ! -f "$expected_file" ]]; then
log_error "Failed to generate blog content - file not found: $expected_file"
return 1
fi
# Validate JSON content
if ! jq empty "$expected_file" 2>/dev/null; then
log_error "Failed to generate blog content - invalid JSON"
return 1
fi
content_file="$expected_file"
# Extract and display blog title
local blog_title=$(jq -r '.title' "$content_file")
log_success "Blog title: '$blog_title'"
# Step 2: Generate images with Imagen 3
log_step "[2/6] Generating content images with Imagen 3..."
if ! generate_all_images "$content_file" "$blog_id"; then
log_error "Image generation failed"
return 1
fi
# Step 2a: Generate featured image if requested
if [[ "$IS_FEATURED" == "true" ]]; then
log_step "[2a/6] Generating featured image..."
if ! generate_featured_image "$content_file" "$blog_id"; then
log_warning "Featured image generation failed, continuing..."
fi
fi
# Step 2b: Generate hero image if requested
if [[ "$IS_HERO" == "true" ]]; then
log_step "[2b/6] Generating hero image..."
if ! generate_hero_image "$content_file" "$blog_id"; then
log_warning "Hero image generation failed, continuing..."
fi
fi
# Step 2c: Generate slider post image if requested
if [[ "$IS_SLIDER_POST" == "true" ]]; then
log_step "[2c/6] Generating slider post image..."
if ! generate_slider_image "$content_file" "$blog_id"; then
log_warning "Slider post image generation failed, continuing..."
fi
fi
# Step 3: Replace image placeholders
log_step "[3/6] Replacing image placeholders in content..."
if ! replace_image_placeholders "$content_file" "$blog_id"; then
log_error "Failed to replace image placeholders"
return 1
fi
# Step 4: Prepare thumbnail and slider images
log_step "[4/6] Preparing thumbnail and slider images..."
if ! prepare_blog_images "$blog_id"; then
log_error "Failed to prepare blog images"
return 1
fi
# Step 5: Build final JSON
log_step "[5/6] Building final JSON for API submission..."
local final_json=$(build_final_json "$content_file" "$blog_id" "$category_id" "$language_id" "$serial_number")
# Check if final JSON file exists (more reliable than return code)
local expected_final_json="${TEMP_DIR}/${blog_id}_final.json"
if [[ ! -f "$expected_final_json" ]]; then
log_error "Failed to build final JSON - file not found: $expected_final_json"
return 1
fi
# Validate final JSON
if ! jq empty "$expected_final_json" 2>/dev/null; then
log_error "Failed to build final JSON - invalid JSON"
return 1
fi
final_json="$expected_final_json"
# Copy final JSON to output directory
cp "$final_json" "${OUTPUT_DIR}/${blog_id}_final.json"
log_success "Final JSON saved to: ${OUTPUT_DIR}/${blog_id}_final.json"
# Step 6: Submit to API
log_step "[6/6] Submitting blog post to API..."
local api_response=$(submit_blog_post "$final_json" "$blog_id")
if [[ $? -ne 0 ]]; then
log_error "Failed to submit blog post"
return 1
fi
# Extract post_id for cost tracking
local post_id=$(echo "$api_response" | jq -r '.data.id // .id // empty' 2>/dev/null)
if [[ -z "$post_id" ]]; then
post_id="unknown"
log_warning "Could not extract post_id from API response"
fi
# Generate cost report
calculate_costs "$blog_id" "$blog_title" "$post_id"
# Success summary
log_success "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
log_success " ✓ Blog generation completed successfully!"
log_success " ✓ Blog ID: $blog_id"
log_success " ✓ Post ID: $post_id"
log_success " ✓ Title: $blog_title"
log_success " ✓ Output: ${OUTPUT_DIR}/${blog_id}_final.json"
log_success "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
return 0
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# BULK CSV PROCESSING
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
process_csv_bulk() {
local csv_file="$1"
if [[ ! -f "$csv_file" ]]; then
log_error "CSV file not found: $csv_file"
return 1
fi
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
log_info " Starting ENHANCED BULK blog generation from CSV"
log_info " CSV File: $csv_file"
log_info " Features: Multilingual, All Images, Custom Dates"
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
# Read CSV and process each line
local line_number=0
local success_count=0
local failed_count=0
local skipped_count=0
while IFS=',' read -r topic category_id language_id languages featured hero_image image_type slider created_at serial_number || [[ -n "$topic" ]]; do
((line_number++))
# Skip header line
if [[ $line_number -eq 1 ]]; then
log_info "CSV Header: topic, category_id, language_id, languages, featured, hero_image, image_type, slider, created_at, serial_number"
continue
fi
# Remove quotes and trim whitespace
topic=$(echo "$topic" | sed 's/^"//;s/"$//' | xargs)
category_id=$(echo "$category_id" | sed 's/^"//;s/"$//' | xargs)
language_id=$(echo "$language_id" | sed 's/^"//;s/"$//' | xargs)
languages=$(echo "$languages" | sed 's/^"//;s/"$//' | xargs)
featured=$(echo "$featured" | sed 's/^"//;s/"$//' | xargs)
hero_image=$(echo "$hero_image" | sed 's/^"//;s/"$//' | xargs)
image_type=$(echo "$image_type" | sed 's/^"//;s/"$//' | xargs)
slider=$(echo "$slider" | sed 's/^"//;s/"$//' | xargs)
created_at=$(echo "$created_at" | sed 's/^"//;s/"$//' | xargs)
serial_number=$(echo "$serial_number" | sed 's/^"//;s/"$//' | xargs)
# Validate required fields
if [[ -z "$topic" ]] || [[ -z "$category_id" ]] || [[ -z "$language_id" ]] || [[ -z "$serial_number" ]]; then
log_warning "Line $line_number: Skipping incomplete data (missing required fields)"
((skipped_count++))
continue
fi
# Validate date format if provided
if [[ -n "$created_at" ]] && ! validate_cli_date_format "$created_at"; then
log_warning "Line $line_number: Invalid date format '$created_at', skipping this entry"
((skipped_count++))
continue
fi
log_info ""
log_info "───────────────────────────────────────────────────────────────"
log_info "Processing CSV Line $line_number"
log_info "Topic: $topic"
log_info "Primary: Category $category_id | Language $language_id | Serial $serial_number"
[[ -n "$languages" ]] && log_info "Additional Languages: $languages"
[[ -n "$featured" && "$featured" != "false" ]] && log_info "Featured: $featured"
[[ -n "$hero_image" && "$hero_image" != "false" ]] && log_info "Hero Image: $hero_image ($image_type)"
[[ -n "$slider" && "$slider" != "false" ]] && log_info "Slider: $slider"
[[ -n "$created_at" ]] && log_info "Created At: $created_at"
log_info "───────────────────────────────────────────────────────────────"
# Set global variables for this blog generation
export TOPIC="$topic"
export CATEGORY_ID="$category_id"
export LANGUAGE_ID="$language_id"
export LANGUAGES="$languages"
export SERIAL_NUMBER="$serial_number"
# Set optional features
export IS_FEATURED="${featured:-false}"
export IS_HERO_POST="${hero_image:-false}"
export IMAGE_TYPE="${image_type:-middle}"
export IS_SLIDER="${slider:-false}"
export CREATED_AT="$created_at"
# Process blog using multilingual workflow
local languages_json="[]"
if [[ -n "$languages" ]]; then
languages_json=$(parse_languages "$languages" "$category_id")
if [[ $? -ne 0 ]]; then
log_error "Line $line_number: Invalid language format: $languages"
((failed_count++))
continue
fi
fi
# Process blog
if process_multilingual_blog "$topic" "$category_id" "$language_id" "$languages_json" "$serial_number"; then
((success_count++))
log_success "Line $line_number: ✓ Success"
else
((failed_count++))
log_error "Line $line_number: ✗ Failed"
fi
# Add delay between bulk generations to respect API limits
if [[ $line_number -lt $(wc -l < "$csv_file") ]]; then
log_info "Waiting 15 seconds before next blog..."
sleep 15
fi
done < "$csv_file"
# Final summary
log_info ""
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
log_info " ENHANCED BULK GENERATION SUMMARY"
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
log_success " ✓ Successful: $success_count"
log_error " ✗ Failed: $failed_count"
log_warning " ⊘ Skipped: $skipped_count"
log_info " Total Processed: $((line_number - 1))"
log_info "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•"
return 0
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# DISPLAY USAGE HELP
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
show_usage() {
cat << 'EOF'
â•”â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•—
â•‘ BLOG GENERATOR WITH AI â•‘
â•‘ Powered by Gemini & Imagen â•‘
╚â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•
USAGE:
./blog_generator.sh [OPTIONS]
SINGLE BLOG GENERATION:
./blog_generator.sh --topic "Your Blog Topic" \
[--category-id 26] \
[--language-id 120] \
[--content-total 1] \
[--slider-total 1] \
[--featured true] \
[--hero TRUE:middle_post] \
[--slider-post true] \
[--api-token YOUR_TOKEN] \
[--created-at "15-01-2024"]
MULTILINGUAL BLOG GENERATION:
./blog_generator.sh --topic "Your Blog Topic" \
[--category-id 26] \
[--language-id 120] \
--languages "es:177:26,fr:178:27,de:179:28" \
[--content-total 1] \
[--featured true] \
[--hero TRUE:middle_post] \
[--slider-post true]
BULK CSV GENERATION:
./blog_generator.sh --csv blogs.csv
TRANSLATE EXISTING BLOG (from API):
./blog_generator.sh --translate-post-id 123 \
--target-languages "es:177:27,fr:178:28" \
[--translation-method google-translate]
TRANSLATE EXISTING BLOG (from JSON file):
./blog_generator.sh --translate-json-file "./temp_blog_generation/blog_final.json" \
--target-languages "es:177:27,fr:178:28" \
--original-post-id 123 \
[--translation-method google-translate]
REQUIRED OPTIONS:
-t, --topic Blog topic (required for single generation)
OPTIONAL OPTIONS (with .env defaults):
-c, --category-id Category ID (uses DEFAULT_CATEGORY_ID from .env if not provided)
-l, --language-id Language ID (uses DEFAULT_LANGUAGE_ID from .env if not provided)
--api-token API token (uses DEFAULT_API_TOKEN from .env if not provided)
TRANSLATION OPTIONS:
--translate-post-id Post ID to fetch from API and translate (required for API translation)
--translate-json-file JSON file path to translate (required for file translation)
--original-post-id Original post ID for JSON file translation (adds content to existing post)
--target-languages Target languages in format "LangCode:LanguageID:CategoryID,LangCode:LanguageID:CategoryID"
Example: "es:177:27,fr:178:28,de:179:29"
Language codes: es (Spanish), fr (French), de (German), pt (Portuguese), zh (Chinese), etc.
IMAGE GENERATION OPTIONS:
--content-total Number of images to generate for content (default: 0 from .env)
Example: --content-total 1 (generates 1 image for content placeholders)
If 0, no images in content. If 1, single image reused for all placeholders.
--slider-total Number of images to generate for slider (default: 0 from .env)
Example: --slider-total 1 (generates 1 image for slider_images array)
--featured Generate featured image (true/false, default: false)
--hero Generate hero image with type (TRUE:middle_post or TRUE:side_post, default: false)
Example: --hero TRUE:middle_post or --hero TRUE:side_post
--slider-post Generate slider post image (true/false, default: false)
OTHER OPTIONS:
--languages Additional languages in format "LangCode:LanguageID:CategoryID,LangCode:LanguageID:CategoryID"
Example: "es:177:26,fr:178:27,de:179:28"
Language codes: es (Spanish), fr (French), de (German), pt (Portuguese), zh (Chinese), etc.
-s, --serial-number Serial number (auto-increment if not provided)
--created-at Custom creation date (format: ${CLI_DATE_FORMAT:-DD-MM-YYYY})
--translation-method Translation method: 'vertex-ai' or 'google-translate' (default: google-translate)
--csv CSV file path for bulk generation
-h, --help Show this help message
DEFAULT VALUES:
All parameters can be set as defaults in .env file. If only --topic is provided,
all other values will be taken from .env defaults:
- DEFAULT_CATEGORY_ID, DEFAULT_LANGUAGE_ID, DEFAULT_LANGUAGES
- DEFAULT_CONTENT_TOTAL, DEFAULT_SLIDER_TOTAL
- DEFAULT_FEATURED, DEFAULT_HERO, DEFAULT_HERO_TYPE, DEFAULT_SLIDER_POST
- DEFAULT_API_TOKEN, DEFAULT_TRANSLATION_METHOD, DEFAULT_CREATED_AT
- CLI_DATE_FORMAT (${CLI_DATE_FORMAT:-DD-MM-YYYY}), API_DATE_FORMAT (${API_DATE_FORMAT:-YYYY-MM-DD})
COST TRACKING:
When ENABLE_COST_TRACKING=true in .env, generates cost reports in JSON format:
- Tracks text generation, image generation, and translation costs
- Supports both Vertex AI and Google Translate API pricing
- Saves cost report as: {post_id}_{blog_title}_cost_report.json
IMAGE SIZES:
- Featured Image: 770x508 pixels
- Hero Image (Middle): 750x945 pixels
- Hero Image (Side): 750x422 pixels
- Slider Post Image: 770x450 pixels
- Content Images: 1024x1024 pixels (default)
CSV FORMAT (Enhanced):
The CSV file supports all features with the following structure:
topic,category_id,language_id,languages,featured,hero_image,image_type,slider,created_at,serial_number
"AI Revolution in Business",26,120,"Spanish:177:27,French:178:28",true,true,middle,true,"12-10-2025",10001
"Future of Mobile Apps",26,120,"Spanish:177:27",true,false,,true,"13/10/2025",10002
"Digital Transformation",26,120,"",false,true,side,false,"2025-10-14",10003
Required columns: topic, category_id, language_id, serial_number
Optional columns: languages, featured, hero_image, image_type, slider, created_at
Date formats: ${SUPPORTED_DATE_FORMATS:-DD-MM-YYYY,DD/MM/YYYY,YYYY-MM-DD,MM/DD/YYYY} (examples: 15-01-2024, 15/01/2024, 2024-01-15, 01/15/2024)
Language format: "Name:LanguageID:CategoryID" or "Name:LanguageID" (uses primary category)
Multiple languages: "Spanish:177:27;French:178:28;German:179:29" (semicolon for CSV compatibility)
ENVIRONMENT VARIABLES (Required):
GEMINI_API_KEY Your Google AI Gemini API key
BLOG_API_TOKEN Your blog API token
Optional:
IMAGEN_API_KEY Imagen API key (if different from GEMINI_API_KEY)
EXAMPLES:
# Basic single blog generation
./blog_generator.sh -t "AI Revolution in 2025" -c 26 -l 120
# Blog with featured image
./blog_generator.sh -t "Future of Technology" -c 26 -l 120 --featured true
# Blog with hero image for middle post
./blog_generator.sh -t "Digital Transformation" -c 26 -l 120 \
--hero-image true --image-type middle
# Blog with all image types and custom date
./blog_generator.sh -t "Complete Guide to AI" -c 26 -l 120 -s 1001 \
--featured true --hero-image true --image-type side \
--slider true --created-at "15-01-2024"
# Different date formats (all supported)
./blog_generator.sh -t "Test Blog" -c 26 -l 120 --created-at "15-01-2024" # DD-MM-YYYY
./blog_generator.sh -t "Test Blog" -c 26 -l 120 --created-at "15/01/2024" # DD/MM/YYYY
./blog_generator.sh -t "Test Blog" -c 26 -l 120 --created-at "2024-01-15" # YYYY-MM-DD
./blog_generator.sh -t "Test Blog" -c 26 -l 120 --created-at "01/15/2024" # MM/DD/YYYY
# Enhanced bulk generation with all features
./blog_generator.sh --csv enhanced_bulk_blogs.csv
# Multilingual blog with all images
./blog_generator.sh -t "AI in Healthcare" -c 26 -l 120 \
--languages "es:177:27,fr:178:28" \
--featured true --hero-image true --slider true
OUTPUT:
- Generated blogs: ./generated_blogs/
- Temporary files: ./temp_blog_generation/
- Log file: ./blog_generator.log
MODELS USED:
- Text Generation: ${TEXT_MODEL}
- Image Generation: ${IMAGE_MODEL}
For more information, visit:
- Gemini API: https://ai.google.dev/gemini-api/docs
- Imagen API: https://ai.google.dev/gemini-api/docs/imagen
EOF
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# MULTILINGUAL SUPPORT FUNCTIONS
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
parse_languages() {
local languages_input="$1"
local default_category_id="$2"
local -a language_array=()
if [[ -z "$languages_input" ]]; then
echo "[]"
return 0
fi
# Parse format: "es:177:26;fr:178:27" (language codes) or "Spanish:177:26;French:178:27" (backward compatible)
# Also support comma format for backward compatibility
if [[ "$languages_input" == *";"* ]]; then
IFS=';' read -ra LANG_PAIRS <<< "$languages_input"
else
IFS=',' read -ra LANG_PAIRS <<< "$languages_input"
fi
for lang_pair in "${LANG_PAIRS[@]}"; do
if [[ "$lang_pair" =~ ^([^:]+):([0-9]+):([0-9]+)$ ]]; then
# New format with category ID: LangCode:LanguageID:CategoryID or Name:LanguageID:CategoryID
local lang_name="${BASH_REMATCH[1]}"
local lang_id="${BASH_REMATCH[2]}"
local category_id="${BASH_REMATCH[3]}"
language_array+=("{\"name\":\"$lang_name\",\"id\":$lang_id,\"category_id\":$category_id}")
elif [[ "$lang_pair" =~ ^([^:]+):([0-9]+)$ ]]; then
# Backward compatible format: LangCode:LanguageID or Name:LanguageID (uses default category)
local lang_name="${BASH_REMATCH[1]}"
local lang_id="${BASH_REMATCH[2]}"
local category_id="$default_category_id"
language_array+=("{\"name\":\"$lang_name\",\"id\":$lang_id,\"category_id\":$category_id}")
else
log_error "Invalid language format: $lang_pair"
log_error "Expected format: 'LangCode:LanguageID:CategoryID' (e.g., 'es:177:26') or 'LangCode:LanguageID' (e.g., 'es:177')"
log_error "Language codes: es (Spanish), fr (French), de (German), pt (Portuguese), zh (Chinese), etc."
return 1
fi
done
# Return JSON array
local json_array="[$(IFS=,; echo "${language_array[*]}")]"
echo "$json_array"
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# BLOG TRANSLATION FUNCTIONS
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# Fetch blog content from API
fetch_blog_from_api() {
local post_id="$1"
log_info "Fetching blog content from API for Post ID: $post_id"
# Construct API URL for fetching post
local api_url="${BLOG_API_URL}/${post_id}"
# Make API call to fetch blog content
local response=$(curl -s -w "\n%{http_code}" -X GET "$api_url" \
-H "X-API-Token: ${BLOG_API_TOKEN}" \
-H "Accept: application/json")
local http_code=$(echo "$response" | tail -n1)
local response_body=$(echo "$response" | sed '$d')
if [[ "$http_code" != "200" ]]; then
log_error "Failed to fetch blog from API (HTTP $http_code)"
log_error "Response: $response_body"
return 1
fi
# Parse the response and extract blog data
local blog_data=$(echo "$response_body" | jq -r '.data')
if [[ "$blog_data" == "null" ]]; then
log_error "No blog data found in API response"
return 1
fi
# Extract main blog content (assuming it's the first content entry)
local main_content=$(echo "$blog_data" | jq -r '.content[0]')
if [[ "$main_content" == "null" ]]; then
log_error "No content found in blog data"
return 1
fi
# Create a temporary JSON file with the blog content in our expected format
local temp_json="${TEMP_DIR}/fetched_blog_${post_id}.json"
# Extract fields and create JSON structure using jq for proper escaping
local title=$(echo "$main_content" | jq -r '.title // ""')
local content=$(echo "$main_content" | jq -r '.content // ""')
local author=$(echo "$main_content" | jq -r '.author // "Sandeep Mundra"')
local category_id=$(echo "$main_content" | jq -r '.post_category_id // 26')
local language_id=$(echo "$main_content" | jq -r '.language_id // 120')
local meta_keywords=$(echo "$main_content" | jq -r '.meta_keywords // ""')
local meta_description=$(echo "$main_content" | jq -r '.meta_description // ""')
# Create JSON structure using jq for proper escaping
jq -n \
--arg title "$title" \
--arg author "$author" \
--arg content "$content" \
--argjson category_id "$category_id" \
--argjson language_id "$language_id" \
--argjson serial_number 1 \
--argjson is_featured false \
--arg thumbnail_image "" \
--argjson slider_images "[]" \
--arg meta_keywords "$meta_keywords" \
--arg meta_description "$meta_description" \
'{
title: $title,
author: $author,
content: $content,
category_id: $category_id,
language_id: $language_id,
serial_number: $serial_number,
is_featured: $is_featured,
thumbnail_image: $thumbnail_image,
slider_images: $slider_images,
meta_keywords: $meta_keywords,
meta_description: $meta_description
}' > "$temp_json"
log_success "Blog content fetched and saved to: $temp_json"
echo "$temp_json"
}
# Translate existing blog to target languages
translate_existing_blog() {
local source_json="$1"
local target_languages="$2"
local translation_method="${3:-$DEFAULT_TRANSLATION_METHOD}"
log_info "Starting translation of existing blog"
log_info "Source: $source_json"
log_info "Target languages: $target_languages"
log_info "Translation method: $translation_method"
# Validate source JSON file exists
if [[ ! -f "$source_json" ]]; then
log_error "Source JSON file not found: $source_json"
return 1
fi
# Parse target languages
local languages_array=()
IFS=',' read -ra LANG_PARTS <<< "$target_languages"
for lang_part in "${LANG_PARTS[@]}"; do
IFS=':' read -ra LANG_INFO <<< "$lang_part"
if [[ ${#LANG_INFO[@]} -eq 3 ]]; then
local lang_name="${LANG_INFO[0]}"
local lang_id="${LANG_INFO[1]}"
local cat_id="${LANG_INFO[2]}"
languages_array+=("{\"name\":\"$lang_name\",\"id\":$lang_id,\"category_id\":$cat_id}")
else
log_error "Invalid language format: $lang_part. Expected: Name:LanguageID:CategoryID"
return 1
fi
done
if [[ ${#languages_array[@]} -eq 0 ]]; then
log_error "No valid target languages found"
return 1
fi
log_info "Parsed ${#languages_array[@]} target language(s)"
# Read source blog content
local source_title=$(jq -r '.title // ""' "$source_json")
local source_content=$(jq -r '.content // ""' "$source_json")
local source_author=$(jq -r '.author // "Sandeep Mundra"' "$source_json")
local source_meta_keywords=$(jq -r '.meta_keywords // ""' "$source_json")
local source_meta_description=$(jq -r '.meta_description // ""' "$source_json")
local source_thumbnail=$(jq -r '.thumbnail_image // ""' "$source_json")
local source_slider_images=$(jq -r '.slider_images // []' "$source_json")
local source_featured=$(jq -r '.is_featured // false' "$source_json")
log_info "Source blog title: $source_title"
# Initialize cost tracking
if [[ "$ENABLE_COST_TRACKING" == "true" ]]; then
rm -f "${TEMP_DIR}/cost_"*.txt
fi
# Translate to each target language
local translation_count=0
for lang_json in "${languages_array[@]}"; do
local lang_name=$(echo "$lang_json" | jq -r '.name')
local lang_id=$(echo "$lang_json" | jq -r '.id')
local cat_id=$(echo "$lang_json" | jq -r '.category_id')
log_info "[$((translation_count + 1))/${#languages_array[@]}] Translating to $lang_name (ID: $lang_id, Category: $cat_id)..."
# Translate content
local translated_title=""
local translated_content=""
local translated_keywords=""
local translated_description=""
if [[ "$translation_method" == "google-translate" ]]; then
log_info "Using Google Cloud Translation API for $lang_name..."
# Use language name directly as language code (Google Translate supports many codes)
# Convert to lowercase for consistency
local target_lang_code=$(echo "$lang_name" | tr '[:upper:]' '[:lower:]')
log_info "Using language code: '$target_lang_code' for Google Translate"
# Translate each field and check for errors
translated_title=$(translate_text_google "$source_title" "$target_lang_code")
if [[ $? -ne 0 ]]; then
log_error "Failed to translate title. Please check the language code '$target_lang_code'"
return 1
fi
translated_content=$(translate_text_google "$source_content" "$target_lang_code")
if [[ $? -ne 0 ]]; then
log_error "Failed to translate content. Please check the language code '$target_lang_code'"
return 1
fi
translated_keywords=$(translate_text_google "$source_meta_keywords" "$target_lang_code")
if [[ $? -ne 0 ]]; then
log_error "Failed to translate keywords. Please check the language code '$target_lang_code'"
return 1
fi
translated_description=$(translate_text_google "$source_meta_description" "$target_lang_code")
if [[ $? -ne 0 ]]; then
log_error "Failed to translate description. Please check the language code '$target_lang_code'"
return 1
fi
# Track translation cost
local total_chars=$((${#source_title} + ${#source_content} + ${#source_meta_keywords} + ${#source_meta_description}))
track_translation "$total_chars" "google-translate"
else
log_info "Using Vertex AI for $lang_name translation..."
# Use language code directly for Vertex AI
local target_lang_code=$(echo "$lang_name" | tr '[:upper:]' '[:lower:]')
log_info "Using language code: '$target_lang_code' for Vertex AI"
# Use Vertex AI for translation with language code
translated_title=$(translate_with_vertex_ai "$source_title" "$target_lang_code")
if [[ $? -ne 0 ]]; then
log_error "Failed to translate title with Vertex AI. Please check the language code '$target_lang_code'"
return 1
fi
translated_content=$(translate_with_vertex_ai "$source_content" "$target_lang_code")
if [[ $? -ne 0 ]]; then
log_error "Failed to translate content with Vertex AI. Please check the language code '$target_lang_code'"
return 1
fi
translated_keywords=$(translate_with_vertex_ai "$source_meta_keywords" "$target_lang_code")
if [[ $? -ne 0 ]]; then
log_error "Failed to translate keywords with Vertex AI. Please check the language code '$target_lang_code'"
return 1
fi
translated_description=$(translate_with_vertex_ai "$source_meta_description" "$target_lang_code")
if [[ $? -ne 0 ]]; then
log_error "Failed to translate description with Vertex AI. Please check the language code '$target_lang_code'"
return 1
fi
# Track translation cost
local total_chars=$((${#source_title} + ${#source_content} + ${#source_meta_keywords} + ${#source_meta_description}))
track_translation "$total_chars" "vertex-ai"
fi
log_success "Translation completed for $lang_name"
# Create translated JSON using jq to properly escape content
local translated_json="${TEMP_DIR}/translated_${lang_name}_${lang_id}.json"
# Determine the post_id to use
local post_id_to_use=""
if [[ -n "$TRANSLATE_POST_ID" ]]; then
# Use the post ID from API fetch
post_id_to_use="$TRANSLATE_POST_ID"
elif [[ -n "$ORIGINAL_POST_ID" ]]; then
# Use the provided original post ID for JSON file translation
post_id_to_use="$ORIGINAL_POST_ID"
fi
# Create JSON using jq for proper escaping (post_id goes in URL, not body)
jq -n \
--arg title "$translated_title" \
--arg author "$source_author" \
--arg content "$translated_content" \
--argjson category_id "$cat_id" \
--argjson language_id "$lang_id" \
--argjson serial_number 1 \
--argjson is_featured "$source_featured" \
--arg thumbnail_image "$source_thumbnail" \
--argjson slider_images "$source_slider_images" \
--arg meta_keywords "$translated_keywords" \
--arg meta_description "$translated_description" \
'{
title: $title,
author: $author,
content: $content,
category_id: $category_id,
language_id: $language_id,
serial_number: $serial_number,
is_featured: $is_featured,
thumbnail_image: $thumbnail_image,
slider_images: $slider_images,
meta_keywords: $meta_keywords,
meta_description: $meta_description
}' > "$translated_json"
# Submit to API
log_info "Submitting translation for language ID: $lang_id (Category: $cat_id)..."
# Determine API endpoint and method based on whether we have a post_id
local api_endpoint=""
local http_method=""
if [[ -n "$post_id_to_use" ]]; then
# Update existing post using PUT to /api/v1/posts/{id}
api_endpoint="${BLOG_API_URL}/${post_id_to_use}"
http_method="PUT"
log_info "Updating existing post ID: $post_id_to_use"
else
# Create new post using POST to /api/v1/posts
api_endpoint="$BLOG_API_URL"
http_method="POST"
log_info "Creating new post"
fi
local response=$(curl -s -w "\n%{http_code}" -X "$http_method" "$api_endpoint" \
-H "X-API-Token: ${BLOG_API_TOKEN}" \
-H "Content-Type: application/json" \
-d @"$translated_json")
local http_code=$(echo "$response" | tail -n1)
local response_body=$(echo "$response" | sed '$d')
if [[ "$http_code" == "200" ]] || [[ "$http_code" == "201" ]]; then
local returned_post_id=$(echo "$response_body" | jq -r '.data.post_id // .data.id // .post_id // .id // "unknown"')
if [[ -n "$post_id_to_use" ]]; then
log_success "Translation updated successfully for Post ID: $post_id_to_use"
else
log_success "Translation submitted successfully! New Post ID: $returned_post_id"
fi
# Save response
echo "$response_body" > "${TEMP_DIR}/translated_${lang_name}_${lang_id}_api_response.json"
else
log_error "Translation submission failed (HTTP $http_code)"
log_error "Response: $response_body"
fi
translation_count=$((translation_count + 1))
done
# Generate cost report if enabled
if [[ "$ENABLE_COST_TRACKING" == "true" ]]; then
local cost_report="${OUTPUT_DIR}/translation_cost_report_$(date +%Y%m%d_%H%M%S).json"
calculate_costs "$cost_report" "Translation" "translation-mode"
log_success "Cost report saved: $cost_report"
fi
log_success "Translation completed for all ${#languages_array[@]} target language(s)"
}
# Translate text using Vertex AI
translate_with_vertex_ai() {
local text="$1"
local target_lang_code="$2"
if [[ -z "$text" ]]; then
echo ""
return 0
fi
# Map language codes to full language names for better AI understanding
local target_language=""
case "$target_lang_code" in
"es") target_language="Spanish" ;;
"fr") target_language="French" ;;
"de") target_language="German" ;;
"pt") target_language="Portuguese" ;;
"zh") target_language="Chinese (Simplified)" ;;
"ja") target_language="Japanese" ;;
"ko") target_language="Korean" ;;
"ar") target_language="Arabic" ;;
"hi") target_language="Hindi" ;;
"ru") target_language="Russian" ;;
"it") target_language="Italian" ;;
"nl") target_language="Dutch" ;;
"en") target_language="English" ;;
"th") target_language="Thai" ;;
"vi") target_language="Vietnamese" ;;
"tr") target_language="Turkish" ;;
"pl") target_language="Polish" ;;
"sv") target_language="Swedish" ;;
"da") target_language="Danish" ;;
"no") target_language="Norwegian" ;;
"fi") target_language="Finnish" ;;
"he") target_language="Hebrew" ;;
"id") target_language="Indonesian" ;;
"ms") target_language="Malay" ;;
"tl") target_language="Filipino" ;;
"uk") target_language="Ukrainian" ;;
"cs") target_language="Czech" ;;
"sk") target_language="Slovak" ;;
"hu") target_language="Hungarian" ;;
"ro") target_language="Romanian" ;;
"bg") target_language="Bulgarian" ;;
"hr") target_language="Croatian" ;;
"sr") target_language="Serbian" ;;
"sl") target_language="Slovenian" ;;
"et") target_language="Estonian" ;;
"lv") target_language="Latvian" ;;
"lt") target_language="Lithuanian" ;;
*)
log_warning "Unknown language code '$target_lang_code' for Vertex AI. Using code directly."
target_language="the language with code '$target_lang_code'"
;;
esac
# Create enhanced translation prompt for Vertex AI
local prompt="You are a professional translator. Translate the following text to $target_language (language code: $target_lang_code).
IMPORTANT INSTRUCTIONS:
1. Preserve ALL HTML tags exactly as they are (including , ,
, , , , - , etc.)
2. Preserve ALL URLs and image links exactly as they are
3. Maintain the original formatting and structure
4. Translate only the text content, not HTML attributes or URLs
5. Ensure the translation is natural, professional, and culturally appropriate
6. Return ONLY the translated text without any additional commentary or explanations
TEXT TO TRANSLATE:
$text"
# Create request body for Vertex AI
local request_body=$(jq -n \
--arg prompt "$prompt" \
'{
"contents": [{
"role": "user",
"parts": [{
"text": $prompt
}]
}],
"generationConfig": {
"temperature": 0.3,
"topP": 0.8,
"topK": 40,
"maxOutputTokens": 8192
}
}')
# Vertex AI endpoint
local endpoint="https://${GCP_LOCATION}-aiplatform.googleapis.com/v1/projects/${GCP_PROJECT_ID}/locations/${GCP_LOCATION}/publishers/google/models/${TEXT_MODEL}:generateContent"
# Call Vertex AI for translation
local response=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$request_body" \
"$endpoint")
# Check for API errors
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
local error_msg=$(echo "$response" | jq -r '.error.message')
log_error "Vertex AI translation error: $error_msg"
return 1
fi
# Extract translated text from response
local translated_text=$(echo "$response" | jq -r '.candidates[0].content.parts[0].text // empty')
# Check if translation was successful
if [[ -z "$translated_text" ]] || [[ "$translated_text" == "null" ]]; then
log_error "⌠Vertex AI failed to translate text for language code '$target_lang_code'"
log_error "📠Please verify the language code is supported by Vertex AI"
log_error "🌠Common codes: es (Spanish), fr (French), de (German), pt (Portuguese), zh (Chinese), ja (Japanese), ko (Korean), ar (Arabic), hi (Hindi), ru (Russian), it (Italian), nl (Dutch)"
return 1
fi
# Clean up the response (remove any extra formatting)
echo "$translated_text" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//'
}
# Translate text using Google Cloud Translation API
translate_text_google() {
local text="$1"
local target_code="$2"
if [[ -z "$text" ]]; then
echo ""
return 0
fi
local response=$(curl -s -X POST \
"https://translation.googleapis.com/language/translate/v2?key=${GOOGLE_TRANSLATE_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"q\": $(echo "$text" | jq -Rs .),
\"source\": \"en\",
\"target\": \"$target_code\",
\"format\": \"text\"
}")
# Check for errors
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
local error_msg=$(echo "$response" | jq -r '.error.message')
local error_code=$(echo "$response" | jq -r '.error.code // "unknown"')
if [[ "$error_msg" == *"language"* ]] || [[ "$error_code" == "400" ]]; then
log_error "⌠Invalid language code '$target_code'"
log_error "📠Please use a valid Google Translate language code."
log_error "🌠Common codes: es (Spanish), fr (French), de (German), pt (Portuguese), zh (Chinese), ja (Japanese), ko (Korean), ar (Arabic), hi (Hindi), ru (Russian), it (Italian), nl (Dutch)"
log_error "📖 Full list: https://cloud.google.com/translate/docs/languages"
else
log_error "Google Translate API error: $error_msg"
fi
return 1
fi
# Extract translated text
echo "$response" | jq -r '.data.translations[0].translatedText // empty'
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# COST TRACKING FUNCTIONS
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# Global cost tracking variables
TOTAL_INPUT_CHARS=0
TOTAL_OUTPUT_CHARS=0
TOTAL_IMAGES=0
TOTAL_TRANSLATIONS=0
TRANSLATION_CHARS=0
track_text_generation() {
local input_content="$1"
local output_content="$2"
local input_chars=$(echo "$input_content" | wc -c)
local output_chars=$(echo "$output_content" | wc -c)
# Store values in temp files to persist across function calls
echo "$input_chars" > "${TEMP_DIR}/cost_input_chars.txt"
echo "$output_chars" > "${TEMP_DIR}/cost_output_chars.txt"
log_info "Text generation: $input_chars input chars, $output_chars output chars"
}
track_image_generation() {
local image_count="$1"
# Store value in temp file to persist across function calls
echo "$image_count" > "${TEMP_DIR}/cost_images.txt"
log_info "Image generation: $image_count images"
}
track_translation() {
local content="$1"
local method="$2"
local char_count=$(echo "$content" | wc -c)
# Read existing values or default to 0
local existing_chars=0
local existing_count=0
if [[ -f "${TEMP_DIR}/cost_translation_chars.txt" ]]; then
existing_chars=$(cat "${TEMP_DIR}/cost_translation_chars.txt")
fi
if [[ -f "${TEMP_DIR}/cost_translation_count.txt" ]]; then
existing_count=$(cat "${TEMP_DIR}/cost_translation_count.txt")
fi
# Add to existing values
local total_chars=$((existing_chars + char_count))
local total_count=$((existing_count + 1))
# Store updated values in temp files
echo "$total_chars" > "${TEMP_DIR}/cost_translation_chars.txt"
echo "$total_count" > "${TEMP_DIR}/cost_translation_count.txt"
log_info "Translation ($method): $char_count characters"
}
calculate_costs() {
local blog_id="$1"
local blog_title="$2"
local post_id="$3"
if [[ "$ENABLE_COST_TRACKING" != "true" ]]; then
return 0
fi
# Read cost tracking data from temp files
local input_chars=0
local output_chars=0
local images=0
local translation_chars=0
local translation_count=0
if [[ -f "${TEMP_DIR}/cost_input_chars.txt" ]]; then
input_chars=$(cat "${TEMP_DIR}/cost_input_chars.txt")
fi
if [[ -f "${TEMP_DIR}/cost_output_chars.txt" ]]; then
output_chars=$(cat "${TEMP_DIR}/cost_output_chars.txt")
fi
if [[ -f "${TEMP_DIR}/cost_images.txt" ]]; then
images=$(cat "${TEMP_DIR}/cost_images.txt")
fi
if [[ -f "${TEMP_DIR}/cost_translation_chars.txt" ]]; then
translation_chars=$(cat "${TEMP_DIR}/cost_translation_chars.txt")
fi
if [[ -f "${TEMP_DIR}/cost_translation_count.txt" ]]; then
translation_count=$(cat "${TEMP_DIR}/cost_translation_count.txt")
fi
# Calculate costs using awk with accurate pricing
local vertex_input_cost=$(awk "BEGIN {printf \"%.6f\", $input_chars * $VERTEX_AI_INPUT_COST_PER_1M_CHARS / 1000000}")
local vertex_output_cost=$(awk "BEGIN {printf \"%.6f\", $output_chars * $VERTEX_AI_OUTPUT_COST_PER_1M_CHARS / 1000000}")
local vertex_text_cost=$(awk "BEGIN {printf \"%.6f\", $vertex_input_cost + $vertex_output_cost}")
local vertex_image_cost=$(awk "BEGIN {printf \"%.2f\", $images * $VERTEX_AI_IMAGE_COST_PER_IMAGE}")
local translation_cost=0
local translation_method="${TRANSLATION_METHOD:-$DEFAULT_TRANSLATION_METHOD}"
if [[ "$translation_method" == "google-translate" ]]; then
translation_cost=$(awk "BEGIN {printf \"%.6f\", $translation_chars * $GOOGLE_TRANSLATE_COST_PER_1M_CHARS / 1000000}")
else
# For Vertex AI translation, use same input/output pricing as text generation
translation_cost=$(awk "BEGIN {printf \"%.6f\", $translation_chars * $VERTEX_AI_OUTPUT_COST_PER_1M_CHARS / 1000000}")
fi
local total_cost=$(awk "BEGIN {printf \"%.6f\", $vertex_text_cost + $vertex_image_cost + $translation_cost}")
# Create cost report JSON
local cost_file="${GENERATED_BLOGS_DIR}/${post_id}_${blog_title//[^a-zA-Z0-9]/_}_cost_report.json"
# Determine rate description
local rate_desc=""
if [[ "$translation_method" == "google-translate" ]]; then
rate_desc="$GOOGLE_TRANSLATE_COST_PER_1M_CHARS per 1M chars"
else
rate_desc="$VERTEX_AI_TEXT_COST_PER_1K_CHARS per 1K chars"
fi
# Create JSON using cat to avoid complex shell escaping
cat > "$cost_file" << EOF
{
"blog_info": {
"blog_id": "$blog_id",
"blog_title": "$blog_title",
"post_id": "$post_id",
"generation_date": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"translation_method": "$translation_method"
},
"usage_stats": {
"text_input_chars": $input_chars,
"text_output_chars": $output_chars,
"images_generated": $images,
"translations_count": $translation_count,
"translation_chars": $translation_chars
},
"cost_breakdown": {
"vertex_ai_text_input": {
"characters": $input_chars,
"rate_per_1m_chars": "$VERTEX_AI_INPUT_COST_PER_1M_CHARS",
"cost_usd": "$vertex_input_cost"
},
"vertex_ai_text_output": {
"characters": $output_chars,
"rate_per_1m_chars": "$VERTEX_AI_OUTPUT_COST_PER_1M_CHARS",
"cost_usd": "$vertex_output_cost"
},
"vertex_ai_images": {
"images": $images,
"rate_per_image": "$VERTEX_AI_IMAGE_COST_PER_IMAGE",
"cost_usd": "$vertex_image_cost"
},
"translations": {
"method": "$translation_method",
"characters": $translation_chars,
"rate": "$rate_desc",
"cost_usd": "$translation_cost"
}
},
"total_cost_usd": "$total_cost",
"cost_summary": {
"text_generation": "$vertex_text_cost",
"image_generation": "$vertex_image_cost",
"translations": "$translation_cost",
"total": "$total_cost"
}
}
EOF
log_success "Cost report saved: $cost_file"
log_info "Total estimated cost: \$${total_cost} USD"
log_info " - Text input: \$${vertex_input_cost} USD ($input_chars chars)"
log_info " - Text output: \$${vertex_output_cost} USD ($output_chars chars)"
log_info " - Image generation: \$${vertex_image_cost} USD ($images images)"
log_info " - Translations ($translation_method): \$${translation_cost} USD ($translation_chars chars)"
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# GOOGLE CLOUD TRANSLATION API
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
generate_google_translate_content() {
local base_content_file="$1"
local blog_id="$2"
local target_language="$3"
local language_id="$4"
log_info "Using Google Cloud Translation API for $target_language..."
# Read the English content
local english_title=$(jq -r '.title' "$base_content_file")
local english_content=$(jq -r '.content' "$base_content_file")
local english_meta_keywords=$(jq -r '.meta_keywords' "$base_content_file")
local english_meta_description=$(jq -r '.meta_description' "$base_content_file")
# Use language code directly (supports both language codes and full names for backward compatibility)
local target_code=""
case "$target_language" in
# Language codes (preferred)
"es"|"fr"|"de"|"it"|"pt"|"nl"|"ru"|"zh"|"ja"|"ko"|"ar"|"hi"|"en"|"th"|"vi"|"tr"|"pl"|"sv"|"da"|"no"|"fi"|"he"|"id"|"ms"|"tl"|"uk"|"cs"|"sk"|"hu"|"ro"|"bg"|"hr"|"sr"|"sl"|"et"|"lv"|"lt")
target_code="$target_language"
;;
# Full language names (backward compatibility)
"Spanish") target_code="es" ;;
"French") target_code="fr" ;;
"German") target_code="de" ;;
"Italian") target_code="it" ;;
"Portuguese") target_code="pt" ;;
"Dutch") target_code="nl" ;;
"Russian") target_code="ru" ;;
"Chinese") target_code="zh" ;;
"Japanese") target_code="ja" ;;
"Korean") target_code="ko" ;;
"Arabic") target_code="ar" ;;
"Hindi") target_code="hi" ;;
*)
# Try using the input directly as a language code
target_code=$(echo "$target_language" | tr '[:upper:]' '[:lower:]')
log_warning "Unknown language '$target_language', trying as language code '$target_code'"
;;
esac
# Function to translate text using Google Cloud Translation API v2
translate_text() {
local text="$1"
local response=$(curl -s -X POST \
"https://translation.googleapis.com/language/translate/v2?key=${GOOGLE_TRANSLATE_API_KEY}" \
-H "Content-Type: application/json" \
-d "{
\"q\": $(echo "$text" | jq -Rs .),
\"source\": \"en\",
\"target\": \"$target_code\",
\"format\": \"html\"
}")
# Check for errors
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
local error_msg=$(echo "$response" | jq -r '.error.message')
log_error "Google Translate API error: $error_msg"
return 1
fi
# Extract translated text
echo "$response" | jq -r '.data.translations[0].translatedText // empty'
}
# Translate each component
log_info "Translating title..."
local translated_title=$(translate_text "$english_title")
if [[ $? -ne 0 ]] || [[ -z "$translated_title" ]]; then
log_error "Google Translate failed for title. Translation method: google-translate (no fallback)"
return 1
fi
log_info "Translating content..."
local translated_content=$(translate_text "$english_content")
if [[ $? -ne 0 ]] || [[ -z "$translated_content" ]]; then
log_error "Google Translate failed for content. Translation method: google-translate (no fallback)"
return 1
fi
log_info "Translating meta keywords..."
local translated_keywords=$(translate_text "$english_meta_keywords")
if [[ $? -ne 0 ]] || [[ -z "$translated_keywords" ]]; then
log_error "Google Translate failed for keywords. Translation method: google-translate (no fallback)"
return 1
fi
log_info "Translating meta description..."
local translated_description=$(translate_text "$english_meta_description")
if [[ $? -ne 0 ]] || [[ -z "$translated_description" ]]; then
log_error "Google Translate failed for description. Translation method: google-translate (no fallback)"
return 1
fi
# Create translated JSON
local translated_file="${TEMP_DIR}/${blog_id}_${target_language}_content.json"
jq -n \
--arg title "$translated_title" \
--arg content "$translated_content" \
--arg keywords "$translated_keywords" \
--arg description "$translated_description" \
'{
"title": $title,
"content": $content,
"meta_keywords": $keywords,
"meta_description": $description
}' > "$translated_file"
# Validate JSON
if ! jq empty "$translated_file" 2>/dev/null; then
log_error "Invalid JSON generated for $target_language translation"
return 1
fi
# Track translation cost
local total_content="$english_title $english_content $english_meta_keywords $english_meta_description"
track_translation "$total_content" "google-translate"
log_success "Google Translate completed for $target_language"
echo "$translated_file"
}
generate_multilingual_content() {
local base_content_file="$1"
local blog_id="$2"
local target_language="$3"
local language_id="$4"
log_step "Generating content for $target_language (ID: $language_id)..."
# Check translation method
local translation_method="${TRANSLATION_METHOD:-$DEFAULT_TRANSLATION_METHOD}"
if [[ "$translation_method" == "google-translate" ]]; then
generate_google_translate_content "$base_content_file" "$blog_id" "$target_language" "$language_id"
else
generate_vertex_ai_translation "$base_content_file" "$blog_id" "$target_language" "$language_id"
fi
}
generate_vertex_ai_translation() {
local base_content_file="$1"
local blog_id="$2"
local target_language_code="$3"
local language_id="$4"
# Read the English content and properly escape for JSON
local english_title=$(jq -r '.title' "$base_content_file")
local english_content=$(jq -r '.content' "$base_content_file")
local english_meta_keywords=$(jq -r '.meta_keywords' "$base_content_file")
local english_meta_description=$(jq -r '.meta_description' "$base_content_file")
# Map language codes to full language names for better AI understanding
local target_language=""
case "$target_language_code" in
"es") target_language="Spanish" ;;
"fr") target_language="French" ;;
"de") target_language="German" ;;
"pt") target_language="Portuguese" ;;
"zh") target_language="Chinese (Simplified)" ;;
"ja") target_language="Japanese" ;;
"ko") target_language="Korean" ;;
"ar") target_language="Arabic" ;;
"hi") target_language="Hindi" ;;
"ru") target_language="Russian" ;;
"it") target_language="Italian" ;;
"nl") target_language="Dutch" ;;
"en") target_language="English" ;;
"th") target_language="Thai" ;;
"vi") target_language="Vietnamese" ;;
"tr") target_language="Turkish" ;;
"pl") target_language="Polish" ;;
"sv") target_language="Swedish" ;;
"da") target_language="Danish" ;;
"no") target_language="Norwegian" ;;
"fi") target_language="Finnish" ;;
"he") target_language="Hebrew" ;;
"id") target_language="Indonesian" ;;
"ms") target_language="Malay" ;;
"tl") target_language="Filipino" ;;
"uk") target_language="Ukrainian" ;;
"cs") target_language="Czech" ;;
"sk") target_language="Slovak" ;;
"hu") target_language="Hungarian" ;;
"ro") target_language="Romanian" ;;
"bg") target_language="Bulgarian" ;;
"hr") target_language="Croatian" ;;
"sr") target_language="Serbian" ;;
"sl") target_language="Slovenian" ;;
"et") target_language="Estonian" ;;
"lv") target_language="Latvian" ;;
"lt") target_language="Lithuanian" ;;
# Backward compatibility - full language names
"Spanish"|"French"|"German"|"Portuguese"|"Chinese"|"Japanese"|"Korean"|"Arabic"|"Hindi"|"Russian"|"Italian"|"Dutch"|"English"|"Thai"|"Vietnamese"|"Turkish"|"Polish"|"Swedish"|"Danish"|"Norwegian"|"Finnish"|"Hebrew"|"Indonesian"|"Malay"|"Filipino"|"Ukrainian"|"Czech"|"Slovak"|"Hungarian"|"Romanian"|"Bulgarian"|"Croatian"|"Serbian"|"Slovenian"|"Estonian"|"Latvian"|"Lithuanian")
target_language="$target_language_code"
;;
*)
log_warning "Unknown language code '$target_language_code' for Vertex AI. Using code directly."
target_language="the language with code '$target_language_code'"
;;
esac
# Load translation prompt template and replace variables
local translation_prompt_template
if [[ -f "${TRANSLATION_PROMPT_FILE:-prompts/translation_prompt.txt}" ]]; then
log_info "Using translation prompt template: ${TRANSLATION_PROMPT_FILE:-prompts/translation_prompt.txt}"
translation_prompt_template=$(load_prompt_template "${TRANSLATION_PROMPT_FILE:-prompts/translation_prompt.txt}")
translation_prompt_template=$(replace_prompt_variables "$translation_prompt_template" "$target_language" "$target_language_code")
else
log_warning "Translation prompt file not found, using fallback prompt"
translation_prompt_template="You are a professional translator and content writer. Translate the following blog post from English to $target_language (language code: $target_language_code) while maintaining the same structure, tone, and style. Keep all HTML tags intact and preserve the image placeholders ({{IMAGE_PROMPT_X}}) exactly as they are."
fi
# Create full translation prompt with content
local translation_prompt="$translation_prompt_template
Original English Content:
Title: $english_title
Meta Keywords: $english_meta_keywords
Meta Description: $english_meta_description
Content to translate:
$english_content
Return ONLY the JSON object, nothing else."
# Generate translation using Vertex AI with proper JSON escaping
local translation_request=$(jq -n \
--arg prompt "$translation_prompt" \
--argjson temp 0.3 \
--argjson max_tokens "$GEMINI_MAX_TOKENS" \
'{
"contents": [{
"role": "user",
"parts": [{
"text": $prompt
}]
}],
"generationConfig": {
"temperature": $temp,
"topK": 40,
"topP": 0.8,
"maxOutputTokens": $max_tokens
}
}')
local endpoint="https://${GCP_LOCATION}-aiplatform.googleapis.com/v1/projects/${GCP_PROJECT_ID}/locations/${GCP_LOCATION}/publishers/google/models/${TEXT_MODEL}:generateContent"
log_info "Calling Vertex AI for $target_language translation..."
local response=$(curl -s -X POST \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json; charset=utf-8" \
-d "$translation_request" \
"$endpoint")
# Save raw response for debugging
local raw_response_file="${TEMP_DIR}/${blog_id}_${target_language}_raw_response.json"
echo "$response" > "$raw_response_file"
# Check for API errors
if echo "$response" | jq -e '.error' > /dev/null 2>&1; then
local error_msg=$(echo "$response" | jq -r '.error.message')
log_error "Vertex AI translation error for $target_language: $error_msg"
return 1
fi
# Extract content from response
local translated_content=$(echo "$response" | jq -r '.candidates[0].content.parts[0].text // empty')
if [[ -z "$translated_content" ]]; then
log_error "No translated content received for $target_language"
return 1
fi
# Clean and validate JSON using comprehensive cleaning functions
log_info "🔧 Cleaning translated JSON for $target_language..."
local cleaned_content=$(echo "$translated_content" | sed 's/```json//g' | sed 's/```//g' | sed '/^$/d')
# Apply comprehensive JSON cleaning
cleaned_content=$(clean_json_text "$cleaned_content")
# Save translated content
local translated_file="${TEMP_DIR}/${blog_id}_${target_language}_content.json"
# Use our comprehensive validation and fixing function
if validate_and_fix_json "$cleaned_content" "$translated_file" "${blog_id}_${target_language}"; then
log_success "✅ Translation JSON for $target_language is valid"
else
log_error "⌠Invalid JSON generated for $target_language translation even after cleaning"
return 1
fi
# Track translation cost
local total_content="$english_title $english_content $english_meta_keywords $english_meta_description"
track_translation "$total_content" "vertex-ai"
log_success "Translation completed for $target_language"
echo "$translated_file"
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# PARSE COMMAND LINE ARGUMENTS
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
parse_arguments() {
TOPIC=""
CATEGORY_ID=""
LANGUAGE_ID=""
LANGUAGES=""
SERIAL_NUMBER=""
CONTENT_TOTAL=""
SLIDER_TOTAL=""
IS_FEATURED="false"
IS_HERO="false"
HERO_TYPE="middle_post"
IS_SLIDER_POST="false"
API_TOKEN=""
CREATED_AT=""
CSV_FILE=""
MODE=""
TRANSLATION_METHOD=""
TRANSLATE_POST_ID=""
TRANSLATE_JSON_FILE=""
TARGET_LANGUAGES=""
ORIGINAL_POST_ID=""
while [[ $# -gt 0 ]]; do
case $1 in
-t|--topic)
TOPIC="$2"
MODE="single"
shift 2
;;
-c|--category-id)
CATEGORY_ID="$2"
shift 2
;;
-l|--language-id)
LANGUAGE_ID="$2"
shift 2
;;
--languages)
LANGUAGES="$2"
shift 2
;;
-s|--serial-number)
SERIAL_NUMBER="$2"
shift 2
;;
--content-total)
CONTENT_TOTAL="$2"
shift 2
;;
--slider-total)
SLIDER_TOTAL="$2"
shift 2
;;
--featured)
IS_FEATURED="$2"
shift 2
;;
--hero)
# Parse hero format: TRUE:middle_post or TRUE:side_post
if [[ "$2" =~ ^TRUE:(.+)$ ]]; then
IS_HERO="true"
HERO_TYPE="${BASH_REMATCH[1]}"
elif [[ "$2" == "TRUE" ]]; then
IS_HERO="true"
HERO_TYPE="middle_post" # default
else
IS_HERO="$2"
fi
shift 2
;;
--slider-post)
IS_SLIDER_POST="$2"
shift 2
;;
--api-token)
API_TOKEN="$2"
shift 2
;;
--created-at)
CREATED_AT="$2"
# Validate date format immediately
if ! validate_cli_date_format "$CREATED_AT"; then
log_error "Invalid date format for --created-at parameter"
exit 1
fi
shift 2
;;
--translation-method)
TRANSLATION_METHOD="$2"
shift 2
;;
--csv)
CSV_FILE="$2"
MODE="bulk"
shift 2
;;
--translate-post-id)
TRANSLATE_POST_ID="$2"
MODE="translate"
shift 2
;;
--translate-json-file)
TRANSLATE_JSON_FILE="$2"
MODE="translate"
shift 2
;;
--target-languages)
TARGET_LANGUAGES="$2"
shift 2
;;
--original-post-id)
ORIGINAL_POST_ID="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
;;
*)
log_error "Unknown option: $1"
show_usage
exit 1
;;
esac
done
# Validate arguments
if [[ -z "$MODE" ]]; then
log_error "No mode specified. Use --topic for single, --csv for bulk, or --translate-* for translation."
show_usage
exit 1
fi
if [[ "$MODE" == "single" ]]; then
if [[ -z "$TOPIC" ]]; then
log_error "Single mode requires: --topic - other parameters can use .env defaults"
show_usage
exit 1
fi
elif [[ "$MODE" == "translate" ]]; then
if [[ -z "$TRANSLATE_POST_ID" && -z "$TRANSLATE_JSON_FILE" ]]; then
log_error "Translation mode requires either --translate-post-id or --translate-json-file"
show_usage
exit 1
fi
if [[ -n "$TRANSLATE_POST_ID" && -n "$TRANSLATE_JSON_FILE" ]]; then
log_error "Cannot use both --translate-post-id and --translate-json-file. Choose one."
show_usage
exit 1
fi
if [[ -z "$TARGET_LANGUAGES" ]]; then
log_error "Translation mode requires --target-languages"
show_usage
exit 1
fi
fi
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# APPLY DEFAULT VALUES FROM .ENV
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
apply_defaults() {
# Apply defaults only if values are not provided
[[ -z "$CATEGORY_ID" ]] && CATEGORY_ID="$DEFAULT_CATEGORY_ID"
[[ -z "$LANGUAGE_ID" ]] && LANGUAGE_ID="$DEFAULT_LANGUAGE_ID"
[[ -z "$LANGUAGES" ]] && LANGUAGES="$DEFAULT_LANGUAGES"
[[ -z "$CONTENT_TOTAL" ]] && CONTENT_TOTAL="$DEFAULT_CONTENT_TOTAL"
[[ -z "$SLIDER_TOTAL" ]] && SLIDER_TOTAL="$DEFAULT_SLIDER_TOTAL"
[[ "$IS_FEATURED" == "false" ]] && [[ "$DEFAULT_FEATURED" == "true" ]] && IS_FEATURED="true"
[[ "$IS_HERO" == "false" ]] && [[ "$DEFAULT_HERO" == "true" ]] && IS_HERO="true"
[[ "$HERO_TYPE" == "middle_post" ]] && [[ -n "$DEFAULT_HERO_TYPE" ]] && HERO_TYPE="$DEFAULT_HERO_TYPE"
[[ "$IS_SLIDER_POST" == "false" ]] && [[ "$DEFAULT_SLIDER_POST" == "true" ]] && IS_SLIDER_POST="true"
[[ -z "$API_TOKEN" ]] && API_TOKEN="$DEFAULT_API_TOKEN"
[[ -z "$CREATED_AT" ]] && CREATED_AT="$DEFAULT_CREATED_AT"
[[ -z "$TRANSLATION_METHOD" ]] && TRANSLATION_METHOD="$DEFAULT_TRANSLATION_METHOD"
log_info "Applied defaults from .env file:"
log_info " Category ID: $CATEGORY_ID"
log_info " Language ID: $LANGUAGE_ID"
log_info " Languages: ${LANGUAGES:-'(none)'}"
log_info " Content Total: $CONTENT_TOTAL"
log_info " Slider Total: $SLIDER_TOTAL"
log_info " Featured: $IS_FEATURED"
log_info " Hero: $IS_HERO ($HERO_TYPE)"
log_info " Slider Post: $IS_SLIDER_POST"
log_info " Translation Method: $TRANSLATION_METHOD"
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# VALIDATE FINAL CONFIGURATION
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
validate_final_config() {
# Validate required parameters after defaults are applied
if [[ "$MODE" == "single" ]]; then
if [[ -z "$CATEGORY_ID" ]] || [[ -z "$LANGUAGE_ID" ]]; then
log_error "Missing required parameters after applying defaults:"
log_error " CATEGORY_ID: ${CATEGORY_ID:-not set}"
log_error " LANGUAGE_ID: ${LANGUAGE_ID:-not set}"
log_error "Please set these in .env file or provide via command line"
exit 1
fi
# Parse additional languages if provided
if [[ -n "$LANGUAGES" ]]; then
local parsed_languages=$(parse_languages "$LANGUAGES" "$CATEGORY_ID")
if [[ $? -ne 0 ]]; then
log_error "Failed to parse languages: $LANGUAGES"
exit 1
fi
log_info "Additional languages parsed: $parsed_languages"
fi
# Validate hero image settings
if [[ "$IS_HERO_POST" == "true" ]] && [[ "$HERO_IMAGE_TYPE" != "middle" ]] && [[ "$HERO_IMAGE_TYPE" != "side" ]]; then
log_error "Hero image type must be 'middle' or 'side'"
exit 1
fi
elif [[ "$MODE" == "bulk" ]]; then
if [[ -z "$CSV_FILE" ]]; then
log_error "Bulk mode requires: --csv "
show_usage
exit 1
fi
fi
}
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
# MAIN EXECUTION
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
main() {
# Start logging
echo "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•" | tee "$LOG_FILE"
echo "Blog Generator Started: $(date '+%Y-%m-%d %H:%M:%S')" | tee -a "$LOG_FILE"
echo "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•" | tee -a "$LOG_FILE"
# Parse arguments
parse_arguments "$@"
# Apply defaults from .env file
apply_defaults
# Validate final configuration
validate_final_config
# Setup
check_dependencies
setup_directories
validate_configuration
log_info "Using Vertex AI with project: $GCP_PROJECT_ID in region: $GCP_LOCATION"
log_info "Text model: $TEXT_MODEL"
log_info "Image model: $IMAGE_MODEL"
# Execute based on mode
local exit_code=0
if [[ "$MODE" == "single" ]]; then
# Check if multilingual generation is requested
if [[ -n "$LANGUAGES" ]]; then
local parsed_languages=$(parse_languages "$LANGUAGES" "$CATEGORY_ID")
if ! process_multilingual_blog "$TOPIC" "$CATEGORY_ID" "$LANGUAGE_ID" "$parsed_languages" "$SERIAL_NUMBER"; then
exit_code=1
fi
else
# Single language generation
if ! process_single_blog "$TOPIC" "$CATEGORY_ID" "$LANGUAGE_ID" "$SERIAL_NUMBER"; then
exit_code=1
fi
fi
elif [[ "$MODE" == "bulk" ]]; then
if ! process_csv_bulk "$CSV_FILE"; then
exit_code=1
fi
elif [[ "$MODE" == "translate" ]]; then
# Translation mode
local source_json=""
if [[ -n "$TRANSLATE_POST_ID" ]]; then
# Fetch from API
source_json=$(fetch_blog_from_api "$TRANSLATE_POST_ID")
if [[ $? -ne 0 ]]; then
exit_code=1
fi
else
# Use provided JSON file
source_json="$TRANSLATE_JSON_FILE"
fi
if [[ -n "$source_json" && $exit_code -eq 0 ]]; then
if ! translate_existing_blog "$source_json" "$TARGET_LANGUAGES" "$TRANSLATION_METHOD"; then
exit_code=1
fi
fi
fi
# Cleanup temporary files (optional)
# Uncomment to auto-cleanup
# rm -rf "$TEMP_DIR"
# log_info "Temporary files cleaned up"
# Final message
echo "" | tee -a "$LOG_FILE"
echo "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•" | tee -a "$LOG_FILE"
if [[ $exit_code -eq 0 ]]; then
echo "✓ Blog Generator Completed Successfully: $(date '+%Y-%m-%d %H:%M:%S')" | tee -a "$LOG_FILE"
else
echo "✗ Blog Generator Completed with Errors: $(date '+%Y-%m-%d %H:%M:%S')" | tee -a "$LOG_FILE"
fi
echo "â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•â•" | tee -a "$LOG_FILE"
exit $exit_code
}
# Run main function with all arguments
main "$@"