Update app.py
Browse files
app.py
CHANGED
|
@@ -11,13 +11,12 @@
|
|
| 11 |
|
| 12 |
|
| 13 |
import sys, types
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
sys.modules["torchvision.transforms.functional_tensor"] = _mod
|
| 21 |
|
| 22 |
# ────────────────────────────────────────────────────────
|
| 23 |
# Standard imports
|
|
@@ -73,7 +72,28 @@ def sample_paths(paths: List[Path] | List[str], n: int = 30) -> List[str]:
|
|
| 73 |
out.append(str(paths[i]))
|
| 74 |
seen.add(i)
|
| 75 |
return out
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
# Flag so UI can know if realesrgan is importable
|
| 78 |
HAVE_REALESRGAN = True
|
| 79 |
|
|
@@ -856,94 +876,94 @@ def step3_encode(frames_dir_state: str | None, prefix_state: str | None, orig_vi
|
|
| 856 |
|
| 857 |
# ───────────────── Quick Mode — one click: All frames → Upscale ×4 → MP4 (audio)
|
| 858 |
|
| 859 |
-
def quick_mode(video: gr.File | None, start_time: str, end_time: str, resize_long: int, prefix_in: str, prog_html: str):
|
| 860 |
-
if not video or not video.name:
|
| 861 |
-
return None, None, None, "Upload a video.", prog_html
|
| 862 |
-
if not (FFMPEG and FFPROBE and HAVE_REALESRGAN):
|
| 863 |
-
return None, None, None, "Missing deps (ffmpeg/ffprobe/realesrgan). See requirements.txt.", prog_html
|
| 864 |
|
| 865 |
-
info = parse_video_info(ffprobe_json(video.name))
|
| 866 |
-
in_fps = info.get("fps") or 30.0
|
| 867 |
-
prefix = sanitize_prefix(prefix_in) or Path(video.name).stem
|
| 868 |
|
| 869 |
-
work = Path(tempfile.mkdtemp(prefix="quick_"))
|
| 870 |
-
raw_dir = work / "frames_raw"; raw_dir.mkdir(parents=True, exist_ok=True)
|
| 871 |
-
up_dir = work / "upscaled"; up_dir.mkdir(parents=True, exist_ok=True)
|
| 872 |
|
| 873 |
# Extract all frames
|
| 874 |
-
extract_cmd = build_ffmpeg_extract(
|
| 875 |
-
input_path=video.name,
|
| 876 |
-
mode="All frames",
|
| 877 |
-
every_seconds=1.0,
|
| 878 |
-
nth_frame=1,
|
| 879 |
-
exact_fps=in_fps,
|
| 880 |
-
start_time=(start_time or "").strip(),
|
| 881 |
-
end_time=(end_time or "").strip(),
|
| 882 |
-
long_side=resize_long,
|
| 883 |
-
out_format="jpg",
|
| 884 |
-
jpg_quality=3,
|
| 885 |
-
png_level=2,
|
| 886 |
-
scene_detect=False,
|
| 887 |
-
scene_thresh=0.3,
|
| 888 |
-
out_pattern=str(raw_dir / f"{prefix}_%05d.jpg"),
|
| 889 |
-
)
|
| 890 |
-
proc = subprocess.Popen(extract_cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True, bufsize=1)
|
| 891 |
-
est = estimate_output_count("All frames", info.get("duration"), in_fps, 1.0, 1, in_fps)
|
| 892 |
-
created = 0
|
| 893 |
-
while True:
|
| 894 |
-
line = proc.stderr.readline()
|
| 895 |
-
if not line and proc.poll() is not None:
|
| 896 |
-
break
|
| 897 |
-
if int(time.time()*10) % 3 == 0:
|
| 898 |
-
created = len(list(raw_dir.glob(f"{prefix}_*.jpg")))
|
| 899 |
-
pct = min(100.0, (created / est) * 100.0) if est else 0
|
| 900 |
-
prog_html = render_progress(pct, f"Phase 1/3: Extracting {created}/{est or '?'}")
|
| 901 |
-
proc.wait()
|
| 902 |
-
|
| 903 |
-
frames = sorted(raw_dir.glob(f"{prefix}_*.jpg"))
|
| 904 |
-
if not frames:
|
| 905 |
-
return None, None, None, "No frames extracted in Quick Mode.", prog_html
|
| 906 |
|
| 907 |
# Upscale x4
|
| 908 |
-
device = "cuda" if os.environ.get("CUDA_VISIBLE_DEVICES") else "cpu"
|
| 909 |
-
upsampler = get_realesrganer("x4plus", 4, 0, (device=="cuda"), device=device)
|
| 910 |
-
|
| 911 |
-
total = len(frames)
|
| 912 |
-
done = 0
|
| 913 |
-
for fp in frames:
|
| 914 |
-
img = Image.open(fp).convert("RGB")
|
| 915 |
-
output, _ = upsampler.enhance(np.array(img), outscale=4)
|
| 916 |
-
Image.fromarray(output).save(up_dir / (Path(fp).stem + ".jpg"), quality=95)
|
| 917 |
-
done += 1
|
| 918 |
-
pct = (done/total)*100 if total else 0
|
| 919 |
-
prog_html = render_progress(pct, f"Phase 2/3: Upscaling {done}/{total}")
|
| 920 |
|
| 921 |
# Encode MP4 with audio
|
| 922 |
-
encode_cmd = build_ffmpeg_encode(str(up_dir), prefix, in_fps, "h264", True, video.name)
|
| 923 |
-
proc2 = subprocess.Popen(encode_cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True, bufsize=1, cwd=str(up_dir))
|
| 924 |
-
while True:
|
| 925 |
-
line = proc2.stderr.readline()
|
| 926 |
-
if not line and proc2.poll() is not None:
|
| 927 |
-
break
|
| 928 |
-
if int(time.time()*10) % 5 == 0:
|
| 929 |
-
prog_html = render_progress(50.0, "Phase 3/3: Encoding…")
|
| 930 |
-
proc2.wait()
|
| 931 |
-
|
| 932 |
-
out_file = Path(up_dir) / "output.mp4"
|
| 933 |
-
if not out_file.exists():
|
| 934 |
-
return None, None, None, "Encoding failed in Quick Mode.", prog_html
|
| 935 |
|
| 936 |
# Intermediates
|
| 937 |
-
zip_frames = work / "frames.zip"
|
| 938 |
-
with zipfile.ZipFile(zip_frames, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 939 |
-
for p in frames:
|
| 940 |
-
zf.write(p, p.name)
|
| 941 |
-
zip_up = work / "upscaled.zip"
|
| 942 |
-
with zipfile.ZipFile(zip_up, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 943 |
-
for p in sorted(up_dir.glob("*.jpg"), key=_natural_key):
|
| 944 |
-
zf.write(p, p.name)
|
| 945 |
|
| 946 |
-
return str(out_file), str(zip_frames), str(zip_up), "Quick Mode complete.", render_progress(100.0, "All done")
|
| 947 |
|
| 948 |
# ───────────────── UI
|
| 949 |
|
|
@@ -952,10 +972,9 @@ def build_ui():
|
|
| 952 |
.cf-title { font-size: 1.6rem; font-weight: 800; }
|
| 953 |
.cmdbox textarea { font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
|
| 954 |
""") as demo:
|
| 955 |
-
gr.
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
""")
|
| 959 |
|
| 960 |
# Shared states (from Step 1)
|
| 961 |
frames_state = gr.State([]) # list[str]
|
|
|
|
| 11 |
|
| 12 |
|
| 13 |
import sys, types
|
| 14 |
+
def _rgb_to_grayscale_np(arr: np.ndarray) -> np.ndarray:
|
| 15 |
+
# arr: HxWx3 uint8
|
| 16 |
+
r, g, b = arr[...,0], arr[...,1], arr[...,2]
|
| 17 |
+
gray = (0.2989*r + 0.5870*g + 0.1140*b).astype(arr.dtype)
|
| 18 |
+
return np.stack([gray, gray, gray], axis=-1)
|
| 19 |
+
|
|
|
|
| 20 |
|
| 21 |
# ────────────────────────────────────────────────────────
|
| 22 |
# Standard imports
|
|
|
|
| 72 |
out.append(str(paths[i]))
|
| 73 |
seen.add(i)
|
| 74 |
return out
|
| 75 |
+
|
| 76 |
+
import base64
|
| 77 |
+
|
| 78 |
+
def load_logo_base64(path: str) -> str:
|
| 79 |
+
with open(path, "rb") as f:
|
| 80 |
+
return base64.b64encode(f.read()).decode("utf-8")
|
| 81 |
+
|
| 82 |
+
# Preload your Bifröst logo
|
| 83 |
+
LOGO_B64 = load_logo_base64(os.path.join("assets", "bifrost_logo.png"))
|
| 84 |
+
|
| 85 |
+
def render_logo_html(px: int = 96) -> str:
|
| 86 |
+
return f"""
|
| 87 |
+
<div style="display:flex;align-items:center;gap:16px;">
|
| 88 |
+
<img src="data:image/png;base64,{LOGO_B64}" style="height:{px}px;width:auto;" />
|
| 89 |
+
<div>
|
| 90 |
+
<div style="font-size:1.6rem;font-weight:800;">Bifröst Beam</div>
|
| 91 |
+
<div style="opacity:0.8;">Video → Frames → Upscale (Nordic Inspired)</div>
|
| 92 |
+
</div>
|
| 93 |
+
</div>
|
| 94 |
+
<hr>
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
# Flag so UI can know if realesrgan is importable
|
| 98 |
HAVE_REALESRGAN = True
|
| 99 |
|
|
|
|
| 876 |
|
| 877 |
# ───────────────── Quick Mode — one click: All frames → Upscale ×4 → MP4 (audio)
|
| 878 |
|
| 879 |
+
#def quick_mode(video: gr.File | None, start_time: str, end_time: str, resize_long: int, prefix_in: str, prog_html: str):
|
| 880 |
+
# if not video or not video.name:
|
| 881 |
+
# return None, None, None, "Upload a video.", prog_html
|
| 882 |
+
# if not (FFMPEG and FFPROBE and HAVE_REALESRGAN):
|
| 883 |
+
# return None, None, None, "Missing deps (ffmpeg/ffprobe/realesrgan). See requirements.txt.", prog_html
|
| 884 |
|
| 885 |
+
# info = parse_video_info(ffprobe_json(video.name))
|
| 886 |
+
# in_fps = info.get("fps") or 30.0
|
| 887 |
+
# prefix = sanitize_prefix(prefix_in) or Path(video.name).stem
|
| 888 |
|
| 889 |
+
# work = Path(tempfile.mkdtemp(prefix="quick_"))
|
| 890 |
+
# raw_dir = work / "frames_raw"; raw_dir.mkdir(parents=True, exist_ok=True)
|
| 891 |
+
# up_dir = work / "upscaled"; up_dir.mkdir(parents=True, exist_ok=True)
|
| 892 |
|
| 893 |
# Extract all frames
|
| 894 |
+
# extract_cmd = build_ffmpeg_extract(
|
| 895 |
+
# input_path=video.name,
|
| 896 |
+
# mode="All frames",
|
| 897 |
+
# every_seconds=1.0,
|
| 898 |
+
# nth_frame=1,
|
| 899 |
+
# exact_fps=in_fps,
|
| 900 |
+
# start_time=(start_time or "").strip(),
|
| 901 |
+
# end_time=(end_time or "").strip(),
|
| 902 |
+
# long_side=resize_long,
|
| 903 |
+
# out_format="jpg",
|
| 904 |
+
# jpg_quality=3,
|
| 905 |
+
# png_level=2,
|
| 906 |
+
# scene_detect=False,
|
| 907 |
+
# scene_thresh=0.3,
|
| 908 |
+
# out_pattern=str(raw_dir / f"{prefix}_%05d.jpg"),
|
| 909 |
+
# )
|
| 910 |
+
# proc = subprocess.Popen(extract_cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True, bufsize=1)
|
| 911 |
+
# est = estimate_output_count("All frames", info.get("duration"), in_fps, 1.0, 1, in_fps)
|
| 912 |
+
# created = 0
|
| 913 |
+
# while True:
|
| 914 |
+
# line = proc.stderr.readline()
|
| 915 |
+
# if not line and proc.poll() is not None:
|
| 916 |
+
# break
|
| 917 |
+
# if int(time.time()*10) % 3 == 0:
|
| 918 |
+
# created = len(list(raw_dir.glob(f"{prefix}_*.jpg")))
|
| 919 |
+
# pct = min(100.0, (created / est) * 100.0) if est else 0
|
| 920 |
+
# prog_html = render_progress(pct, f"Phase 1/3: Extracting {created}/{est or '?'}")
|
| 921 |
+
# proc.wait()
|
| 922 |
+
|
| 923 |
+
# frames = sorted(raw_dir.glob(f"{prefix}_*.jpg"))
|
| 924 |
+
# if not frames:
|
| 925 |
+
# return None, None, None, "No frames extracted in Quick Mode.", prog_html
|
| 926 |
|
| 927 |
# Upscale x4
|
| 928 |
+
# device = "cuda" if os.environ.get("CUDA_VISIBLE_DEVICES") else "cpu"
|
| 929 |
+
# upsampler = get_realesrganer("x4plus", 4, 0, (device=="cuda"), device=device)
|
| 930 |
+
|
| 931 |
+
# total = len(frames)
|
| 932 |
+
# done = 0
|
| 933 |
+
# for fp in frames:
|
| 934 |
+
# img = Image.open(fp).convert("RGB")
|
| 935 |
+
# output, _ = upsampler.enhance(np.array(img), outscale=4)
|
| 936 |
+
# Image.fromarray(output).save(up_dir / (Path(fp).stem + ".jpg"), quality=95)
|
| 937 |
+
# done += 1
|
| 938 |
+
# pct = (done/total)*100 if total else 0
|
| 939 |
+
# prog_html = render_progress(pct, f"Phase 2/3: Upscaling {done}/{total}")
|
| 940 |
|
| 941 |
# Encode MP4 with audio
|
| 942 |
+
# encode_cmd = build_ffmpeg_encode(str(up_dir), prefix, in_fps, "h264", True, video.name)
|
| 943 |
+
# proc2 = subprocess.Popen(encode_cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True, bufsize=1, cwd=str(up_dir))
|
| 944 |
+
# while True:
|
| 945 |
+
# line = proc2.stderr.readline()
|
| 946 |
+
# if not line and proc2.poll() is not None:
|
| 947 |
+
# break
|
| 948 |
+
# if int(time.time()*10) % 5 == 0:
|
| 949 |
+
# prog_html = render_progress(50.0, "Phase 3/3: Encoding…")
|
| 950 |
+
# proc2.wait()
|
| 951 |
+
|
| 952 |
+
# out_file = Path(up_dir) / "output.mp4"
|
| 953 |
+
# if not out_file.exists():
|
| 954 |
+
# return None, None, None, "Encoding failed in Quick Mode.", prog_html
|
| 955 |
|
| 956 |
# Intermediates
|
| 957 |
+
# zip_frames = work / "frames.zip"
|
| 958 |
+
# with zipfile.ZipFile(zip_frames, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 959 |
+
# for p in frames:
|
| 960 |
+
# zf.write(p, p.name)
|
| 961 |
+
# zip_up = work / "upscaled.zip"
|
| 962 |
+
# with zipfile.ZipFile(zip_up, "w", zipfile.ZIP_DEFLATED) as zf:
|
| 963 |
+
# for p in sorted(up_dir.glob("*.jpg"), key=_natural_key):
|
| 964 |
+
# zf.write(p, p.name)
|
| 965 |
|
| 966 |
+
# return str(out_file), str(zip_frames), str(zip_up), "Quick Mode complete.", render_progress(100.0, "All done")
|
| 967 |
|
| 968 |
# ───────────────── UI
|
| 969 |
|
|
|
|
| 972 |
.cf-title { font-size: 1.6rem; font-weight: 800; }
|
| 973 |
.cmdbox textarea { font-family: ui-monospace, Menlo, monospace; font-size: 12px; }
|
| 974 |
""") as demo:
|
| 975 |
+
gr.HTML(render_logo_html(96))
|
| 976 |
+
gr.Markdown("Three-step workflow. Video → Frames → Upscale → Re-encode")
|
| 977 |
+
|
|
|
|
| 978 |
|
| 979 |
# Shared states (from Step 1)
|
| 980 |
frames_state = gr.State([]) # list[str]
|