vidaio / services /scoring /server.py
DavidHstar's picture
Update model files
f72237c verified
Raw
History Blame Contribute Delete
106 kB
import os
import cv2
import glob
import json
import time
import math
import torch
import random
import asyncio
import aiohttp
from loguru import logger
import tempfile
import subprocess
import numpy as np
from pydantic import BaseModel
from typing import Optional, List
from firerequests import FireRequests
from vidaio_subnet_core import CONFIG
from torchvision.models import resnet50
from fastapi import FastAPI, HTTPException
import torchvision.transforms as transforms
from pieapp_metric import calculate_pieapp_score
from vidaio_subnet_core.utilities.storage_client import storage_client
from vmaf_metric import calculate_vmaf, convert_mp4_to_y4m, trim_video
from services.video_scheduler.video_utils import get_trim_video_path, delete_videos_with_fileid
from scoring_function import calculate_compression_score
# Compression scoring constants
COMPRESSION_RATE_WEIGHT = 0.7 # w_c
COMPRESSION_VMAF_WEIGHT = 0.3 # w_vmaf
SOFT_THRESHOLD_MARGIN = 5.0 # Margin below VMAF threshold for soft scoring zone
app = FastAPI()
fire_requests = FireRequests()
VMAF_THRESHOLD = CONFIG.score.vmaf_threshold
PIEAPP_SAMPLE_COUNT = CONFIG.score.pieapp_sample_count
PIEAPP_THRESHOLD = CONFIG.score.pieapp_threshold
VMAF_SAMPLE_COUNT = CONFIG.score.vmaf_sample_count
class UpscalingScoringRequest(BaseModel):
"""
Request model for upscaling scoring. Contains URLs for distorted videos and the reference video path.
"""
distorted_urls: List[str]
reference_paths: List[str]
uids: List[int]
payload_urls: List[str]
video_ids: List[str]
uploaded_object_names: List[str]
content_lengths: List[int]
task_types: List[str]
fps: Optional[float] = None
subsample: Optional[int] = 1
verbose: Optional[bool] = False
progress: Optional[bool] = False
class CompressionScoringRequest(BaseModel):
"""
Request model for compression scoring. Contains URLs for distorted videos and the reference video path.
"""
distorted_urls: List[str]
reference_paths: List[str]
uids: List[int]
video_ids: List[str]
uploaded_object_names: List[str]
vmaf_threshold: float
fps: Optional[float] = None
subsample: Optional[int] = 1
verbose: Optional[bool] = False
progress: Optional[bool] = False
class OrganicsUpscalingScoringRequest(BaseModel):
"""
Request model for scoring. Contains URLs for distorted videos and the reference video path.
"""
distorted_urls: List[str]
reference_urls: List[str]
task_types: List[str]
uids: List[int]
fps: Optional[float] = None
subsample: Optional[int] = 1
verbose: Optional[bool] = False
progress: Optional[bool] = False
class OrganicsCompressionScoringRequest(BaseModel):
"""
Request model for scoring. Contains URLs for distorted videos and the reference video path.
"""
distorted_urls: List[str]
reference_urls: List[str]
vmaf_thresholds: List[float]
uids: List[int]
fps: Optional[float] = None
subsample: Optional[int] = 1
verbose: Optional[bool] = False
progress: Optional[bool] = False
class UpscalingScoringResponse(BaseModel):
"""
Response model for upscaling scoring. Contains the list of calculated scores for each distorted video.
"""
vmaf_scores: List[float]
pieapp_scores: List[float]
quality_scores: List[float]
length_scores: List[float]
final_scores: List[float]
reasons: List[str]
class CompressionScoringResponse(BaseModel):
"""
Response model for compression scoring. Contains the list of calculated scores for each distorted video.
"""
vmaf_scores: List[float]
compression_rates: List[float]
final_scores: List[float]
reasons: List[str]
class OrganicsUpscalingScoringResponse(BaseModel):
"""
Response model for organics scoring. Contains the list of calculated scores for each distorted video.
"""
vmaf_scores: List[float]
pieapp_scores: List[float]
quality_scores: List[float]
length_scores: List[float]
final_scores: List[float]
reasons: List[str]
class OrganicsCompressionScoringResponse(BaseModel):
"""
Response model for organics scoring. Contains the list of calculated scores for each distorted video.
"""
vmaf_scores: List[float]
compression_rates: List[float]
final_scores: List[float]
reasons: List[str]
# Load pre-trained model for feature extraction
def load_quality_model():
"""Load ResNet50 model for quality assessment"""
from torchvision.models import ResNet50_Weights
model = resnet50(weights=ResNet50_Weights.IMAGENET1K_V1)
model.eval()
return model
async def download_video(video_url: str, verbose: bool) -> tuple[str, float]:
"""
Download a video from the given URL and save it to a temporary file.
Args:
video_url (str): The URL of the video to download.
verbose (bool): Whether to show download progress.
Returns:
tuple[str, float]: A tuple containing the path to the downloaded video file
and the time taken to download it.
Raises:
Exception: If the download fails or takes longer than the timeout.
"""
try:
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as vid_temp:
file_path = vid_temp.name # Path to the temporary file
if verbose:
logger.info(f"Downloading video from {video_url} to {file_path}")
timeout = aiohttp.ClientTimeout(sock_connect=30, total=120)
start_time = time.time() # Record start time
async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(video_url) as response:
if response.status != 200:
raise Exception(f"Failed to download video. HTTP status: {response.status}")
with open(file_path, "wb") as f:
async for chunk in response.content.iter_chunked(2 * 1024 * 1024):
f.write(chunk)
end_time = time.time() # Record end time
download_time = end_time - start_time # Calculate download duration
if not os.path.exists(file_path) or os.path.getsize(file_path) == 0:
raise Exception(f"Download failed or file is empty: {file_path}")
if verbose:
logger.info(f"File successfully downloaded to: {file_path}")
logger.info(f"Download time: {download_time:.2f} seconds")
return file_path, download_time
except aiohttp.ClientError as e:
raise Exception(f"Download failed due to a network error: {e}")
except asyncio.TimeoutError:
raise Exception("Download timed out")
# Function to get ClipIQA+ score programmatically
def get_clipiqa_score(video_path, num_frames=3):
"""Get ClipIQA+ score for a video file"""
frames = extract_frames(video_path, num_frames)
score = calculate_clipiqa_plus_score(frames)
return score
# Extract frames from video
def extract_frames(video_path, num_frames=3, frame_indices=None):
"""Extract frames from video using specified indices or random ones"""
cap = cv2.VideoCapture(video_path)
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
if total_frames < num_frames:
raise ValueError(f"Video has only {total_frames} frames, but {num_frames} frames requested")
# Use provided frame indices or select random ones
if frame_indices is None:
frame_indices = random.sample(range(total_frames), num_frames)
else:
# Validate that all requested indices are within bounds
if max(frame_indices) >= total_frames:
raise ValueError(f"Frame index {max(frame_indices)} exceeds video length {total_frames}")
frames = []
for idx in frame_indices:
cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
ret, frame = cap.read()
if ret:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # Convert BGR to RGB
frames.append(frame)
else:
raise ValueError(f"Failed to read frame at index {idx}")
cap.release()
return frames
def extract_frames_from_y4m(y4m_path):
"""
Extract frames from Y4M file for color validation.
This reuses the Y4M file already created for VMAF calculation,
eliminating redundant video processing.
Args:
y4m_path: Path to Y4M file (output of convert_mp4_to_y4m)
Returns:
list: List of frames as numpy arrays (BGR format for cv2)
"""
frames = []
# Create temporary directory for extracted frames
temp_dir = tempfile.mkdtemp()
output_pattern = os.path.join(temp_dir, "frame_%04d.png")
try:
# Extract all frames from Y4M as PNG using FFmpeg
# Y4M files already contain only the selected frames, so no filtering needed
extract_cmd = [
"ffmpeg", "-i", y4m_path,
output_pattern,
"-y",
"-hide_banner", "-loglevel", "error"
]
result = subprocess.run(
extract_cmd,
capture_output=True,
text=True,
timeout=30
)
if result.returncode != 0:
logger.error(f"FFmpeg Y4M frame extraction failed: {result.stderr}")
return frames
# Read extracted PNG files in order
png_files = sorted(glob.glob(os.path.join(temp_dir, "frame_*.png")))
for png_file in png_files:
frame = cv2.imread(png_file)
if frame is not None:
frames.append(frame)
else:
logger.warning(f"Failed to read extracted frame: {png_file}")
logger.debug(f"Successfully extracted {len(frames)} frames from Y4M file")
except subprocess.TimeoutExpired:
logger.error("FFmpeg Y4M frame extraction timed out")
except Exception as e:
logger.error(f"Error extracting frames from Y4M: {e}")
finally:
# Cleanup temporary files
try:
for png_file in glob.glob(os.path.join(temp_dir, "*.png")):
os.remove(png_file)
os.rmdir(temp_dir)
except Exception as e:
logger.warning(f"Failed to cleanup temp frames: {e}")
return frames
def validate_color_channels_on_frames(frames):
"""
Validate that frames have color information (not grayscale).
Reuses frames already extracted for VMAF calculation.
Args:
frames: List of frames (BGR numpy arrays from cv2)
Returns:
tuple: (is_valid, reason)
"""
try:
if not frames:
return False, "No frames available for color validation"
color_threshold = 5.0 # Same threshold as before
for i, frame in enumerate(frames):
# Convert BGR to RGB
img_array = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Extract RGB channels
r_channel = img_array[:,:,0].astype(float)
g_channel = img_array[:,:,1].astype(float)
b_channel = img_array[:,:,2].astype(float)
# Calculate average difference between channels
rg_diff = np.mean(np.abs(r_channel - g_channel))
rb_diff = np.mean(np.abs(r_channel - b_channel))
gb_diff = np.mean(np.abs(g_channel - b_channel))
# If all channels are nearly identical, it's grayscale
if rg_diff < color_threshold and rb_diff < color_threshold and gb_diff < color_threshold:
logger.warning(
f"Frame {i} appears to be grayscale: "
f"RG diff={rg_diff:.2f}, RB diff={rb_diff:.2f}, GB diff={gb_diff:.2f}"
)
return False, f"Video has no color information (grayscale). UV channels required. Frame {i} detected as grayscale."
return True, "Color channels validated"
except Exception as e:
logger.error(f"Error validating color channels: {e}")
return False, f"Error validating color: {str(e)}"
def validate_chroma_quality_on_frames(ref_frames, dist_frames, threshold=0.7):
"""
Validate chroma (UV) quality by comparing reference and distorted frames.
Reuses frames already extracted for VMAF calculation.
Args:
ref_frames: List of reference frames (BGR numpy arrays)
dist_frames: List of distorted frames (BGR numpy arrays)
threshold: Minimum acceptable chroma similarity (0.0-1.0)
Returns:
tuple: (is_valid, reason)
"""
try:
if len(ref_frames) != len(dist_frames):
return False, "Frame count mismatch between reference and distorted"
if not ref_frames or not dist_frames:
return False, "No frames available for chroma validation"
chroma_ratios = []
for i, (ref_frame, dist_frame) in enumerate(zip(ref_frames, dist_frames)):
# Convert BGR to YUV
ref_yuv = cv2.cvtColor(ref_frame, cv2.COLOR_BGR2YUV)
dist_yuv = cv2.cvtColor(dist_frame, cv2.COLOR_BGR2YUV)
# Extract U and V channels
ref_u = ref_yuv[:, :, 1].astype(float)
ref_v = ref_yuv[:, :, 2].astype(float)
dist_u = dist_yuv[:, :, 1].astype(float)
dist_v = dist_yuv[:, :, 2].astype(float)
# Calculate chroma variance/energy
ref_u_variance = np.var(ref_u)
ref_v_variance = np.var(ref_v)
dist_u_variance = np.var(dist_u)
dist_v_variance = np.var(dist_v)
ref_chroma_energy = ref_u_variance + ref_v_variance
dist_chroma_energy = dist_u_variance + dist_v_variance
if ref_chroma_energy > 0:
chroma_ratio = dist_chroma_energy / ref_chroma_energy
chroma_ratios.append(chroma_ratio)
logger.debug(f"Frame {i} chroma ratio: {chroma_ratio:.3f}")
if not chroma_ratios:
return False, "Could not calculate chroma ratios"
# Calculate average chroma ratio
avg_chroma_ratio = np.mean(chroma_ratios)
logger.info(f"Average chroma quality ratio: {avg_chroma_ratio:.3f}, Threshold: {threshold}")
if avg_chroma_ratio < threshold:
return False, f"Chroma quality too low: {avg_chroma_ratio:.3f} < {threshold} (UV channels reduced/degraded)"
return True, f"Chroma quality validated: {avg_chroma_ratio:.3f}"
except Exception as e:
logger.error(f"Error validating chroma quality: {e}")
return False, f"Error validating chroma: {str(e)}"
# Calculate ClipIQA+ inspired score
def calculate_clipiqa_plus_score(frames):
"""Calculate ClipIQA+ inspired score for given frames"""
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = load_quality_model().to(device)
# Preprocessing pipeline
preprocess = transforms.Compose([
transforms.ToPILImage(),
transforms.Resize((224, 224)),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
scores = []
for frame in frames:
# Preprocess frame
frame_tensor = preprocess(frame).unsqueeze(0).to(device)
with torch.no_grad():
# Extract features using ResNet50
features = model(frame_tensor)
# Calculate quality metrics
# 1. Feature magnitude (higher = more complex/rich content)
feature_magnitude = torch.norm(features, p=2).item()
# 2. Feature variance (higher = more diverse content)
feature_variance = torch.var(features).item()
# 3. Sharpness estimation using Laplacian variance
gray_frame = cv2.cvtColor(frame, cv2.COLOR_RGB2GRAY)
laplacian_var = cv2.Laplacian(gray_frame, cv2.CV_64F).var()
# 4. Contrast estimation
contrast = np.std(gray_frame)
# Combine metrics with weights (inspired by ClipIQA+ approach)
# Normalize each metric to [0, 1] range
norm_magnitude = min(1.0, feature_magnitude / 100.0)
norm_variance = min(1.0, feature_variance / 1000.0)
norm_sharpness = min(1.0, laplacian_var / 1000.0)
norm_contrast = min(1.0, contrast / 100.0)
# Weighted combination (ClipIQA+ inspired weights)
quality_score = (
0.3 * norm_magnitude +
0.25 * norm_variance +
0.25 * norm_sharpness +
0.2 * norm_contrast
)
scores.append(quality_score)
# Return the average score across all frames
return np.mean(scores)
def calculate_psnr(ref_frame: np.ndarray, dist_frame: np.ndarray) -> float:
"""
Calculate Peak Signal-to-Noise Ratio (PSNR) between reference and distorted frames.
Args:
ref_frame (np.ndarray): The reference video frame.
dist_frame (np.ndarray): The distorted video frame.
Returns:
float: The PSNR value between the reference and distorted frames.
"""
mse = np.mean((ref_frame - dist_frame) ** 2)
if mse == 0:
return 1000 # Maximum PSNR value (perfect similarity)
return 10 * np.log10((255.0**2) / mse)
def calculate_length_score(content_length):
"""
Convert content length in seconds to a normalized length score.
Args:
content_length (float): Video duration in seconds (5-320s)
Returns:
float: Normalized length score (0-1)
"""
return math.log(1 + content_length) / math.log(1 + 320)
def calculate_preliminary_score(quality_score, length_score, quality_weight=0.5, length_weight=0.5):
"""
Calculate the preliminary score from quality and length scores.
Args:
quality_score (float): Normalized quality score (0-1)
length_score (float): Normalized length score (0-1)
quality_weight (float): Weight for quality component (default: 0.5)
length_weight (float): Weight for length component (default: 0.5)
Returns:
float: Preliminary combined score (0-1)
"""
return (quality_score * quality_weight) + (length_score * length_weight)
def calculate_final_score(s_pre):
"""
Transform preliminary score into final score using exponential function.
Args:
s_pre (float): Preliminary score (0-1)
Returns:
float: Final exponentially-transformed score
"""
return 0.1 * math.exp(6.979 * (s_pre - 0.5))
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def calculate_quality_score(pieapp_score):
sigmoid_normalized_score = sigmoid(pieapp_score)
original_at_zero = (1 - (np.log10(sigmoid(0) + 1) / np.log10(3.5))) ** 2.5
original_at_two = (1 - (np.log10(sigmoid(2.0) + 1) / np.log10(3.5))) ** 2.5
original_value = (1 - (np.log10(sigmoid_normalized_score + 1) / np.log10(3.5))) ** 2.5
scaled_value = 1 - ((original_value - original_at_zero) / (original_at_two - original_at_zero))
return scaled_value
def get_sample_frames(ref_cap, dist_cap, total_frames):
"""
Get sample frames from both reference and distorted videos.
Args:
ref_cap: Reference video capture
dist_cap: Distorted video capture
total_frames: Total number of frames in the videos
Returns:
tuple: (ref_frames, dist_frames) - Lists of sampled frames
"""
# Determine how many frames to sample
frames_to_sample = min(PIEAPP_SAMPLE_COUNT, total_frames)
# Generate a random starting point that ensures we can get consecutive frames
# without exceeding the total number of frames
max_start_frame = total_frames - frames_to_sample
if max_start_frame <= 0:
start_frame = 0
else:
start_frame = random.randint(0, max_start_frame)
logger.info(f"Sampling {frames_to_sample} consecutive frames starting from frame {start_frame}")
# Extract frames from reference video
ref_frames = []
ref_cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
for _ in range(frames_to_sample):
ret, frame = ref_cap.read()
if not ret:
break
ref_frames.append(frame)
# Extract frames from distorted video
dist_frames = []
dist_cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
for _ in range(frames_to_sample):
ret, frame = dist_cap.read()
if not ret:
break
dist_frames.append(frame)
return ref_frames, dist_frames
def get_frame_count(video_path):
"""
Get frame count using ffprobe with optimized approach.
First tries fast metadata read, falls back to frame counting if needed.
"""
try:
# Method 1: Fast metadata read (similar to OpenCV speed)
cmd = [
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=nb_frames", # Read from metadata (FAST)
"-of", "default=nokey=1:noprint_wrappers=1",
video_path
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
timeout=5 # Fast timeout for metadata read
)
frame_count_str = result.stdout.strip()
if frame_count_str and frame_count_str != "N/A":
frame_count = int(frame_count_str)
logger.debug(f"ffprobe (fast): {video_path} has {frame_count} frames")
return frame_count
# Method 2: Frame counting fallback (slower but accurate)
logger.debug(f"Metadata unavailable, counting frames for {video_path}")
cmd = [
"ffprobe",
"-v", "error",
"-count_frames", # Actually count frames (SLOWER)
"-select_streams", "v:0",
"-show_entries", "stream=nb_read_frames",
"-of", "default=nokey=1:noprint_wrappers=1",
video_path
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
timeout=30 # Longer timeout for counting
)
frame_count = int(result.stdout.strip())
logger.debug(f"ffprobe (counted): {video_path} has {frame_count} frames")
return frame_count
except subprocess.TimeoutExpired:
logger.error(f"ffprobe timeout for {video_path}")
raise Exception(f"Frame count timeout: ffprobe took too long")
except subprocess.CalledProcessError as e:
logger.error(f"ffprobe failed for {video_path}: {e.stderr}")
raise Exception(f"Frame count failed: ffprobe error - {e.stderr}")
except ValueError as e:
logger.error(f"ffprobe returned invalid frame count for {video_path}: {e}")
raise Exception(f"Frame count failed: invalid ffprobe output")
except Exception as e:
logger.error(f"Unexpected error with ffprobe for {video_path}: {e}")
raise Exception(f"Frame count failed: {e}")
def is_valid_video(video_path):
"""
Check if a video file is valid and can be opened using ffprobe.
Optimized for speed - only checks basic properties without counting frames.
Returns:
bool: True if video is valid, False otherwise
"""
try:
# First check if file exists and has size
if not os.path.exists(video_path):
logger.error(f"Video file does not exist: {video_path}")
return False
file_size = os.path.getsize(video_path)
if file_size == 0:
logger.error(f"Video file is empty: {video_path}")
return False
if file_size < 1024: # Less than 1KB is suspicious
logger.warning(f"Video file is very small ({file_size} bytes): {video_path}")
# Use ffprobe to validate video stream (FAST - no frame counting)
cmd = [
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=codec_type,width,height,duration", # Basic properties only
"-of", "csv=p=0",
video_path
]
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=True,
timeout=5 # Reduced timeout since we're not counting frames
)
# Parse output: format is "video,width,height,duration"
output = result.stdout.strip()
if not output:
logger.error(f"ffprobe returned empty output for {video_path}")
return False
parts = output.split(',')
if len(parts) < 3:
logger.error(f"ffprobe validation failed: incomplete output for {video_path}")
return False
# Check if it's a video stream
if parts[0] != "video":
logger.error(f"ffprobe validation failed: not a video file: {video_path}")
return False
# Check if width and height are valid
try:
width = int(parts[1])
height = int(parts[2])
if width <= 0 or height <= 0:
logger.error(f"Invalid video dimensions: {width}x{height} for {video_path}")
return False
except (ValueError, IndexError):
logger.error(f"Could not parse video dimensions for {video_path}")
return False
# Optionally check duration if available
if len(parts) > 3 and parts[3]:
try:
duration = float(parts[3])
if duration <= 0:
logger.warning(f"Video has invalid duration: {duration}s for {video_path}")
except ValueError:
pass # Duration not critical for validation
return True
except subprocess.TimeoutExpired:
logger.error(f"ffprobe validation timeout for {video_path}")
return False
except subprocess.CalledProcessError as e:
logger.error(f"ffprobe validation failed for {video_path}: {e.stderr}")
return False
except Exception as e:
logger.error(f"Unexpected error validating {video_path}: {e}")
return False
except subprocess.TimeoutExpired:
logger.error(f"ffprobe validation timeout for {video_path}")
return False
except subprocess.CalledProcessError as e:
logger.error(f"ffprobe validation failed for {video_path}: {e.stderr}")
return False
except Exception as e:
logger.error(f"Unexpected error validating {video_path}: {e}")
return False
def validate_dist_encoding_settings(dist_path: str, ref_path: str, task: str):
"""
Validate that distorted video uses specific encoding settings.
"""
try:
# Enhanced ffprobe to capture both stream and format encoder tags
cmd = [
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=codec_name,profile,level,sample_aspect_ratio,pix_fmt,"
"width,height,r_frame_rate,color_space,color_primaries,color_transfer",
"-show_entries", "format=tags=encoder",
"-show_format",
"-of", "json",
dist_path
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=10)
info = json.loads(result.stdout)
video_stream = info.get("streams", [{}])[0]
format_info = info.get("format", {})
format_tags = format_info.get("tags", {})
codec = video_stream.get("codec_name", "")
profile = video_stream.get("profile", "")
level = video_stream.get("level")
sar = video_stream.get("sample_aspect_ratio", "1:1")
pix_fmt = video_stream.get("pix_fmt", "")
width = video_stream.get("width", 0)
height = video_stream.get("height", 0)
container = format_info.get("format_name", "")
fps_str = video_stream.get("r_frame_rate", "0/1")
encoder_tag = ""
# Colorspace properties
dist_color_space = video_stream.get("color_space", None)
dist_color_primaries = video_stream.get("color_primaries", None)
dist_color_transfer = video_stream.get("color_transfer", None)
# Check encoder tags
stream_tags = video_stream.get("tags", {})
encoder_tag = stream_tags.get("encoder", "").lower() or format_tags.get("encoder", "").lower()
errors = []
ref_fps_str = get_video_fps(ref_path, original_str=True)
if fps_str != ref_fps_str:
errors.append(f"FPS must be {ref_fps_str}, got {fps_str}")
if task == "compression":
ref_width, ref_height = get_video_dimensions(ref_path)
if width != ref_width or height != ref_height:
errors.append(f"Resolution must be {ref_width}x{ref_height}, got {width}x{height}")
# REQUIRED encoding checks
if task == "compression" and codec != "av1" or task == "upscaling" and codec != "hevc":
errors.append(f"Codec must be AV1 for compression & hevc for upscaling, got {codec}")
if "ivf" in container.lower():
errors.append("Container must be MP4, got IVF (incompatible for concatenation)")
if container not in ["mov,mp4,m4a,3gp,3g2,mj2", "mp4", "isom"]:
errors.append(f"Container must be proper MP4, got {container}")
if profile != "Main":
errors.append(f"AV1 profile must be 'Main', got {profile}")
if sar != "1:1":
errors.append(f"Sample aspect ratio must be 1:1, got {sar}")
if pix_fmt != "yuv420p":
errors.append(f"Pixel format must be yuv420p, got {pix_fmt}")
# Colorspace validation
if ref_path and os.path.exists(ref_path):
try:
# Get reference colorspace
ref_cmd = [
"ffprobe", "-v", "error", "-select_streams", "v:0",
"-show_entries", "stream=color_space,color_primaries,color_transfer",
"-of", "json", ref_path
]
ref_result = subprocess.run(ref_cmd, capture_output=True, text=True,
timeout=5, check=True)
ref_info = json.loads(ref_result.stdout)
ref_stream = ref_info.get("streams", [{}])[0]
ref_color_space = ref_stream.get("color_space", None)
ref_color_primaries = ref_stream.get("color_primaries", None)
ref_color_transfer = ref_stream.get("color_transfer", None)
# Colorspace - mismatch affects color accuracy
if (ref_color_space and dist_color_space and
ref_color_space != dist_color_space):
errors.append(f"color_space mismatch, reference: {ref_color_space}, distorted: {dist_color_space}")
# Transfer characteristics - mismatch affects VMAF significantly
if (ref_color_transfer and dist_color_transfer and
ref_color_transfer != dist_color_transfer):
errors.append(f"color_transfer mismatch, reference: {ref_color_transfer}, distorted: {dist_color_transfer}")
# Primaries - less critical but still affects color accuracy
if (ref_color_primaries and dist_color_primaries and
ref_color_primaries != dist_color_primaries):
errors.append(f"color_primaries mismatch, reference: {ref_color_primaries}, distorted: {dist_color_primaries}")
logger.warning(f" Ref: space={ref_color_space}, primaries={ref_color_primaries}, transfer={ref_color_transfer}")
logger.warning(f" Dist: space={dist_color_space}, primaries={dist_color_primaries}, transfer={dist_color_transfer}")
# NOT a hard fail - allows innovation in HDR/Dolby Vision/etc.
except subprocess.CalledProcessError:
logger.warning("Could not read reference colorspace info")
# SVT-AV1 detection, only logging for now
is_svtav1 = any(keyword in encoder_tag for keyword in [
"libsvtav1", "svt-av1", "svtav1"
])
# # AV1 Level validation (FFmpeg uses internal integer levels)
# av1_level_valid = False
# if level is not None:
# # FFmpeg level 12 ≈ AV1 Level 5.1 (4K), map roughly
# # This is internal FFmpeg representation, not spec levels
# if isinstance(level, int) and level >= 8: # Reasonable minimum for HD+
# av1_level_valid = True
# else:
# errors.append(f"AV1 level {level} too low for practical use")
# else:
# # Level missing is acceptable if other params valid
# logger.warning("AV1 level not specified in metadata")
# Encoder preference validation (not blocking)
if is_svtav1:
logger.info(f"✅ SVT-AV1 encoder detected: {encoder_tag}")
elif encoder_tag:
other_av1_encoders = ["libaom-av1", "rav1e", "libdav1d"]
is_known_av1 = any(other in encoder_tag for other in other_av1_encoders)
if is_known_av1:
logger.info(f"ℹ️ Known AV1 encoder: {encoder_tag}")
else:
logger.warning(f"⚠️ Unknown encoder: {encoder_tag}")
else:
logger.warning("⚠️ No encoder tag found")
logger.debug(f"Video analysis: {width}x{height}@{fps_str}, {codec}/{profile}, "
f"container={container}, color_space={dist_color_space}, "
f"SVT-AV1={is_svtav1}")
if errors:
return False, "; ".join(errors)
color_info = f"space={dist_color_space or 'default'}" if dist_color_space else ""
encoder_status = "SVT-AV1" if is_svtav1 else "Other AV1"
return True, f"Valid encoding ({encoder_status}, {width}x{height}, {color_info}, level {level})"
except subprocess.CalledProcessError as e:
return False, f"ffprobe failed: {e.stderr}"
except Exception as e:
return False, f"Validation error: {str(e)}"
def get_video_dimensions(video_path):
"""
Get video dimensions (width, height) using ffprobe.
This avoids OpenCV's AV1 decoder warnings.
Returns:
tuple: (width, height) or (None, None) if failed
"""
try:
cmd = [
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=width,height",
"-of", "csv=p=0",
video_path
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=5)
width, height = map(int, result.stdout.strip().split(','))
return width, height
except Exception as e:
logger.warning(f"ffprobe dimension check failed for {video_path}: {e}")
return None, None
def get_video_fps(video_path, original_str=False):
"""
Get video FPS using ffprobe.
This avoids OpenCV's AV1 decoder warnings.
Returns:
float: FPS or None if failed
"""
try:
cmd = [
"ffprobe",
"-v", "error",
"-select_streams", "v:0",
"-show_entries", "stream=r_frame_rate",
"-of", "default=noprint_wrappers=1:nokey=1",
video_path
]
result = subprocess.run(cmd, capture_output=True, text=True, check=True, timeout=5)
# r_frame_rate returns format like "30000/1001" or "30/1"
fps_str = result.stdout.strip()
if original_str:
return fps_str
if '/' in fps_str:
num, den = map(int, fps_str.split('/'))
return num / den
return float(fps_str)
except Exception as e:
logger.warning(f"ffprobe FPS check failed for {video_path}: {e}")
return None
def calculate_pieapp_score_on_samples(ref_frames, dist_frames):
"""
Calculate PIE-APP score on sampled frames without creating temporary files.
Args:
ref_frames: List of reference frames
dist_frames: List of distorted frames
Returns:
float: Average PIE-APP score
"""
if not ref_frames:
logger.info("No ref frames to process")
return -100
if not dist_frames:
logger.info("No dist frames to process")
return 2.0
class FrameProvider:
def __init__(self, frames):
self.frames = frames
self.current_frame = 0
self.frame_count = len(frames)
def read(self):
if self.current_frame < self.frame_count:
frame = self.frames[self.current_frame]
self.current_frame += 1
return True, frame
return False, None
def get(self, prop_id):
if prop_id == cv2.CAP_PROP_FRAME_COUNT:
return self.frame_count
return 0
def set(self, prop_id, value):
if prop_id == cv2.CAP_PROP_POS_FRAMES:
self.current_frame = int(value) if value < self.frame_count else self.frame_count
return True
return False
def release(self):
# Nothing to release
pass
def isOpened(self):
return self.frame_count > 0
ref_provider = FrameProvider(ref_frames)
dist_provider = FrameProvider(dist_frames)
try:
score = calculate_pieapp_score(ref_provider, dist_provider, frame_interval=1)
return score
except Exception as e:
logger.error(f"Error calculating PieAPP score on frames: {str(e)}")
return -100
def upscale_video(input_path, scale_factor=2):
"""
Upscales a video using FFmpeg by the specified scale factor.
Args:
input_path (str): Path to the input video file
scale_factor (int): Factor by which to upscale the video (default: 2)
Returns:
str: Path to the upscaled video file
"""
filename, extension = os.path.splitext(input_path)
output_path = f"{filename}_upscaled{extension}"
# Get video dimensions using ffprobe (avoids AV1 warnings)
width, height = get_video_dimensions(input_path)
if width is None or height is None:
raise ValueError(f"Could not get video dimensions for {input_path}")
new_width = width * scale_factor
new_height = height * scale_factor
cmd = [
"ffmpeg",
"-i", input_path,
"-vf", f"scale={new_width}:{new_height}",
"-c:v", "libx264",
"-preset", "medium",
"-crf", "18",
"-an",
output_path
]
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
logger.info(f"Successfully upscaled video to {new_width}x{new_height}")
return output_path
except subprocess.CalledProcessError as e:
logger.error(f"Error upscaling video: {e}")
return input_path
@app.post("/score_upscaling_synthetics")
async def score_upscaling_synthetics(request: UpscalingScoringRequest) -> UpscalingScoringResponse:
logger.info("#################### 🤖 start upscaling request scoring ####################")
start_time = time.time()
quality_scores = []
length_scores = []
final_scores = []
vmaf_scores = []
pieapp_scores = []
reasons = []
if len(request.reference_paths) != len(request.distorted_urls):
raise HTTPException(
status_code=400,
detail="Number of reference paths must match number of distorted URLs"
)
if len(request.uids) != len(request.distorted_urls):
raise HTTPException(
status_code=400,
detail="Number of UIDs must match number of distorted URLs"
)
if len(request.payload_urls) != len(request.distorted_urls):
raise HTTPException(
status_code=400,
detail="Number of payload URLs must match number of distorted URLs"
)
for idx, (ref_path, dist_url, payload_url, uid, video_id, uploaded_object_name, content_length, task_type) in enumerate(zip(
request.reference_paths,
request.distorted_urls,
request.payload_urls,
request.uids,
request.video_ids,
request.uploaded_object_names,
request.content_lengths,
request.task_types
)):
try:
logger.info(f"🧩 Processing pair {idx+1}/{len(request.distorted_urls)}: UID {uid} 🧩")
uid_start_time = time.time() # Start time for this UID
ref_y4m_path = None
dist_path = None
payload_path = None
scale_factor = 2
if task_type == "SD24K":
scale_factor = 4
logger.info(f"scale factor: {scale_factor}")
# Validate reference video using ffprobe (avoids AV1 warnings)
if not is_valid_video(ref_path):
# Add diagnostic information
file_exists = os.path.exists(ref_path)
file_size = os.path.getsize(ref_path) if file_exists else 0
logger.error(f"Error opening reference video file {ref_path}.")
logger.info(f" File exists: {file_exists}")
logger.info(f" File size: {file_size} bytes")
logger.info(f" Current working directory: {os.getcwd()}")
logger.info(f" Assigning score of 0.")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(-100)
reasons.append(f"error opening reference video file: {ref_path} (exists: {file_exists}, size: {file_size})")
continue
try:
dist_path, download_time = await download_video(dist_url, request.verbose)
except Exception as e:
error_msg = f"Failed to download distorted video from {dist_url}: {str(e)}"
logger.error(f"{error_msg}. Assigning score of 0.")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append("failed to download video file from url")
continue
try:
payload_path, download_time = await download_video(payload_url, request.verbose)
except Exception as e:
error_msg = f"Failed to download payload video from {payload_path}: {str(e)}"
logger.error(f"{error_msg}. Assigning score of 0.")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append("failed to download video file from url")
continue
step_time = time.time() - uid_start_time
# Validate video using ffprobe (avoids AV1 warnings)
if not is_valid_video(dist_path):
logger.error(f"Error opening distorted video file from {dist_url}. Assigning score of 0.")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append("error opening distorted video file")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
continue
# === RESOLUTION CHECK ===
dist_width, dist_height = get_video_dimensions(dist_path)
logger.info(f"Distorted video resolution: {dist_width}x{dist_height}")
ref_width, ref_height = get_video_dimensions(payload_path)
logger.info(f"Reference video resolution: {ref_width}x{ref_height}")
expected_width = ref_width * scale_factor
expected_height = ref_height * scale_factor
if dist_width != expected_width or dist_height != expected_height:
logger.info(f"resolution mismatch: expected {expected_width}x{expected_height}, got {dist_width}x{dist_height}. penalizing miner.")
reasons.append("MINER FAILURE: incorrect upscaling resolution")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0) # 0 = miner penalty
continue
logger.info(f"Resolution check passed: expected {expected_width}x{expected_height}, got {dist_width}x{dist_height}.")
# Only log success after confirming the file was validated
step_time = time.time() - uid_start_time
logger.info(f"♎️ 1. Validated reference video and miner output in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
ref_total_frames = get_frame_count(ref_path)
step_time = time.time() - uid_start_time
logger.info(f"♎️ 2. Retrieved reference video frame count ({ref_total_frames}) in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if ref_total_frames < 10:
logger.info(f"Video must contain at least 10 frames. Assigning score of 0.")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(-100)
reasons.append("reference video has fewer than 10 frames")
continue
sample_size = min(PIEAPP_SAMPLE_COUNT, ref_total_frames)
max_start_frame = ref_total_frames - sample_size
start_frame = 0 if max_start_frame <= 0 else random.randint(0, max_start_frame)
logger.info(f"Selected frame range for pieapp score {idx+1}: {start_frame} to {start_frame + sample_size - 1}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 3. Selected frame range in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Open video with OpenCV only when needed for frame extraction
ref_cap = cv2.VideoCapture(ref_path)
ref_frames = []
ref_cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
for _ in range(sample_size):
ret, frame = ref_cap.read()
if not ret:
break
ref_frames.append(frame)
ref_cap.release()
step_time = time.time() - uid_start_time
logger.info(f"♎️ 4. Extracted sampled frames from reference video in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Validate encoding settings (MUST be AV1 in proper MP4 with specific params)
is_valid_encoding, encoding_msg = validate_dist_encoding_settings(dist_path, ref_path, task="upscaling")
if not is_valid_encoding:
logger.error(f"Invalid encoding settings for distorted video {dist_path}: {encoding_msg}")
logger.info(f" Required: AV1 codec, Main profile, yuv420p, MP4 container, 1:1 SAR")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append(f"invalid encoding settings: {encoding_msg}")
continue
step_time = time.time() - uid_start_time
logger.info(f"♎️ 5. Validated encoding settings ({encoding_msg}) in {step_time:.2f} seconds.")
random_frames = sorted(random.sample(range(ref_total_frames), VMAF_SAMPLE_COUNT))
logger.info(f"Randomly selected {VMAF_SAMPLE_COUNT} frames for VMAF score: frame list: {random_frames}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 6. Selected random frames for VMAF in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
ref_y4m_path = convert_mp4_to_y4m(ref_path, random_frames)
logger.info("The reference video has been successfully converted to Y4M format.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 7. Converted reference video to Y4M in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if len(dist_url) < 10:
logger.info(f"Wrong dist download URL: {dist_url}. Assigning score of 0.")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append("Invalid download URL: the distorted video download URL must be at least 10 characters long.")
continue
step_time = time.time() - uid_start_time
logger.info(f"♎️ 8. Validated distorted video in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Get frame count using ffprobe (no AV1 warnings)
dist_total_frames = get_frame_count(dist_path)
logger.info(f"Distorted video has {dist_total_frames} frames.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 9. Retrieved distorted video frame count in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if dist_total_frames != ref_total_frames:
logger.info(
f"Video length mismatch for pair {idx+1}: ref({ref_total_frames}) != dist({dist_total_frames}). Assigning score of 0."
)
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append("video length mismatch")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
continue
# Calculate VMAF
try:
vmaf_start = time.time()
vmaf_score = calculate_vmaf(ref_y4m_path, dist_path, random_frames, neg_model=False)
vmaf_calc_time = time.time() - vmaf_start
logger.info(f"☣️☣️ VMAF calculation took {vmaf_calc_time:.2f} seconds.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 10. Completed VMAF calculation in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if vmaf_score is not None:
vmaf_scores.append(vmaf_score)
else:
vmaf_score = 0.0
vmaf_scores.append(vmaf_score)
logger.info(f"🎾 VMAF score is {vmaf_score}")
except Exception as e:
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append("failed to calculate VMAF score due to video dimension mismatch")
logger.error(f"Error calculating VMAF score: {e}")
continue
if vmaf_score / 100 < VMAF_THRESHOLD:
logger.info(f"VMAF score is too low, giving zero score, current VMAF score: {vmaf_score}")
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append(f"VMAF score is too low, current VMAF score: {vmaf_score}")
continue
# Open video with OpenCV only when needed for frame extraction
dist_cap = cv2.VideoCapture(dist_path)
# Extract distorted frames
dist_frames = []
dist_cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
for _ in range(sample_size):
ret, frame = dist_cap.read()
if not ret:
break
dist_frames.append(frame)
step_time = time.time() - uid_start_time
logger.info(f"♎️ 11. Extracted sampled frames from distorted video in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Calculate PieAPP score
pieapp_score = calculate_pieapp_score_on_samples(ref_frames, dist_frames)
step_time = time.time() - uid_start_time
logger.info(f"♎️ 12. Calculated PieAPP score in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
logger.info(f"🎾 PieAPP score is {pieapp_score}")
if pieapp_score == -100:
logger.error(f"Uncertain error in pieapp calculation")
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(-100)
reasons.append("Uncertain error in pieapp calculation")
dist_cap.release()
continue
pieapp_scores.append(pieapp_score)
s_q = calculate_quality_score(pieapp_score)
logger.info(f"🏀 Quality score is {s_q}")
dist_cap.release()
s_l = calculate_length_score(content_length)
s_pre = calculate_preliminary_score(s_q, s_l)
s_f = calculate_final_score(s_pre)
quality_scores.append(s_q)
length_scores.append(s_l)
final_scores.append(s_f)
reasons.append("success")
step_time = time.time() - uid_start_time
logger.info(f"🛑 Processed one UID in {step_time:.2f} seconds.")
except Exception as e:
error_msg = f"Failed to process video from {dist_url}: {str(e)}"
logger.error(f"{error_msg}. Assigning score of 0.")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append("failed to process video")
finally:
# Clean up resources for this pair
# ref_cap is released immediately after use for frame extraction
if ref_y4m_path and os.path.exists(ref_y4m_path):
os.unlink(ref_y4m_path)
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
if payload_path and os.path.exists(payload_path):
os.unlink(payload_path)
# Delete the uploaded object
storage_client.delete_file(uploaded_object_name)
for ref_path in request.reference_paths:
if os.path.exists(ref_path):
os.unlink(ref_path)
# tmp_directory = "/tmp"
# try:
# logger.info("🧹 Cleaning up temporary files in /tmp...")
# for file_path in glob.glob(os.path.join(tmp_directory, "*.mp4")):
# os.remove(file_path)
# logger.info(f"Deleted: {file_path}")
# for file_path in glob.glob(os.path.join(tmp_directory, "*.y4m")):
# os.remove(file_path)
# logger.info(f"Deleted: {file_path}")
# except Exception as e:
# logger.error(f"⚠️ Error during cleanup: {e}")
processed_time = time.time() - start_time
logger.info(f"Completed batch scoring of {len(request.distorted_urls)} pairs within {processed_time:.2f} seconds")
logger.info(f"🍉🍉🍉 Calculated final scores: {final_scores} 🍉🍉🍉")
return UpscalingScoringResponse(
vmaf_scores=vmaf_scores,
pieapp_scores=pieapp_scores,
quality_scores=quality_scores,
length_scores=length_scores,
final_scores=final_scores,
reasons=reasons
)
@app.post("/score_compression_synthetics")
async def score_compression_synthetics(request: CompressionScoringRequest) -> CompressionScoringResponse:
logger.info("#################### 🤖 start compression request scoring ####################")
start_time = time.time()
compression_rates = []
final_scores = []
vmaf_scores = []
reasons = []
if len(request.reference_paths) != len(request.distorted_urls):
raise HTTPException(
status_code=400,
detail="Number of reference paths must match number of distorted URLs"
)
if len(request.uids) != len(request.distorted_urls):
raise HTTPException(
status_code=400,
detail="Number of UIDs must match number of distorted URLs"
)
vmaf_threshold = request.vmaf_threshold
for idx, (ref_path, dist_url, uid, video_id, uploaded_object_name) in enumerate(zip(
request.reference_paths,
request.distorted_urls,
request.uids,
request.video_ids,
request.uploaded_object_names,
)):
try:
logger.info(f"🧩 Processing pair {idx+1}/{len(request.distorted_urls)}: UID {uid} 🧩")
uid_start_time = time.time() # Start time for this UID
ref_y4m_path = None
dist_path = None
dist_y4m_path = None
# Validate reference video using ffprobe (avoids AV1 warnings)
if not is_valid_video(ref_path):
# Add diagnostic information
file_exists = os.path.exists(ref_path)
file_size = os.path.getsize(ref_path) if file_exists else 0
logger.error(f"Error opening reference video file {ref_path}.")
logger.info(f" File exists: {file_exists}")
logger.info(f" File size: {file_size} bytes")
logger.info(f" Current working directory: {os.getcwd()}")
logger.info(f" Assigning score of 0.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(-100)
reasons.append(f"error opening reference video file: {ref_path} (exists: {file_exists}, size: {file_size})")
continue
# Only log success after confirming the file was validated
step_time = time.time() - uid_start_time
logger.info(f"♎️ 1. Validated reference video in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
ref_total_frames = get_frame_count(ref_path)
step_time = time.time() - uid_start_time
logger.info(f"♎️ 2. Retrieved reference video frame count ({ref_total_frames}) in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if ref_total_frames < 10:
logger.error(f"Video must contain at least 10 frames. Assigning score of 0.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(-100)
reasons.append("reference video has fewer than 10 frames")
continue
# Get reference video file size for compression rate calculation
ref_file_size = os.path.getsize(ref_path)
logger.info(f"Reference video file size: {ref_file_size} bytes")
if len(dist_url) < 10:
logger.error(f"Wrong dist download URL: {dist_url}. Assigning score of 0.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append("Invalid download URL: the distorted video download URL must be at least 10 characters long.")
continue
try:
dist_path, download_time = await download_video(dist_url, request.verbose)
except Exception as e:
error_msg = f"Failed to download video from {dist_url}: {str(e)}"
logger.error(f"{error_msg}. Assigning score of 0.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append(error_msg)
continue
step_time = time.time() - uid_start_time
logger.info(f"♎️ 3. Downloaded distorted video in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Validate video using ffprobe (avoids AV1 warnings)
if not is_valid_video(dist_path):
logger.error(f"Error opening distorted video file from {dist_url}. Assigning score of 0.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append("error opening distorted video file")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
continue
step_time = time.time() - uid_start_time
logger.info(f"♎️ 4. Validated distorted video in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Validate encoding settings (MUST be AV1 in proper MP4 with specific params)
is_valid_encoding, encoding_msg = validate_dist_encoding_settings(dist_path, ref_path, task="compression")
if not is_valid_encoding:
logger.error(f"Invalid encoding settings for distorted video {dist_path}: {encoding_msg}")
logger.info(f" Required: AV1 codec, Main profile, yuv420p, MP4 container, 1:1 SAR")
vmaf_scores.append(0.0)
compression_rates.append(0.9999)
final_scores.append(0.0)
reasons.append(f"invalid encoding settings: {encoding_msg}")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
continue
step_time = time.time() - uid_start_time
logger.info(f"♎️ 4.5. Validated encoding settings ({encoding_msg}) in {step_time:.2f} seconds.")
dist_total_frames = get_frame_count(dist_path)
logger.info(f"Distorted video has {dist_total_frames} frames.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 5. Retrieved distorted video frame count in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if dist_total_frames != ref_total_frames:
logger.error(
f"Video length mismatch for pair {idx+1}: ref({ref_total_frames}) != dist({dist_total_frames}). Assigning score of 0."
)
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append("video length mismatch")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
continue
# Get distorted video file size for compression rate calculation
dist_file_size = os.path.getsize(dist_path)
logger.info(f"Distorted video file size: {dist_file_size} bytes")
# Calculate compression rate BEFORE VMAF calculation
if ref_file_size > 0 and dist_file_size < ref_file_size:
compression_rate = dist_file_size / ref_file_size
logger.info(f"Compression rate: {compression_rate:.4f} ({dist_file_size}/{ref_file_size})")
else:
compression_rate = 1.0
logger.info("Reference file size is 0 or distorted file size >= reference, setting compression rate to 1.0")
# Sample frames for VMAF calculation
random_frames = sorted(random.sample(range(ref_total_frames), VMAF_SAMPLE_COUNT))
logger.info(f"Randomly selected {VMAF_SAMPLE_COUNT} frames for VMAF score: frame list: {random_frames}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 6. Selected random frames for VMAF in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
ref_y4m_path = convert_mp4_to_y4m(ref_path, random_frames)
logger.info("The reference video has been successfully converted to Y4M format.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 7. Converted reference video to Y4M in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Calculate VMAF and get Y4M paths for validation
dist_y4m_path = None
try:
vmaf_start = time.time()
vmaf_score, dist_y4m_path = calculate_vmaf(ref_y4m_path, dist_path, random_frames, neg_model=True, return_y4m_path=True)
vmaf_calc_time = time.time() - vmaf_start
logger.info(f"☣️☣️ VMAF calculation took {vmaf_calc_time:.2f} seconds.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 8. Completed VMAF calculation in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if vmaf_score is not None:
vmaf_scores.append(vmaf_score)
else:
vmaf_score = 0.0
vmaf_scores.append(vmaf_score)
logger.info(f"🎾 VMAF score is {vmaf_score} , Threshold: {vmaf_threshold}, Diff: {vmaf_score - vmaf_threshold}")
except Exception as e:
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append("failed to calculate VMAF score due to video dimension mismatch")
logger.error(f"Error calculating VMAF score: {e}")
if dist_y4m_path and os.path.exists(dist_y4m_path):
os.unlink(dist_y4m_path)
continue
# Now reuse the Y4M files for color validation (no redundant conversion!)
# Extract frames for color validation from Y4M files
logger.info(f"Extracting frames from Y4M for color validation (reusing VMAF Y4M files)")
dist_frames = extract_frames_from_y4m(dist_y4m_path)
step_time = time.time() - uid_start_time
logger.info(f"♎️ 8.5. Extracted {len(dist_frames)} frames from Y4M for color validation in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Validate color channels (grayscale check)
color_valid, color_reason = validate_color_channels_on_frames(dist_frames)
if not color_valid:
logger.error(f"UID {uid}: {color_reason}")
vmaf_scores.append(0.0)
compression_rates.append(0.9999)
final_scores.append(0.0)
reasons.append(f"Color validation failed: {color_reason}")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
if dist_y4m_path and os.path.exists(dist_y4m_path):
os.unlink(dist_y4m_path)
continue
logger.info(f"✅ UID {uid}: Color channels validated - {color_reason}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 8.6. Validated color channels in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Extract reference frames from Y4M for chroma quality comparison
ref_frames = extract_frames_from_y4m(ref_y4m_path)
# Validate chroma quality (prevents partial UV reduction)
chroma_valid, chroma_reason = validate_chroma_quality_on_frames(
ref_frames, dist_frames, threshold=0.7
)
if not chroma_valid:
logger.error(f"UID {uid}: {chroma_reason}")
vmaf_scores.append(0.0)
compression_rates.append(0.9999)
final_scores.append(0.0)
reasons.append(f"Chroma validation failed: {chroma_reason}")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
if dist_y4m_path and os.path.exists(dist_y4m_path):
os.unlink(dist_y4m_path)
continue
logger.info(f"✅ UID {uid}: Chroma quality validated - {chroma_reason}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 8.7. Validated chroma quality in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Calculate compression score using the proper formula
# Check scoring function for details
final_score, compression_component, quality_component, reason = calculate_compression_score(
vmaf_score=vmaf_score,
compression_rate=compression_rate,
vmaf_threshold=vmaf_threshold,
compression_weight=COMPRESSION_RATE_WEIGHT,
quality_weight=COMPRESSION_VMAF_WEIGHT,
soft_threshold_margin=SOFT_THRESHOLD_MARGIN
)
# quality component:
logger.info(f"VMAF quality component: {quality_component:.4f}")
# compression component
logger.info(f"🎯 Compression score is {compression_component:.4f}")
# final score
logger.info(f"🎯 Final score is {final_score:.4f}")
compression_rates.append(compression_rate)
final_scores.append(final_score)
reasons.append(reason)
step_time = time.time() - uid_start_time
logger.info(f"🛑 Processed one UID in {step_time:.2f} seconds.")
except Exception as e:
error_msg = f"Failed to process video from {dist_url}: {str(e)}"
logger.error(f"{error_msg}. Assigning score of 0.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append("failed to process video")
finally:
# Clean up resources for this pair
# ref_cap is not used in this endpoint (using ffprobe instead)
if ref_y4m_path and os.path.exists(ref_y4m_path):
os.unlink(ref_y4m_path)
if dist_y4m_path and os.path.exists(dist_y4m_path):
os.unlink(dist_y4m_path)
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
# Delete the uploaded object
storage_client.delete_file(uploaded_object_name)
for ref_path in request.reference_paths:
if os.path.exists(ref_path):
os.unlink(ref_path)
# tmp_directory = "/tmp"
# try:
# logger.info("🧹 Cleaning up temporary files in /tmp...")
# for file_path in glob.glob(os.path.join(tmp_directory, "*.mp4")):
# os.remove(file_path)
# logger.info(f"Deleted: {file_path}")
# for file_path in glob.glob(os.path.join(tmp_directory, "*.y4m")):
# os.remove(file_path)
# logger.info(f"Deleted: {file_path}")
# except Exception as e:
# logger.error(f"⚠️ Error during cleanup: {e}")
processed_time = time.time() - start_time
logger.info(f"Completed batch scoring of {len(request.distorted_urls)} pairs within {processed_time:.2f} seconds")
logger.info(f"🍉🍉🍉 Calculated final scores: {final_scores} 🍉🍉🍉")
return CompressionScoringResponse(
vmaf_scores=vmaf_scores,
compression_rates=compression_rates,
final_scores=final_scores,
reasons=reasons
)
@app.post("/score_organics_upscaling")
async def score_organics_upscaling(request: OrganicsUpscalingScoringRequest) -> OrganicsUpscalingScoringResponse:
logger.info("#################### 🤖 start scoring ####################")
batch_start_time = time.time()
vmaf_scores = []
pieapp_scores = []
quality_scores = []
length_scores = []
final_scores = []
reasons = []
distorted_video_paths = []
for dist_url in request.distorted_urls:
if len(dist_url) < 10:
distorted_video_paths.append(None)
continue
try:
path, download_time = await download_video(dist_url, request.verbose)
distorted_video_paths.append(path)
except Exception as e:
logger.error(f"failed to download distorted video: {dist_url}, error: {e}")
distorted_video_paths.append(None)
for idx, (ref_url, dist_path, uid, task_type) in enumerate(
zip(request.reference_urls, distorted_video_paths, request.uids, request.task_types)
):
logger.info(f"🧩 processing {uid}.... downloading reference video.... 🧩")
ref_path = None
ref_upscaled_y4m_path = None
ref_clip_path = None
dist_clip_path = None
ref_upscaled_clip_path = None
scale_factor = 2
if task_type == "SD24K":
scale_factor = 4
logger.info(f"scale factor: {scale_factor}")
# === REFERENCE VIDEO VALIDATION (NOT MINER'S FAULT) ===
try:
# download reference video
if len(ref_url) < 10:
logger.info(f"invalid reference download url: {ref_url}. skipping scoring")
reasons.append("invalid reference url - skipped")
vmaf_scores.append(-1)
pieapp_scores.append(-1)
quality_scores.append(-1)
length_scores.append(-1)
final_scores.append(-1) # -1 means skip, don't penalize
continue
ref_path, download_time = await download_video(ref_url, request.verbose)
# Validate reference video using ffprobe (avoids AV1 warnings)
if not is_valid_video(ref_path):
file_exists = os.path.exists(ref_path)
file_size = os.path.getsize(ref_path) if file_exists else 0
logger.error(f"error opening reference video from {ref_url}. skipping scoring")
logger.info(f" File path: {ref_path}")
logger.info(f" File exists: {file_exists}")
logger.info(f" File size: {file_size} bytes")
reasons.append("corrupted reference video - skipped")
vmaf_scores.append(-1)
pieapp_scores.append(-1)
quality_scores.append(-1)
length_scores.append(-1)
final_scores.append(-1) # -1 means skip, don't penalize
continue
# Get video metadata using ffprobe (no AV1 warnings)
ref_total_frames = get_frame_count(ref_path)
ref_fps = get_video_fps(ref_path)
ref_width, ref_height = get_video_dimensions(ref_path)
if ref_fps is None or ref_width is None or ref_height is None:
logger.error(f"failed to get reference video metadata. skipping scoring")
reasons.append("failed to read reference video metadata - skipped")
vmaf_scores.append(-1)
pieapp_scores.append(-1)
quality_scores.append(-1)
length_scores.append(-1)
final_scores.append(-1)
continue
if ref_total_frames < 10:
logger.info(f"reference video too short (<10 frames). skipping scoring")
reasons.append("reference video too short - skipped")
vmaf_scores.append(-1)
pieapp_scores.append(-1)
quality_scores.append(-1)
length_scores.append(-1)
final_scores.append(-1) # -1 means skip, don't penalize
continue
except Exception as e:
logger.error(f"system error processing reference video: {e}. skipping scoring")
reasons.append("system error with reference - skipped")
vmaf_scores.append(-1)
pieapp_scores.append(-1)
quality_scores.append(-1)
length_scores.append(-1)
final_scores.append(-1) # -1 means skip, don't penalize
continue
# === MINER OUTPUT VALIDATION (MINER'S FAULT) ===
try:
# check if distorted video failed to download
if dist_path is None:
logger.error(f"failed to download distorted video for uid {uid}. penalizing miner.")
reasons.append("MINER FAILURE: failed to download distorted video, invalid url")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0) # 0 = miner penalty
continue
# Check if miner's output can be opened (using ffprobe to avoid AV1 warnings)
if not is_valid_video(dist_path):
logger.error(f"error opening distorted video from {dist_path}. penalizing miner.")
reasons.append("MINER FAILURE: corrupted output video")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0) # 0 = miner penalty
continue
is_valid_encoding, encoding_msg = validate_dist_encoding_settings(dist_path, ref_path, task="upscaling")
if not is_valid_encoding:
logger.error(f"Invalid encoding settings for distorted video {dist_path}: {encoding_msg}")
logger.info(f" Required: AV1 codec, Main profile, yuv420p, MP4 container, 1:1 SAR")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append(f"invalid encoding settings: {encoding_msg}")
continue
# Get frame count and dimensions using ffprobe (no AV1 warnings)
dist_total_frames = get_frame_count(dist_path)
dist_width, dist_height = get_video_dimensions(dist_path)
if dist_width is None or dist_height is None:
logger.error(f"failed to get video dimensions for {dist_path}. penalizing miner.")
reasons.append("MINER FAILURE: cannot read video dimensions")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
continue
logger.info(f"distorted video has {dist_total_frames} frames.")
# Check frame count match
if dist_total_frames != ref_total_frames:
logger.info(f"video length mismatch: ref({ref_total_frames}) != dist({dist_total_frames}). penalizing miner.")
reasons.append("MINER FAILURE: video length mismatch")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0) # 0 = miner penalty
continue
# Check minimum frame count for miner output
if dist_total_frames < 10:
logger.info(f"miner output too short (<10 frames). penalizing miner.")
reasons.append("MINER FAILURE: output video too short")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0) # 0 = miner penalty
continue
# === RESOLUTION CHECK ===
expected_width = ref_width * scale_factor
expected_height = ref_height * scale_factor
if dist_width != expected_width or dist_height != expected_height:
logger.info(f"resolution mismatch: expected {expected_width}x{expected_height}, got {dist_width}x{dist_height}. penalizing miner.")
reasons.append("MINER FAILURE: incorrect upscaling resolution")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0) # 0 = miner penalty
continue
except Exception as e:
logger.error(f"error validating miner output: {e}. penalizing miner.")
reasons.append(f"MINER FAILURE: {e}")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0) # 0 = miner penalty
continue
# === MAIN SCORING LOGIC ===
try:
# Calculate 0.5 second clip duration
clip_duration = 0.25 # seconds
# Calculate maximum start time to ensure we don't exceed video length
ref_duration = ref_total_frames / ref_fps
max_start_time = max(0, ref_duration - clip_duration)
start_time = random.uniform(0, max_start_time)
logger.info(f"Creating 0.5-second clips starting from {start_time:.2f} seconds")
# Create 0.5-second reference clip
ref_clip_path = trim_video(ref_path, start_time, clip_duration)
logger.info(f"Created reference clip: {ref_clip_path}")
# Create 0.5-second distorted clip
dist_clip_path = trim_video(dist_path, start_time, clip_duration)
logger.info(f"Created distorted clip: {dist_clip_path}")
# Upscale the reference clip
ref_upscaled_clip_path = upscale_video(ref_clip_path, scale_factor)
logger.info(f"Created upscaled reference clip: {ref_upscaled_clip_path}")
# Calculate ClipIQ scores
logger.info("Calculating ClipIQ scores...")
ref_clipiqa_score = get_clipiqa_score(ref_clip_path, num_frames=3)
dist_clipiqa_score = get_clipiqa_score(dist_clip_path, num_frames=3)
ref_upscaled_clipiqa_score = get_clipiqa_score(ref_upscaled_clip_path, num_frames=3)
logger.info(f"Reference ClipIQ score: {ref_clipiqa_score:.4f}")
logger.info(f"Distorted ClipIQ score: {dist_clipiqa_score:.4f}")
logger.info(f"Upscaled reference ClipIQ score: {ref_upscaled_clipiqa_score:.4f}")
# Create Y4M files for VMAF calculation
logger.info("Creating Y4M files for VMAF calculation...")
# Get the number of frames in the clips for Y4M conversion using ffprobe
ref_clip_frames = get_frame_count(ref_upscaled_clip_path)
ref_y4m_path = convert_mp4_to_y4m(ref_upscaled_clip_path, list(range(ref_clip_frames)))
# Calculate VMAF score
logger.info("Calculating VMAF score...")
vmaf_score = calculate_vmaf(ref_y4m_path, dist_clip_path, list(range(ref_clip_frames)), neg_model=False)
if vmaf_score is None:
vmaf_score = 0.0
logger.info(f"VMAF score: {vmaf_score:.4f}")
# Check VMAF threshold
if vmaf_score < 50:
logger.info(f"VMAF score below threshold (50): {vmaf_score}. Assigning score 0.")
vmaf_scores.append(vmaf_score)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0)
reasons.append(f"VMAF score too low: {vmaf_score}")
continue
# Final scoring based on ClipIQ scores
if dist_clipiqa_score > ref_clipiqa_score:
final_score = 3
reason = "upscaled better than reference"
elif dist_clipiqa_score > (ref_upscaled_clipiqa_score * 1.05):
final_score = 2
reason = "upscaled at least 5% better than ffmpeg upscaled"
elif dist_clipiqa_score > (ref_upscaled_clipiqa_score):
final_score = 1
reason = "Better than ffmpeg upscaled"
else:
final_score = 0
reason = "Worse than ffmpeg upscaled"
logger.info(f"Final score: {final_score} ({reason})")
vmaf_scores.append(vmaf_score)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(final_score)
reasons.append(f"{reason}")
except Exception as e:
logger.error(f"error in main scoring logic: {e}. penalizing miner.")
reasons.append(f"MINER FAILURE: scoring error - {e}")
vmaf_scores.append(0.0)
pieapp_scores.append(0.0)
quality_scores.append(0.0)
length_scores.append(0.0)
final_scores.append(0.0) # 0 = miner penalty
finally:
# Clean up resources
if ref_path and os.path.exists(ref_path):
os.unlink(ref_path)
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
if ref_upscaled_y4m_path and os.path.exists(ref_upscaled_y4m_path):
os.unlink(ref_upscaled_y4m_path)
if ref_clip_path and os.path.exists(ref_clip_path):
os.unlink(ref_clip_path)
if dist_clip_path and os.path.exists(dist_clip_path):
os.unlink(dist_clip_path)
if ref_upscaled_clip_path and os.path.exists(ref_upscaled_clip_path):
os.unlink(ref_upscaled_clip_path)
processed_time = time.time() - batch_start_time
logger.info(f"🍉🍉🍉 calculated scores: {final_scores} 🍉🍉🍉")
logger.info(f"completed one batch scoring within {processed_time:.2f} seconds")
# Summary statistics
successful_miners = sum(1 for score in final_scores if score >= 1)
failed_miners = sum(1 for score in final_scores if score == 0.0)
skipped_miners = sum(1 for score in final_scores if score == -1)
logger.info(f"📊 SCORING SUMMARY:")
logger.info(f" ✅ Successful miners: {successful_miners}")
logger.info(f" ❌ Failed miners: {failed_miners}")
logger.info(f" ⏭️ Skipped miners: {skipped_miners}")
return OrganicsUpscalingScoringResponse(
vmaf_scores=vmaf_scores,
pieapp_scores=pieapp_scores,
quality_scores=quality_scores,
length_scores=length_scores,
final_scores=final_scores,
reasons=reasons
)
@app.post("/score_organics_compression")
async def score_organics_compression(request: OrganicsCompressionScoringRequest) -> OrganicsCompressionScoringResponse:
logger.info("#################### 🤖 start scoring ####################")
start_time = time.time()
vmaf_scores = []
compression_rates = []
final_scores = []
reasons = []
distorted_video_paths = []
for dist_url in request.distorted_urls:
if len(dist_url) < 10:
distorted_video_paths.append(None)
continue
try:
path, download_time = await download_video(dist_url, request.verbose)
distorted_video_paths.append(path)
except Exception as e:
logger.error(f"failed to download distorted video: {dist_url}, error: {e}")
distorted_video_paths.append(None)
for idx, (ref_url, dist_path, uid, vmaf_threshold) in enumerate(
zip(request.reference_urls, distorted_video_paths, request.uids, request.vmaf_thresholds)
):
logger.info(f"🧩 processing {uid}.... downloading reference video.... 🧩")
ref_path = None
ref_y4m_path = None
dist_y4m_path = None
ref_clip_path = None
dist_clip_path = None
uid_start_time = time.time() # start time for each uid
# === REFERENCE VIDEO VALIDATION (NOT MINER'S FAULT) ===
try:
# download reference video
if len(ref_url) < 10:
logger.info(f"invalid reference download url: {ref_url}. skipping scoring")
vmaf_scores.append(-1)
compression_rates.append(-1)
reasons.append("invalid reference url - skipped")
final_scores.append(-1) # -1 means skip, don't penalize
continue
ref_path, download_time = await download_video(ref_url, request.verbose)
# Validate reference video using ffprobe (avoids AV1 warnings)
if not is_valid_video(ref_path):
file_exists = os.path.exists(ref_path)
file_size = os.path.getsize(ref_path) if file_exists else 0
logger.error(f"error opening reference video from {ref_url}. skipping scoring")
logger.info(f" File path: {ref_path}")
logger.info(f" File exists: {file_exists}")
logger.info(f" File size: {file_size} bytes")
vmaf_scores.append(-1)
compression_rates.append(-1)
reasons.append("corrupted reference video - skipped")
final_scores.append(-1) # -1 means skip, don't penalize
continue
ref_total_frames = get_frame_count(ref_path)
# Get reference video file size for compression rate calculation
ref_file_size = os.path.getsize(ref_path)
logger.info(f"Reference video file size: {ref_file_size} bytes")
if ref_total_frames < 10:
logger.info(f"reference video too short (<10 frames). skipping scoring")
vmaf_scores.append(-1)
compression_rates.append(-1)
reasons.append("reference video too short - skipped")
final_scores.append(-1) # -1 means skip, don't penalize
continue
except Exception as e:
logger.error(f"system error processing reference video: {e}. skipping scoring")
vmaf_scores.append(-1)
compression_rates.append(-1)
reasons.append("system error with reference - skipped")
final_scores.append(-1) # -1 means skip, don't penalize
continue
# === MINER OUTPUT VALIDATION (MINER'S FAULT) ===
try:
# check if distorted video failed to download
if dist_path is None:
logger.error(f"failed to download distorted video for uid {uid}. penalizing miner.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999)
reasons.append("MINER FAILURE: failed to download distorted video, invalid url")
final_scores.append(0.0) # 0 = miner penalty
continue
# Check if miner's output can be opened (using ffprobe to avoid AV1 warnings)
if not is_valid_video(dist_path):
logger.error(f"error opening distorted video from {dist_path}. penalizing miner.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999)
reasons.append("MINER FAILURE: corrupted output video")
final_scores.append(0.0) # 0 = miner penalty
continue
is_valid_encoding, encoding_msg = validate_dist_encoding_settings(dist_path, ref_path, task="compression")
if not is_valid_encoding:
logger.error(f"Invalid encoding settings for distorted video {dist_path}: {encoding_msg}")
logger.info(f" Required: AV1 codec, Main profile, yuv420p, MP4 container, 1:1 SAR")
vmaf_scores.append(0.0)
compression_rates.append(0.9999)
final_scores.append(0.0)
reasons.append(f"invalid encoding settings: {encoding_msg}")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
continue
dist_total_frames = get_frame_count(dist_path)
logger.info(f"Distorted video has {dist_total_frames} frames.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 5. Retrieved distorted video frame count in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if dist_total_frames != ref_total_frames:
logger.error(
f"Video length mismatch for pair {idx+1}: ref({ref_total_frames}) != dist({dist_total_frames}). Assigning score of 0."
)
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append("video length mismatch")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
continue
# Get distorted video file size for compression rate calculation
dist_file_size = os.path.getsize(dist_path)
logger.info(f"Distorted video file size: {dist_file_size} bytes")
# Calculate compression rate BEFORE VMAF calculation
if ref_file_size > 0 and dist_file_size < ref_file_size:
compression_rate = dist_file_size / ref_file_size
logger.info(f"Compression rate: {compression_rate:.4f} ({dist_file_size}/{ref_file_size})")
else:
compression_rate = 1.0
logger.info("Reference file size is 0 or distorted file size >= reference, setting compression rate to 1.0")
# Calculate 0.5 second clip duration for VMAF calculation
clip_duration = 0.5 # seconds
# Get video FPS to calculate frame-based timing using ffprobe (no AV1 warnings)
ref_fps = get_video_fps(ref_path)
logger.info(f"Reference video FPS: {ref_fps}")
# Calculate maximum start time to ensure we don't exceed video length
ref_duration = ref_total_frames / ref_fps
max_start_time = max(0, ref_duration - clip_duration)
start_time = random.uniform(0, max_start_time)
logger.info(f"Creating 0.5-second clips starting from {start_time:.2f} seconds")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 6. Selected random start time for video chunks in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Create 0.5-second reference clip
ref_clip_path = trim_video(ref_path, start_time, clip_duration)
logger.info(f"Created reference clip: {ref_clip_path}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 7. Created reference video clip in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Create 0.5-second distorted clip
dist_clip_path = trim_video(dist_path, start_time, clip_duration)
logger.info(f"Created distorted clip: {dist_clip_path}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 8. Created distorted video clip in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Get the number of frames in the clips for Y4M conversion
ref_clip_frames = get_frame_count(ref_clip_path)
logger.info(f"Reference clip has {ref_clip_frames} frames.")
# Ensure we don't sample more frames than available
sample_count = min(VMAF_SAMPLE_COUNT, ref_clip_frames)
random_frames = sorted(random.sample(range(ref_clip_frames), sample_count))
ref_y4m_path = convert_mp4_to_y4m(ref_clip_path, random_frames)
logger.info("The reference video clip has been successfully converted to Y4M format.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 9. Converted reference video clip to Y4M in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Calculate VMAF on chunked videos and get Y4M paths for validation
dist_y4m_path = None
try:
vmaf_start = time.time()
vmaf_score, dist_y4m_path = calculate_vmaf(ref_y4m_path, dist_clip_path, random_frames, neg_model=True, return_y4m_path=True)
vmaf_calc_time = time.time() - vmaf_start
logger.info(f"☣️☣️ VMAF calculation took {vmaf_calc_time:.2f} seconds.")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 10. Completed VMAF calculation in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
if vmaf_score is not None:
vmaf_scores.append(vmaf_score)
else:
vmaf_score = 0.0
vmaf_scores.append(vmaf_score)
logger.info(f"🎾 VMAF score is {vmaf_score}")
except Exception as e:
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append("failed to calculate VMAF score due to video dimension mismatch")
logger.error(f"Error calculating VMAF score: {e}")
if dist_y4m_path and os.path.exists(dist_y4m_path):
os.unlink(dist_y4m_path)
continue
# Now reuse the Y4M files for color validation (no redundant conversion!)
# Extract frames for color validation from Y4M files
logger.info(f"Extracting frames from Y4M for color validation from clip (reusing VMAF Y4M files)")
dist_clip_frames = extract_frames_from_y4m(dist_y4m_path)
step_time = time.time() - uid_start_time
logger.info(f"♎️ 10.5. Extracted {len(dist_clip_frames)} frames from Y4M for color validation in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Validate color channels (grayscale check)
color_valid, color_reason = validate_color_channels_on_frames(dist_clip_frames)
if not color_valid:
logger.error(f"UID {uid}: {color_reason}")
vmaf_scores.append(0.0)
compression_rates.append(0.9999)
final_scores.append(0.0)
reasons.append(f"Color validation failed: {color_reason}")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
if ref_clip_path and os.path.exists(ref_clip_path):
os.unlink(ref_clip_path)
if dist_clip_path and os.path.exists(dist_clip_path):
os.unlink(dist_clip_path)
if dist_y4m_path and os.path.exists(dist_y4m_path):
os.unlink(dist_y4m_path)
continue
logger.info(f"✅ UID {uid}: Color channels validated - {color_reason}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 10.6. Validated color channels in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Extract reference frames from Y4M for chroma quality comparison
ref_clip_frames_data = extract_frames_from_y4m(ref_y4m_path)
# Validate chroma quality (prevents partial UV reduction)
chroma_valid, chroma_reason = validate_chroma_quality_on_frames(
ref_clip_frames_data, dist_clip_frames, threshold=0.7
)
if not chroma_valid:
logger.error(f"UID {uid}: {chroma_reason}")
vmaf_scores.append(0.0)
compression_rates.append(0.9999)
final_scores.append(0.0)
reasons.append(f"Chroma validation failed: {chroma_reason}")
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
if ref_clip_path and os.path.exists(ref_clip_path):
os.unlink(ref_clip_path)
if dist_clip_path and os.path.exists(dist_clip_path):
os.unlink(dist_clip_path)
if dist_y4m_path and os.path.exists(dist_y4m_path):
os.unlink(dist_y4m_path)
continue
logger.info(f"✅ UID {uid}: Chroma quality validated - {chroma_reason}")
step_time = time.time() - uid_start_time
logger.info(f"♎️ 10.7. Validated chroma quality in {step_time:.2f} seconds. Total time: {step_time:.2f} seconds.")
# Calculate compression score using the proper formula
# Check scoring function for details
final_score, compression_component, quality_component, reason = calculate_compression_score(
vmaf_score=vmaf_score,
compression_rate=compression_rate,
vmaf_threshold=vmaf_threshold,
compression_weight=COMPRESSION_RATE_WEIGHT,
quality_weight=COMPRESSION_VMAF_WEIGHT,
soft_threshold_margin=SOFT_THRESHOLD_MARGIN
)
# quality component:
logger.info(f"VMAF quality component: {quality_component:.4f}")
# compression component
logger.info(f"🎯 Compression score is {compression_component:.4f}")
# final score
logger.info(f"🎯 Final score is {final_score:.4f}")
# vmaf_scores.append(vmaf_score)
compression_rates.append(compression_rate)
final_scores.append(final_score)
reasons.append(reason)
step_time = time.time() - uid_start_time
logger.info(f"🛑 Processed one UID in {step_time:.2f} seconds.")
except Exception as e:
error_msg = f"Failed to process video from {dist_url}: {str(e)}"
logger.error(f"{error_msg}. Assigning score of 0.")
vmaf_scores.append(0.0)
compression_rates.append(0.9999) # No compression achieved
final_scores.append(0.0)
reasons.append("failed to process video")
finally:
# Clean up resources for this pair
# ref_cap is not used in this endpoint (using ffprobe instead)
# dist_cap is not used in this endpoint (using ffprobe instead)
if ref_path and os.path.exists(ref_path):
os.unlink(ref_path)
if dist_path and os.path.exists(dist_path):
os.unlink(dist_path)
if ref_y4m_path and os.path.exists(ref_y4m_path):
os.unlink(ref_y4m_path)
if dist_y4m_path and os.path.exists(dist_y4m_path):
os.unlink(dist_y4m_path)
# Clean up chunked video files
if ref_clip_path and os.path.exists(ref_clip_path):
os.unlink(ref_clip_path)
if dist_clip_path and os.path.exists(dist_clip_path):
os.unlink(dist_clip_path)
return OrganicsCompressionScoringResponse(
vmaf_scores=vmaf_scores,
compression_rates=compression_rates,
final_scores=final_scores,
reasons=reasons
)
if __name__ == "__main__":
import uvicorn
host = CONFIG.score.host
port = CONFIG.score.port
uvicorn.run(app, host=host, port=port)