Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 82 additions & 10 deletions adobe-autotag-container/adobe_autotag_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,59 @@

s3 = boto3.client('s3')

def _chunk_page_range(chunk_key):
"""Return (page_start, page_end) for a chunk key like temp/<base>/<base>_chunk_N.pdf.

Uses the splitter's fixed 200-pages-per-chunk constant to compute the range.
Returns (None, None) if the chunk number cannot be parsed.
"""
import re
PAGES_PER_CHUNK = 200
if not chunk_key:
return None, None
m = re.search(r'_chunk_(\d+)\.pdf$', chunk_key)
if not m:
return None, None
n = int(m.group(1))
page_start = (n - 1) * PAGES_PER_CHUNK + 1
page_end = n * PAGES_PER_CHUNK
return page_start, page_end


def report_failure(bucket_name, file_base_name, chunk_key, reason_category, message):
"""Write a structured failure-detail file the Step Functions failure-handler
aggregates into the user-facing result/FAILED_<name>.json marker.

Station: 'adobe' (Adobe AutoTag/Extract). Best-effort and exception-proof:
reporting a failure must never throw a second failure that masks the original.
"""
page_start, page_end = _chunk_page_range(chunk_key)
logger.error(
f"File: {file_base_name}, Status: FAILED | station=adobe | "
f"reason={reason_category} | page={page_start}-{page_end} | {message}"
)

if not bucket_name or not file_base_name:
return
detail = {
"station": "adobe",
"reason_category": reason_category,
"message": str(message)[:2000],
}
if page_start is not None:
detail["page_start"] = page_start
detail["page_end"] = page_end
try:
s3.put_object(
Bucket=bucket_name,
Key=f"temp/{file_base_name}/_errors/adobe_failure.json",
Body=json.dumps(detail).encode("utf-8"),
ContentType="application/json",
)
except Exception as e:
logger.error(f"Filename : {file_base_name} | Could not write failure detail: {e}")


def download_file_from_s3(bucket_name,file_base_name, file_key, local_path):
"""
Download a file from an S3 bucket.
Expand Down Expand Up @@ -430,7 +483,8 @@ def create_sqlite_db(by_page, filename, images_output_dir, object_ids, image_pat
prev TEXT,
current TEXT,
next TEXT,
context TEXT
context TEXT,
page_num INTEGER
)
""")

Expand Down Expand Up @@ -524,12 +578,13 @@ def create_sqlite_db(by_page, filename, images_output_dir, object_ids, image_pat
print(" ======================")
# Insert the data into the SQLite database.
cursor.execute("""
INSERT INTO image_data (objid, img_path, context)
VALUES (?, ?, ?)
INSERT INTO image_data (objid, img_path, context, page_num)
VALUES (?, ?, ?, ?)
""", (
current_candidate["objid"],
current_candidate["filePaths"][0].split("/")[-1],
context
context,
pg_num + 1
))
print("Added in the database: ", current_candidate["objid"],
current_candidate["filePaths"][0].split("/")[-1])
Expand Down Expand Up @@ -620,7 +675,8 @@ def extract_images_from_excel(filename, figure_path, autotag_report_path, images
prev TEXT,
current TEXT,
next TEXT,
context TEXT
context TEXT,
page_num INTEGER
)
""")

Expand All @@ -640,11 +696,12 @@ def main():
"""
file_key = None
file_base_name = None

try:
bucket_name = os.getenv('S3_BUCKET_NAME')
s3_file_key = None
bucket_name = os.getenv('S3_BUCKET_NAME')

try:
s3_file_key = os.getenv('S3_FILE_KEY')

if not bucket_name or not s3_file_key:
logging.error("Error: S3_BUCKET_NAME and S3_FILE_KEY environment variables are required.")
sys.exit(1)
Expand Down Expand Up @@ -714,21 +771,36 @@ def main():
logging.info(f'Filename : {file_key} | Processing completed successfully')
logger.info(f"File: {file_base_name}, Status: Succeeded in First ECS task")

except (ServiceApiException, ServiceUsageException, SdkException) as e:
except ServiceUsageException as e:
# Quota / credits exhausted — not a document problem, retrying later may work.
logger.error(f"File: {file_base_name}, Status: Failed in First ECS task - Adobe API Quota")
logger.error(f"Filename : {file_key} | Adobe API Quota Error: {e}")
report_failure(bucket_name, file_base_name, s3_file_key, "ADOBE_API",
f"Adobe API quota or credits exhausted. Please try again later. Adobe error: {e}")
sys.exit(1)
except (ServiceApiException, SdkException) as e:
logger.error(f"File: {file_base_name}, Status: Failed in First ECS task - Adobe API Error")
logger.error(f"Filename : {file_key} | Adobe API Error: {e}")
report_failure(bucket_name, file_base_name, s3_file_key, "ADOBE_API",
f"Adobe API failed for this document. This document may be too complex for our Adobe API to handle. Adobe error: {e}")
sys.exit(1)
except ClientError as e:
logger.error(f"File: {file_base_name}, Status: Failed in First ECS task - AWS Error")
logger.error(f"Filename : {file_key} | AWS Error: {e}")
report_failure(bucket_name, file_base_name, s3_file_key, "INFRA",
f"AWS infrastructure error: {e}")
sys.exit(1)
except FileNotFoundError as e:
logger.error(f"File: {file_base_name}, Status: Failed in First ECS task - File Not Found")
logger.error(f"Filename : {file_key} | File Not Found Error: {e}")
report_failure(bucket_name, file_base_name, s3_file_key, "ADOBE_API",
"Adobe API failed for this document. This document may be too complex for our Adobe API to handle.")
sys.exit(1)
except Exception as e:
logger.error(f"File: {file_base_name}, Status: Failed in First ECS task")
logger.error(f"Filename : {file_key} | Unexpected Error: {e}")
report_failure(bucket_name, file_base_name, s3_file_key, "UNKNOWN",
f"Unexpected error processing this document: {e}")
sys.exit(1)

if __name__ == "__main__":
Expand Down
106 changes: 98 additions & 8 deletions alt-text-generator-container/alt_text_generator.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,73 @@ function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}

const MAX_ASPECT_RATIO = 20;

function getImageDimensions(buffer) {
try {
if (buffer[0] === 0x89 && buffer[1] === 0x50) {
// PNG: width at offset 16, height at offset 20 (4 bytes each, big-endian)
const width = buffer.readUInt32BE(16);
const height = buffer.readUInt32BE(20);
return { width, height };
}
if (buffer[0] === 0xFF && buffer[1] === 0xD8) {
// JPEG: scan for SOF0 marker (0xFFC0)
let offset = 2;
while (offset < buffer.length - 9) {
if (buffer[offset] === 0xFF) {
const marker = buffer[offset + 1];
if (marker >= 0xC0 && marker <= 0xCF && marker !== 0xC4 && marker !== 0xC8 && marker !== 0xCC) {
const height = buffer.readUInt16BE(offset + 5);
const width = buffer.readUInt16BE(offset + 7);
return { width, height };
}
const segLen = buffer.readUInt16BE(offset + 2);
offset += 2 + segLen;
} else {
offset++;
}
}
}
} catch (e) {
return null;
}
return null;
}

function isAspectRatioValid(dimensions) {
if (!dimensions || !dimensions.width || !dimensions.height) return true;
const { width, height } = dimensions;
const ratio = Math.max(width / height, height / width);
return ratio <= MAX_ASPECT_RATIO;
}

/**
* Writes a structured failure-detail file that the Step Functions failure-handler
* aggregates into the user-facing result/FAILED_<name>.json marker.
*/
async function reportFailure(bucketName, fileBaseName, reasonCategory, message) {
logger.error(`File: ${fileBaseName}, Status: FAILED | station=alttext | reason=${reasonCategory} | ${message}`);

if (!bucketName || !fileBaseName) return;
const detail = {
station: "alttext",
reason_category: reasonCategory,
message: String(message).slice(0, 2000),
};
try {
await s3Client.send(new PutObjectCommand({
Bucket: bucketName,
Key: `temp/${fileBaseName}/_errors/alttext_failure.json`,
Body: JSON.stringify(detail),
ContentType: "application/json",
}));
} catch (e) {
logger.error(`Filename: ${fileBaseName} | Could not write failure detail: ${e.message || e}`);
}
}



/**
* Invokes the Bedrock AI model to generate alt text for a given image.
Expand Down Expand Up @@ -471,6 +538,7 @@ async function startProcess() {
context_json: {
context: row.context,
},
page_num: row.page_num || null,
};
});
} catch (err) {
Expand All @@ -492,6 +560,7 @@ async function startProcess() {
logger.info(`Filename: ${filebasename} | imageObjects: ${imageObjects}`);
logger.info(`Filename: ${filebasename} | Total images to process: ${imageObjects.length}`);

let skippedCount = 0;
for (const imageObject of imageObjects) {
try {
const getObjectParams = {
Expand All @@ -502,7 +571,7 @@ async function startProcess() {
logger.info(`Filename: ${filebasename} | Image Object Bucketname: ${bucketName}`);
const command = new GetObjectCommand(getObjectParams);
const { Body } = await s3Client.send(command);

// Stream the body contents to a buffer
const chunks = [];
await pipeline(Body, async function* (source) {
Expand All @@ -511,6 +580,18 @@ async function startProcess() {
}
});
const fileBuffer = Buffer.concat(chunks);

// Aspect-ratio pre-check: skip images with extreme ratios
const dimensions = getImageDimensions(fileBuffer);
if (dimensions && !isAspectRatioValid(dimensions)) {
const ratio = Math.max(dimensions.width / dimensions.height, dimensions.height / dimensions.width).toFixed(1);
const pageDesc = imageObject.page_num ? ` | page=${imageObject.page_num}` : "";
logger.info(`File: ${filebasename}, Status: INFO | station=alttext | image=${imageObject.id}${pageDesc} | action=decorative | reason=complex_image_aspect_ratio_${ratio}:1`);
skippedCount++;
combinedResults[imageObject.id] = "Decorative element";
continue;
}

const localFilePath = path.join(__dirname, `${imageObject.path.split('/').pop()}`);
logger.info(`Filename: ${filebasename} | Local File Path: ${localFilePath}`);
fs_1.writeFileSync(localFilePath, fileBuffer);
Expand All @@ -522,26 +603,33 @@ async function startProcess() {
logger.info(`Filename: ${filebasename} | Alt text generation succeeded for image ${imageObject.id} (${successCount} succeeded, ${failureCount} failed)`);
} catch (error) {
failureCount++;
logger.error(`Filename: ${filebasename} | Alt text generation failed for image ${imageObject.id}: ${error.message || error}`);
const pageDesc = imageObject.page_num ? ` | page=${imageObject.page_num}` : "";
logger.error(`File: ${filebasename}, Status: FAILED | station=alttext | image=${imageObject.id}${pageDesc} | reason=BEDROCK_API | ${error.message || error}`);
logger.info(`Filename: ${filebasename} | Progress: ${successCount} succeeded, ${failureCount} failed`);
}
await sleep(2000);
}

// Check if we have any images and if all of them failed
if (imageObjects.length > 0 && successCount === 0) {
const processedImages = imageObjects.length - skippedCount;
if (processedImages > 0 && successCount === 0) {
logger.error(`Filename: ${filebasename} | All ${failureCount} alt text generation requests failed - likely due to throttling or Bedrock API issues`);
logger.error(`File: ${filebasename}, Status: Failed in second ECS task - All Bedrock requests failed`);
const failedPages = [...new Set(
imageObjects
.filter(img => img.page_num != null && !combinedResults.hasOwnProperty(img.id))
.map(img => img.page_num)
)].sort((a, b) => a - b);
const pageInfo = failedPages.length > 0 ? ` Failed on pages: ${failedPages.join(', ')}.` : '';
await reportFailure(bucketName, filebasename, "BEDROCK_API",
`All ${failureCount} Bedrock alt-text requests failed (throttling or Bedrock API issues).${pageInfo}`);
process.exit(1);
}

logger.info(`Filename: ${filebasename} | Alt text generation complete: ${successCount} succeeded, ${failureCount} failed out of ${imageObjects.length} images`);

let defaultText = "No text available";
logger.info(`Filename: ${filebasename} | Alt text generation complete: ${successCount} succeeded, ${failureCount} failed, ${skippedCount} decorative out of ${imageObjects.length} images`);

for (const imageObject of imageObjects) {
if (!combinedResults.hasOwnProperty(imageObject.id)) {
combinedResults[imageObject.id] = defaultText;
combinedResults[imageObject.id] = "Decorative element";
}
}

Expand All @@ -558,6 +646,8 @@ async function startProcess() {
} catch (error) {
logger.info(`File: ${filebasename}, Status: Error in second ECS task`);
logger.error(`Filename: ${filebasename} | Error processing images: ${error}`);
await reportFailure(bucketName, filebasename, "UNKNOWN",
`Error processing images: ${error.message || error}`);
process.exit(1);
}
}
Expand Down
Loading