Commit ·
8c64cc3
1
Parent(s): 2e6074b
fix defaults
Browse files- init_defaults.py +42 -11
- main.py +4 -0
- routers/presets.py +15 -1
- routers/subtitle_generator.py +65 -27
- schemas.py +7 -0
init_defaults.py
CHANGED
|
@@ -22,8 +22,14 @@ defaults = {
|
|
| 22 |
"pop_up_scale": 1.2,
|
| 23 |
"highlight_mode": "karaoke",
|
| 24 |
"back_box_enabled": True,
|
| 25 |
-
"display_mode": "
|
| 26 |
-
"max_words_per_line": 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
},
|
| 28 |
"gaming": {
|
| 29 |
"name": "gaming",
|
|
@@ -40,8 +46,14 @@ defaults = {
|
|
| 40 |
"pop_up_scale": 1.5,
|
| 41 |
"highlight_mode": "karaoke",
|
| 42 |
"back_box_enabled": False,
|
| 43 |
-
"display_mode": "
|
| 44 |
-
"max_words_per_line": 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
},
|
| 46 |
"professional": {
|
| 47 |
"name": "professional",
|
|
@@ -59,7 +71,13 @@ defaults = {
|
|
| 59 |
"highlight_mode": "instant",
|
| 60 |
"back_box_enabled": True,
|
| 61 |
"display_mode": "sentence",
|
| 62 |
-
"max_words_per_line": 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
},
|
| 64 |
"storyteller": {
|
| 65 |
"name": "storyteller",
|
|
@@ -77,12 +95,25 @@ defaults = {
|
|
| 77 |
"highlight_mode": "karaoke",
|
| 78 |
"back_box_enabled": True,
|
| 79 |
"display_mode": "sentence",
|
| 80 |
-
"max_words_per_line": 4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
}
|
| 82 |
}
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
"pop_up_scale": 1.2,
|
| 23 |
"highlight_mode": "karaoke",
|
| 24 |
"back_box_enabled": True,
|
| 25 |
+
"display_mode": "sentence",
|
| 26 |
+
"max_words_per_line": 3,
|
| 27 |
+
"uppercase": False,
|
| 28 |
+
"letter_spacing": 0.0,
|
| 29 |
+
"background_opacity": 1.0,
|
| 30 |
+
"glow_intensity": 0,
|
| 31 |
+
"rotation_angle": 0.0,
|
| 32 |
+
"margin_h": 20
|
| 33 |
},
|
| 34 |
"gaming": {
|
| 35 |
"name": "gaming",
|
|
|
|
| 46 |
"pop_up_scale": 1.5,
|
| 47 |
"highlight_mode": "karaoke",
|
| 48 |
"back_box_enabled": False,
|
| 49 |
+
"display_mode": "sentence",
|
| 50 |
+
"max_words_per_line": 2,
|
| 51 |
+
"uppercase": True,
|
| 52 |
+
"letter_spacing": 1.0,
|
| 53 |
+
"background_opacity": 0.8,
|
| 54 |
+
"glow_intensity": 2,
|
| 55 |
+
"rotation_angle": -3.0,
|
| 56 |
+
"margin_h": 20
|
| 57 |
},
|
| 58 |
"professional": {
|
| 59 |
"name": "professional",
|
|
|
|
| 71 |
"highlight_mode": "instant",
|
| 72 |
"back_box_enabled": True,
|
| 73 |
"display_mode": "sentence",
|
| 74 |
+
"max_words_per_line": 5,
|
| 75 |
+
"uppercase": False,
|
| 76 |
+
"letter_spacing": 0.5,
|
| 77 |
+
"background_opacity": 0.9,
|
| 78 |
+
"glow_intensity": 0,
|
| 79 |
+
"rotation_angle": 0.0,
|
| 80 |
+
"margin_h": 40
|
| 81 |
},
|
| 82 |
"storyteller": {
|
| 83 |
"name": "storyteller",
|
|
|
|
| 95 |
"highlight_mode": "karaoke",
|
| 96 |
"back_box_enabled": True,
|
| 97 |
"display_mode": "sentence",
|
| 98 |
+
"max_words_per_line": 4,
|
| 99 |
+
"uppercase": True,
|
| 100 |
+
"letter_spacing": 0.0,
|
| 101 |
+
"background_opacity": 1.0,
|
| 102 |
+
"glow_intensity": 1,
|
| 103 |
+
"rotation_angle": 2.0,
|
| 104 |
+
"margin_h": 30
|
| 105 |
}
|
| 106 |
}
|
| 107 |
|
| 108 |
+
def init_all_defaults():
|
| 109 |
+
"""الدالة التي سنستدعيها عند بدء تشغيل التطبيق"""
|
| 110 |
+
os.makedirs(PRESETS_DIR, exist_ok=True)
|
| 111 |
+
for name, data in defaults.items():
|
| 112 |
+
file_path = os.path.join(PRESETS_DIR, f"{name}.json")
|
| 113 |
+
# لا نعيد الكتابة إذا كان الملف موجوداً بالفعل إلا إذا أردت تحديثه
|
| 114 |
+
with open(file_path, "w", encoding="utf-8") as f:
|
| 115 |
+
json.dump(data, f, indent=4)
|
| 116 |
+
print(f"✅ Initialized preset: {name}")
|
| 117 |
+
|
| 118 |
+
if __name__ == "__main__":
|
| 119 |
+
init_all_defaults()
|
main.py
CHANGED
|
@@ -3,8 +3,12 @@ from fastapi.middleware.cors import CORSMiddleware
|
|
| 3 |
import uvicorn
|
| 4 |
from datetime import datetime
|
| 5 |
from routers import video, files, presets
|
|
|
|
| 6 |
import os
|
| 7 |
|
|
|
|
|
|
|
|
|
|
| 8 |
# إعداد المسارات لتكون دائماً داخل مجلد Clipping
|
| 9 |
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 10 |
UPLOAD_BASE_DIR = os.path.join(BASE_DIR, "temp_videos")
|
|
|
|
| 3 |
import uvicorn
|
| 4 |
from datetime import datetime
|
| 5 |
from routers import video, files, presets
|
| 6 |
+
from init_defaults import init_all_defaults
|
| 7 |
import os
|
| 8 |
|
| 9 |
+
# تهيئة الإعدادات الافتراضية عند بدء التشغيل
|
| 10 |
+
init_all_defaults()
|
| 11 |
+
|
| 12 |
# إعداد المسارات لتكون دائماً داخل مجلد Clipping
|
| 13 |
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
| 14 |
UPLOAD_BASE_DIR = os.path.join(BASE_DIR, "temp_videos")
|
routers/presets.py
CHANGED
|
@@ -33,6 +33,13 @@ async def save_preset(
|
|
| 33 |
back_box_enabled: bool = Form(True),
|
| 34 |
display_mode: str = Form("word", description="word or sentence"),
|
| 35 |
max_words_per_line: int = Form(3, description="Max words per line in sentence mode"),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
custom_font_file: Optional[UploadFile] = File(None, description="Optional: Upload a .ttf or .otf font file")
|
| 37 |
):
|
| 38 |
"""
|
|
@@ -71,7 +78,14 @@ async def save_preset(
|
|
| 71 |
"highlight_mode": highlight_mode,
|
| 72 |
"back_box_enabled": back_box_enabled,
|
| 73 |
"display_mode": display_mode,
|
| 74 |
-
"max_words_per_line": max_words_per_line
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
}
|
| 76 |
|
| 77 |
# 3. حفظ ملف الـ JSON
|
|
|
|
| 33 |
back_box_enabled: bool = Form(True),
|
| 34 |
display_mode: str = Form("word", description="word or sentence"),
|
| 35 |
max_words_per_line: int = Form(3, description="Max words per line in sentence mode"),
|
| 36 |
+
uppercase: bool = Form(False, description="Convert English text to ALL CAPS"),
|
| 37 |
+
letter_spacing: float = Form(0.0, description="Spacing between characters"),
|
| 38 |
+
line_spacing: int = Form(0, description="Vertical spacing between lines"),
|
| 39 |
+
background_opacity: float = Form(1.0, description="Opacity of background box (0.0 to 1.0)"),
|
| 40 |
+
glow_intensity: int = Form(0, description="Intensity of glow effect (0 to 10)"),
|
| 41 |
+
rotation_angle: float = Form(0.0, description="Rotation angle in degrees"),
|
| 42 |
+
margin_h: int = Form(20, description="Horizontal margin"),
|
| 43 |
custom_font_file: Optional[UploadFile] = File(None, description="Optional: Upload a .ttf or .otf font file")
|
| 44 |
):
|
| 45 |
"""
|
|
|
|
| 78 |
"highlight_mode": highlight_mode,
|
| 79 |
"back_box_enabled": back_box_enabled,
|
| 80 |
"display_mode": display_mode,
|
| 81 |
+
"max_words_per_line": max_words_per_line,
|
| 82 |
+
"uppercase": uppercase,
|
| 83 |
+
"letter_spacing": letter_spacing,
|
| 84 |
+
"line_spacing": line_spacing,
|
| 85 |
+
"background_opacity": background_opacity,
|
| 86 |
+
"glow_intensity": glow_intensity,
|
| 87 |
+
"rotation_angle": rotation_angle,
|
| 88 |
+
"margin_h": margin_h
|
| 89 |
}
|
| 90 |
|
| 91 |
# 3. حفظ ملف الـ JSON
|
routers/subtitle_generator.py
CHANGED
|
@@ -27,6 +27,15 @@ def generate_pro_ass(transcription, preset, output_path):
|
|
| 27 |
secondary = rgb_to_ass(preset.secondary_color)
|
| 28 |
outline = rgb_to_ass(preset.outline_color)
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
header = f"""[Script Info]
|
| 31 |
ScriptType: v4.00+
|
| 32 |
PlayResX: 1080
|
|
@@ -35,8 +44,8 @@ ScaledBorderAndShadow: yes
|
|
| 35 |
|
| 36 |
[V4+ Styles]
|
| 37 |
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
| 38 |
-
Style: Main,{preset.font_name},{preset.font_size},{primary},{secondary},{outline},&H00000000,-1,0,0,0,100,100,
|
| 39 |
-
Style: BackBox,{preset.font_name},{preset.font_size},{secondary},{secondary},&H00000000,
|
| 40 |
"""
|
| 41 |
|
| 42 |
events = "\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
|
@@ -74,52 +83,81 @@ Style: BackBox,{preset.font_name},{preset.font_size},{secondary},{secondary},&H0
|
|
| 74 |
|
| 75 |
if preset.display_mode == "word":
|
| 76 |
# الوضع التقليدي: كلمة بكلمة
|
| 77 |
-
for word_data in sentence.words:
|
|
|
|
| 78 |
start_t = format_ass_time(word_data.start)
|
| 79 |
end_t = format_ass_time(word_data.end)
|
| 80 |
duration = word_data.end - word_data.start
|
| 81 |
|
| 82 |
-
t1 = min(
|
| 83 |
-
t2 = t1 +
|
| 84 |
scale = int(preset.pop_up_scale * 100)
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
if preset.back_box_enabled:
|
| 88 |
-
events += f"Dialogue: 0,{start_t},{end_t},BackBox,,0,0,0,,{{\\bord5\\1a&H40&}}{anim}{
|
| 89 |
-
events += f"Dialogue: 1,{start_t},{end_t},Main,,0,0,0,,{anim}{
|
| 90 |
|
| 91 |
else:
|
| 92 |
# وضع الجملة: تظهر الجملة كاملة والكلمة النشطة تبرز
|
| 93 |
-
#
|
| 94 |
-
full_text = sentence.text
|
| 95 |
-
# جعل النص الأساسي شفاف قليلاً (dimmed) ليبرز الـ Highlight
|
| 96 |
-
dim_alpha = "\\1a&H80&"
|
| 97 |
-
|
| 98 |
-
if preset.back_box_enabled:
|
| 99 |
-
events += f"Dialogue: 0,{s_start},{s_end},BackBox,,0,0,0,,{{{dim_alpha}}}{full_text}\n"
|
| 100 |
-
else:
|
| 101 |
-
events += f"Dialogue: 0,{s_start},{s_end},Main,,0,0,0,,{{{dim_alpha}}}{full_text}\n"
|
| 102 |
|
| 103 |
-
# 2. طبقة الكلمات النشطة (Highlight)
|
| 104 |
for i, target_word in enumerate(sentence.words):
|
| 105 |
w_start = format_ass_time(target_word.start)
|
| 106 |
w_end = format_ass_time(target_word.end)
|
| 107 |
duration = target_word.end - target_word.start
|
| 108 |
|
| 109 |
-
# ب
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
|
| 114 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
scale = int(preset.pop_up_scale * 100)
|
| 116 |
-
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
|
| 119 |
# استخدام \alpha&HFF& لإخفاء الكلمات غير النشطة في هذه الطبقة فقط
|
| 120 |
-
|
| 121 |
|
| 122 |
-
events += f"Dialogue: 1,{w_start},{w_end},Main,,0,0,0,,{
|
| 123 |
|
| 124 |
with open(output_path, "w", encoding="utf-8-sig") as f:
|
| 125 |
f.write(header + events)
|
|
|
|
| 27 |
secondary = rgb_to_ass(preset.secondary_color)
|
| 28 |
outline = rgb_to_ass(preset.outline_color)
|
| 29 |
|
| 30 |
+
# تحويل الشفافية (0.0 - 1.0) إلى صيغة ASS (00 - FF)
|
| 31 |
+
# ملاحظة: في ASS، الـ 00 هو معتم تماماً و FF هو شفاف تماماً
|
| 32 |
+
back_alpha = int((1.0 - getattr(preset, 'background_opacity', 1.0)) * 255)
|
| 33 |
+
back_color = f"&H{back_alpha:02X}000000" # لون خلفية أسود مع شفافية متغيرة
|
| 34 |
+
|
| 35 |
+
spacing = getattr(preset, 'letter_spacing', 0.0)
|
| 36 |
+
angle = getattr(preset, 'rotation_angle', 0.0)
|
| 37 |
+
margin_h = getattr(preset, 'margin_h', 20)
|
| 38 |
+
|
| 39 |
header = f"""[Script Info]
|
| 40 |
ScriptType: v4.00+
|
| 41 |
PlayResX: 1080
|
|
|
|
| 44 |
|
| 45 |
[V4+ Styles]
|
| 46 |
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
|
| 47 |
+
Style: Main,{preset.font_name},{preset.font_size},{primary},{secondary},{outline},&H00000000,-1,0,0,0,100,100,{spacing},{angle},1,{preset.outline_width},{preset.shadow_depth},{preset.alignment},{margin_h},{margin_h},{preset.margin_v},1
|
| 48 |
+
Style: BackBox,{preset.font_name},{preset.font_size},{secondary},{secondary},&H00000000,{back_color},-1,0,0,0,100,100,{spacing},{angle},3,10,0,{preset.alignment},{margin_h},{margin_h},{preset.margin_v},1
|
| 49 |
"""
|
| 50 |
|
| 51 |
events = "\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\n"
|
|
|
|
| 83 |
|
| 84 |
if preset.display_mode == "word":
|
| 85 |
# الوضع التقليدي: كلمة بكلمة
|
| 86 |
+
for i, word_data in enumerate(sentence.words):
|
| 87 |
+
word_text = word_data.word.upper() if preset.uppercase else word_data.word
|
| 88 |
start_t = format_ass_time(word_data.start)
|
| 89 |
end_t = format_ass_time(word_data.end)
|
| 90 |
duration = word_data.end - word_data.start
|
| 91 |
|
| 92 |
+
t1 = min(60, int(duration * 150))
|
| 93 |
+
t2 = t1 + 80
|
| 94 |
scale = int(preset.pop_up_scale * 100)
|
| 95 |
+
|
| 96 |
+
# إضافة تأثير التوهج (Blur) إذا كان مفعلاً
|
| 97 |
+
glow_val = getattr(preset, 'glow_intensity', 0)
|
| 98 |
+
blur_tag = f"\\be{glow_val}" if glow_val > 0 else ""
|
| 99 |
+
|
| 100 |
+
# انيميشن احترافي مع دوران ديناميكي أو ثابت
|
| 101 |
+
if angle != 0:
|
| 102 |
+
rotation = f"\\frz{angle}"
|
| 103 |
+
else:
|
| 104 |
+
rotation = "\\frz-2" if i % 2 == 0 else "\\frz2"
|
| 105 |
+
|
| 106 |
+
anim = f"{{\\fscx80\\fscy80{rotation}{blur_tag}\\t(0,{t1},0.3,\\fscx{scale}\\fscy{scale})\\t({t1},{t2},1.5,\\fscx100\\fscy100)}}"
|
| 107 |
|
| 108 |
if preset.back_box_enabled:
|
| 109 |
+
events += f"Dialogue: 0,{start_t},{end_t},BackBox,,0,0,0,,{{\\bord5\\1a&H40&}}{anim}{word_text}\n"
|
| 110 |
+
events += f"Dialogue: 1,{start_t},{end_t},Main,,0,0,0,,{anim}{word_text}\n"
|
| 111 |
|
| 112 |
else:
|
| 113 |
# وضع الجملة: تظهر الجملة كاملة والكلمة النشطة تبرز
|
| 114 |
+
# قمنا بتعديل المنطق لإخفاء الكلمة الأصلية في الخلفية أثناء ظهور الـ Popup
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
|
|
|
|
| 116 |
for i, target_word in enumerate(sentence.words):
|
| 117 |
w_start = format_ass_time(target_word.start)
|
| 118 |
w_end = format_ass_time(target_word.end)
|
| 119 |
duration = target_word.end - target_word.start
|
| 120 |
|
| 121 |
+
# 1. طبقة الخلفية (الجملة كاملة مع إخفاء الكلمة النشطة حالياً)
|
| 122 |
+
words_list_base = [w.word.upper() if preset.uppercase else w.word for w in sentence.words]
|
| 123 |
+
prefix_base = " ".join(words_list_base[:i]) + (" " if i > 0 else "")
|
| 124 |
+
suffix_base = (" " if i < len(words_list_base)-1 else "") + " ".join(words_list_base[i+1:])
|
| 125 |
+
target_word_base = words_list_base[i]
|
| 126 |
+
|
| 127 |
+
# جعل النص الأساسي شفاف قليلاً (dimmed) وإخفاء الكلمة النشطة بـ \alpha&HFF&
|
| 128 |
+
dim_alpha = "\\1a&H80&"
|
| 129 |
+
base_display_text = f"{{{dim_alpha}}}{prefix_base}{{\\alpha&HFF&}}{target_word_base}{{\\alpha&H00&}}{{{dim_alpha}}}{suffix_base}"
|
| 130 |
|
| 131 |
+
if preset.back_box_enabled:
|
| 132 |
+
events += f"Dialogue: 0,{w_start},{w_end},BackBox,,0,0,0,,{base_display_text}\n"
|
| 133 |
+
else:
|
| 134 |
+
events += f"Dialogue: 0,{w_start},{w_end},Main,,0,0,0,,{base_display_text}\n"
|
| 135 |
+
|
| 136 |
+
# 2. طبقة الكلمات النشطة (Highlight / Popup)
|
| 137 |
+
prefix_hl = " ".join(words_list_base[:i]) + (" " if i > 0 else "")
|
| 138 |
+
suffix_hl = (" " if i < len(words_list_base)-1 else "") + " ".join(words_list_base[i+1:])
|
| 139 |
+
target_word_hl = words_list_base[i]
|
| 140 |
+
|
| 141 |
+
t1 = min(60, int(duration * 150))
|
| 142 |
+
t2 = t1 + 80
|
| 143 |
scale = int(preset.pop_up_scale * 100)
|
| 144 |
+
|
| 145 |
+
# إضافة تأثير التوهج (Blur)
|
| 146 |
+
glow_val = getattr(preset, 'glow_intensity', 0)
|
| 147 |
+
blur_tag = f"\\be{glow_val}" if glow_val > 0 else ""
|
| 148 |
+
|
| 149 |
+
# الـ Highlight يكون بلون الـ secondary وبدون شفافية مع تأثير Pop وميلان
|
| 150 |
+
if angle != 0:
|
| 151 |
+
rotation = f"\\frz{angle}"
|
| 152 |
+
else:
|
| 153 |
+
rotation = "\\frz-2" if i % 2 == 0 else "\\frz2"
|
| 154 |
+
|
| 155 |
+
anim = f"{{\\1c{secondary}\\1a&H00&\\fscx80\\fscy80{rotation}{blur_tag}\\t(0,{t1},0.3,\\fscx{scale}\\fscy{scale})\\t({t1},{t2},1.5,\\fscx100\\fscy100)}}"
|
| 156 |
|
| 157 |
# استخدام \alpha&HFF& لإخفاء الكلمات غير النشطة في هذه الطبقة فقط
|
| 158 |
+
display_text_hl = f"{{\\alpha&HFF&}}{prefix_hl}{anim}{target_word_hl}{{\\alpha&HFF&}}{suffix_hl}"
|
| 159 |
|
| 160 |
+
events += f"Dialogue: 1,{w_start},{w_end},Main,,0,0,0,,{display_text_hl}\n"
|
| 161 |
|
| 162 |
with open(output_path, "w", encoding="utf-8-sig") as f:
|
| 163 |
f.write(header + events)
|
schemas.py
CHANGED
|
@@ -99,6 +99,13 @@ class SubtitlePreset(BaseModel):
|
|
| 99 |
back_box_enabled: bool = True
|
| 100 |
display_mode: str = "word" # "word" or "sentence"
|
| 101 |
max_words_per_line: int = 3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
class ClipRequest(BaseModel):
|
| 104 |
video_url: Optional[str] = None
|
|
|
|
| 99 |
back_box_enabled: bool = True
|
| 100 |
display_mode: str = "word" # "word" or "sentence"
|
| 101 |
max_words_per_line: int = 3
|
| 102 |
+
uppercase: bool = False
|
| 103 |
+
letter_spacing: float = 0.0
|
| 104 |
+
line_spacing: int = 0
|
| 105 |
+
background_opacity: float = 1.0
|
| 106 |
+
glow_intensity: int = 0
|
| 107 |
+
rotation_angle: float = 0.0
|
| 108 |
+
margin_h: int = 20
|
| 109 |
|
| 110 |
class ClipRequest(BaseModel):
|
| 111 |
video_url: Optional[str] = None
|