Spaces:
Running
Running
File size: 7,212 Bytes
638b572 dd80798 cdb0a9f dd80798 638b572 3ce0520 cdb0a9f 638b572 cdb0a9f 638b572 3ce0520 638b572 ef735cd 638b572 dd80798 638b572 3ce0520 cdb0a9f 3ce0520 d3d7fb6 638b572 cdb0a9f 3ce0520 cdb0a9f 3ce0520 638b572 d3d7fb6 638b572 d3d7fb6 638b572 d3d7fb6 638b572 d3d7fb6 638b572 cdb0a9f 638b572 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | from pathlib import Path
from typing import Optional
import hashlib, base64, binascii, math, subprocess, shutil, os, logging, uuid, tempfile
from dotenv import load_dotenv
from imageio_ffmpeg import get_ffmpeg_exe
import boto3
load_dotenv()
logger = logging.getLogger(__name__)
CANONICAL_STATIC_BASE = "https://adgen.static.lookfinity.com"
# ---------- COLOR FILTERS ----------
SAFE_EFFECTS = {
"warm": "eq=brightness=0.01:contrast=1.02:saturation=1.08,colorbalance=rs=0.04:bs=-0.03",
"fresh": "eq=brightness=0.015:contrast=1.03:saturation=1.12,colorbalance=gs=0.03:bs=0.02",
"bright_clean": "eq=brightness=0.02:contrast=1.03:saturation=1.04",
"natural": "eq=brightness=0.006:contrast=1.015:saturation=1.025",
"product_clean": "eq=brightness=0.018:contrast=1.04:saturation=1.07,unsharp=3:3:0.2",
"light_sharpen": "eq=brightness=0.005:contrast=1.02:saturation=1.04,unsharp=3:3:0.18",
"soft": "eq=brightness=0.01:contrast=0.98:saturation=1.04",
"muted": "eq=brightness=0.005:contrast=0.97:saturation=0.92",
"cinematic": "eq=brightness=-0.005:contrast=1.06:saturation=0.98,colorbalance=rs=0.015:bs=0.025",
"subtle": "eq=brightness=0.004:contrast=1.01:saturation=1.015",
}
def _public_base_url() -> str:
base = (os.getenv("NEW_BASE") or "").strip().rstrip("/")
return base or CANONICAL_STATIC_BASE
# ---------- HASH UTILS ----------
def _sha256(path: Path, chunk: int = 1 << 20) -> str:
h = hashlib.sha256()
with path.open("rb") as f:
for b in iter(lambda: f.read(chunk), b""):
h.update(b)
return h.hexdigest()
def _hashid_short(sha256_hex: str, length: int = 12) -> str:
b = binascii.unhexlify(sha256_hex)
return base64.urlsafe_b64encode(b).decode().rstrip("=")[:length]
# ---------- FFMPEG ----------
def _ffmpeg_bin() -> str:
try:
return get_ffmpeg_exe()
except Exception:
p = os.environ.get("FFMPEG_BIN") or shutil.which("ffmpeg")
if not p:
raise FileNotFoundError("ffmpeg not found. Install it or set FFMPEG_BIN")
return p
FFMPEG = _ffmpeg_bin()
def _run_ffmpeg(cmd: list[str]) -> None:
cmd = cmd[:]
cmd[0] = FFMPEG
proc = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if proc.returncode != 0:
raise RuntimeError(proc.stderr.decode(errors="ignore") or "ffmpeg failed")
# ---------- THUMBNAIL ----------
def extract_thumbnail(video_path: str, time_position: str = "00:00:01") -> str:
"""
Extracts a thumbnail (JPEG) from the video and returns it as a base64 string.
Default frame at 1 second.
"""
tmp_thumb = Path(tempfile.gettempdir()) / f"{Path(video_path).stem}_thumb.jpg"
cmd = [
"ffmpeg", "-nostdin", "-y",
"-ss", time_position,
"-i", video_path,
"-frames:v", "1",
"-q:v", "2",
str(tmp_thumb),
]
_run_ffmpeg(cmd)
with open(tmp_thumb, "rb") as f:
encoded = base64.b64encode(f.read()).decode("utf-8")
return encoded
# ---------- AUGMENT VIDEO ----------
def augment_video_random(
*,
input_path: str,
output_path: Optional[str] = None,
crf: int = 20,
preset: str = "medium",
variation_idx: int = 0,
border_width: int = 0,
border_color: str = "black",
effect_name: Optional[str] = None,
) -> str:
if effect_name and effect_name in SAFE_EFFECTS:
vf = SAFE_EFFECTS[effect_name]
else:
import random
k_b, k_c, k_h, k_s = [random.randint(-3, 3) for _ in range(4)]
brightness = max(-1.0, min(1.0, k_b * 0.2))
contrast = max(0.0, min(2.0, 1.0 + k_c * 0.03))
hue_rad = math.radians(k_h * 3.0)
sat_scale = max(0.0, 1.0 + k_s * 0.05)
vf = f"hue=h={hue_rad:.6f}:s={sat_scale:.4f},eq=contrast={contrast:.4f}:brightness={brightness:.4f}"
if border_width > 0:
ffmpeg_color = border_color.replace("#", "0x") if border_color.startswith("#") else border_color
bw = int(border_width)
vf += f",pad=iw+{bw*2}:ih+{bw*2}:{bw}:{bw}:{ffmpeg_color}"
inp = Path(input_path)
suffix = f"_augmented_{variation_idx}" if variation_idx > 0 else "_augmented"
out = Path(output_path) / f"{inp.stem}{suffix}.mp4" if output_path else inp.with_name(f"{inp.stem}{suffix}.mp4")
cmd = [
"ffmpeg", "-nostdin", "-y",
"-i", str(inp),
"-map", "0:v:0", "-map", "0:a?",
"-vf", vf,
"-c:v", "libx264", "-preset", preset, "-crf", str(crf),
"-pix_fmt", "yuv420p",
"-c:a", "copy",
"-movflags", "+faststart",
str(out),
]
_run_ffmpeg(cmd)
return str(out)
# ---------- VIDEO UPLOAD TO R2 ----------
def upload_video_to_r2(video_bytes: bytes, file_name: str) -> str:
s3 = boto3.client(
"s3",
endpoint_url=os.getenv("R2_ENDPOINT"),
aws_access_key_id=os.getenv("R2_ACCESS_KEY"),
aws_secret_access_key=os.getenv("R2_SECRET_KEY"),
region_name="auto",
)
bucket = os.getenv("R2_BUCKET_NAME")
key = f"videos/{uuid.uuid4()}_{file_name}"
s3.put_object(Bucket=bucket, Key=key, Body=video_bytes, ContentType="video/mp4")
return f"{_public_base_url()}/{key}"
# ---------- PROCESS + UPLOAD ----------
def process_video_with_hash_info(
input_path: str,
output_path: Optional[str] = None,
variation_idx: int = 0,
border_width: int = 0,
border_color: str = "black",
effect_name: Optional[str] = None,
) -> dict:
logger.info(f"[process] Starting process_video_with_hash_info for {Path(input_path).name}")
logger.info(f"[process] Step 1/5 β computing input hash")
in_sha = _sha256(Path(input_path))
in_id = _hashid_short(in_sha)
logger.info(f"[process] Step 2/5 β augmenting video (effect={effect_name or 'random'})")
out_path = augment_video_random(
input_path=input_path,
output_path=output_path,
variation_idx=variation_idx,
border_width=border_width,
border_color=border_color,
effect_name=effect_name,
)
logger.info(f"[process] Step 3/5 β computing output hash")
out_sha = _sha256(Path(out_path))
out_id = _hashid_short(out_sha)
logger.info(f"[process] Step 4/5 β extracting thumbnail")
safe_tmp = Path(tempfile.gettempdir()) / Path(out_path).name
shutil.copy(out_path, safe_tmp)
# thumbnail
try:
thumbnail_b64 = extract_thumbnail(str(safe_tmp))
except Exception as e:
logger.error(f"Thumbnail extraction failed: {e}")
thumbnail_b64 = ""
logger.info(f"[process] Step 5/5 β uploading to R2")
try:
with open(safe_tmp, "rb") as f:
video_bytes = f.read()
r2_url = upload_video_to_r2(video_bytes, Path(safe_tmp).name)
except Exception as e:
logger.error(f"Upload to R2 failed: {e}")
r2_url = None
logger.info(f"[process] Done. output_hashid={out_id}, r2_url={r2_url}")
return {
"input_hashid": in_id,
"output_name": Path(safe_tmp).name,
"output_path": str(safe_tmp),
"output_hashid": out_id,
"output_r2_url": r2_url,
"thumbnail": thumbnail_b64,
"effect": effect_name or "random",
} |