Update Saqel Platform: 2026-09-08 23:29:32

This commit is contained in:
Hamza-Ayed
2026-09-08 23:29:32 +03:00
parent 590326baa0
commit 90487a42ad
3 changed files with 57 additions and 24 deletions
+6 -6
View File
@@ -340,10 +340,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.12.19" version: "0.12.18"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
@@ -356,10 +356,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.18.0" version: "1.17.0"
nested: nested:
dependency: transitive dependency: transitive
description: description:
@@ -585,10 +585,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" version: "0.7.9"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -18,9 +18,36 @@ namespace App\Services;
use App\Core\Database; use App\Core\Database;
use App\Core\Security; use App\Core\Security;
use App\Core\RedisClient;
class AiVideoAnalyzerService class AiVideoAnalyzerService
{ {
/**
* Select a Gemini key from the configured comma-separated pool. Redis
* provides a process-safe round robin counter across PHP-FPM workers.
* No key material is logged or returned to callers.
*/
private static function nextGeminiApiKey(): string
{
$raw = trim((string)(getenv('GEMINI_API_KEYS') ?: getenv('GEMINI_API_KEY') ?: ''));
$keys = array_values(array_filter(array_map('trim', explode(',', $raw))));
if (empty($keys)) {
return '';
}
if (count($keys) === 1) {
return $keys[0];
}
try {
$turn = (int)RedisClient::getInstance()->incr('saqel:gemini:round_robin');
return $keys[($turn - 1) % count($keys)];
} catch (\Throwable $e) {
// The analysis can still proceed during a Redis outage. The time
// bucket alternates keys without exposing either value.
return $keys[(int)(floor(time() / 60) % count($keys))];
}
}
/** /**
* Fail-closed admission gate for teacher uploads. The media stays in PHP's * Fail-closed admission gate for teacher uploads. The media stays in PHP's
* temporary upload area until Gemini and technical checks approve it. * temporary upload area until Gemini and technical checks approve it.
@@ -50,7 +77,7 @@ class AiVideoAnalyzerService
if (!$hasVideo || !$hasAudio || $duration <= 0 || $duration > 1500) { if (!$hasVideo || !$hasAudio || $duration <= 0 || $duration > 1500) {
return ['decision' => 'rejected', 'reason' => 'الفيديو يجب أن يحتوي صورة وصوتاً وأن لا يتجاوز 25 دقيقة', 'duration_seconds' => $duration]; return ['decision' => 'rejected', 'reason' => 'الفيديو يجب أن يحتوي صورة وصوتاً وأن لا يتجاوز 25 دقيقة', 'duration_seconds' => $duration];
} }
$key = trim((string)getenv('GEMINI_API_KEY')); $key = self::nextGeminiApiKey();
if ($key === '') return ['decision' => 'needs_manual_review', 'reason' => 'Gemini غير مهيأ؛ لم يتم رفع الفيديو', 'duration_seconds' => $duration]; if ($key === '') return ['decision' => 'needs_manual_review', 'reason' => 'Gemini غير مهيأ؛ لم يتم رفع الفيديو', 'duration_seconds' => $duration];
// Extract a representative frame; Gemini receives real media evidence, // Extract a representative frame; Gemini receives real media evidence,
@@ -200,7 +227,7 @@ class AiVideoAnalyzerService
public static function evaluatePedagogicalQuality(int $lessonId, string $lessonTitle, int $duration, array $curriculum): array public static function evaluatePedagogicalQuality(int $lessonId, string $lessonTitle, int $duration, array $curriculum): array
{ {
try { try {
$geminiKey = getenv('GEMINI_API_KEY'); $geminiKey = self::nextGeminiApiKey();
$alignmentScore = 0.0; $alignmentScore = 0.0;
$clarityScore = 0.0; $clarityScore = 0.0;
$outcomes = [ $outcomes = [
@@ -312,7 +339,7 @@ class AiVideoAnalyzerService
*/ */
private static function generateAnalysis(string $lessonTitle, int $duration, array $curriculum): array private static function generateAnalysis(string $lessonTitle, int $duration, array $curriculum): array
{ {
$geminiKey = getenv('GEMINI_API_KEY'); $geminiKey = self::nextGeminiApiKey();
if (!empty($geminiKey)) { if (!empty($geminiKey)) {
try { try {
+21 -15
View File
@@ -27,22 +27,28 @@ spl_autoload_register(function ($class) {
// 2. Load Environment Variables with Multi-Path Fallback // 2. Load Environment Variables with Multi-Path Fallback
try { try {
$candidatePaths = [ // Production secrets belong outside the web/document root. Do not load
'/home/intaleqapp-saqel/.env', // backend/.env: it may be a placeholder and must never shadow the real
'/home/intaleqapp-saqel/htdocs/saqel.intaleqapp.com/.env', // CloudPanel environment file.
'/home/intaleqapp-saqel/htdocs/saqel.intaleqapp.com/saqel/.env', $envPath = getenv('SAQEL_ENV_FILE') ?: '/home/intaleqapp-saqel/.env';
'/home/intaleqapp-saqel/htdocs/saqel.intaleqapp.com/saqel/backend/.env',
APP_ROOT . '/.env',
APP_ROOT . '/../.env'
];
$loaded = false; $loaded = false;
foreach ($candidatePaths as $path) { if (file_exists($envPath)) {
if (file_exists($path)) { \App\Core\Env::load($envPath);
define('LOADED_ENV_PATH', $path); define('LOADED_ENV_PATH', $envPath);
\App\Core\Env::load($path); $loaded = true;
$loaded = true;
break; // Support either GEMINI_API_KEYS=key1,key2 or the legacy singular
// variable containing a comma-separated pool. Legacy services receive
// one safe key; services with round-robin support consume the pool.
$geminiPool = trim((string)(getenv('GEMINI_API_KEYS') ?: getenv('GEMINI_API_KEY') ?: ''));
$geminiKeys = array_values(array_filter(array_map('trim', explode(',', $geminiPool))));
if (!empty($geminiKeys)) {
putenv('GEMINI_API_KEYS=' . implode(',', $geminiKeys));
$_ENV['GEMINI_API_KEYS'] = implode(',', $geminiKeys);
$_SERVER['GEMINI_API_KEYS'] = implode(',', $geminiKeys);
putenv('GEMINI_API_KEY=' . $geminiKeys[0]);
$_ENV['GEMINI_API_KEY'] = $geminiKeys[0];
$_SERVER['GEMINI_API_KEY'] = $geminiKeys[0];
} }
} }