Spaces:
Runtime error
Runtime error
Upload app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import ffmpeg
|
| 3 |
+
import tempfile
|
| 4 |
+
import os
|
| 5 |
+
from PIL import Image
|
| 6 |
+
import numpy as np
|
| 7 |
+
import os
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
# 设置umask确保新创建的文件有正确的权限
|
| 11 |
+
os.umask(0o022)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def subliminal_insert(video_path,
|
| 15 |
+
frame_img_path,
|
| 16 |
+
interval_frames,
|
| 17 |
+
duration_frames,
|
| 18 |
+
output_dir,
|
| 19 |
+
progress=None):
|
| 20 |
+
"""
|
| 21 |
+
将特定帧按间隔植入视频流中
|
| 22 |
+
"""
|
| 23 |
+
if not os.path.exists(video_path):
|
| 24 |
+
raise FileNotFoundError("视频文件不存在")
|
| 25 |
+
if not os.path.exists(frame_img_path):
|
| 26 |
+
raise FileNotFoundError("图片文件不存在")
|
| 27 |
+
|
| 28 |
+
temp_output = None
|
| 29 |
+
frame_temp = None
|
| 30 |
+
|
| 31 |
+
try:
|
| 32 |
+
# 获取视频基本信息
|
| 33 |
+
probe = ffmpeg.probe(video_path)
|
| 34 |
+
video_info = next(s for s in probe['streams']
|
| 35 |
+
if s['codec_type'] == 'video')
|
| 36 |
+
width = int(video_info['width'])
|
| 37 |
+
height = int(video_info['height'])
|
| 38 |
+
fps = eval(video_info['r_frame_rate'])
|
| 39 |
+
|
| 40 |
+
print(f"视频信息: {width}x{height} @ {fps}fps")
|
| 41 |
+
|
| 42 |
+
if progress is not None:
|
| 43 |
+
progress(0.1, "正在处理图片...")
|
| 44 |
+
|
| 45 |
+
# 调整图片大小以匹配视频尺寸
|
| 46 |
+
img = Image.open(frame_img_path)
|
| 47 |
+
img = img.resize((width, height))
|
| 48 |
+
frame_temp = tempfile.NamedTemporaryFile(delete=False,
|
| 49 |
+
suffix='.png').name
|
| 50 |
+
img.save(frame_temp)
|
| 51 |
+
|
| 52 |
+
if progress is not None:
|
| 53 |
+
progress(0.2, "正在准备处理视频...")
|
| 54 |
+
|
| 55 |
+
# 使用系统临时目录
|
| 56 |
+
gradio_temp_dir = os.path.join(tempfile.gettempdir(), "gradio")
|
| 57 |
+
os.makedirs(gradio_temp_dir, exist_ok=True)
|
| 58 |
+
temp_output = os.path.join(gradio_temp_dir,
|
| 59 |
+
f"output_{int(time.time())}.mp4")
|
| 60 |
+
print(f"将创建输出文件: {temp_output}")
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
if progress is not None:
|
| 64 |
+
progress(0.3, "正在处理视频...")
|
| 65 |
+
|
| 66 |
+
# 构建ffmpeg命令
|
| 67 |
+
input_video = ffmpeg.input(video_path)
|
| 68 |
+
input_image = ffmpeg.input(frame_temp)
|
| 69 |
+
|
| 70 |
+
# 构建filter complex
|
| 71 |
+
v = ffmpeg.filter(
|
| 72 |
+
[input_video, input_image],
|
| 73 |
+
'overlay',
|
| 74 |
+
enable=f'if(lt(mod(n,{interval_frames}),{duration_frames}),1,0)'
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
# 检查输入视频是否有音频流
|
| 78 |
+
has_audio = any(s['codec_type'] == 'audio'
|
| 79 |
+
for s in probe['streams'])
|
| 80 |
+
|
| 81 |
+
# 构建输出
|
| 82 |
+
if has_audio:
|
| 83 |
+
stream = ffmpeg.output(v,
|
| 84 |
+
input_video.audio,
|
| 85 |
+
temp_output,
|
| 86 |
+
acodec='copy',
|
| 87 |
+
vcodec='libx264',
|
| 88 |
+
preset='ultrafast',
|
| 89 |
+
crf=23,
|
| 90 |
+
movflags='+faststart')
|
| 91 |
+
else:
|
| 92 |
+
stream = ffmpeg.output(v,
|
| 93 |
+
temp_output,
|
| 94 |
+
vcodec='libx264',
|
| 95 |
+
preset='ultrafast',
|
| 96 |
+
crf=23,
|
| 97 |
+
movflags='+faststart')
|
| 98 |
+
|
| 99 |
+
# 打印完整命令
|
| 100 |
+
cmd = " ".join(stream.compile())
|
| 101 |
+
print("FFmpeg命令:", cmd)
|
| 102 |
+
|
| 103 |
+
# 运行命令
|
| 104 |
+
stdout, stderr = stream.run(capture_stdout=True,
|
| 105 |
+
capture_stderr=True,
|
| 106 |
+
overwrite_output=True)
|
| 107 |
+
|
| 108 |
+
if progress is not None:
|
| 109 |
+
progress(0.9, "正在完成处理...")
|
| 110 |
+
|
| 111 |
+
# 检查输出文件
|
| 112 |
+
if not os.path.exists(temp_output):
|
| 113 |
+
raise Exception(f"输出文件不存在: {temp_output}")
|
| 114 |
+
|
| 115 |
+
if os.path.getsize(temp_output) == 0:
|
| 116 |
+
raise Exception(f"输出文件大小为0: {temp_output}")
|
| 117 |
+
|
| 118 |
+
# 设置文件权限
|
| 119 |
+
try:
|
| 120 |
+
os.chmod(temp_output, 0o644)
|
| 121 |
+
except Exception as e:
|
| 122 |
+
print(f"设置文件权限时出错: {str(e)}")
|
| 123 |
+
|
| 124 |
+
print(
|
| 125 |
+
f"成功创建输出文件: {temp_output}, 大小: {os.path.getsize(temp_output)} bytes"
|
| 126 |
+
)
|
| 127 |
+
|
| 128 |
+
if progress is not None:
|
| 129 |
+
progress(1.0, "处理完成")
|
| 130 |
+
|
| 131 |
+
return temp_output
|
| 132 |
+
|
| 133 |
+
except ffmpeg.Error as e:
|
| 134 |
+
print("FFmpeg stdout:",
|
| 135 |
+
e.stdout.decode('utf8') if e.stdout else "No stdout")
|
| 136 |
+
print("FFmpeg stderr:",
|
| 137 |
+
e.stderr.decode('utf8') if e.stderr else "No stderr")
|
| 138 |
+
raise Exception(
|
| 139 |
+
f"FFmpeg处理失败: {e.stderr.decode('utf8') if e.stderr else str(e)}"
|
| 140 |
+
)
|
| 141 |
+
|
| 142 |
+
except Exception as e:
|
| 143 |
+
print(f"处理视频时出错: {str(e)}")
|
| 144 |
+
if isinstance(e, ffmpeg.Error):
|
| 145 |
+
if e.stderr:
|
| 146 |
+
print("FFmpeg错误输出:", e.stderr.decode('utf8'))
|
| 147 |
+
raise
|
| 148 |
+
|
| 149 |
+
finally:
|
| 150 |
+
# 清理临时文件
|
| 151 |
+
if frame_temp and os.path.exists(frame_temp):
|
| 152 |
+
try:
|
| 153 |
+
os.unlink(frame_temp)
|
| 154 |
+
except:
|
| 155 |
+
pass
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def process_video(video_input,
|
| 159 |
+
frame_input,
|
| 160 |
+
interval,
|
| 161 |
+
duration,
|
| 162 |
+
progress=gr.Progress()):
|
| 163 |
+
"""
|
| 164 |
+
处理视频和图片输入
|
| 165 |
+
"""
|
| 166 |
+
# 输入验证
|
| 167 |
+
if interval <= 0 or duration <= 0:
|
| 168 |
+
raise ValueError("间隔和持续时间必须大于0")
|
| 169 |
+
if interval < duration:
|
| 170 |
+
raise ValueError("间隔必须大于持续时间")
|
| 171 |
+
if interval < 12: # 安全限制
|
| 172 |
+
raise ValueError("为了安全起见,间隔不能小于12帧")
|
| 173 |
+
|
| 174 |
+
temp_files = [] # 跟踪临时文件
|
| 175 |
+
|
| 176 |
+
# 创建一个固定的输出目录(在当前工作目录下)
|
| 177 |
+
output_dir = os.path.join(os.getcwd(), "outputs")
|
| 178 |
+
os.makedirs(output_dir, exist_ok=True)
|
| 179 |
+
# 确保目录有正确的权限
|
| 180 |
+
os.chmod(output_dir, 0o777)
|
| 181 |
+
|
| 182 |
+
try:
|
| 183 |
+
# 检查输入是否为空
|
| 184 |
+
if video_input is None or frame_input is None:
|
| 185 |
+
raise ValueError("视频或图片输入为空")
|
| 186 |
+
|
| 187 |
+
# 处理视频输入
|
| 188 |
+
video_temp = tempfile.NamedTemporaryFile(delete=False,
|
| 189 |
+
suffix='.mp4').name
|
| 190 |
+
temp_files.append(video_temp)
|
| 191 |
+
|
| 192 |
+
if isinstance(video_input, str):
|
| 193 |
+
video_temp = video_input
|
| 194 |
+
else:
|
| 195 |
+
try:
|
| 196 |
+
with open(video_temp, 'wb') as f:
|
| 197 |
+
f.write(video_input.read() if hasattr(video_input, 'read'
|
| 198 |
+
) else video_input)
|
| 199 |
+
except Exception as e:
|
| 200 |
+
raise Exception(f"视频写入失败: {str(e)}")
|
| 201 |
+
|
| 202 |
+
# 处理图片输入
|
| 203 |
+
frame_temp = tempfile.NamedTemporaryFile(delete=False,
|
| 204 |
+
suffix='.png').name
|
| 205 |
+
temp_files.append(frame_temp)
|
| 206 |
+
|
| 207 |
+
try:
|
| 208 |
+
if hasattr(frame_input, 'save'):
|
| 209 |
+
frame_input.save(frame_temp)
|
| 210 |
+
elif isinstance(frame_input, np.ndarray):
|
| 211 |
+
Image.fromarray(frame_input).save(frame_temp)
|
| 212 |
+
else:
|
| 213 |
+
raise ValueError("不支持的图片格式")
|
| 214 |
+
except Exception as e:
|
| 215 |
+
raise Exception(f"图片处理失败: {str(e)}")
|
| 216 |
+
|
| 217 |
+
output_path = subliminal_insert(video_temp, frame_temp, int(interval),
|
| 218 |
+
int(duration), output_dir, progress)
|
| 219 |
+
|
| 220 |
+
print(f"处理完成,输出文件: {output_path}")
|
| 221 |
+
return output_path
|
| 222 |
+
|
| 223 |
+
except Exception as e:
|
| 224 |
+
print(f"处理失败: {str(e)}")
|
| 225 |
+
raise
|
| 226 |
+
|
| 227 |
+
finally:
|
| 228 |
+
# 清理临时文件
|
| 229 |
+
for temp_file in temp_files:
|
| 230 |
+
if temp_file != video_input and os.path.exists(temp_file):
|
| 231 |
+
try:
|
| 232 |
+
os.unlink(temp_file)
|
| 233 |
+
except Exception:
|
| 234 |
+
pass
|
| 235 |
+
|
| 236 |
+
|
| 237 |
+
# 创建Gradio界面
|
| 238 |
+
iface = gr.Interface(fn=process_video,
|
| 239 |
+
inputs=[
|
| 240 |
+
gr.Video(label="输入视频", format="mp4"),
|
| 241 |
+
gr.Image(label="要植入的帧", type="pil"),
|
| 242 |
+
gr.Slider(minimum=12,
|
| 243 |
+
maximum=60,
|
| 244 |
+
value=24,
|
| 245 |
+
step=1,
|
| 246 |
+
label="植入间隔(帧数)",
|
| 247 |
+
info="每隔多少帧插入一次图片"),
|
| 248 |
+
gr.Slider(minimum=1,
|
| 249 |
+
maximum=10,
|
| 250 |
+
value=1,
|
| 251 |
+
step=1,
|
| 252 |
+
label="植入持续时间(帧数)",
|
| 253 |
+
info="每次插入的图片持续多少帧")
|
| 254 |
+
],
|
| 255 |
+
outputs=gr.File(label="输出视频", visible=True),
|
| 256 |
+
title="阈下植入",
|
| 257 |
+
description="将特定图片帧按指定间隔植入到视频中",
|
| 258 |
+
article="""
|
| 259 |
+
使用说明:
|
| 260 |
+
1. 上传要处理的视频文件(MP4格式)
|
| 261 |
+
2. 上传要植入的图片
|
| 262 |
+
3. 调整植入间隔和持续时间
|
| 263 |
+
4. 点击提交开始处理
|
| 264 |
+
|
| 265 |
+
注意事项:
|
| 266 |
+
- 间隔帧数必须大于持续帧数
|
| 267 |
+
- 建议间隔至少为24帧(1秒)
|
| 268 |
+
- 处理时间取决于视频长度和复杂度
|
| 269 |
+
""",
|
| 270 |
+
flagging_mode="never",
|
| 271 |
+
theme="default")
|
| 272 |
+
|
| 273 |
+
if __name__ == "__main__":
|
| 274 |
+
iface.launch(
|
| 275 |
+
debug=True,
|
| 276 |
+
show_error=True,
|
| 277 |
+
server_name="0.0.0.0",
|
| 278 |
+
server_port=7861,
|
| 279 |
+
share=True,
|
| 280 |
+
allowed_paths=[
|
| 281 |
+
"/tmp", "/tmp/gradio",
|
| 282 |
+
os.path.join(os.getcwd(), "outputs")
|
| 283 |
+
], # 明确允许访问的路径
|
| 284 |
+
root_path="", # 确保根路径正确
|
| 285 |
+
)
|