import os
import uuid
import shutil
import subprocess
from flask import Flask, request, send_file, render_template, jsonify
# Config
UPLOAD_DIR = "uploads"
OUTPUT_DIR = "outputs"
CLIP_DURATION = 5 # seconds per image clip BEFORE transitions
TRANS_DURATION = 1 # seconds of crossfade between clips
RESOLUTION = (1280, 720) # output resolution (width, height)
FPS = 25
os.makedirs(UPLOAD_DIR, exist_ok=True)
os.makedirs(OUTPUT_DIR, exist_ok=True)
app = Flask(__name__)
def run(cmd):
"""Run subprocess and raise if fails."""
print("RUN:", " ".join(cmd))
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
if proc.returncode != 0:
print("ERROR STDOUT:", proc.stdout)
print("ERROR STDERR:", proc.stderr)
raise RuntimeError(f"Command failed: {' '.join(cmd)}\n{proc.stderr}")
return proc.stdout
def create_zoompan_clip(image_path, out_path, duration=CLIP_DURATION, res=RESOLUTION, fps=FPS, zoom_amount=1.15):
"""
Create a video clip from a single image that slowly zooms (Ken Burns).
Produces a file with same codec/params so concatenation later is easier.
"""
w, h = res
# compute zoompan parameters: zoom increases from 1 to zoom_amount across duration*fps frames
frames = duration * fps
# ffmpeg zoompan expression: zoom='1+((zoom_amount-1)/frames)*in', but we will compute relative frame-based expression
zoom_expr = f"zoom='if(eq(in,0),1,zoom+({zoom_amount}-1)/{frames})'"
# simpler approach: use scale and crop with zoom parameter using z=1+0.0... incremental
# We'll use simple zoompan filter template:
zoompan_filter = (
f"zoompan=d={frames}:s={w}x{h}:z='zoom+({zoom_amount}-1)/{frames}':x='iw/2-(iw/{ { 'z' } }/2)':y='ih/2-(ih/{{z}}/2)'"
)
# The above is quite complex to generate safely across shells; instead we'll use a more robust, widely-compatible command:
# Use "-vf scale=...,zoompan=..." pattern (some ffmpeg builds require escaping; keep it simple).
cmd = [
"ffmpeg", "-y", "-loop", "1", "-t", str(duration),
"-i", image_path,
"-vf",
f"scale={w}:{h},zoompan=z='if(lte(zoom,1.0),1,zoom)+({zoom_amount}-1)/{frames}':d={frames}:s={w}x{h}",
"-r", str(fps),
"-c:v", "libx264", "-pix_fmt", "yuv420p",
"-movflags", "+faststart",
out_path
]
# In practice the zoompan expression above can be brittle depending on ffmpeg version/shell escaping.
# We'll use a two-step reliable approach below instead.
# fallback to simpler implementation:
try:
run(cmd)
except Exception:
# fallback: create a still image video with a single slow zoom using scale and crop via 'scale' + 'crop' animated by 'zoompan' simpler formula
# Simpler: use zoompan with 'zoom=1.0+0.01*in' style
cmd2 = [
"ffmpeg", "-y", "-loop", "1", "-t", str(duration), "-i", image_path,
"-vf",
f"scale={w}:{h},zoompan=z='1+({zoom_amount}-1)*in/{frames}':d={frames}:s={w}x{h}",
"-r", str(fps),
"-c:v", "libx264", "-pix_fmt", "yuv420p", "-movflags", "+faststart",
out_path
]
run(cmd2)
def build_xfade_chain(input_clips, output_path, clip_duration=CLIP_DURATION, trans_duration=TRANS_DURATION, fps=FPS):
"""
Use ffmpeg xfade filter to chain multiple clips with crossfades.
Pattern for multiple inputs:
-i clip0 -i clip1 -i clip2 ...
filter_complex:
[0:v]setpts=PTS-STARTPTS[v0];
[1:v]setpts=PTS-STARTPTS[v1];
...
[v0][v1]xfade=transition=fade:duration=TRANS:offset=OFFSET0[v01];
[v01][v2]xfade=transition=fade:duration=TRANS:offset=OFFSET1[v012];
...
final mapping: -map [vFINAL]
"""
if len(input_clips) == 0:
raise ValueError("No clips to join")
if len(input_clips) == 1:
# just move single clip to output
shutil.copyfile(input_clips[0], output_path)
return
cmd = ["ffmpeg", "-y"]
for clip in input_clips:
cmd += ["-i", clip]
# Build filter_complex
filter_lines = []
# setpts lines
for i in range(len(input_clips)):
filter_lines.append(f"[{i}:v]setpts=PTS-STARTPTS[v{i}]")
# chain xfade filters
# offsets accumulate: offset_k = (k+1) * (clip_duration - trans_duration)
prev_label = "v0"
cur_index = 1
for k in range(len(input_clips)-1):
a = prev_label
b = f"v{cur_index}"
offset = (k+1) * (clip_duration - trans_duration)
out_label = f"vx_{k+1}"
# Use a popular xfade transition (fade) — you can change name to e.g. slideleft, luma, etc.
filter_lines.append(f"[{a}][{b}]xfade=transition=fade:duration={trans_duration}:offset={offset}[{out_label}]")
prev_label = out_label
cur_index += 1
filter_complex = ";".join(filter_lines)
cmd += ["-filter_complex", filter_complex]
# map final video stream
cmd += ["-map", f"[{prev_label}]", "-c:v", "libx264", "-r", str(fps), "-pix_fmt", "yuv420p", output_path]
run(cmd)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/generate", methods=["POST"])
def generate():
"""
Expect multipart form data:
- files[]: multiple image files
- music: optional background music file
- title_text[]: optional text captions aligned with each image (same order)
- clip_duration, trans_duration, resolution_w, resolution_h (optional)
"""
# Create working dirs
job_id = str(uuid.uuid4())[:8]
work = os.path.join(UPLOAD_DIR, job_id)
os.makedirs(work, exist_ok=True)
out_job = os.path.join(OUTPUT_DIR, job_id)
os.makedirs(out_job, exist_ok=True)
# Params from form
try:
clip_duration = float(request.form.get("clip_duration", CLIP_DURATION))
trans_duration = float(request.form.get("trans_duration", TRANS_DURATION))
res_w = int(request.form.get("resolution_w", RESOLUTION[0]))
res_h = int(request.form.get("resolution_h", RESOLUTION[1]))
except Exception:
return jsonify({"error": "invalid numeric params"}), 400
files = request.files.getlist("files[]")
if not files:
return jsonify({"error": "no files uploaded"}), 400
# Save images
image_paths = []
for i, f in enumerate(files):
fname = f"{i:03d}_{secure_filename := (f.filename or f'image_{i}.jpg').replace(' ', '_')}"
path = os.path.join(work, fname)
f.save(path)
image_paths.append(path)
# Optional music
music_file = request.files.get("music")
music_path = None
if music_file and music_file.filename:
music_path = os.path.join(work, f"music_{secure_filename}")
music_file.save(music_path)
# Create per-image zoompan clips
clip_paths = []
for idx, img in enumerate(image_paths):
clip_out = os.path.join(work, f"clip_{idx:03d}.mp4")
# call ffmpeg to produce zoom-animated clip
try:
create_zoompan_clip(img, clip_out, duration=int(clip_duration), res=(res_w, res_h))
except Exception as e:
# If zoompan generation fails, fallback to simple still-image to video using loop
print(f"zoompan failed for {img}: {e}, using fallback.")
cmd = [
"ffmpeg", "-y", "-loop", "1", "-t", str(int(clip_duration)), "-i", img,
"-vf", f"scale={res_w}:{res_h}",
"-c:v", "libx264", "-pix_fmt", "yuv420p", clip_out
]
run(cmd)
clip_paths.append(clip_out)
# Build final video with xfade chain
final_out = os.path.join(out_job, f"{job_id}_final.mp4")
try:
build_xfade_chain(clip_paths, final_out, clip_duration=int(clip_duration), trans_duration=float(trans_duration))
except Exception as e:
return jsonify({"error": f"Failed to build final video: {e}"}), 500
# If music provided, merge with audio (loop/resample) and set shortest or fade
if music_path:
merged = os.path.join(out_job, f"{job_id}_with_music.mp4")
# Add music, keep video, use -shortest so output ends when shorter stream ends (video)
cmd = [
"ffmpeg", "-y", "-i", final_out, "-stream_loop", "-1", "-i", music_path,
"-c:v", "copy", "-c:a", "aac", "-b:a", "192k", "-shortest", merged
]
try:
run(cmd)
final_out = merged
except Exception as e:
print("Failed to add music:", e)
# Return file
return send_file(final_out, as_attachment=True, download_name=f"photo_video_{job_id}.mp4")
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=7860)
අම්මගෙ වැල් වීඩියෝ එක
අම්මගෙ වැල් විඩියෝ එක මේ කතාව වෙනකොට මගෙ වයස 18 යි.අපේ අම්මගෙ නම ශානිකා.එයාට යාලුවෝ කතා කලේ ශානි කියල.අම්මගෙ වයස අවුරුදු 40 යි..එයාගෙ ඇග මාර ලස්සනයි.හැඩයි.තන් දෙක 37 සයිස් වගේ.පස්ස නම් ඉතින් ආයෙ කියල වැඩක් නෑ..අම්ම සුදුයි.උසයි.කලව දෙක පිරිල ලස්සනට ..එයා අදින්නේ ගොඩක් කොටට.තාත්තා ඉන්නෙ රට.ඉතින් කොහොමහරි දවසක් මම ක්ලාස් එකක ඉන්නකොට මගෙ යාලුවො තුන් හතර දෙනෙක් වීඩියෝ එකක් බල බල මොනවද කියවනව දැක්කා.මම ලගට යනකොට උන් මට කතා කරලා “මේ ජනියා,මෙහෙ වරෙන් මරු ඇන්ටි බඩුවක් ඉන්නවා.මරු සැපක් දෙනවා කොල්ලො දෙන්නෙක්ට එකපාර..මාත් ඉක්මනින් බැලුවා ..බලනකොට සුදු ගෑනු කෙනෙකුට කලුම කලු කොල්ලො දෙන්නෙක් හුකන සීන් එකක් තියෙන්නේ ඇදක දාගෙන මූනවල් පේන්නෙ නෑ .එකෙක් ඩොගි ස්ටයිල් එකෙන් හුකන ගමන් අනිකා පොල්ල කටේ ඔබන සීන් එකක්.ටිකක් බලනකොට මට තේරුනා මේ අපේ ගෙදර අම්මගෙ කාමරේ නේද කියල?මගෙ ඇගට හීන් දාඩියක් දැම්මා... see more
Comments
Post a Comment