Skip to main content

Auto Generator API

Overview

The Auto Generator API lets you generate polished business presentations from structured data and natural language prompts. It's designed to align with your organization's templates, branding, and audience context.

Use it to:

  • Automatically create decks based on user prompts
  • Track generation progress using callback IDs
  • Retrieve metadata like audiences for targeted presentations
  • Regenerate specific slides or entire presentations
  • Download completed presentations

Authentication

All endpoints require API key-based authentication. Add the API key in the request Authorization header using the Bearer token format:

Example:

Authorization: Bearer your-api-key

Common Headers

All responses include CORS headers:

{
"Access-Control-Allow-Origin": "*"
}

Key Endpoints

  • POST /api/v1/autogenerator – Create a new presentation
  • GET /api/v1/autogenerator/status – Track generation progress
  • POST /api/v1/autogenerator/download – Download generated presentation
  • POST /api/v1/autogenerator/node-change – Get node change options for a slide
  • POST /api/v1/autogenerator/regenerate – Regenerate slides or presentation
  • GET /api/v1/themes – Retrieve themes
  • POST /api/v1/upload – Upload file for processing
  • POST /api/v1/preprocess – Upload file chunks for preprocessing
  • POST /api/v1/validate – Validate and finalize uploaded files and links
  • POST /api/v1/autogenerator/reaction-feedback – Submit feedback and reactions
  • GET /api/v1/audiences – Retrieve target audience metadata

Auto Generator – Create Presentation

Create a new presentation generation job.

POST /api/v1/autogenerator

Request Body

{
"prompt": "string", // Required: Slide content prompt
"template_id": "string", // Optional: Slide design template ID. Takes priority over "settings.template" if both are provided.
"texts": ["string"], // Optional: Additional context text
"files": [ // Optional: Array of file objects from upload API response
{
"file_id": "string", // Required: File ID from upload response
"file_name": "string", //Required: Original file name
"file_type": "string", // Required: MIME type (e.g., "application/pdf")
"s3_path": "string", // Required: S3 storage path
"s3_bucket": "string" // Required: S3 bucket name
}
],
"web_links": ["string"], // Optional: URLs for contextual info
"audience": { // Optional: Target audience object from audience API response
"id": "string", // Audience ID (email or identifier)
"fullName": "string", // Full name of the audience
"audienceId": "string", // Unique audience identifier
"num_id": "number", // Numeric ID
"type": "string", // Type (e.g., "user", "group")
"ownerID": "string", // Owner ID (nullable)
"isActive": "boolean", // Whether audience is active
"isPrivate": "boolean", // Privacy setting
"isShared": "boolean", // Sharing status
"fingerPrint": "string", // Audience fingerprint (e.g., "Producer")
"fingerprintResponse": "array", // Fingerprint response data
"shared": "boolean" // Shared status
},
"layout_file":{"file_id":"string"}, // Optional: Layout file_id from validate API to preserve the same layout structure during generation
"settings":{
"template":"string", // Optional: Default presentation template to override account settings (e.g., "prezent_corporate_2022").
"ai_model":"string", // Optional: AI model to use for content generation (e.g., "astrid_standard", "astrid_advanced")
"image_preferences":["string"], // Optional: Preferred image sources for visual content (e.g., ["prezent_library", "company_library"])
"knowledge_base":"string", // Optional: Knowledge base scope for contextual generation (e.g., "uploaded_and_web", "uploaded")
"data_sources":["string"], // Optional: Configure how data sources are added to the presentation (e.g., ["add_sources_to_footer", "add_sources_to_slides_note"])
"preserve_text": "boolean", // Optional: Preserve original text content from prompt or add context
"speaker_notes":["string"], // Optional: Configure speaker notes generation and placement (e.g., ["add_speaker_notes","add_speaker_notes_to_slides_note"])
"voice_settings": { // Optional: Voice and tone settings for content generation
"type": "string", // Type of voice setting: "brand", "standard", or "custom"
"details": { // Required only when type is "custom"
"tone": "string", // Tone of voice (e.g., "neutral",)
"emotion": "string" // Emotional tone (e.g., "optimistic")
}
},
}
}

Auto Generator – Check Status

Track the progress of a previously submitted presentation generation request using the callback_id.

GET /api/v1/autogenerator/status?callback_id={callback_id}

Query Parameters

callback_id (required): The callback ID from the generation request

Auto Generator – Check Regeneration Status

Track the progress of a previously submitted regeneration request using the regeneration callback_id and the original deck's deck_callback_id.

GET /api/v1/autogenerator/status?callback_id={callback_id}&deck_callback_id={deck_callback_id}

Query Parameters

callback_id (required): The regeneration callback ID from the regeneration request

deck_callback_id (required): The deck callback ID of the original deck

Download Generated Presentation

Download a merged presentation file.

POST /api/v1/autogenerator/download
{
"callback_id": "string", // Required: Callback ID for tracking
}

Get Node Change Options

Retrieve available node change options for a specific slide. This API returns node variations that can be used to change the node configuration of a slide.

POST /api/v1/autogenerator/node-change

Request Body

{
"callback_id": "string", // Required: Presentation callback ID
"slide_callback_id": "string" // Required: Slide callback ID for which to get node change options
}

Regenerate Presentation

Regenerate specific slides or entire presentations with new parameters.

POST /api/v1/autogenerator/regenerate?operation={operation}

Query Parameters

- `operation` (required): Type of regeneration operation

Supported Operations:

- `audience_change` - Change target audience
- `template_change` - Change presentation template
- `slide_change` - Change specific slides
- `context_change` - Change context for slides
- `smart_chart_change` - Change smart chart configurations
- `node_change` - Change node configurations
- `story_content_change` - Change story content
- `add_slide` - Add a new slide to the presentation
- `duration_change` - Change presentation duration
- `slides_footer_change` - Update data sources footer settings
- `voice_change` - Change voice and tone settings

**Request Body**

**audience_change:**
```json
{
"callback_id": "string", // Required: Original callback ID
"audience": { // Required: Audience object
"id": "string", // Email or identifier (e.g., "user@example.com")
"num_id": "number", // Numeric ID
"type": "string", // Type: "user" or "group"
"ownerID": "string", // Owner ID (can be empty string)
"email": "string", // Email address
"fingerPrint": "string", // Fingerprint (e.g., "Surgeon", "Producer")
"fingerprintResponse": "array" // Fingerprint response data
}
}

template_change:

{
"callback_id": "string", // Required: Original callback ID
"template_code": "string" // Required: Template code to switch to
}

slide_change:

{
"callback_id": "string", // Required: Original callback ID
"slide_override": { // Required: Slide override configuration
"workflow": "string", // slide callback ID
"slide": "string", // Slide source identifier
"ui_slide_position": "number", // Slide position (0-based index)
"ui_total_slides": "number" // Total number of slides in presentation
}
}

node_change:

{
"callback_id": "string", // Required: Original callback ID
"slide_override": { // Required: Node configuration
"workflow": "string", // slide callback ID
"node_count": "number", // Number of nodes
"ui_slide_position": "number", // Slide position (0-based index)
"ui_total_slides": "number", // Total number of slides
"unique_id": "string" // Slide source identifier
}
}

smart_chart_change:

{
"callback_id": "string", // Required: Original callback ID
"slide_override": { // Required: Chart configuration
"workflow": "string", // slide callback ID
"slide_type": "string", // Chart type (e.g., "stacked_column", "bar", "line")
"ui_slide_position": "number", // Slide position (0-based index)
"ui_total_slides": "number" // Total number of slides
}
}

context_change:

{
"callback_id": "string", // Required: Original callback ID
"context": { // Required: Context information
"context": "string", // Context text/prompt
"extract_graph_data": "boolean", // Whether to extract graph data
"externalContext": "boolean", // Whether to use external context
"files": "array", // Array of file objects
"web_links": "array", // Array of web links
"texts": "array" // Array of text strings
},
"slide_override": { // Required: Slide configuration
"workflow": "string", // slide callback ID
"ui_slide_position": "number", // Slide position (0-based index)
"ui_total_slides": "number" // Total number of slides
}
}

add_slide:

{
"callback_id": "string", // Required: Original callback ID
"context": { // Required: New slide context
"context": "string", // Slide content prompt
"extract_graph_data": "boolean", // Whether to extract graph data
"externalContext": "boolean", // Whether to use external context
"files": "array", // Array of file objects
"web_links": "array", // Array of web links
"texts": "array" // Array of text strings
},
"slide_position": "number" // Required: Position to insert new slide
}

duration_change:

{
"callback_id": "string", // Required: Original callback ID
"duration": { // Required: Duration settings
"no_of_slides": "number" // Number of slides (1 slide = 3 min)
}
}

slides_footer_change:

{
"callback_id": "string", // Required: Original callback ID
"data_sources_settings": { // Required: Data sources settings
"add_sources_to_footer": "boolean", // Add sources to footer
"add_sources_to_slides_note": "boolean" // Add sources to slide notes
}
}

voice_change:

{
"callback_id": "string", // Required: Original callback ID
"voice_settings": { // Required: Voice settings
"type": "string", // Voice type: "standard", "brand", or "custom"
"details": { // Required only when type is "custom"
"tone": "string", // Tone of voice (e.g., "neutral", "formal")
"emotion": "string" // Emotional tone (e.g., "optimistic", "confident")
}
}
}

story_content_change:

{
"callback_id": "string", // Required: Original callback ID
"story_content_override": [ // Required: Array of story content objects
{
"title": "string", // Main title of the slide
"subtitle": "string", // Subtitle (can be null)
"content": [ // Array of content strings
"string"
],
"section_header": [ // Array of section headers
"string"
],
"hub_title": "string", // Hub title (can be empty string)
"slide_type": "string", // Slide type (e.g., "normal")
"ref_id": "string", // Reference ID
"workflow": "string", // Slide callback ID
"ui_slide_position": "number", // Slide position (0-based index)
"ui_total_slides": "number" // Total number of slides
}
]
}

Retrieve Themes

Retrieve themes associated with the company.

GET /api/v1/autogenerator/templates

Query Parameters

All query parameters are optional:

ParameterTypeDescription
sortStringSort order for themes by name. Valid values: name:asc (ascending) or name:desc (descending)
nameStringSearch for templates by name

Upload File

Upload a file for processing.

POST /api/v1/upload
{
"fileContent": "string", // Required: Base64 encoded file content
"fileName": "string" // Required: Name of the file with extension
}

Supported File Types: pptx, docx, txt, pdf, xls, xlsx, csv

Preprocess File

Upload file content in chunks to the server for processing. This API handles chunked file uploads, validates file types and filenames, and prepares files for final processing. A unique code is automatically prepended to the filename to ensure uniqueness.

POST /api/v1/preprocess

Request Body

{
"fileContent": "string", // Required: Base64-encoded file content
"fileName": "string", // Required: Name of the file including extension (max 100 characters)
"chunkIndex": "number", // Required: Zero-based index of the current chunk (>= 0)
"totalChunks": "number", // Required: Total number of chunks for this file (> 0)
"requestIdentifier": "string" // Required: Unique identifier for this upload request (UUID format)
}

Field Validation Rules

FieldTypeValidation Rules
fileContentStringRequired, must not be empty, must include data URL format
fileNameStringRequired, 1-100 characters, no special characters, no multiple extensions
chunkIndexNumberRequired, must be >= 0
totalChunksNumberRequired, must be > 0
requestIdentifierStringRequired, UUID format

Supported File Types: pptx, ppt, docx, pdf, txt, xlsx, csv

Important Notes:

For files under 5MB: Send as a single chunk (totalChunks = 1, chunkIndex = 0).

For files over 5MB: Split into 5MB chunks and send parallel requests with the same requestIdentifier.

Wait for all chunk uploads to complete successfully before calling the validate API.

A unique 5-character code is prepended to the filename (e.g., A1B2C_quarterly-report.pdf).

Validate File

Validates and finalizes uploaded files and web links. This API processes file identifiers (from previous preprocess operations), completes the file upload process, validates file integrity, and returns detailed file information including S3 paths. It also supports web link validation.

POST /api/v1/validate

Request Body

Option 1: Validate Files Only

{
"fileIdentifiers": { // Object mapping request identifiers to file names
"string": "string" // "requestIdentifier": "fileName.ext"
}
}

Option 2: Validate Web Links Only

{
"webLinks": ["string"] // Array of web URLs to validate
}

Option 3: Validate Both Files and Web Links

{
"fileIdentifiers": { // Optional: Object mapping request identifiers to file names
"string": "string"
},
"webLinks": ["string"] // Optional: Array of web URLs to validate
}

Field Validation Rules

FieldTypeValidation Rules
fileIdentifiersObjectOptional, must be an object if provided, at least one of fileIdentifiers or webLinks is required
webLinksArrayOptional, must be an array if provided, at least one of fileIdentifiers or webLinks is required

Notes:

At least one of fileIdentifiers or webLinks must be provided.

Can validate multiple files and links in a single request.

File identifiers should use the requestIdentifier from the preprocess API response.

File names should be the original file names (without the prepended code).

Reaction Feedback

Submit user feedback and reactions for generated presentations. This endpoint supports likes/dislikes and detailed textual feedback.

POST /api/v1/autogenerator/reaction-feedback

Request Body

Option 1: Like

{
"uuid": "string", // Required: Presentation callback ID
"type": "liked", // Required: Feedback type
"value": "boolean" // Required: true for like, false for dislike
}

Option 2: Feedback

{
"uuid": "string", // Required: Presentation callback ID
"type": "feedback", // Required: Feedback type
"value": "string", // Required: Detailed feedback text
"shareDetails": "boolean" // Required: Whether to share feedback details
}

Retrieve Audiences

Use this to fetch a list of audiences in your organization (used in generation context).

GET /api/v1/audiences

Query Parameters

id (optional): Search for specific audience by ID

sort (optional): Sort order for results

Common Error Responses

Unauthorized Access

{
"statusCode": 401,
"body": {
"error": {
"code": "UNAUTHORIZED",
"message": "Unauthorized access."
}
}
}

Method Not Allowed

{
"statusCode": 405,
"body": {
"error": {
"code": "METHOD_NOT_ALLOWED",
"message": "HTTP method not allowed."
}
}
}

Endpoint Not Found

{
"statusCode": 404,
"body": {
"error": {
"code": "ENDPOINT_NOT_FOUND",
"message": "API path not found."
}
}
}

Invalid Input

{
"statusCode": 422,
"body": {
"error": {
"code": "INVALID_INPUT",
"message": "Invalid input provided.",
"details": ["error details"]
}
}
}

Internal Server Error

{
"statusCode": 500,
"body": {
"error": {
"code": "INTERNAL_SERVER_ERROR",
"message": "An unexpected server error occurred.",
"details": "error message"
}
}
}

Sample Test Script

#!/usr/bin/env python3
"""
Test script for the complete /api/v1/autogenerator workflow
Tests the full process: generate -> poll status -> download presentation
"""

import requests
import json
import base64
import os
import time
from typing import Dict, List, Optional

class AutogeneratorAPITester:
def __init__(self, base_url: str, api_key: str):
"""
Initialize the API tester

Args:
base_url: Base URL of the API (e.g., 'https://api.prezent.ai')
api_key: API key for authentication
"""
self.base_url = base_url.rstrip('/')
self.api_key = api_key
self.headers = {
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
}

def test_basic_autogenerator(self, prompt: str, template_id: str) -> Dict:
"""
Test autogenerator API with minimal required parameters

Args:
prompt: The slide content prompt (required)
template_id: The slide design template ID (required)

Returns:
API response as dictionary
"""
payload = {
"prompt": prompt,
"template_id": template_id
}

return self._make_request(payload)

def poll_status(self, callback_id: str, max_attempts: int = 150) -> Dict:
"""
Poll the status endpoint every 2 seconds until status is "success"

Args:
callback_id: The callback ID received from the generate call
max_attempts: Maximum number of polling attempts (default: 150 = 5 minutes)

Returns:
Final status response or error
"""
url = f"{self.base_url}/api/v1/autogenerator/status"

print(f"Polling status for callback ID: {callback_id}")

attempt = 1
while attempt <= max_attempts:
try:
response = requests.get(url, headers=self.headers, params={"callback_id": callback_id}, timeout=30)

if response.status_code == 200:
status_data = response.json()
current_status = status_data.get('resData', {}).get('status', 'unknown')

if current_status == "success":
print(f"Status: {current_status}")
return status_data
elif current_status == "failed":
print(f"Status: {current_status}")
return status_data
elif current_status in ["pending", "processing", "in_progress"]:
time.sleep(2)
else:
time.sleep(2)
else:
time.sleep(2)

except Exception as e:
time.sleep(2)

attempt += 1

return {"error": True, "message": "Max polling attempts reached"}

def download_presentation(self, callback_id: str) -> Dict:
"""
Call the download endpoint to get the final download URL

Args:
callback_id: The callback ID received from the generate call

Returns:
Download response with download_url
"""
url = f"{self.base_url}/api/v1/autogenerator/download"

payload = {
"callback_id": callback_id
}

try:
response = requests.post(url, headers=self.headers, json=payload, timeout=30)

if response.status_code == 200:
download_data = response.json()
return download_data
else:
return {"error": True, "status_code": response.status_code, "response": response.text}

except Exception as e:
return {"error": True, "exception": str(e)}

def complete_autogenerator_workflow(self, prompt: str, template_id: str) -> Dict:
"""
Complete autogenerator workflow: generate -> poll status -> download

Args:
prompt: The slide content prompt
template_id: The slide design template ID

Returns:
Final result with download URL
"""
print(f"Starting autogenerator workflow...")

# Step 1: Generate presentation
generate_result = self.test_basic_autogenerator(prompt, template_id)

if generate_result.get("error"):
return generate_result

# Extract callback ID
callback_id = generate_result.get('resData', {}).get('callback_id')
if not callback_id:
return {"error": True, "message": "No callback ID received"}

print(f"Generation initiated. Callback ID: {callback_id}")

# Step 2: Poll status until completion
status_result = self.poll_status(callback_id)

if status_result.get("error"):
return status_result

# Step 3: Download presentation
download_result = self.download_presentation(callback_id)

if download_result.get("error"):
return download_result

print("Workflow completed successfully!")
return download_result

def _make_request(self, payload: Dict) -> Dict:
"""
Make the actual HTTP request to the API

Args:
payload: Request payload

Returns:
API response as dictionary
"""
url = f"{self.base_url}/api/v1/autogenerator"

try:
response = requests.post(url, headers=self.headers, json=payload, timeout=30)

if response.status_code == 200:
response_data = response.json()
return response_data
else:
return {"error": True, "status_code": response.status_code, "response": response.text}

except Exception as e:
return {"error": True, "exception": str(e)}

def main():
"""Main test function"""
# Configuration - Update these values
BASE_URL = "https://api.prezent.ai" # Replace with your actual API domain
API_KEY = "your-api-key-here" # Replace with your actual API key

# Test data
test_prompt = "Create a 2 slide presentation about artificial intelligence and its applications in modern business"
test_template_id = "template_123" # Replace with actual template ID

# Initialize tester
tester = AutogeneratorAPITester(BASE_URL, API_KEY)

# Test complete workflow
result = tester.complete_autogenerator_workflow(test_prompt, test_template_id)

if not result.get("error"):
download_url = result.get('resData', {}).get('download_url', 'N/A')
print(f"Download URL: {download_url}")
else:
print(f"Error: {result.get('message', 'Unknown error')}")

if __name__ == "__main__":
# Check if configuration is set
if "api.prezent.ai" in BASE_URL or "your-api-key-here" in API_KEY:
print("Please update the BASE_URL and API_KEY variables in the script before running!")
print(" BASE_URL: Your API domain (e.g., 'https://api.prezent.ai')")
print(" API_KEY: Your authentication API key")
exit(1)

main()

File Upload & Validation Script

#!/usr/bin/env python3
"""
AutoGenerator File Upload & Validation Tool
============================================

A standalone script to upload and validate files for the AutoGenerator API.
Supports chunked uploads with retry logic, batch processing, and validation.

Author: Prezent Engineering Team
Version: 1.0.0
Python: 3.6+

Quick Start:
-----------
1. Upload a file:
python3 autogen_file_uploader.py --file document.pdf --api-key YOUR_KEY

2. Validate a link:
python3 autogen_file_uploader.py --link https://example.com/doc --api-key YOUR_KEY

3. Batch upload:
python3 autogen_file_uploader.py --batch files.txt --api-key YOUR_KEY

For detailed help:
python3 autogen_file_uploader.py --help
"""

import requests
import json
import base64
import os
import sys
import time
import uuid
from typing import Dict, List, Optional, Tuple
from pathlib import Path


class FileUploadTester:
"""
Handles file upload with chunking and validation for AutoGenerator API
"""

# File limits
MAX_FILE_SIZE_MB = 200
MAX_PAGES = 100

# Chunking configuration
CHUNK_SIZE = 5 * 1024 * 1024 # 5MB chunks
BATCH_SIZE = 5 # 5 chunks per batch
MAX_RETRIES = 3
RETRY_DELAY = 1 # seconds

# Supported file types
SUPPORTED_EXTENSIONS = [".pptx", ".docx", ".txt", ".pdf", ".xls", ".xlsx", ".csv"]

def __init__(self, base_url: str, api_key: str, verbose: bool = True):
"""
Initialize the file upload tester

Args:
base_url: Base URL of the API (e.g., 'https://api.prezent.ai')
api_key: API key for authentication
verbose: Enable verbose output
"""
self.base_url = base_url.rstrip("/")
self.api_key = api_key
self.verbose = verbose
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
}

def log(self, message: str, force: bool = False):
"""Print message if verbose mode is enabled"""
if self.verbose or force:
print(message)

def validate_file(self, file_path: str) -> Tuple[bool, Optional[str]]:
"""Validate file before upload"""
file_path_obj = Path(file_path)

if not file_path_obj.exists():
return False, f"File does not exist: {file_path}"

file_ext = file_path_obj.suffix.lower()
if file_ext not in self.SUPPORTED_EXTENSIONS:
return (
False,
f"Unsupported file type: {file_ext}. Supported: {', '.join(self.SUPPORTED_EXTENSIONS)}",
)

file_size_mb = file_path_obj.stat().st_size / (1024 * 1024)
if file_size_mb > self.MAX_FILE_SIZE_MB:
return (
False,
f"File exceeds size limit: {file_size_mb:.2f}MB > {self.MAX_FILE_SIZE_MB}MB",
)

return True, None

def read_file_as_base64(self, file_path: str) -> str:
"""Read file and convert to base64 data URL"""
with open(file_path, "rb") as file:
file_content = file.read()
base64_content = base64.b64encode(file_content).decode("utf-8")

file_ext = Path(file_path).suffix.lower()
mime_types = {
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
".pdf": "application/pdf",
".txt": "text/plain",
".xls": "application/vnd.ms-excel",
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
".csv": "text/csv",
}

mime_type = mime_types.get(file_ext, "application/octet-stream")
data_url = f"data:{mime_type};base64,{base64_content}"

return data_url

def create_chunks(
self, data_url: str, file_name: str, request_id: str
) -> List[Dict]:
"""Split file data into chunks"""
chunks = []
chunk_index = 0

for start in range(0, len(data_url), self.CHUNK_SIZE):
chunk_data = data_url[start : start + self.CHUNK_SIZE]
chunks.append(
{
"chunk": chunk_data,
"chunkIndex": chunk_index,
"requestIdentifier": request_id,
"filename": file_name,
}
)
chunk_index += 1

return chunks

def upload_chunk_with_retry(self, chunk: Dict, total_chunks: int) -> Dict:
"""Upload a single chunk with retry logic"""
url = f"{self.base_url}/api/v1/preprocess"

payload = {
"fileContent": chunk["chunk"],
"requestIdentifier": chunk["requestIdentifier"],
"fileName": chunk["filename"],
"chunkIndex": chunk["chunkIndex"],
"totalChunks": total_chunks,
}

for attempt in range(self.MAX_RETRIES):
try:
response = requests.post(
url, headers=self.headers, json=payload, timeout=60
)

if response.status_code == 200:
return response.json()
elif attempt < self.MAX_RETRIES - 1:
self.log(
f" Chunk {chunk['chunkIndex']} failed with status {response.status_code} (attempt {attempt + 1}/{self.MAX_RETRIES}), retrying..."
)
time.sleep(self.RETRY_DELAY)
else:
# Parse error response
try:
error_data = response.json()
except:
error_data = response.text

return {
"error": True,
"status_code": response.status_code,
"api_response": error_data,
"chunk_index": chunk["chunkIndex"],
"url": url,
}

except Exception as e:
if attempt < self.MAX_RETRIES - 1:
self.log(
f" Chunk {chunk['chunkIndex']} exception (attempt {attempt + 1}/{self.MAX_RETRIES}), retrying..."
)
time.sleep(self.RETRY_DELAY)
else:
return {
"error": True,
"exception": str(e),
"chunk_index": chunk["chunkIndex"],
"url": url,
}

return {"error": True, "message": "Max retries exceeded"}

def upload_batch(
self, batch: List[Dict], total_chunks: int, batch_num: int
) -> List[Dict]:
"""Upload a batch of chunks"""
self.log(f" Uploading batch {batch_num} ({len(batch)} chunks)...")
responses = []

for chunk in batch:
response = self.upload_chunk_with_retry(chunk, total_chunks)
responses.append(response)

if response.get("error"):
self.log(f" ✗ Chunk {chunk['chunkIndex']} failed")
else:
self.log(f" ✓ Chunk {chunk['chunkIndex']} uploaded")

return responses

def upload_file_chunks(self, file_path: str) -> Tuple[bool, str, List[Dict]]:
"""Upload file in chunks"""
file_name = Path(file_path).name
request_id = str(uuid.uuid4())

self.log(f"\nReading file: {file_name}")
data_url = self.read_file_as_base64(file_path)

self.log(
f"Creating chunks (chunk size: {self.CHUNK_SIZE / (1024**2):.1f}MB)..."
)
chunks = self.create_chunks(data_url, file_name, request_id)
total_chunks = len(chunks)
self.log(f"Created {total_chunks} chunks")

batches = [
chunks[i : i + self.BATCH_SIZE]
for i in range(0, len(chunks), self.BATCH_SIZE)
]
self.log(f"Created {len(batches)} batches")

self.log("\nUploading chunks...")
all_responses = []

for batch_num, batch in enumerate(batches, 1):
batch_responses = self.upload_batch(batch, total_chunks, batch_num)
all_responses.extend(batch_responses)

errors = [r for r in batch_responses if r.get("error")]
if errors:
self.log(
f"\n✗ Batch {batch_num} had {len(errors)} failed chunks", force=True
)
# Show error details
for error in errors:
self.log(
f"\n Error details for chunk {error.get('chunk_index')}:",
force=True,
)
if error.get("status_code"):
self.log(
f" HTTP Status: {error.get('status_code')}", force=True
)
if error.get("api_response"):
self.log(
f" API Response: {json.dumps(error.get('api_response'), indent=6)}",
force=True,
)
if error.get("exception"):
self.log(f" Exception: {error.get('exception')}", force=True)
return False, request_id, all_responses

self.log(f" ✓ Batch {batch_num} completed successfully")
progress = (batch_num / len(batches)) * 100
self.log(f" Progress: {progress:.1f}%")

self.log("\n✓ All chunks uploaded successfully!")
return True, request_id, all_responses

def validate_uploaded_file(self, request_id: str, file_name: str) -> Dict:
"""Validate the uploaded file"""
url = f"{self.base_url}/api/v1/validate"
payload = {"fileIdentifiers": {request_id: file_name}}

self.log(f"\nValidating file...")

try:
response = requests.post(
url, headers=self.headers, json=payload, timeout=60
)

if response.status_code == 200:
validation_data = response.json()
self.log("✓ Validation completed")
return validation_data
else:
# Parse error response
try:
error_data = response.json()
except:
error_data = response.text

return {
"error": True,
"stage": "validation",
"status_code": response.status_code,
"api_response": error_data,
"url": url,
"message": f"Validation API returned {response.status_code}",
}

except Exception as e:
return {
"error": True,
"stage": "validation",
"exception": str(e),
"url": url,
"message": f"Validation request failed: {str(e)}",
}

def validate_link(self, web_url: str) -> Dict:
"""Validate a web link"""
self.log("=" * 80, force=True)
self.log("LINK VALIDATION", force=True)
self.log("=" * 80, force=True)

url = f"{self.base_url}/api/v1/validate"
payload = {"webLinks": [web_url]}

self.log(f"\nValidating link: {web_url}")

try:
response = requests.post(
url, headers=self.headers, json=payload, timeout=60
)

if response.status_code == 200:
validation_data = response.json()
self.log("✓ Link validation completed")

# Print validation results
self.print_validation_results(validation_data)

return validation_data
else:
# Parse error response
try:
error_data = response.json()
except:
error_data = response.text

return {
"error": True,
"stage": "link_validation",
"status_code": response.status_code,
"api_response": error_data,
"url": url,
"message": f"Link validation API returned {response.status_code}",
}

except Exception as e:
return {
"error": True,
"stage": "link_validation",
"exception": str(e),
"url": url,
"message": f"Link validation request failed: {str(e)}",
}

def upload_and_validate_file(self, file_path: str) -> Dict:
"""Complete workflow: validate -> upload -> validate"""
self.log("=" * 80, force=True)
self.log("FILE UPLOAD AND VALIDATION", force=True)
self.log("=" * 80, force=True)

# Step 1: Pre-upload validation
self.log("\n[Step 1] Pre-upload validation...")
is_valid, error_msg = self.validate_file(file_path)

if not is_valid:
return {"error": True, "stage": "pre-validation", "message": error_msg}

file_size_mb = Path(file_path).stat().st_size / (1024 * 1024)
self.log(f"✓ File is valid")
self.log(f" File: {Path(file_path).name}")
self.log(f" Size: {file_size_mb:.2f}MB")
self.log(f" Type: {Path(file_path).suffix}")

# Step 2: Upload file chunks
self.log("\n[Step 2] Uploading file chunks...")
success, request_id, responses = self.upload_file_chunks(file_path)

if not success:
return {
"error": True,
"stage": "upload",
"message": "Failed to upload all chunks",
"request_id": request_id,
"responses": responses,
}

# Step 3: Validate uploaded file
self.log("\n[Step 3] Validating uploaded file...")
validation_result = self.validate_uploaded_file(
request_id, Path(file_path).name
)

if validation_result.get("error"):
return validation_result

# Print validation results
self.print_validation_results(validation_result)

return validation_result

def print_validation_results(self, validation_result: Dict):
"""Print validation results in a formatted way"""
self.log("\n" + "=" * 80, force=True)
self.log("VALIDATION RESULT", force=True)
self.log("=" * 80, force=True)

files_data = validation_result.get("data", {}).get("files", [])

if files_data:
for file_data in files_data:
self.log(f"\nFile ID: {file_data.get('file_id')}", force=True)
self.log(f"Status: {file_data.get('status')}", force=True)
self.log(
f"Number of Pages: {file_data.get('number_of_pages', 'N/A')}",
force=True,
)
self.log(f"Page Size: {file_data.get('page_size', 'N/A')}", force=True)
self.log(
f"Is Accessible: {file_data.get('is_accessible', 'N/A')}",
force=True,
)
self.log(
f"Is Password Protected: {file_data.get('is_password_protected', 'N/A')}",
force=True,
)
self.log(
f"Is Corrupted: {file_data.get('is_corrupted', 'N/A')}", force=True
)

if file_data.get("status") != "success":
self.log(f"\n⚠ Validation Issues:", force=True)
if file_data.get("number_of_pages", 0) > self.MAX_PAGES:
self.log(
f" - File exceeds page limit ({file_data.get('number_of_pages')} > {self.MAX_PAGES})",
force=True,
)
if file_data.get("is_password_protected"):
self.log(f" - File is password protected", force=True)
if file_data.get("is_corrupted"):
self.log(f" - File is corrupted", force=True)
if not file_data.get("is_accessible"):
self.log(f" - File is not accessible", force=True)

web_links = validation_result.get("data", {}).get("web_links", [])
if web_links:
for link_data in web_links:
self.log(f"\nURL: {link_data.get('web_url')}", force=True)
self.log(f"Status: {link_data.get('status')}", force=True)
self.log(
f"Is Healthy: {link_data.get('is_healthy', 'N/A')}", force=True
)
self.log(
f"Is Scrappable: {link_data.get('is_scrappable', 'N/A')}",
force=True,
)
self.log(
f"Is GDrive Link: {link_data.get('is_gdrive_link', 'N/A')}",
force=True,
)
self.log(
f"Is Accessible: {link_data.get('is_accessible', 'N/A')}",
force=True,
)

self.log("\n" + "=" * 80, force=True)

# Print complete API response
self.log("\nCOMPLETE API RESPONSE:", force=True)
self.log("=" * 80, force=True)
self.log(json.dumps(validation_result, indent=2), force=True)
self.log("=" * 80, force=True)


def print_help():
"""Print comprehensive help message"""
help_text = """
╔════════════════════════════════════════════════════════════════════════════╗
║ AutoGenerator File Upload & Validation Tool ║
║ Version 1.0.0 ║
╚════════════════════════════════════════════════════════════════════════════╝

DESCRIPTION:
A standalone tool to upload and validate files for the AutoGenerator API.
Supports chunked uploads (5MB chunks), batch processing, automatic retries,
and comprehensive validation.

USAGE:
python3 autogen_file_uploader.py [OPTIONS]

OPTIONS:
--file PATH Upload and validate a file
--link URL Validate a web link (Google Drive, URLs, etc.)
--batch FILE Upload multiple files from a text file (one path per line)
--api-key KEY API authentication key (required)
--url URL Base API URL (default: https://api.prezent.ai)
--quiet Suppress verbose output (only show results)
--help, -h Show this help message

EXAMPLES:

1. Upload a single file:
python3 autogen_file_uploader.py \\
--file /path/to/document.pdf \\
--api-key YOUR_API_KEY

2. Validate a Google Drive link:
python3 autogen_file_uploader.py \\
--link "https://docs.google.com/document/d/YOUR_DOC_ID" \\
--api-key YOUR_API_KEY

3. Batch upload from file list:
python3 autogen_file_uploader.py \\
--batch file_list.txt \\
--api-key YOUR_API_KEY

(file_list.txt should contain one file path per line)

4. Upload with custom API endpoint:
python3 autogen_file_uploader.py \\
--file document.docx \\
--api-key YOUR_KEY \\
--url https://custom-api.example.com

5. Quiet mode (minimal output):
python3 autogen_file_uploader.py \\
--file document.pdf \\
--api-key YOUR_KEY \\
--quiet

SUPPORTED FILE TYPES:
• PowerPoint (.pptx)
• Word Documents (.docx)
• PDF Files (.pdf)
• Text Files (.txt)
• Excel (.xls, .xlsx)
• CSV (.csv)

FILE LIMITS:
• Maximum file size: 200 MB
• Maximum pages: 100 pages

UPLOAD DETAILS:
• Chunk size: 5 MB
• Batch size: 5 chunks per batch
• Retry attempts: 3 per chunk
• Retry delay: 1 second

WORKFLOW:
1. Pre-upload validation (file exists, type, size)
2. File chunking (5MB chunks)
3. Batch upload with retry logic
4. Post-upload validation (pages, corruption, password)
5. Results display

EXIT CODES:
0 - Success
1 - Error occurred

API ENDPOINTS USED:
• POST /api/v1/preprocess
(Upload file chunks)

• POST /api/v1/validate
(Validate uploaded files or links)

TROUBLESHOOTING:

Error: "File does not exist"
→ Check the file path is correct and accessible

Error: "Unsupported file type"
→ Ensure file extension is supported (see SUPPORTED FILE TYPES)

Error: "File exceeds size limit"
→ File must be under 200MB

Error: "Authentication failed"
→ Verify your API key is correct

Error: "Chunk upload failed"
→ Check network connectivity. Script automatically retries 3 times.

Error: "File is password protected"
→ Remove password protection before uploading

Error: "File is corrupted"
→ File may be damaged, try re-exporting

BATCH FILE FORMAT:
Create a text file with one file path per line:

/path/to/file1.pdf
/path/to/file2.docx
/path/to/file3.pptx

NOTES:
• The script automatically handles chunking for large files
• Failed chunks are automatically retried (up to 3 attempts)
• Progress is displayed in real-time during upload
• Validation checks file integrity, page count, and accessibility

ENVIRONMENT:
• Python 3.6 or higher required
• Required package: requests (pip install requests)

SUPPORT:
For issues or questions, contact the AutoGenerator API team.

═══════════════════════════════════════════════════════════════════════════════
"""
print(help_text)


def process_batch_file(batch_file: str, tester: FileUploadTester) -> Dict:
"""Process multiple files from a batch file"""
if not os.path.exists(batch_file):
return {"error": True, "message": f"Batch file not found: {batch_file}"}

with open(batch_file, "r") as f:
file_paths = [line.strip() for line in f if line.strip()]

if not file_paths:
return {"error": True, "message": "Batch file is empty"}

print("=" * 80)
print(f"BATCH UPLOAD - {len(file_paths)} FILES")
print("=" * 80)

results = []
for idx, file_path in enumerate(file_paths, 1):
print(f"\n[{idx}/{len(file_paths)}] Processing: {file_path}")
print("-" * 80)

result = tester.upload_and_validate_file(file_path)
results.append({"file": file_path, "result": result})

# Print summary
print("\n" + "=" * 80)
print("BATCH UPLOAD SUMMARY")
print("=" * 80)

successful = sum(1 for r in results if not r["result"].get("error"))
failed = len(results) - successful

print(f"\nTotal Files: {len(results)}")
print(f"✓ Successful: {successful}")
print(f"✗ Failed: {failed}")

if failed > 0:
print("\nFailed Files:")
for r in results:
if r["result"].get("error"):
print(f" - {r['file']}: {r['result'].get('message')}")

return {"total": len(results), "successful": successful, "failed": failed}


def main():
"""Main function for CLI usage"""
args = sys.argv[1:]

# Show help if no arguments or --help flag
if not args or "--help" in args or "-h" in args:
print_help()
return

# Parse arguments
file_path = None
web_link = None
batch_file = None
api_key = None
base_url = "https://api.prezent.ai"
quiet = False

i = 0
while i < len(args):
if args[i] == "--file" and i + 1 < len(args):
file_path = args[i + 1]
i += 2
elif args[i] == "--link" and i + 1 < len(args):
web_link = args[i + 1]
i += 2
elif args[i] == "--batch" and i + 1 < len(args):
batch_file = args[i + 1]
i += 2
elif args[i] == "--api-key" and i + 1 < len(args):
api_key = args[i + 1]
i += 2
elif args[i] == "--url" and i + 1 < len(args):
base_url = args[i + 1]
i += 2
elif args[i] == "--quiet":
quiet = True
i += 1
else:
print(f"Unknown argument: {args[i]}")
print("Use --help for usage information")
sys.exit(1)

# Validate required arguments
if not api_key:
print("✗ ERROR: --api-key is required")
print("\nUse --help for usage information")
sys.exit(1)

if not file_path and not web_link and not batch_file:
print("✗ ERROR: One of --file, --link, or --batch must be provided")
print("\nUse --help for usage information")
sys.exit(1)

# Initialize tester
tester = FileUploadTester(base_url, api_key, verbose=not quiet)

# Execute workflow
try:
if batch_file:
result = process_batch_file(batch_file, tester)
elif file_path:
result = tester.upload_and_validate_file(file_path)
else:
result = tester.validate_link(web_link)

# Handle result
if result.get("error"):
print("\n" + "=" * 80, file=sys.stderr)
print("❌ ERROR OCCURRED", file=sys.stderr)
print("=" * 80, file=sys.stderr)

print(f"\n✗ {result.get('message', 'Unknown error')}", file=sys.stderr)

if result.get("stage"):
print(f"\nFailed Stage: {result.get('stage')}", file=sys.stderr)

if result.get("url"):
print(f"API Endpoint: {result.get('url')}", file=sys.stderr)

if result.get("status_code"):
print(
f"\nHTTP Status Code: {result.get('status_code')}", file=sys.stderr
)

# Common status code explanations
status_explanations = {
400: "Bad Request - The request was invalid or malformed",
401: "Unauthorized - Invalid or missing API key",
403: "Forbidden - You don't have permission to access this resource",
404: "Not Found - The requested resource was not found",
429: "Too Many Requests - Rate limit exceeded",
500: "Internal Server Error - Server encountered an error",
502: "Bad Gateway - Invalid response from upstream server",
503: "Service Unavailable - Server is temporarily unavailable",
}

status_code = result.get("status_code")
if status_code in status_explanations:
print(
f"Explanation: {status_explanations[status_code]}",
file=sys.stderr,
)

if result.get("api_response"):
print("\nAPI Error Response:", file=sys.stderr)
print("-" * 80, file=sys.stderr)
try:
if isinstance(result.get("api_response"), dict):
print(
json.dumps(result.get("api_response"), indent=2),
file=sys.stderr,
)
else:
print(result.get("api_response"), file=sys.stderr)
except:
print(str(result.get("api_response")), file=sys.stderr)
print("-" * 80, file=sys.stderr)

if result.get("exception"):
print(f"\nException: {result.get('exception')}", file=sys.stderr)

if result.get("responses"):
failed_chunks = [
r for r in result.get("responses", []) if r.get("error")
]
if failed_chunks:
print(f"\nFailed Chunks: {len(failed_chunks)}", file=sys.stderr)

print("\n" + "=" * 80, file=sys.stderr)
sys.exit(1)
else:
print(f"\n✓ SUCCESS")
sys.exit(0)

except KeyboardInterrupt:
print("\n\n✗ Operation cancelled by user")
sys.exit(1)
except Exception as e:
print(f"\n✗ UNEXPECTED ERROR: {str(e)}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
main()