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)
TikTok Downloader — Simple Blogger Page TikTok Downloader Paste a TikTok video URL and get a downloadable MP4 link. This page is a frontend for a simple backend — see the included server example in the README below. TikTok video URL Get Download Link How it works: the frontend sends the TikTok URL to your server (/api/fetch). The server fetches the video (using a downloader like yt-dlp or a scraping method), returns a direct video URL or streams the file back. Host the backend yourself. Important: only download content you have rights to or permission to download. Respect TikTok's Terms of Service and copyright law. Developer README & server example README - Quickstart FRONTEND: This page (single HTML). To use on Blogger you can paste this into a "HTML/JavaScript" gadget or host as a static page and iframe it into Blogger. BACKEND (...
Comments
Post a Comment