Files
cv/server/generate_cv.php
2026-06-02 16:37:46 +03:00

306 lines
12 KiB
PHP

<?php
// ============================================================================
// Dynamic ATS-Optimized CV Generator & AI Proxy (Backend)
// ============================================================================
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
$autoloadPath = __DIR__ . '/vendor/autoload.php';
if (!file_exists($autoloadPath)) {
http_response_code(500);
echo json_encode(["error" => "Vendor folder not found. Please run 'composer install'."]);
exit;
}
require_once $autoloadPath;
use Dompdf\Dompdf;
use Dompdf\Options;
use Dotenv\Dotenv;
// Path to the .env file located outside the document root for security
// Assuming script is in /home/user/htdocs/domain.com/cv/server/
// and .env is in /home/user/
$envPath = realpath(__DIR__ . '/../../../..');
if ($envPath && file_exists($envPath . '/.env')) {
$dotenv = Dotenv::createImmutable($envPath);
$dotenv->load();
}
$rawData = file_get_contents('php://input');
$data = json_decode($rawData, true);
$action = $data['action'] ?? 'generateText';
// Prioritize API key from .env over frontend payload
$apiKey = $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY') ?: ($data['apiKey'] ?? '');
if (empty($apiKey)) {
http_response_code(400);
echo json_encode(["error" => "Missing apiKey. Please set GEMINI_API_KEY in .env or pass it in request."]);
exit;
}
// Standardized on gemini-flash-lite-latest due to quota limits
$model = "gemini-flash-lite-latest";
$geminiUrl = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key=" . $apiKey;
// ==========================================
// ACTION 1: Generate ATS PDF CV
// ==========================================
if ($action === 'generatePdf') {
$jobDescription = $data['jobDescription'] ?? '';
$template = $data['template'] ?? 'default'; // 'amman' or 'default'
// --- Amman Market Prompt (Senior Backend Engineer & Technical Lead) ---
if ($template === 'amman') {
$prompt = "You are an expert ATS CV tailor for the Jordan/Amman local tech market. Read the following job description and generate tailored content.
STRICT INTEGRITY RULES:
1. NEVER invent skills I do not have. My TRUE technical stack is: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), PostgreSQL/PostGIS, Docker, Flutter, GetX, BLoC, WebSockets, OpenStreetMap.
2. Do NOT add data science skills like TensorFlow, PyTorch, Scikit-learn, Hadoop, or MLOps.
3. My title MUST be aligned with 'Senior Backend Engineer', 'Technical Lead', or 'Lead Backend Engineer'. NEVER use 'Solutions Architect', 'AI Developer', or 'CTO'.
Return ONLY a valid JSON object with EXACTLY three keys: 'headline', 'summary', and 'skills'.
The 'headline' should be a clean, confident title based on my TRUE skills — matching Amman market expectations (Senior/Lead Backend Engineer).
The 'summary' should open with a hook about building and scaling production systems in the MENA region, emphasizing delivery, cost optimization, and hands-on engineering. Connect my real achievements to the job requirements. Keep it professional and direct — NO academic fluff, NO 'I leverage my expertise' phrases.
The 'skills' should be a comma-separated list of 10 highly relevant ATS keywords from MY ACTUAL SKILLS that match the job. Prioritize backend, API, database, and infrastructure skills over GIS.
Do NOT use markdown blocks like ```json, just return raw JSON text.
Job Description:
" . substr($jobDescription, 0, 4000);
}
// --- Default Prompt (Current — Enterprise/GCC) ---
else {
$prompt = "You are an expert ATS CV tailor. Read the following job description and generate tailored content for my CV to maximize my chances of getting an interview.
STRICT INTEGRITY RULES:
1. NEVER invent skills I do not have. My TRUE technical stack is: PHP (Workerman), Node.js, NestJS, Python (FastAPI/Flask), PostgreSQL/PostGIS, Docker, Flutter, GetX, BLoC, WebSockets, OpenStreetMap.
2. Do NOT add data science skills like TensorFlow, PyTorch, Scikit-learn, Hadoop, or MLOps.
3. My title MUST be aligned with 'Solutions Architect', 'Senior Backend Engineer', or 'Senior Mobile Engineer'. NEVER title me 'Senior AI Developer'.
Return ONLY a valid JSON object with EXACTLY three keys: 'headline', 'summary', and 'skills'.
The 'headline' should be a clean, confident title based on my TRUE skills.
The 'summary' MUST open with exactly this hook: 'Built two production ride-hailing platforms from zero to thousands of users, on proprietary infrastructure, in high-complexity and emerging markets.' Then use the next 2 sentences to seamlessly tie my relevant TRUE skills to the job description requirements.
The 'skills' should be a comma-separated list of 10 highly relevant ATS keywords from MY ACTUAL SKILLS that match the job.
Do NOT use markdown blocks like ```json, just return raw JSON text.
Job Description:
" . substr($jobDescription, 0, 4000);
}
$payload = json_encode([
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => ["temperature" => 0.2, "responseMimeType" => "application/json"]
]);
$ch = curl_init($geminiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
http_response_code(500);
echo json_encode(["error" => "Gemini API Error", "details" => json_decode($response)]);
exit;
}
$responseData = json_decode($response, true);
$aiText = $responseData['candidates'][0]['content']['parts'][0]['text'] ?? '{}';
$aiText = str_replace(['```json', '```'], '', $aiText);
$parsedJson = json_decode(trim($aiText), true);
$defaultHeadline = ($template === 'amman')
? "Senior Backend Engineer & Technical Lead"
: "Solutions Architect & Technical Leader";
$headline = $parsedJson['headline'] ?? $defaultHeadline;
$summary = $parsedJson['summary'] ?? "Experienced professional with a strong background in software engineering.";
$skills = $parsedJson['skills'] ?? "Architecture, APIs, Cloud, Backend Systems, System Design";
$templateFile = ($template === 'amman')
? __DIR__ . '/cv_template_amman.html'
: __DIR__ . '/cv_template.html';
$html = file_get_contents($templateFile);
$html = str_replace('{{JOB_HEADLINE}}', htmlspecialchars($headline), $html);
$html = str_replace('{{TAILORED_SUMMARY}}', htmlspecialchars($summary), $html);
$html = str_replace('{{DYNAMIC_SKILLS}}', htmlspecialchars($skills), $html);
try {
$options = new Options();
$options->set('isHtml5ParserEnabled', true);
$options->set('defaultFont', 'Helvetica');
$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4', 'portrait');
$dompdf->render();
$pdfOutput = $dompdf->output();
$rawJobTitle = $data['jobTitle'] ?? 'Job';
$safeJobTitle = preg_replace('/[^a-zA-Z0-9\-_]/', '_', $rawJobTitle);
$safeJobTitle = trim($safeJobTitle, '_');
$fileName = "Hamza_Ayed - {$safeJobTitle}.pdf";
echo json_encode([
"success" => true,
"pdf" => base64_encode($pdfOutput),
"filename" => $fileName
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode(["error" => "PDF Generation Failed", "details" => $e->getMessage()]);
}
exit;
}
// ==========================================
// ACTION 2: Standard Proxy (Text generation)
// ==========================================
if ($action === 'generateText') {
$prompt = $data['prompt'] ?? '';
$payload = json_encode([
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => ["temperature" => 0.7, "maxOutputTokens" => 8192]
]);
$ch = curl_init($geminiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
http_response_code($httpCode);
echo $response;
exit;
}
// Pass the exact Gemini response back to the extension
echo $response;
exit;
}
// ==========================================
// ACTION 3: Smart Comment Generator
// ==========================================
if ($action === 'generateComment') {
$postText = substr($data['postText'] ?? '', 0, 3000);
if (empty($postText)) {
http_response_code(400);
echo json_encode(["error" => "postText is required."]);
exit;
}
$promptFile = __DIR__ . '/prompts/comment_prompt.txt';
if (!file_exists($promptFile)) {
http_response_code(500);
echo json_encode(["error" => "Comment prompt file not found on server."]);
exit;
}
$promptTemplate = file_get_contents($promptFile);
$prompt = str_replace('{{POST_TEXT}}', $postText, $promptTemplate);
$payload = json_encode([
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => ["temperature" => 0.75, "maxOutputTokens" => 256]
]);
$ch = curl_init($geminiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
http_response_code(500);
echo json_encode(["error" => "Gemini API Error", "details" => json_decode($response)]);
exit;
}
$responseData = json_decode($response, true);
$commentText = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? '');
if (empty($commentText)) {
http_response_code(500);
echo json_encode(["error" => "Empty comment from AI."]);
exit;
}
echo json_encode(["success" => true, "comment" => $commentText]);
exit;
}
// ==========================================
// ACTION 4: Repurpose Post Generator
// ==========================================
if ($action === 'repurposePost') {
$postText = substr($data['postText'] ?? '', 0, 3000);
if (empty($postText)) {
http_response_code(400);
echo json_encode(["error" => "postText is required."]);
exit;
}
$promptFile = __DIR__ . '/prompts/repurpose_prompt.txt';
if (!file_exists($promptFile)) {
http_response_code(500);
echo json_encode(["error" => "Repurpose prompt file not found on server."]);
exit;
}
$promptTemplate = file_get_contents($promptFile);
$prompt = str_replace('{{POST_TEXT}}', $postText, $promptTemplate);
$payload = json_encode([
"contents" => [["parts" => [["text" => $prompt]]]],
"generationConfig" => ["temperature" => 0.75, "maxOutputTokens" => 800]
]);
$ch = curl_init($geminiUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200) {
http_response_code(500);
echo json_encode(["error" => "Gemini API Error", "details" => json_decode($response)]);
exit;
}
$responseData = json_decode($response, true);
$resultText = trim($responseData['candidates'][0]['content']['parts'][0]['text'] ?? '');
if (empty($resultText)) {
http_response_code(500);
echo json_encode(["error" => "Empty result from AI."]);
exit;
}
echo json_encode(["success" => true, "result" => $resultText]);
exit;
}