ex510 commited on
Commit
ab4b623
·
verified ·
1 Parent(s): d9f6776

Upload 3 files

Browse files
templates/__init__.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Template Registry
3
+ بيسجّل كل التمبلتس تلقائياً
4
+ لما تضيف تمبلت جديد — بس استورده هنا
5
+ """
6
+
7
+ from .showcase_arabic import ShowcaseArabic
8
+ # from .showcase_dark import ShowcaseDark ← تمبلت جديد
9
+ # from .minimal_product import MinimalProduct ← تمبلت جديد
10
+
11
+ TEMPLATES = {
12
+ t.NAME: t
13
+ for t in [
14
+ ShowcaseArabic,
15
+ # ShowcaseDark,
16
+ # MinimalProduct,
17
+ ]
18
+ }
19
+
20
+
21
+ def get_template(name: str):
22
+ cls = TEMPLATES.get(name)
23
+ if not cls:
24
+ available = list(TEMPLATES.keys())
25
+ raise ValueError(f"التمبلت '{name}' مش موجود. المتاح: {available}")
26
+ return cls()
27
+
28
+
29
+ def list_templates() -> list:
30
+ return [cls().info for cls in TEMPLATES.values()]
templates/base.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Base Template Class
3
+ كل تمبلت جديد بيرث من الكلاس ده
4
+ """
5
+
6
+ from abc import ABC, abstractmethod
7
+ from PIL import Image
8
+ from dataclasses import dataclass
9
+ from typing import Optional
10
+
11
+
12
+ @dataclass
13
+ class RenderRequest:
14
+ """البيانات اللي بتيجي من n8n"""
15
+ title: str
16
+ discount: str = ""
17
+ badge: str = ""
18
+ phone: str = ""
19
+ website: str = ""
20
+ image_path: str = ""
21
+ music_path: str = ""
22
+ output_path: str = "/tmp/output.mp4"
23
+ # إضافات اختيارية
24
+ bg_left: str = ""
25
+ bg_right: str = ""
26
+ duration: int = 6
27
+ fps: int = 30
28
+ width: int = 1280
29
+ height: int = 720
30
+ music_volume: float = 0.20
31
+
32
+
33
+ class BaseTemplate(ABC):
34
+ """
35
+ الكلاس الأساسي — كل تمبلت بيرث منه
36
+
37
+ عشان تعمل تمبلت جديد:
38
+ 1. عمل ملف في templates/
39
+ 2. ترث من BaseTemplate
40
+ 3. تعمل make_frame بتاعك
41
+ """
42
+
43
+ NAME = "base"
44
+ DESCRIPTION = "Base template"
45
+ AUTHOR = ""
46
+
47
+ def ease_out(self, t: float) -> float:
48
+ return 1 - (1 - t) ** 3
49
+
50
+ def ease_in_out(self, t: float) -> float:
51
+ return t * t * (3 - 2 * t)
52
+
53
+ def load_font(self, size: int):
54
+ from PIL import ImageFont
55
+ candidates = [
56
+ '/tmp/arabic.ttf',
57
+ '/usr/share/fonts/truetype/noto/NotoNaskhArabic-Bold.ttf',
58
+ '/usr/share/fonts/truetype/noto/NotoSansArabic-Bold.ttf',
59
+ '/usr/share/fonts/opentype/noto/NotoNaskhArabic-Bold.otf',
60
+ 'C:/Windows/Fonts/arial.ttf',
61
+ ]
62
+ for path in candidates:
63
+ import os
64
+ if os.path.exists(path):
65
+ try:
66
+ return ImageFont.truetype(path, size)
67
+ except:
68
+ continue
69
+ return ImageFont.load_default()
70
+
71
+ def parse_color(self, s: str, default: tuple) -> tuple:
72
+ try:
73
+ return tuple(int(x) for x in s.split(','))
74
+ except:
75
+ return default
76
+
77
+ @abstractmethod
78
+ def make_frame(self, t: float, req: RenderRequest, product_img, logo_img) -> Image.Image:
79
+ """
80
+ ارسم frame واحد
81
+ t = الوقت الحالي بالثواني
82
+ يرجع PIL Image RGB
83
+ """
84
+ pass
85
+
86
+ @property
87
+ def info(self) -> dict:
88
+ return {
89
+ "name": self.NAME,
90
+ "description": self.DESCRIPTION,
91
+ "author": self.AUTHOR,
92
+ }
templates/showcase_arabic.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Template: showcase_arabic
3
+ تمبلت عرض المنتجات بالعربي
4
+ خلفية بنفسجية + نصوص عربية + badge ذهبي
5
+ """
6
+
7
+ from PIL import Image, ImageDraw
8
+ from .base import BaseTemplate, RenderRequest
9
+
10
+
11
+ class ShowcaseArabic(BaseTemplate):
12
+
13
+ NAME = "showcase_arabic"
14
+ DESCRIPTION = "تمبلت عرض منتج احترافي — خلفية بنفسجية وبادج ذهبي"
15
+ AUTHOR = "your_name"
16
+
17
+ # ألوان افتراضية
18
+ DEFAULT_BG_LEFT = (98, 70, 180)
19
+ DEFAULT_BG_RIGHT = (15, 15, 45)
20
+
21
+ # ============================================================
22
+ def _create_bg(self, w, h, cl, cr):
23
+ img = Image.new('RGBA', (w, h))
24
+ d = ImageDraw.Draw(img)
25
+ for x in range(w):
26
+ if x < w * 0.6:
27
+ r2 = x / (w * 0.6)
28
+ c = (
29
+ int(cl[0] * (1 - r2 * 0.3)),
30
+ int(cl[1] * (1 - r2 * 0.3)),
31
+ int(cl[2] * (1 - r2 * 0.1)),
32
+ 255
33
+ )
34
+ else:
35
+ r2 = (x - w * 0.6) / (w * 0.4)
36
+ c = (
37
+ int(cl[0] * 0.7 * (1-r2) + cr[0] * r2),
38
+ int(cl[1] * 0.7 * (1-r2) + cr[1] * r2),
39
+ int(cl[2] * 0.9 * (1-r2) + cr[2] * r2),
40
+ 255
41
+ )
42
+ d.line([(x, 0), (x, h)], fill=c)
43
+ return img
44
+
45
+ def _draw_badge(self, draw, cx, cy, r, lines, font):
46
+ if r < 5:
47
+ return
48
+ draw.ellipse([cx-r-8, cy-r-8, cx+r+8, cy+r+8], fill=(160, 130, 10))
49
+ draw.ellipse([cx-r, cy-r, cx+r, cy+r ], fill=(200, 160, 20))
50
+ total_h = sum(font.getbbox(l)[3] for l in lines) + (len(lines)-1) * 4
51
+ y = cy - total_h // 2
52
+ for line in lines:
53
+ b = font.getbbox(line)
54
+ draw.text((cx - (b[2]-b[0])//2, y), line, font=font, fill=(255, 255, 255))
55
+ y += b[3] + 4
56
+
57
+ # ============================================================
58
+ def make_frame(self, t, req: RenderRequest, product_img, logo_img):
59
+ W, H, D = req.width, req.height, req.duration
60
+
61
+ cl = self.parse_color(req.bg_left, self.DEFAULT_BG_LEFT)
62
+ cr = self.parse_color(req.bg_right, self.DEFAULT_BG_RIGHT)
63
+
64
+ # خلفية
65
+ frame = self._create_bg(W, H, cl, cr)
66
+
67
+ # زخرفة دوائر
68
+ ov = Image.new('RGBA', (W, H), (0, 0, 0, 0))
69
+ od = ImageDraw.Draw(ov)
70
+ od.ellipse([W*.45, -H*.3, W*1.2, H*1.3], fill=(30, 20, 70, 80))
71
+ od.ellipse([W*.55, H*.3, W*1.1, H*1.1], fill=(20, 15, 50, 60))
72
+ frame = Image.alpha_composite(frame, ov)
73
+
74
+ ft = self.load_font(72)
75
+ fd = self.load_font(48)
76
+ fc = self.load_font(30)
77
+
78
+ # --- صورة المنتج ---
79
+ ip = min(1.0, t / (D * 0.4))
80
+ io = int((1 - self.ease_out(ip)) * -W * 0.5)
81
+ if product_img:
82
+ pw = int(W * 0.42)
83
+ ph = int(product_img.height * pw / product_img.width)
84
+ rs = product_img.resize((pw, ph), Image.LANCZOS)
85
+ px = int(W * 0.02) + io
86
+ py = (H - ph) // 2
87
+ frame.paste(rs, (px, py), rs if rs.mode == 'RGBA' else None)
88
+
89
+ tl = Image.new('RGBA', (W, H), (0, 0, 0, 0))
90
+ td = ImageDraw.Draw(tl)
91
+
92
+ # --- نصوص ---
93
+ tp = min(1.0, max(0.0, (t - D*.2) / (D*.4)))
94
+ to = int((1 - self.ease_out(tp)) * -H * 0.3)
95
+ ta = int(self.ease_out(tp) * 255)
96
+
97
+ RX = int(W * 0.95)
98
+ ty = int(H * 0.15) + to
99
+ for i, line in enumerate(req.title.replace('\\n', '\n').split('\n')):
100
+ b = ft.getbbox(line)
101
+ tw = b[2] - b[0]
102
+ td.text((RX - tw, ty + i*90), line, font=ft, fill=(255, 255, 255, ta))
103
+
104
+ if req.discount:
105
+ b = fd.getbbox(req.discount)
106
+ dw = b[2] - b[0]
107
+ td.text((RX - dw, int(H*.55) + to), req.discount,
108
+ font=fd, fill=(220, 160, 40, ta))
109
+
110
+ # --- معلومات اتصال ---
111
+ cp = min(1.0, max(0.0, (t - D*.5) / (D*.3)))
112
+ ca = int(self.ease_out(cp) * 255)
113
+ cy2 = int(H * 0.68)
114
+ for i, txt in enumerate([req.phone, req.website]):
115
+ if not txt:
116
+ continue
117
+ b = fc.getbbox(txt)
118
+ tw = b[2] - b[0]
119
+ td.text((RX - tw, cy2 + i*42), txt,
120
+ font=fc, fill=(200-i*20, 200-i*20, 200-i*20, ca))
121
+
122
+ frame = Image.alpha_composite(frame, tl)
123
+
124
+ # --- badge ---
125
+ if req.badge:
126
+ bp = min(1.0, max(0.0, (t - D*.1) / (D*.3)))
127
+ bs = self.ease_out(bp)
128
+ if bs > 0.05:
129
+ bl = Image.new('RGBA', (W, H), (0, 0, 0, 0))
130
+ bd = ImageDraw.Draw(bl)
131
+ self._draw_badge(bd,
132
+ int(W*.08), int(H*.18),
133
+ int(70 * bs),
134
+ req.badge.replace('\\n', '\n').split('\n'),
135
+ self.load_font(int(32 * bs)))
136
+ frame = Image.alpha_composite(frame, bl)
137
+
138
+ return frame.convert('RGB')