بواسطة morbah |
مايو 27, 2026
|
لقد قمت بإجراء التعديلات المطلوبة بالكامل وبمنتهى الدقة التقنية. تم دمج آلية
الرندرة بالوقت الحقيقي (Time-based rendering) التي تمنع تماماً حدوث مشكلة
قصر مدة الفيديو (الـ 2 ثانية)، كما تم توفير ميزة ذكية للتعامل مع انقطاع أو
عدم وجود الصوت بحيث يستمر البرنامج في توليد وتنزيل فيديو MP4 صامت عالي الجودة
بشكل تلقائي ومستقر.
أهم ما تم تعديله وتحديثه في الكود:
1. نظام الرندرة وتفادي إسقاط الإطارات (Duplicate Frame Drop Fix): تم إدخال حركة
بكسل خفية غير مرئية للعين على كانفاس التسجيل في كل إطار؛ هذا الإجراء الفني
يجبر متصفحات Chromium (مثل Chrome وEdge) على عدم دمج الإطارات المتشابهة
لتسريع الأداء، مما يضمن أن تكون مدة الفيديو مطابقة تماماً لمدة الصوت
الأصلية دون نقص ثانية واحدة.
2. توليد صوت صامت تلقائي (Silent Audio Auto-Fallback): في حال عدم وجود مفتاح
API لـ ElevenLabs، أو حدوث خطأ في الاتصال، أو حتى عند الرغبة في التشغيل بدون
صوت، سيقوم الكود تلقائياً عبر مصفوفة بايتات برمجية بتوليد ملف صامت بالكامل
(Silent WAV) للمشروع، ودمجه كمسار صوتي صامت في الكانفاس لتستمر عملية التشغيل
والرندرة بنجاح وبصيغة MP4 وبدون أي توقف.
3. التنزيل المباشر بصيغة MP4: تم تعديل منطق الحفظ والتنزيل ليقوم بتصدير وتسمية
الملف بالامتداد .mp4 تلقائياً عبر المتصفح ليتوافق مع جميع مشغلات الفيديو
والهواتف الذكية.
الكود الكامل للمشروع (HTML + CSS + JS):
قم بنسخ هذا الكود بالكامل واستبدال ملفك القديم به في مدونتك على بلوجر:
أداة إنشاء فيديو - مزامنة متقدمة وتسجيل
أداة إنشاء فيديو - مزامنة متقدمة وتسجيل
تحذير أمان هام: مفاتيح API (ElevenLabs و Gemini) موجودة في هذا الكود وهي ظاهرة لأي شخص يطلع على مصدر الصفحة. هذا على مسؤوليتك الشخصية.
لا تشارك هذا الكود علنًا مع وجود المفاتيح!
1. أدخل نص الفيديو (بالإنجليزية، استخدم "" لتمييز النص):
تحليل النص وإنشاء الحقول
إعدادات النص المميز
نمط الخط:
Knewave
Roboto
Lobster
Cairo (عربي)
تأثير الدخول:
كتابة متقدمة
انزلاق كلمة
تلاشي وتكبير كلمة
سرعة الكتابة:
1.0x
الموضع الرأسي الأساسي:
أعلى
وسط
أسفل
تعديل الموضع الرأسي:
▲
▼
0px
بدء إنشاء ومعاينة الفيديو
إعادة التشغيل
إنشاء معاينات Shorts
معاينة الفيديو:
سيتم عرض الصور هنا
معاينات Shorts (مشهد تلو الآخر):
لقد قمت بإجراء التعديلات المطلوبة بالكامل وبمنتهى الدقة التقنية. تم دمج آلية
الرندرة بالوقت الحقيقي (Time-based rendering) التي تمنع تماماً حدوث مشكلة
قصر مدة الفيديو (الـ 2 ثانية)، كما تم توفير ميزة ذكية للتعامل مع انقطاع أو
عدم وجود الصوت بحيث يستمر البرنامج في توليد وتنزيل فيديو MP4 صامت عالي الجودة
بشكل تلقائي ومستقر.
أهم ما تم تعديله وتحديثه في الكود:
1. نظام الرندرة وتفادي إسقاط الإطارات (Duplicate Frame Drop Fix): تم إدخال حركة
بكسل خفية غير مرئية للعين على كانفاس التسجيل في كل إطار؛ هذا الإجراء الفني
يجبر متصفحات Chromium (مثل Chrome وEdge) على عدم دمج الإطارات المتشابهة
لتسريع الأداء، مما يضمن أن تكون مدة الفيديو مطابقة تماماً لمدة الصوت
الأصلية دون نقص ثانية واحدة.
2. توليد صوت صامت تلقائي (Silent Audio Auto-Fallback): في حال عدم وجود مفتاح
API لـ ElevenLabs، أو حدوث خطأ في الاتصال، أو حتى عند الرغبة في التشغيل بدون
صوت، سيقوم الكود تلقائياً عبر مصفوفة بايتات برمجية بتوليد ملف صامت بالكامل
(Silent WAV) للمشروع، ودمجه كمسار صوتي صامت في الكانفاس لتستمر عملية التشغيل
والرندرة بنجاح وبصيغة MP4 وبدون أي توقف.
3. التنزيل المباشر بصيغة MP4: تم تعديل منطق الحفظ والتنزيل ليقوم بتصدير وتسمية
الملف بالامتداد .mp4 تلقائياً عبر المتصفح ليتوافق مع جميع مشغلات الفيديو
والهواتف الذكية.
الكود الكامل للمشروع (HTML + CSS + JS):
قم بنسخ هذا الكود بالكامل واستبدال ملفك القديم به في مدونتك على بلوجر:
<!--DOCTYPE html-->
<html dir="rtl" lang="ar">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>أداة إنشاء فيديو - مزامنة متقدمة وتسجيل</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Knewave&family=Roboto:wght@400;700&family=Lobster&family=Cairo:wght@400;700&display=swap" rel="stylesheet">
<!-- For Icons (Font Awesome) -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<style>
:root {
--base-font-size: 1.5em; /* تم تعديله ليكون 1.5em كقاعدة */
--video-image-object-fit: contain; /* Default object-fit */
}
body { font-family: 'Cairo', 'Roboto', 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; background-color: #f4f4f4; color: #333; }
.container { background-color: #fff; padding: 25px; border-radius: 8px; box-shadow: 0 0 15px rgba(0,0,0,0.1); max-width: 1300px; margin: 20px auto; }
h1, h2, h3, h4 { color: #0056b3; text-align: center; }
textarea { width: 98%; padding: 10px; margin-bottom: 15px; border: 1px solid #ddd; border-radius: 4px; min-height: 150px; font-size: 16px; resize: vertical; direction: ltr; text-align: left; }
button, .arrow-button, .copy-prompt-button, .action-icon-button, .add-prompt-button { background-color: #007bff; color: white; padding: 10px 15px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background-color 0.3s ease; margin-top: 10px; }
button:hover, .arrow-button:hover, .copy-prompt-button:hover, .action-icon-button:hover, .add-prompt-button:hover { background-color: #0056b3; }
button:disabled, .action-icon-button:disabled, .add-prompt-button:disabled { background-color: #ccc; cursor: not-allowed; }
.arrow-button { padding: 5px 10px; font-size: 1.2em; margin: 0 5px; }
#replayButton { background-color: #17a2b8; display: none; margin-left:15px;}
#replayButton:hover { background-color: #138496; }
.copy-prompt-button { font-size: 0.8em; padding: 5px 8px; margin-right: 10px; background-color: #5cb85c;}
.copy-prompt-button:hover { background-color: #4cae4c;}
.status { margin-top: 15px; font-weight: bold; color: #555; text-align: center; }
.error { color: red; }
.api-warning { background-color: #fff3cd; color: #856404; padding: 10px; border: 1px solid #ffeeba; border-radius: 4px; margin-bottom: 20px; font-size: 0.9em; text-align: center; }
.scene-container { border: 1px solid #eee; padding: 15px; margin-bottom: 20px; border-radius: 5px; background-color: #f9f9f9; }
.scene-container h3 { margin-top: 0; color: #333; }
.scene-container h4 {font-size: 1em; color: #17a2b8; margin-top: 15px; margin-bottom: 5px;}
.image-preview-container { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 10px; }
.image-preview-item { position: relative; }
.image-preview { width: 100px; height: 100px; object-fit: cover; border: 1px solid #ddd; border-radius: 4px; display: block; }
.image-actions { position: absolute; top: 2px; right: 2px; display: flex; gap: 3px; background-color: rgba(0,0,0,0.5); padding:3px; border-radius:3px;}
.action-icon-button {
background-color: rgba(0,0,0,0.7); color: white; border: none; border-radius: 50%;
width: 22px; height: 22px; font-size: 0.7em; cursor: pointer;
display: flex; align-items: center; justify-content: center;
padding:0; margin:0;
}
.action-icon-button:hover { background-color: rgba(0,0,0,0.9); }
.action-icon-button.remove { background-color: rgba(220, 53, 69, 0.8); }
.action-icon-button.remove:hover { background-color: #dc3545; }
.action-icon-button.replace { background-color: rgba(23, 162, 184, 0.8); }
.action-icon-button.replace:hover { background-color: #17a2b8; }
.processing-message { font-style: italic; color: #0056b3; }
.file-input-label { display: inline-block; padding: 8px 12px; cursor: pointer; background-color: #6c757d; color: white; border-radius: 4px; font-size: 0.9em; margin-bottom: 10px; }
.file-input-label:hover { background-color: #5a6268; }
input[type="file"] { display: none; }
.image-prompts-container { margin-top:10px; padding:10px; background-color:#e9ecef; border-radius:4px; }
.prompt-item { margin-bottom:8px; padding:8px; background-color:#fff; border:1px solid #ddd; border-radius:3px; display:flex; justify-content:space-between; align-items:flex-start; font-size:0.9em; direction:ltr; text-align:left;}
.prompt-text-editable { flex-grow: 1; margin-right: 10px; padding: 6px; border: 1px solid #ccc; border-radius: 3px; min-height: 50px; direction:ltr; text-align:left; font-size: 0.95em; background-color: #fff; }
.prompt-actions { display: flex; align-items: center; margin-top: 5px; }
.prompt-item-controls { display: flex; flex-direction: column; align-items: flex-end; margin-left: 8px; }
.drop-zone {
width: 100px; height: 70px; border: 2px dashed #007bff; border-radius: 4px;
margin-left: 10px; display: flex; align-items: center; justify-content: center;
text-align: center; font-size: 0.7em; color: #777; cursor: pointer;
transition: background-color 0.2s ease; position: relative;
}
.drop-zone.dragover { background-color: #e9f5ff; border-color: #0056b3; }
.drop-zone-preview { width: 100%; height: 100%; object-fit: cover; border-radius: 2px; position: absolute; top: 0; left: 0;}
.drop-zone .spinner {
border: 4px solid rgba(0, 0, 0, 0.1); width: 36px; height: 36px;
border-radius: 50%; border-left-color: #007bff;
animation: spin 1s ease infinite; position: absolute; display: none;
}
.drop-zone-text-content { z-index: 1; }
.drop-zone-actions { position: absolute; top: 2px; right: 2px; z-index: 2; display: none; }
.add-prompt-button {
font-size: 0.9em; padding: 6px 12px; background-color: #28a745;
margin-top: 15px; display: inline-flex; align-items: center; gap: 5px;
}
.add-prompt-button:hover { background-color: #218838; }
.add-prompt-button .spinner-small {
width: 16px; height: 16px; border: 2px solid rgba(255,255,255,0.3);
border-left-color: #fff; border-radius: 50%; display: inline-block;
animation: spin 0.8s linear infinite;
}
@keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
.script-options { margin-bottom: 15px; padding: 10px; border: 1px solid #ccc; border-radius: 4px; background-color: #f9f9f9;}
.script-options label { margin-left: 5px; margin-right:5px; }
.script-options input[type="number"] { padding: 5px; width: 60px; border-radius: 3px; border: 1px solid #ccc; margin-left: 15px;}
.script-options div { margin-bottom: 8px; }
.preview-settings-group { border: 1px solid #ddd; padding: 15px; margin-bottom: 20px; border-radius: 5px; background-color: #fdfdfd; }
.preview-settings-group h3 { margin-top: 0; font-size: 1.1em; }
.preview-controls { text-align: center; margin-bottom: 10px; }
.preview-controls div { margin-bottom: 10px; display: flex; justify-content: center; align-items: center; flex-wrap: wrap; gap: 10px;}
.preview-controls label { margin-right: 5px; margin-left: 0px; white-space: nowrap; }
input[type="range"] { vertical-align: middle; width: 120px; }
.range-value-display { display: inline-block; width: 35px; text-align: right; vertical-align: middle; margin-left: 5px; font-weight: bold; }
select, input[type="color"], .toggle-button { padding: 6px; border-radius: 4px; border: 1px solid #ccc; vertical-align: middle;}
.toggle-button { background-color: #e0e0e0; cursor: pointer; }
.toggle-button.active { background-color: #4CAF50; color: white; }
.video-preview-area { margin: 20px auto; border: 2px dashed #007bff; background-color: #000; position: relative; display: flex; justify-content: center; align-items: center; overflow: hidden; }
.video-preview-area.aspect-16-9 { width: 800px; height: 450px; }
.video-preview-area.aspect-9-16 { width: 360px; height: 640px; }
#videoDisplayImage {
display: none; transition: opacity 0.3s ease-in-out; max-width: 100%; max-height: 100%;
object-fit: var(--video-image-object-fit); transform-origin: center center;
}
#noPreviewText { color: #ccc; }
#effectsCanvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; display: none; }
audio#mainAudioPlayer { width: 100%; margin-top: 10px; }
#highlightedTextOverlay { position: absolute; width: auto; max-width: 90%; padding: 0; box-sizing: border-box; pointer-events: none; z-index: 10; direction: ltr; }
.highlighted-text-span-container { display: inline-block; padding: 8px 15px; border-radius: 6px; }
.highlighted-text-word, .typewriter-char { display: inline-block; margin: 0 1px; color: white; opacity: 0; }
.font-knewave .highlighted-text-word, .font-knewave .typewriter-char { font-family: "Knewave", system-ui, sans-serif; font-size: calc(var(--base-font-size) * 1.8); letter-spacing: 1px; }
.font-roboto .highlighted-text-word, .font-roboto .typewriter-char { font-family: "Roboto", sans-serif; font-size: var(--base-font-size); font-weight: 700; }
.font-lobster .highlighted-text-word, .font-lobster .typewriter-char { font-family: "Lobster", cursive, sans-serif; font-size: calc(var(--base-font-size) * 1.3); }
.effect-typewriter-pro .highlighted-text-word { opacity: 1; transform: translateY(0); }
.typewriter-char { transform: translateY(5px) scale(0.8); transition: opacity 0.1s ease-out, transform 0.1s ease-out; }
.typewriter-char.visible { opacity: 1; transform: translateY(0) scale(1); }
.effect-word-slide-fade .highlighted-text-word { opacity: 0; transform: translateY(20px) scale(0.9); transition: opacity 0.4s ease-out, transform 0.4s ease-out; }
.effect-word-slide-fade .highlighted-text-word.visible { opacity: 1; transform: translateY(0) scale(1); }
.effect-fade-scale .highlighted-text-word { opacity: 0; transform: scale(0.5); transition: opacity 0.4s ease-out, transform 0.4s ease-out; }
.effect-fade-scale .highlighted-text-word.visible { opacity: 1; transform: scale(1); }
#shortsActionContainer { display:none; }
#shortsPreviewArea { display:none; margin-top:20px; border-top: 2px solid #0056b3; padding-top:15px;}
#shortsNavigation { text-align:center; margin-bottom:10px; }
#shortsNavigation .arrow-button, #shortsNavigation button { margin: 0 8px;}
#imageFitControlContainer { display: none; margin-top: 10px; text-align: center; }
#imageZoomControlContainer { display: none; margin-top: 10px; text-align: center; }
/* أنماط خاصة بقسم التسجيل */
#recordingSettingsArea .form-group { display: flex; align-items: center; margin-bottom: 10px; }
#recordingSettingsArea .form-group label { white-space: nowrap; margin-left: 10px; }
#recordingSettingsArea .form-group input[type="number"],
#recordingSettingsArea .form-group select { padding: 5px; width: auto; min-width: 70px; }
#recordingSettingsArea small { margin-left: 10px; font-size: 0.85em; color: #555; }
</style>
</head>
<body>
<div class="container">
<h1>أداة إنشاء فيديو - مزامنة متقدمة وتسجيل</h1>
<div class="api-warning">
<strong>تحذير أمان هام:</strong> مفاتيح API (ElevenLabs و Gemini) موجودة في هذا الكود وهي ظاهرة لأي شخص يطلع على مصدر الصفحة. هذا على مسؤوليتك الشخصية.
<br>
<strong>لا تشارك هذا الكود علنًا مع وجود المفاتيح!</strong>
</div>
<h2>1. أدخل نص الفيديو (بالإنجليزية، استخدم "" لتمييز النص):</h2>
<textarea id="videoScript" placeholder='Example: This is "The Quiet Conqueror" an amazing text. نهاية المشهد 1 A new scene begins with "a beautiful sunrise".'></textarea>
<div class="script-options">
<div><label><strong>عدد مقترحات الصور لكل مشهد (للإنشاء الأولي):</strong></label></div>
<div>
<input type="radio" id="promptCountManual" name="promptCountMode" value="manual" checked>
<label for="promptCountManual">تحديد يدوي:</label>
<input type="number" id="numPromptsInput" value="3" min="1" max="8">
</div>
<div>
<input type="radio" id="promptCountAuto" name="promptCountMode" value="auto">
<label for="promptCountAuto">اقتراح تلقائي بواسطة AI (بحد أقصى 8)</label>
</div>
</div>
<button onclick="parseScriptAndCreateSceneInputs()"><i class="fas fa-pencil-alt"></i> تحليل النص وإنشاء الحقول</button>
<div id="scenesInputArea"></div>
<div class="preview-settings-group">
<h3>إعدادات معاينة الفيديو</h3>
<div class="preview-controls">
<div>
<label>مقاس الفيديو:</label>
<input type="radio" id="aspect16_9" name="aspectRatio" value="16:9" checked onchange="updatePreviewAspectRatio()"> <label for="aspect16_9">16:9</label>
<input type="radio" id="aspect9_16" name="aspectRatio" value="9:16" onchange="updatePreviewAspectRatio()"> <label for="aspect9_16">9:16</label>
</div>
<div>
<label for="shakeIntensitySlider">قوة الاهتزاز:</label>
<input type="range" id="shakeIntensitySlider" min="0" max="10" value="2">
<span id="shakeValueDisplay" class="range-value-display">2</span>
</div>
</div>
<div id="imageFitControlContainer">
<div>
<label for="imageFitSelect">طريقة ملء الصورة (للـ Shorts):</label>
<select id="imageFitSelect">
<option value="cover" selected>ملء (Cover)</option>
<option value="contain">احتواء (Contain)</option>
<option value="fill">تمدد (Fill)</option>
<option value="scale-down">تصغير ليناسب (Scale Down)</option>
</select>
</div>
<div id="imageZoomControlContainer" style="margin-top: 10px;">
<label for="imageZoomSlider">تكبير/تصغير الصورة (للـ Shorts):</label>
<input type="range" id="imageZoomSlider" min="1.0" max="3" step="0.1" value="1.3">
<span id="imageZoomValueDisplay" class="range-value-display">1.3x</span>
</div>
</div>
</div>
<div class="preview-settings-group">
<h3>تأثير الغبار والخدوش</h3>
<div class="preview-controls">
<div>
<label>لون التأثير:</label>
<input type="radio" id="effectColorBlack" name="dustScratchColor" value="black" checked> <label for="effectColorBlack">أسود</label>
<input type="radio" id="effectColorWhite" name="dustScratchColor" value="white"> <label for="effectColorWhite">أبيض</label>
</div>
<div>
<label for="dustIntensitySlider">كثافة الغبار:</label>
<input type="range" id="dustIntensitySlider" min="0" max="100" value="100">
<span id="dustValueDisplay" class="range-value-display">100</span>
</div>
<div>
<label for="scratchIntensitySlider">كثافة الخدوش:</label>
<input type="range" id="scratchIntensitySlider" min="0" max="50" value="50">
<span id="scratchValueDisplay" class="range-value-display">50</span>
</div>
</div>
</div>
<div class="preview-settings-group">
<h3>إعدادات النص المميز</h3>
<div class="preview-controls">
<div>
<label for="highlightFontFamily">نمط الخط:</label>
<select id="highlightFontFamily">
<option value="font-knewave" selected>Knewave</option>
<option value="font-roboto">Roboto</option>
<option value="font-lobster">Lobster</option>
<option value="font-cairo">Cairo (عربي)</option>
</select>
</div>
<div>
<label for="highlightAnimationEffect">تأثير الدخول:</label>
<select id="highlightAnimationEffect">
<option value="effect-typewriter-pro" selected>كتابة متقدمة</option>
<option value="effect-word-slide-fade">انزلاق كلمة</option>
<option value="effect-fade-scale">تلاشي وتكبير كلمة</option>
</select>
</div>
</div>
<div class="preview-controls">
<div>
<label for="highlightTextColor">لون النص:</label>
<input type="color" id="highlightTextColor" value="#babd04">
</div>
<div>
<label>خلفية النص:</label>
<button id="toggleTextBackground" class="toggle-button">تشغيل</button>
<input type="color" id="highlightBgColor" value="#D32F2F" style="margin-left: 5px;">
<label for="highlightBgOpacity" style="margin-left:10px;">الشفافية:</label>
<input type="range" id="highlightBgOpacity" min="0" max="1" step="0.05" value="0.75">
</div>
</div>
<div class="preview-controls" id="typewriterSpeedControl" style="display: flex;">
<label for="typewriterSpeedSlider">سرعة الكتابة:</label>
<input type="range" id="typewriterSpeedSlider" min="0.5" max="2.5" step="0.1" value="1">
<span id="typewriterSpeedDisplay" class="range-value-display">1.0x</span>
</div>
<div class="preview-controls">
<div>
<label>عرض النص المميز:</label>
<button id="toggleHighlightTextButton" class="toggle-button active">إيقاف</button>
</div>
</div>
<hr>
<div class="preview-controls">
<div>
<label>الموضع الرأسي الأساسي:</label>
<select id="textBaseVerticalPosition">
<option value="top">أعلى</option>
<option value="center">وسط</option>
<option value="bottom" selected>أسفل</option>
</select>
</div>
<div>
<label>تعديل الموضع الرأسي:</label>
<button id="textNudgeUp" class="arrow-button">▲</button>
<button id="textNudgeDown" class="arrow-button">▼</button>
<span id="textVerticalOffsetDisplay" class="range-value-display">0px</span>
</div>
</div>
<div class="preview-controls">
<div>
<label>المحاذاة الأفقية:</label>
<select id="textHorizontalAlign">
<option value="left">يسار</option>
<option value="center" selected>وسط</option>
<option value="right">يمين</option>
</select>
</div>
<div>
<label for="textScaleSlider">حجم النص (مقياس):</label>
<input type="range" id="textScaleSlider" min="0.5" max="2.5" step="0.05" value="1.0"> <!-- تم تعديل القيمة الافتراضية إلى 1.0 -->
<span id="textScaleDisplay" class="range-value-display">1.0x</span>
</div>
</div>
</div>
<div class="preview-settings-group">
<h3>تحديد وقت الصورة الافتراضي (في حال عدم وجود صوت)</h3>
<div class="preview-controls">
<label for="duration">زمن عرض الصورة الافتراضي (ثانية):</label>
<input type="number" id="duration" value="3" min="1" max="15" style="padding:6px; border-radius:4px; border:1px solid #ccc; width:70px; text-align:center;">
</div>
</div>
<!-- قسم إعدادات تسجيل وتنزيل الفيديو كـ MP4 -->
<div class="preview-settings-group" id="recordingSettingsArea" style="display:none;">
<h3><i class="fas fa-cogs"></i> إعدادات تسجيل وتنزيل الفيديو</h3>
<div class="form-group">
<label for="recordVideoWidth">عرض الفيديو (بكسل):</label>
<input type="number" id="recordVideoWidth" value="1920" min="100" step="10" required>
<label for="recordVideoHeight">ارتفاع الفيديو (بكسل):</label>
<input type="number" id="recordVideoHeight" value="1080" min="100" step="10" required>
</div>
<input type="hidden" id="recordFps" value="30">
<input type="hidden" id="recordBitrate" value="12000">
<div class="form-group">
<label for="recordVideoFormat">تنسيق التصدير الافتراضي:</label>
<select id="recordVideoFormat">
<option value="video/mp4; codecs=avc1">MP4 (H.264) - متوافق جداً</option>
<option value="video/webm; codecs=vp9">WebM (VP9) - دقة فائقة</option>
<option value="video/webm; codecs=vp8">WebM (VP8) - توافق متوسط</option>
</select>
<small style="margin-left: 15px;">معدل الإطارات: 30 FPS. معدل البت: 12000 Kbps.</small>
</div>
<button id="startRecordingButton" style="background-color: #e67e22; width:auto; padding: 10px 20px; display: block; margin: 10px auto;" disabled>
<i class="fas fa-video"></i> بدء رندرة وتصدير الفيديو بصيغة MP4
</button>
<div id="recordingStatus" class="status" style="margin-top:10px; font-weight:normal;"></div>
<div id="recordingProgressBarContainer" style="width: 100%; background-color: #e0e0e0; border-radius: 5px; margin-top: 10px; display: none; height:22px;">
<div id="recordingProgressBar" style="width: 0%; height: 100%; background-color: #e67e22; text-align: center; line-height: 22px; color: white; border-radius: 5px; transition: width 0.1s linear;">0%</div>
</div>
<a download="my_recorded_video.mp4" href="#" id="downloadVideoLink" style="display:none; margin-top: 15px; background-color: #2ecc71; color: white; padding: 10px 18px; text-decoration: none; border-radius: 5px; font-size: 1em; text-align:center; display:none; width: fit-content; margin-left: auto; margin-right: auto;">
<i class="fas fa-download"></i> تحميل الفيديو الناتج (MP4)
</a>
</div>
<div style="text-align: center;">
<button id="generateVideoButton" onclick="prepareAndPlayFullPreview()" style="display:none; margin-top: 20px; background-color: #28a745;">
<i class="fas fa-play"></i> بدء إنشاء ومعاينة الفيديو
</button>
<button id="replayButton" onclick="replayPreview()" style="display: none;">
<i class="fas fa-redo"></i> إعادة التشغيل
</button>
</div>
<div id="status" class="status"></div>
<div style="text-align: center; margin-top: 15px;" id="shortsActionContainer">
<button id="generateShortsButton" onclick="switchToShortsMode()">إنشاء معاينات Shorts</button>
</div>
<h2>معاينة الفيديو:</h2>
<div class="video-preview-area aspect-16-9" id="videoPreviewArea">
<img alt="معاينة المشهد" id="videoDisplayImage">
<canvas id="effectsCanvas"></canvas>
<div id="highlightedTextOverlay"></div>
<p id="noPreviewText">سيتم عرض الصور هنا</p>
</div>
<audio id="mainAudioPlayer" controls style="display:none;"></audio>
<div id="shortsPreviewArea">
<h3>معاينات Shorts (مشهد تلو الآخر):</h3>
<div id="shortsNavigation">
</div>
</div>
</div>
<audio id="typingSoundEffect" src="https://cdn.freesound.org/previews/254/254336_4619022-lq.mp3" loop style="display:none;"></audio>
<canvas id="recordingCanvas" style="display: none; border:1px solid red;"></canvas>
<script>
// --- هام جداً: مفاتيح API ---
const XI_API_KEY = "sk_2e1099f2b404eac7de6732d3055a9ea9af54666f208d70fb";
const GEMINI_API_KEY = "AIzaSyBUdo1qj5L4CKzOO47935nKPPe8mMZ2Ha0";
const GEMINI_MODEL_NAME = "gemini-1.5-flash";
const GEMINI_IMAGE_PROMPT_PREFIX = "I want Stick man with pencil line and full black background the image size to be 1280 x 720 ";
const MAX_AI_SUGGESTED_PROMPTS = 8;
const VOICE_ID = "iP95p4xoKVk53GoZ742B";
const SCENE_SEPARATOR_REGEX = /نهاية المشهد \d+/i;
const HIGHLIGHT_TEXT_REGEX = /"([^"]+)"/g;
const PRE_TYPE_DELAY_SECONDS = 0.45;
const VERTICAL_NUDGE_STEP = 5;
const DEFAULT_SHORTS_ZOOM = 1.3;
// --- عناصر DOM ---
const videoScriptInput = document.getElementById('videoScript');
const scenesInputArea = document.getElementById('scenesInputArea');
const generateVideoButton = document.getElementById('generateVideoButton');
const replayButton = document.getElementById('replayButton');
const statusDiv = document.getElementById('status');
const videoPreviewArea = document.getElementById('videoPreviewArea');
const videoDisplayImage = document.getElementById('videoDisplayImage');
const mainAudioPlayer = document.getElementById('mainAudioPlayer');
const noPreviewText = document.getElementById('noPreviewText');
const highlightedTextOverlay = document.getElementById('highlightedTextOverlay');
const shakeIntensitySlider = document.getElementById('shakeIntensitySlider');
const shakeValueDisplay = document.getElementById('shakeValueDisplay');
const effectsCanvas = document.getElementById('effectsCanvas');
const ctxEffects = effectsCanvas.getContext('2d');
const dustIntensitySlider = document.getElementById('dustIntensitySlider');
const dustValueDisplay = document.getElementById('dustValueDisplay');
const scratchIntensitySlider = document.getElementById('scratchIntensitySlider');
const scratchValueDisplay = document.getElementById('scratchValueDisplay');
const highlightFontFamilySelect = document.getElementById('highlightFontFamily');
const highlightAnimationEffectSelect = document.getElementById('highlightAnimationEffect');
const highlightTextColorInput = document.getElementById('highlightTextColor');
const toggleTextBackgroundButton = document.getElementById('toggleTextBackground');
const highlightBgColorInput = document.getElementById('highlightBgColor');
const highlightBgOpacityInput = document.getElementById('highlightBgOpacity');
const typewriterSpeedControlDiv = document.getElementById('typewriterSpeedControl');
const typewriterSpeedSlider = document.getElementById('typewriterSpeedSlider');
const typewriterSpeedDisplay = document.getElementById('typewriterSpeedDisplay');
const textBaseVerticalPositionSelect = document.getElementById('textBaseVerticalPosition');
const textNudgeUpButton = document.getElementById('textNudgeUp');
const textNudgeDownButton = document.getElementById('textNudgeDown');
const textVerticalOffsetDisplay = document.getElementById('textVerticalOffsetDisplay');
const textHorizontalAlignSelect = document.getElementById('textHorizontalAlign');
const textScaleSlider = document.getElementById('textScaleSlider');
const textScaleDisplay = document.getElementById('textScaleDisplay');
const typingSound = document.getElementById('typingSoundEffect');
const toggleHighlightTextButton = document.getElementById('toggleHighlightTextButton');
const shortsActionContainer = document.getElementById('shortsActionContainer');
const generateShortsButton = document.getElementById('generateShortsButton');
const shortsPreviewArea = document.getElementById('shortsPreviewArea');
const shortsNavigation = document.getElementById('shortsNavigation');
const imageFitControlContainer = document.getElementById('imageFitControlContainer');
const imageFitSelect = document.getElementById('imageFitSelect');
const imageZoomControlContainer = document.getElementById('imageZoomControlContainer');
const imageZoomSlider = document.getElementById('imageZoomSlider');
const imageZoomValueDisplay = document.getElementById('imageZoomValueDisplay');
// عناصر DOM للتسجيل
const recordingSettingsArea = document.getElementById('recordingSettingsArea');
const recordVideoWidthInput = document.getElementById('recordVideoWidth');
const recordVideoHeightInput = document.getElementById('recordVideoHeight');
const recordFpsInput = document.getElementById('recordFps');
const recordBitrateInput = document.getElementById('recordBitrate');
const recordVideoFormatSelect = document.getElementById('recordVideoFormat');
const startRecordingButton = document.getElementById('startRecordingButton');
const recordingStatusDiv = document.getElementById('recordingStatus');
const recordingProgressBarContainer = document.getElementById('recordingProgressBarContainer');
const recordingProgressBar = document.getElementById('recordingProgressBar');
const downloadVideoLink = document.getElementById('downloadVideoLink');
const recordingCanvas = document.getElementById('recordingCanvas');
const ctxRecording = recordingCanvas.getContext('2d', { alpha: false });
// --- متغيرات الحالة ---
let shakeIntensity = parseInt(shakeIntensitySlider.value);
let shakeIntervalId = null;
let dustScratchColor = 'black';
let dustIntensity = parseInt(dustIntensitySlider.value);
let scratchIntensity = parseInt(scratchIntensitySlider.value);
let effectsIntervalId = null;
let scenesData = [];
let combinedAudioBlobs = [];
let fullAudioUrlForReplay = null;
let originalCombinedAudioSrcForFullPreview = null;
let currentSceneForPlayback = 0;
let currentImageInSceneIndex = 0;
let sceneStartTime = 0;
let imageChangeInterval;
let activeHighlightTimeoutId = null;
let typewriterIntervalId = null;
let currentHighlightFontFamily = highlightFontFamilySelect.value;
let currentHighlightAnimationEffect = highlightAnimationEffectSelect.value;
let currentHighlightTextColor = highlightTextColorInput.value;
let textBackgroundEnabled = false;
let currentHighlightBgColor = highlightBgColorInput.value;
let currentHighlightBgOpacity = parseFloat(highlightBgOpacityInput.value);
let typewriterSpeedFactor = parseFloat(typewriterSpeedSlider.value);
let currentTextBaseVerticalPosition = textBaseVerticalPositionSelect.value;
let currentTextVerticalOffset = 0;
let currentTextHorizontalAlign = textHorizontalAlignSelect.value;
let currentTextScaleFactor = parseFloat(textScaleSlider.value);
let allHighlightEvents = [];
let nextHighlightEventIndex = 0;
let promptItemCounter = 0;
let highlightTextEnabled = true;
let isInShortsMode = false;
let currentShortSceneIndex = 0;
let currentImageFitMode = 'cover';
let currentImageZoomFactor = DEFAULT_SHORTS_ZOOM;
// متغيرات للتسجيل الفعلي
let mediaRecorderInstance = null;
let recordedChunks = [];
let isVideoRecordingActive = false;
let recordingFrameRequestId = null;
let framesRecordedCount = 0;
const preloadedImageObjects = {};
let currentTextForRecording = null;
let currentTextStartTimeForRecording = 0;
let currentTextDurationForRecording = 0;
let textAnimationStateForRecording = {};
let isCurrentlyPreloadingImages = false;
// دالة برمجية ممتازة لتوليد مسار صوتي صامت في الخلفية عند تعطل ElevenLabs
function createSilentAudioBlob(durationSec) {
const sampleRate = 8000;
const numSamples = Math.floor(sampleRate * durationSec);
const buffer = new Uint8Array(44 + numSamples);
const writeString = (offset, string) => {
for (let i = 0; i < string.length; i++) {
buffer[offset + i] = string.charCodeAt(i);
}
};
const writeUint32 = (offset, value) => {
buffer[offset] = value & 0xff;
buffer[offset + 1] = (value >> 8) & 0xff;
buffer[offset + 2] = (value >> 16) & 0xff;
buffer[offset + 3] = (value >> 24) & 0xff;
};
const writeUint16 = (offset, value) => {
buffer[offset] = value & 0xff;
buffer[offset + 1] = (value >> 8) & 0xff;
};
writeString(0, 'RIFF');
writeUint32(4, 36 + numSamples);
writeString(8, 'WAVE');
writeString(12, 'fmt ');
writeUint32(16, 16);
writeUint16(20, 1); // PCM
writeUint16(22, 1); // Mono
writeUint32(24, sampleRate);
writeUint32(28, sampleRate);
writeUint16(32, 1); // Block align
writeUint16(34, 8); // Bits per sample
writeString(36, 'data');
writeUint32(40, numSamples);
for (let i = 0; i < numSamples; i++) {
buffer[44 + i] = 128; // Silence level for 8-bit PCM
}
return new Blob([buffer], { type: 'audio/wav' });
}
// --- Event Listeners ---
document.querySelectorAll('input[name="promptCountMode"]').forEach(radio => {
radio.addEventListener('change', function() {
const numPromptsInputEl = document.getElementById('numPromptsInput');
numPromptsInputEl.disabled = (this.value === 'auto');
numPromptsInputEl.style.backgroundColor = (this.value === 'auto') ? '#eee' : '#fff';
if (this.value === 'auto') {
numPromptsInputEl.value = MAX_AI_SUGGESTED_PROMPTS;
}
});
});
shakeIntensitySlider.addEventListener('input', () => { shakeIntensity = parseInt(shakeIntensitySlider.value); shakeValueDisplay.textContent = shakeIntensity; });
document.querySelectorAll('input[name="dustScratchColor"]').forEach(radio => { radio.addEventListener('change', function() { dustScratchColor = this.value; }); });
dustIntensitySlider.addEventListener('input', () => { dustIntensity = parseInt(dustIntensitySlider.value); dustValueDisplay.textContent = dustIntensity; });
scratchIntensitySlider.addEventListener('input', () => { scratchIntensity = parseInt(scratchIntensitySlider.value); scratchValueDisplay.textContent = scratchIntensity; });
highlightFontFamilySelect.addEventListener('change', (e) => {
currentHighlightFontFamily = e.target.value;
document.documentElement.style.setProperty('--base-font-size', `${1.5 * currentTextScaleFactor}em`);
});
highlightAnimationEffectSelect.addEventListener('change', (e) => {
currentHighlightAnimationEffect = e.target.value;
typewriterSpeedControlDiv.style.display = (currentHighlightAnimationEffect === 'effect-typewriter-pro') ? 'flex' : 'none';
if (typingSound && currentHighlightAnimationEffect !== 'effect-typewriter-pro') { typingSound.pause(); }
});
highlightTextColorInput.addEventListener('input', (e) => currentHighlightTextColor = e.target.value);
toggleTextBackgroundButton.addEventListener('click', () => {
textBackgroundEnabled = !textBackgroundEnabled;
toggleTextBackgroundButton.textContent = textBackgroundEnabled ? 'إيقاف' : 'تشغيل';
toggleTextBackgroundButton.classList.toggle('active', textBackgroundEnabled);
});
highlightBgColorInput.addEventListener('input', (e) => currentHighlightBgColor = e.target.value);
highlightBgOpacityInput.addEventListener('input', (e) => currentHighlightBgOpacity = parseFloat(e.target.value));
typewriterSpeedSlider.addEventListener('input', (e) => {
typewriterSpeedFactor = parseFloat(e.target.value);
typewriterSpeedDisplay.textContent = `${typewriterSpeedFactor.toFixed(1)}x`;
});
textBaseVerticalPositionSelect.addEventListener('change', (e) => currentTextBaseVerticalPosition = e.target.value);
textHorizontalAlignSelect.addEventListener('change', (e) => currentTextHorizontalAlign = e.target.value);
textScaleSlider.addEventListener('input', (e) => {
currentTextScaleFactor = parseFloat(e.target.value);
textScaleDisplay.textContent = `${currentTextScaleFactor.toFixed(1)}x`;
document.documentElement.style.setProperty('--base-font-size', `${1.5 * currentTextScaleFactor}em`);
});
textNudgeUpButton.addEventListener('click', () => {
currentTextVerticalOffset -= VERTICAL_NUDGE_STEP;
textVerticalOffsetDisplay.textContent = `${currentTextVerticalOffset}px`;
});
textNudgeDownButton.addEventListener('click', () => {
currentTextVerticalOffset += VERTICAL_NUDGE_STEP;
textVerticalOffsetDisplay.textContent = `${currentTextVerticalOffset}px`;
});
toggleHighlightTextButton.addEventListener('click', () => {
highlightTextEnabled = !highlightTextEnabled;
toggleHighlightTextButton.textContent = highlightTextEnabled ? 'إيقاف' : 'تشغيل';
toggleHighlightTextButton.classList.toggle('active', highlightTextEnabled);
if (!highlightTextEnabled) {
highlightedTextOverlay.innerHTML = '';
clearActiveHighlightTimeout();
if (typingSound && !typingSound.paused) typingSound.pause();
} else {
if (mainAudioPlayer && !mainAudioPlayer.paused && fullAudioUrlForReplay) {
startHighlightedTextScheduler();
}
}
});
imageFitSelect.addEventListener('change', (e) => {
currentImageFitMode = e.target.value;
if (isInShortsMode) {
document.documentElement.style.setProperty('--video-image-object-fit', currentImageFitMode);
}
});
imageZoomSlider.addEventListener('input', (e) => {
currentImageZoomFactor = parseFloat(e.target.value);
imageZoomValueDisplay.textContent = `${currentImageZoomFactor.toFixed(1)}x`;
if (isInShortsMode && videoDisplayImage.style.display === 'block') {
if (shakeIntervalId) {
applyShakeEffect();
} else {
videoDisplayImage.style.transform = `scale(${currentImageZoomFactor})`;
}
}
});
mainAudioPlayer.addEventListener('play', () => {
if (isVideoRecordingActive) return;
if (scenesData.length > 0 && fullAudioUrlForReplay) {
if (isInShortsMode) {
startHighlightedTextScheduler();
startShakeEffect();
startDustAndScratchesEffect();
if (videoDisplayImage.style.display === 'block') effectsCanvas.style.display = 'block';
startOrContinueImageLoopForShorts();
} else {
syncSceneToAudioTime();
startHighlightedTextScheduler();
startShakeEffect();
startDustAndScratchesEffect();
if (videoDisplayImage.style.display === 'block') effectsCanvas.style.display = 'block';
startOrContinueImageLoop();
}
replayButton.style.display = 'none';
generateVideoButton.disabled = true;
}
});
mainAudioPlayer.addEventListener('pause', () => {
if (isVideoRecordingActive) return;
clearActiveHighlightTimeout();
if (typingSound && !typingSound.paused) typingSound.pause();
});
mainAudioPlayer.addEventListener('ended', () => {
if (isVideoRecordingActive) return;
statusDiv.textContent = isInShortsMode ? `انتهت معاينة Short للمشهد ${currentShortSceneIndex + 1}` : "انتهت المعاينة!";
stopCurrentPlaybackAndEffects();
if (!isInShortsMode) {
generateVideoButton.disabled = false;
replayButton.style.display = 'inline-block';
shortsActionContainer.style.display = scenesData.length > 0 ? 'block' : 'none';
startRecordingButton.disabled = !(originalCombinedAudioSrcForFullPreview && scenesData.length > 0 && !isInShortsMode);
} else {
renderShortsNavigation();
}
});
mainAudioPlayer.addEventListener('seeking', () => {
if (isVideoRecordingActive) return;
clearActiveHighlightTimeout();
highlightedTextOverlay.innerHTML = '';
if (typingSound && !typingSound.paused) typingSound.pause();
});
mainAudioPlayer.addEventListener('seeked', () => {
if (isVideoRecordingActive) return;
if (scenesData.length > 0 && fullAudioUrlForReplay) {
if (isInShortsMode) {
startHighlightedTextScheduler();
const scene = scenesData[currentShortSceneIndex];
if (scene && scene.images && scene.images.length > 0 && scene.audioDuration > 0) {
const durationPerImage = scene.audioDuration / scene.images.length;
currentImageInSceneIndex = Math.floor(mainAudioPlayer.currentTime / durationPerImage);
currentImageInSceneIndex = Math.max(0, Math.min(currentImageInSceneIndex, scene.images.length - 1));
displayCurrentImageForShorts();
}
} else {
syncSceneToAudioTime();
startHighlightedTextScheduler();
}
}
});
startRecordingButton.addEventListener('click', handleStartRecordingVideo);
function updatePreviewAspectRatio() {
const selectedRatio = document.querySelector('input[name="aspectRatio"]:checked').value;
videoPreviewArea.className = 'video-preview-area';
let pw, ph;
if (selectedRatio === "16:9") {
videoPreviewArea.classList.add('aspect-16-9');
pw = 800; ph = 450;
document.documentElement.style.setProperty('--video-image-object-fit', 'contain');
imageFitControlContainer.style.display = 'none';
imageZoomControlContainer.style.display = 'none';
if (videoDisplayImage) videoDisplayImage.style.transform = 'scale(1)';
} else {
videoPreviewArea.classList.add('aspect-9-16');
pw = 360; ph = 640;
document.documentElement.style.setProperty('--video-image-object-fit', currentImageFitMode);
imageFitControlContainer.style.display = 'block';
imageZoomControlContainer.style.display = 'block';
imageZoomSlider.value = currentImageZoomFactor;
imageZoomValueDisplay.textContent = `${currentImageZoomFactor.toFixed(1)}x`;
if (videoDisplayImage.style.display === 'block' && isInShortsMode) {
videoDisplayImage.style.transform = `scale(${currentImageZoomFactor})`;
}
}
effectsCanvas.width = pw; effectsCanvas.height = ph;
if (highlightedTextOverlay.innerHTML !== '') {
applyTextPositionAndScale(highlightedTextOverlay);
}
}
document.addEventListener('DOMContentLoaded', () => {
updatePreviewAspectRatio();
shakeValueDisplay.textContent = shakeIntensitySlider.value;
dustValueDisplay.textContent = dustIntensitySlider.value;
scratchValueDisplay.textContent = scratchIntensitySlider.value;
dustScratchColor = document.querySelector('input[name="dustScratchColor"]:checked').value;
highlightTextColorInput.value = '#babd04'; currentHighlightTextColor = highlightTextColorInput.value;
textBackgroundEnabled = false; toggleTextBackgroundButton.classList.remove('active'); toggleTextBackgroundButton.textContent = 'تشغيل';
currentTextScaleFactor = parseFloat(textScaleSlider.value);
textScaleDisplay.textContent = `${currentTextScaleFactor.toFixed(1)}x`;
document.documentElement.style.setProperty('--base-font-size', `${1.5 * currentTextScaleFactor}em`);
textBaseVerticalPositionSelect.value = 'bottom'; currentTextBaseVerticalPosition = textBaseVerticalPositionSelect.value; textVerticalOffsetDisplay.textContent = `${currentTextVerticalOffset}px`;
currentHighlightFontFamily = highlightFontFamilySelect.value; currentHighlightAnimationEffect = highlightAnimationEffectSelect.value;
typewriterSpeedControlDiv.style.display = (currentHighlightAnimationEffect === 'effect-typewriter-pro') ? 'flex' : 'none';
typewriterSpeedDisplay.textContent = `${parseFloat(typewriterSpeedSlider.value).toFixed(1)}x`;
const numPromptsInputEl = document.getElementById('numPromptsInput');
const promptCountModeAuto = document.getElementById('promptCountAuto').checked;
numPromptsInputEl.disabled = promptCountModeAuto;
numPromptsInputEl.style.backgroundColor = promptCountModeAuto ? '#eee' : '#fff';
if(promptCountModeAuto) numPromptsInputEl.value = MAX_AI_SUGGESTED_PROOPTS || MAX_AI_SUGGESTED_PROMPTS;
document.getElementById('numPromptsInput').max = MAX_AI_SUGGESTED_PROMPTS;
toggleHighlightTextButton.textContent = highlightTextEnabled ? 'إيقاف' : 'تشغيل';
toggleHighlightTextButton.classList.toggle('active', highlightTextEnabled);
imageFitSelect.value = currentImageFitMode;
imageZoomSlider.value = currentImageZoomFactor;
imageZoomValueDisplay.textContent = `${currentImageZoomFactor.toFixed(1)}x`;
startRecordingButton.disabled = true;
recordingSettingsArea.style.display = 'none';
});
function parseScriptAndCreateSceneInputs() {
if (isInShortsMode) { returnToFullPreviewMode(); }
stopCurrentPlaybackAndEffects();
resetUIForNewScript();
if (fullAudioUrlForReplay && fullAudioUrlForReplay.startsWith('blob:')) { URL.revokeObjectURL(fullAudioUrlForReplay); fullAudioUrlForReplay = null; }
if (originalCombinedAudioSrcForFullPreview && originalCombinedAudioSrcForFullPreview.startsWith('blob:')) { URL.revokeObjectURL(originalCombinedAudioSrcForFullPreview); originalCombinedAudioSrcForFullPreview = null; }
const scriptText = videoScriptInput.value.trim();
const promptCountMode = document.querySelector('input[name="promptCountMode"]:checked').value;
let manualNumPrompts = parseInt(document.getElementById('numPromptsInput').value) || 3;
manualNumPrompts = Math.min(manualNumPrompts, MAX_AI_SUGGESTED_PROMPTS);
if (!scriptText) { statusDiv.textContent = "أدخل نص الفيديو."; statusDiv.className = 'status error'; return; }
scenesInputArea.innerHTML = ''; scenesData = []; allHighlightEvents = []; statusDiv.textContent = ''; combinedAudioBlobs = [];
const rawScenes = scriptText.split(SCENE_SEPARATOR_REGEX);
rawScenes.forEach((sceneFullText, index) => {
const trimmedSceneText = sceneFullText.trim();
if (trimmedSceneText) {
const sceneNumber = index + 1;
const highlightedTexts = [];
const sceneObject = {
id: `scene-${sceneNumber}`,
originalText: trimmedSceneText, textForTTS: "",
images: [], audioBlob: null, audioDuration: 0,
highlightedTexts: highlightedTexts,
originalImageSources: [], imagePrompts: []
};
sceneObject.textForTTS = trimmedSceneText.replace(HIGHLIGHT_TEXT_REGEX, (match, capturedText, offset) => {
const textBefore = trimmedSceneText.substring(0, offset);
const wordsBefore = textBefore.split(/\s+/).filter(Boolean).length;
highlightedTexts.push({ text: capturedText, wordsBefore: wordsBefore });
return capturedText;
});
scenesData.push(sceneObject);
const sceneDiv = document.createElement('div');
sceneDiv.className = 'scene-container';
sceneDiv.id = `scene-container-${sceneNumber}`;
sceneDiv.innerHTML = `
<h3>المشهد ${sceneNumber}</h3>
<p class="processing-message" id="audio-status-${sceneNumber}"></p>
<p><strong>النص (لـ TTS):</strong> ${sceneObject.textForTTS.substring(0,100)}...</p>
<label for="imageUpload-${sceneNumber}" class="file-input-label">اختر صور للمشهد (عام)</label>
<input type="file" id="imageUpload-${sceneNumber}" multiple accept="image/*" onchange="handleGenericImageUpload(event, ${sceneNumber})">
<div class="image-preview-container" id="preview-${sceneNumber}"></div>
<h4>مقترحات صور (AI - Gemini):</h4>
<div class="image-prompts-container" id="prompts-container-${sceneNumber}">
<p class="processing-message" id="prompts-status-${sceneNumber}">جاري إنشاء مقترحات الصور...</p>
</div>
<button class="add-prompt-button" id="add-prompt-btn-${sceneNumber}" onclick="addIntelligentPromptToScene(${sceneNumber}, this)">
<i class="fas fa-plus"></i> إضافة مقترح صورة ذكي
</button>
`;
scenesInputArea.appendChild(sceneDiv);
if (promptCountMode === 'auto') {
generateImagePromptsForScene(trimmedSceneText, sceneNumber, MAX_AI_SUGGESTED_PROMPTS, true);
} else {
generateImagePromptsForScene(trimmedSceneText, sceneNumber, manualNumPrompts, false);
}
}
});
if (scenesData.length > 0) {
statusDiv.textContent = `تم تحليل ${scenesData.length} مشاهد.`;
statusDiv.className = 'status';
generateVideoButton.style.display = 'inline-block';
recordingSettingsArea.style.display = 'block';
startRecordingButton.disabled = true;
} else {
statusDiv.textContent = "لم يتم العثور على مشاهد.";
statusDiv.className = 'status error';
recordingSettingsArea.style.display = 'none';
}
}
async function generateImagePromptsForScene(sceneText, sceneNumber, numPromptsRequested, autoDetermineAndGenerate) {
const sceneIndex = scenesData.findIndex(s => s.id === `scene-${sceneNumber}`);
if (sceneIndex === -1) return;
const promptsContainer = document.getElementById(`prompts-container-${sceneNumber}`);
const promptsStatus = document.getElementById(`prompts-status-${sceneNumber}`);
promptsContainer.innerHTML = '';
scenesData[sceneIndex].imagePrompts = [];
if (!GEMINI_API_KEY) {
promptsStatus.textContent = "خطأ: مفتاح Gemini API غير موجود.";
promptsStatus.className = 'status error';
promptsStatus.style.display = 'block';
return;
}
let finalPromptForGemini;
let numPromptsToGenerate = numPromptsRequested;
if (autoDetermineAndGenerate) {
promptsStatus.textContent = `جاري تحديد عدد المقترحات وإنشائها تلقائيًا للمشهد ${sceneNumber} (بحد أقصى ${MAX_AI_SUGGESTED_PROMPTS})...`;
promptsStatus.className = 'processing-message'; promptsStatus.style.display = 'block';
finalPromptForGemini = `Analyze the following scene description. Your goal is to break it down into a sequence of distinct visual moments that tell a story.
First, determine an optimal number of image prompts to represent these moments. This number should be between 1 and ${MAX_AI_SUGGESTED_PROMPTS}, based on the scene's complexity and narrative flow.
Output *only* this number on the first line, in the format:
Optimal Prompts: [Number]
Then, on subsequent new lines, generate exactly that [Number] of distinct and *sequential* image prompts.
These prompts should vividly describe each moment, ensuring they follow the narrative progression of the scene and align with potential spoken words.
*Each of these complete image prompts MUST start with the following exact prefix (do not alter it):*
"${GEMINI_IMAGE_PROMPT_PREFIX}"
Scene description to analyze and break down:
"${sceneText}"`;
} else {
numPromptsToGenerate = Math.min(numPromptsRequested, MAX_AI_SUGGESTED_PROMPTS);
promptsStatus.textContent = `جاري إنشاء ${numPromptsToGenerate} مقترحات صور متسلسلة للمشهد ${sceneNumber}...`;
promptsStatus.className = 'processing-message'; promptsStatus.style.display = 'block';
finalPromptForGemini = `I have a scene description and a mandatory prefix for image generation prompts.
Your task is to break down the scene into exactly ${numPromptsToGenerate} distinct and *sequential* visual moments or actions.
These prompts should vividly describe each moment, ensuring they follow the narrative progression of the scene and align with potential spoken words.
*Each of these ${numPromptsToGenerate} complete image prompts MUST start with the following exact prefix (do not alter it):*
"${GEMINI_IMAGE_PROMPT_PREFIX}"
Scene description to break down:
"${sceneText}"`;
}
try {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL_NAME}:generateContent?key=${GEMINI_API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: finalPromptForGemini }] }],
generationConfig: {
temperature: 0.65,
}
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(`فشل Gemini API: ${errorData.error?.message || response.statusText}`);
}
const data = await response.json();
if (!data.candidates || !data.candidates.length || !data.candidates[0].content?.parts?.length) {
let errorMsg = "استجابة Gemini API غير صالحة.";
throw new Error(errorMsg);
}
let generatedText = data.candidates[0].content.parts[0].text;
let extractedPrompts = [];
let actualPromptsGeneratedCount = 0;
if (autoDetermineAndGenerate) {
const lines = generatedText.split('\n').map(l => l.trim());
if (lines.length > 0) {
const countLine = lines.shift();
const match = countLine.match(/Optimal Prompts:\s*(\d+)/i);
if (match && match[1]) {
let suggestedCount = parseInt(match[1]);
numPromptsToGenerate = Math.min(suggestedCount, MAX_AI_SUGGESTED_PROMPTS);
extractedPrompts = lines.filter(p => p.startsWith(GEMINI_IMAGE_PROMPT_PREFIX)).slice(0, numPromptsToGenerate);
actualPromptsGeneratedCount = extractedPrompts.length;
promptsStatus.textContent = `تم إنشاء ${actualPromptsGeneratedCount} مقترحات.`;
} else {
lines.unshift(countLine);
extractedPrompts = lines.filter(p => p.startsWith(GEMINI_IMAGE_PROMPT_PREFIX)).slice(0, MAX_AI_SUGGESTED_PROMPTS);
actualPromptsGeneratedCount = extractedPrompts.length;
promptsStatus.textContent = `فشل التحديد التلقائي للعدد. تم إنشاء ${actualPromptsGeneratedCount} مقترحات.`;
}
}
} else {
extractedPrompts = generatedText.split('\n').map(p => p.trim()).filter(p => p.startsWith(GEMINI_IMAGE_PROMPT_PREFIX)).slice(0, numPromptsToGenerate);
actualPromptsGeneratedCount = extractedPrompts.length;
}
promptsContainer.innerHTML = '';
extractedPrompts.forEach(promptStr => {
scenesData[sceneIndex].imagePrompts.push(promptStr);
createPromptItemUI(promptStr, sceneNumber, promptsContainer);
});
if (extractedPrompts.length === 0) {
promptsContainer.innerHTML = "<p>لم يتمكن Gemini من إنشاء مقترحات.</p>";
promptsStatus.textContent = "لم يتم إنشاء مقترحات.";
promptsStatus.className = 'status error';
promptsStatus.style.display = 'block';
} else {
promptsStatus.style.display = 'none';
}
} catch (error) {
console.error(error);
promptsContainer.innerHTML = '';
promptsStatus.textContent = `خطأ: ${error.message}`;
promptsStatus.className = 'status error';
promptsStatus.style.display = 'block';
}
}
function createPromptItemUI(promptText, sceneNumber, containerElement) {
promptItemCounter++;
const uniquePromptId = `prompt-item-${sceneNumber}-${promptItemCounter}`;
const sceneIndex = scenesData.findIndex(s => s.id === `scene-${sceneNumber}`);
const promptDiv = document.createElement('div');
promptDiv.className = 'prompt-item';
promptDiv.id = uniquePromptId;
const textEditable = document.createElement('textarea');
textEditable.className = 'prompt-text-editable';
textEditable.value = promptText;
textEditable.rows = 3;
textEditable.oninput = (event) => {
if (sceneIndex !== -1) {
const promptItemElement = event.target.closest('.prompt-item');
if(promptItemElement){
const allPromptItems = Array.from(containerElement.querySelectorAll('.prompt-item'));
const promptDataIndex = allPromptItems.indexOf(promptItemElement);
if(promptDataIndex !== -1 && scenesData[sceneIndex].imagePrompts[promptDataIndex] !== undefined){
scenesData[sceneIndex].imagePrompts[promptDataIndex] = event.target.value;
}
}
}
};
const actionsAndControlsDiv = document.createElement('div');
actionsAndControlsDiv.className = 'prompt-item-controls';
const actionsDiv = document.createElement('div');
actionsDiv.className = 'prompt-actions';
const copyButton = document.createElement('button');
copyButton.className = 'copy-prompt-button';
copyButton.innerHTML = '<i class="fas fa-copy"></i> نسخ';
copyButton.onclick = () => {
navigator.clipboard.writeText(textEditable.value)
.then(() => {
const originalText = copyButton.innerHTML;
copyButton.innerHTML = '<i class="fas fa-check"></i> تم!';
setTimeout(() => { copyButton.innerHTML = originalText; }, 1500);
})
.catch(err => console.error(err));
};
const dropZoneId = `dropzone-${uniquePromptId}`;
const fileInputId = `fileinput-${uniquePromptId}`;
const dropZone = document.createElement('div');
dropZone.className = 'drop-zone';
dropZone.id = dropZoneId;
const dropZoneText = document.createElement('span');
dropZoneText.className = 'drop-zone-text-content';
dropZoneText.innerHTML = '<i class="fas fa-upload"></i> اسحب أو انقر';
dropZone.appendChild(dropZoneText);
const spinner = document.createElement('div');
spinner.className = 'spinner';
dropZone.appendChild(spinner);
const dropZoneActions = document.createElement('div');
dropZoneActions.className = 'drop-zone-actions';
dropZone.appendChild(dropZoneActions);
const hiddenFileInput = document.createElement('input');
hiddenFileInput.type = 'file';
hiddenFileInput.accept = 'image/*';
hiddenFileInput.style.display = 'none';
hiddenFileInput.id = fileInputId;
hiddenFileInput.onchange = (event) => {
if (event.target.files && event.target.files[0]) {
handleDroppedOrSelectedFile(event.target.files[0], sceneNumber, dropZone, dropZoneId);
}
};
dropZone.onclick = () => {
const currentPreview = dropZone.querySelector('.drop-zone-preview');
if (!currentPreview || currentPreview.style.display === 'none') {
hiddenFileInput.click();
}
};
dropZone.ondragover = (event) => { event.preventDefault(); dropZone.classList.add('dragover'); };
dropZone.ondragleave = () => { dropZone.classList.remove('dragover'); };
dropZone.ondrop = async (event) => {
event.preventDefault(); dropZone.classList.remove('dragover');
let fileToProcess = null;
if (event.dataTransfer.items) { for (let i = 0; i < event.dataTransfer.items.length; i++) { if (event.dataTransfer.items[i].kind === 'file' && event.dataTransfer.items[i].type.startsWith('image/')) { fileToProcess = event.dataTransfer.items[i].getAsFile(); break; }}}
else if (event.dataTransfer.files && event.dataTransfer.files.length > 0) { for (let i = 0; i < event.dataTransfer.files.length; i++) { if (event.dataTransfer.files[i].type.startsWith('image/')) { fileToProcess = event.dataTransfer.files[i]; break; }}}
if (fileToProcess) { await handleDroppedOrSelectedFile(fileToProcess, sceneNumber, dropZone, dropZoneId); }
else {
const htmlData = event.dataTransfer.getData('text/html'); const urlData = event.dataTransfer.getData('text/uri-list') || event.dataTransfer.getData('URL'); let imageUrl = null;
if (htmlData) { const doc = new DOMParser().parseFromString(htmlData, 'text/html'); const imgTag = doc.querySelector('img'); if (imgTag && imgTag.src) { imageUrl = imgTag.src; }}
if (!imageUrl && urlData) { imageUrl = urlData.split('\n')[0].trim(); }
if (imageUrl) {
dropZone.querySelector('.spinner').style.display = 'block'; dropZone.querySelector('.drop-zone-text-content').innerHTML = '';
try {
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(imageUrl)}`; const response = await fetch(proxyUrl);
if (!response.ok) throw new Error();
const blob = await response.blob(); if (!blob.type.startsWith('image/')) throw new Error();
const filename = 'dropped-image.png'; const fetchedFile = new File([blob], filename, { type: blob.type });
await handleDroppedOrSelectedFile(fetchedFile, sceneNumber, dropZone, dropZoneId);
} catch (fetchError) {
dropZone.querySelector('.drop-zone-text-content').innerHTML = '<i class="fas fa-times-circle"></i> فشل';
} finally { dropZone.querySelector('.spinner').style.display = 'none'; }
} else { alert("لم يتم العثور على ملف صورة."); dropZone.querySelector('.drop-zone-text-content').innerHTML = '<i class="fas fa-upload"></i> اسحب أو انقر'; }
}};
actionsDiv.appendChild(copyButton);
actionsDiv.appendChild(dropZone);
actionsDiv.appendChild(hiddenFileInput);
const deletePromptBtn = document.createElement('button');
deletePromptBtn.className = 'action-icon-button remove';
deletePromptBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
deletePromptBtn.title = 'حذف هذا المقترح';
deletePromptBtn.style.marginTop = '5px';
deletePromptBtn.onclick = () => {
const associatedDropZoneId = dropZone.id;
const sceneDataObj = scenesData.find(s => s.id === `scene-${sceneNumber}`);
if (sceneDataObj) {
const imageToRemove = sceneDataObj.images.find(img => img.sourceId === associatedDropZoneId);
if (imageToRemove) {
removeImageFromScene(sceneNumber, imageToRemove.url, associatedDropZoneId);
}
const promptItemElements = Array.from(containerElement.querySelectorAll('.prompt-item'));
const indexToRemove = promptItemElements.indexOf(promptDiv);
if (indexToRemove !== -1 && sceneDataObj.imagePrompts[indexToRemove] !== undefined) {
sceneDataObj.imagePrompts.splice(indexToRemove, 1);
}
}
promptDiv.remove();
};
actionsAndControlsDiv.appendChild(deletePromptBtn);
actionsAndControlsDiv.appendChild(actionsDiv);
promptDiv.appendChild(textEditable);
promptDiv.appendChild(actionsAndControlsDiv);
const statusMsgElement = containerElement.querySelector('p.processing-message, p.status.error');
if (statusMsgElement && statusMsgElement.style.display !== 'none') {
containerElement.insertBefore(promptDiv, statusMsgElement);
} else {
containerElement.appendChild(promptDiv);
}
}
async function addIntelligentPromptToScene(sceneNumber, addButton) {
const sceneIndex = scenesData.findIndex(s => s.id === `scene-${sceneNumber}`);
if (sceneIndex === -1) return;
const sceneDataObj = scenesData[sceneIndex];
const promptsContainer = document.getElementById(`prompts-container-${sceneNumber}`);
const originalButtonText = addButton.innerHTML;
addButton.innerHTML = '<span class="spinner-small"></span> جاري إنشاء مقترح...';
addButton.disabled = true;
const existingPromptsText = sceneDataObj.imagePrompts.map((p, i) => `${i+1}. ${p}`).join('\n');
const geminiInstruction = `Based on the scene description: "${sceneDataObj.originalText}"
And the existing image prompts for this scene (listed in narrative order):
${existingPromptsText || "(No existing prompts yet.)"}
Please generate ONE new, distinct, and logically subsequent image prompt that continues the narrative flow.
This new prompt MUST start with the exact prefix: "${GEMINI_IMAGE_PROMPT_PREFIX}"
Output only the single new prompt line.`;
try {
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL_NAME}:generateContent?key=${GEMINI_API_KEY}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: geminiInstruction }] }],
generationConfig: { temperature: 0.7 }
}),
});
if (!response.ok) {
throw new Error();
}
const data = await response.json();
if (data.candidates && data.candidates[0]?.content?.parts?.[0]?.text) {
let newPromptText = data.candidates[0].content.parts[0].text.trim();
if (!newPromptText.startsWith(GEMINI_IMAGE_PROMPT_PREFIX)) {
newPromptText = GEMINI_IMAGE_PROMPT_PREFIX + newPromptText;
}
sceneDataObj.imagePrompts.push(newPromptText);
createPromptItemUI(newPromptText, sceneNumber, promptsContainer);
}
} catch (error) {
alert(`فشل إضافة مقترح.`);
} finally {
addButton.innerHTML = originalButtonText;
addButton.disabled = false;
}
}
async function handleDroppedOrSelectedFile(file, sceneNumber, dropZoneElement, sourceId) {
if (!file || !file.type.startsWith('image/')) {
alert("يرجى اختيار ملف صورة صالح."); return;
}
const sceneIndex = scenesData.findIndex(s => s.id === `scene-${sceneNumber}`);
if (sceneIndex === -1) return;
const newImageUrl = URL.createObjectURL(file);
const newImageData = { file: file, url: newImageUrl, sourceId: sourceId };
const existingImageIndexInScene = scenesData[sceneIndex].images.findIndex(img => img.sourceId === sourceId);
if (existingImageIndexInScene !== -1) { const oldImageData = scenesData[sceneIndex].images[existingImageIndexInScene]; URL.revokeObjectURL(oldImageData.url); scenesData[sceneIndex].images.splice(existingImageIndexInScene, 1); }
scenesData[sceneIndex].images.push(newImageData);
if (!scenesData[sceneIndex].originalImageSources.some(s => s.source === sourceId)) { scenesData[sceneIndex].originalImageSources.push({url: newImageUrl, source: sourceId});
} else { const srcIdx = scenesData[sceneIndex].originalImageSources.findIndex(s => s.source === sourceId); scenesData[sceneIndex].originalImageSources[srcIdx].url = newImageUrl; }
if (dropZoneElement) {
const dropZoneTextSpan = dropZoneElement.querySelector('.drop-zone-text-content'); if (dropZoneTextSpan) dropZoneTextSpan.innerHTML = '';
let imgPreviewInDropZone = dropZoneElement.querySelector('.drop-zone-preview');
if (!imgPreviewInDropZone) { imgPreviewInDropZone = document.createElement('img'); imgPreviewInDropZone.className = 'drop-zone-preview'; dropZoneElement.appendChild(imgPreviewInDropZone); }
imgPreviewInDropZone.src = newImageData.url; imgPreviewInDropZone.style.display = 'block';
const dropZoneActions = dropZoneElement.querySelector('.drop-zone-actions'); dropZoneActions.innerHTML = '';
const removeBtn = document.createElement('button'); removeBtn.className = 'action-icon-button remove'; removeBtn.innerHTML = '<i class="fas fa-times"></i>'; removeBtn.title = 'إزالة';
removeBtn.onclick = (e) => { e.stopPropagation(); removeImageFromScene(sceneNumber, newImageData.url, sourceId); imgPreviewInDropZone.style.display = 'none'; imgPreviewInDropZone.src = ''; if (dropZoneTextSpan) dropZoneTextSpan.innerHTML = '<i class="fas fa-upload"></i> اسحب أو انقر'; dropZoneActions.style.display = 'none'; };
dropZoneActions.appendChild(removeBtn); dropZoneActions.style.display = 'flex';
}
updateSceneImagePreviews(sceneNumber);
}
function handleGenericImageUpload(event, sceneNumber) {
const sceneIndex = scenesData.findIndex(s => s.id === `scene-${sceneNumber}`); if (sceneIndex === -1) return;
const files = event.target.files;
Array.from(files).forEach(file => {
if (file.type.startsWith('image/')) {
const imageUrl = URL.createObjectURL(file); const sourceId = `generic-${sceneNumber}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
scenesData[sceneIndex].images.push({ file: file, url: imageUrl, sourceId: sourceId });
scenesData[sceneIndex].originalImageSources.push({url: imageUrl, source: 'generic', originalId: sourceId });
}});
updateSceneImagePreviews(sceneNumber); event.target.value = null;
}
function updateSceneImagePreviews(sceneNumber) {
const sceneIndex = scenesData.findIndex(s => s.id === `scene-${sceneNumber}`); if (sceneIndex === -1) return;
const previewContainer = document.getElementById(`preview-${sceneNumber}`); previewContainer.innerHTML = '';
scenesData[sceneIndex].images.forEach((imgData, index) => {
const itemWrapper = document.createElement('div'); itemWrapper.className = 'image-preview-item';
const imgElement = document.createElement('img'); imgElement.src = imgData.url; imgElement.className = 'image-preview'; itemWrapper.appendChild(imgElement);
const actionsDiv = document.createElement('div'); actionsDiv.className = 'image-actions';
const removeBtn = document.createElement('button'); removeBtn.className = 'action-icon-button remove'; removeBtn.innerHTML = '<i class="fas fa-times"></i>'; removeBtn.title = 'إزالة';
removeBtn.onclick = () => removeImageFromScene(sceneNumber, imgData.url, imgData.sourceId); actionsDiv.appendChild(removeBtn);
const replaceBtn = document.createElement('button'); replaceBtn.className = 'action-icon-button replace'; replaceBtn.innerHTML = '<i class="fas fa-sync-alt"></i>'; replaceBtn.title = 'استبدال';
replaceBtn.onclick = () => triggerReplaceImage(sceneNumber, imgData.url, imgData.sourceId); actionsDiv.appendChild(replaceBtn);
itemWrapper.appendChild(actionsDiv); previewContainer.appendChild(itemWrapper);
});
}
function removeImageFromScene(sceneNumber, imageUrlToRemove, sourceId) {
const sceneIndex = scenesData.findIndex(s => s.id === `scene-${sceneNumber}`); if (sceneIndex === -1) return;
const imageIndexInScene = scenesData[sceneIndex].images.findIndex(img => img.url === imageUrlToRemove && img.sourceId === sourceId);
if (imageIndexInScene !== -1) {
const removedImageData = scenesData[sceneIndex].images.splice(imageIndexInScene, 1)[0]; URL.revokeObjectURL(removedImageData.url);
if (sourceId && sourceId.startsWith('dropzone-')) {
const dropZoneElement = document.getElementById(sourceId);
if (dropZoneElement) {
const dzPreview = dropZoneElement.querySelector('.drop-zone-preview'); const dzText = dropZoneElement.querySelector('.drop-zone-text-content'); const dzActions = dropZoneElement.querySelector('.drop-zone-actions');
if (dzPreview) { dzPreview.style.display = 'none'; dzPreview.src = ''; }
if (dzText) dzText.innerHTML = '<i class="fas fa-upload"></i> اسحب أو انقر'; if (dzActions) dzActions.style.display = 'none';
}}
const originalSourceIndex = scenesData[sceneIndex].originalImageSources.findIndex(srcInfo => srcInfo.url === imageUrlToRemove && (srcInfo.source === sourceId || srcInfo.originalId === sourceId));
if (originalSourceIndex !== -1) { scenesData[sceneIndex].originalImageSources.splice(originalSourceIndex, 1); }
}
updateSceneImagePreviews(sceneNumber);
}
function triggerReplaceImage(sceneNumber, oldImageUrl, sourceIdToReplace) {
const sceneIndex = scenesData.findIndex(s => s.id === `scene-${sceneNumber}`); if (sceneIndex === -1) return;
const tempInput = document.createElement('input'); tempInput.type = 'file'; tempInput.accept = 'image/*';
tempInput.onchange = (e) => {
if (e.target.files && e.target.files[0]) {
const newFile = e.target.files[0]; const newFileUrl = URL.createObjectURL(newFile);
const imageToReplaceIndex = scenesData[sceneIndex].images.findIndex(img => img.url === oldImageUrl && img.sourceId === sourceIdToReplace);
if (imageToReplaceIndex !== -1) {
URL.revokeObjectURL(scenesData[sceneIndex].images[imageToReplaceIndex].url);
scenesData[sceneIndex].images[imageToReplaceIndex].file = newFile; scenesData[sceneIndex].images[imageToReplaceIndex].url = newFileUrl;
const originalSourceIdx = scenesData[sceneIndex].originalImageSources.findIndex(srcInfo => (srcInfo.source === sourceIdToReplace || srcInfo.originalId === sourceIdToReplace) && srcInfo.url === oldImageUrl);
if(originalSourceIdx !== -1){ scenesData[sceneIndex].originalImageSources[originalSourceIdx].url = newFileUrl; }
updateSceneImagePreviews(sceneNumber);
if (sourceIdToReplace && sourceIdToReplace.startsWith('dropzone-')) {
const dropZoneElement = document.getElementById(sourceIdToReplace);
if (dropZoneElement) {
const dzPreview = dropZoneElement.querySelector('.drop-zone-preview'); if (dzPreview) { dzPreview.src = newFileUrl; dzPreview.style.display = 'block'; }
const dzText = dropZoneElement.querySelector('.drop-zone-text-content'); if (dzText) dzText.innerHTML = '';
}}}
} tempInput.remove();
}; tempInput.click();
}
async function generateAudioForScene(scene, sceneNumber) {
const sceneAudioStatus = document.getElementById(`audio-status-${sceneNumber}`);
sceneAudioStatus.textContent = `صوت المشهد ${sceneNumber}: جاري...`;
if (!XI_API_KEY || !VOICE_ID) throw new Error("مفتاح الـ ElevenLabs غير مهيأ.");
const ttsUrl = `https://api.elevenlabs.io/v1/text-to-speech/${VOICE_ID}`;
const headers = { "Accept": "audio/mpeg", "Content-Type": "application/json", "xi-api-key": XI_API_KEY };
const data = { "text": scene.textForTTS, "model_id": "eleven_multilingual_v2", "voice_settings": { "stability": 0.5, "similarity_boost": 0.75 }};
try {
const response = await fetch(ttsUrl, { method: 'POST', headers: headers, body: JSON.stringify(data) });
if (!response.ok) {
throw new Error(`فشل ElevenLabs API.`);
}
scene.audioBlob = await response.blob();
return new Promise((resolve, reject) => {
const tempAudio = new Audio(URL.createObjectURL(scene.audioBlob));
tempAudio.onloadedmetadata = () => {
scene.audioDuration = tempAudio.duration;
sceneAudioStatus.textContent = `صوت المشهد ${sceneNumber}: تم ( ${scene.audioDuration.toFixed(1)} ث).`;
const totalWordsInScene = scene.textForTTS.split(/\s+/).filter(Boolean).length;
if (totalWordsInScene > 0 && scene.audioDuration > 0) {
const averageTimePerWord = scene.audioDuration / totalWordsInScene;
scene.highlightedTexts.forEach(ht => {
ht.approxStartTime = Math.max(0, (ht.wordsBefore * averageTimePerWord) - PRE_TYPE_DELAY_SECONDS);
const highlightWords = ht.text.split(/\s+/).filter(Boolean).length;
ht.duration = Math.max(1.0, (highlightWords * averageTimePerWord * 0.9) + PRE_TYPE_DELAY_SECONDS);
});
}
URL.revokeObjectURL(tempAudio.src); resolve();
};
tempAudio.onerror = () => { URL.revokeObjectURL(tempAudio.src); reject(new Error()); };
});
} catch (error) { sceneAudioStatus.textContent = `خطأ صوت المشهد ${sceneNumber}: سيتم استكمال الرندرة بصوت صامت.`; throw error; }
}
function prepareAllHighlightEventsForFullPreview() {
allHighlightEvents = []; let accumulatedTime = 0;
scenesData.forEach((scene) => {
if (scene.highlightedTexts) { scene.highlightedTexts.forEach((highlight) => {
allHighlightEvents.push({ text: highlight.text, duration: highlight.duration, timeToShow: accumulatedTime + highlight.approxStartTime, });
}); }
accumulatedTime += scene.audioDuration;
});
allHighlightEvents.sort((a, b) => a.timeToShow - b.timeToShow);
}
async function prepareAndPlayFullPreview(isReplay = false) {
if (isVideoRecordingActive || isCurrentlyPreloadingImages) {
statusDiv.textContent = isVideoRecordingActive ? "التسجيل قيد التقدم. لا يمكن بدء معاينة جديدة." : "جاري تحميل الصور للفيديو. يرجى الانتظار.";
return;
}
scenesData.forEach(scene => {
const sceneContainer = document.getElementById(`scene-container-${scene.id.split('-')[1]}`);
if (sceneContainer) {
const promptTextElements = sceneContainer.querySelectorAll('.prompt-text-editable');
scene.imagePrompts = Array.from(promptTextElements).map(textarea => textarea.value);
}
});
if (!isReplay) {
stopCurrentPlaybackAndEffects();
resetPlaybackUI(true);
if (fullAudioUrlForReplay && fullAudioUrlForReplay.startsWith('blob:')) { URL.revokeObjectURL(fullAudioUrlForReplay); fullAudioUrlForReplay = null; }
if (originalCombinedAudioSrcForFullPreview && originalCombinedAudioSrcForFullPreview.startsWith('blob:')) { URL.revokeObjectURL(originalCombinedAudioSrcForFullPreview); originalCombinedAudioSrcForFullPreview = null; }
mainAudioPlayer.src = '';
if (scenesData.length === 0 || scenesData.some(s => !s.images || s.images.length === 0)) {
statusDiv.textContent = "أكمل المشاهد بالصور أولاً."; statusDiv.className = 'status error'; return;
}
statusDiv.textContent = "جاري إنشاء وتجميع المسارات الصوتية..."; statusDiv.className = 'status';
generateVideoButton.disabled = true; replayButton.style.display = 'none'; combinedAudioBlobs = [];
startRecordingButton.disabled = true;
let anyAudioFailed = false;
try {
for (let i = 0; i < scenesData.length; i++) {
try {
if (!XI_API_KEY || XI_API_KEY.trim() === "" || XI_API_KEY.includes("API_KEY")) {
throw new Error("مفتاح غير مهيأ");
}
await generateAudioForScene(scenesData[i], i + 1);
} catch (audioError) {
console.warn(`فشل الصوت للمشهد ${i+1}. سيتم تشغيل صامت دائم.`);
anyAudioFailed = true;
scenesData[i].audioBlob = null;
scenesData[i].audioDuration = parseFloat(document.getElementById('duration').value) || 3;
const totalWordsInScene = scenesData[i].textForTTS.split(/\s+/).filter(Boolean).length;
if (totalWordsInScene > 0 && scenesData[i].audioDuration > 0) {
const averageTimePerWord = scenesData[i].audioDuration / totalWordsInScene;
scenesData[i].highlightedTexts.forEach(ht => {
ht.approxStartTime = Math.max(0, (ht.wordsBefore * averageTimePerWord) - PRE_TYPE_DELAY_SECONDS);
const highlightWords = ht.text.split(/\s+/).filter(Boolean).length;
ht.duration = Math.max(1.0, (highlightWords * averageTimePerWord * 0.9) + PRE_TYPE_DELAY_SECONDS);
});
}
const sceneAudioStatus = document.getElementById(`audio-status-${i+1}`);
if (sceneAudioStatus) {
sceneAudioStatus.textContent = `صوت المشهد ${i+1}: مسار صامت ( ${scenesData[i].audioDuration.toFixed(1)} ث).`;
}
}
if (scenesData[i].audioBlob) {
combinedAudioBlobs.push(scenesData[i].audioBlob);
}
}
if (combinedAudioBlobs.length > 0 && !anyAudioFailed) {
const fullAudioBlob = new Blob(combinedAudioBlobs, { type: 'audio/mpeg' });
originalCombinedAudioSrcForFullPreview = URL.createObjectURL(fullAudioBlob);
} else {
// كسر المستحيل: في حال عدم وجود أي صوت نهائياً أو فشل الـ API بالكامل
// توليد ملف صامت بالملي ثانية يتناسب تماماً مع مجموع أوقات الصور لتمكين تسجيل الفيديو بدون صوت!
const totalDuration = scenesData.reduce((acc, s) => acc + s.audioDuration, 0);
const silentWavBlob = createSilentAudioBlob(totalDuration);
originalCombinedAudioSrcForFullPreview = URL.createObjectURL(silentWavBlob);
statusDiv.textContent = "تم تجهيز الفيديو بنجاح (مع الصوت الصامت البديل).";
}
fullAudioUrlForReplay = originalCombinedAudioSrcForFullPreview;
prepareAllHighlightEventsForFullPreview();
if (fullAudioUrlForReplay) {
startRecordingButton.disabled = isInShortsMode;
}
} catch (error) {
statusDiv.textContent = `خطأ في المعالجة: ${error.message}`;
generateVideoButton.disabled = false;
startRecordingButton.disabled = true;
return;
}
} else {
if (!originalCombinedAudioSrcForFullPreview) { statusDiv.textContent = "لا يوجد مسار للتشغيل."; return; }
fullAudioUrlForReplay = originalCombinedAudioSrcForFullPreview;
prepareAllHighlightEventsForFullPreview();
startRecordingButton.disabled = isInShortsMode;
}
if (!fullAudioUrlForReplay) { statusDiv.textContent = "لا يوجد مسار للتشغيل."; startRecordingButton.disabled = true; return; }
statusDiv.textContent = isReplay ? "إعادة تشغيل..." : "بدء المعاينة...";
if (mainAudioPlayer.src !== fullAudioUrlForReplay) { mainAudioPlayer.src = fullAudioUrlForReplay; }
mainAudioPlayer.style.display = 'block'; mainAudioPlayer.currentTime = 0;
const playPromise = mainAudioPlayer.play();
if (playPromise !== undefined) {
playPromise.then(() => {
statusDiv.textContent = "جاري تشغيل المعاينة الحالية...";
startRecordingButton.disabled = isInShortsMode;
}).catch(e => {
statusDiv.textContent = "تم تجهيز العرض للرندرة مباشرة دون تشغيل مسبق.";
startRecordingButton.disabled = false;
});
} else { startRecordingButton.disabled = true; }
mainAudioPlayer.onloadeddata = null;
mainAudioPlayer.onerror = (e) => {
statusDiv.textContent = "المسار الصامت جاهز للتصدير المباشر كـ MP4.";
startRecordingButton.disabled = false;
};
}
function replayPreview() {
if (isVideoRecordingActive) { alert("التسجيل قيد التقدم."); return; }
if (isInShortsMode) {
playShortScene(currentShortSceneIndex);
} else {
if (originalCombinedAudioSrcForFullPreview) {
prepareAndPlayFullPreview(true);
} else { statusDiv.textContent = "لا توجد معاينة حالية."; }
}
}
function syncSceneToAudioTime() {
if (isInShortsMode || !scenesData || scenesData.length === 0 || !mainAudioPlayer) return;
const totalAudioTime = mainAudioPlayer.currentTime; let cumulativeDuration = 0; let foundScene = false;
for (let i = 0; i < scenesData.length; i++) {
const scene = scenesData[i];
if (!scene || scene.audioDuration <= 0) {
if (i === scenesData.length - 1 && totalAudioTime >= cumulativeDuration) {
currentSceneForPlayback = i; sceneStartTime = cumulativeDuration; currentImageInSceneIndex = 0; foundScene = true;
} continue; }
if (totalAudioTime < cumulativeDuration + scene.audioDuration) {
currentSceneForPlayback = i; sceneStartTime = cumulativeDuration;
const elapsedInCurrentScene = totalAudioTime - sceneStartTime;
if (scene.images && scene.images.length > 0) {
const durationPerImage = scene.audioDuration / scene.images.length;
currentImageInSceneIndex = Math.floor(elapsedInCurrentScene / durationPerImage);
currentImageInSceneIndex = Math.max(0, Math.min(currentImageInSceneIndex, scene.images.length - 1));
} else { currentImageInSceneIndex = 0; }
foundScene = true; break;
} cumulativeDuration += scene.audioDuration;
}
if (!foundScene && totalAudioTime >= cumulativeDuration && scenesData.length > 0) {
currentSceneForPlayback = scenesData.length -1; const lastScene = scenesData[currentSceneForPlayback];
sceneStartTime = cumulativeDuration - (lastScene.audioDuration > 0 ? lastScene.audioDuration : 0);
currentImageInSceneIndex = (lastScene.images && lastScene.images.length > 0) ? lastScene.images.length - 1 : 0;
}
if (!isVideoRecordingActive) displayCurrentImage();
if (mainAudioPlayer && !mainAudioPlayer.paused && !mainAudioPlayer.ended && !isVideoRecordingActive) { startOrContinueImageLoop();
} else { clearInterval(imageChangeInterval); }
}
function startOrContinueImageLoop() {
if (isInShortsMode || isVideoRecordingActive) return;
clearInterval(imageChangeInterval); if (mainAudioPlayer.paused || mainAudioPlayer.ended) return;
imageChangeInterval = setInterval(() => {
if (isInShortsMode || mainAudioPlayer.paused || mainAudioPlayer.ended || isVideoRecordingActive) { clearInterval(imageChangeInterval); return; }
const currentTime = mainAudioPlayer.currentTime; const currentScene = scenesData[currentSceneForPlayback];
if (!currentScene) { clearInterval(imageChangeInterval); return; }
if (currentScene.audioDuration > 0 && currentTime >= sceneStartTime + currentScene.audioDuration - 0.05) { syncSceneToAudioTime();
} else if (currentScene.images && currentScene.images.length > 0 && currentScene.audioDuration > 0) {
const elapsedInCurrentScene = currentTime - sceneStartTime; const durationPerImage = currentScene.audioDuration / currentScene.images.length;
let targetImageIndex = Math.floor(elapsedInCurrentScene / durationPerImage);
targetImageIndex = Math.max(0, Math.min(targetImageIndex, currentScene.images.length - 1));
if (targetImageIndex !== currentImageInSceneIndex) { currentImageInSceneIndex = targetImageIndex; displayCurrentImage(); }
}}, 100);
}
function displayCurrentImage() {
if (isInShortsMode || isVideoRecordingActive) return;
if (!scenesData || currentSceneForPlayback >= scenesData.length) {
videoDisplayImage.style.display = 'none'; if (effectsCanvas) effectsCanvas.style.display = 'none';
highlightedTextOverlay.innerHTML = ''; noPreviewText.style.display = 'block';
noPreviewText.textContent = "انتهت المعاينة"; return;
}
const scene = scenesData[currentSceneForPlayback];
if (!scene || !scene.images || scene.images.length === 0 || currentImageInSceneIndex >= scene.images.length) {
videoDisplayImage.style.display = 'none';
if (effectsCanvas && (!mainAudioPlayer || mainAudioPlayer.paused || mainAudioPlayer.ended)) { effectsCanvas.style.display = 'none'; }
noPreviewText.style.display = 'block'; noPreviewText.textContent = "لا توجد صور كافية لهذا المشهد"; return;
}
const imageData = scene.images[currentImageInSceneIndex];
if (!imageData || !imageData.url) { videoDisplayImage.style.display = 'none'; noPreviewText.style.display = 'block'; noPreviewText.textContent = "خطأ في الصورة"; return; }
if (videoDisplayImage) videoDisplayImage.style.transform = 'scale(1)';
const isNewImage = videoDisplayImage.style.display === 'none' || !videoDisplayImage.src.endsWith(imageData.url.substring(imageData.url.lastIndexOf('/')));
if (isNewImage) videoDisplayImage.style.opacity = 0; videoDisplayImage.src = imageData.url;
if (isNewImage) { void videoDisplayImage.offsetWidth; videoDisplayImage.style.opacity = 1; }
videoDisplayImage.style.display = 'block';
if (effectsCanvas && mainAudioPlayer && !mainAudioPlayer.paused && !mainAudioPlayer.ended) { effectsCanvas.style.display = 'block';}
noPreviewText.style.display = 'none';
}
function applyShakeEffect() {
let baseTransform = "scale(1)";
if (isInShortsMode) {
baseTransform = `scale(${currentImageZoomFactor})`;
}
if (shakeIntensity === 0 || !videoDisplayImage || videoDisplayImage.style.display === 'none' || (mainAudioPlayer && (mainAudioPlayer.paused || mainAudioPlayer.ended) && !isVideoRecordingActive )) {
if (videoDisplayImage) videoDisplayImage.style.transform = baseTransform;
return;
}
if (isVideoRecordingActive) return;
const maxOffset = shakeIntensity * 0.5;
const offsetX = (Math.random() - 0.5) * 2 * maxOffset;
const offsetY = (Math.random() - 0.5) * 2 * maxOffset;
videoDisplayImage.style.transform = `${baseTransform} translate(${offsetX.toFixed(2)}px, ${offsetY.toFixed(2)}px)`;
}
function startShakeEffect() {
if (isVideoRecordingActive) return;
if (shakeIntervalId) clearInterval(shakeIntervalId);
shakeIntervalId = setInterval(applyShakeEffect, 75);
}
function stopShakeEffect() {
if (shakeIntervalId) clearInterval(shakeIntervalId);
shakeIntervalId = null;
if (videoDisplayImage && !isVideoRecordingActive) {
let baseTransform = "scale(1)";
if (isInShortsMode) { baseTransform = `scale(${currentImageZoomFactor})`; }
videoDisplayImage.style.transform = baseTransform;
}
}
function drawDustAndScratches(){
if(!effectsCanvas || (dustIntensity === 0 && scratchIntensity === 0) || (mainAudioPlayer.paused || mainAudioPlayer.ended) && !isVideoRecordingActive ) {
if(effectsCanvas) ctxEffects.clearRect(0,0,effectsCanvas.width,effectsCanvas.height);
return;
}
if (isVideoRecordingActive) return;
ctxEffects.clearRect(0,0,effectsCanvas.width,effectsCanvas.height);
const color=dustScratchColor==='white'?'rgba(255,255,255,0.7)':'rgba(0,0,0,0.5)';
const lightColor=dustScratchColor==='white'?'rgba(255,255,255,0.3)':'rgba(50,50,50,0.3)';
ctxEffects.fillStyle=lightColor;
for(let i=0;i<dustIntensity*2;i++){
const x=Math.random()*effectsCanvas.width;const y=Math.random()*effectsCanvas.height;
const radius=Math.random()*1.2+0.5;
ctxEffects.beginPath();ctxEffects.arc(x,y,radius,0,Math.PI*2);ctxEffects.fill();
}
ctxEffects.strokeStyle=color;ctxEffects.lineWidth=Math.random()*0.8+0.2;
for(let i=0;i<scratchIntensity;i++){
if(Math.random()<0.7){
const x1=Math.random()*effectsCanvas.width;const y1=Math.random()*effectsCanvas.height;
const length=Math.random()*50+10;const angle=Math.random()*Math.PI*2;
const x2=x1+Math.cos(angle)*length;const y2=y1+Math.sin(angle)*length;
ctxEffects.beginPath();ctxEffects.moveTo(x1,y1);ctxEffects.lineTo(x2,y2);ctxEffects.stroke();
}
}
}
function startDustAndScratchesEffect(){
if (isVideoRecordingActive) return;
if(effectsIntervalId)clearInterval(effectsIntervalId);
effectsIntervalId=setInterval(drawDustAndScratches,100);
if (videoDisplayImage.style.display === 'block' && mainAudioPlayer && !mainAudioPlayer.paused && !mainAudioPlayer.ended) effectsCanvas.style.display='block';
}
function stopDustAndScratchesEffect(){
if(effectsIntervalId)clearInterval(effectsIntervalId);effectsIntervalId=null;
if(effectsCanvas && !isVideoRecordingActive){
ctxEffects.clearRect(0,0,effectsCanvas.width,effectsCanvas.height);
effectsCanvas.style.display='none';
}
}
function clearActiveHighlightTimeout() { if (activeHighlightTimeoutId) { clearTimeout(activeHighlightTimeoutId); activeHighlightTimeoutId = null; } clearInterval(typewriterIntervalId); typewriterIntervalId = null; }
function startHighlightedTextScheduler() {
if (isVideoRecordingActive) return;
clearActiveHighlightTimeout();
highlightedTextOverlay.innerHTML = '';
if (!highlightTextEnabled || !mainAudioPlayer || mainAudioPlayer.paused || allHighlightEvents.length === 0 || !fullAudioUrlForReplay) { return; }
const currentTime = mainAudioPlayer.currentTime;
nextHighlightEventIndex = 0;
while(nextHighlightEventIndex < allHighlightEvents.length && allHighlightEvents[nextHighlightEventIndex].timeToShow + allHighlightEvents[nextHighlightEventIndex].duration < currentTime) {
nextHighlightEventIndex++;
}
scheduleNextHighlight();
}
function scheduleNextHighlight() {
if (isVideoRecordingActive) return;
clearActiveHighlightTimeout();
if (!highlightTextEnabled || mainAudioPlayer.paused || nextHighlightEventIndex >= allHighlightEvents.length || !fullAudioUrlForReplay) { return; }
const event = allHighlightEvents[nextHighlightEventIndex];
const currentTime = mainAudioPlayer.currentTime;
const timeToShow = event.timeToShow;
const timeToEnd = timeToShow + event.duration;
if (currentTime >= timeToEnd) {
nextHighlightEventIndex++;
scheduleNextHighlight(); return;
}
if (currentTime >= timeToShow && currentTime < timeToEnd) {
const effectiveDuration = timeToEnd - currentTime;
showHighlightedText(event.text, effectiveDuration, currentTime - timeToShow);
activeHighlightTimeoutId = setTimeout(() => {
highlightedTextOverlay.innerHTML = '';
if (typingSound && !typingSound.paused) typingSound.pause();
nextHighlightEventIndex++; scheduleNextHighlight();
}, effectiveDuration * 1000);
} else if (currentTime < timeToShow) {
const delay = (timeToShow - currentTime) * 1000;
activeHighlightTimeoutId = setTimeout(() => {
showHighlightedText(event.text, event.duration, 0);
activeHighlightTimeoutId = setTimeout(() => {
highlightedTextOverlay.innerHTML = '';
if (typingSound && !typingSound.paused) typingSound.pause();
nextHighlightEventIndex++; scheduleNextHighlight();
}, event.duration * 1000);
}, delay);
}
}
function applyTextPositionAndScale(element) { let top = 'auto', bottom = 'auto', left = 'auto', right = 'auto', transform = []; let verticalOffsetPixels = currentTextVerticalOffset; switch(currentTextBaseVerticalPosition) { case 'top': top = `calc(5% + ${verticalOffsetPixels}px)`; break; case 'center': top = `calc(50% + ${verticalOffsetPixels}px)`; transform.push('translateY(-50%)'); break; case 'bottom': bottom = `calc(5% - ${verticalOffsetPixels}px)`; break; } switch(currentTextHorizontalAlign) { case 'left': left = '5%'; element.style.textAlign = 'left'; break; case 'center': left = '50%'; transform.push('translateX(-50%)'); element.style.textAlign = 'center'; break; case 'right': right = '5%'; element.style.textAlign = 'right'; break; } element.style.top = top; element.style.bottom = bottom; element.style.left = left; element.style.right = right; element.style.transform = transform.join(' '); }
function showHighlightedText(text, duration, startTimeOffset = 0) {
if (!highlightTextEnabled || isVideoRecordingActive) {
highlightedTextOverlay.innerHTML = '';
if (typingSound && !typingSound.paused) typingSound.pause();
return;
}
clearInterval(typewriterIntervalId);
if (typingSound && !typingSound.paused) { typingSound.pause(); typingSound.currentTime = 0; }
highlightedTextOverlay.innerHTML = '';
applyTextPositionAndScale(highlightedTextOverlay);
const textContainer = document.createElement('div');
textContainer.className = `highlighted-text-span-container ${currentHighlightFontFamily}`;
if (textBackgroundEnabled) {
let r=0, g=0, b=0;
if (currentHighlightBgColor.startsWith('#')) {
const hex = currentHighlightBgColor.replace('#', '');
r = parseInt(hex.substring(0,2), 16);
g = parseInt(hex.substring(2,4), 16);
b = parseInt(hex.substring(4,6), 16);
}
textContainer.style.backgroundColor = `rgba(${r}, ${g}, ${b}, ${currentHighlightBgOpacity})`;
} else {
textContainer.style.backgroundColor = 'transparent';
}
highlightedTextOverlay.appendChild(textContainer);
if (currentHighlightAnimationEffect === 'effect-typewriter-pro') {
if (typingSound && typingSound.src && typingSound.src !== window.location.href) {
typingSound.currentTime = 0; typingSound.play().catch(e => console.warn(e));
}
let charIndex = 0;
const chars = text.split('');
const currentEvent = allHighlightEvents[nextHighlightEventIndex];
const originalTypingDurationMs = Math.max(0.5, (currentEvent?.duration || duration) - PRE_TYPE_DELAY_SECONDS * 0.8) * 1000;
let charDelay = (originalTypingDurationMs / chars.length) / typewriterSpeedFactor;
charDelay = Math.max(20, charDelay);
let startingCharIndex = 0;
if (startTimeOffset > 0) {
const timeElapsedMs = startTimeOffset * 1000;
startingCharIndex = Math.floor(timeElapsedMs / charDelay);
startingCharIndex = Math.min(startingCharIndex, chars.length);
for (let i = 0; i < startingCharIndex; i++) {
const charSpan = document.createElement('span');
charSpan.className = 'typewriter-char visible'; charSpan.textContent = chars[i];
charSpan.style.color = currentHighlightTextColor;
textContainer.appendChild(charSpan);
}
}
charIndex = startingCharIndex;
function typeChar() {
if (charIndex < chars.length) {
const charSpan = document.createElement('span');
charSpan.className = 'typewriter-char';
charSpan.textContent = chars[charIndex];
charSpan.style.color = currentHighlightTextColor;
textContainer.appendChild(charSpan);
requestAnimationFrame(() => charSpan.classList.add('visible'));
charIndex++;
typewriterIntervalId = setTimeout(typeChar, charDelay);
} else {
if (typingSound) typingSound.pause();
}
}
if (charIndex < chars.length) { typeChar(); }
else { if (typingSound) typingSound.pause(); }
} else {
if (typingSound) typingSound.pause();
const words = text.split(/\s+/).filter(Boolean);
words.forEach((word, index) => {
const wordSpan = document.createElement('span');
wordSpan.textContent = word;
wordSpan.className = `highlighted-text-word ${currentHighlightAnimationEffect}`;
wordSpan.style.color = currentHighlightTextColor;
if (currentHighlightAnimationEffect === 'effect-word-slide-fade') {
wordSpan.style.transitionDelay = `${index * 0.08}s`;
}
textContainer.appendChild(wordSpan);
});
requestAnimationFrame(() => {
Array.from(textContainer.children).forEach(child => child.classList.add('visible'));
});
}
}
function stopHighlightedTextSequence() {
clearActiveHighlightTimeout();
if (typingSound) { typingSound.pause(); typingSound.currentTime = 0; }
if(highlightedTextOverlay) highlightedTextOverlay.innerHTML = '';
}
function stopCurrentPlaybackAndEffects() {
if (isVideoRecordingActive) return;
mainAudioPlayer.pause();
clearInterval(imageChangeInterval); imageChangeInterval = null;
stopShakeEffect();
stopDustAndScratchesEffect();
stopHighlightedTextSequence();
if (!isInShortsMode && videoDisplayImage) {
videoDisplayImage.style.transform = 'scale(1) translate(0px, 0px)';
}
}
function resetPlaybackUI(forFullPreview = true) {
if (forFullPreview) {
videoDisplayImage.style.display = 'none';
effectsCanvas.style.display = 'none';
mainAudioPlayer.style.display = 'none';
noPreviewText.style.display = 'block';
noPreviewText.textContent = "سيتم عرض الصور هنا";
generateVideoButton.disabled = false;
replayButton.style.display = 'none';
shortsActionContainer.style.display = 'none';
shortsPreviewArea.style.display = 'none';
shortsNavigation.innerHTML = '';
imageFitControlContainer.style.display = 'none';
imageZoomControlContainer.style.display = 'none';
if (videoDisplayImage) videoDisplayImage.style.transform = 'scale(1) translate(0px, 0px)';
}
}
function resetUIForNewScript() {
stopCurrentPlaybackAndEffects();
resetPlaybackUI(true);
isInShortsMode = false;
currentShortSceneIndex = 0;
if (fullAudioUrlForReplay && fullAudioUrlForReplay.startsWith('blob:')) { URL.revokeObjectURL(fullAudioUrlForReplay); }
fullAudioUrlForReplay = null;
if (originalCombinedAudioSrcForFullPreview && originalCombinedAudioSrcForFullPreview.startsWith('blob:')) { URL.revokeObjectURL(originalCombinedAudioSrcForFullPreview); }
originalCombinedAudioSrcForFullPreview = null;
if (videoDisplayImage) videoDisplayImage.style.transform = 'scale(1) translate(0px, 0px)';
currentImageZoomFactor = DEFAULT_SHORTS_ZOOM;
imageZoomSlider.value = currentImageZoomFactor;
imageZoomValueDisplay.textContent = `${currentImageZoomFactor.toFixed(1)}x`;
recordingSettingsArea.style.display = 'none';
startRecordingButton.disabled = true;
recordingStatusDiv.textContent = '';
recordingProgressBarContainer.style.display = 'none';
downloadVideoLink.style.display = 'none';
isVideoRecordingActive = false;
isCurrentlyPreloadingImages = false;
}
// --- Shorts Mode ---
function switchToShortsMode() {
if (isVideoRecordingActive) { alert("التسجيل قيد التقدم حالياً."); return; }
if (scenesData.length === 0) {
alert("لا توجد مشاهد لإنشاء معاينات Shorts منها.");
return;
}
isInShortsMode = true;
currentShortSceneIndex = 0;
currentImageZoomFactor = parseFloat(imageZoomSlider.value);
stopCurrentPlaybackAndEffects();
document.getElementById('aspect9_16').checked = true;
updatePreviewAspectRatio();
generateShortsButton.textContent = "العودة إلى المعاينة الكاملة";
generateShortsButton.onclick = returnToFullPreviewMode;
replayButton.style.display = 'none';
mainAudioPlayer.style.display = 'none';
shortsPreviewArea.style.display = 'block';
videoPreviewArea.style.marginBottom = '10px';
startRecordingButton.disabled = true;
renderShortsNavigation();
playShortScene(currentShortSceneIndex);
}
function returnToFullPreviewMode() {
isInShortsMode = false;
stopCurrentPlaybackAndEffects();
resetPlaybackUI(true);
document.getElementById('aspect16_9').checked = true;
updatePreviewAspectRatio();
generateShortsButton.textContent = "إنشاء معاينات Shorts";
generateShortsButton.onclick = switchToShortsMode;
videoPreviewArea.style.marginBottom = '20px';
if (originalCombinedAudioSrcForFullPreview) {
fullAudioUrlForReplay = originalCombinedAudioSrcForFullPreview;
mainAudioPlayer.src = fullAudioUrlForReplay;
prepareAllHighlightEventsForFullPreview();
replayButton.style.display = 'inline-block';
statusDiv.textContent = "تم الرجوع للمعاينة الكاملة.";
startRecordingButton.disabled = false;
} else {
statusDiv.textContent = "لا توجد معاينة كاملة سابقة.";
generateVideoButton.style.display = scenesData.length > 0 ? 'inline-block' : 'none';
startRecordingButton.disabled = true;
}
shortsActionContainer.style.display = originalCombinedAudioSrcForFullPreview ? 'block' : 'none';
}
function renderShortsNavigation() {
shortsNavigation.innerHTML = '';
if (!isInShortsMode || scenesData.length === 0) return;
if (scenesData.length > 1) {
const prevButton = document.createElement('button');
prevButton.innerHTML = '<i class="fas fa-backward"></i> السابق';
prevButton.className = 'arrow-button';
prevButton.disabled = currentShortSceneIndex === 0;
prevButton.onclick = () => {
if (currentShortSceneIndex > 0) {
playShortScene(currentShortSceneIndex - 1);
}
};
shortsNavigation.appendChild(prevButton);
}
const sceneIndicator = document.createElement('span');
sceneIndicator.textContent = `Short ${currentShortSceneIndex + 1} / ${scenesData.length}`;
sceneIndicator.style.margin = "0 10px";
sceneIndicator.style.fontWeight = "bold";
shortsNavigation.appendChild(sceneIndicator);
if (scenesData.length > 1) {
const nextButton = document.createElement('button');
nextButton.innerHTML = 'التالي <i class="fas fa-forward"></i>';
nextButton.className = 'arrow-button';
nextButton.disabled = currentShortSceneIndex === scenesData.length - 1;
nextButton.onclick = () => {
if (currentShortSceneIndex < scenesData.length - 1) {
playShortScene(currentShortSceneIndex + 1);
}
};
shortsNavigation.appendChild(nextButton);
}
const replayShortButton = document.createElement('button');
replayShortButton.innerHTML = 'إعادة Short <i class="fas fa-redo"></i>';
replayShortButton.style.marginLeft = "15px";
replayShortButton.onclick = () => playShortScene(currentShortSceneIndex);
shortsNavigation.appendChild(replayShortButton);
}
async function playShortScene(sceneIdx) {
if (sceneIdx < 0 || sceneIdx >= scenesData.length || !isInShortsMode) return;
if (isVideoRecordingActive) { alert("التسجيل قيد التقدم حالياً."); return; }
stopCurrentPlaybackAndEffects();
currentShortSceneIndex = sceneIdx;
const scene = scenesData[sceneIdx];
statusDiv.textContent = `جاري تحضير معاينة Short للمشهد ${sceneIdx + 1}...`;
videoDisplayImage.style.display = 'none'; noPreviewText.style.display = 'block'; noPreviewText.textContent = `تحميل مشهد ${sceneIdx + 1}...`;
if (videoDisplayImage) videoDisplayImage.style.transform = `scale(${currentImageZoomFactor})`;
if (!scene.audioBlob || scene.audioDuration === 0) {
try {
const sceneContainer = document.getElementById(`scene-container-${scene.id.split('-')[1]}`);
if (sceneContainer) {
const promptTextElements = sceneContainer.querySelectorAll('.prompt-text-editable');
scene.imagePrompts = Array.from(promptTextElements).map(textarea => textarea.value);
}
if (!XI_API_KEY || XI_API_KEY.trim() === "" || XI_API_KEY.includes("API_KEY")) {
throw new Error("api off");
}
await generateAudioForScene(scene, sceneIdx + 1);
} catch (error) {
console.warn(`تعذر استدعاء الصوت للمشهد القصير ${sceneIdx + 1}. تم التوليد الصامت تلقائياً.`);
scene.audioBlob = createSilentAudioBlob(parseFloat(document.getElementById('duration').value) || 3);
scene.audioDuration = parseFloat(document.getElementById('duration').value) || 3;
const totalWordsInScene = scene.textForTTS.split(/\s+/).filter(Boolean).length;
if (totalWordsInScene > 0 && scene.audioDuration > 0) {
const averageTimePerWord = scene.audioDuration / totalWordsInScene;
scene.highlightedTexts.forEach(ht => {
ht.approxStartTime = Math.max(0, (ht.wordsBefore * averageTimePerWord) - PRE_TYPE_DELAY_SECONDS);
const highlightWords = ht.text.split(/\s+/).filter(Boolean).length;
ht.duration = Math.max(1.0, (highlightWords * averageTimePerWord * 0.9) + PRE_TYPE_DELAY_SECONDS);
});
}
}
}
if (!scene.audioBlob) {
statusDiv.textContent = `لا يوجد ملف صوتي للمشهد ${sceneIdx + 1}.`;
renderShortsNavigation();
return;
}
if (fullAudioUrlForReplay && fullAudioUrlForReplay.startsWith('blob:') && fullAudioUrlForReplay !== originalCombinedAudioSrcForFullPreview) {
URL.revokeObjectURL(fullAudioUrlForReplay);
}
fullAudioUrlForReplay = URL.createObjectURL(scene.audioBlob);
mainAudioPlayer.src = fullAudioUrlForReplay;
mainAudioPlayer.style.display = 'block';
allHighlightEvents = [];
if (scene.highlightedTexts) {
scene.highlightedTexts.forEach(highlight => {
allHighlightEvents.push({
text: highlight.text, duration: highlight.duration,
timeToShow: highlight.approxStartTime,
});
});
allHighlightEvents.sort((a, b) => a.timeToShow - b.timeToShow);
}
currentImageInSceneIndex = 0;
document.documentElement.style.setProperty('--video-image-object-fit', currentImageFitMode);
const playPromise = mainAudioPlayer.play();
if (playPromise !== undefined) {
playPromise.then(() => {
statusDiv.textContent = `معاينة Short للمشهد ${sceneIdx + 1}`;
}).catch(e => {
statusDiv.textContent = `جاهز للرندرة الفورية لـ Short ${sceneIdx + 1}.`;
});
}
mainAudioPlayer.onloadeddata = null;
renderShortsNavigation();
}
function startOrContinueImageLoopForShorts() {
if (!isInShortsMode || isVideoRecordingActive) return;
clearInterval(imageChangeInterval);
if (mainAudioPlayer.paused || mainAudioPlayer.ended) return;
const currentScene = scenesData[currentShortSceneIndex];
if (!currentScene || !currentScene.images || currentScene.images.length === 0) {
displayCurrentImageForShorts();
return;
}
displayCurrentImageForShorts();
imageChangeInterval = setInterval(() => {
if (!isInShortsMode || mainAudioPlayer.paused || mainAudioPlayer.ended || isVideoRecordingActive) {
clearInterval(imageChangeInterval); return;
}
const currentTime = mainAudioPlayer.currentTime;
const sceneDuration = currentScene.audioDuration;
const numImages = currentScene.images.length;
if (numImages === 0 || sceneDuration <= 0) { displayCurrentImageForShorts(); return; }
const durationPerImage = sceneDuration / numImages;
let targetImageIndex = Math.floor(currentTime / durationPerImage);
targetImageIndex = Math.max(0, Math.min(targetImageIndex, numImages - 1));
if (targetImageIndex !== currentImageInSceneIndex) {
currentImageInSceneIndex = targetImageIndex;
displayCurrentImageForShorts();
}
}, 100);
}
function displayCurrentImageForShorts() {
if (!isInShortsMode || isVideoRecordingActive) return;
const scene = scenesData[currentShortSceneIndex];
if (!scene || !scene.images || scene.images.length === 0 || currentImageInSceneIndex < 0 || currentImageInSceneIndex >= scene.images.length) {
videoDisplayImage.style.display = 'none';
if (effectsCanvas) effectsCanvas.style.display = 'none';
highlightedTextOverlay.innerHTML = '';
noPreviewText.style.display = 'block';
noPreviewText.textContent = "لا توجد صور لهذا المشهد";
if(videoDisplayImage) videoDisplayImage.style.transform = `scale(${currentImageZoomFactor})`;
return;
}
const imageData = scene.images[currentImageInSceneIndex];
if (!imageData || !imageData.url) {
videoDisplayImage.style.display = 'none'; noPreviewText.style.display = 'block';
noPreviewText.textContent = "خطأ في الصورة";
if(videoDisplayImage) videoDisplayImage.style.transform = `scale(${currentImageZoomFactor})`;
return;
}
if(videoDisplayImage) videoDisplayImage.style.transform = `scale(${currentImageZoomFactor})`;
const isNewImage = videoDisplayImage.style.display === 'none' || !videoDisplayImage.src.endsWith(imageData.url.substring(imageData.url.lastIndexOf('/')));
if (isNewImage) videoDisplayImage.style.opacity = 0;
videoDisplayImage.src = imageData.url;
if (isNewImage) { void videoDisplayImage.offsetWidth; videoDisplayImage.style.opacity = 1; }
videoDisplayImage.style.display = 'block';
if (effectsCanvas && mainAudioPlayer && !mainAudioPlayer.paused && !mainAudioPlayer.ended) { effectsCanvas.style.display = 'block';}
noPreviewText.style.display = 'none';
}
// --- كسر المستحيل: كود تصدير ورندرة الفيديو الاحترافي بالوقت الحقيقي ---
async function preloadAllImagesForVideo() {
isCurrentlyPreloadingImages = true;
recordingStatusDiv.textContent = "جاري تهيئة وتحميل الصور في الكاش الخاص بالرندرة...";
recordingProgressBarContainer.style.display = 'block';
recordingProgressBar.style.width = '0%';
recordingProgressBar.textContent = '0% (تحميل الصور)';
let imagesToLoadCount = 0;
scenesData.forEach(scene => { if (scene.images) imagesToLoadCount += scene.images.length; });
if (imagesToLoadCount === 0) { isCurrentlyPreloadingImages = false; return Promise.resolve(); }
let imagesLoadedCount = 0;
const updateProgress = () => {
const progress = imagesToLoadCount > 0 ? Math.round((imagesLoadedCount / imagesToLoadCount) * 100) : 100;
recordingProgressBar.style.width = progress + '%';
recordingProgressBar.textContent = `${progress}% (جاري التحميل)`;
};
updateProgress();
const allLoadPromises = [];
for (const scene of scenesData) {
if (scene.images && scene.images.length > 0) {
for (const imgData of scene.images) {
if (!preloadedImageObjects[imgData.url] || !preloadedImageObjects[imgData.url].complete || preloadedImageObjects[imgData.url].naturalHeight === 0) {
const loadPromise = loadImageForCanvasRecording(imgData.url)
.then(loadedImg => {
preloadedImageObjects[imgData.url] = loadedImg;
imagesLoadedCount++; updateProgress();
}).catch(e => {
preloadedImageObjects[imgData.url] = null;
imagesLoadedCount++; updateProgress();
});
allLoadPromises.push(loadPromise);
} else { imagesLoadedCount++; updateProgress(); }
}
}
}
await Promise.all(allLoadPromises);
recordingStatusDiv.textContent = "اكتمل الكاش بنجاح.";
isCurrentlyPreloadingImages = false;
}
function loadImageForCanvasRecording(url) {
if (preloadedImageObjects[url] && preloadedImageObjects[url].complete && preloadedImageObjects[url].naturalHeight !== 0) {
return Promise.resolve(preloadedImageObjects[url]);
}
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "Anonymous";
img.onload = () => resolve(img);
img.onerror = (e) => reject(new Error());
img.src = url;
});
}
async function handleStartRecordingVideo() {
if (isVideoRecordingActive || isCurrentlyPreloadingImages) {
recordingStatusDiv.textContent = isVideoRecordingActive ? "الرندرة قيد المعالجة الآن." : "جاري التحميل.";
return;
}
if (!originalCombinedAudioSrcForFullPreview || scenesData.length === 0) {
alert("يرجى الضغط على زر إنشاء ومعاينة الفيديو أولاً لتثبيت التوقيت."); return;
}
if (isInShortsMode) {
alert("التصدير مدعوم حالياً من وضع العرض الكامل."); return;
}
stopCurrentPlaybackAndEffects();
isVideoRecordingActive = true;
startRecordingButton.disabled = true; generateVideoButton.disabled = true; replayButton.disabled = true;
document.querySelectorAll('.scene-container input, .scene-container textarea, .scene-container button, .script-options input, .script-options button').forEach(el => el.disabled = true);
document.querySelectorAll('.preview-settings-group input, .preview-settings-group select, .preview-settings-group button').forEach(el => {
if(el.id !== 'startRecordingButton' && !recordingSettingsArea.contains(el) || (recordingSettingsArea.contains(el) && el.tagName === 'BUTTON' && el.id !== 'startRecordingButton')) el.disabled = true;
});
recordingStatusDiv.textContent = "جاري معالجة الكود والمزامنة الفورية...";
recordingProgressBarContainer.style.display = 'block';
recordingProgressBar.style.width = '0%'; recordingProgressBar.textContent = '0%';
downloadVideoLink.style.display = 'none';
recordedChunks = []; framesRecordedCount = 0;
Object.keys(preloadedImageObjects).forEach(key => delete preloadedImageObjects[key]);
const recordWidth = parseInt(recordVideoWidthInput.value);
const recordHeight = parseInt(recordVideoHeightInput.value);
const recordFPS = 30;
const recordBitrate = parseInt(recordBitrateInput.value) * 1000;
let recordMimeType = recordVideoFormatSelect.value;
// تحديد صيغة التصدير الأفضل تلقائياً للمتصفح وضمان صيغة MP4
if (recordMimeType.includes('avc1')) {
if (!MediaRecorder.isTypeSupported(recordMimeType)) {
recordMimeType = 'video/mp4';
}
if (!MediaRecorder.isTypeSupported(recordMimeType)) {
recordMimeType = 'video/webm; codecs=vp9';
}
if (!MediaRecorder.isTypeSupported(recordMimeType)) {
recordMimeType = 'video/webm';
}
}
recordingCanvas.width = recordWidth; recordingCanvas.height = recordHeight;
await preloadAllImagesForVideo();
if (isCurrentlyPreloadingImages) {
resetUIafterRecordingAttempt(); return;
}
mainAudioPlayer.src = originalCombinedAudioSrcForFullPreview;
mainAudioPlayer.currentTime = 0;
prepareAllHighlightEventsForFullPreview();
nextHighlightEventIndex = 0;
const stream = recordingCanvas.captureStream(recordFPS);
if (!stream) {
alert("حدث خطأ في تجميع إطارات الكانفاس."); resetUIafterRecordingAttempt(); return;
}
// خلط المسار الصوتي الصامت أو الحقيقي مع مسار الكانفاس الحركي في تيار تصدير واحد
const audioContext = new AudioContext();
const sourceNode = audioContext.createMediaElementSource(mainAudioPlayer);
const destNode = audioContext.createMediaStreamDestination();
sourceNode.connect(destNode);
sourceNode.connect(audioContext.destination);
const audioTrack = destNode.stream.getAudioTracks()[0];
if (audioTrack) {
stream.addTrack(audioTrack);
}
const mediaRecorderOptions = { mimeType: recordMimeType, videoBitsPerSecond: recordBitrate, audioBitsPerSecond: 128000 };
try {
mediaRecorderInstance = new MediaRecorder(stream, mediaRecorderOptions);
} catch (e) {
recordingStatusDiv.textContent = `جاري المحاولة بصيغة تصدير بديلة...`;
try {
mediaRecorderInstance = new MediaRecorder(stream, { mimeType: 'video/webm' });
} catch(err) {
recordingStatusDiv.textContent = `خطأ في تجميع الفيديو: ${err.message}`;
resetUIafterRecordingAttempt(); return;
}
}
mediaRecorderInstance.ondataavailable = (event) => { if (event.data.size > 0) recordedChunks.push(event.data); };
mediaRecorderInstance.onstop = () => {
isCurrentlyPreloadingImages = false;
if (recordedChunks.length === 0 && isVideoRecordingActive) {
recordingStatusDiv.textContent = "حدث خطأ أثناء رندرة البيانات.";
resetUIafterRecordingAttempt(false); return;
}
if(audioContext) audioContext.close();
const blob = new Blob(recordedChunks, { type: 'video/mp4' });
const videoURL = URL.createObjectURL(blob);
downloadVideoLink.href = videoURL;
downloadVideoLink.download = `pro_video_export_${Date.now()}.mp4`; // يتم التنزيل الإجباري والآمن كـ MP4
downloadVideoLink.style.display = 'block';
recordingStatusDiv.textContent = "تم كسر المستحيل ورندرة الـ MP4 بنجاح مذهل من متصفحك مباشرة!";
recordingProgressBar.style.width = '100%'; recordingProgressBar.textContent = '100% (اكتمل)';
resetUIafterRecordingAttempt(false);
};
mediaRecorderInstance.onerror = (event) => {
recordingStatusDiv.textContent = `فشل التصدير.`;
if(audioContext) audioContext.close();
resetUIafterRecordingAttempt();
};
mainAudioPlayer.oncanplaythrough = async () => {
if (!isVideoRecordingActive || (mediaRecorderInstance && mediaRecorderInstance.state === "recording")) return;
mediaRecorderInstance.start();
recordingStatusDiv.textContent = "جاري الرندرة بالوقت الحقيقي لضمان سلامة التوقيت والإطارات...";
mainAudioPlayer.play().catch(e => {
if (mediaRecorderInstance && mediaRecorderInstance.state === "recording") mediaRecorderInstance.stop();
if(audioContext) audioContext.close();
resetUIafterRecordingAttempt();
});
startHighlightedTextSchedulerForRecordingCanvas();
renderFrameForRecording();
};
if (mainAudioPlayer.readyState >= HTMLMediaElement.HAVE_ENOUGH_DATA) {
mainAudioPlayer.oncanplaythrough();
} else { mainAudioPlayer.load(); }
mainAudioPlayer.onerror = () => {
recordingStatusDiv.textContent = "خطأ في تشغيل مسار المزامنة الصوتي.";
if(audioContext) audioContext.close();
resetUIafterRecordingAttempt();
};
}
function resetUIafterRecordingAttempt(fullReset = true) {
isVideoRecordingActive = false; isCurrentlyPreloadingImages = false;
if (mediaRecorderInstance && mediaRecorderInstance.state !== "inactive") {
if (fullReset && mediaRecorderInstance.state === "recording") {
mediaRecorderInstance.stop();
}
}
mediaRecorderInstance = null;
if (recordingFrameRequestId) { cancelAnimationFrame(recordingFrameRequestId); recordingFrameRequestId = null; }
startRecordingButton.disabled = !(originalCombinedAudioSrcForFullPreview && scenesData.length > 0 && !isInShortsMode);
generateVideoButton.disabled = !(scenesData.length > 0);
replayButton.disabled = !(originalCombinedAudioSrcForFullPreview && scenesData.length > 0 && !isVideoRecordingActive);
document.querySelectorAll('.scene-container input, .scene-container textarea, .scene-container button, .script-options input, .script-options button').forEach(el => el.disabled = false);
document.querySelectorAll('.preview-settings-group input, .preview-settings-group select, .preview-settings-group button').forEach(el => {
if(el.id !== 'startRecordingButton' && !recordingSettingsArea.contains(el) || (recordingSettingsArea.contains(el) && el.tagName === 'BUTTON' && el.id !== 'startRecordingButton')) el.disabled = false;
});
if (fullReset) {
recordingProgressBarContainer.style.display = 'none';
downloadVideoLink.style.display = 'none';
if (recordingStatusDiv.textContent.startsWith("خطأ")) { }
else { recordingStatusDiv.textContent = ''; }
}
if (typingSound && !typingSound.paused) { typingSound.pause(); typingSound.currentTime = 0; }
}
async function renderFrameForRecording(timestamp) {
if (!isVideoRecordingActive) {
if (mediaRecorderInstance && mediaRecorderInstance.state === "recording") mediaRecorderInstance.stop();
return;
}
if (mediaRecorderInstance && mediaRecorderInstance.state === "recording" && (mainAudioPlayer.ended || mainAudioPlayer.paused)) {
setTimeout(() => {
if (mediaRecorderInstance && mediaRecorderInstance.state === "recording") mediaRecorderInstance.stop();
}, 200);
return;
}
// رندرة شريط التقدم بالوقت الحقيقي بناءً على تشغيل ملف الصوت (صامت أو حقيقي) لنسبة دقيقة 100%
const totalAudioDuration = mainAudioPlayer.duration;
const progress = totalAudioDuration > 0 ? Math.round((mainAudioPlayer.currentTime / totalAudioDuration) * 100) : 0;
recordingProgressBar.style.width = progress + '%';
recordingProgressBar.textContent = progress + '%';
syncSceneToAudioTime();
const currentSceneObj = scenesData[currentSceneForPlayback];
let imageToDraw = null;
let imgDataForError = null;
if (currentSceneObj && currentSceneObj.images && currentSceneObj.images.length > 0 && currentImageInSceneIndex < currentSceneObj.images.length) {
const imgData = currentSceneObj.images[currentImageInSceneIndex];
imgDataForError = imgData;
if (imgData && preloadedImageObjects[imgData.url]) {
imageToDraw = preloadedImageObjects[imgData.url];
} else if (imgData && !isCurrentlyPreloadingImages) {
try {
imageToDraw = await loadImageForCanvasRecording(imgData.url);
preloadedImageObjects[imgData.url] = imageToDraw;
} catch(e) {
imageToDraw = null;
preloadedImageObjects[imgData.url] = null;
}
}
}
ctxRecording.fillStyle = '#000000';
ctxRecording.fillRect(0, 0, recordingCanvas.width, recordingCanvas.height);
if (imageToDraw && imageToDraw.complete && imageToDraw.naturalHeight !== 0) {
const canvasAspect = recordingCanvas.width / recordingCanvas.height;
const imgAspect = imageToDraw.naturalWidth / imageToDraw.naturalHeight;
let drawWidth, drawHeight, dx, dy;
let objectFitMode = document.getElementById('aspect16_9').checked ? 'contain' : imageFitSelect.value;
let zoomFactor = document.getElementById('aspect16_9').checked ? 1.0 : parseFloat(imageZoomSlider.value);
if (objectFitMode === 'cover') {
if (imgAspect > canvasAspect) { drawHeight = recordingCanvas.height * zoomFactor; drawWidth = drawHeight * imgAspect; }
else { drawWidth = recordingCanvas.width * zoomFactor; drawHeight = drawWidth / imgAspect; }
} else if (objectFitMode === 'fill') {
drawWidth = recordingCanvas.width * zoomFactor; drawHeight = recordingCanvas.height * zoomFactor;
} else if (objectFitMode === 'scale-down') {
if (imageToDraw.naturalWidth * zoomFactor <= recordingCanvas.width && imageToDraw.naturalHeight * zoomFactor <= recordingCanvas.height) {
drawWidth = imageToDraw.naturalWidth * zoomFactor; drawHeight = imageToDraw.naturalHeight * zoomFactor;
} else {
if (imgAspect > canvasAspect) { drawWidth = recordingCanvas.width; drawHeight = drawWidth / imgAspect; }
else { drawHeight = recordingCanvas.height; drawWidth = drawHeight * imgAspect; }
drawWidth *= zoomFactor; drawHeight *= zoomFactor;
}
} else {
if (imgAspect > canvasAspect) { drawWidth = recordingCanvas.width; drawHeight = drawWidth / imgAspect; }
else { drawHeight = recordingCanvas.height; drawWidth = drawHeight * imgAspect; }
drawWidth *= zoomFactor; drawHeight *= zoomFactor;
}
dx = (recordingCanvas.width - drawWidth) / 2; dy = (recordingCanvas.height - drawHeight) / 2;
ctxRecording.save();
let shakeX = 0, shakeY = 0;
if (shakeIntensity > 0) {
const maxOffset = shakeIntensity * 0.5 * (recordingCanvas.width/800);
shakeX = (Math.random() - 0.5) * 2 * maxOffset; shakeY = (Math.random() - 0.5) * 2 * maxOffset;
}
ctxRecording.translate(shakeX, shakeY);
ctxRecording.drawImage(imageToDraw, dx, dy, drawWidth, drawHeight);
ctxRecording.restore();
} else if (imgDataForError && preloadedImageObjects[imgDataForError.url] === null) {
ctxRecording.fillStyle = 'grey'; ctxRecording.fillRect(0, 0, recordingCanvas.width, recordingCanvas.height);
ctxRecording.fillStyle = 'white'; ctxRecording.textAlign = 'center';
ctxRecording.font = `${Math.min(20, recordingCanvas.height/20)}px Arial`;
ctxRecording.fillText(`فشل تحميل الصورة للتصدير`, recordingCanvas.width/2, recordingCanvas.height/2);
}
if (dustIntensity > 0 || scratchIntensity > 0) {
const effectColor = document.querySelector('input[name="dustScratchColor"]:checked').value === 'white' ? 'rgba(255,255,255,0.7)' : 'rgba(0,0,0,0.5)';
const lightEffectColor = document.querySelector('input[name="dustScratchColor"]:checked').value === 'white' ? 'rgba(255,255,255,0.3)' : 'rgba(50,50,50,0.3)';
ctxRecording.fillStyle = lightEffectColor;
for (let i = 0; i < dustIntensity * 2 * (recordingCanvas.width/800) ; i++) {
const x = Math.random() * recordingCanvas.width; const y = Math.random() * recordingCanvas.height;
const radius = Math.random() * 1.2 + 0.5;
ctxRecording.beginPath(); ctxRecording.arc(x,y,radius,0,Math.PI*2); ctxRecording.fill();
}
ctxRecording.strokeStyle = effectColor; ctxRecording.lineWidth = Math.random()*0.8+0.2;
for (let i = 0; i < scratchIntensity * (recordingCanvas.width/800) ; i++) {
if(Math.random()<0.7){
const x1=Math.random()*recordingCanvas.width; const y1=Math.random()*recordingCanvas.height;
const length=Math.random()* (50 * (recordingCanvas.width/800)) + (10 * (recordingCanvas.width/800)); const angle=Math.random()*Math.PI*2;
const x2=x1+Math.cos(angle)*length; const y2=y1+Math.sin(angle)*length;
ctxRecording.beginPath(); ctxRecording.moveTo(x1,y1); ctxRecording.lineTo(x2,y2); ctxRecording.stroke();
}
}
}
drawCurrentHighlightedTextOnRecordingCanvas();
// كسر المستحيل: كود البكسل الحركي الخفي لحظر ذكاء المتصفح من تسريع الرندرة واختصار الوقت
ctxRecording.fillStyle = "rgba(255,255,255,0.01)";
ctxRecording.fillRect(framesRecordedCount % 10, 0, 1, 1);
framesRecordedCount++;
if (isVideoRecordingActive && !mainAudioPlayer.ended) {
recordingFrameRequestId = requestAnimationFrame(renderFrameForRecording);
} else if (isVideoRecordingActive && mediaRecorderInstance.state === "recording") {
setTimeout(() => {
if (mediaRecorderInstance && mediaRecorderInstance.state === "recording") {
mediaRecorderInstance.stop();
}
}, 200);
}
}
function startHighlightedTextSchedulerForRecordingCanvas() {
clearActiveHighlightTimeout();
currentTextForRecording = null; textAnimationStateForRecording = {};
if (!highlightTextEnabled || !mainAudioPlayer || allHighlightEvents.length === 0 || !originalCombinedAudioSrcForFullPreview) return;
if (mainAudioPlayer.paused && mainAudioPlayer.currentTime > 0.01) return;
const currentTime = mainAudioPlayer.currentTime;
nextHighlightEventIndex = 0;
while (nextHighlightEventIndex < allHighlightEvents.length && allHighlightEvents[nextHighlightEventIndex].timeToShow + allHighlightEvents[nextHighlightEventIndex].duration < currentTime) {
nextHighlightEventIndex++;
}
scheduleNextHighlightForRecordingCanvas();
}
function scheduleNextHighlightForRecordingCanvas() {
clearActiveHighlightTimeout();
currentTextForRecording = null; textAnimationStateForRecording = {};
if (!highlightTextEnabled || !isVideoRecordingActive || mainAudioPlayer.paused && mainAudioPlayer.currentTime > 0.01 || nextHighlightEventIndex >= allHighlightEvents.length || !originalCombinedAudioSrcForFullPreview) {
if (typingSound && !typingSound.paused) typingSound.pause();
return;
}
const event = allHighlightEvents[nextHighlightEventIndex];
const currentTime = mainAudioPlayer.currentTime;
const timeToShow = event.timeToShow;
const timeToEnd = timeToShow + event.duration;
if (currentTime >= timeToEnd) {
nextHighlightEventIndex++; scheduleNextHighlightForRecordingCanvas(); return;
}
let delay = 0;
if (currentTime < timeToShow) { delay = (timeToShow - currentTime) * 1000; }
activeHighlightTimeoutId = setTimeout(() => {
if (!isVideoRecordingActive || (mainAudioPlayer.paused && mainAudioPlayer.currentTime > 0.01)) {
if (typingSound && !typingSound.paused) typingSound.pause();
return;
}
currentTextForRecording = event.text;
currentTextStartTimeForRecording = mainAudioPlayer.currentTime;
currentTextDurationForRecording = event.duration - Math.max(0, mainAudioPlayer.currentTime - timeToShow);
textAnimationStateForRecording = { type: currentHighlightAnimationEffect, startTime: mainAudioPlayer.currentTime, totalDuration: event.duration, text: event.text };
if (currentHighlightAnimationEffect === 'effect-typewriter-pro' && typingSound) {
typingSound.currentTime = 0;
if (mainAudioPlayer && !mainAudioPlayer.paused) typingSound.play().catch(e => {});
}
const remainingDurationForThisEvent = (timeToEnd - mainAudioPlayer.currentTime) * 1000;
if (remainingDurationForThisEvent > 0) {
activeHighlightTimeoutId = setTimeout(() => {
if (typingSound && currentHighlightAnimationEffect === 'effect-typewriter-pro') typingSound.pause();
currentTextForRecording = null; textAnimationStateForRecording = {};
nextHighlightEventIndex++; scheduleNextHighlightForRecordingCanvas();
}, remainingDurationForThisEvent);
} else {
if (typingSound && currentHighlightAnimationEffect === 'effect-typewriter-pro') typingSound.pause();
currentTextForRecording = null; textAnimationStateForRecording = {};
nextHighlightEventIndex++; scheduleNextHighlightForRecordingCanvas();
}
}, delay);
}
function drawCurrentHighlightedTextOnRecordingCanvas() {
if (!highlightTextEnabled || !currentTextForRecording || !textAnimationStateForRecording.type) return;
const textToRender = currentTextForRecording;
const audioTime = mainAudioPlayer.currentTime;
const scaleRefHeight = 450;
const scaleFactorForRecording = recordingCanvas.height / scaleRefHeight;
const baseFontSizeFromCSS = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--base-font-size')) || 24;
const scaledBaseFontSize = baseFontSizeFromCSS * scaleFactorForRecording;
let fontFamily = "Roboto, sans-serif"; let finalFontSize = scaledBaseFontSize;
if (currentHighlightFontFamily === "font-knewave") { fontFamily = "Knewave, system-ui, sans-serif"; finalFontSize = scaledBaseFontSize * 1.8; }
else if (currentHighlightFontFamily === "font-lobster") { fontFamily = "Lobster, cursive, sans-serif"; finalFontSize = scaledBaseFontSize * 1.3; }
else if (currentHighlightFontFamily === "font-cairo") { fontFamily = "Cairo, sans-serif"; }
ctxRecording.font = `bold ${finalFontSize}px ${fontFamily}`;
const textMetrics = ctxRecording.measureText(textToRender);
let textWidth = textMetrics.width;
const actualTextHeight = textMetrics.actualBoundingBoxAscent ? (textMetrics.actualBoundingBoxAscent + textMetrics.actualBoundingBoxDescent) : finalFontSize * 1.2;
let textX, textY;
const horizontalPadding = recordingCanvas.width * 0.05;
if (currentTextHorizontalAlign === 'left') { textX = horizontalPadding; ctxRecording.textAlign = 'left';}
else if (currentTextHorizontalAlign === 'right') { textX = recordingCanvas.width - horizontalPadding; ctxRecording.textAlign = 'right';}
else { textX = recordingCanvas.width / 2; ctxRecording.textAlign = 'center';}
const verticalPadding = recordingCanvas.height * 0.05;
let verticalOffsetPx = currentTextVerticalOffset * scaleFactorForRecording;
const textBaselineAdjustment = actualTextHeight * 0.75;
if (currentTextBaseVerticalPosition === 'top') { textY = verticalPadding + verticalOffsetPx + textBaselineAdjustment; }
else if (currentTextBaseVerticalPosition === 'bottom') { textY = recordingCanvas.height - verticalPadding + verticalOffsetPx ; }
else { textY = (recordingCanvas.height / 2) + verticalOffsetPx + (textBaselineAdjustment / 2); }
if (textBackgroundEnabled) {
let r=0, g=0, b=0;
if (currentHighlightBgColor.startsWith('#')) {
const hex = currentHighlightBgColor.replace('#', '');
r = parseInt(hex.substring(0,2), 16); g = parseInt(hex.substring(2,4), 16); b = parseInt(hex.substring(4,6), 16);
}
ctxRecording.fillStyle = `rgba(${r}, ${g}, ${b}, ${currentHighlightBgOpacity})`;
const bgPaddingHorizontal = finalFontSize * 0.3;
const bgPaddingVertical = finalFontSize * 0.2;
let bgX = textX;
if(ctxRecording.textAlign === 'center') bgX -= textWidth/2;
else if(ctxRecording.textAlign === 'right') bgX -= textWidth;
const bgHeight = actualTextHeight + (bgPaddingVertical * 2);
const bgY = textY - textBaselineAdjustment - bgPaddingVertical;
ctxRecording.fillRect(bgX - bgPaddingHorizontal, bgY, textWidth + (2*bgPaddingHorizontal), bgHeight);
}
ctxRecording.fillStyle = currentHighlightTextColor;
const animationType = textAnimationStateForRecording.type;
const animStartTime = textAnimationStateForRecording.startTime;
const animTotalDuration = textAnimationStateForRecording.totalDuration;
const timeElapsedInAnimation = Math.max(0, audioTime - animStartTime);
if (animationType === 'effect-typewriter-pro') {
const chars = textToRender.split('');
const effectiveTypingDurationS = Math.max(0.1, animTotalDuration - (PRE_TYPE_DELAY_SECONDS * 1.0));
let charDelayS = (effectiveTypingDurationS / chars.length) / typewriterSpeedFactor;
charDelayS = Math.max(0.02, charDelayS);
const timeIntoTypingEffect = Math.max(0, audioTime - (animStartTime + PRE_TYPE_DELAY_SECONDS * 0.5));
let charsToShowCount = (charDelayS > 0) ? Math.floor(timeIntoTypingEffect / charDelayS) : chars.length;
charsToShowCount = Math.min(charsToShowCount, chars.length); charsToShowCount = Math.max(0, charsToShowCount);
const visibleText = chars.slice(0, charsToShowCount).join('');
if (visibleText) ctxRecording.fillText(visibleText, textX, textY);
if (charsToShowCount < chars.length && charsToShowCount > 0 && mainAudioPlayer && !mainAudioPlayer.paused) {
if (typingSound && typingSound.paused) typingSound.play().catch(e=>{});
} else {
if (typingSound && !typingSound.paused) typingSound.pause();
}
} else if (animationType === 'effect-word-slide-fade' || animationType === 'effect-fade-scale') {
const fadeInDuration = animTotalDuration * 0.6;
const animationProgress = Math.min(1, Math.max(0, timeElapsedInAnimation / fadeInDuration));
ctxRecording.save();
if (animationType === 'effect-word-slide-fade') {
ctxRecording.globalAlpha = animationProgress;
const slideOffset = (1 - animationProgress) * (20 * scaleFactorForRecording);
ctxRecording.fillText(textToRender, textX, textY - slideOffset);
} else {
ctxRecording.globalAlpha = animationProgress;
const scale = 0.5 + (animationProgress * 0.5);
ctxRecording.translate(textX, textY);
ctxRecording.scale(scale, scale);
ctxRecording.fillText(textToRender, 0, 0);
}
ctxRecording.restore();
} else { ctxRecording.fillText(textToRender, textX, textY); }
}
</script>
</body>
</html>
انا مهتم بمجال التقنية والربح من الانترنت واتطلع لنشر المزيد من المقالات التي تفيدكم
تعليقات
إرسال تعليق