Compare commits

...

No commits in common. "main" and "master" have entirely different histories.
main ... master

8 changed files with 535 additions and 2 deletions

10
.gitignore vendored Normal file
View File

@ -0,0 +1,10 @@
__pycache__/
*.pyc
*.pyo
logs/
*.log
nohup.out
nohup_gpu*.out
py3/
*.egg-info/
*.pid

View File

@ -1,2 +0,0 @@
# video-eval

8
ah.py Normal file
View 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
View 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
View 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
View 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()

View 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)

View File

@ -0,0 +1 @@
{"port": 8901, "startswiths": ["api"], "filesroot": "/tmp"}