Spaces:
Running
Running
| import gradio as gr | |
| from faster_whisper import WhisperModel | |
| import os | |
| import subprocess | |
| import cv2 | |
| import asyncio | |
| import edge_tts | |
| import shutil | |
| import time | |
| import numpy as np | |
| import re | |
| import json | |
| import uuid # 👈 Session ID ထုတ်ရန်အတွက် Import လုပ်ခြင်း | |
| from google import genai | |
| from datetime import datetime, date | |
| from PIL import Image, ImageDraw, ImageFont | |
| # ===================================================================== | |
| # ⚙️ SECURITY SETTINGS & LOGIN CONFIG | |
| # ===================================================================== | |
| MAX_SUBTITLE_DURATION_SECONDS = 7.0 | |
| # 👥 LOGIN ACCOUNTS CONFIG (အကောင့်တစ်ခုတည်းကို လူအများ မျှသုံးနိုင်ပါသည်) | |
| ACCOUNTS = { | |
| "user1": "123123", # Free User (Session/Browser အလိုက် တစ်ရက် ၂ ပုဒ်) | |
| "admin": "admin789" # Admin User (Unlimited) | |
| } | |
| EXPIRY_DATE_STR = "2026-8-31" | |
| # 🛑 USER RATE LIMIT TRACKER (Session/Browser ID အလိုက် ရေတွက်မည်) | |
| USER_LIMIT_TRACKER = {} | |
| SYSTEM_INSTRUCTION = "You are a professional movie recap writer. Translate movie subtitle lines into natural, engaging, and thrilling Burmese movie recap style. Keep it concise." | |
| MODEL_NAME = "gemini-2.5-flash" | |
| def check_app_expiry(): | |
| try: | |
| expiry_date = datetime.strptime(EXPIRY_DATE_STR, "%Y-%m-%d").date() | |
| current_date = datetime.now().date() | |
| if current_date > expiry_date: | |
| return False, f"❌ ဤ App သည် သက်တမ်းကုန်ဆုံးသွားပါပြီ ({EXPIRY_DATE_STR})။" | |
| return True, "Active" | |
| except Exception as e: | |
| return False, f"လုံခြုံရေး စစ်ဆေးမှု မှားယွင်းနေပါသည်- {str(e)}" | |
| print("Loading Multilingual Faster-Whisper Base Model...") | |
| model = WhisperModel("base", device="cpu", compute_type="int8", cpu_threads=4) | |
| # ===================================================================== | |
| # 🖼️ REAL-TIME INTERACTIVE PREVIEW GENERATOR | |
| # ===================================================================== | |
| def update_preview_image(video_path, blur_y_percent, blur_strength): | |
| if not video_path: return None | |
| try: | |
| cap = cv2.VideoCapture(video_path) | |
| ret, frame = cap.read() | |
| cap.release() | |
| if not ret: return None | |
| h_o, w_o, _ = frame.shape | |
| b_h = int(h_o * 0.12) | |
| b_y = max(0, min(int(h_o * (blur_y_percent / 100)) - (b_h // 2), h_o - b_h)) | |
| k_size = int(blur_strength) | |
| if k_size % 2 == 0: k_size += 1 | |
| preview_frame = frame.copy() | |
| roi = preview_frame[b_y:b_y+b_h, 0:w_o] | |
| if roi.shape[0] > 0 and roi.shape[1] > 0: | |
| small_roi = cv2.resize(roi, (w_o // 4, b_h // 4), interpolation=cv2.INTER_LINEAR) | |
| blurred_small = cv2.GaussianBlur(small_roi, (k_size // 4 | 1, k_size // 4 | 1), 0) | |
| preview_frame[b_y:b_y+b_h, 0:w_o] = cv2.resize(blurred_small, (w_o, b_h), interpolation=cv2.INTER_LINEAR) | |
| cv2.rectangle(preview_frame, (0, b_y), (w_o, b_y+b_h), (0, 0, 255), 3) | |
| preview_rgb = cv2.cvtColor(preview_frame, cv2.COLOR_BGR2RGB) | |
| return Image.fromarray(preview_rgb) | |
| except Exception as e: | |
| print(f"Preview Error: {e}") | |
| return None | |
| # ===================================================================== | |
| # ⚡ DYNAMIC USER-PROVIDED API KEY TRANSLATION ENGINE | |
| # ===================================================================== | |
| def google_backup_translate(text, source_lang="en"): | |
| from urllib.parse import quote | |
| import urllib.request | |
| import json | |
| try: | |
| url = f"https://translate.googleapis.com/translate_a/single?client=gtx&sl={source_lang}&tl=my&dt=t&q={quote(text)}" | |
| req = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0'}) | |
| response = urllib.request.urlopen(req, timeout=5).read().decode('utf-8') | |
| result = json.loads(response) | |
| return result[0][0][0] or text | |
| except: | |
| return text | |
| def translate_segments_batch(segments, user_api_key, source_lang="en"): | |
| if not segments: return segments | |
| if not user_api_key or not user_api_key.strip(): | |
| print(f"⚠️ User က Gemini API Key မထည့်ထားပါ။ Google Translate Backup စနစ်ဖြင့် ဘာသာပြန်နေပါသည်။ (Source Lang: {source_lang})") | |
| for seg in segments: | |
| seg['mm_text'] = google_backup_translate(seg['text'], source_lang=source_lang) | |
| return segments | |
| payload_dict = {str(idx): seg['text'] for idx, seg in enumerate(segments)} | |
| large_prompt_text = json.dumps(payload_dict, ensure_ascii=False, indent=2) | |
| prompt = f""" | |
| You are an expert movie recap translator. Translate the following movie subtitle lines (which are originally in '{source_lang}' language) into thrilling, natural, and engaging Burmese movie recap style. | |
| CRITICAL: You MUST respond in valid JSON format only, keeping the exact same keys (0, 1, 2, etc.) as the input. The values should be the translated Burmese text. | |
| Do NOT include any markdown formatting like ```json or ``` in your response. Respond with pure JSON raw string only. | |
| Input Data: | |
| {large_prompt_text} | |
| """ | |
| translated_map = {} | |
| try: | |
| client = genai.Client(api_key=user_api_key.strip()) | |
| response = client.models.generate_content( | |
| model=MODEL_NAME, | |
| contents=prompt, | |
| config={ | |
| "system_instruction": SYSTEM_INSTRUCTION, | |
| "temperature": 0.3, | |
| } | |
| ) | |
| response_text = response.text.strip() | |
| if response_text.startswith("```"): | |
| response_text = response_text.split("\n", 1)[1].rsplit("\n", 1)[0].strip() | |
| if response_text.startswith("json"): | |
| response_text = response_text.split("\n", 1)[1].strip() | |
| translated_map = json.loads(response_text) | |
| except Exception as e: | |
| print(f"⚠️ User Gemini API Error သို့မဟုတ် Format Error: {e} -> Google Translate သို့ ပြောင်းလဲနေသည်။") | |
| for idx, seg in enumerate(segments): | |
| key_str = str(idx) | |
| if key_str in translated_map and translated_map[key_str]: | |
| seg['mm_text'] = translated_map[key_str] | |
| else: | |
| seg['mm_text'] = google_backup_translate(seg['text'], source_lang=source_lang) | |
| return segments | |
| # ===================================================================== | |
| # 🎬 TEXT WRAPPING & RENDER SYSTEMS | |
| # ===================================================================== | |
| def segment_myanmar_syllables(text): | |
| return re.findall(r'[a-zA-Z0-9\s\-\.,!\?]+|[\u1000-\u102a\u103f\u1040-\u1049]+[\u102b-\u103e\u1060-\u109f]*|[^\s]', text) | |
| def wrap_text_myanmar_smart(text, font, max_width, draw): | |
| cleaned_text = text.replace(" ြ", "ြ").replace("ြ ", "ြ").strip() | |
| tokens = segment_myanmar_syllables(cleaned_text) | |
| lines, current_line = [], "" | |
| for token in tokens: | |
| test_line = current_line + token | |
| bbox = draw.textbbox((0, 0), test_line, font=font) | |
| if (bbox[2] - bbox[0]) <= max_width: | |
| current_line = test_line | |
| else: | |
| if current_line: lines.append(current_line.strip()) | |
| current_line = token | |
| if current_line: lines.append(current_line.strip()) | |
| return lines | |
| def hex_to_rgb(hex_str): | |
| if not hex_str: return (255, 255, 0) | |
| hex_str = hex_str.lstrip('#') | |
| return tuple(int(hex_str[i:i+2], 16) for i in (0, 2, 4)) | |
| def draw_line_perfect_rendering(draw, position, text, font_primary, fill_color, stroke_w, stroke_c): | |
| x, y = position | |
| clean_text = text.replace(" ြ", "ြ").replace("ြ ", "ြ") | |
| draw.text((x, y), clean_text, font=font_primary, fill=fill_color, stroke_width=stroke_w, stroke_fill=stroke_c) | |
| def generate_voice_sync(text, voice_id, filename, desired_speed, target_duration_sec=None): | |
| rate_percentage = int((desired_speed - 1.0) * 100) | |
| rate_str = f"{'+' if rate_percentage >= 0 else ''}{rate_percentage}%" | |
| if target_duration_sec and target_duration_sec > 0: | |
| char_count = len(text) | |
| cps = char_count / target_duration_sec | |
| if cps > 15: rate_str = f"+{rate_percentage + 30}%" | |
| elif cps > 11: rate_str = f"+{rate_percentage + 15}%" | |
| elif cps > 7: rate_str = f"+{rate_percentage + 5}%" | |
| async def _async_gen(): | |
| communicate = edge_tts.Communicate(text, voice_id, rate=rate_str) | |
| await communicate.save(filename) | |
| try: loop = asyncio.get_event_loop() | |
| except RuntimeError: | |
| loop = asyncio.new_event_loop() | |
| asyncio.set_event_loop(loop) | |
| if loop.is_running(): | |
| import threading | |
| t = threading.Thread(target=lambda: asyncio.run(_async_gen())) | |
| t.start() | |
| t.join() | |
| else: | |
| loop.run_until_complete(_async_gen()) | |
| # ===================================================================== | |
| # 🎬 HIGH-SPEED PRODUCTION AUTOMATION ENGINE | |
| # ===================================================================== | |
| def process_magic_recap_video( | |
| video_path, user_api_key, ratio_select, background_fill, enable_zoom, zoom_level, | |
| logo_file, mirror_flip, filter_color, voice_gender, tone_style, | |
| text_color, stroke_color, blur_y_percent, blur_strength, sub_pos_percent, desired_speed, | |
| session_id, # 👈 Browser Session State | |
| request: gr.Request, | |
| progress=gr.Progress(track_tqdm=True) | |
| ): | |
| is_valid, msg = check_app_expiry() | |
| if not is_valid: raise gr.Error(msg) | |
| if not video_path: return None | |
| # 🛑 LOGIN ACCOUNT & SESSION ID ယူခြင်း | |
| login_username = request.username if (request and hasattr(request, 'username')) else "user1" | |
| if login_username == "admin": | |
| user_identifier = "admin" | |
| else: | |
| # Browser Session ID ဖြင့် လူတစ်ဦးစီကို သီးခြား ခွဲခြားမည် | |
| user_identifier = f"session_{session_id}" | |
| current_timestamp = time.time() | |
| print(f"👤 အသုံးပြုသူ Account: {login_username} | Identifier: {user_identifier}") | |
| # Admin မဟုတ်ပါက Session/Browser အလိုက် ၁ ရက် (၂) ပုဒ် Limit စစ်ဆေးမည် | |
| if user_identifier != "admin": | |
| if user_identifier in USER_LIMIT_TRACKER: | |
| user_data = USER_LIMIT_TRACKER[user_identifier] | |
| first_time = user_data["first_time"] | |
| count = user_data["count"] | |
| elapsed_time = current_timestamp - first_time | |
| if elapsed_time < 86400: # ၂၄ နာရီ အတွင်း | |
| if count >= 2: # ၂ ပုဒ် ပြည့်ပါက တားဆီးမည် | |
| remaining_seconds = 86400 - elapsed_time | |
| rem_hours = int(remaining_seconds // 3600) | |
| rem_mins = int((remaining_seconds % 3600) // 60) | |
| raise gr.Error(f"❌ ခွင့်ပြုချက်ကျော်လွန်နေပါသည်။ သင့် Browser မှ တစ်ရက်လျှင် ဗီဒီယို (၂) ပုဒ်သာ ထုတ်ယူခွင့်ရှိသည်။ ပြန်လည်စမ်းသပ်ရန် {rem_hours} နာရီ {rem_mins} မိနစ် လိုအပ်ပါသေးသည်။") | |
| else: | |
| USER_LIMIT_TRACKER[user_identifier] = {"count": 0, "first_time": current_timestamp} | |
| else: | |
| USER_LIMIT_TRACKER[user_identifier] = {"count": 0, "first_time": current_timestamp} | |
| temp_dir = "temp_space_workspace" | |
| if os.path.exists(temp_dir): shutil.rmtree(temp_dir) | |
| os.makedirs(temp_dir, exist_ok=True) | |
| try: | |
| progress(0.10, desc="🎙️ Faster-Whisper ဖြင့် စာသားဖတ်နေပါသည်...") | |
| segments_raw, info = model.transcribe(video_path, beam_size=1) | |
| raw_segments = [{"start": seg.start, "end": seg.end, "text": seg.text} for seg in segments_raw] | |
| cap = cv2.VideoCapture(video_path) | |
| fps = cap.get(cv2.CAP_PROP_FPS) or 30.0 | |
| orig_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) | |
| orig_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) | |
| video_duration = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) / fps | |
| if not raw_segments: | |
| raw_segments = [{'start': 0.0, 'end': min(6.0, video_duration), 'text': "Welcome to this movie recap."}] | |
| segments = [] | |
| for seg in raw_segments: | |
| s_start = seg['start'] | |
| s_end = seg['end'] | |
| s_text = seg['text'].strip() | |
| if not s_text: continue | |
| dur = s_end - s_start | |
| if dur > MAX_SUBTITLE_DURATION_SECONDS and MAX_SUBTITLE_DURATION_SECONDS > 0: | |
| words = s_text.split() | |
| chunks_count = int(np.ceil(dur / MAX_SUBTITLE_DURATION_SECONDS)) | |
| words_per_chunk = int(np.ceil(len(words) / chunks_count)) | |
| for i in range(chunks_count): | |
| w_sub = words[i*words_per_chunk : (i+1)*words_per_chunk] | |
| if not w_sub: continue | |
| segments.append({'start': s_start + (i * (dur / chunks_count)), 'end': min(s_end, s_start + ((i+1) * (dur / chunks_count))), 'text': " ".join(w_sub)}) | |
| else: | |
| segments.append({'start': s_start, 'end': s_end, 'text': s_text}) | |
| detected_lang = info.language | |
| if user_api_key and user_api_key.strip(): | |
| progress(0.30, desc=f"⚡ ထည့်သွင်းထားသော Gemini API ({detected_lang}) စနစ်ဖြင့် ဘာသာပြန်နေပါသည်...") | |
| else: | |
| progress(0.30, desc=f"🌐 Google Translate Backup ({detected_lang}) စနစ်ဖြင့် ဘာသာပြန်နေပါသည်...") | |
| segments = translate_segments_batch(segments, user_api_key, source_lang=detected_lang) | |
| if ratio_select == "9:16 (Tiktok/Reels)": target_w, target_h = 720, 1280 | |
| else: target_w, target_h = 1280, 720 | |
| logo_img = None | |
| if logo_file: | |
| try: | |
| logo_cv = cv2.imread(logo_file.name, cv2.IMREAD_UNCHANGED) | |
| if logo_cv is not None: | |
| l_w = int(target_w * 0.16) | |
| logo_img = cv2.resize(logo_cv, (l_w, int(l_w * (logo_cv.shape[0] / logo_cv.shape[1])))) | |
| except: pass | |
| progress(0.50, desc="🎙️ AI အသံများ စတင်ဖန်တီးနေပါသည်...") | |
| audio_segments = [] | |
| python_srt_segments = [] | |
| voice_id = "my-MM-NilarNeural" if "မိန်းကလေး" in voice_gender else "my-MM-ThihaNeural" | |
| v_segments_time_map = [] | |
| total_adjusted_duration = 0.0 | |
| for idx, seg in enumerate(segments): | |
| mm_text = seg.get('mm_text', seg['text']) | |
| mm_text = mm_text.replace(" ြ", "ြ").replace("ြ ", "ြ").strip() | |
| orig_start, orig_end = float(seg.get('start', 0.0)), float(seg.get('end', 0.0)) | |
| orig_dur = orig_end - orig_start if (orig_end - orig_start) > 0 else 2.0 | |
| raw_seg_filename = os.path.join(temp_dir, f"raw_{idx}.mp3") | |
| fixed_seg_filename = os.path.join(temp_dir, f"fixed_{idx}.mp3") | |
| generate_voice_sync(mm_text, voice_id, raw_seg_filename, 1.15, orig_dur) | |
| if os.path.exists(raw_seg_filename) and os.path.getsize(raw_seg_filename) > 0: | |
| subprocess.run([ | |
| 'ffmpeg', '-y', '-i', raw_seg_filename, | |
| '-filter:a', f"atempo={desired_speed}", | |
| fixed_seg_filename | |
| ], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| if os.path.exists(fixed_seg_filename) and os.path.getsize(fixed_seg_filename) > 0: | |
| probe_res = subprocess.run(['ffprobe', '-v', 'error', '-show_entries', 'format=duration', '-of', 'default=noprint_wrappers=1:nokey=1', fixed_seg_filename], stdout=subprocess.PIPE, text=True) | |
| try: audio_dur = float(probe_res.stdout.strip()) | |
| except: audio_dur = orig_dur / desired_speed | |
| python_srt_segments.append({'start': total_adjusted_duration, 'end': total_adjusted_duration + audio_dur, 'text': mm_text}) | |
| audio_segments.append(fixed_seg_filename) | |
| v_segments_time_map.append({'orig_start': orig_start, 'orig_end': orig_end, 'new_start': total_adjusted_duration, 'new_end': total_adjusted_duration + audio_dur, 'pts_ratio': audio_dur / orig_dur}) | |
| total_adjusted_duration += audio_dur | |
| progress(0.70, desc="⚡ Render ဗီဒီယိုနှင့် စာတန်းထိုးများ ပေါင်းစပ်နေပါသည်...") | |
| output_video_path = os.path.abspath("magic_recap_output.mp4") | |
| if os.path.exists(output_video_path): os.remove(output_video_path) | |
| final_burn_temp = os.path.join(temp_dir, "final_burn_temp.mp4") | |
| video_writer = cv2.VideoWriter(final_burn_temp, cv2.VideoWriter_fourcc(*'mp4v'), fps, (target_w, target_h)) | |
| font_size = int(target_h * 0.038) | |
| font_path = "Myanmar font.ttf" | |
| font_primary = ImageFont.truetype(font_path, font_size) if os.path.exists(font_path) else ImageFont.load_default() | |
| t_color, s_color = hex_to_rgb(text_color), hex_to_rgb(stroke_color) | |
| b_h = int(target_h * 0.12) | |
| b_y = max(0, min(int(target_h * (blur_y_percent / 100)) - (b_h // 2), target_h - b_h)) | |
| m_w, s_w = int(target_w * 0.90), max(2, int(font_size * 0.12)) | |
| k_size = int(blur_strength) | 1 | |
| total_output_frames = int(total_adjusted_duration * fps) | |
| for f_out_idx in range(total_output_frames): | |
| c_sec = f_out_idx / fps | |
| target_orig_sec = 0.0 | |
| for mapping in v_segments_time_map: | |
| if mapping['new_start'] <= c_sec <= mapping['new_end']: | |
| target_orig_sec = mapping['orig_start'] + ((c_sec - mapping['new_start']) / mapping['pts_ratio']) | |
| break | |
| else: | |
| if v_segments_time_map: target_orig_sec = v_segments_time_map[-1]['orig_end'] | |
| target_frame_idx = int(target_orig_sec * fps) | |
| cap.set(cv2.CAP_PROP_POS_FRAMES, target_frame_idx) | |
| ret, orig_frame = cap.read() | |
| if not ret or orig_frame is None: | |
| orig_frame = np.zeros((orig_h, orig_w, 3), dtype=np.uint8) | |
| if background_fill == "Blur Background (အနောက်ခံ ဝါးမည်)": | |
| small_bg = cv2.resize(orig_frame, (target_w // 4, target_h // 4), interpolation=cv2.INTER_LINEAR) | |
| blurred_small_bg = cv2.blur(small_bg, (11, 11)) | |
| bg_layer = cv2.resize(blurred_small_bg, (target_w, target_h), interpolation=cv2.INTER_LINEAR) | |
| else: | |
| bg_layer = np.zeros((target_h, target_w, 3), dtype=np.uint8) | |
| if enable_zoom and zoom_level > 1.0: | |
| fg_cropped = orig_frame[int((orig_h - orig_h/zoom_level)//2):int((orig_h + orig_h/zoom_level)//2), int((orig_w - orig_w/zoom_level)//2):int((orig_w + orig_w/zoom_level)//2)] | |
| else: | |
| fg_cropped = orig_frame | |
| fg_w = target_w | |
| fg_h = int(fg_w / (fg_cropped.shape[1] / fg_cropped.shape[0])) | |
| if fg_h > target_h: | |
| fg_h = target_h | |
| fg_w = int(fg_h * (fg_cropped.shape[1] / fg_cropped.shape[0])) | |
| fg_resized = cv2.flip(cv2.resize(fg_cropped, (fg_w, fg_h)), 1) if mirror_flip else cv2.resize(fg_cropped, (fg_w, fg_h)) | |
| if filter_color == "Chrome Cool": fg_resized = cv2.convertScaleAbs(fg_resized, alpha=1.0, beta=15) | |
| elif filter_color == "Warm Cinema": fg_resized = cv2.convertScaleAbs(fg_resized, alpha=1.05, beta=5) | |
| else: fg_resized = cv2.convertScaleAbs(fg_resized, alpha=0.99, beta=2) | |
| bg_layer[(target_h - fg_h)//2:(target_h - fg_h)//2+fg_h, (target_w - fg_w)//2:(target_w - fg_w)//2+fg_w] = fg_resized | |
| frame = bg_layer | |
| if logo_img is not None: | |
| ly, lx = 25, target_w - logo_img.shape[1] - 25 | |
| if logo_img.shape[2] == 4: | |
| alpha_l = logo_img[:, :, 3] / 255.0 | |
| for c in range(3): frame[ly:ly+logo_img.shape[0], lx:lx+logo_img.shape[1], c] = alpha_l * logo_img[:, :, c] + (1.0 - alpha_l) * frame[ly:ly+logo_img.shape[0], lx:lx+logo_img.shape[1], c] | |
| else: frame[ly:ly+logo_img.shape[0], lx:lx+logo_img.shape[1]] = logo_img[:, :, :3] | |
| if b_h > 0 and (b_y + b_h) <= target_h: | |
| roi = frame[b_y:b_y+b_h, 0:target_w] | |
| if roi.shape[0] > 0 and roi.shape[1] > 0: | |
| roi_small = cv2.resize(roi, (target_w // 4, b_h // 4), interpolation=cv2.INTER_LINEAR) | |
| roi_blur = cv2.GaussianBlur(roi_small, (k_size // 4 | 1, k_size // 4 | 1), 0) | |
| frame[b_y:b_y+b_h, 0:target_w] = cv2.resize(roi_blur, (target_w, b_h), interpolation=cv2.INTER_LINEAR) | |
| text_str = "" | |
| for s in python_srt_segments: | |
| if s['start'] <= c_sec <= s['end']: | |
| text_str = s['text'] | |
| break | |
| if text_str and isinstance(font_primary, ImageFont.FreeTypeFont): | |
| pil_img = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) | |
| draw = ImageDraw.Draw(pil_img) | |
| sub_lines = wrap_text_myanmar_smart(text_str, font_primary, m_w, draw) | |
| total_text_height = sum([draw.textbbox((0, 0), l, font=font_primary)[3] - draw.textbbox((0, 0), l, font=font_primary)[1] for l in sub_lines]) | |
| curr_y = int(target_h - total_text_height - (target_h * (sub_pos_percent / 100))) | |
| for line in sub_lines: | |
| clean_line = line.replace(" ြ", "ြ").replace("ြ ", "ြ") | |
| bbox = draw.textbbox((0, 0), clean_line, font=font_primary) | |
| draw_line_perfect_rendering(draw, ((target_w - (bbox[2] - bbox[0])) // 2, curr_y), clean_line, font_primary, t_color, s_w, s_color) | |
| curr_y += (bbox[3] - bbox[1]) + 10 | |
| frame = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR) | |
| video_writer.write(frame) | |
| cap.release() | |
| video_writer.release() | |
| merged_audio_path = os.path.join(temp_dir, "final_speech_track.mp3") | |
| if audio_segments: | |
| inputs_cmd = [] | |
| for idx, audio_file in enumerate(audio_segments): inputs_cmd.extend(['-i', audio_file]) | |
| subprocess.run(['ffmpeg', '-y'] + inputs_cmd + ['-filter_complex', f"concat=n={len(audio_segments)}:v=0:a=1[outa]", '-map', '[outa]', '-c:a', 'libmp3lame', '-b:a', '192k', merged_audio_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| has_audio = os.path.exists(merged_audio_path) and os.path.getsize(merged_audio_path) > 0 | |
| else: has_audio = False | |
| progress(0.90, desc="⚡ ရုပ်သံနှင့် အသံလှိုင်းများကို အပြီးသတ် ပေါင်းစပ်နေပါသည်...") | |
| if has_audio: | |
| subprocess.run(['ffmpeg', '-y', '-i', final_burn_temp, '-i', merged_audio_path, '-c:v', 'libx264', '-preset', 'ultrafast', '-threads', '0', '-pix_fmt', 'yuv420p', '-c:a', 'aac', '-b:a', '192k', '-map', '0:v:0', '-map', '1:a:0', '-shortest', output_video_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| else: | |
| subprocess.run(['ffmpeg', '-y', '-i', final_burn_temp, '-c:v', 'libx264', '-preset', 'ultrafast', '-threads', '0', '-pix_fmt', 'yuv420p', output_video_path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) | |
| if os.path.exists(temp_dir): shutil.rmtree(temp_dir) | |
| # 📈 ဗီဒီယို အောင်မြင်စွာ ထုတ်ယူပြီးမှသာ ထို Session ၏ Count ကို ၁ တိုးမည် | |
| if user_identifier != "admin": | |
| USER_LIMIT_TRACKER[user_identifier]["count"] += 1 | |
| print(f"✅ [{user_identifier}] ဗီဒီယိုထုတ်ယူမှု အောင်မြင်ပါသည်။ ယနေ့ ထုတ်ယူပြီးစီးမှု: {USER_LIMIT_TRACKER[user_identifier]['count']} ကြိမ်") | |
| return output_video_path | |
| except Exception as e: | |
| if os.path.exists(temp_dir): shutil.rmtree(temp_dir) | |
| raise gr.Error(f"❌ အမှားအယွင်း တစ်ခု ဖြစ်ပွားခဲ့သည်- {str(e)}") | |
| # ===================================================================== | |
| # 🎨 GRADIO INTERFACE | |
| # ===================================================================== | |
| def create_main_app_interface(): | |
| is_valid, msg = check_app_expiry() | |
| with gr.Blocks() as main_app: | |
| # 🔑 အသုံးပြုသူတိုင်းအတွက် သီးသန့် Session ID ထုတ်ပေးခြင်း | |
| session_id_state = gr.State(lambda: str(uuid.uuid4())) | |
| gr.Markdown("<h1 style='text-align: center; color: #5B50F3;'>✨ Video Auto Recap (Multi-Account) ✨</h1>") | |
| if not is_valid: | |
| gr.Markdown(f"### {msg}") | |
| return main_app | |
| gr.Markdown("<div style='background-color: #333; padding: 10px; border-radius: 8px; text-align: center; color: #fff;'>📢 <b>Free App Rules:</b> တစ်ရက်လျှင် Video (၂) ပုဒ် အခမဲ့ ထုတ်ယူခွင့် ရှိပါသည်</div>") | |
| with gr.Row(): | |
| with gr.Column(scale=2): | |
| video_input = gr.Video(label="🎥 ဗီဒီယို ထည့်ရန်", sources=["upload"]) | |
| preview_image = gr.Image(label="🖼️ Interactive Blur Preview", interactive=False) | |
| with gr.Column(scale=1): | |
| user_api_key = gr.Textbox(label="🔑 သင်၏ Gemini API Key ထည့်ရန် (မထည့်ပါက Google Translate ဖြင့် လုပ်ဆောင်မည်)", placeholder="AIZAcy... (Optional)", type="password") | |
| ratio_select = gr.Dropdown(choices=["9:16 (Tiktok/Reels)", "16:9 (Landscape)"], value="9:16 (Tiktok/Reels)", label="ဗီဒီယိုအမျိုးအစား") | |
| background_fill = gr.Radio(choices=["Blur Background (အနောက်ခံ ဝါးမည်)", "Black Background (အမည်းရောင်ထားမည်)"], value="Blur Background (အနောက်ခံ ဝါးမည်)", label="နောက်ခံ ဖြည့်စွက်မှု") | |
| with gr.Column(variant="panel"): | |
| gr.Markdown("### 🗣️ အသံနှင့် စာတန်းထိုး ဆက်တင်များ") | |
| voice_select = gr.Radio(choices=["🧕 မိန်းကလေး (Female)", "👨 ယောကျာ်လေး (Male)"], value="🧕 မိန်းကလေး (Female)", label="AI အသံ အမျိုးအစား") | |
| tone_style = gr.Dropdown(choices=["Thriller", "Comedy", "Dramatic", "Action/Epic"], value="Thriller", label="🎬 Narrative Tone") | |
| text_color_input = gr.ColorPicker(label="စာလုံးအရောင်", value="#FFFF00") | |
| stroke_color_input = gr.ColorPicker(label="အနားသတ်အရောင်", value="#000000") | |
| sub_pos_percent = gr.Slider(minimum=0, maximum=100, value=15, step=1, label="မြန်မာစာတန်း တည်နေရာ %") | |
| blur_y_percent = gr.Slider(minimum=50, maximum=100, value=75, step=1, label="📍 မူရင်းစာတန်းဖျောက်မည့်နေရာ %") | |
| blur_strength = gr.Slider(minimum=5, maximum=151, value=51, step=2, label="🌫️ မူရင်းစာတန်း ဝါးမည့်ပမာဏ") | |
| desired_speed = gr.Slider(minimum=1.0, maximum=1.6, value=1.35, step=0.05, label="🎙️🎬 အသံနှင့် ဗီဒီယို အရှိန်မြှင့်နှုန်း") | |
| with gr.Accordion("⚙️ အဆင့်မြင့် ဆက်တင်များ", open=False): | |
| enable_zoom = gr.Checkbox(label="Zoom & Crop သုံးရန်", value=False) | |
| zoom_level = gr.Slider(minimum=1.0, maximum=3.0, value=1.0, step=0.1, label="Zoom Level") | |
| logo_file = gr.File(label="လိုဂို ထည့်ရန် (Optional)") | |
| mirror_flip = gr.Checkbox(label="ဘယ်ညာ ပြောင်းရန် (Mandatory Auto-Active)", value=True) | |
| filter_color = gr.Dropdown(choices=["None (အလိုအလျောက်ကုဒ်ပြောင်းမည်)", "Chrome Cool", "Warm Cinema"], value="None (အလိုအလျောက်ကုဒ်ပြောင်းမည်)", label="ဗီဒီယို Filter") | |
| submit_btn = gr.Button("🚀 Generate Video", variant="primary") | |
| with gr.Column(variant="panel"): | |
| output_video = gr.Video(label="✅ ပြီးပြည့်စုံသော ဗီဒီယို") | |
| video_input.change(fn=update_preview_image, inputs=[video_input, blur_y_percent, blur_strength], outputs=preview_image) | |
| blur_y_percent.change(fn=update_preview_image, inputs=[video_input, blur_y_percent, blur_strength], outputs=preview_image) | |
| blur_strength.change(fn=update_preview_image, inputs=[video_input, blur_y_percent, blur_strength], outputs=preview_image) | |
| submit_btn.click( | |
| fn=process_magic_recap_video, | |
| inputs=[ | |
| video_input, user_api_key, ratio_select, background_fill, enable_zoom, zoom_level, | |
| logo_file, mirror_flip, filter_color, voice_select, tone_style, text_color_input, | |
| stroke_color_input, blur_y_percent, blur_strength, sub_pos_percent, desired_speed, | |
| session_id_state # 👈 session_id ကို Function ထဲ ပို့ပေးခြင်း | |
| ], | |
| outputs=[output_video] | |
| ) | |
| return main_app | |
| def verify_member(username, password): | |
| if username in ACCOUNTS and ACCOUNTS[username] == password: | |
| return True | |
| return False | |
| demo_app = create_main_app_interface() | |
| if __name__ == "__main__": | |
| demo_app.launch(auth=verify_member, theme=gr.themes.Default()) |