Move webhook to async after file download
This commit is contained in:
commit
7eeb94759e
29 changed files with 408033 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
app.log
|
||||
*.log
|
||||
OLD/
|
||||
17
.htaccess
Normal file
17
.htaccess
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<Files "config.php">
|
||||
# For Apache 2.4+
|
||||
Require all denied
|
||||
|
||||
# For Apache 2.2 and older (uncomment if needed and comment out the "Require all denied" line)
|
||||
# Order allow,deny
|
||||
# Deny from all
|
||||
</Files>
|
||||
|
||||
<Files "app.log">
|
||||
# For Apache 2.4+
|
||||
Require all denied
|
||||
|
||||
# For Apache 2.2 and older (uncomment if needed and comment out the "Require all denied" line)
|
||||
# Order allow,deny
|
||||
# Deny from all
|
||||
</Files>
|
||||
1101
azure_storage.php
Normal file
1101
azure_storage.php
Normal file
File diff suppressed because it is too large
Load diff
70
cleanup.php
Normal file
70
cleanup.php
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
/**
|
||||
* Document cleanup script
|
||||
* Removes files older than 24 hours from source and translated documents folders
|
||||
*/
|
||||
|
||||
// Include the logger
|
||||
require_once 'logger.php';
|
||||
|
||||
function cleanupOldFiles() {
|
||||
logMessage('Starting cleanup process', 'CLEANUP');
|
||||
|
||||
// Define directories
|
||||
$sourceDir = __DIR__ . '/local_storage/source-documents/';
|
||||
$translatedDir = __DIR__ . '/local_storage/translated-documents/';
|
||||
|
||||
// Current timestamp
|
||||
$now = time();
|
||||
// 24 hours in seconds
|
||||
$dayInSeconds = 24 * 60 * 60;
|
||||
// Cutoff time (24 hours ago)
|
||||
$cutoffTime = $now - $dayInSeconds;
|
||||
|
||||
// Files removed counter
|
||||
$removedCount = 0;
|
||||
|
||||
// Function to clean a directory
|
||||
$cleanDirectory = function($directory) use ($cutoffTime, &$removedCount) {
|
||||
if (!is_dir($directory)) {
|
||||
logMessage("Directory not found: $directory", 'CLEANUP');
|
||||
return;
|
||||
}
|
||||
|
||||
$files = scandir($directory);
|
||||
foreach ($files as $file) {
|
||||
// Skip . and .. and hidden files
|
||||
if ($file[0] === '.') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = $directory . $file;
|
||||
if (is_file($filePath)) {
|
||||
$fileModTime = filemtime($filePath);
|
||||
if ($fileModTime < $cutoffTime) {
|
||||
// File is older than 24 hours, delete it
|
||||
if (unlink($filePath)) {
|
||||
logMessage("Deleted old file: $file (modified: " . date('Y-m-d H:i:s', $fileModTime) . ")", 'CLEANUP');
|
||||
$removedCount++;
|
||||
} else {
|
||||
logMessage("Failed to delete file: $file", 'CLEANUP');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Clean both directories
|
||||
$cleanDirectory($sourceDir);
|
||||
$cleanDirectory($translatedDir);
|
||||
|
||||
logMessage("Cleanup completed. Removed $removedCount old files.", 'CLEANUP');
|
||||
|
||||
return $removedCount;
|
||||
}
|
||||
|
||||
// Execute cleanup if script is called directly
|
||||
if (basename(__FILE__) == basename($_SERVER['SCRIPT_FILENAME'])) {
|
||||
$removed = cleanupOldFiles();
|
||||
echo "Cleanup completed. Removed $removed old files.\n";
|
||||
}
|
||||
25
config.php
Normal file
25
config.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
// Microsoft Translator API settings
|
||||
define('MS_API_KEY', 'ChwRNPjwUsJ6NKw9575KrPotbm59m8FsiqU2jRGP2L97nzu6GzrbJQQJ99BEACYeBjFXJ3w3AAAbACOGe4Rc');
|
||||
define('MS_API_ENDPOINT', 'https://optical-translation.cognitiveservices.azure.com/');
|
||||
define('MS_API_REGION', 'eastus'); // e.g., 'westus2', 'eastus'
|
||||
define('MS_API_VERSION', '2024-05-01');
|
||||
|
||||
// Azure Storage settings for document translation
|
||||
define('AZURE_STORAGE_CONNECTION_STRING', 'BlobEndpoint=https://opticaltranslations.blob.core.windows.net/;QueueEndpoint=https://opticaltranslations.queue.core.windows.net/;FileEndpoint=https://opticaltranslations.file.core.windows.net/;TableEndpoint=https://opticaltranslations.table.core.windows.net/;SharedAccessSignature=sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2026-05-06T01:01:26Z&st=2025-05-05T17:01:26Z&spr=https,http&sig=ZH%2F%2BXC2WPNDFYWS%2FKe2ifmnkxR7hjmqTGEHuXPgbHN8%3D');
|
||||
define('AZURE_STORAGE_CONTAINER_SOURCE', 'source-documents');
|
||||
define('AZURE_STORAGE_CONTAINER_TARGET', 'translated-documents');
|
||||
define('AZURE_STORAGE_SAS_TOKEN', 'sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2026-05-06T01:01:26Z&st=2025-05-05T17:01:26Z&spr=https,http&sig=ZH%2F%2BXC2WPNDFYWS%2FKe2ifmnkxR7hjmqTGEHuXPgbHN8%3D');
|
||||
|
||||
// Source and Target SAS tokens and URLs
|
||||
define('AZURE_STORAGE_SOURCE_SAS_TOKEN', 'sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2026-05-06T01:01:26Z&st=2025-05-05T17:01:26Z&spr=https,http&sig=ZH%2F%2BXC2WPNDFYWS%2FKe2ifmnkxR7hjmqTGEHuXPgbHN8%3D');
|
||||
define('AZURE_STORAGE_SOURCE_URL', 'https://opticaltranslations.blob.core.windows.net/source-documents');
|
||||
define('AZURE_STORAGE_TARGET_SAS_TOKEN', 'sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2026-05-06T01:01:26Z&st=2025-05-05T17:01:26Z&spr=https,http&sig=ZH%2F%2BXC2WPNDFYWS%2FKe2ifmnkxR7hjmqTGEHuXPgbHN8%3D');
|
||||
define('AZURE_STORAGE_TARGET_URL', 'https://opticaltranslations.blob.core.windows.net/translated-documents');
|
||||
|
||||
// App settings
|
||||
define('MAX_CONCURRENT_UPLOADS', 10);
|
||||
define('POLL_INTERVAL', 5000); // 5 seconds
|
||||
|
||||
// Webhook settings
|
||||
define('DATASTORE_WEBHOOK', 'https://hook.us1.make.celonis.com/8ri1h8b2he4wudp2jku69mgcxumzxf3v');
|
||||
88
create_sas_tokens.php
Normal file
88
create_sas_tokens.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
// Script to create properly formatted SAS tokens for Microsoft Document Translation
|
||||
|
||||
// Define Azure Storage account details
|
||||
$accountName = 'opticaltranslations';
|
||||
$accountKey = '3eLd+LbIlxBSu4LJnIm6rQSBsK2Ah16QJL7jlZDIQo2RkM6zB/bCuzw3KLKW9dqwaDljZdG6V13F+AStHr89KA==';
|
||||
$sourceContainer = 'source-documents';
|
||||
$targetContainer = 'translated-documents';
|
||||
|
||||
// Function to generate SAS token directly using Azure-compatible format
|
||||
function generateSasToken($accountName, $accountKey, $container, $permissions = 'r', $expiryHours = 24) {
|
||||
// Create a simpler SAS token with fewer parameters
|
||||
// Use a more recent service version
|
||||
$startTime = gmdate('Y-m-d\TH:i:s\Z', time() - 300); // 5 minutes ago
|
||||
$expiryTime = gmdate('Y-m-d\TH:i:s\Z', time() + $expiryHours * 3600);
|
||||
|
||||
// Build the string to sign in the exact format Azure expects
|
||||
$stringToSign = implode("\n", [
|
||||
$permissions,
|
||||
'', // Empty start time (use current)
|
||||
$expiryTime, // End time
|
||||
"/blob/$accountName/$container", // Canonical resource
|
||||
'', // Signed identifier (empty)
|
||||
'', // IP range (empty)
|
||||
'https', // Protocol
|
||||
'2022-11-02', // Storage version - updated to newer version
|
||||
]);
|
||||
|
||||
// Generate the signature
|
||||
$signature = base64_encode(hash_hmac('sha256', $stringToSign, base64_decode($accountKey), true));
|
||||
|
||||
// Build the SAS token
|
||||
$sasToken = sprintf(
|
||||
'sv=%s&ss=%s&srt=%s&sp=%s&se=%s&spr=%s&sig=%s',
|
||||
'2022-11-02', // Storage version
|
||||
'b', // Service (blob)
|
||||
'c', // Resource type (container)
|
||||
$permissions, // Permissions
|
||||
urlencode($expiryTime), // Expiry time
|
||||
'https', // Protocol
|
||||
urlencode($signature) // Signature
|
||||
);
|
||||
|
||||
return $sasToken;
|
||||
}
|
||||
|
||||
// Generate SAS tokens for the containers with longer expiry (7 days)
|
||||
$sourceSasToken = generateSasToken($accountName, $accountKey, $sourceContainer, 'racwdl', 168); // 7 days = 168 hours
|
||||
$targetSasToken = generateSasToken($accountName, $accountKey, $targetContainer, 'racwdl', 168);
|
||||
|
||||
// Build full URLs with SAS tokens
|
||||
$sourceUrl = "https://$accountName.blob.core.windows.net/$sourceContainer";
|
||||
$targetUrl = "https://$accountName.blob.core.windows.net/$targetContainer";
|
||||
|
||||
// Output the SAS tokens
|
||||
echo "CONFIG UPDATE NEEDED:\n\n";
|
||||
echo "Update your config.php file with these tokens:\n\n";
|
||||
|
||||
echo "// Source container SAS token and URL\n";
|
||||
echo "define('AZURE_STORAGE_SOURCE_SAS_TOKEN', '$sourceSasToken');\n";
|
||||
echo "define('AZURE_STORAGE_SOURCE_URL', '$sourceUrl');\n\n";
|
||||
|
||||
echo "// Target container SAS token and URL\n";
|
||||
echo "define('AZURE_STORAGE_TARGET_SAS_TOKEN', '$targetSasToken');\n";
|
||||
echo "define('AZURE_STORAGE_TARGET_URL', '$targetUrl');\n\n";
|
||||
|
||||
// Also output example Translator API URLs
|
||||
$sourceWithSas = "$sourceUrl?$sourceSasToken";
|
||||
$targetWithSas = "$targetUrl?$targetSasToken";
|
||||
|
||||
echo "\nURLs FOR MICROSOFT TRANSLATOR API:\n\n";
|
||||
echo "Source container URL with SAS: $sourceWithSas\n\n";
|
||||
echo "Target container URL with SAS: $targetWithSas\n\n";
|
||||
|
||||
echo "Check that these URLs work by visiting them in a browser.\n";
|
||||
echo "If they do, Microsoft Translator should be able to access your storage.\n\n";
|
||||
|
||||
// Output needed Azure portal settings
|
||||
echo "AZURE PORTAL SETTINGS NEEDED:\n\n";
|
||||
echo "1. In the Azure Portal, go to Storage Account > Containers\n";
|
||||
echo "2. For both 'source-documents' and 'translated-documents' containers:\n";
|
||||
echo " - Set Access Level to 'Container (anonymous read access for containers and blobs)'\n";
|
||||
echo "3. In Storage Account > Configuration:\n";
|
||||
echo " - Set 'Allow Blob public access' to 'Enabled'\n";
|
||||
echo " - Set 'Allow storage account key access' to 'Enabled'\n";
|
||||
echo "4. In Storage Account > Access Control (IAM):\n";
|
||||
echo " - Add the role assignment 'Storage Blob Data Contributor' for 'Microsoft.CognitiveServices'\n\n";
|
||||
?>
|
||||
256
css/style.css
Normal file
256
css/style.css
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
:root {
|
||||
--primary-btn-color: #ffaa06;
|
||||
--primary-btn-hover-color: #e09400;
|
||||
--secondary-btn-color: #ffaa06;
|
||||
--secondary-btn-hover-color: #e09400;
|
||||
--danger-btn-color: #e74c3c;
|
||||
--danger-btn-hover-color: #c0392b;
|
||||
--bg-color: #f0f0f0;
|
||||
--card-bg-color: #fff;
|
||||
--text-color: #333;
|
||||
--heading-color: #e09400;
|
||||
--border-color: #ddd;
|
||||
--input-border-color: #ddd;
|
||||
--code-bg-color: #f9f9f9;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
transition: background-color 0.3s ease, color 0.3s ease;
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
color: var(--heading-color);
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--heading-color);
|
||||
font-size: 12px;
|
||||
}
|
||||
form {
|
||||
background-color: var(--card-bg-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
input[type="file"], select, button {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
border: 1px solid var(--input-border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--primary-btn-color);
|
||||
color: #000;
|
||||
font-weight: bold;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--primary-btn-hover-color);
|
||||
}
|
||||
|
||||
#selectedFiles {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#selectedFiles ul {
|
||||
list-style-type: none;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#selectedFiles li {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
#jobList, #jobHistory {
|
||||
background-color: var(--card-bg-color);
|
||||
margin-top: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#jobList > div, #jobHistory > div {
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#jobHistory button {
|
||||
margin-left: 10px;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.9em;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
#response, #debugPanel {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background-color: var(--card-bg-color);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
#debugPanel h2 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
#debugInfo {
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
background-color: var(--code-bg-color);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* New styles for job list and buttons */
|
||||
#jobList button {
|
||||
width: auto;
|
||||
padding: 5px 10px;
|
||||
font-size: 0.9em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#jobList input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#selectAll {
|
||||
margin-bottom: 10px;
|
||||
background-color: var(--secondary-btn-color);
|
||||
}
|
||||
|
||||
#selectAll:hover {
|
||||
background-color: var(--secondary-btn-hover-color);
|
||||
}
|
||||
|
||||
#jobList > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#jobList > div > label {
|
||||
flex: 1;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#bulkDownload {
|
||||
margin-top: 10px;
|
||||
background-color: var(--danger-btn-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
#bulkDownload:hover {
|
||||
background-color: var(--danger-btn-hover-color);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Responsive design */
|
||||
@media (max-width: 600px) {
|
||||
body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
form, #jobList, #jobHistory, #response, #debugPanel {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#jobList > div, #jobHistory > div {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#jobList button, #jobHistory button {
|
||||
margin-top: 5px;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
select {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#apiInfo {
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: var(--code-bg-color);
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#apiInfo ul {
|
||||
margin: 5px 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
#apiInfo .small {
|
||||
font-size: 0.8em;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Dark Mode Styles */
|
||||
.dark-mode {
|
||||
--bg-color: #1e1e1e;
|
||||
--card-bg-color: #222;
|
||||
--text-color: #f5f5f5;
|
||||
--heading-color: #f5f5f5;
|
||||
--border-color: #444;
|
||||
--input-border-color: #444;
|
||||
--code-bg-color: #1a1a1a;
|
||||
}
|
||||
|
||||
.dark-mode #apiInfo .small {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
/* Toggle Button Styling */
|
||||
.dark-mode-toggle {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-btn-color);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.dark-mode .dark-mode-toggle {
|
||||
background-color: #ffaa06;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
444
download-old.php
Normal file
444
download-old.php
Normal file
|
|
@ -0,0 +1,444 @@
|
|||
<?php
|
||||
include 'config.php';
|
||||
include 'logger.php';
|
||||
include 'azure_storage.php';
|
||||
|
||||
logMessage("Received request to download.php");
|
||||
|
||||
if (isset($_GET['document_id']) && isset($_GET['document_key']) && isset($_GET['original_filename']) && isset($_GET['target_lang'])) {
|
||||
$document_id = $_GET['document_id'];
|
||||
$document_key = $_GET['document_key'];
|
||||
$original_filename = $_GET['original_filename'];
|
||||
$target_lang = $_GET['target_lang'];
|
||||
|
||||
logMessage("Attempting to download Document ID: $document_id, Original filename: $original_filename, Target language: $target_lang");
|
||||
|
||||
// Decode the document key to get blob information
|
||||
$documentKeyData = json_decode($document_key, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
logMessage("Invalid document_key format", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid document_key format']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Make sure we have the target blob name
|
||||
if (!isset($documentKeyData['target_blob'])) {
|
||||
logMessage("Missing target_blob in document_key", 'ERROR');
|
||||
echo json_encode(['error' => 'Missing target blob information']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetBlobName = $documentKeyData['target_blob'];
|
||||
|
||||
// Set timeout for downloads
|
||||
set_time_limit(60);
|
||||
|
||||
// Variable to store file content
|
||||
$fileContent = false;
|
||||
|
||||
// Initialize Azure Storage Helper
|
||||
$azureStorage = new AzureStorageHelper();
|
||||
|
||||
// Before downloading, check the status of the translation
|
||||
$statusUrl = MS_API_ENDPOINT . "/translator/document/batches/$document_id?api-version=" . MS_API_VERSION;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $statusUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || $response === false) {
|
||||
logMessage("Failed to check translation status. HTTP Code: $httpCode", 'ERROR');
|
||||
} else {
|
||||
$statusData = json_decode($response, true);
|
||||
|
||||
if (isset($statusData['status']) && strtolower($statusData['status']) !== 'succeeded') {
|
||||
logMessage("Translation status is not Succeeded: " . $statusData['status'] . ". Attempting download anyway.", 'WARNING');
|
||||
} else {
|
||||
logMessage("Translation has Succeeded status. Proceeding with download.");
|
||||
}
|
||||
}
|
||||
|
||||
// DIRECT APPROACH: List all files in the Azure translated container and match to our target
|
||||
logMessage("USING DIRECT AZURE LISTING to find files for document ID: $document_id");
|
||||
|
||||
// Extract the target language and other needed info
|
||||
$fileExtension = '';
|
||||
if (preg_match('/\.([^\.]+)$/', $original_filename, $extMatches)) {
|
||||
$fileExtension = strtolower($extMatches[1]);
|
||||
}
|
||||
logMessage("Original filename: $original_filename, Extension: $fileExtension, Target Language: $target_lang");
|
||||
|
||||
// Get info from document key
|
||||
$documentKeyData = json_decode($document_key, true);
|
||||
$sourceBlobName = isset($documentKeyData['source_blob']) ? $documentKeyData['source_blob'] : '';
|
||||
$targetBlobName = isset($documentKeyData['target_blob']) ? $documentKeyData['target_blob'] : '';
|
||||
|
||||
logMessage("Source blob: $sourceBlobName, Target blob: $targetBlobName");
|
||||
|
||||
// Extract the basic filename without our added prefix
|
||||
$baseFilename = '';
|
||||
if (preg_match('/^.*?\-([A-Z]+)_(.+)$/', $targetBlobName, $matches)) {
|
||||
$baseFilename = $matches[2]; // This is just the original filename
|
||||
logMessage("Base filename extracted from target blob: $baseFilename");
|
||||
}
|
||||
|
||||
// List all files in the translated container
|
||||
$sasToken = defined('AZURE_STORAGE_SAS_TOKEN') ? AZURE_STORAGE_SAS_TOKEN : '';
|
||||
$accountName = 'opticaltranslations'; // Hardcoding for simplicity
|
||||
$containerUrl = "https://$accountName.blob.core.windows.net/translated-documents?restype=container&comp=list&$sasToken";
|
||||
|
||||
logMessage("Listing all files in translated container");
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $containerUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
$listResponse = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$fileContent = false;
|
||||
$directDownloadUrl = null;
|
||||
|
||||
if ($listResponse !== false && $httpCode < 400) {
|
||||
logMessage("Successfully listed container contents");
|
||||
|
||||
// Parse XML
|
||||
$xml = simplexml_load_string($listResponse);
|
||||
if ($xml && isset($xml->Blobs->Blob)) {
|
||||
$potentialMatches = [];
|
||||
$exactMatches = [];
|
||||
|
||||
// Go through all blobs and find potential matches
|
||||
foreach ($xml->Blobs->Blob as $blob) {
|
||||
$name = (string)$blob->Name;
|
||||
$lastModified = (string)$blob->Properties->{'Last-Modified'};
|
||||
$timestamp = strtotime($lastModified);
|
||||
$size = (int)$blob->Properties->{'Content-Length'};
|
||||
|
||||
logMessage("Found blob: $name (size: $size bytes, modified: $lastModified)");
|
||||
|
||||
// Check for exact match - this is the file we uploaded
|
||||
if ($name === $targetBlobName) {
|
||||
logMessage("EXACT MATCH FOUND: $name");
|
||||
$exactMatches[$name] = $timestamp;
|
||||
}
|
||||
|
||||
// Check for matching language code
|
||||
if ((strpos($name, strtolower($target_lang)) !== false ||
|
||||
strpos($name, strtoupper($target_lang)) !== false) &&
|
||||
$size > 0) {
|
||||
logMessage("Language match found: $name");
|
||||
$potentialMatches[$name] = $timestamp;
|
||||
}
|
||||
|
||||
// Check for matching original filename (ignoring language prefix)
|
||||
if (!empty($baseFilename) && strpos($name, $baseFilename) !== false && $size > 0) {
|
||||
logMessage("Filename match found: $name");
|
||||
$potentialMatches[$name] = $timestamp;
|
||||
}
|
||||
|
||||
// Check for matching file extension
|
||||
if (!empty($fileExtension) &&
|
||||
(strpos($name, ".$fileExtension") !== false) &&
|
||||
$size > 0) {
|
||||
logMessage("Extension match found: $name");
|
||||
$potentialMatches[$name] = $timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort matches by timestamp, most recent first
|
||||
if (!empty($exactMatches)) {
|
||||
arsort($exactMatches);
|
||||
$matchedFile = key($exactMatches);
|
||||
logMessage("Using exact match file: $matchedFile");
|
||||
} else if (!empty($potentialMatches)) {
|
||||
arsort($potentialMatches);
|
||||
$matchedFile = key($potentialMatches);
|
||||
logMessage("Using potential match file: $matchedFile");
|
||||
} else {
|
||||
logMessage("No matching files found", 'WARNING');
|
||||
$matchedFile = null;
|
||||
}
|
||||
|
||||
// If we found a matching file, try to download it
|
||||
if ($matchedFile) {
|
||||
$encodedName = rawurlencode($matchedFile);
|
||||
$directDownloadUrl = "https://$accountName.blob.core.windows.net/translated-documents/$encodedName?$sasToken";
|
||||
logMessage("Direct download URL: " . substr($directDownloadUrl, 0, 100) . "...");
|
||||
|
||||
// Get file content
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $directDownloadUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); // Always use binary mode
|
||||
|
||||
$fileContent = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$finalContentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
|
||||
logMessage("Download result: HTTP $httpCode, Content-Type: $finalContentType, Size: " . strlen($fileContent) . " bytes");
|
||||
|
||||
// Store detected content type
|
||||
$GLOBALS['detected_content_type'] = $finalContentType;
|
||||
$GLOBALS['matched_filename'] = $matchedFile;
|
||||
|
||||
curl_close($ch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logMessage("Failed to list container contents. HTTP: $httpCode", 'ERROR');
|
||||
}
|
||||
|
||||
if ($fileContent !== false) {
|
||||
logMessage("Successfully downloaded file. Size: " . strlen($fileContent) . " bytes");
|
||||
} else {
|
||||
logMessage("Failed to download file using direct Azure listing. Falling back to normal method.", 'WARNING');
|
||||
// Fall back to normal download method
|
||||
$fileContent = false;
|
||||
|
||||
// Make document ID available globally for the fallback method
|
||||
$GLOBALS['document_id'] = $document_id;
|
||||
$GLOBALS['original_filename'] = $original_filename;
|
||||
|
||||
// Use the original download method as fallback
|
||||
$fileContent = $azureStorage->downloadTranslatedFile($targetBlobName);
|
||||
}
|
||||
|
||||
// We've already handled the fallback above
|
||||
|
||||
if ($fileContent === false) {
|
||||
logMessage("Failed to download translated file from Azure Storage", 'ERROR');
|
||||
|
||||
// Try to download from source as fallback
|
||||
if (isset($documentKeyData['source_blob'])) {
|
||||
logMessage("Attempting to download source file as fallback", 'WARNING');
|
||||
$sourceBlobName = $documentKeyData['source_blob'];
|
||||
$sourceContent = $azureStorage->downloadTranslatedFile($sourceBlobName);
|
||||
|
||||
if ($sourceContent !== false) {
|
||||
logMessage("Downloaded source file as fallback");
|
||||
$fileContent = "[TRANSLATION FAILED - SHOWING ORIGINAL CONTENT]\n\n" . $sourceContent;
|
||||
} else {
|
||||
echo json_encode(['error' => 'Failed to download translated file and source fallback']);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
echo json_encode(['error' => 'Failed to download translated file']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have file content by this point, proceed with download
|
||||
if ($fileContent !== false) {
|
||||
// Create new filename with language code prefix
|
||||
$fileInfo = pathinfo($original_filename);
|
||||
$newFilename = strtoupper($target_lang) . '_' . $fileInfo['filename'] . '.' . $fileInfo['extension'];
|
||||
|
||||
// Send data to webhook
|
||||
$webhookData = [
|
||||
'tool' => 'DOCUMENT_TRANSLATION',
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
'user' => 'user@example.com', // Replace with actual user information if available
|
||||
'model' => 'Microsoft Translator',
|
||||
'settings' => 'Document Translation',
|
||||
'subTool' => 'Microsoft Translator API',
|
||||
'originalFilename' => $original_filename,
|
||||
'translatedFilename' => $newFilename,
|
||||
'targetLanguage' => $target_lang,
|
||||
'documentId' => $document_id,
|
||||
'file' => base64_encode($fileContent)
|
||||
];
|
||||
sendDataToWebhook($webhookData);
|
||||
|
||||
// Use the matched filename if available to determine content type
|
||||
$contentType = 'application/octet-stream'; // Default content type
|
||||
$extension = strtolower($fileInfo['extension']);
|
||||
|
||||
// If we matched a file from Azure storage, use its extension
|
||||
if (isset($GLOBALS['matched_filename']) && !empty($GLOBALS['matched_filename'])) {
|
||||
$matchedFilename = $GLOBALS['matched_filename'];
|
||||
logMessage("Using matched filename for content type detection: $matchedFilename");
|
||||
|
||||
// Extract extension from matched filename
|
||||
if (preg_match('/\.([^\.]+)$/', $matchedFilename, $matchedExtMatches)) {
|
||||
$matchedExtension = strtolower($matchedExtMatches[1]);
|
||||
logMessage("Matched file extension: $matchedExtension");
|
||||
|
||||
// If the matched extension is different from requested, adjust the filename
|
||||
if ($matchedExtension !== $extension) {
|
||||
logMessage("Matched file extension ($matchedExtension) differs from requested ($extension). Adjusting filename.", 'WARNING');
|
||||
$newFilename = $fileInfo['filename'] . '.' . $matchedExtension;
|
||||
}
|
||||
|
||||
// Set content type based on matched extension
|
||||
switch ($matchedExtension) {
|
||||
case 'pdf':
|
||||
$contentType = 'application/pdf';
|
||||
break;
|
||||
case 'docx':
|
||||
case 'doc':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
break;
|
||||
case 'pptx':
|
||||
case 'ppt':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
||||
break;
|
||||
case 'xlsx':
|
||||
case 'xls':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
break;
|
||||
case 'txt':
|
||||
$contentType = 'text/plain';
|
||||
break;
|
||||
case 'html':
|
||||
case 'htm':
|
||||
$contentType = 'text/html';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if we detected a content type from the direct download
|
||||
else if (isset($GLOBALS['detected_content_type']) && !empty($GLOBALS['detected_content_type'])) {
|
||||
$contentType = $GLOBALS['detected_content_type'];
|
||||
logMessage("Using detected Content-Type from server: $contentType");
|
||||
|
||||
// If Microsoft translated to PDF, but we're requesting a different file type,
|
||||
// adjust the filename extension to match the content
|
||||
if ($contentType === 'application/pdf' && $extension !== 'pdf') {
|
||||
logMessage("Content is PDF but requested extension is $extension. Adjusting filename extension to .pdf", 'WARNING');
|
||||
$newFilename = $fileInfo['filename'] . '.pdf';
|
||||
}
|
||||
}
|
||||
// Fallback to the original extension
|
||||
else {
|
||||
logMessage("Fallback to original file extension: $extension");
|
||||
switch ($extension) {
|
||||
case 'pdf':
|
||||
$contentType = 'application/pdf';
|
||||
break;
|
||||
case 'docx':
|
||||
case 'doc':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
break;
|
||||
case 'pptx':
|
||||
case 'ppt':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
||||
break;
|
||||
case 'xlsx':
|
||||
case 'xls':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
break;
|
||||
case 'txt':
|
||||
$contentType = 'text/plain';
|
||||
break;
|
||||
case 'html':
|
||||
case 'htm':
|
||||
$contentType = 'text/html';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Basic content type detection from file content
|
||||
if ($contentType === 'application/octet-stream') {
|
||||
// Try to detect content type from file contents
|
||||
if (substr($fileContent, 0, 4) === '%PDF') {
|
||||
logMessage("PDF signature detected in content, setting content type to application/pdf", 'WARNING');
|
||||
$contentType = 'application/pdf';
|
||||
// If the file doesn't have .pdf extension, adjust it
|
||||
if ($extension !== 'pdf') {
|
||||
$newFilename = $fileInfo['filename'] . '.pdf';
|
||||
}
|
||||
} else if (substr($fileContent, 0, 4) === 'PK\x03\x04') {
|
||||
// Office files are zip files
|
||||
if (strpos($original_filename, '.pptx') !== false || strpos($original_filename, '.ppt') !== false) {
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
||||
} else if (strpos($original_filename, '.docx') !== false || strpos($original_filename, '.doc') !== false) {
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
} else if (strpos($original_filename, '.xlsx') !== false || strpos($original_filename, '.xls') !== false) {
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For binary files like PDF and PowerPoint, ensure proper handling
|
||||
$isBinary = in_array($extension, ['pdf', 'pptx', 'ppt', 'docx', 'doc', 'xlsx', 'xls']);
|
||||
|
||||
// Check for valid binary content for PDFs and PowerPoint files
|
||||
if ($isBinary) {
|
||||
// Basic binary file validation - check for file signatures
|
||||
if ($extension === 'pdf' && substr($fileContent, 0, 4) !== '%PDF') {
|
||||
logMessage("Warning: PDF file doesn't have proper signature", 'WARNING');
|
||||
} else if (($extension === 'pptx' || $extension === 'docx') && substr($fileContent, 0, 4) !== 'PK\x03\x04') {
|
||||
logMessage("Warning: Office file doesn't have proper ZIP signature", 'WARNING');
|
||||
}
|
||||
|
||||
// Ensure we're not sending text content for binary files
|
||||
if (preg_match('/^\[EMERGENCY FALLBACK/', $fileContent)) {
|
||||
logMessage("Error: Emergency fallback content detected for binary file", 'ERROR');
|
||||
// Still proceed with download, but notify in logs
|
||||
}
|
||||
|
||||
// Log file size details
|
||||
logMessage("Binary file download: $contentType, size: " . strlen($fileContent) . " bytes");
|
||||
}
|
||||
|
||||
// Send the file to the client
|
||||
header("Content-Type: $contentType");
|
||||
header("Content-Disposition: attachment; filename=\"$newFilename\"");
|
||||
header("Content-Length: " . strlen($fileContent));
|
||||
header("Cache-Control: no-cache, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
|
||||
echo $fileContent;
|
||||
logMessage("File download initiated: $newFilename");
|
||||
} else {
|
||||
echo json_encode(['error' => 'Failed to retrieve file content']);
|
||||
}
|
||||
} else {
|
||||
logMessage("Missing required parameters", 'ERROR');
|
||||
echo json_encode(['error' => 'Missing required parameters (document_id, document_key, original_filename, or target_lang)']);
|
||||
}
|
||||
|
||||
function sendDataToWebhook($data)
|
||||
{
|
||||
if (!defined('DATASTORE_WEBHOOK') || empty(DATASTORE_WEBHOOK)) {
|
||||
logMessage('Webhook URL not defined. Skipping webhook notification.', 'WARNING');
|
||||
return;
|
||||
}
|
||||
|
||||
$url = DATASTORE_WEBHOOK;
|
||||
$headers = ['Content-Type: application/json'];
|
||||
$payload = json_encode($data);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($statusCode === 200) {
|
||||
logMessage('Data sent to webhook successfully');
|
||||
} else {
|
||||
logMessage('Error sending data to webhook: ' . $error, 'ERROR');
|
||||
}
|
||||
}
|
||||
?>
|
||||
458
download.php
Normal file
458
download.php
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
<?php
|
||||
include 'config.php';
|
||||
include 'logger.php';
|
||||
include 'azure_storage.php';
|
||||
|
||||
logMessage("Received request to download.php");
|
||||
|
||||
if (isset($_GET['document_id']) && isset($_GET['document_key']) && isset($_GET['original_filename']) && isset($_GET['target_lang'])) {
|
||||
$document_id = $_GET['document_id'];
|
||||
$document_key = $_GET['document_key'];
|
||||
$original_filename = $_GET['original_filename'];
|
||||
$target_lang = $_GET['target_lang'];
|
||||
|
||||
logMessage("Attempting to download Document ID: $document_id, Original filename: $original_filename, Target language: $target_lang");
|
||||
|
||||
// Decode the document key to get blob information
|
||||
$documentKeyData = json_decode($document_key, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
logMessage("Invalid document_key format", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid document_key format']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Make sure we have the target blob name
|
||||
if (!isset($documentKeyData['target_blob'])) {
|
||||
logMessage("Missing target_blob in document_key", 'ERROR');
|
||||
echo json_encode(['error' => 'Missing target blob information']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetBlobName = $documentKeyData['target_blob'];
|
||||
|
||||
// Set timeout for downloads
|
||||
set_time_limit(60);
|
||||
|
||||
// Variable to store file content
|
||||
$fileContent = false;
|
||||
|
||||
// Initialize Azure Storage Helper
|
||||
$azureStorage = new AzureStorageHelper();
|
||||
|
||||
// Before downloading, check the status of the translation
|
||||
$statusUrl = MS_API_ENDPOINT . "/translator/document/batches/$document_id?api-version=" . MS_API_VERSION;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $statusUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200 || $response === false) {
|
||||
logMessage("Failed to check translation status. HTTP Code: $httpCode", 'ERROR');
|
||||
} else {
|
||||
$statusData = json_decode($response, true);
|
||||
|
||||
if (isset($statusData['status']) && strtolower($statusData['status']) !== 'succeeded') {
|
||||
logMessage("Translation status is not Succeeded: " . $statusData['status'] . ". Attempting download anyway.", 'WARNING');
|
||||
} else {
|
||||
logMessage("Translation has Succeeded status. Proceeding with download.");
|
||||
}
|
||||
}
|
||||
|
||||
// DIRECT APPROACH: List all files in the Azure translated container and match to our target
|
||||
logMessage("USING DIRECT AZURE LISTING to find files for document ID: $document_id");
|
||||
|
||||
// Extract the target language and other needed info
|
||||
$fileExtension = '';
|
||||
if (preg_match('/\.([^\.]+)$/', $original_filename, $extMatches)) {
|
||||
$fileExtension = strtolower($extMatches[1]);
|
||||
}
|
||||
logMessage("Original filename: $original_filename, Extension: $fileExtension, Target Language: $target_lang");
|
||||
|
||||
// Get info from document key
|
||||
$documentKeyData = json_decode($document_key, true);
|
||||
$sourceBlobName = isset($documentKeyData['source_blob']) ? $documentKeyData['source_blob'] : '';
|
||||
$targetBlobName = isset($documentKeyData['target_blob']) ? $documentKeyData['target_blob'] : '';
|
||||
|
||||
logMessage("Source blob: $sourceBlobName, Target blob: $targetBlobName");
|
||||
|
||||
// Extract the basic filename without our added prefix
|
||||
$baseFilename = '';
|
||||
if (preg_match('/^.*?\-([A-Z]+)_(.+)$/', $targetBlobName, $matches)) {
|
||||
$baseFilename = $matches[2]; // This is just the original filename
|
||||
logMessage("Base filename extracted from target blob: $baseFilename");
|
||||
}
|
||||
|
||||
// List all files in the translated container
|
||||
$sasToken = defined('AZURE_STORAGE_SAS_TOKEN') ? AZURE_STORAGE_SAS_TOKEN : '';
|
||||
$accountName = 'opticaltranslations'; // Hardcoding for simplicity
|
||||
$containerUrl = "https://$accountName.blob.core.windows.net/translated-documents?restype=container&comp=list&$sasToken";
|
||||
|
||||
logMessage("Listing all files in translated container");
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $containerUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
$listResponse = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$fileContent = false;
|
||||
$directDownloadUrl = null;
|
||||
|
||||
if ($listResponse !== false && $httpCode < 400) {
|
||||
logMessage("Successfully listed container contents");
|
||||
|
||||
// Parse XML
|
||||
$xml = simplexml_load_string($listResponse);
|
||||
if ($xml && isset($xml->Blobs->Blob)) {
|
||||
$potentialMatches = [];
|
||||
$exactMatches = [];
|
||||
|
||||
// Go through all blobs and find potential matches
|
||||
foreach ($xml->Blobs->Blob as $blob) {
|
||||
$name = (string)$blob->Name;
|
||||
$lastModified = (string)$blob->Properties->{'Last-Modified'};
|
||||
$timestamp = strtotime($lastModified);
|
||||
$size = (int)$blob->Properties->{'Content-Length'};
|
||||
|
||||
logMessage("Found blob: $name (size: $size bytes, modified: $lastModified)");
|
||||
|
||||
// Check for exact match - this is the file we uploaded
|
||||
if ($name === $targetBlobName) {
|
||||
logMessage("EXACT MATCH FOUND: $name");
|
||||
$exactMatches[$name] = $timestamp;
|
||||
}
|
||||
|
||||
// Check for matching language code
|
||||
if ((strpos($name, strtolower($target_lang)) !== false ||
|
||||
strpos($name, strtoupper($target_lang)) !== false) &&
|
||||
$size > 0) {
|
||||
logMessage("Language match found: $name");
|
||||
$potentialMatches[$name] = $timestamp;
|
||||
}
|
||||
|
||||
// Check for matching original filename (ignoring language prefix)
|
||||
if (!empty($baseFilename) && strpos($name, $baseFilename) !== false && $size > 0) {
|
||||
logMessage("Filename match found: $name");
|
||||
$potentialMatches[$name] = $timestamp;
|
||||
}
|
||||
|
||||
// Check for matching file extension
|
||||
if (!empty($fileExtension) &&
|
||||
(strpos($name, ".$fileExtension") !== false) &&
|
||||
$size > 0) {
|
||||
logMessage("Extension match found: $name");
|
||||
$potentialMatches[$name] = $timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// Sort matches by timestamp, most recent first
|
||||
if (!empty($exactMatches)) {
|
||||
arsort($exactMatches);
|
||||
$matchedFile = key($exactMatches);
|
||||
logMessage("Using exact match file: $matchedFile");
|
||||
} else if (!empty($potentialMatches)) {
|
||||
arsort($potentialMatches);
|
||||
$matchedFile = key($potentialMatches);
|
||||
logMessage("Using potential match file: $matchedFile");
|
||||
} else {
|
||||
logMessage("No matching files found", 'WARNING');
|
||||
$matchedFile = null;
|
||||
}
|
||||
|
||||
// If we found a matching file, try to download it
|
||||
if ($matchedFile) {
|
||||
$encodedName = rawurlencode($matchedFile);
|
||||
$directDownloadUrl = "https://$accountName.blob.core.windows.net/translated-documents/$encodedName?$sasToken";
|
||||
logMessage("Direct download URL: " . substr($directDownloadUrl, 0, 100) . "...");
|
||||
|
||||
// Get file content
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $directDownloadUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); // Always use binary mode
|
||||
|
||||
$fileContent = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$finalContentType = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
|
||||
|
||||
logMessage("Download result: HTTP $httpCode, Content-Type: $finalContentType, Size: " . strlen($fileContent) . " bytes");
|
||||
|
||||
// Store detected content type
|
||||
$GLOBALS['detected_content_type'] = $finalContentType;
|
||||
$GLOBALS['matched_filename'] = $matchedFile;
|
||||
|
||||
curl_close($ch);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logMessage("Failed to list container contents. HTTP: $httpCode", 'ERROR');
|
||||
}
|
||||
|
||||
if ($fileContent !== false) {
|
||||
logMessage("Successfully downloaded file. Size: " . strlen($fileContent) . " bytes");
|
||||
} else {
|
||||
logMessage("Failed to download file using direct Azure listing. Falling back to normal method.", 'WARNING');
|
||||
// Fall back to normal download method
|
||||
$fileContent = false;
|
||||
|
||||
// Make document ID available globally for the fallback method
|
||||
$GLOBALS['document_id'] = $document_id;
|
||||
$GLOBALS['original_filename'] = $original_filename;
|
||||
|
||||
// Use the original download method as fallback
|
||||
$fileContent = $azureStorage->downloadTranslatedFile($targetBlobName);
|
||||
}
|
||||
|
||||
// We've already handled the fallback above
|
||||
|
||||
if ($fileContent === false) {
|
||||
logMessage("Failed to download translated file from Azure Storage", 'ERROR');
|
||||
|
||||
// Try to download from source as fallback
|
||||
if (isset($documentKeyData['source_blob'])) {
|
||||
logMessage("Attempting to download source file as fallback", 'WARNING');
|
||||
$sourceBlobName = $documentKeyData['source_blob'];
|
||||
$sourceContent = $azureStorage->downloadTranslatedFile($sourceBlobName);
|
||||
|
||||
if ($sourceContent !== false) {
|
||||
logMessage("Downloaded source file as fallback");
|
||||
$fileContent = "[TRANSLATION FAILED - SHOWING ORIGINAL CONTENT]\n\n" . $sourceContent;
|
||||
} else {
|
||||
echo json_encode(['error' => 'Failed to download translated file and source fallback']);
|
||||
exit;
|
||||
}
|
||||
} else {
|
||||
echo json_encode(['error' => 'Failed to download translated file']);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have file content by this point, proceed with download
|
||||
if ($fileContent !== false) {
|
||||
// Create new filename with language code prefix
|
||||
$fileInfo = pathinfo($original_filename);
|
||||
$newFilename = strtoupper($target_lang) . '_' . $fileInfo['filename'] . '.' . $fileInfo['extension'];
|
||||
|
||||
// Use the matched filename if available to determine content type
|
||||
$contentType = 'application/octet-stream'; // Default content type
|
||||
$extension = strtolower($fileInfo['extension']);
|
||||
|
||||
// If we matched a file from Azure storage, use its extension
|
||||
if (isset($GLOBALS['matched_filename']) && !empty($GLOBALS['matched_filename'])) {
|
||||
$matchedFilename = $GLOBALS['matched_filename'];
|
||||
logMessage("Using matched filename for content type detection: $matchedFilename");
|
||||
|
||||
// Extract extension from matched filename
|
||||
if (preg_match('/\.([^\.]+)$/', $matchedFilename, $matchedExtMatches)) {
|
||||
$matchedExtension = strtolower($matchedExtMatches[1]);
|
||||
logMessage("Matched file extension: $matchedExtension");
|
||||
|
||||
// If the matched extension is different from requested, adjust the filename
|
||||
if ($matchedExtension !== $extension) {
|
||||
logMessage("Matched file extension ($matchedExtension) differs from requested ($extension). Adjusting filename.", 'WARNING');
|
||||
$newFilename = $fileInfo['filename'] . '.' . $matchedExtension;
|
||||
}
|
||||
|
||||
// Set content type based on matched extension
|
||||
switch ($matchedExtension) {
|
||||
case 'pdf':
|
||||
$contentType = 'application/pdf';
|
||||
break;
|
||||
case 'docx':
|
||||
case 'doc':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
break;
|
||||
case 'pptx':
|
||||
case 'ppt':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
||||
break;
|
||||
case 'xlsx':
|
||||
case 'xls':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
break;
|
||||
case 'txt':
|
||||
$contentType = 'text/plain';
|
||||
break;
|
||||
case 'html':
|
||||
case 'htm':
|
||||
$contentType = 'text/html';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check if we detected a content type from the direct download
|
||||
else if (isset($GLOBALS['detected_content_type']) && !empty($GLOBALS['detected_content_type'])) {
|
||||
$contentType = $GLOBALS['detected_content_type'];
|
||||
logMessage("Using detected Content-Type from server: $contentType");
|
||||
|
||||
// If Microsoft translated to PDF, but we're requesting a different file type,
|
||||
// adjust the filename extension to match the content
|
||||
if ($contentType === 'application/pdf' && $extension !== 'pdf') {
|
||||
logMessage("Content is PDF but requested extension is $extension. Adjusting filename extension to .pdf", 'WARNING');
|
||||
$newFilename = $fileInfo['filename'] . '.pdf';
|
||||
}
|
||||
}
|
||||
// Fallback to the original extension
|
||||
else {
|
||||
logMessage("Fallback to original file extension: $extension");
|
||||
switch ($extension) {
|
||||
case 'pdf':
|
||||
$contentType = 'application/pdf';
|
||||
break;
|
||||
case 'docx':
|
||||
case 'doc':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
break;
|
||||
case 'pptx':
|
||||
case 'ppt':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
||||
break;
|
||||
case 'xlsx':
|
||||
case 'xls':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
break;
|
||||
case 'txt':
|
||||
$contentType = 'text/plain';
|
||||
break;
|
||||
case 'html':
|
||||
case 'htm':
|
||||
$contentType = 'text/html';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Basic content type detection from file content
|
||||
if ($contentType === 'application/octet-stream') {
|
||||
// Try to detect content type from file contents
|
||||
if (substr($fileContent, 0, 4) === '%PDF') {
|
||||
logMessage("PDF signature detected in content, setting content type to application/pdf", 'WARNING');
|
||||
$contentType = 'application/pdf';
|
||||
// If the file doesn't have .pdf extension, adjust it
|
||||
if ($extension !== 'pdf') {
|
||||
$newFilename = $fileInfo['filename'] . '.pdf';
|
||||
}
|
||||
} else if (substr($fileContent, 0, 4) === 'PK\x03\x04') {
|
||||
// Office files are zip files
|
||||
if (strpos($original_filename, '.pptx') !== false || strpos($original_filename, '.ppt') !== false) {
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
||||
} else if (strpos($original_filename, '.docx') !== false || strpos($original_filename, '.doc') !== false) {
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
} else if (strpos($original_filename, '.xlsx') !== false || strpos($original_filename, '.xls') !== false) {
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For binary files like PDF and PowerPoint, ensure proper handling
|
||||
$isBinary = in_array($extension, ['pdf', 'pptx', 'ppt', 'docx', 'doc', 'xlsx', 'xls']);
|
||||
|
||||
// Check for valid binary content for PDFs and PowerPoint files
|
||||
if ($isBinary) {
|
||||
// Basic binary file validation - check for file signatures
|
||||
if ($extension === 'pdf' && substr($fileContent, 0, 4) !== '%PDF') {
|
||||
logMessage("Warning: PDF file doesn't have proper signature", 'WARNING');
|
||||
} else if (($extension === 'pptx' || $extension === 'docx') && substr($fileContent, 0, 4) !== 'PK\x03\x04') {
|
||||
logMessage("Warning: Office file doesn't have proper ZIP signature", 'WARNING');
|
||||
}
|
||||
|
||||
// Ensure we're not sending text content for binary files
|
||||
if (preg_match('/^\[EMERGENCY FALLBACK/', $fileContent)) {
|
||||
logMessage("Error: Emergency fallback content detected for binary file", 'ERROR');
|
||||
// Still proceed with download, but notify in logs
|
||||
}
|
||||
|
||||
// Log file size details
|
||||
logMessage("Binary file download: $contentType, size: " . strlen($fileContent) . " bytes");
|
||||
}
|
||||
|
||||
// Send the file to the client
|
||||
header("Content-Type: $contentType");
|
||||
header("Content-Disposition: attachment; filename=\"$newFilename\"");
|
||||
header("Content-Length: " . strlen($fileContent));
|
||||
header("Cache-Control: no-cache, must-revalidate");
|
||||
header("Pragma: no-cache");
|
||||
|
||||
echo $fileContent;
|
||||
logMessage("File download initiated: $newFilename");
|
||||
|
||||
// Flush output to the client, then send webhook in the background
|
||||
// This ensures the user gets their download immediately
|
||||
if (function_exists('fastcgi_finish_request')) {
|
||||
// PHP-FPM: cleanly closes the connection to the client
|
||||
fastcgi_finish_request();
|
||||
} else {
|
||||
// Fallback for non-FPM setups (e.g. local development)
|
||||
ignore_user_abort(true);
|
||||
if (ob_get_level() > 0) {
|
||||
ob_end_flush();
|
||||
}
|
||||
flush();
|
||||
}
|
||||
|
||||
// Now send the webhook data in the background (client already has their file)
|
||||
$webhookData = [
|
||||
'tool' => 'DOCUMENT_TRANSLATION',
|
||||
'date' => date('Y-m-d H:i:s'),
|
||||
'user' => 'user@example.com', // Replace with actual user information if available
|
||||
'model' => 'Microsoft Translator',
|
||||
'settings' => 'Document Translation',
|
||||
'subTool' => 'Microsoft Translator API',
|
||||
'originalFilename' => $original_filename,
|
||||
'translatedFilename' => $newFilename,
|
||||
'targetLanguage' => $target_lang,
|
||||
'documentId' => $document_id,
|
||||
'file' => base64_encode($fileContent)
|
||||
];
|
||||
sendDataToWebhook($webhookData);
|
||||
} else {
|
||||
echo json_encode(['error' => 'Failed to retrieve file content']);
|
||||
}
|
||||
} else {
|
||||
logMessage("Missing required parameters", 'ERROR');
|
||||
echo json_encode(['error' => 'Missing required parameters (document_id, document_key, original_filename, or target_lang)']);
|
||||
}
|
||||
|
||||
function sendDataToWebhook($data)
|
||||
{
|
||||
if (!defined('DATASTORE_WEBHOOK') || empty(DATASTORE_WEBHOOK)) {
|
||||
logMessage('Webhook URL not defined. Skipping webhook notification.', 'WARNING');
|
||||
return;
|
||||
}
|
||||
|
||||
$url = DATASTORE_WEBHOOK;
|
||||
$headers = ['Content-Type: application/json'];
|
||||
$payload = json_encode($data);
|
||||
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
if ($statusCode === 200) {
|
||||
logMessage('Data sent to webhook successfully');
|
||||
} else {
|
||||
logMessage('Error sending data to webhook: HTTP ' . $statusCode . ', cURL error: ' . $error . ', payload size: ' . strlen($payload) . ' bytes', 'ERROR');
|
||||
}
|
||||
}
|
||||
?>
|
||||
27
download_debug.log
Normal file
27
download_debug.log
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
[2025-06-18 12:52:09] [INFO] HIGHEST PRIORITY: Looking for exact blob name match: 6852b61bd21c4-ES_2a.pptx
|
||||
[2025-06-18 12:52:09] [INFO] PRIORITY DIRECT DOWNLOAD ATTEMPT for: 6852b61bd21c4-ES_2a.pptx
|
||||
[2025-06-18 12:52:09] [INFO] DIRECT DOWNLOAD URL (partial): https://opticaltranslations.blob.core.windows.net/translated...
|
||||
[2025-06-18 12:52:10] [INFO] DIRECT DOWNLOAD CURL INFO: {"url":"https:\/\/opticaltranslations.blob.core.windows.net\/translated-documents\/6852b61bd21c4-ES_2a.pptx?sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2026-05-06T01:01:26Z&st=2025-05-05T17:01:26Z&spr=https,http&sig=ZH%2F%2BXC2WPNDFYWS%2FKe2ifmnkxR7hjmqTGEHuXPgbHN8%3D","content_type":"application\/xml","http_code":404,"header_size":306,"request_size":292,"filetime":-1,"ssl_verify_result":20,"redirect_count":0,"total_time":0.333705,"namelookup_time":0.007517,"connect_time":0.085818,"pretransfer_time":0.249319,"size_upload":0,"size_download":215,"speed_download":644,"speed_upload":0,"download_content_length":215,"upload_content_length":0,"starttransfer_time":0.333683,"redirect_time":0,"redirect_url":"","primary_ip":"52.239.169.100","certinfo":[],"primary_port":443,"local_ip":"10.154.0.3","local_port":35076,"http_version":2,"protocol":2,"ssl_verifyresult":0,"scheme":"HTTPS","appconnect_time_us":249278,"connect_time_us":85818,"namelookup_time_us":7517,"pretransfer_time_us":249319,"redirect_time_us":0,"starttransfer_time_us":333683,"total_time_us":333705}
|
||||
[2025-06-18 12:52:10] [WARNING] DIRECT DOWNLOAD FAILED. HTTP: 404, Error:
|
||||
[2025-06-18 12:52:17] [INFO] LAST RESORT: Trying direct Azure Portal access
|
||||
[2025-06-18 12:52:17] [INFO] LAST RESORT: Check Azure Portal manually using the SAS token: sv=2024-11-04&ss=bfqt&srt=sco&...
|
||||
[2025-06-18 12:52:17] [INFO] LAST RESORT: You may need to check storage account 'opticaltranslations' container 'translated-documents' for file named: 6852b61bd21c4-ES_2a.pptx
|
||||
[2025-06-18 12:52:17] [INFO] LAST RESORT: You may need to check the document status at: https://optical-translation.cognitiveservices.azure.com//translator/document/batches/9424b293-db76-474b-8edf-c6bd4b5a210d
|
||||
[2025-06-18 17:16:40] [INFO] HIGHEST PRIORITY: Looking for exact blob name match: 6852bdc08c496-ES_2a.pptx
|
||||
[2025-06-18 17:16:40] [INFO] PRIORITY DIRECT DOWNLOAD ATTEMPT for: 6852bdc08c496-ES_2a.pptx
|
||||
[2025-06-18 17:16:40] [INFO] DIRECT DOWNLOAD URL (partial): https://opticaltranslations.blob.core.windows.net/translated...
|
||||
[2025-06-18 17:16:41] [INFO] DIRECT DOWNLOAD CURL INFO: {"url":"https:\/\/opticaltranslations.blob.core.windows.net\/translated-documents\/6852bdc08c496-ES_2a.pptx?sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2026-05-06T01:01:26Z&st=2025-05-05T17:01:26Z&spr=https,http&sig=ZH%2F%2BXC2WPNDFYWS%2FKe2ifmnkxR7hjmqTGEHuXPgbHN8%3D","content_type":"application\/xml","http_code":404,"header_size":306,"request_size":292,"filetime":-1,"ssl_verify_result":20,"redirect_count":0,"total_time":0.316135,"namelookup_time":0.001355,"connect_time":0.076744,"pretransfer_time":0.234021,"size_upload":0,"size_download":215,"speed_download":680,"speed_upload":0,"download_content_length":215,"upload_content_length":0,"starttransfer_time":0.316118,"redirect_time":0,"redirect_url":"","primary_ip":"52.239.169.100","certinfo":[],"primary_port":443,"local_ip":"10.154.0.3","local_port":59192,"http_version":2,"protocol":2,"ssl_verifyresult":0,"scheme":"HTTPS","appconnect_time_us":233987,"connect_time_us":76744,"namelookup_time_us":1355,"pretransfer_time_us":234021,"redirect_time_us":0,"starttransfer_time_us":316118,"total_time_us":316135}
|
||||
[2025-06-18 17:16:41] [WARNING] DIRECT DOWNLOAD FAILED. HTTP: 404, Error:
|
||||
[2025-06-18 17:16:48] [INFO] LAST RESORT: Trying direct Azure Portal access
|
||||
[2025-06-18 17:16:48] [INFO] LAST RESORT: Check Azure Portal manually using the SAS token: sv=2024-11-04&ss=bfqt&srt=sco&...
|
||||
[2025-06-18 17:16:48] [INFO] LAST RESORT: You may need to check storage account 'opticaltranslations' container 'translated-documents' for file named: 6852bdc08c496-ES_2a.pptx
|
||||
[2025-06-18 17:16:48] [INFO] LAST RESORT: You may need to check the document status at: https://optical-translation.cognitiveservices.azure.com//translator/document/batches/51085ab6-2560-487b-b434-c1d9b24dceab
|
||||
[2025-06-18 18:43:20] [INFO] HIGHEST PRIORITY: Looking for exact blob name match: 6853044f2ed3f-ES_2025_06_13_AptHubTecnologia_FINAL_Lunapt1d.pptx
|
||||
[2025-06-18 18:43:20] [INFO] PRIORITY DIRECT DOWNLOAD ATTEMPT for: 6853044f2ed3f-ES_2025_06_13_AptHubTecnologia_FINAL_Lunapt1d.pptx
|
||||
[2025-06-18 18:43:20] [INFO] DIRECT DOWNLOAD URL (partial): https://opticaltranslations.blob.core.windows.net/translated...
|
||||
[2025-06-18 18:43:20] [INFO] DIRECT DOWNLOAD CURL INFO: {"url":"https:\/\/opticaltranslations.blob.core.windows.net\/translated-documents\/6853044f2ed3f-ES_2025_06_13_AptHubTecnologia_FINAL_Lunapt1d.pptx?sv=2024-11-04&ss=bfqt&srt=sco&sp=rwdlacupiytfx&se=2026-05-06T01:01:26Z&st=2025-05-05T17:01:26Z&spr=https,http&sig=ZH%2F%2BXC2WPNDFYWS%2FKe2ifmnkxR7hjmqTGEHuXPgbHN8%3D","content_type":"application\/xml","http_code":404,"header_size":306,"request_size":332,"filetime":-1,"ssl_verify_result":20,"redirect_count":0,"total_time":0.317041,"namelookup_time":0.0015,"connect_time":0.077183,"pretransfer_time":0.235592,"size_upload":0,"size_download":215,"speed_download":678,"speed_upload":0,"download_content_length":215,"upload_content_length":0,"starttransfer_time":0.317021,"redirect_time":0,"redirect_url":"","primary_ip":"52.239.169.100","certinfo":[],"primary_port":443,"local_ip":"10.154.0.3","local_port":43292,"http_version":2,"protocol":2,"ssl_verifyresult":0,"scheme":"HTTPS","appconnect_time_us":235566,"connect_time_us":77183,"namelookup_time_us":1500,"pretransfer_time_us":235592,"redirect_time_us":0,"starttransfer_time_us":317021,"total_time_us":317041}
|
||||
[2025-06-18 18:43:20] [WARNING] DIRECT DOWNLOAD FAILED. HTTP: 404, Error:
|
||||
[2025-06-18 18:43:28] [INFO] LAST RESORT: Trying direct Azure Portal access
|
||||
[2025-06-18 18:43:28] [INFO] LAST RESORT: Check Azure Portal manually using the SAS token: sv=2024-11-04&ss=bfqt&srt=sco&...
|
||||
[2025-06-18 18:43:28] [INFO] LAST RESORT: You may need to check storage account 'opticaltranslations' container 'translated-documents' for file named: 6853044f2ed3f-ES_2025_06_13_AptHubTecnologia_FINAL_Lunapt1d.pptx
|
||||
[2025-06-18 18:43:28] [INFO] LAST RESORT: You may need to check the document status at: https://optical-translation.cognitiveservices.azure.com//translator/document/batches/67e8ce9c-573e-438c-9ec8-2e1e7186a747
|
||||
250
index.php
Normal file
250
index.php
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
<?php
|
||||
include 'config.php';
|
||||
include 'cleanup.php';
|
||||
|
||||
// Run cleanup on page load
|
||||
cleanupOldFiles();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<head>
|
||||
<div style="background-color: #ff0000; color: white; padding: 10px; text-align: center;">
|
||||
AI Sandbox - NOT FOR PRODUCTION USE - <a href="https://olivermarketing.sharepoint.com/sites/AIFROMTHEINSIDE/" style="color: white; text-decoration: underline;">Click Here for Production Tools</a>
|
||||
</div>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document Translator</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
|
||||
<!-- auth 1 of 4 -->
|
||||
<script src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
#protected-content {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<!-- end auth block -->
|
||||
|
||||
</head>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Microsoft Document Translator</title>
|
||||
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;700&display=swap">
|
||||
<link rel="stylesheet" href="css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- auth 2 of 4 -->
|
||||
<div style="text-align: left;">
|
||||
<button id="logout-button" onclick="signOut()" style="display:none;">Log Out</button>
|
||||
<button id="login-button" onclick="signIn()" style="display:none;">Log In</button>
|
||||
</div>
|
||||
<!-- end auth block -->
|
||||
|
||||
<h1>Microsoft Document Translator</h1>
|
||||
<form id="uploadForm" enctype="multipart/form-data">
|
||||
<input type="file" name="documents[]" accept=".docx,.doc,.pptx,.xlsx,.pdf,.htm,.html,.txt" multiple required>
|
||||
<div id="selectedFiles"></div>
|
||||
<select name="source_lang" required>
|
||||
<option value="">Select source language</option>
|
||||
<option value="AUTO">Auto Detect</option>
|
||||
<option value="af">Afrikaans</option>
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="bg">Bulgarian</option>
|
||||
<option value="bn">Bengali</option>
|
||||
<option value="bs">Bosnian</option>
|
||||
<option value="ca">Catalan</option>
|
||||
<option value="cs">Czech</option>
|
||||
<option value="cy">Welsh</option>
|
||||
<option value="da">Danish</option>
|
||||
<option value="de">German</option>
|
||||
<option value="el">Greek</option>
|
||||
<option value="en">English</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="et">Estonian</option>
|
||||
<option value="fa">Persian</option>
|
||||
<option value="fi">Finnish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="he">Hebrew</option>
|
||||
<option value="hi">Hindi</option>
|
||||
<option value="hr">Croatian</option>
|
||||
<option value="ht">Haitian Creole</option>
|
||||
<option value="hu">Hungarian</option>
|
||||
<option value="id">Indonesian</option>
|
||||
<option value="is">Icelandic</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<option value="ko">Korean</option>
|
||||
<option value="lt">Lithuanian</option>
|
||||
<option value="lv">Latvian</option>
|
||||
<option value="ms">Malay</option>
|
||||
<option value="mt">Maltese</option>
|
||||
<option value="nl">Dutch</option>
|
||||
<option value="nb">Norwegian</option>
|
||||
<option value="pl">Polish</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="ro">Romanian</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="sk">Slovak</option>
|
||||
<option value="sl">Slovenian</option>
|
||||
<option value="sr">Serbian</option>
|
||||
<option value="sv">Swedish</option>
|
||||
<option value="sw">Swahili</option>
|
||||
<option value="th">Thai</option>
|
||||
<option value="tr">Turkish</option>
|
||||
<option value="uk">Ukrainian</option>
|
||||
<option value="ur">Urdu</option>
|
||||
<option value="vi">Vietnamese</option>
|
||||
<option value="zh-Hans">Chinese (Simplified)</option>
|
||||
<option value="zh-Hant">Chinese (Traditional)</option>
|
||||
</select>
|
||||
<select name="target_lang" required>
|
||||
<option value="">Select target language</option>
|
||||
<option value="af">Afrikaans</option>
|
||||
<option value="ar">Arabic</option>
|
||||
<option value="bg">Bulgarian</option>
|
||||
<option value="bn">Bengali</option>
|
||||
<option value="bs">Bosnian</option>
|
||||
<option value="ca">Catalan</option>
|
||||
<option value="cs">Czech</option>
|
||||
<option value="cy">Welsh</option>
|
||||
<option value="da">Danish</option>
|
||||
<option value="de">German</option>
|
||||
<option value="el">Greek</option>
|
||||
<option value="en">English</option>
|
||||
<option value="en-GB">English (British)</option>
|
||||
<option value="en-US">English (American)</option>
|
||||
<option value="es">Spanish</option>
|
||||
<option value="et">Estonian</option>
|
||||
<option value="fa">Persian</option>
|
||||
<option value="fi">Finnish</option>
|
||||
<option value="fr">French</option>
|
||||
<option value="he">Hebrew</option>
|
||||
<option value="hi">Hindi</option>
|
||||
<option value="hr">Croatian</option>
|
||||
<option value="ht">Haitian Creole</option>
|
||||
<option value="hu">Hungarian</option>
|
||||
<option value="id">Indonesian</option>
|
||||
<option value="is">Icelandic</option>
|
||||
<option value="it">Italian</option>
|
||||
<option value="ja">Japanese</option>
|
||||
<option value="ko">Korean</option>
|
||||
<option value="lt">Lithuanian</option>
|
||||
<option value="lv">Latvian</option>
|
||||
<option value="ms">Malay</option>
|
||||
<option value="mt">Maltese</option>
|
||||
<option value="nl">Dutch</option>
|
||||
<option value="nb">Norwegian</option>
|
||||
<option value="pl">Polish</option>
|
||||
<option value="pt">Portuguese</option>
|
||||
<option value="pt-PT">Portuguese (Portugal)</option>
|
||||
<option value="pt-BR">Portuguese (Brazil)</option>
|
||||
<option value="ro">Romanian</option>
|
||||
<option value="ru">Russian</option>
|
||||
<option value="sk">Slovak</option>
|
||||
<option value="sl">Slovenian</option>
|
||||
<option value="sr">Serbian</option>
|
||||
<option value="sv">Swedish</option>
|
||||
<option value="sw">Swahili</option>
|
||||
<option value="th">Thai</option>
|
||||
<option value="tr">Turkish</option>
|
||||
<option value="uk">Ukrainian</option>
|
||||
<option value="ur">Urdu</option>
|
||||
<option value="vi">Vietnamese</option>
|
||||
<option value="zh-Hans">Chinese (Simplified)</option>
|
||||
<option value="zh-Hant">Chinese (Traditional)</option>
|
||||
</select>
|
||||
<select name="formality">
|
||||
<option value="default">Default formality</option>
|
||||
<option value="more">More formal</option>
|
||||
<option value="less">Less formal</option>
|
||||
</select>
|
||||
<button type="submit">Upload and Translate</button>
|
||||
</form>
|
||||
<div id="jobList">
|
||||
<h2>Current Jobs</h2>
|
||||
<button id="bulkDownload" style="display:none;">Bulk Download Selected</button>
|
||||
</div>
|
||||
<h3 id="apiStatus">Status: Using Microsoft Document Translation API with Azure Storage</h3>
|
||||
<div id="apiInfo">
|
||||
</li>Document Translation API limit: 20MB per document</li>
|
||||
|
||||
</li>Character limit: 40,000 characters per document</li>
|
||||
<p><strong>Current Configuration:</strong></p>
|
||||
<ul>
|
||||
<li>API: Microsoft Document Translation API</li>
|
||||
<li>Storage: Azure Blob Storage</li>
|
||||
<li>Region: East US</li>
|
||||
</ul>
|
||||
<p class="small">Note: Translation jobs are stored for 24 hours only.</p>
|
||||
</div>
|
||||
<div id="jobHistory">
|
||||
<h2>Job History</h2>
|
||||
|
||||
</div>
|
||||
<div id="response"></div>
|
||||
<script src="js/script.js"></script>
|
||||
<!-- auth 4 of 4 NOTE: ensure values for clientID, authority (URL with tenant ID) and redirectUri are correct below -->
|
||||
<script>
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: "9079054c-9620-4757-a256-23413042f1ef",
|
||||
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
|
||||
redirectUri: "https://ai-sandbox.oliver.solutions/format"
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: "sessionStorage",
|
||||
storeAuthStateInCookie: true,
|
||||
}
|
||||
};
|
||||
|
||||
const loginRequest = {
|
||||
scopes: ["user.read"]
|
||||
};
|
||||
|
||||
const myMSALObj = new msal.PublicClientApplication(msalConfig);
|
||||
|
||||
signIn();
|
||||
|
||||
function signIn() {
|
||||
myMSALObj.loginPopup(loginRequest)
|
||||
.then(loginResponse => {
|
||||
console.log("User logged in:", loginResponse.account.username);
|
||||
thisUser = loginResponse.account.username;
|
||||
sessionStorage.setItem('accessToken', loginResponse.accessToken);
|
||||
showProtectedContent(); // Show protected content after successful login
|
||||
onAuthenticated(); // Special for this app
|
||||
}).catch(error => {
|
||||
console.error("Error during login:", error);
|
||||
});
|
||||
}
|
||||
|
||||
function signOut() {
|
||||
// Clear the session storage and (does not) sign out from Microsoft Identity
|
||||
sessionStorage.removeItem('accessToken');
|
||||
//myMSALObj.logoutPopup();
|
||||
console.log("User logged out.");
|
||||
document.getElementById('protected-content').style.display = 'none'; // Hide protected content
|
||||
document.getElementById('logout-button').style.display = 'none'; // Hide logout button
|
||||
document.getElementById('login-button').style.display = 'flex'; // Show login button
|
||||
}
|
||||
|
||||
|
||||
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 = 'flex'; // Show logout button
|
||||
document.getElementById('login-button').style.display = 'none'; // Hide login button
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the user is already logged in when the page loads
|
||||
window.addEventListener('load', showProtectedContent);
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
BIN
js/.DS_Store
vendored
Normal file
BIN
js/.DS_Store
vendored
Normal file
Binary file not shown.
309
js/script.js
Normal file
309
js/script.js
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
const POLL_INTERVAL = 5000; // 5 seconds
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('DOM fully loaded and parsed');
|
||||
loadJobHistory();
|
||||
|
||||
document.getElementById('uploadForm').addEventListener('submit', handleFormSubmit);
|
||||
document.getElementById('bulkDownload').addEventListener('click', bulkDownload);
|
||||
|
||||
document.querySelector('input[type="file"]').addEventListener('change', updateSelectedFiles);
|
||||
|
||||
addSelectAllButton();
|
||||
|
||||
// Initialize dark mode
|
||||
initDarkMode();
|
||||
|
||||
console.log('Microsoft Translator Document Translation initialized');
|
||||
});
|
||||
|
||||
function handleFormSubmit(e) {
|
||||
e.preventDefault();
|
||||
console.log('Form submitted');
|
||||
const formData = new FormData(this);
|
||||
const files = formData.getAll('documents[]');
|
||||
const targetLang = formData.get('target_lang');
|
||||
|
||||
console.log(`Number of files selected: ${files.length}`);
|
||||
|
||||
files.forEach((file, index) => {
|
||||
console.log(`Processing file ${index + 1}: ${file.name}`);
|
||||
const individualFormData = new FormData();
|
||||
individualFormData.append('file', file);
|
||||
individualFormData.append('source_lang', formData.get('source_lang'));
|
||||
individualFormData.append('target_lang', targetLang);
|
||||
individualFormData.append('formality', formData.get('formality'));
|
||||
|
||||
uploadFile(individualFormData);
|
||||
});
|
||||
}
|
||||
|
||||
function uploadFile(formData) {
|
||||
console.log(`Uploading file: ${formData.get('file').name}`);
|
||||
|
||||
const endpoint = 'process.php';
|
||||
console.log(`Using endpoint: ${endpoint}`);
|
||||
|
||||
fetch(endpoint, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(`Response from ${endpoint}:`, data);
|
||||
if (data.document_id && data.document_key) {
|
||||
console.log(`Document ID received: ${data.document_id}, Document Key: ${data.document_key}`);
|
||||
addJobToList(data.document_id, formData.get('file').name);
|
||||
pollJobStatus(data.document_id, data.document_key);
|
||||
addToJobHistory(data.document_id, formData.get('file').name, data.document_key, formData.get('target_lang'));
|
||||
} else if (data.error) {
|
||||
console.error('Error:', data.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
});
|
||||
}
|
||||
|
||||
function addJobToList(documentId, fileName) {
|
||||
console.log(`Adding job to list: ${fileName} (ID: ${documentId})`);
|
||||
const jobList = document.getElementById('jobList');
|
||||
const jobItem = document.createElement('div');
|
||||
jobItem.id = `job-${documentId}`;
|
||||
jobItem.innerHTML = `
|
||||
<input type="checkbox" id="check-${documentId}">
|
||||
<label for="check-${documentId}">${fileName} - Document ID: ${documentId} - Status: Uploading</label>
|
||||
<button onclick="downloadJob('${documentId}')" style="display:none;">Download</button>
|
||||
`;
|
||||
jobList.appendChild(jobItem);
|
||||
|
||||
if (!document.getElementById('selectAll')) {
|
||||
addSelectAllButton();
|
||||
}
|
||||
document.getElementById('bulkDownload').style.display = 'block';
|
||||
}
|
||||
|
||||
function pollJobStatus(documentId, documentKey) {
|
||||
console.log(`Starting to poll status for document ID: ${documentId}`);
|
||||
|
||||
const statusEndpoint = 'status.php';
|
||||
console.log(`Using status endpoint: ${statusEndpoint}`);
|
||||
|
||||
const pollInterval = setInterval(() => {
|
||||
console.log(`Polling status for document ID: ${documentId}`);
|
||||
fetch(`${statusEndpoint}?document_id=${documentId}&document_key=${documentKey}`)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log(`Status response for document ID ${documentId}:`, data);
|
||||
updateJobStatus(documentId, data.status);
|
||||
if (data.status === 'done') {
|
||||
console.log(`Job completed for document ID: ${documentId}`);
|
||||
clearInterval(pollInterval);
|
||||
|
||||
// Update job history with completed status
|
||||
let history = JSON.parse(localStorage.getItem('jobHistory')) || [];
|
||||
const jobIndex = history.findIndex(job => job.documentId === documentId);
|
||||
if (jobIndex !== -1) {
|
||||
history[jobIndex].status = 'Completed';
|
||||
localStorage.setItem('jobHistory', JSON.stringify(history));
|
||||
updateJobHistoryDisplay();
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error polling status:', error);
|
||||
});
|
||||
}, POLL_INTERVAL);
|
||||
}
|
||||
|
||||
function updateJobStatus(documentId, status) {
|
||||
console.log(`Updating job status: ${documentId} - ${status}`);
|
||||
const jobItem = document.getElementById(`job-${documentId}`);
|
||||
if (jobItem) {
|
||||
const label = jobItem.querySelector('label');
|
||||
label.textContent = `${label.textContent.split(' - ')[0]} - Status: ${status}`;
|
||||
|
||||
if (status === 'done') {
|
||||
jobItem.querySelector('button').style.display = 'inline-block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function downloadJob(documentId) {
|
||||
console.log(`Initiating download for document ID: ${documentId}`);
|
||||
let history = JSON.parse(localStorage.getItem('jobHistory')) || [];
|
||||
const job = history.find(job => job.documentId === documentId);
|
||||
|
||||
if (job && job.documentKey && job.fileName && job.targetLang) {
|
||||
console.log(`Found job in history: ${JSON.stringify(job)}`);
|
||||
const downloadUrl = `download.php?document_id=${documentId}&document_key=${job.documentKey}&original_filename=${encodeURIComponent(job.fileName)}&target_lang=${job.targetLang}`;
|
||||
|
||||
// Create a hidden iframe to trigger the download and webhook
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.style.display = 'none';
|
||||
document.body.appendChild(iframe);
|
||||
iframe.src = downloadUrl;
|
||||
|
||||
// Remove the iframe after a short delay
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(iframe);
|
||||
}, 5000);
|
||||
} else {
|
||||
console.error(`Job not found in history or missing required information for document ID: ${documentId}`);
|
||||
alert('This job is no longer available for download or is missing required information.');
|
||||
updateJobHistoryDisplay();
|
||||
}
|
||||
}
|
||||
|
||||
function addToJobHistory(documentId, fileName, documentKey, targetLang) {
|
||||
console.log(`Adding job to history: ${documentId}, ${fileName}, ${documentKey}, ${targetLang}`);
|
||||
let history = JSON.parse(localStorage.getItem('jobHistory')) || [];
|
||||
history.push({ documentId, fileName, documentKey, targetLang, status: 'Uploading', timestamp: Date.now() });
|
||||
localStorage.setItem('jobHistory', JSON.stringify(history));
|
||||
updateJobHistoryDisplay();
|
||||
}
|
||||
|
||||
function loadJobHistory() {
|
||||
updateJobHistoryDisplay();
|
||||
}
|
||||
|
||||
function updateJobHistoryDisplay() {
|
||||
cleanupOldJobs();
|
||||
|
||||
const historyContainer = document.getElementById('jobHistory');
|
||||
const history = JSON.parse(localStorage.getItem('jobHistory')) || [];
|
||||
|
||||
let historyHTML = '<h2>Job History</h2>';
|
||||
history.forEach(job => {
|
||||
const date = new Date(job.timestamp);
|
||||
historyHTML += `
|
||||
<div>
|
||||
${job.fileName} - ${job.status} - ${date.toLocaleString()}
|
||||
${job.status === 'Completed' ?
|
||||
`<button onclick="downloadJob('${job.documentId}')">Download</button>` :
|
||||
''}
|
||||
</div>`;
|
||||
});
|
||||
|
||||
// Add clear history button
|
||||
if (history.length > 0) {
|
||||
historyHTML += '<button id="clearHistory">Clear History</button>';
|
||||
} else {
|
||||
historyHTML += '<p>No translation jobs yet.</p>';
|
||||
}
|
||||
|
||||
historyContainer.innerHTML = historyHTML;
|
||||
|
||||
if (history.length > 0) {
|
||||
document.getElementById('clearHistory').addEventListener('click', clearJobHistory);
|
||||
}
|
||||
}
|
||||
|
||||
function clearJobHistory() {
|
||||
localStorage.removeItem('jobHistory');
|
||||
updateJobHistoryDisplay();
|
||||
}
|
||||
|
||||
function bulkDownload() {
|
||||
const checkedJobs = document.querySelectorAll('#jobList input[type="checkbox"]:checked');
|
||||
checkedJobs.forEach(checkbox => {
|
||||
const documentId = checkbox.id.replace('check-', '');
|
||||
downloadJob(documentId);
|
||||
});
|
||||
}
|
||||
|
||||
function updateSelectedFiles() {
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const fileList = document.getElementById('selectedFiles');
|
||||
|
||||
if (!fileList) {
|
||||
const newFileList = document.createElement('div');
|
||||
newFileList.id = 'selectedFiles';
|
||||
fileInput.parentNode.insertBefore(newFileList, fileInput.nextSibling);
|
||||
}
|
||||
|
||||
const files = fileInput.files;
|
||||
let fileListHTML = '<strong>Selected Files:</strong><ul>';
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
fileListHTML += `<li>${files[i].name}</li>`;
|
||||
}
|
||||
fileListHTML += '</ul>';
|
||||
document.getElementById('selectedFiles').innerHTML = fileListHTML;
|
||||
}
|
||||
|
||||
function addSelectAllButton() {
|
||||
const jobList = document.getElementById('jobList');
|
||||
if (!document.getElementById('selectAll')) {
|
||||
const selectAllButton = document.createElement('button');
|
||||
selectAllButton.id = 'selectAll';
|
||||
selectAllButton.textContent = 'Select All';
|
||||
selectAllButton.onclick = toggleSelectAll;
|
||||
jobList.insertBefore(selectAllButton, jobList.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const checkboxes = document.querySelectorAll('#jobList input[type="checkbox"]');
|
||||
const selectAllButton = document.getElementById('selectAll');
|
||||
const allChecked = Array.from(checkboxes).every(cb => cb.checked);
|
||||
|
||||
checkboxes.forEach(cb => cb.checked = !allChecked);
|
||||
selectAllButton.textContent = allChecked ? 'Select All' : 'Deselect All';
|
||||
}
|
||||
|
||||
function cleanupOldJobs() {
|
||||
let history = JSON.parse(localStorage.getItem('jobHistory')) || [];
|
||||
const twoHoursAgo = Date.now() - (2 * 60 * 60 * 1000); // 2 hours in milliseconds
|
||||
|
||||
history = history.filter(job => job.timestamp > twoHoursAgo);
|
||||
|
||||
localStorage.setItem('jobHistory', JSON.stringify(history));
|
||||
}
|
||||
|
||||
// Dark mode functionality
|
||||
function initDarkMode() {
|
||||
// Add dark mode toggle button
|
||||
const darkModeToggle = document.createElement('button');
|
||||
darkModeToggle.id = 'darkModeToggle';
|
||||
darkModeToggle.className = 'dark-mode-toggle';
|
||||
darkModeToggle.title = 'Toggle Dark Mode';
|
||||
darkModeToggle.innerHTML = `
|
||||
<span id="lightModeIcon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M8 11a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 1a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<span id="darkModeIcon" style="display: none;">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 16 16">
|
||||
<path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z"/>
|
||||
</svg>
|
||||
</span>
|
||||
`;
|
||||
document.body.appendChild(darkModeToggle);
|
||||
|
||||
// Check for saved dark mode preference
|
||||
const darkModeEnabled = localStorage.getItem('darkMode') === 'enabled';
|
||||
|
||||
if (darkModeEnabled) {
|
||||
document.body.classList.add('dark-mode');
|
||||
document.getElementById('lightModeIcon').style.display = 'none';
|
||||
document.getElementById('darkModeIcon').style.display = 'block';
|
||||
}
|
||||
|
||||
// Toggle dark mode when button is clicked
|
||||
document.getElementById('darkModeToggle').addEventListener('click', function() {
|
||||
document.body.classList.toggle('dark-mode');
|
||||
|
||||
// Save preference and toggle icons
|
||||
if (document.body.classList.contains('dark-mode')) {
|
||||
localStorage.setItem('darkMode', 'enabled');
|
||||
document.getElementById('lightModeIcon').style.display = 'none';
|
||||
document.getElementById('darkModeIcon').style.display = 'block';
|
||||
} else {
|
||||
localStorage.setItem('darkMode', 'disabled');
|
||||
document.getElementById('lightModeIcon').style.display = 'block';
|
||||
document.getElementById('darkModeIcon').style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
67
list_container.php
Normal file
67
list_container.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
// Tool to list all files in a container
|
||||
include 'config.php';
|
||||
|
||||
// Set up the container query URL
|
||||
$accountName = 'opticaltranslations';
|
||||
$container = isset($_GET['container']) ? $_GET['container'] : 'translated-documents';
|
||||
$sasToken = AZURE_STORAGE_SAS_TOKEN;
|
||||
|
||||
// Build URL with SAS token
|
||||
$url = "https://$accountName.blob.core.windows.net/$container?restype=container&comp=list&$sasToken";
|
||||
|
||||
echo "<h1>Container Contents: $container</h1>";
|
||||
echo "<p>URL: $url</p>";
|
||||
|
||||
// Make the request
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode === 200) {
|
||||
// Parse XML response
|
||||
$xml = simplexml_load_string($response);
|
||||
if ($xml) {
|
||||
echo "<h2>Files:</h2>";
|
||||
echo "<table border='1'>";
|
||||
echo "<tr><th>Name</th><th>Size</th><th>Last Modified</th><th>URL</th></tr>";
|
||||
|
||||
if (isset($xml->Blobs->Blob)) {
|
||||
foreach ($xml->Blobs->Blob as $blob) {
|
||||
$name = (string)$blob->Name;
|
||||
$size = (string)$blob->Properties->{'Content-Length'};
|
||||
$lastModified = (string)$blob->Properties->{'Last-Modified'};
|
||||
$url = "https://$accountName.blob.core.windows.net/$container/$name?$sasToken";
|
||||
$downloadLink = htmlspecialchars($url);
|
||||
|
||||
echo "<tr>";
|
||||
echo "<td>$name</td>";
|
||||
echo "<td>$size bytes</td>";
|
||||
echo "<td>$lastModified</td>";
|
||||
echo "<td><a href='$downloadLink' target='_blank'>Download</a></td>";
|
||||
echo "</tr>";
|
||||
}
|
||||
} else {
|
||||
echo "<tr><td colspan='4'>No files found in container</td></tr>";
|
||||
}
|
||||
|
||||
echo "</table>";
|
||||
} else {
|
||||
echo "<p>Error parsing XML response</p>";
|
||||
echo "<pre>" . htmlspecialchars($response) . "</pre>";
|
||||
}
|
||||
} else {
|
||||
echo "<p>Error: HTTP code $httpCode</p>";
|
||||
echo "<pre>" . htmlspecialchars($response) . "</pre>";
|
||||
}
|
||||
|
||||
// Add links to switch containers
|
||||
echo "<p>";
|
||||
echo "<a href='?container=source-documents'>View Source Documents</a> | ";
|
||||
echo "<a href='?container=translated-documents'>View Translated Documents</a>";
|
||||
echo "</p>";
|
||||
?>
|
||||
136
local_download.php
Normal file
136
local_download.php
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
// Modified download.php to work with local storage approach
|
||||
include 'config.php';
|
||||
include 'logger.php';
|
||||
include 'storage_model.php';
|
||||
|
||||
logMessage("Received request to local_download.php");
|
||||
|
||||
if (isset($_GET['document_id']) && isset($_GET['document_key']) && isset($_GET['original_filename']) && isset($_GET['target_lang'])) {
|
||||
$document_id = $_GET['document_id'];
|
||||
$document_key = $_GET['document_key'];
|
||||
$original_filename = $_GET['original_filename'];
|
||||
$target_lang = $_GET['target_lang'];
|
||||
|
||||
logMessage("Attempting to download Document ID: $document_id, Original filename: $original_filename, Target language: $target_lang");
|
||||
|
||||
// Decode the document key to get blob information
|
||||
$documentKeyData = json_decode($document_key, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
logMessage("Invalid document_key format", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid document_key format']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Make sure we have the target blob name
|
||||
if (!isset($documentKeyData['target_blob'])) {
|
||||
logMessage("Missing target_blob in document_key", 'ERROR');
|
||||
echo json_encode(['error' => 'Missing target blob information']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$targetFileName = $documentKeyData['target_blob'];
|
||||
|
||||
// Set timeout for downloads
|
||||
set_time_limit(60);
|
||||
|
||||
// Initialize Storage Helper
|
||||
$storageHelper = new StorageHelper();
|
||||
|
||||
// First check if the translation is completed successfully
|
||||
$statusUrl = MS_API_ENDPOINT . "/translator/document/batches/$document_id?api-version=" . MS_API_VERSION;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $statusUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$translationSucceeded = false;
|
||||
|
||||
if ($httpCode === 200 || $response !== false) {
|
||||
$statusData = json_decode($response, true);
|
||||
|
||||
if (isset($statusData['status']) && strtolower($statusData['status']) === 'succeeded') {
|
||||
$translationSucceeded = true;
|
||||
logMessage("Translation has Succeeded status.");
|
||||
} else {
|
||||
logMessage("Translation status is not Succeeded: " . ($statusData['status'] ?? 'unknown') . ". Creating placeholder file.", 'WARNING');
|
||||
}
|
||||
} else {
|
||||
logMessage("Failed to check translation status. HTTP Code: $httpCode", 'ERROR');
|
||||
}
|
||||
|
||||
// Prepare file path
|
||||
$localTargetPath = dirname(__FILE__) . '/local_storage/translated-documents/' . $targetFileName;
|
||||
|
||||
// Create a placeholder file if real translation isn't available
|
||||
if (!$translationSucceeded && !file_exists($localTargetPath)) {
|
||||
$placeholderContent = "This is a placeholder for the translated content.\n\n";
|
||||
$placeholderContent .= "In a real implementation, the Microsoft Translator service would have translated your document.\n";
|
||||
$placeholderContent .= "Original Filename: $original_filename\n";
|
||||
$placeholderContent .= "Target Language: $target_lang\n";
|
||||
$placeholderContent .= "Document ID: $document_id\n\n";
|
||||
$placeholderContent .= "The Microsoft Translator API is reporting an access issue with Azure Blob Storage.\n";
|
||||
$placeholderContent .= "Please refer to the AZURE_SETUP_GUIDE.md file for instructions on fixing this issue.";
|
||||
|
||||
file_put_contents($localTargetPath, $placeholderContent);
|
||||
logMessage("Created placeholder file at: $localTargetPath");
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
$fileContent = file_get_contents($localTargetPath);
|
||||
|
||||
if ($fileContent === false) {
|
||||
logMessage("Failed to read translated file", 'ERROR');
|
||||
echo json_encode(['error' => 'Failed to read translated file']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Create new filename with language code prefix
|
||||
$fileInfo = pathinfo($original_filename);
|
||||
$newFilename = strtoupper($target_lang) . '_' . $fileInfo['filename'] . '.' . $fileInfo['extension'];
|
||||
|
||||
// Determine content type based on file extension
|
||||
$contentType = 'application/octet-stream'; // Default content type
|
||||
$extension = strtolower($fileInfo['extension']);
|
||||
|
||||
switch ($extension) {
|
||||
case 'pdf':
|
||||
$contentType = 'application/pdf';
|
||||
break;
|
||||
case 'docx':
|
||||
case 'doc':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
||||
break;
|
||||
case 'pptx':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
||||
break;
|
||||
case 'xlsx':
|
||||
$contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
break;
|
||||
case 'txt':
|
||||
$contentType = 'text/plain';
|
||||
break;
|
||||
case 'html':
|
||||
case 'htm':
|
||||
$contentType = 'text/html';
|
||||
break;
|
||||
}
|
||||
|
||||
// Send the file to the client
|
||||
header("Content-Type: $contentType");
|
||||
header("Content-Disposition: attachment; filename=\"$newFilename\"");
|
||||
echo $fileContent;
|
||||
logMessage("File download initiated: $newFilename");
|
||||
} else {
|
||||
logMessage("Missing required parameters", 'ERROR');
|
||||
echo json_encode(['error' => 'Missing required parameters (document_id, document_key, original_filename, or target_lang)']);
|
||||
}
|
||||
?>
|
||||
214
local_process.php
Normal file
214
local_process.php
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
<?php
|
||||
// This is a modified version of process.php that uses local storage rather than Azure Blob Storage
|
||||
|
||||
// Enable error reporting for debugging
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
include 'config.php';
|
||||
include 'logger.php';
|
||||
include 'storage_model.php';
|
||||
|
||||
// Create log file if it doesn't exist
|
||||
$logFile = 'app.log';
|
||||
if (!file_exists($logFile)) {
|
||||
file_put_contents($logFile, "Log started at " . date('Y-m-d H:i:s') . PHP_EOL);
|
||||
chmod($logFile, 0666);
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
logMessage("Received request to local_process.php");
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
logMessage("Received POST request");
|
||||
|
||||
if (!isset($_FILES['file'])) {
|
||||
logMessage("No file received", 'ERROR');
|
||||
echo json_encode(['error' => 'No file received']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
logMessage("File received: " . $file['name']);
|
||||
|
||||
// Check for upload errors
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
$uploadErrors = [
|
||||
1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
|
||||
2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
|
||||
3 => 'The uploaded file was only partially uploaded',
|
||||
4 => 'No file was uploaded',
|
||||
6 => 'Missing a temporary folder',
|
||||
7 => 'Failed to write file to disk',
|
||||
8 => 'A PHP extension stopped the file upload'
|
||||
];
|
||||
$errorMessage = isset($uploadErrors[$file['error']]) ? $uploadErrors[$file['error']] : 'Unknown upload error';
|
||||
logMessage("File upload error: " . $errorMessage, 'ERROR');
|
||||
echo json_encode(['error' => 'File upload error: ' . $errorMessage]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Verify file
|
||||
if (!file_exists($file['tmp_name']) || !is_readable($file['tmp_name'])) {
|
||||
logMessage("Temporary file does not exist or is not readable: " . $file['tmp_name'], 'ERROR');
|
||||
echo json_encode(['error' => 'Temporary file does not exist or is not readable']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Get translation parameters
|
||||
$source_lang = $_POST['source_lang'] ?? '';
|
||||
$target_lang = $_POST['target_lang'] ?? '';
|
||||
$formality = $_POST['formality'] ?? 'default';
|
||||
|
||||
logMessage("Source Language: $source_lang");
|
||||
logMessage("Target Language: $target_lang");
|
||||
logMessage("Formality: $formality");
|
||||
|
||||
// Initialize Storage Helper
|
||||
logMessage("Initializing Storage Helper");
|
||||
$storageHelper = new StorageHelper();
|
||||
|
||||
// Upload file to local storage
|
||||
logMessage("Uploading file to local storage: " . $file['name']);
|
||||
$sourceFile = $storageHelper->uploadSourceFile($file['tmp_name'], $file['name']);
|
||||
if (!$sourceFile) {
|
||||
logMessage("Failed to save file to local storage", 'ERROR');
|
||||
echo json_encode(['error' => 'Failed to save file to local storage']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set up target info for the translated file
|
||||
logMessage("Setting up target info for translated file");
|
||||
$targetInfo = $storageHelper->getTargetInfo($file['name'], $target_lang);
|
||||
if (!$targetInfo) {
|
||||
logMessage("Failed to set up target file info", 'ERROR');
|
||||
echo json_encode(['error' => 'Failed to set up target file info']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prepare the request to Microsoft Document Translation API
|
||||
$batchTranslationUrl = MS_API_ENDPOINT . 'translator/document/batches?api-version=' . MS_API_VERSION;
|
||||
logMessage("Prepared translation API URL: $batchTranslationUrl");
|
||||
|
||||
// Format the source and target language codes correctly
|
||||
$formattedSourceLang = $source_lang !== 'AUTO' ? strtolower($source_lang) : '';
|
||||
$formattedTargetLang = strtolower($target_lang);
|
||||
|
||||
// Handle special cases for Microsoft's language codes
|
||||
if ($formattedTargetLang === 'en-gb') {
|
||||
$formattedTargetLang = 'en-GB';
|
||||
} else if ($formattedTargetLang === 'en-us') {
|
||||
$formattedTargetLang = 'en-US';
|
||||
} else if ($formattedTargetLang === 'pt-pt') {
|
||||
$formattedTargetLang = 'pt-PT';
|
||||
} else if ($formattedTargetLang === 'pt-br') {
|
||||
$formattedTargetLang = 'pt-BR';
|
||||
}
|
||||
|
||||
// Generate URLs for Microsoft Translator API
|
||||
$sourceUrl = $sourceFile['file_url']; // Direct file URL
|
||||
$targetUrl = $targetInfo['blob_url']; // Directory URL for target
|
||||
|
||||
logMessage("Source URL: $sourceUrl");
|
||||
logMessage("Target URL: $targetUrl");
|
||||
|
||||
// Create the request body for Document Translation API
|
||||
$requestBody = [
|
||||
'inputs' => [
|
||||
[
|
||||
'source' => [
|
||||
'sourceUrl' => $sourceUrl,
|
||||
'storageSource' => 'AzureBlob', // We say AzureBlob but using local URLs
|
||||
'language' => $formattedSourceLang ?: 'en'
|
||||
],
|
||||
'targets' => [
|
||||
[
|
||||
'targetUrl' => $targetUrl,
|
||||
'storageSource' => 'AzureBlob',
|
||||
'language' => $formattedTargetLang,
|
||||
'category' => 'general'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
// Set formality if supported
|
||||
if ($formality === 'more' || $formality === 'less') {
|
||||
$requestBody['inputs'][0]['targets'][0]['formality'] = ($formality === 'more') ? 'Formal' : 'Informal';
|
||||
}
|
||||
|
||||
logMessage("Prepared request body: " . json_encode($requestBody, JSON_PRETTY_PRINT));
|
||||
|
||||
// Make the API call
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $batchTranslationUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
|
||||
$jsonBody = json_encode($requestBody);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody);
|
||||
|
||||
logMessage("Sending request to Microsoft Translator API");
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
logMessage("CURL Info: " . json_encode(curl_getinfo($ch)));
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
logMessage("cURL Error: " . $error, 'ERROR');
|
||||
echo json_encode(['error' => 'cURL error: ' . $error]);
|
||||
} else {
|
||||
logMessage("API Response Code: $httpCode");
|
||||
logMessage("API Response: $response");
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
logMessage("HTTP Error Response: $httpCode - $response", 'ERROR');
|
||||
echo json_encode(['error' => "HTTP Error: $httpCode", 'details' => $response]);
|
||||
} else {
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
// Store information for later use
|
||||
if (isset($result['id'])) {
|
||||
// Create a custom response that matches what our frontend expects
|
||||
$translationResponse = [
|
||||
'document_id' => $result['id'], // Microsoft's batch ID
|
||||
'document_key' => json_encode([
|
||||
'source_blob' => $sourceFile['blob_name'],
|
||||
'target_blob' => $targetInfo['blob_name'],
|
||||
'ms_response' => $result
|
||||
])
|
||||
];
|
||||
echo json_encode($translationResponse);
|
||||
logMessage("Translation job submitted successfully with ID: " . $result['id']);
|
||||
} else {
|
||||
logMessage("Microsoft Translator API error: " . json_encode($result), 'ERROR');
|
||||
echo json_encode(['error' => 'Microsoft Translator API response missing ID', 'details' => $result]);
|
||||
}
|
||||
} else {
|
||||
logMessage("Invalid JSON response: " . json_last_error_msg(), 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid JSON response: ' . json_last_error_msg(), 'raw_response' => $response]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
logMessage("Exception: " . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 'ERROR');
|
||||
echo json_encode(['error' => 'Server error: ' . $e->getMessage()]);
|
||||
}
|
||||
} else {
|
||||
logMessage("Received non-POST request", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid request method']);
|
||||
}
|
||||
?>
|
||||
148
local_status.php
Normal file
148
local_status.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
// Modified status.php to work with local storage approach
|
||||
include 'config.php';
|
||||
include 'logger.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
logMessage("Received request to local_status.php");
|
||||
|
||||
if (isset($_GET['document_id']) && isset($_GET['document_key'])) {
|
||||
$document_id = $_GET['document_id'];
|
||||
$document_key = $_GET['document_key'];
|
||||
|
||||
logMessage("Checking status for Document ID: $document_id");
|
||||
|
||||
// Decode the document key to get details
|
||||
$documentKeyData = json_decode($document_key, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
logMessage("Invalid document_key format", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid document_key format']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Add polling timeout for performance
|
||||
set_time_limit(30);
|
||||
|
||||
// Check batch translation status in Microsoft Translator API
|
||||
$statusUrl = MS_API_ENDPOINT . "/translator/document/batches/$document_id?api-version=" . MS_API_VERSION;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $statusUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
logMessage("cURL Error: " . $error, 'ERROR');
|
||||
echo json_encode(['error' => 'cURL error: ' . $error]);
|
||||
} else {
|
||||
logMessage("API Response Code: $httpCode");
|
||||
logMessage("API Response: $response");
|
||||
|
||||
$msResponse = json_decode($response, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
// Convert Microsoft's status format to match what our frontend expects
|
||||
$status = 'in_progress';
|
||||
$errorMessage = '';
|
||||
|
||||
// Check overall status
|
||||
if (isset($msResponse['status'])) {
|
||||
$msStatus = strtolower($msResponse['status']);
|
||||
|
||||
if ($msStatus === 'succeeded' || $msStatus === 'validated') {
|
||||
$status = 'done';
|
||||
|
||||
// If succeeded, copy the file from Microsoft's expected location to our local storage
|
||||
// This would normally happen via Azure Blob Storage notification
|
||||
// For now, we'll just assume success and show a placeholder message
|
||||
if ($msStatus === 'succeeded') {
|
||||
logMessage("Translation succeeded - would download translated file here");
|
||||
|
||||
// In a real implementation, check if the translated file exists
|
||||
// and if not, write a placeholder file
|
||||
if (isset($documentKeyData['target_blob'])) {
|
||||
$targetFileName = $documentKeyData['target_blob'];
|
||||
$targetPath = dirname(__FILE__) . '/local_storage/translated-documents/' . $targetFileName;
|
||||
|
||||
// Create a placeholder file if it doesn't exist
|
||||
if (!file_exists($targetPath)) {
|
||||
$placeholderContent = "This is a placeholder for the translated content.\n";
|
||||
$placeholderContent .= "In a real implementation, the Microsoft Translator service would store the translated file at this location.\n";
|
||||
$placeholderContent .= "Status: Translation succeeded.";
|
||||
|
||||
file_put_contents($targetPath, $placeholderContent);
|
||||
logMessage("Created placeholder translated file at: $targetPath");
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($msStatus === 'failed' || $msStatus === 'cancelled') {
|
||||
$status = 'error';
|
||||
|
||||
// Extract error details for better debugging
|
||||
if (isset($msResponse['error'])) {
|
||||
$errorMessage = $msResponse['error']['code'] . ': ' . $msResponse['error']['message'];
|
||||
|
||||
// Log error details
|
||||
logMessage("Translation error: " . $errorMessage, 'ERROR');
|
||||
|
||||
// If there's an inner error with more details, include it
|
||||
if (isset($msResponse['error']['innerError'])) {
|
||||
$innerError = $msResponse['error']['innerError']['code'] . ': ' . $msResponse['error']['innerError']['message'];
|
||||
$errorMessage .= " (" . $innerError . ")";
|
||||
logMessage("Inner error: " . $innerError, 'ERROR');
|
||||
}
|
||||
}
|
||||
} elseif ($msStatus === 'validationfailed') {
|
||||
$status = 'error';
|
||||
$errorMessage = 'Validation failed: ';
|
||||
|
||||
// Extract error details
|
||||
if (isset($msResponse['error'])) {
|
||||
$errorMessage .= $msResponse['error']['code'] . ': ' . $msResponse['error']['message'];
|
||||
|
||||
// Log validation error
|
||||
logMessage("Validation error: " . $errorMessage, 'ERROR');
|
||||
|
||||
// Include inner error if available
|
||||
if (isset($msResponse['error']['innerError'])) {
|
||||
$innerError = $msResponse['error']['innerError']['code'] . ': ' . $msResponse['error']['innerError']['message'];
|
||||
$errorMessage .= " (" . $innerError . ")";
|
||||
logMessage("Inner validation error: " . $innerError, 'ERROR');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a response that matches what the frontend expects
|
||||
$statusResponse = [
|
||||
'document_id' => $document_id,
|
||||
'status' => $status,
|
||||
'seconds_remaining' => isset($msResponse['summary']['estimatedTranslationTimeInSeconds'])
|
||||
? $msResponse['summary']['estimatedTranslationTimeInSeconds']
|
||||
: 0,
|
||||
'ms_status' => isset($msResponse['status']) ? $msResponse['status'] : 'unknown',
|
||||
'error_message' => $errorMessage
|
||||
];
|
||||
|
||||
// Include the original Microsoft response for debugging
|
||||
$statusResponse['ms_response'] = $msResponse;
|
||||
|
||||
echo json_encode($statusResponse);
|
||||
} else {
|
||||
logMessage("Invalid JSON response", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid JSON response', 'raw_response' => $response]);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logMessage("Missing document_id or document_key", 'ERROR');
|
||||
echo json_encode(['error' => 'Missing document_id or document_key']);
|
||||
}
|
||||
?>
|
||||
BIN
local_storage/.DS_Store
vendored
Executable file
BIN
local_storage/.DS_Store
vendored
Executable file
Binary file not shown.
BIN
local_storage/source-documents/.DS_Store
vendored
Executable file
BIN
local_storage/source-documents/.DS_Store
vendored
Executable file
Binary file not shown.
134539
local_storage/source-documents/69a56544c754a-Petroni_BajadaSocialFY2026.pdf
Normal file
134539
local_storage/source-documents/69a56544c754a-Petroni_BajadaSocialFY2026.pdf
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
BIN
local_storage/translated-documents/.DS_Store
vendored
Executable file
BIN
local_storage/translated-documents/.DS_Store
vendored
Executable file
Binary file not shown.
63
logger.php
Normal file
63
logger.php
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
function logMessage($message, $type = 'INFO') {
|
||||
// Enable logging
|
||||
$logFile = 'app.log';
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$logEntry = "[$timestamp] [$type] $message" . PHP_EOL;
|
||||
|
||||
file_put_contents($logFile, $logEntry, FILE_APPEND);
|
||||
|
||||
// Create a specific debug log for download issues
|
||||
if (strpos($message, 'DIRECT DOWNLOAD') !== false ||
|
||||
strpos($message, 'PRIORITY') !== false ||
|
||||
strpos($message, 'LAST RESORT') !== false ||
|
||||
strpos($message, 'downloadTranslatedFile') !== false) {
|
||||
$debugLogFile = 'download_debug.log';
|
||||
file_put_contents($debugLogFile, $logEntry, FILE_APPEND);
|
||||
}
|
||||
|
||||
// Also output to console if it's a CLI request
|
||||
if (php_sapi_name() === 'cli') {
|
||||
echo $logEntry;
|
||||
}
|
||||
}
|
||||
|
||||
function logApiCall($url, $method, $headers, $body, $response, $httpCode) {
|
||||
// Sanitize headers to remove sensitive information
|
||||
$sanitizedHeaders = [];
|
||||
foreach ($headers as $header) {
|
||||
// Remove API keys from log
|
||||
if (strpos($header, 'Subscription-Key') !== false || strpos($header, 'Authorization') !== false) {
|
||||
$sanitizedHeaders[] = preg_replace('/(Key|Auth-Key|Authorization):\s*[^\s]+/', '$1: [REDACTED]', $header);
|
||||
} else {
|
||||
$sanitizedHeaders[] = $header;
|
||||
}
|
||||
}
|
||||
|
||||
// For SAS tokens and connection strings in the body
|
||||
$sanitizedBody = $body;
|
||||
if (is_array($sanitizedBody)) {
|
||||
$json = json_encode($sanitizedBody);
|
||||
// Remove SAS tokens from logs
|
||||
if (strpos($json, 'sv=') !== false && strpos($json, 'sig=') !== false) {
|
||||
$json = preg_replace('/(\?|&)(sv|sig|se|sp|st|spr)=[^&]+/', '$1$2=[REDACTED]', $json);
|
||||
$sanitizedBody = json_decode($json, true) ?: $sanitizedBody;
|
||||
}
|
||||
}
|
||||
|
||||
// Truncate response if it's too large
|
||||
$sanitizedResponse = $response;
|
||||
if (strlen($response) > 1000) {
|
||||
$sanitizedResponse = substr($response, 0, 1000) . '... [truncated]';
|
||||
}
|
||||
|
||||
$logMessage = "API Call:" . PHP_EOL;
|
||||
$logMessage .= "URL: $url" . PHP_EOL;
|
||||
$logMessage .= "Method: $method" . PHP_EOL;
|
||||
$logMessage .= "Headers: " . json_encode($sanitizedHeaders) . PHP_EOL;
|
||||
$logMessage .= "Body: " . json_encode($sanitizedBody) . PHP_EOL;
|
||||
$logMessage .= "Response Code: $httpCode" . PHP_EOL;
|
||||
$logMessage .= "Response: $sanitizedResponse" . PHP_EOL;
|
||||
|
||||
logMessage($logMessage, 'API');
|
||||
}
|
||||
265
process.php
Normal file
265
process.php
Normal file
|
|
@ -0,0 +1,265 @@
|
|||
<?php
|
||||
// Enable error reporting for debugging
|
||||
ini_set('display_errors', 1);
|
||||
ini_set('display_startup_errors', 1);
|
||||
error_reporting(E_ALL);
|
||||
|
||||
include 'config.php';
|
||||
include 'logger.php';
|
||||
include 'azure_storage.php';
|
||||
|
||||
|
||||
// Create log file if it doesn't exist
|
||||
$logFile = 'app.log';
|
||||
if (!file_exists($logFile)) {
|
||||
file_put_contents($logFile, "Log started at " . date('Y-m-d H:i:s') . PHP_EOL);
|
||||
chmod($logFile, 0666); // Make writable by web server
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
logMessage("Received request to process.php");
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
logMessage("Received POST request");
|
||||
|
||||
if (!isset($_FILES['file'])) {
|
||||
logMessage("No file received", 'ERROR');
|
||||
echo json_encode(['error' => 'No file received']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$file = $_FILES['file'];
|
||||
logMessage("File received: " . $file['name']);
|
||||
|
||||
// Check for upload errors
|
||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||
$uploadErrors = [
|
||||
1 => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
|
||||
2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form',
|
||||
3 => 'The uploaded file was only partially uploaded',
|
||||
4 => 'No file was uploaded',
|
||||
6 => 'Missing a temporary folder',
|
||||
7 => 'Failed to write file to disk',
|
||||
8 => 'A PHP extension stopped the file upload'
|
||||
];
|
||||
$errorMessage = isset($uploadErrors[$file['error']]) ? $uploadErrors[$file['error']] : 'Unknown upload error';
|
||||
logMessage("File upload error: " . $errorMessage, 'ERROR');
|
||||
echo json_encode(['error' => 'File upload error: ' . $errorMessage]);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Check if the file exists and is readable
|
||||
if (!file_exists($file['tmp_name']) || !is_readable($file['tmp_name'])) {
|
||||
logMessage("Temporary file does not exist or is not readable: " . $file['tmp_name'], 'ERROR');
|
||||
echo json_encode(['error' => 'Temporary file does not exist or is not readable']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$source_lang = $_POST['source_lang'] ?? '';
|
||||
$target_lang = $_POST['target_lang'] ?? '';
|
||||
$formality = $_POST['formality'] ?? 'default';
|
||||
|
||||
logMessage("Source Language: $source_lang");
|
||||
logMessage("Target Language: $target_lang");
|
||||
logMessage("Formality: $formality");
|
||||
|
||||
// Initialize Azure Storage Helper
|
||||
logMessage("Initializing Azure Storage Helper");
|
||||
$azureStorage = new AzureStorageHelper();
|
||||
|
||||
// Ensure containers exist
|
||||
logMessage("Ensuring Azure Storage containers exist");
|
||||
if (!$azureStorage->ensureContainersExist()) {
|
||||
logMessage("Failed to verify Azure Storage containers", 'ERROR');
|
||||
echo json_encode(['error' => 'Failed to verify Azure Storage containers']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Upload file to Azure Blob Storage
|
||||
logMessage("Uploading file to Azure Blob Storage: " . $file['name']);
|
||||
$sourceBlob = $azureStorage->uploadSourceFile($file['tmp_name'], $file['name']);
|
||||
if (!$sourceBlob) {
|
||||
logMessage("Failed to upload file to Azure Storage", 'ERROR');
|
||||
echo json_encode(['error' => 'Failed to upload file to Azure Storage']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Set up target blob for the translated file
|
||||
logMessage("Setting up target blob for translated file");
|
||||
$targetBlob = $azureStorage->getTargetBlob($file['name'], $target_lang);
|
||||
if (!$targetBlob) {
|
||||
logMessage("Failed to set up target blob", 'ERROR');
|
||||
echo json_encode(['error' => 'Failed to set up target blob']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Prepare the request to Microsoft Document Translation API
|
||||
$batchTranslationUrl = MS_API_ENDPOINT . '/translator/document/batches?api-version=' . MS_API_VERSION;
|
||||
logMessage("Prepared translation API URL: $batchTranslationUrl");
|
||||
|
||||
// Format the source and target language codes correctly
|
||||
$formattedSourceLang = $source_lang !== 'AUTO' ? strtolower($source_lang) : '';
|
||||
$formattedTargetLang = strtolower($target_lang);
|
||||
|
||||
// Handle special cases for Microsoft's language codes
|
||||
if ($formattedTargetLang === 'en-gb') {
|
||||
$formattedTargetLang = 'en-GB';
|
||||
} else if ($formattedTargetLang === 'en-us') {
|
||||
$formattedTargetLang = 'en-US';
|
||||
} else if ($formattedTargetLang === 'pt-pt') {
|
||||
$formattedTargetLang = 'pt-PT';
|
||||
} else if ($formattedTargetLang === 'pt-br') {
|
||||
$formattedTargetLang = 'pt-BR';
|
||||
}
|
||||
|
||||
// For Microsoft Document Translation, we need container-level URLs
|
||||
// The sourceUrl should be the container URL with SAS token
|
||||
// Extract account name from connection string
|
||||
preg_match('/AccountName=([^;]+)/', AZURE_STORAGE_CONNECTION_STRING, $nameMatches);
|
||||
$accountName = !empty($nameMatches[1]) ? $nameMatches[1] : 'opticaltranslations';
|
||||
|
||||
// Use the container URLs directly with SAS tokens
|
||||
$sasToken = defined('AZURE_STORAGE_SAS_TOKEN') ? AZURE_STORAGE_SAS_TOKEN : $sourceBlob['sas_token'];
|
||||
$sourceUrl = $sourceBlob['blob_url'] . '?' . $sasToken;
|
||||
$targetUrl = $targetBlob['blob_url'] . '?' . $sasToken;
|
||||
|
||||
logMessage("Source Container URL with SAS (partial): " . substr($sourceUrl, 0, 100) . "...");
|
||||
logMessage("Target Container URL with SAS (partial): " . substr($targetUrl, 0, 100) . "...");
|
||||
|
||||
// Let's try a different approach with direct blob URLs
|
||||
// Extract account name from connection string
|
||||
preg_match('/AccountName=([^;]+)/', AZURE_STORAGE_CONNECTION_STRING, $nameMatches);
|
||||
$accountName = !empty($nameMatches[1]) ? $nameMatches[1] : 'opticaltranslations';
|
||||
$sasToken = defined('AZURE_STORAGE_SAS_TOKEN') ? AZURE_STORAGE_SAS_TOKEN : '';
|
||||
|
||||
// Format the blob name to be URL-safe (replace spaces with %20)
|
||||
$safeBlobName = str_replace(' ', '%20', $sourceBlob['blob_name']);
|
||||
|
||||
// Direct blob URLs with SAS token - make sure the URL is properly formed
|
||||
$sourceDirectUrl = "https://$accountName.blob.core.windows.net/".AZURE_STORAGE_CONTAINER_SOURCE."/$safeBlobName?$sasToken";
|
||||
|
||||
// Target folder URL - make sure to include the container name
|
||||
$targetDirectUrl = "https://$accountName.blob.core.windows.net/".AZURE_STORAGE_CONTAINER_TARGET."?$sasToken";
|
||||
|
||||
// Use container-level URLs with filter parameter, similar to the test script
|
||||
// Instead of pointing directly to the blob, point to the container and specify the blob with filter
|
||||
$sourceContainerUrl = "https://$accountName.blob.core.windows.net/".AZURE_STORAGE_CONTAINER_SOURCE."?$sasToken";
|
||||
$targetContainerUrl = "https://$accountName.blob.core.windows.net/".AZURE_STORAGE_CONTAINER_TARGET."?$sasToken";
|
||||
|
||||
// Modified request body for Document Translation API
|
||||
$requestBody = [
|
||||
'inputs' => [
|
||||
[
|
||||
'source' => [
|
||||
'sourceUrl' => $sourceContainerUrl,
|
||||
'storageSource' => 'AzureBlob',
|
||||
'language' => $formattedSourceLang ?: 'en', // Default to English if auto-detect
|
||||
'filter' => [
|
||||
'prefix' => $sourceBlob['blob_name'],
|
||||
'includeSubfolders' => false
|
||||
]
|
||||
],
|
||||
'targets' => [
|
||||
[
|
||||
'targetUrl' => $targetContainerUrl,
|
||||
'storageSource' => 'AzureBlob',
|
||||
'language' => $formattedTargetLang,
|
||||
'category' => 'general'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
// Log the full request body for debugging
|
||||
logMessage("Full request body: " . json_encode($requestBody, JSON_PRETTY_PRINT));
|
||||
|
||||
// Add source language if specified (not AUTO)
|
||||
if (!empty($formattedSourceLang)) {
|
||||
$requestBody['inputs'][0]['source']['language'] = $formattedSourceLang;
|
||||
}
|
||||
|
||||
// Set formality if supported by Microsoft Translator
|
||||
if ($formality === 'more' || $formality === 'less') {
|
||||
$requestBody['inputs'][0]['targets'][0]['formality'] = ($formality === 'more') ? 'Formal' : 'Informal';
|
||||
}
|
||||
|
||||
logMessage("Prepared request body: " . json_encode($requestBody));
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $batchTranslationUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION,
|
||||
'Content-Type: application/json'
|
||||
]);
|
||||
|
||||
$jsonBody = json_encode($requestBody);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonBody);
|
||||
|
||||
logMessage("Sending request to Microsoft Translator API");
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
|
||||
logMessage("CURL Info: " . json_encode(curl_getinfo($ch)));
|
||||
|
||||
logApiCall($batchTranslationUrl, 'POST', [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION,
|
||||
'Content-Type: application/json'
|
||||
], $requestBody, $response, $httpCode);
|
||||
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
logMessage("cURL Error: " . $error, 'ERROR');
|
||||
echo json_encode(['error' => 'cURL error: ' . $error]);
|
||||
} else {
|
||||
logMessage("API Response Code: $httpCode");
|
||||
logMessage("API Response: $response");
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
logMessage("HTTP Error Response: $httpCode - $response", 'ERROR');
|
||||
echo json_encode(['error' => "HTTP Error: $httpCode", 'details' => $response]);
|
||||
} else {
|
||||
$result = json_decode($response, true);
|
||||
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
// Store the blob information for later use
|
||||
if (isset($result['id'])) {
|
||||
// Create a custom response that matches what our frontend expects
|
||||
$translationResponse = [
|
||||
'document_id' => $result['id'], // Microsoft's batch ID
|
||||
'document_key' => json_encode([
|
||||
'source_blob' => $sourceBlob['blob_name'],
|
||||
'target_blob' => $targetBlob['blob_name'],
|
||||
'ms_response' => $result
|
||||
])
|
||||
];
|
||||
echo json_encode($translationResponse);
|
||||
logMessage("Translation job submitted successfully with ID: " . $result['id']);
|
||||
} else {
|
||||
logMessage("Microsoft Translator API error: " . json_encode($result), 'ERROR');
|
||||
echo json_encode(['error' => 'Microsoft Translator API response missing ID', 'details' => $result]);
|
||||
}
|
||||
} else {
|
||||
logMessage("Invalid JSON response: " . json_last_error_msg(), 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid JSON response: ' . json_last_error_msg(), 'raw_response' => $response]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
|
||||
} catch (Exception $e) {
|
||||
logMessage("Exception: " . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 'ERROR');
|
||||
echo json_encode(['error' => 'Server error: ' . $e->getMessage()]);
|
||||
}
|
||||
} else {
|
||||
logMessage("Received non-POST request", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid request method']);
|
||||
}
|
||||
62
reset.php
Normal file
62
reset.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
<?php
|
||||
// Reset script for debugging
|
||||
// This will clear the log file and any local storage
|
||||
|
||||
include 'logger.php';
|
||||
|
||||
// Clear log file
|
||||
file_put_contents('app.log', "Log reset at " . date('Y-m-d H:i:s') . PHP_EOL);
|
||||
chmod('app.log', 0666); // Make writable by web server
|
||||
|
||||
// Log the reset
|
||||
logMessage("Application reset started");
|
||||
|
||||
// Clear local storage
|
||||
$sourcePath = 'local_storage/source-documents';
|
||||
$targetPath = 'local_storage/translated-documents';
|
||||
|
||||
function clearDirectory($dir) {
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = array_diff(scandir($dir), ['.', '..']);
|
||||
|
||||
foreach ($files as $file) {
|
||||
$path = $dir . '/' . $file;
|
||||
if (is_dir($path)) {
|
||||
clearDirectory($path);
|
||||
rmdir($path);
|
||||
} else {
|
||||
unlink($path);
|
||||
logMessage("Removed file: $path");
|
||||
}
|
||||
}
|
||||
|
||||
logMessage("Cleared directory: $dir");
|
||||
}
|
||||
|
||||
// Ensure local storage directories exist
|
||||
if (!is_dir('local_storage')) {
|
||||
mkdir('local_storage', 0777, true);
|
||||
logMessage("Created local_storage directory");
|
||||
}
|
||||
|
||||
if (!is_dir($sourcePath)) {
|
||||
mkdir($sourcePath, 0777, true);
|
||||
logMessage("Created source documents directory");
|
||||
} else {
|
||||
clearDirectory($sourcePath);
|
||||
}
|
||||
|
||||
if (!is_dir($targetPath)) {
|
||||
mkdir($targetPath, 0777, true);
|
||||
logMessage("Created translated documents directory");
|
||||
} else {
|
||||
clearDirectory($targetPath);
|
||||
}
|
||||
|
||||
logMessage("Application reset completed");
|
||||
|
||||
echo "Application reset complete. Log file and local storage have been cleared.";
|
||||
?>
|
||||
128
status.php
Normal file
128
status.php
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
include 'config.php';
|
||||
include 'logger.php';
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
logMessage("Received request to status.php");
|
||||
|
||||
if (isset($_GET['document_id']) && isset($_GET['document_key'])) {
|
||||
$document_id = $_GET['document_id'];
|
||||
$document_key = $_GET['document_key'];
|
||||
|
||||
logMessage("Checking status for Document ID: $document_id");
|
||||
|
||||
// Decode the document key to get Microsoft-specific details
|
||||
$documentKeyData = json_decode($document_key, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
logMessage("Invalid document_key format", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid document_key format']);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Add polling timeout for performance
|
||||
set_time_limit(30); // Set timeout to 30 seconds for this request
|
||||
|
||||
// Check batch translation status in Microsoft Translator API
|
||||
$statusUrl = MS_API_ENDPOINT . "/translator/document/batches/$document_id?api-version=" . MS_API_VERSION;
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $statusUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
|
||||
logApiCall($statusUrl, 'GET', [
|
||||
'Ocp-Apim-Subscription-Key: ' . MS_API_KEY,
|
||||
'Ocp-Apim-Subscription-Region: ' . MS_API_REGION
|
||||
], [], $response, $httpCode);
|
||||
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
logMessage("cURL Error: " . $error, 'ERROR');
|
||||
echo json_encode(['error' => 'cURL error: ' . $error]);
|
||||
} else {
|
||||
logMessage("API Response Code: $httpCode");
|
||||
logMessage("API Response: $response");
|
||||
|
||||
$msResponse = json_decode($response, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
// Convert Microsoft's status format to match what our frontend expects
|
||||
$status = 'in_progress';
|
||||
$errorMessage = '';
|
||||
|
||||
// Check overall status
|
||||
if (isset($msResponse['status'])) {
|
||||
$msStatus = strtolower($msResponse['status']);
|
||||
|
||||
if ($msStatus === 'succeeded' || $msStatus === 'validated') {
|
||||
$status = 'done';
|
||||
} elseif ($msStatus === 'failed' || $msStatus === 'cancelled') {
|
||||
$status = 'error';
|
||||
|
||||
// Extract error details for better debugging
|
||||
if (isset($msResponse['error'])) {
|
||||
$errorMessage = $msResponse['error']['code'] . ': ' . $msResponse['error']['message'];
|
||||
|
||||
// Log error details
|
||||
logMessage("Translation error: " . $errorMessage, 'ERROR');
|
||||
|
||||
// If there's an inner error with more details, include it
|
||||
if (isset($msResponse['error']['innerError'])) {
|
||||
$innerError = $msResponse['error']['innerError']['code'] . ': ' . $msResponse['error']['innerError']['message'];
|
||||
$errorMessage .= " (" . $innerError . ")";
|
||||
logMessage("Inner error: " . $innerError, 'ERROR');
|
||||
}
|
||||
}
|
||||
} elseif ($msStatus === 'validationfailed') {
|
||||
$status = 'error';
|
||||
$errorMessage = 'Validation failed: ';
|
||||
|
||||
// Extract error details
|
||||
if (isset($msResponse['error'])) {
|
||||
$errorMessage .= $msResponse['error']['code'] . ': ' . $msResponse['error']['message'];
|
||||
|
||||
// Log validation error
|
||||
logMessage("Validation error: " . $errorMessage, 'ERROR');
|
||||
|
||||
// Include inner error if available
|
||||
if (isset($msResponse['error']['innerError'])) {
|
||||
$innerError = $msResponse['error']['innerError']['code'] . ': ' . $msResponse['error']['innerError']['message'];
|
||||
$errorMessage .= " (" . $innerError . ")";
|
||||
logMessage("Inner validation error: " . $innerError, 'ERROR');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a response that matches what the frontend expects
|
||||
$statusResponse = [
|
||||
'document_id' => $document_id,
|
||||
'status' => $status,
|
||||
'seconds_remaining' => isset($msResponse['summary']['estimatedTranslationTimeInSeconds'])
|
||||
? $msResponse['summary']['estimatedTranslationTimeInSeconds']
|
||||
: 0,
|
||||
'ms_status' => isset($msResponse['status']) ? $msResponse['status'] : 'unknown',
|
||||
'error_message' => $errorMessage
|
||||
];
|
||||
|
||||
// Include the original Microsoft response for complete debugging
|
||||
$statusResponse['ms_response'] = $msResponse;
|
||||
|
||||
echo json_encode($statusResponse);
|
||||
} else {
|
||||
logMessage("Invalid JSON response", 'ERROR');
|
||||
echo json_encode(['error' => 'Invalid JSON response', 'raw_response' => $response]);
|
||||
}
|
||||
}
|
||||
|
||||
curl_close($ch);
|
||||
} else {
|
||||
logMessage("Missing document_id or document_key", 'ERROR');
|
||||
echo json_encode(['error' => 'Missing document_id or document_key']);
|
||||
}
|
||||
231
storage_model.php
Normal file
231
storage_model.php
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
<?php
|
||||
// This is a drop-in replacement for azure_storage.php that uses local storage only
|
||||
include_once 'config.php';
|
||||
include_once 'logger.php';
|
||||
|
||||
/**
|
||||
* Storage Helper Class for Document Translation
|
||||
*
|
||||
* This class provides helper functions to work with local file storage
|
||||
* as a replacement for Azure Blob Storage.
|
||||
*/
|
||||
class StorageHelper {
|
||||
private $sourceContainer;
|
||||
private $targetContainer;
|
||||
private $basePath;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct() {
|
||||
$this->sourceContainer = AZURE_STORAGE_CONTAINER_SOURCE;
|
||||
$this->targetContainer = AZURE_STORAGE_CONTAINER_TARGET;
|
||||
$this->basePath = dirname(__FILE__) . '/local_storage';
|
||||
|
||||
// Ensure containers exist
|
||||
$this->ensureContainersExist();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to the source container in local storage
|
||||
*
|
||||
* @param string $filePath Path to the temporary file
|
||||
* @param string $fileName Name of the file
|
||||
* @return array|bool Returns array with file path on success, false on failure
|
||||
*/
|
||||
public function uploadSourceFile($filePath, $fileName) {
|
||||
logMessage("Uploading file to local storage: $fileName from $filePath");
|
||||
|
||||
try {
|
||||
// Create a unique file name to avoid collisions
|
||||
$safeFileName = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $fileName);
|
||||
$uniqueId = uniqid();
|
||||
$storageFileName = $uniqueId . '-' . $safeFileName;
|
||||
|
||||
logMessage("Generated storage filename: $storageFileName");
|
||||
|
||||
// Verify file exists and is readable
|
||||
if (!file_exists($filePath)) {
|
||||
logMessage("Error: File does not exist at path: $filePath", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_readable($filePath)) {
|
||||
logMessage("Error: File is not readable at path: $filePath", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
|
||||
$fileSize = filesize($filePath);
|
||||
logMessage("File size: $fileSize bytes");
|
||||
|
||||
if ($fileSize <= 0) {
|
||||
logMessage("Error: File is empty or could not determine file size", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Save the file to local storage
|
||||
$localStorageDir = $this->basePath . '/' . $this->sourceContainer;
|
||||
if (!file_exists($localStorageDir)) {
|
||||
if (!mkdir($localStorageDir, 0777, true)) {
|
||||
logMessage("Failed to create local storage directory: $localStorageDir", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$localFilePath = $localStorageDir . '/' . $storageFileName;
|
||||
if (!copy($filePath, $localFilePath)) {
|
||||
logMessage("Failed to copy file to local storage: $localFilePath", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
|
||||
logMessage("File saved to local storage: $localFilePath");
|
||||
|
||||
// Generate URLs for Microsoft Translator API access
|
||||
// Note: Microsoft Translator must be able to reach these URLs
|
||||
$baseUrl = $this->getBaseUrl();
|
||||
$sourceUrl = $baseUrl . "/local_storage/{$this->sourceContainer}/$storageFileName";
|
||||
|
||||
logMessage("Generated source URL: $sourceUrl");
|
||||
|
||||
return [
|
||||
'blob_url' => $baseUrl . "/local_storage/{$this->sourceContainer}",
|
||||
'blob_name' => $storageFileName,
|
||||
'file_url' => $sourceUrl
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
logMessage("Error processing file: " . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 'ERROR');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target info for translation result
|
||||
*
|
||||
* @param string $fileName Original file name
|
||||
* @param string $targetLang Target language
|
||||
* @return array|bool Returns array with file info on success, false on failure
|
||||
*/
|
||||
public function getTargetInfo($fileName, $targetLang) {
|
||||
logMessage("Setting up target file for: $fileName, language: $targetLang");
|
||||
|
||||
try {
|
||||
// Create a unique file name for the translated file
|
||||
$safeFileName = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $fileName);
|
||||
$uniqueId = uniqid();
|
||||
$storageFileName = $uniqueId . '-' . strtoupper($targetLang) . '_' . $safeFileName;
|
||||
|
||||
logMessage("Generated target filename: $storageFileName");
|
||||
|
||||
// Ensure local storage directory exists
|
||||
$localStorageDir = $this->basePath . '/' . $this->targetContainer;
|
||||
if (!file_exists($localStorageDir)) {
|
||||
if (!mkdir($localStorageDir, 0777, true)) {
|
||||
logMessage("Failed to create local target directory: $localStorageDir", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
logMessage("Created local target directory: $localStorageDir");
|
||||
}
|
||||
|
||||
// Generate URLs for Microsoft Translator API access
|
||||
$baseUrl = $this->getBaseUrl();
|
||||
$targetUrl = $baseUrl . "/local_storage/{$this->targetContainer}";
|
||||
|
||||
logMessage("Target directory URL: $targetUrl");
|
||||
logMessage("Using target filename: $storageFileName");
|
||||
|
||||
return [
|
||||
'blob_url' => $targetUrl,
|
||||
'blob_name' => $storageFileName
|
||||
];
|
||||
} catch (Exception $e) {
|
||||
logMessage("Error setting up target file: " . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 'ERROR');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download a translated file from the target container
|
||||
*
|
||||
* @param string $fileName Name of the file
|
||||
* @return string|bool Returns file content on success, false on failure
|
||||
*/
|
||||
public function downloadTranslatedFile($fileName) {
|
||||
logMessage("Retrieving translated file: $fileName");
|
||||
|
||||
try {
|
||||
// Check if the translated file exists in local storage
|
||||
$localTargetPath = $this->basePath . '/' . $this->targetContainer . '/' . $fileName;
|
||||
|
||||
if (file_exists($localTargetPath)) {
|
||||
logMessage("Found local translated file at: $localTargetPath");
|
||||
$fileContent = file_get_contents($localTargetPath);
|
||||
|
||||
if ($fileContent === false) {
|
||||
logMessage("Error reading local translated file: $localTargetPath", 'ERROR');
|
||||
return false;
|
||||
} else {
|
||||
logMessage("Local translated file read successfully");
|
||||
return $fileContent;
|
||||
}
|
||||
} else {
|
||||
logMessage("Translated file not found: $localTargetPath", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
logMessage("Error in download process: " . $e->getMessage() . "\nStack trace: " . $e->getTraceAsString(), 'ERROR');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the container directories exist
|
||||
*
|
||||
* @return bool Returns true if containers exist or were created, false on failure
|
||||
*/
|
||||
public function ensureContainersExist() {
|
||||
logMessage("Ensuring storage directories exist: {$this->sourceContainer} and {$this->targetContainer}");
|
||||
|
||||
try {
|
||||
// Create source container directory if it doesn't exist
|
||||
$sourceDir = $this->basePath . '/' . $this->sourceContainer;
|
||||
if (!file_exists($sourceDir)) {
|
||||
if (!mkdir($sourceDir, 0777, true)) {
|
||||
logMessage("Failed to create source directory: $sourceDir", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
logMessage("Created source directory: $sourceDir");
|
||||
}
|
||||
|
||||
// Create target container directory if it doesn't exist
|
||||
$targetDir = $this->basePath . '/' . $this->targetContainer;
|
||||
if (!file_exists($targetDir)) {
|
||||
if (!mkdir($targetDir, 0777, true)) {
|
||||
logMessage("Failed to create target directory: $targetDir", 'ERROR');
|
||||
return false;
|
||||
}
|
||||
logMessage("Created target directory: $targetDir");
|
||||
}
|
||||
|
||||
logMessage("Storage directories verified successfully");
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
logMessage("Error creating storage directories: " . $e->getMessage(), 'ERROR');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for the application
|
||||
*
|
||||
* @return string The base URL
|
||||
*/
|
||||
private function getBaseUrl() {
|
||||
// Determine the base URL from the server variables
|
||||
$protocol = isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? 'https' : 'http';
|
||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
$uri = rtrim(dirname($_SERVER['REQUEST_URI'] ?? ''), '/');
|
||||
|
||||
return "$protocol://$host$uri";
|
||||
}
|
||||
}
|
||||
?>
|
||||
54
trns.svg
Normal file
54
trns.svg
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 400 250">
|
||||
<!-- Generator: Adobe Illustrator 29.2.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 116) -->
|
||||
<defs>
|
||||
<style>
|
||||
.st0, .st1 {
|
||||
font-family: Montserrat-Thin, Montserrat;
|
||||
font-size: 57.1px;
|
||||
font-weight: 200;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.st1, .st2, .st3 {
|
||||
stroke-miterlimit: 10;
|
||||
}
|
||||
|
||||
.st1, .st3 {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
}
|
||||
|
||||
.st2 {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
.st2, .st3 {
|
||||
stroke-width: 2px;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<rect class="st3" x="242.1" y="86.3" width="85.1" height="85.1"/>
|
||||
<g>
|
||||
<polyline class="st3" points="197.3 101.6 223.3 127.6 197.6 153.3"/>
|
||||
<line class="st3" x1="165.7" y1="127.8" x2="223.3" y2="127.6"/>
|
||||
</g>
|
||||
<rect class="st3" x="64.3" y="86.8" width="85.1" height="85.1"/>
|
||||
<text class="st0" transform="translate(86 147.1)"><tspan x="0" y="0">A</tspan></text>
|
||||
<text class="st1" transform="translate(86 147.1)"><tspan x="0" y="0">A</tspan></text>
|
||||
<g>
|
||||
<path d="M66.9,55v-14.7h5.8c1.6,0,2.9.3,4.1.9,1.2.6,2.1,1.5,2.7,2.6.7,1.1,1,2.4,1,3.8s-.3,2.7-1,3.8c-.7,1.1-1.6,2-2.7,2.6-1.2.6-2.5.9-4.1.9h-5.8ZM67.9,54h4.7c1.4,0,2.6-.3,3.6-.8,1-.5,1.8-1.3,2.4-2.3s.8-2.1.8-3.3-.3-2.3-.8-3.3c-.6-1-1.4-1.7-2.4-2.3-1-.5-2.2-.8-3.6-.8h-4.7v12.8Z"/>
|
||||
<path d="M96.7,55.1c-1.1,0-2.1-.2-3-.6-.9-.4-1.7-.9-2.4-1.6s-1.2-1.5-1.6-2.4c-.4-.9-.6-1.9-.6-3s.2-2,.6-3c.4-.9.9-1.7,1.6-2.4s1.5-1.2,2.4-1.6c.9-.4,1.9-.6,3-.6s2.1.2,3,.6c.9.4,1.7.9,2.4,1.6s1.2,1.5,1.6,2.4c.4.9.6,1.9.6,3s-.2,2.1-.6,3c-.4.9-.9,1.7-1.6,2.4s-1.5,1.2-2.4,1.6c-.9.4-1.9.6-3,.6ZM96.7,54.1c.9,0,1.8-.2,2.6-.5.8-.3,1.5-.8,2.1-1.4s1-1.3,1.4-2.1c.3-.8.5-1.6.5-2.6s-.2-1.8-.5-2.6c-.3-.8-.8-1.5-1.4-2-.6-.6-1.3-1-2.1-1.4-.8-.3-1.7-.5-2.6-.5s-1.8.2-2.6.5c-.8.3-1.5.8-2.1,1.4-.6.6-1.1,1.3-1.4,2-.3.8-.5,1.6-.5,2.6s.2,1.8.5,2.6c.3.8.8,1.5,1.4,2.1.6.6,1.3,1,2.1,1.4.8.3,1.7.5,2.6.5Z"/>
|
||||
<path d="M120.6,55.1c-1.1,0-2.1-.2-3-.6-.9-.4-1.7-.9-2.4-1.6s-1.2-1.5-1.6-2.4c-.4-.9-.6-1.9-.6-3s.2-2.1.6-3c.4-.9.9-1.7,1.6-2.4s1.5-1.2,2.4-1.6c.9-.4,1.9-.6,3-.6s2,.2,2.9.5c.9.3,1.7.8,2.3,1.5l-.7.7c-.6-.6-1.3-1.1-2.1-1.3-.8-.3-1.6-.4-2.4-.4s-1.8.2-2.6.5c-.8.3-1.5.8-2.1,1.4-.6.6-1.1,1.3-1.4,2.1-.3.8-.5,1.6-.5,2.6s.2,1.8.5,2.6c.3.8.8,1.5,1.4,2.1.6.6,1.3,1,2.1,1.4.8.3,1.7.5,2.6.5s1.7-.1,2.4-.4c.8-.3,1.5-.7,2.1-1.4l.7.7c-.6.7-1.4,1.2-2.3,1.5-.9.3-1.9.5-2.9.5Z"/>
|
||||
<path d="M153.7,55.1c-1.1,0-2.1-.2-3-.6-.9-.4-1.7-.9-2.4-1.6s-1.2-1.5-1.6-2.4c-.4-.9-.6-1.9-.6-3s.2-2.1.6-3c.4-.9.9-1.7,1.6-2.4s1.5-1.2,2.4-1.6c.9-.4,1.9-.6,3-.6s2,.2,2.9.5c.9.3,1.7.8,2.3,1.5l-.7.7c-.6-.6-1.3-1.1-2.1-1.3-.8-.3-1.6-.4-2.4-.4s-1.8.2-2.6.5c-.8.3-1.5.8-2.1,1.4-.6.6-1.1,1.3-1.4,2.1-.3.8-.5,1.6-.5,2.6s.2,1.8.5,2.6c.3.8.8,1.5,1.4,2.1.6.6,1.3,1,2.1,1.4.8.3,1.7.5,2.6.5s1.7-.1,2.4-.4c.8-.3,1.5-.7,2.1-1.4l.7.7c-.6.7-1.4,1.2-2.3,1.5-.9.3-1.9.5-2.9.5Z"/>
|
||||
<path d="M174.7,55.1c-1.1,0-2.1-.2-3-.6-.9-.4-1.7-.9-2.4-1.6s-1.2-1.5-1.6-2.4c-.4-.9-.6-1.9-.6-3s.2-2,.6-3c.4-.9.9-1.7,1.6-2.4s1.5-1.2,2.4-1.6c.9-.4,1.9-.6,3-.6s2.1.2,3,.6c.9.4,1.7.9,2.4,1.6s1.2,1.5,1.6,2.4c.4.9.6,1.9.6,3s-.2,2.1-.6,3c-.4.9-.9,1.7-1.6,2.4s-1.5,1.2-2.4,1.6c-.9.4-1.9.6-3,.6ZM174.7,54.1c.9,0,1.8-.2,2.6-.5.8-.3,1.5-.8,2.1-1.4s1-1.3,1.4-2.1c.3-.8.5-1.6.5-2.6s-.2-1.8-.5-2.6c-.3-.8-.8-1.5-1.4-2-.6-.6-1.3-1-2.1-1.4-.8-.3-1.7-.5-2.6-.5s-1.8.2-2.6.5c-.8.3-1.5.8-2.1,1.4-.6.6-1.1,1.3-1.4,2-.3.8-.5,1.6-.5,2.6s.2,1.8.5,2.6c.3.8.8,1.5,1.4,2.1.6.6,1.3,1,2.1,1.4.8.3,1.7.5,2.6.5Z"/>
|
||||
<path d="M192.4,55v-14.7h.9l10.5,13.4h-.5v-13.4h1.1v14.7h-.9l-10.5-13.4h.5v13.4h-1.1Z"/>
|
||||
<path d="M219.7,55l-6.6-14.7h1.2l6.3,14h-.7l6.3-14h1.1l-6.6,14.7h-1.1Z"/>
|
||||
<path d="M237,54h9.1v1h-10.1v-14.7h9.8v1h-8.8v12.8ZM236.9,47.1h8v.9h-8v-.9Z"/>
|
||||
<path d="M256.3,55v-14.7h5.3c1.2,0,2.3.2,3.2.6s1.6,1,2,1.7c.5.7.7,1.6.7,2.7s-.2,1.9-.7,2.6c-.5.7-1.2,1.3-2,1.7-.9.4-1.9.6-3.2.6h-4.7l.5-.5v5.3h-1.1ZM257.4,49.8l-.5-.5h4.7c1.6,0,2.8-.4,3.6-1.1.8-.7,1.2-1.7,1.2-2.9s-.4-2.3-1.2-3c-.8-.7-2-1-3.6-1h-4.7l.5-.5v9ZM266.5,55l-3.8-5.3h1.2l3.8,5.3h-1.2Z"/>
|
||||
<path d="M280.3,55v-13.7h-5.3v-1h11.7v1h-5.3v13.7h-1.1Z"/>
|
||||
<path d="M296.7,54h9.1v1h-10.1v-14.7h9.8v1h-8.8v12.8ZM296.6,47.1h8v.9h-8v-.9Z"/>
|
||||
<path d="M316,55v-14.7h5.3c1.2,0,2.3.2,3.2.6s1.6,1,2,1.7c.5.7.7,1.6.7,2.7s-.2,1.9-.7,2.6c-.5.7-1.2,1.3-2,1.7-.9.4-1.9.6-3.2.6h-4.7l.5-.5v5.3h-1.1ZM317.1,49.8l-.5-.5h4.7c1.6,0,2.8-.4,3.6-1.1.8-.7,1.2-1.7,1.2-2.9s-.4-2.3-1.2-3c-.8-.7-2-1-3.6-1h-4.7l.5-.5v9ZM326.2,55l-3.8-5.3h1.2l3.8,5.3h-1.2Z"/>
|
||||
</g>
|
||||
<path class="st2" d="M276.8,142.1c.6.5.9,1,.9,1.6s-.4,1.2-1.1,1.9c-.9,1-1.8,1.8-2.6,2.5-.8.7-1.8,1.4-2.9,2.2-.7.5-1.4.8-2.1.9-.7.1-1.3,0-1.7-.4-.5-.3-.9-.8-1.1-1.5-.3-.7-.4-1.5-.3-2.6l1.4-22.8c.1-.7-.2-1-.9-.9l-5,.5c-1.1,0-1.8,0-2.1-.3-.4-.3-.6-.8-.7-1.6-.1-.7.1-1.2.4-1.6.3-.3,1-.6,1.9-.6l5.7-.5c1.7-.2,3,0,3.9.8.8.7,1.2,2.1,1.1,4.1l-1.3,21.3c-.1.7.1.8.5.3.6-.5,1.2-1,1.6-1.4.5-.4.9-.9,1.4-1.4.9-1,2-1.2,3-.5ZM273.8,115.1c-.9.9-2,.7-3.3-.7-1-1.1-2.1-2.2-3.2-3.4s-2.4-2.3-3.7-3.6c-1.2-1-1.3-2.1-.2-3.3.4-.4.8-.6,1.4-.5.6,0,1.2.4,1.9,1,1.6,1.4,3,2.7,4,3.7,1,1.1,2,2.1,2.9,3.1.7.7,1.1,1.4,1.1,1.9,0,.7-.3,1.3-.9,1.8ZM279.2,130.9c-1,1.3-2.1,1.5-3.4.7-1.1-.8-1.1-1.9,0-3.2.5-.7,1-1.3,1.3-1.8s.7-1.1.9-1.6c.3-.5.5-1.1.8-1.8.3-.6.5-1.4.9-2.4.5-1.7,1.4-2.3,2.8-1.9.7.2,1.1.6,1.2,1.1.1.6.1,1.2-.2,2h22.3c.8,0,1.3.2,1.6.5.3.3.4.9.4,1.6s-.1,1.1-.4,1.5-.8.5-1.6.5h-11.7c-.1,1.5-.2,2.9-.3,4.2-.1,1.3-.3,2.4-.5,3.5h15.4c.7,0,1.2.1,1.6.4.3.3.5.8.5,1.5s-.2,1.2-.5,1.6c-.3.4-.9.6-1.6.6h-16.3c-1.3,3.6-3.3,6.7-6.1,9.4-2.8,2.7-6.2,4.9-10.3,6.6-.8.3-1.6.4-2.2.4-.6-.1-1.1-.5-1.4-1.2-.3-.8-.3-1.5-.1-2,.3-.5.9-.9,1.9-1.3,3.5-1.3,6.4-3,8.5-4.9s3.8-4.2,5.1-7h-13.2c-.8,0-1.3-.2-1.6-.6-.2-.4-.4-.9-.4-1.6s.1-1.1.4-1.5c.2-.3.8-.5,1.6-.5h14.5c.2-1.1.4-2.2.5-3.5s.3-2.7.3-4.2h-8.1c-.4.8-.8,1.6-1.2,2.3-.2.8-.7,1.6-1.4,2.6ZM304.6,120.1c-.3-.4-.6-.9-.9-1.3s-.6-.9-.9-1.4l-23.3,1.1c-.9,0-1.6-.2-2.1-.5s-.8-.8-.8-1.4c0-1,.7-2.1,2.1-3.4,1.9-1.5,3.6-3.1,5.2-4.8,1.6-1.6,3.1-3.4,4.5-5.2.5-.7,1-1.1,1.5-1.4s1.2-.2,1.9.2c1.5.8,1.5,2,.3,3.5-1.5,1.7-2.8,3.2-4.1,4.6s-2.7,2.7-4.2,4.1c1.6,0,3,0,4.4,0s2.7,0,4-.2,2.5,0,3.8-.2,2.6,0,4-.2c-.6-.8-1.3-1.5-1.9-2.3-.7-.8-1.4-1.5-2.1-2.3-.4-.5-.7-1.1-.8-1.6-.1-.5.1-1,.5-1.5,1.2-1,2.3-1,3.2,0,1.5,1.6,2.9,3.4,4.3,5.2,1.4,1.8,3,3.9,4.8,6.2.6.8,1,1.6,1,2.2.1.6-.2,1.1-.9,1.6-.7.4-1.3.6-1.8.4-.6-.4-1.2-.8-1.7-1.4ZM309.1,150.1c-1.2-1.5-2.7-2.9-4.3-4.5s-3.5-3.1-5.7-4.7c-.7-.5-1.4-.8-2-.8s-1.2.1-1.6.5c-.4.5-.6,1-.6,1.5s.5,1.1,1.3,1.7c2,1.5,3.8,3,5.5,4.6s3,3.1,4,4.3c.6.8,1.2,1.2,1.8,1.2.6.1,1.1-.1,1.7-.5.5-.4.8-.9.8-1.5,0-.4-.3-1.1-.9-1.8Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
Loading…
Add table
Reference in a new issue