kritnatee's picture
download
raw
12.8 kB
"""
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.