Compare commits
No commits in common. "main" and "master" have entirely different histories.
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
nohup.out
|
||||||
|
nohup_gpu*.out
|
||||||
|
py3/
|
||||||
|
*.egg-info/
|
||||||
|
*.pid
|
||||||
8
ah.py
Normal file
8
ah.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
from ahserver.webapp import webapp
|
||||||
|
|
||||||
|
def init():
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
webapp(init)
|
||||||
302
app/api/eval/index.dspy
Normal file
302
app/api/eval/index.dspy
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
# -*- coding:utf-8 -*-
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
from ahserver.filestorage import FileStorage
|
||||||
|
|
||||||
|
try:
|
||||||
|
fs = FileStorage()
|
||||||
|
|
||||||
|
# Get uploaded file - params_kw gives web path for uploads
|
||||||
|
web_path = None
|
||||||
|
file_path = None
|
||||||
|
|
||||||
|
if 'video' in params_kw:
|
||||||
|
web_path = params_kw['video']
|
||||||
|
elif 'file' in params_kw:
|
||||||
|
web_path = params_kw['file']
|
||||||
|
|
||||||
|
if not web_path:
|
||||||
|
return json.dumps({
|
||||||
|
"error": "No video file uploaded. Use multipart/form-data with field 'video' or 'file'.",
|
||||||
|
"score": 0,
|
||||||
|
"passed": False,
|
||||||
|
"details": {},
|
||||||
|
"suggestions": ["Upload a video file (mp4) via POST multipart/form-data"]
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
|
if isinstance(web_path, str) and len(web_path) > 0:
|
||||||
|
file_path = fs.realPath(web_path)
|
||||||
|
|
||||||
|
if not file_path or not os.path.exists(file_path):
|
||||||
|
return json.dumps({
|
||||||
|
"error": "Uploaded file not found",
|
||||||
|
"web_path": str(web_path),
|
||||||
|
"resolved_path": str(file_path),
|
||||||
|
"score": 0,
|
||||||
|
"passed": False,
|
||||||
|
"details": {},
|
||||||
|
"suggestions": []
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
|
score = 100
|
||||||
|
details = {}
|
||||||
|
suggestions = []
|
||||||
|
duration = 0
|
||||||
|
|
||||||
|
# --- ffprobe: get overall info ---
|
||||||
|
probe_data = None
|
||||||
|
try:
|
||||||
|
probe_cmd = [
|
||||||
|
"ffprobe", "-v", "quiet", "-print_format", "json",
|
||||||
|
"-show_format", "-show_streams", file_path
|
||||||
|
]
|
||||||
|
probe_out = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=60)
|
||||||
|
probe_data = json.loads(probe_out.stdout)
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({
|
||||||
|
"error": "ffprobe failed: " + str(e),
|
||||||
|
"score": 0,
|
||||||
|
"passed": False,
|
||||||
|
"details": {},
|
||||||
|
"suggestions": []
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
|
fmt = probe_data.get("format", {})
|
||||||
|
streams = probe_data.get("streams", [])
|
||||||
|
|
||||||
|
video_stream = None
|
||||||
|
audio_stream = None
|
||||||
|
for s in streams:
|
||||||
|
if s.get("codec_type") == "video" and video_stream is None:
|
||||||
|
video_stream = s
|
||||||
|
elif s.get("codec_type") == "audio" and audio_stream is None:
|
||||||
|
audio_stream = s
|
||||||
|
|
||||||
|
# 1) Duration
|
||||||
|
duration = float(fmt.get("duration", 0))
|
||||||
|
details["duration_seconds"] = round(duration, 2)
|
||||||
|
if duration < 1:
|
||||||
|
score -= 20
|
||||||
|
suggestions.append("Video is extremely short (less than 1 second)")
|
||||||
|
elif duration < 5:
|
||||||
|
score -= 5
|
||||||
|
suggestions.append("Video is very short (less than 5 seconds)")
|
||||||
|
|
||||||
|
# 2) Resolution and aspect ratio
|
||||||
|
if video_stream:
|
||||||
|
width = int(video_stream.get("width", 0))
|
||||||
|
height = int(video_stream.get("height", 0))
|
||||||
|
details["resolution"] = str(width) + "x" + str(height)
|
||||||
|
|
||||||
|
if width > 0 and height > 0:
|
||||||
|
from math import gcd
|
||||||
|
g = gcd(width, height)
|
||||||
|
ar_w = width // g
|
||||||
|
ar_h = height // g
|
||||||
|
details["aspect_ratio"] = str(ar_w) + ":" + str(ar_h)
|
||||||
|
|
||||||
|
pixels = width * height
|
||||||
|
if pixels >= 3840 * 2160:
|
||||||
|
details["quality_tier"] = "4K UHD"
|
||||||
|
elif pixels >= 1920 * 1080:
|
||||||
|
details["quality_tier"] = "Full HD"
|
||||||
|
elif pixels >= 1280 * 720:
|
||||||
|
details["quality_tier"] = "HD"
|
||||||
|
elif pixels >= 640 * 480:
|
||||||
|
details["quality_tier"] = "SD"
|
||||||
|
score -= 10
|
||||||
|
suggestions.append("Consider upgrading to at least HD (1280x720) resolution")
|
||||||
|
else:
|
||||||
|
details["quality_tier"] = "Low"
|
||||||
|
score -= 20
|
||||||
|
suggestions.append("Resolution is very low. Use at least 640x480 for acceptable quality")
|
||||||
|
else:
|
||||||
|
score -= 15
|
||||||
|
suggestions.append("Could not determine video resolution")
|
||||||
|
else:
|
||||||
|
score -= 50
|
||||||
|
suggestions.append("No video stream found in the file")
|
||||||
|
|
||||||
|
# 3) Bitrate and codec
|
||||||
|
if video_stream:
|
||||||
|
codec_name = video_stream.get("codec_name", "unknown")
|
||||||
|
details["video_codec"] = codec_name
|
||||||
|
|
||||||
|
v_bitrate = video_stream.get("bit_rate")
|
||||||
|
if v_bitrate:
|
||||||
|
v_bitrate = int(v_bitrate)
|
||||||
|
else:
|
||||||
|
fmt_bitrate = fmt.get("bit_rate")
|
||||||
|
if fmt_bitrate:
|
||||||
|
v_bitrate = int(fmt_bitrate)
|
||||||
|
|
||||||
|
if v_bitrate:
|
||||||
|
v_bitrate_kbps = v_bitrate // 1000
|
||||||
|
details["video_bitrate_kbps"] = v_bitrate_kbps
|
||||||
|
|
||||||
|
w = int(video_stream.get("width", 0))
|
||||||
|
h = int(video_stream.get("height", 0))
|
||||||
|
pixels = w * h
|
||||||
|
if pixels > 0:
|
||||||
|
bpp = v_bitrate / (pixels * 30)
|
||||||
|
details["bits_per_pixel_per_frame"] = round(bpp, 3)
|
||||||
|
if bpp < 0.01:
|
||||||
|
score -= 15
|
||||||
|
suggestions.append("Bitrate is very low for the resolution - expect compression artifacts")
|
||||||
|
elif bpp < 0.03:
|
||||||
|
score -= 5
|
||||||
|
suggestions.append("Bitrate is somewhat low - consider increasing for better quality")
|
||||||
|
|
||||||
|
if codec_name not in ["h264", "hevc", "h265", "av1", "vp9"]:
|
||||||
|
score -= 5
|
||||||
|
suggestions.append("Video codec '" + codec_name + "' is not modern. Consider H.264 or H.265")
|
||||||
|
|
||||||
|
# Audio info
|
||||||
|
if audio_stream:
|
||||||
|
a_codec = audio_stream.get("codec_name", "unknown")
|
||||||
|
a_sample_rate = audio_stream.get("sample_rate", "unknown")
|
||||||
|
a_channels = audio_stream.get("channels", 0)
|
||||||
|
details["audio_codec"] = a_codec
|
||||||
|
details["audio_sample_rate"] = a_sample_rate
|
||||||
|
details["audio_channels"] = a_channels
|
||||||
|
|
||||||
|
a_bitrate = audio_stream.get("bit_rate")
|
||||||
|
if a_bitrate:
|
||||||
|
details["audio_bitrate_kbps"] = int(a_bitrate) // 1000
|
||||||
|
|
||||||
|
if a_codec not in ["aac", "opus", "mp3", "flac", "vorbis", "ac3", "eac3"]:
|
||||||
|
score -= 5
|
||||||
|
suggestions.append("Audio codec '" + a_codec + "' may have compatibility issues")
|
||||||
|
else:
|
||||||
|
details["audio_codec"] = "none"
|
||||||
|
suggestions.append("No audio stream detected")
|
||||||
|
|
||||||
|
# 4) Frame rate
|
||||||
|
if video_stream:
|
||||||
|
fps_str = video_stream.get("r_frame_rate", "0/1")
|
||||||
|
try:
|
||||||
|
num, den = fps_str.split("/")
|
||||||
|
fps = float(num) / float(den) if float(den) != 0 else 0
|
||||||
|
except Exception:
|
||||||
|
fps = 0
|
||||||
|
details["frame_rate_fps"] = round(fps, 2)
|
||||||
|
|
||||||
|
avg_fps_str = video_stream.get("avg_frame_rate", "0/1")
|
||||||
|
try:
|
||||||
|
num2, den2 = avg_fps_str.split("/")
|
||||||
|
avg_fps = float(num2) / float(den2) if float(den2) != 0 else 0
|
||||||
|
except Exception:
|
||||||
|
avg_fps = fps
|
||||||
|
|
||||||
|
details["avg_frame_rate_fps"] = round(avg_fps, 2)
|
||||||
|
|
||||||
|
if fps > 0 and avg_fps > 0:
|
||||||
|
fps_diff = abs(fps - avg_fps) / fps
|
||||||
|
details["frame_rate_consistency"] = round(1.0 - fps_diff, 4)
|
||||||
|
if fps_diff > 0.1:
|
||||||
|
score -= 10
|
||||||
|
suggestions.append("Frame rate is inconsistent - may cause playback stuttering")
|
||||||
|
elif fps_diff > 0.05:
|
||||||
|
score -= 3
|
||||||
|
else:
|
||||||
|
details["frame_rate_consistency"] = "unknown"
|
||||||
|
|
||||||
|
if fps < 20:
|
||||||
|
score -= 10
|
||||||
|
suggestions.append("Frame rate is below 20fps - video may appear choppy")
|
||||||
|
|
||||||
|
# 5) Audio/video sync check
|
||||||
|
if video_stream and audio_stream:
|
||||||
|
v_start = float(video_stream.get("start_time", 0))
|
||||||
|
a_start = float(audio_stream.get("start_time", 0))
|
||||||
|
sync_diff = abs(v_start - a_start)
|
||||||
|
details["av_sync_offset_seconds"] = round(sync_diff, 4)
|
||||||
|
|
||||||
|
if sync_diff > 0.1:
|
||||||
|
score -= 15
|
||||||
|
suggestions.append("Audio/video sync offset: " + str(round(sync_diff, 3)) + "s. May cause lip-sync issues")
|
||||||
|
elif sync_diff > 0.05:
|
||||||
|
score -= 5
|
||||||
|
details["av_sync_status"] = "minor offset"
|
||||||
|
else:
|
||||||
|
details["av_sync_status"] = "good"
|
||||||
|
else:
|
||||||
|
details["av_sync_status"] = "N/A (missing audio or video stream)"
|
||||||
|
|
||||||
|
# 6) Scene change detection using ffmpeg
|
||||||
|
try:
|
||||||
|
scene_cmd = [
|
||||||
|
"ffmpeg", "-i", file_path,
|
||||||
|
"-vf", "select=gt(scene\\,0.3),showinfo",
|
||||||
|
"-vsync", "vfr", "-f", "null", "-"
|
||||||
|
]
|
||||||
|
scene_out = subprocess.run(scene_cmd, capture_output=True, text=True, timeout=120)
|
||||||
|
scene_lines = [l for l in scene_out.stderr.split("\n") if "showinfo" in l and "pts_time" in l]
|
||||||
|
scene_count = len(scene_lines)
|
||||||
|
details["scene_changes_detected"] = scene_count
|
||||||
|
|
||||||
|
if duration > 0:
|
||||||
|
scene_rate = scene_count / duration
|
||||||
|
details["scene_changes_per_second"] = round(scene_rate, 3)
|
||||||
|
if scene_rate > 2:
|
||||||
|
score -= 5
|
||||||
|
suggestions.append("Very high scene change rate - video may be too fast-paced")
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
details["scene_changes_detected"] = "timeout"
|
||||||
|
suggestions.append("Scene change detection timed out")
|
||||||
|
except Exception as e:
|
||||||
|
details["scene_changes_detected"] = "error: " + str(e)[:100]
|
||||||
|
|
||||||
|
# 7) Subtitle / text overlay detection
|
||||||
|
try:
|
||||||
|
text_detect_cmd = [
|
||||||
|
"ffmpeg", "-i", file_path,
|
||||||
|
"-vf", "crop=iw:ih*0.15:0:ih*0.85,format=gray,threshold=0.7",
|
||||||
|
"-frames:v", "5", "-f", "image2pipe", "-vcodec", "rawvideo",
|
||||||
|
"-pix_fmt", "gray", "-"
|
||||||
|
]
|
||||||
|
text_out = subprocess.run(text_detect_cmd, capture_output=True, timeout=60)
|
||||||
|
|
||||||
|
if text_out.stdout and len(text_out.stdout) > 0:
|
||||||
|
data = text_out.stdout
|
||||||
|
total = len(data)
|
||||||
|
white_count = sum(1 for b in data if b > 200)
|
||||||
|
white_ratio = white_count / total if total > 0 else 0
|
||||||
|
details["subtitle_text_ratio"] = round(white_ratio, 4)
|
||||||
|
if white_ratio > 0.05:
|
||||||
|
details["subtitles_burned_in"] = True
|
||||||
|
suggestions.append("Burned-in subtitles detected. Consider separate subtitle tracks")
|
||||||
|
elif white_ratio > 0.02:
|
||||||
|
details["subtitles_burned_in"] = "possible"
|
||||||
|
suggestions.append("Possible burned-in subtitles - verify manually")
|
||||||
|
else:
|
||||||
|
details["subtitles_burned_in"] = False
|
||||||
|
else:
|
||||||
|
details["subtitles_burned_in"] = "unknown"
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
details["subtitles_burned_in"] = "timeout"
|
||||||
|
except Exception as e:
|
||||||
|
details["subtitles_burned_in"] = "error: " + str(e)[:100]
|
||||||
|
|
||||||
|
# Clamp score
|
||||||
|
score = max(0, min(100, score))
|
||||||
|
passed = score >= 60
|
||||||
|
|
||||||
|
return json.dumps({
|
||||||
|
"score": score,
|
||||||
|
"passed": passed,
|
||||||
|
"details": details,
|
||||||
|
"suggestions": suggestions
|
||||||
|
}, ensure_ascii=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return json.dumps({
|
||||||
|
"error": str(e),
|
||||||
|
"traceback": traceback.format_exc(),
|
||||||
|
"score": 0,
|
||||||
|
"passed": False,
|
||||||
|
"details": {},
|
||||||
|
"suggestions": []
|
||||||
|
}, ensure_ascii=False)
|
||||||
14
conf/config.json
Normal file
14
conf/config.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"website": {
|
||||||
|
"paths": [["$[workdir]$/app", ""]],
|
||||||
|
"client_max_size": 500000000,
|
||||||
|
"host": "0.0.0.0",
|
||||||
|
"port": 8901,
|
||||||
|
"coding": "utf-8",
|
||||||
|
"indexes": ["index.html", "index.dspy"],
|
||||||
|
"processors": [[".dspy", "dspy"]],
|
||||||
|
"startswiths": [
|
||||||
|
{"leading": "/idfile", "registerfunction": "idfile"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
9
video-eval/ah.py
Normal file
9
video-eval/ah.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
from ahserver import configuredServer
|
||||||
|
from appPublic.worker import schedule_once
|
||||||
|
|
||||||
|
async def init():
|
||||||
|
pass
|
||||||
|
|
||||||
|
server = configuredServer()
|
||||||
|
server.add_startup(init)
|
||||||
|
server.run()
|
||||||
191
video-eval/app/api/eval/index.dspy
Normal file
191
video-eval/app/api/eval/index.dspy
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ahserver.filestorage import FileStorage
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Get uploaded file
|
||||||
|
video_rel = params_kw.get("video_file", "")
|
||||||
|
if not video_rel:
|
||||||
|
result = {"error": "missing video_file parameter", "score": 0, "passed": False}
|
||||||
|
else:
|
||||||
|
fs = FileStorage()
|
||||||
|
video_path = fs.realPath(video_rel)
|
||||||
|
|
||||||
|
if not os.path.exists(video_path):
|
||||||
|
result = {"error": f"file not found: {video_path}", "score": 0, "passed": False}
|
||||||
|
else:
|
||||||
|
details = {}
|
||||||
|
score = 100
|
||||||
|
suggestions = []
|
||||||
|
|
||||||
|
# 1. ffprobe - get basic info
|
||||||
|
probe_cmd = [
|
||||||
|
"ffprobe", "-v", "quiet", "-print_format", "json",
|
||||||
|
"-show_format", "-show_streams", video_path
|
||||||
|
]
|
||||||
|
probe_out = subprocess.run(probe_cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
probe = json.loads(probe_out.stdout) if probe_out.returncode == 0 else {}
|
||||||
|
|
||||||
|
format_info = probe.get("format", {})
|
||||||
|
streams = probe.get("streams", [])
|
||||||
|
|
||||||
|
# Find video and audio streams
|
||||||
|
v_stream = None
|
||||||
|
a_stream = None
|
||||||
|
for s in streams:
|
||||||
|
if s.get("codec_type") == "video" and not v_stream:
|
||||||
|
v_stream = s
|
||||||
|
elif s.get("codec_type") == "audio" and not a_stream:
|
||||||
|
a_stream = s
|
||||||
|
|
||||||
|
# 2. Resolution check
|
||||||
|
if v_stream:
|
||||||
|
width = int(v_stream.get("width", 0))
|
||||||
|
height = int(v_stream.get("height", 0))
|
||||||
|
details["resolution"] = f"{width}x{height}"
|
||||||
|
details["codec"] = v_stream.get("codec_name", "unknown")
|
||||||
|
details["duration"] = float(format_info.get("duration", 0))
|
||||||
|
details["bitrate"] = int(format_info.get("bit_rate", 0))
|
||||||
|
|
||||||
|
# FPS
|
||||||
|
fps_str = v_stream.get("r_frame_rate", "0/1")
|
||||||
|
if "/" in fps_str:
|
||||||
|
num, den = fps_str.split("/")
|
||||||
|
fps = float(num) / float(den) if float(den) > 0 else 0
|
||||||
|
else:
|
||||||
|
fps = float(fps_str)
|
||||||
|
details["fps"] = round(fps, 2)
|
||||||
|
|
||||||
|
# Resolution scoring
|
||||||
|
if width < 1280 or height < 720:
|
||||||
|
score -= 20
|
||||||
|
suggestions.append(f"分辨率偏低({width}x{height}),建议至少1280x720")
|
||||||
|
elif width >= 1920 and height >= 1080:
|
||||||
|
pass # good
|
||||||
|
else:
|
||||||
|
score -= 5
|
||||||
|
|
||||||
|
# Aspect ratio
|
||||||
|
if height > 0:
|
||||||
|
ratio = width / height
|
||||||
|
if abs(ratio - 16/9) > 0.1:
|
||||||
|
score -= 5
|
||||||
|
suggestions.append(f"宽高比({ratio:.2f})不是标准16:9")
|
||||||
|
|
||||||
|
# Codec check
|
||||||
|
codec = v_stream.get("codec_name", "")
|
||||||
|
if codec not in ["h264", "h265", "hevc"]:
|
||||||
|
score -= 10
|
||||||
|
suggestions.append(f"视频编码{codec}不常用,建议使用h264/h265")
|
||||||
|
|
||||||
|
# Bitrate check
|
||||||
|
bitrate = int(format_info.get("bit_rate", 0))
|
||||||
|
if bitrate > 0:
|
||||||
|
bitrate_kbps = bitrate / 1000
|
||||||
|
details["bitrate_kbps"] = round(bitrate_kbps, 1)
|
||||||
|
if bitrate_kbps < 1000:
|
||||||
|
score -= 15
|
||||||
|
suggestions.append(f"码率偏低({bitrate_kbps:.0f}kbps),视频质量可能较差")
|
||||||
|
elif bitrate_kbps > 8000:
|
||||||
|
score -= 5
|
||||||
|
suggestions.append(f"码率偏高({bitrate_kbps:.0f}kbps),文件体积较大")
|
||||||
|
else:
|
||||||
|
score -= 50
|
||||||
|
suggestions.append("未检测到视频流")
|
||||||
|
details["resolution"] = "N/A"
|
||||||
|
|
||||||
|
# 3. Audio check
|
||||||
|
if a_stream:
|
||||||
|
details["audio_codec"] = a_stream.get("codec_name", "unknown")
|
||||||
|
details["audio_sample_rate"] = a_stream.get("sample_rate", "unknown")
|
||||||
|
details["audio_channels"] = a_stream.get("channels", 0)
|
||||||
|
|
||||||
|
audio_codec = a_stream.get("codec_name", "")
|
||||||
|
if audio_codec not in ["aac", "mp3", "opus", "flac"]:
|
||||||
|
score -= 5
|
||||||
|
suggestions.append(f"音频编码{audio_codec}不常用")
|
||||||
|
else:
|
||||||
|
score -= 20
|
||||||
|
suggestions.append("未检测到音频流")
|
||||||
|
|
||||||
|
# 4. Audio/Video sync check (basic: compare durations)
|
||||||
|
if v_stream and a_stream:
|
||||||
|
v_dur = float(v_stream.get("duration", 0))
|
||||||
|
a_dur = float(a_stream.get("duration", 0))
|
||||||
|
if v_dur > 0 and a_dur > 0:
|
||||||
|
drift = abs(v_dur - a_dur)
|
||||||
|
details["av_drift_seconds"] = round(drift, 3)
|
||||||
|
if drift > 0.1:
|
||||||
|
score -= 10
|
||||||
|
suggestions.append(f"音视频时长差{drift:.3f}s,可能存在同步问题")
|
||||||
|
|
||||||
|
# 5. Scene change detection (basic via ffprobe)
|
||||||
|
try:
|
||||||
|
scene_cmd = [
|
||||||
|
"ffprobe", "-v", "quiet", "-print_format", "json",
|
||||||
|
"-select_streams", "v:0",
|
||||||
|
"-show_frames", "-f", "null", "-"
|
||||||
|
]
|
||||||
|
# Use a simpler approach: count keyframes
|
||||||
|
keyframe_cmd = [
|
||||||
|
"ffprobe", "-v", "error", "-select_streams", "v:0",
|
||||||
|
"-show_entries", "frame=pict_type", "-of", "csv=p=0",
|
||||||
|
video_path
|
||||||
|
]
|
||||||
|
kf_out = subprocess.run(keyframe_cmd, capture_output=True, text=True, timeout=60)
|
||||||
|
if kf_out.stdout:
|
||||||
|
frames = kf_out.stdout.strip().split("\n")
|
||||||
|
total_frames = len(frames)
|
||||||
|
keyframes = sum(1 for f in frames if f.startswith("I"))
|
||||||
|
details["total_frames"] = total_frames
|
||||||
|
details["keyframes"] = keyframes
|
||||||
|
|
||||||
|
if total_frames > 0:
|
||||||
|
kf_ratio = keyframes / total_frames
|
||||||
|
details["keyframe_ratio"] = round(kf_ratio, 4)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 6. Subtitle burn-in detection (check for text in first few frames)
|
||||||
|
try:
|
||||||
|
sub_cmd = [
|
||||||
|
"ffmpeg", "-i", video_path, "-vf",
|
||||||
|
"select='eq(n,0)+eq(n,100)+eq(n,200)',ocr",
|
||||||
|
"-f", "null", "-"
|
||||||
|
]
|
||||||
|
# OCR not always available, skip if fails
|
||||||
|
sub_out = subprocess.run(sub_cmd, capture_output=True, text=True, timeout=15)
|
||||||
|
# Check if ffmpeg has OCR filter
|
||||||
|
if "No such filter" not in sub_out.stderr:
|
||||||
|
ocr_text = sub_out.stderr
|
||||||
|
# If OCR found text, subtitles may be burned in
|
||||||
|
if any(c.isalpha() for c in ocr_text):
|
||||||
|
details["subtitle_detected"] = True
|
||||||
|
else:
|
||||||
|
details["subtitle_detected"] = False
|
||||||
|
else:
|
||||||
|
details["subtitle_detected"] = "OCR unavailable"
|
||||||
|
except Exception:
|
||||||
|
details["subtitle_detected"] = "check_skipped"
|
||||||
|
|
||||||
|
# Final score clamping
|
||||||
|
score = max(0, min(100, score))
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"score": score,
|
||||||
|
"passed": score >= 60,
|
||||||
|
"details": details,
|
||||||
|
"suggestions": suggestions,
|
||||||
|
"file": os.path.basename(video_path)
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
result = {"error": str(e), "traceback": traceback.format_exc(), "score": 0, "passed": False}
|
||||||
|
|
||||||
|
return json.dumps(result, ensure_ascii=False)
|
||||||
1
video-eval/conf/config.json
Normal file
1
video-eval/conf/config.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"port": 8901, "startswiths": ["api"], "filesroot": "/tmp"}
|
||||||
Loading…
x
Reference in New Issue
Block a user