Spaces:
Paused
Paused
| from PIL import Image, ImageDraw, ImageFont, ImageFilter | |
| import math | |
| # --- استایل «طراحی هوشمند» (AI Dynamic) --- | |
| # هوش مصنوعی (Gemini) هرگز کد اجرا نمیکند؛ فقط یک شیء طراحی محدود (ai_design) با | |
| # چند فیلد از پیش تعریفشده برمیگرداند (entry, exit, timing, stagger, glow, رنگها). | |
| # این استایل دیگر هیچ کادر/باکس/پسزمینهای دور متن یا دور کلمات نمیسازد؛ تمرکز فقط | |
| # روی حرکت ورود/خروج کلمات و رنگ متن است (و در صورت درخواست، درخشش نور اطراف متن). | |
| # مقادیر آن قبلاً در app.py (_clamp_ai_design) کاملاً اعتبارسنجی و clamp شدهاند. | |
| # این فایل صرفاً همان پارامترها را میخواند و واقعاً فریم را رسم میکند؛ به همین دلیل | |
| # خروجی همیشه امن، پایدار و بدون امکان کرش است. | |
| config = { | |
| "ids": ["ai_dynamic"], | |
| "name": "طراحی هوشمند (AI)", | |
| "panels": ["font", "size", "position"], | |
| "default_font": "vazir" | |
| } | |
| frontend_template = """ | |
| <span class="ai-dynamic-word" style=" | |
| display: inline-block; vertical-align: middle; margin: 0 4px; | |
| color: {{COLOR}} !important; opacity: {{OPACITY}}; font-family: inherit; | |
| ">{{WORD}}</span> | |
| """ | |
| DEFAULT_DESIGN = { | |
| "entry": "fall", | |
| "exit": "fade", | |
| "timing": "smooth", | |
| "stagger": 0.04, | |
| "glow": "none", | |
| "primaryColor": "#FFFFFF", | |
| "activeColor": "#FFD700", | |
| "glowColor": "#7C4DFF", | |
| "font": "vazir", | |
| "fontSize": 60, | |
| } | |
| def _get_design(style_config): | |
| raw = getattr(style_config, 'ai_design', None) or {} | |
| if not isinstance(raw, dict): | |
| raw = {} | |
| d = dict(DEFAULT_DESIGN) | |
| for k in DEFAULT_DESIGN: | |
| if k in raw and raw[k] is not None: | |
| d[k] = raw[k] | |
| # سازگاری با طراحیهای قدیمیتر که ممکن است هنوز shapeColor داشته باشند | |
| if "glowColor" not in raw and "shapeColor" in raw: | |
| d["glowColor"] = raw["shapeColor"] | |
| return d | |
| def _ease(name, p): | |
| p = max(0.0, min(1.0, p)) | |
| if name == "linear": | |
| return p | |
| if name == "punch": | |
| # شروع تند و توقف نرم با کمی سرریز (overshoot) خفیف | |
| return 1 - pow(1 - p, 3) | |
| if name == "elastic": | |
| if p <= 0 or p >= 1: | |
| return p | |
| return pow(2, -10 * p) * math.sin((p * 10 - 0.75) * (2 * math.pi) / 3) + 1 | |
| # smooth (پیشفرض) | |
| return 1 - (1 - p) * (1 - p) | |
| def draw_frame(draw, img, width, height, style_config, lines, line_metrics, active_idx, font, color_parser, word_infos=None): | |
| is_rtl = getattr(style_config, 'text_direction', 'rtl') != 'ltr' | |
| design = _get_design(style_config) | |
| primary_color = color_parser(design["primaryColor"], (255, 255, 255, 255)) | |
| active_color = color_parser(design["activeColor"], (255, 215, 0, 255)) | |
| glow_color = color_parser(design["glowColor"], (124, 77, 255, 255)) | |
| line_height_px = int(style_config.fontSize * 1.5) | |
| total_block_height = len(lines) * line_height_px | |
| bottom_reference = height - style_config.marginV | |
| start_y_of_block = bottom_reference - total_block_height | |
| current_render_time = getattr(style_config, 'current_render_time', 0.0) | |
| text_start_y = start_y_of_block | |
| vertical_corr = int(style_config.fontSize * 0.1) | |
| current_line_y = text_start_y - vertical_corr | |
| space_w = draw.textlength(" ", font=font) | |
| global_word_counter = 0 | |
| entry = design["entry"] | |
| exit_type = design["exit"] | |
| timing = design["timing"] | |
| stagger = max(0.0, min(0.12, float(design["stagger"]) if isinstance(design["stagger"], (int, float)) else 0.04)) | |
| glow = design["glow"] | |
| ANIM_DUR = 0.3 | |
| EXIT_DUR = 0.3 | |
| glow_blur_radius = {"none": 0, "soft": 4, "strong": 8}.get(glow, 0) | |
| glow_intensity = {"none": 0, "soft": 160, "strong": 230}.get(glow, 0) | |
| for line_idx, metrics in enumerate(line_metrics): | |
| cursor_x = (width + metrics["width"]) / 2 + (style_config.x or 0) if is_rtl else (width - metrics["width"]) / 2 + (style_config.x or 0) | |
| text_y_pos = current_line_y | |
| for w_i, word in enumerate(metrics["words"]): | |
| w_len = metrics["word_widths"][w_i] | |
| word_x = (cursor_x - w_len) if is_rtl else cursor_x | |
| should_draw = False | |
| dx, dy, alpha_factor = 0.0, 0.0, 1.0 | |
| is_active_word = False | |
| w_info = None | |
| extra_delay = stagger * w_i # تاخیر آبشاری بین کلمات پشتسرهم داخل یک خط | |
| if word_infos and global_word_counter < len(word_infos): | |
| w_info = word_infos[global_word_counter] | |
| if current_render_time >= w_info.start: | |
| should_draw = True | |
| time_passed = max(0.0, (current_render_time - w_info.start) - extra_delay) | |
| progress = min(1.0, time_passed / ANIM_DUR) | |
| eased = _ease(timing, progress) | |
| if entry == "fall": | |
| dy = -70 * (1 - eased) | |
| elif entry == "rise": | |
| dy = 65 * (1 - eased) | |
| elif entry == "pop": | |
| dy = -18 * (1 - eased) | |
| elif entry == "slide_left": | |
| dx = 85 * (1 - eased) | |
| elif entry == "slide_right": | |
| dx = -85 * (1 - eased) | |
| # "fade" فقط آلفا دارد | |
| alpha_factor = max(0.0, min(1.0, eased)) | |
| is_active_word = current_render_time >= w_info.start and current_render_time < w_info.end | |
| # --- افکت خروج بعد از پایان گفتار این کلمه --- | |
| if current_render_time >= w_info.end and exit_type != "none": | |
| time_after = current_render_time - w_info.end | |
| exit_progress = min(1.0, time_after / EXIT_DUR) | |
| exit_eased = _ease("smooth", exit_progress) | |
| if exit_type == "fade": | |
| alpha_factor = max(0.4, 1.0 - exit_eased * 0.6) | |
| elif exit_type == "shrink": | |
| alpha_factor = max(0.35, 1.0 - exit_eased * 0.65) | |
| dy += 6 * exit_eased | |
| elif exit_type == "fall": | |
| dy += 25 * exit_eased | |
| alpha_factor = max(0.0, 1.0 - exit_eased) | |
| elif exit_type == "rise": | |
| dy -= 25 * exit_eased | |
| alpha_factor = max(0.0, 1.0 - exit_eased) | |
| else: | |
| should_draw = False | |
| else: | |
| if active_idx != -1: | |
| should_draw = (global_word_counter <= active_idx) | |
| is_active_word = (global_word_counter == active_idx) | |
| else: | |
| should_draw = False | |
| if should_draw and alpha_factor > 0.01: | |
| this_color = primary_color | |
| if getattr(style_config, 'useActiveColor', True) and is_active_word: | |
| this_color = active_color | |
| if word_infos and global_word_counter < len(word_infos): | |
| w_obj = word_infos[global_word_counter] | |
| if hasattr(w_obj, 'color') and w_obj.color: | |
| this_color = color_parser(w_obj.color, this_color) | |
| final_alpha = int(this_color[3] * alpha_factor) | |
| final_color = (this_color[0], this_color[1], this_color[2], final_alpha) | |
| draw_x = word_x + dx | |
| draw_y = text_y_pos + dy | |
| # --- درخشش نور نرم اطراف متن (بلور واقعی)؛ دیگر هیچ کادر/باکسی رسم نمیشود --- | |
| if glow_blur_radius > 0: | |
| pad = glow_blur_radius * 3 + 8 | |
| layer_w = int(w_len) + pad * 2 | |
| layer_h = int(style_config.fontSize * 1.3) + pad * 2 | |
| if layer_w > 0 and layer_h > 0: | |
| glow_layer = Image.new('RGBA', (layer_w, layer_h), (0, 0, 0, 0)) | |
| glow_draw = ImageDraw.Draw(glow_layer) | |
| glow_a = int(glow_intensity * alpha_factor) | |
| glow_draw.text((pad, pad), word, font=font, fill=(*glow_color[:3], glow_a), | |
| direction=getattr(style_config, 'text_direction', 'rtl'), language=getattr(style_config, 'lang_code', 'fa')) | |
| blurred = glow_layer.filter(ImageFilter.GaussianBlur(glow_blur_radius)) | |
| img.paste(blurred, (int(draw_x - pad), int(draw_y - pad)), blurred) | |
| draw.text((draw_x, draw_y), word, font=font, fill=final_color, | |
| direction=getattr(style_config, 'text_direction', 'rtl'), language=getattr(style_config, 'lang_code', 'fa')) | |
| cursor_x += (-(w_len + space_w) if is_rtl else (w_len + space_w)) | |
| global_word_counter += 1 | |
| current_line_y += line_height_px | |