Buckets:
| """ | |
| scene-planner.py — Agent: วิเคราะห์สคริปต์และวางแผนการเปลี่ยนฉาก | |
| Version 2.0 — Gemini API + Thai mood detection + Rule-based fallback | |
| Usage: | |
| python scene-planner.py script.txt --images-dir images/ --output scenes.json | |
| python scene-planner.py script.txt --gemini-key YOUR_KEY --output scenes.json | |
| """ | |
| import argparse, json, os, sys, re, glob | |
| # --- Transition definitions --- | |
| TRANSITIONS = { | |
| "fade": {"desc": "ค่อยๆจาง", "style": "soft"}, | |
| "crossfade": {"desc": "ภาพซ้อนกัน", "style": "dreamy"}, | |
| "slide_left": {"desc": "เลื่อนซ้าย", "style": "energetic"}, | |
| "slide_right": {"desc": "เลื่อนขวา", "style": "return"}, | |
| "slide_up": {"desc": "เลื่อนขึ้น", "style": "climax"}, | |
| "slide_down": {"desc": "เลื่อนลง", "style": "descent"}, | |
| "dissolve": {"desc": "ละลายช้าๆ", "style": "memory"}, | |
| "fadeblack": {"desc": "จางเป็นดำ", "style": "dramatic"}, | |
| "fadewhite": {"desc": "จางเป็นขาว", "style": "ethereal"}, | |
| "pixelize": {"desc": "像素化", "style": "digital"}, | |
| "circlecrop": {"desc": "วงกลมตัด", "style": "focus"}, | |
| } | |
| # --- Thai mood keywords --- | |
| THAI_MOOD = { | |
| "happy": ["สุข", "ดีใจ", "รัก", "สนุก", "ยิ้ม", "หัวเราะ", "สดใส", "รื่นเริง", "เฉลิมฉลอง", "ปาร์ตี้", "อร่อย", "delicious", "happy", "joy", "love", "fun", "smile", "laugh", "beautiful", "wonderful", "great"], | |
| "sad": ["เศร้า", "เหงา", "เสียใจ", "ร้องไห้", "โดดเดี่ยว", "สูญเสีย", "อาลัย", "เศร้าโศก", "ปวดร้าว", "sad", "lonely", "cry", "loss", "dark", "grief", "mourn", "tears"], | |
| "calm": ["เงียบ", "สงบ", "เยือกเย็น", "นุ่มนวล", "ผ่อนคลาย", "พักผ่อน", "ธรรมชาติ", "ป่า", "ทะเล", "ภูเขา", "quiet", "peaceful", "gentle", "soft", "slow", "nature", "calm", "relax", "serene"], | |
| "action": ["วิ่ง", "เร็ว", "ทันที", "พุ่ง", "ตื่นเต้น", "แข่ง", "สู้", "ต่อสู้", "ลุย", "action", "run", "fast", "rush", "fight", "quick", "sudden", "speed", "energy"], | |
| "dramatic": ["ทันใด", "ปรากฏ", "เหลือเชื่อ", "น่าทึ่ง", "ยิ่งใหญ่", "อลังการ", "พลิกผัน", "dramatic", "suddenly", "reveal", "amazing", "incredible", "epic", "powerful"], | |
| "reflective": ["คิด", "นึก", "เคย", "อดีต", "ความทรงจำ", "หวน", "ทบทวน", "นิ่ง思考", "reflect", "remember", "think", "wonder", "past", "memory", "once", "contemplate"], | |
| "mysterious": ["ลึกลับ", "ปริศนา", "เงื่อนงำ", "ซ่อน", "อำพราง", "mysterious", "mystery", "secret", "hidden", "enigma", "puzzle"], | |
| "romantic": ["รัก", "โรแมนติก", "กุหลาบ", "จูบ", "ริมฝีปาก", "หัวใจ", "love", "romance", "rose", "kiss", "heart", "romantic", "passion"], | |
| } | |
| # --- Mood to transition mapping --- | |
| MOOD_TRANSITIONS = { | |
| "happy": ["slide_left", "slide_up", "crossfade", "zoom_in"], | |
| "sad": ["fade", "dissolve", "fadeblack"], | |
| "calm": ["fade", "crossfade", "dissolve", "fadewhite"], | |
| "action": ["slide_left", "slide_right", "slide_up", "pixelize"], | |
| "dramatic": ["zoom_in", "slide_up", "fadeblack", "circlecrop"], | |
| "reflective": ["dissolve", "fade", "crossfade", "fadewhite"], | |
| "mysterious": ["fadeblack", "pixelize", "circlecrop", "dissolve"], | |
| "romantic": ["crossfade", "fadewhite", "dissolve", "fade"], | |
| } | |
| # --- Gemini API --- | |
| def gemini_analyze(script_text, api_key): | |
| """Use Gemini 2.0 Flash to analyze script and plan scenes.""" | |
| try: | |
| import google.generativeai as genai | |
| genai.configure(api_key=api_key) | |
| model = genai.GenerativeModel('gemini-2.0-flash') | |
| prompt = f"""You are a professional video director. Analyze this Thai script and create a scene-by-scene production plan. | |
| SCRIPT: | |
| {script_text} | |
| For each scene, determine: | |
| 1. The mood/emotion (one of: happy, sad, calm, action, dramatic, reflective, mysterious, romantic) | |
| 2. The best transition IN (from: fade, crossfade, slide_left, slide_right, slide_up, slide_down, dissolve, fadeblack, fadewhite, pixelize, circlecrop) | |
| 3. The best transition OUT | |
| 4. A short English prompt for AI image generation (describing the scene visually) | |
| 5. Suggested duration (2-6 seconds based on content complexity) | |
| IMPORTANT RULES: | |
| - Transitions should VARY - never use the same transition for 3+ consecutive scenes | |
| - Match transitions to mood: action scenes → fast transitions (slide), calm scenes → soft transitions (fade/dissolve) | |
| - The image prompt should be in English, detailed, cinematic style | |
| - Return ONLY valid JSON array, no other text | |
| Output format: | |
| [ | |
| {{ | |
| "scene": 1, | |
| "narrative_th": "สรุปเนื้อเรื่องฉากนี้ (ภาษาไทย)", | |
| "image_prompt": "Detailed English prompt for AI image generation", | |
| "mood": "calm", | |
| "transition_in": "fade", | |
| "transition_out": "crossfade", | |
| "duration": 3.0 | |
| }} | |
| ]""" | |
| response = model.generate_content(prompt) | |
| text = response.text.strip() | |
| # Extract JSON from response | |
| if "```json" in text: | |
| text = text.split("```json")[1].split("```")[0].strip() | |
| elif "```" in text: | |
| text = text.split("```")[1].split("```")[0].strip() | |
| # Find JSON array | |
| start = text.find("[") | |
| end = text.rfind("]") + 1 | |
| if start >= 0 and end > start: | |
| text = text[start:end] | |
| return json.loads(text) | |
| except Exception as e: | |
| print(f" [!] Gemini API error: {e}") | |
| return None | |
| # --- Rule-based planner --- | |
| def detect_mood(text): | |
| """Detect mood from text using keyword matching.""" | |
| scores = {k: 0 for k in THAI_MOOD} | |
| words = text.lower().split() | |
| for mood, keywords in THAI_MOOD.items(): | |
| for kw in keywords: | |
| if kw in text.lower(): | |
| scores[mood] += 1 | |
| if max(scores.values()) == 0: | |
| return "calm" | |
| return max(scores, key=scores.get) | |
| def rule_based_plan(scenes): | |
| """Generate scene plan using rules.""" | |
| plan = [] | |
| used_transitions = [] | |
| for i, scene in enumerate(scenes): | |
| mood = detect_mood(scene["text"]) | |
| # Pick transition not used in last 2 scenes | |
| available = MOOD_TRANSITIONS.get(mood, ["fade"]) | |
| for t in available: | |
| if t not in used_transitions[-2:]: | |
| break | |
| else: | |
| t = available[0] | |
| used_transitions.append(t) | |
| # Pick different transition for out | |
| out_available = [x for x in MOOD_TRANSITIONS.get(mood, ["fade"]) if x != t] | |
| t_out = out_available[0] if out_available else "fade" | |
| # Generate English prompt from Thai text | |
| image_prompt = generate_image_prompt(scene["text"]) | |
| plan.append({ | |
| "scene": i + 1, | |
| "narrative_th": scene["text"].strip()[:200], | |
| "image_prompt": image_prompt, | |
| "mood": mood, | |
| "transition_in": t, | |
| "transition_out": t_out, | |
| "duration": max(2.5, min(5.0, len(scene["text"].split()) * 0.25)) | |
| }) | |
| return plan | |
| def generate_image_prompt(thai_text): | |
| """Generate a basic English image prompt from Thai text keywords.""" | |
| # Simple keyword-based prompt generation | |
| prompts = { | |
| "พระอาทิตย์": "sunset over horizon, golden light, cinematic sky", | |
| "ชายหาด": "tropical beach, waves, sand, palm trees, golden hour", | |
| "ตลาด": "bustling market, colorful stalls, people shopping, vibrant atmosphere", | |
| "ภูเขา": "mountain landscape, misty peaks, green valleys, dramatic sky", | |
| "กาแฟ": "cozy coffee shop, warm lighting, steaming cup, wooden interior", | |
| "วัด": "ancient temple, golden spires, serene atmosphere, sunset light", | |
| "ฝน": "rainy window, water droplets, moody atmosphere, soft light", | |
| "สวน": "beautiful garden, flowers, green plants, sunlight", | |
| "ดาว": "starry night sky, milky way, dark blue, celestial", | |
| "เมือง": "cityscape, modern buildings, lights, urban atmosphere", | |
| "ทะเล": "ocean view, blue water, waves, horizon, peaceful", | |
| "ป่า": "forest path, green canopy, sunlight filtering through trees", | |
| "รถ": "vehicle on road, motion, urban scene", | |
| "เรือ": "boat on water, harbor, fishing vessels", | |
| "แผนที่": "map, geographical display, strategic planning", | |
| "เอกสาร": "vintage documents, historical papers, aged texture", | |
| } | |
| for keyword, prompt in prompts.items(): | |
| if keyword in thai_text: | |
| return f"cinematic photo, {prompt}, high quality, detailed, 4k" | |
| return f"cinematic scene, Thai atmosphere, beautiful composition, high quality, detailed, 4k" | |
| # --- Parse script --- | |
| def parse_script(filepath): | |
| """Parse script file into scenes.""" | |
| with open(filepath, 'r', encoding='utf-8') as f: | |
| content = f.read() | |
| # Split by double newline | |
| blocks = re.split(r'\n\s*\n', content) | |
| scenes = [] | |
| img_pattern = re.compile(r'^(?:IMG|Image|image)\s*:\s*(.+)', re.IGNORECASE) | |
| for block in blocks: | |
| lines = block.strip().split('\n') | |
| text_lines = [] | |
| image = "" | |
| for line in lines: | |
| m = img_pattern.match(line.strip()) | |
| if m: | |
| image = m.group(1).strip() | |
| elif line.strip(): | |
| text_lines.append(line.strip()) | |
| if text_lines: | |
| scenes.append({"text": " ".join(text_lines), "image": image}) | |
| return scenes | |
| def match_images(scenes, images_dir): | |
| """Match scenes to available images.""" | |
| available = [] | |
| for ext in ("*.png", "*.jpg", "*.jpeg", "*.webp"): | |
| available.extend(sorted(glob.glob(os.path.join(images_dir, "**", ext), recursive=True))) | |
| for i, scene in enumerate(scenes): | |
| if scene.get("image"): | |
| matches = [f for f in available if scene["image"] in f] | |
| if matches: | |
| scene["image"] = matches[0] | |
| continue | |
| if available: | |
| scene["image"] = available[i % len(available)] | |
| return scenes | |
| # --- Main --- | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="Scene Planning Agent v2.0") | |
| parser.add_argument("script", help="Script file (.txt)") | |
| parser.add_argument("--images-dir", default="images", help="Images directory") | |
| parser.add_argument("--output", default="scenes.json", help="Output file") | |
| parser.add_argument("--gemini-key", help="Gemini API key (optional)") | |
| args = parser.parse_args() | |
| print("[1/3] Parsing script...") | |
| scenes = parse_script(args.script) | |
| if not scenes: | |
| print("ERROR: No scenes found"); sys.exit(1) | |
| print(f" Found {len(scenes)} scenes") | |
| print("[2/3] Planning transitions...") | |
| plan = None | |
| if args.gemini_key: | |
| print(" Using Gemini 2.0 Flash (free tier)...") | |
| script_text = "\n\n".join([f"Scene {i+1}: {s['text']}" for i, s in enumerate(scenes)]) | |
| plan = gemini_analyze(script_text, args.gemini_key) | |
| if not plan: | |
| if args.gemini_key: | |
| print(" Gemini failed, falling back to rules...") | |
| else: | |
| print(" Using rule-based planning (no Gemini key)") | |
| plan = rule_based_plan(scenes) | |
| # Match images | |
| if os.path.isdir(args.images_dir): | |
| scenes_with_images = match_images([{"text": p["narrative_th"], "image": ""} for p in plan], args.images_dir) | |
| for i, p in enumerate(plan): | |
| if i < len(scenes_with_images): | |
| p["start_image"] = scenes_with_images[i].get("image", "") | |
| print("[3/3] Saving plan...") | |
| output = {"scenes": plan, "total_scenes": len(plan)} | |
| with open(args.output, 'w', encoding='utf-8') as f: | |
| json.dump(output, f, ensure_ascii=False, indent=2) | |
| print(f"\n -> {args.output}") | |
| print(f"\n Scene Plan:") | |
| for s in plan: | |
| print(f" S{s['scene']}: [{s['mood']}] {s['transition_in']}→{s['transition_out']} ({s['duration']}s)") | |
| print(f" Prompt: {s['image_prompt'][:80]}...") | |
Xet Storage Details
- Size:
- 12.8 kB
- Xet hash:
- d48c450db3f05303afed00f9c6ff9b5397dc616f8c9d15e5d0de6990a1e3f05f
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.