- Integrated Microsoft Authentication Library (MSAL) for secure login - Protected content is now hidden until successful authentication - Added login/logout functionality with proper state management - Updated client ID and redirect URI for production deployment - Authentication buttons positioned on left side of interface - Session storage maintains authentication state across page reloads 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
576 lines
19 KiB
PHP
Executable file
576 lines
19 KiB
PHP
Executable file
<?php
|
|
$config = require 'config.php';
|
|
|
|
$uploadDir = 'uploads/';
|
|
$logDir = 'logs/';
|
|
|
|
// Create necessary directories
|
|
foreach ([$uploadDir, $logDir] as $dir) {
|
|
if (!file_exists($dir)) {
|
|
mkdir($dir, 0777, true);
|
|
}
|
|
}
|
|
|
|
$response = [];
|
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|
if (isset($_FILES['files'])) {
|
|
$files = reArrayFiles($_FILES['files']);
|
|
|
|
foreach ($files as $file) {
|
|
$fileName = basename($file['name']);
|
|
$targetFilePath = $uploadDir . $fileName;
|
|
|
|
if (move_uploaded_file($file['tmp_name'], $targetFilePath)) {
|
|
try {
|
|
// Convert image to base64
|
|
$imageData = base64_encode(file_get_contents($targetFilePath));
|
|
$result = callOpenAI($imageData, $config);
|
|
|
|
// Log the response
|
|
logResponse($fileName, $result);
|
|
|
|
$response[$fileName] = ['success' => $result];
|
|
} catch (Exception $e) {
|
|
$error = 'Error processing image: ' . $e->getMessage();
|
|
logResponse($fileName, $error, true);
|
|
$response[$fileName] = ['error' => $error];
|
|
}
|
|
} else {
|
|
$response[$fileName] = ['error' => 'Failed to upload image'];
|
|
}
|
|
}
|
|
}
|
|
|
|
header('Content-Type: application/json');
|
|
echo json_encode(['response' => $response]);
|
|
exit;
|
|
}
|
|
|
|
function callOpenAI($base64Image, $config) {
|
|
$url = 'https://api.openai.com/v1/chat/completions';
|
|
|
|
$headers = [
|
|
'Content-Type: application/json',
|
|
'Authorization: Bearer ' . $config['openai_key']
|
|
];
|
|
|
|
$data = [
|
|
'model' => $config['openai_model'],
|
|
'messages' => [
|
|
[
|
|
'role' => 'system',
|
|
'content' => $config['system_prompt']
|
|
],
|
|
[
|
|
'role' => 'user',
|
|
'content' => [
|
|
[
|
|
'type' => 'text',
|
|
'text' => 'Please analyze this image and provide two alt text descriptions in the following format exactly:
|
|
|
|
Short version: [brief 150 character description]
|
|
|
|
Long version: [detailed 400 character description]'
|
|
],
|
|
[
|
|
'type' => 'image_url',
|
|
'image_url' => [
|
|
'url' => 'data:image/jpeg;base64,' . $base64Image
|
|
]
|
|
]
|
|
]
|
|
]
|
|
],
|
|
'max_tokens' => 1000
|
|
];
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
|
|
$result = curl_exec($ch);
|
|
|
|
if (curl_errno($ch)) {
|
|
throw new Exception(curl_error($ch));
|
|
}
|
|
|
|
curl_close($ch);
|
|
|
|
$response = json_decode($result, true);
|
|
|
|
if (isset($response['error'])) {
|
|
throw new Exception($response['error']['message']);
|
|
}
|
|
|
|
// Get the markdown content from OpenAI
|
|
$markdownContent = $response['choices'][0]['message']['content'];
|
|
|
|
// Log the raw markdown
|
|
logResponse('markdown_response', $markdownContent);
|
|
|
|
// Convert markdown to HTML using Parsedown
|
|
$html = markdownToHtml($markdownContent);
|
|
|
|
// Log the HTML conversion
|
|
logResponse('html_conversion', $html);
|
|
|
|
// Extract alt texts from the HTML
|
|
$shortAlt = extractShortAlt($html);
|
|
$longAlt = extractLongAlt($html);
|
|
|
|
// Return JSON response
|
|
return json_encode([
|
|
'shortAlt' => $shortAlt,
|
|
'longAlt' => $longAlt
|
|
]);
|
|
}
|
|
|
|
function markdownToHtml($markdown) {
|
|
// Basic markdown to HTML conversion
|
|
$html = preg_replace('/\*\*(.*?)\*\*/s', '<strong>$1</strong>', $markdown);
|
|
$html = preg_replace('/\*(.*?)\*/s', '<em>$1</em>', $html);
|
|
$html = preg_replace('/`(.*?)`/s', '<code>$1</code>', $html);
|
|
$html = nl2br($html);
|
|
return $html;
|
|
}
|
|
|
|
function extractShortAlt($html) {
|
|
$patterns = [
|
|
'/<strong>Short (?:version|alt|description):<\/strong>\s*(.*?)(?:<br|$)/i',
|
|
'/Short (?:version|alt|description):\s*(.*?)(?:<br|$)/i',
|
|
'/Short:?\s*(.*?)(?:<br|$)/i'
|
|
];
|
|
|
|
foreach ($patterns as $pattern) {
|
|
if (preg_match($pattern, $html, $matches)) {
|
|
return cleanHtml(trim($matches[1]));
|
|
}
|
|
}
|
|
|
|
// Fallback: try to get the first substantial line
|
|
$lines = explode('<br />', $html);
|
|
foreach ($lines as $line) {
|
|
$cleaned = cleanHtml($line);
|
|
if (strlen($cleaned) > 10) { // Arbitrary minimum length to avoid headers
|
|
return $cleaned;
|
|
}
|
|
}
|
|
|
|
return "Short description not found";
|
|
}
|
|
|
|
function extractLongAlt($html) {
|
|
$patterns = [
|
|
'/<strong>Long (?:version|alt|description):<\/strong>\s*(.*?)(?:<br|$)/i',
|
|
'/Long (?:version|alt|description):\s*(.*?)(?:<br|$)/i',
|
|
'/Long:?\s*(.*?)(?:<br|$)/i'
|
|
];
|
|
|
|
foreach ($patterns as $pattern) {
|
|
if (preg_match($pattern, $html, $matches)) {
|
|
return cleanHtml(trim($matches[1]));
|
|
}
|
|
}
|
|
|
|
// Fallback: try to get everything after the short description
|
|
$parts = preg_split('/<br \/><br \/>/', $html);
|
|
if (count($parts) > 1) {
|
|
array_shift($parts); // Remove first part (assumed to be short description)
|
|
return cleanHtml(implode(' ', $parts));
|
|
}
|
|
|
|
return "Long description not found";
|
|
}
|
|
|
|
function cleanHtml($text) {
|
|
// Remove HTML tags and decode entities
|
|
$text = strip_tags($text);
|
|
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5);
|
|
$text = trim($text);
|
|
return $text;
|
|
}
|
|
|
|
function logResponse($fileName, $response, $isError = false) {
|
|
global $logDir;
|
|
$logFile = $logDir . 'openai_responses.log';
|
|
$logEntry = date('Y-m-d H:i:s') . " - File: $fileName\n";
|
|
$logEntry .= "Raw Response:\n" . print_r($response, true) . "\n";
|
|
|
|
if (!$isError) {
|
|
try {
|
|
$decoded = json_decode($response, true);
|
|
if ($decoded) {
|
|
$logEntry .= "Decoded Response:\n";
|
|
$logEntry .= "Short Alt: " . ($decoded['shortAlt'] ?? 'none') . "\n";
|
|
$logEntry .= "Long Alt: " . ($decoded['longAlt'] ?? 'none') . "\n";
|
|
}
|
|
} catch (Exception $e) {
|
|
$logEntry .= "Error decoding JSON: " . $e->getMessage() . "\n";
|
|
}
|
|
}
|
|
|
|
$logEntry .= "----------------------------------------\n";
|
|
file_put_contents($logFile, $logEntry, FILE_APPEND);
|
|
}
|
|
|
|
function reArrayFiles($files) {
|
|
$file_array = array();
|
|
$file_count = count($files['name']);
|
|
$file_keys = array_keys($files);
|
|
|
|
for ($i = 0; $i < $file_count; $i++) {
|
|
foreach ($file_keys as $key) {
|
|
$file_array[$i][$key] = $files[$key][$i];
|
|
}
|
|
}
|
|
return $file_array;
|
|
}
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Multi-File Alt Text Generator</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap" rel="stylesheet">
|
|
<!-- Auth: MSAL library -->
|
|
<script src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
|
<style>
|
|
body, input, button, textarea {
|
|
font-family: 'Montserrat', sans-serif;
|
|
}
|
|
.app-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
margin: 50px auto;
|
|
max-width: 1200px;
|
|
padding: 0 20px;
|
|
}
|
|
.logo {
|
|
width: 200px;
|
|
height: 125px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.image-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
width: 100%;
|
|
margin-top: 20px;
|
|
}
|
|
.image-box {
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
padding: 10px;
|
|
background: #f9f9f9;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
}
|
|
.image-preview {
|
|
width: 100%;
|
|
height: 200px;
|
|
object-fit: contain;
|
|
background: #fff;
|
|
}
|
|
.alt-text {
|
|
padding: 10px;
|
|
background: #fff;
|
|
border-radius: 4px;
|
|
word-wrap: break-word;
|
|
}
|
|
.status {
|
|
text-align: center;
|
|
padding: 5px;
|
|
border-radius: 4px;
|
|
}
|
|
.processing {
|
|
background: #fff3cd;
|
|
color: #856404;
|
|
}
|
|
.complete {
|
|
background: #d4edda;
|
|
color: #155724;
|
|
}
|
|
.pending {
|
|
background: #cce5ff;
|
|
color: #004085;
|
|
}
|
|
.error {
|
|
background: #f8d7da;
|
|
color: #721c24;
|
|
}
|
|
.file-upload-container {
|
|
margin-bottom: 20px;
|
|
}
|
|
.file-upload-label, .download-button {
|
|
display: inline-block;
|
|
padding: 10px 20px;
|
|
cursor: pointer;
|
|
background-color: #28a745;
|
|
color: white;
|
|
border-radius: 5px;
|
|
transition: background-color 0.3s ease;
|
|
border: none;
|
|
font-size: 16px;
|
|
margin: 0 10px;
|
|
}
|
|
.file-upload-label:hover, .download-button:hover {
|
|
background-color: #218838;
|
|
}
|
|
.download-container {
|
|
margin-top: 20px;
|
|
display: none;
|
|
}
|
|
|
|
/* Auth styles */
|
|
#protected-content {
|
|
display: none;
|
|
}
|
|
|
|
.auth-buttons {
|
|
text-align: left;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.auth-button {
|
|
display: inline-block;
|
|
padding: 10px 20px;
|
|
cursor: pointer;
|
|
background-color: #007bff;
|
|
color: white;
|
|
border-radius: 5px;
|
|
transition: background-color 0.3s ease;
|
|
border: none;
|
|
font-size: 16px;
|
|
margin: 0 10px;
|
|
}
|
|
|
|
.auth-button:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="app-container">
|
|
<img src="alt_text.svg" alt="Vivid Alt Texter.svg" class="logo">
|
|
|
|
<!-- Auth buttons -->
|
|
<div class="auth-buttons">
|
|
<button id="logout-button" class="auth-button" onclick="signOut()" style="display:none;">Log Out</button>
|
|
<button id="login-button" class="auth-button" onclick="signIn()" style="display:none;">Log In</button>
|
|
</div>
|
|
|
|
<div id="protected-content">
|
|
<div class="file-upload-container">
|
|
<form id="uploadForm" enctype="multipart/form-data">
|
|
<input type="file" id="fileUpload" name="files[]" accept=".png, .jpg, .jpeg" multiple style="display: none;">
|
|
<label for="fileUpload" class="file-upload-label">Upload Images</label>
|
|
</form>
|
|
</div>
|
|
<div id="imageGrid" class="image-grid"></div>
|
|
<div class="download-container">
|
|
<button id="downloadAllCSV" class="download-button">Download All (CSV)</button>
|
|
<button id="downloadAllTxt" class="download-button">Download Individual Files</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let processedFiles = {};
|
|
let totalFiles = 0;
|
|
let completedFiles = 0;
|
|
|
|
document.getElementById('fileUpload').addEventListener('change', function(event) {
|
|
const files = Array.from(event.target.files);
|
|
totalFiles = files.length;
|
|
completedFiles = 0;
|
|
|
|
// Clear existing grid
|
|
document.getElementById('imageGrid').innerHTML = '';
|
|
processedFiles = {};
|
|
|
|
files.forEach((file, index) => {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
createImageBox(file, e.target.result, index);
|
|
processImage(file);
|
|
}
|
|
reader.readAsDataURL(file);
|
|
});
|
|
});
|
|
|
|
function createImageBox(file, dataUrl, index) {
|
|
const box = document.createElement('div');
|
|
box.className = 'image-box';
|
|
box.id = `box-${file.name}`;
|
|
|
|
const img = document.createElement('img');
|
|
img.src = dataUrl;
|
|
img.className = 'image-preview';
|
|
|
|
const status = document.createElement('div');
|
|
status.className = 'status pending';
|
|
status.textContent = 'To be processed';
|
|
status.id = `status-${file.name}`;
|
|
|
|
const altTextContainer = document.createElement('div');
|
|
altTextContainer.className = 'alt-text';
|
|
altTextContainer.id = `alt-${file.name}`;
|
|
altTextContainer.style.display = 'none';
|
|
|
|
box.appendChild(img);
|
|
box.appendChild(status);
|
|
box.appendChild(altTextContainer);
|
|
|
|
document.getElementById('imageGrid').appendChild(box);
|
|
}
|
|
|
|
async function processImage(file) {
|
|
const status = document.getElementById(`status-${file.name}`);
|
|
status.className = 'status processing';
|
|
status.textContent = 'Processing...';
|
|
|
|
const formData = new FormData();
|
|
formData.append('files[]', file);
|
|
|
|
try {
|
|
const response = await fetch(window.location.href, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
const data = await response.json();
|
|
|
|
if (data.response[file.name].error) {
|
|
handleError(file.name, data.response[file.name].error);
|
|
} else {
|
|
handleSuccess(file.name, data.response[file.name].success);
|
|
}
|
|
} catch (error) {
|
|
handleError(file.name, 'Failed to process image');
|
|
}
|
|
|
|
completedFiles++;
|
|
if (completedFiles === totalFiles) {
|
|
document.querySelector('.download-container').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function handleSuccess(fileName, response) {
|
|
const status = document.getElementById(`status-${fileName}`);
|
|
const altContainer = document.getElementById(`alt-${fileName}`);
|
|
|
|
try {
|
|
const parsedResponse = JSON.parse(response);
|
|
processedFiles[fileName] = parsedResponse;
|
|
|
|
status.className = 'status complete';
|
|
status.textContent = 'Complete';
|
|
|
|
altContainer.style.display = 'block';
|
|
altContainer.innerHTML = `
|
|
<strong>Short Alt:</strong> ${parsedResponse.shortAlt}<br>
|
|
<strong>Long Alt:</strong> ${parsedResponse.longAlt}
|
|
`;
|
|
} catch (e) {
|
|
handleError(fileName, 'Error parsing response');
|
|
}
|
|
}
|
|
|
|
function handleError(fileName, error) {
|
|
const status = document.getElementById(`status-${fileName}`);
|
|
status.className = 'status error';
|
|
status.textContent = error;
|
|
}
|
|
|
|
document.getElementById('downloadAllCSV').addEventListener('click', function() {
|
|
let csv = 'Filename,Short Alt Text,Long Alt Text\n';
|
|
|
|
Object.entries(processedFiles).forEach(([fileName, data]) => {
|
|
csv += `"${fileName}","${data.shortAlt}","${data.longAlt}"\n`;
|
|
});
|
|
|
|
downloadFile(csv, 'alt_text_results.csv', 'text/csv');
|
|
});
|
|
|
|
document.getElementById('downloadAllTxt').addEventListener('click', function() {
|
|
Object.entries(processedFiles).forEach(([fileName, data]) => {
|
|
const content = `Short Alt Text: ${data.shortAlt}\nLong Alt Text: ${data.longAlt}`;
|
|
const txtFileName = fileName.split('.').slice(0, -1).join('.') + '_alt.txt';
|
|
downloadFile(content, txtFileName, 'text/plain');
|
|
});
|
|
});
|
|
|
|
function downloadFile(content, fileName, contentType) {
|
|
const blob = new Blob([content], { type: contentType });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = fileName;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// Auth Configuration
|
|
const msalConfig = {
|
|
auth: {
|
|
clientId: "01d33c7c-5640-4986-b4db-06af63a7d285",
|
|
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
|
|
redirectUri: "https://brandtechsandbox.oliver.solutions/alt-text-generator/"
|
|
},
|
|
cache: {
|
|
cacheLocation: "sessionStorage",
|
|
storeAuthStateInCookie: true,
|
|
}
|
|
};
|
|
|
|
const loginRequest = {
|
|
scopes: ["user.read"]
|
|
};
|
|
|
|
const myMSALObj = new msal.PublicClientApplication(msalConfig);
|
|
|
|
// Initialize authentication on page load
|
|
signIn();
|
|
|
|
function signIn() {
|
|
myMSALObj.loginPopup(loginRequest)
|
|
.then(loginResponse => {
|
|
console.log("User logged in:", loginResponse.account.username);
|
|
sessionStorage.setItem('accessToken', loginResponse.accessToken);
|
|
showProtectedContent();
|
|
}).catch(error => {
|
|
console.error("Error during login:", error);
|
|
});
|
|
}
|
|
|
|
function signOut() {
|
|
// Clear the session storage
|
|
sessionStorage.removeItem('accessToken');
|
|
console.log("User logged out.");
|
|
document.getElementById('protected-content').style.display = 'none';
|
|
document.getElementById('logout-button').style.display = 'none';
|
|
document.getElementById('login-button').style.display = 'block';
|
|
}
|
|
|
|
function showProtectedContent() {
|
|
// Verify that the access token exists before showing protected content
|
|
const accessToken = sessionStorage.getItem('accessToken');
|
|
if (accessToken) {
|
|
document.getElementById('protected-content').style.display = 'block';
|
|
document.getElementById('logout-button').style.display = 'block';
|
|
document.getElementById('login-button').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// Check if the user is already logged in when the page loads
|
|
window.addEventListener('load', showProtectedContent);
|
|
</script>
|
|
</body>
|
|
</html>
|