|
|
import spaces |
|
|
import gradio as gr |
|
|
import tempfile |
|
|
import os |
|
|
import subprocess |
|
|
import shutil |
|
|
import asyncio |
|
|
from pathlib import Path |
|
|
import uuid |
|
|
import logging |
|
|
|
|
|
|
|
|
logging.basicConfig(level=logging.INFO) |
|
|
logger = logging.getLogger(__name__) |
|
|
|
|
|
|
|
|
MAX_PROCESS_TIMEOUT = 120 |
|
|
TEMP_BASE_DIR = Path("/tmp/video_processing") |
|
|
TEMP_BASE_DIR.mkdir(exist_ok=True, parents=True) |
|
|
|
|
|
async def run_ffmpeg_async(cmd, timeout=120): |
|
|
"""Асинхронный запуск ffmpeg с таймаутом""" |
|
|
try: |
|
|
process = await asyncio.create_subprocess_exec( |
|
|
*cmd, |
|
|
stdout=asyncio.subprocess.PIPE, |
|
|
stderr=asyncio.subprocess.PIPE |
|
|
) |
|
|
|
|
|
try: |
|
|
stdout, stderr = await asyncio.wait_for( |
|
|
process.communicate(), |
|
|
timeout=timeout |
|
|
) |
|
|
|
|
|
if process.returncode != 0: |
|
|
error_msg = stderr.decode() if stderr else "Unknown error" |
|
|
logger.error(f"FFmpeg error: {error_msg}") |
|
|
raise Exception(f"FFmpeg failed: {error_msg}") |
|
|
|
|
|
return stdout, stderr |
|
|
|
|
|
except asyncio.TimeoutError: |
|
|
try: |
|
|
process.kill() |
|
|
except: |
|
|
pass |
|
|
raise Exception(f"FFmpeg timeout after {timeout} seconds") |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"FFmpeg execution error: {e}") |
|
|
raise |
|
|
|
|
|
async def process_video_with_resources(start_video, job_id): |
|
|
"""Обработка видео с изоляцией ресурсов""" |
|
|
|
|
|
work_dir = TEMP_BASE_DIR / job_id |
|
|
work_dir.mkdir(exist_ok=True) |
|
|
|
|
|
|
|
|
input_path = work_dir / "input.mp4" |
|
|
audio_path = work_dir / "with_audio.mp4" |
|
|
blurred_path = work_dir / "blurred.mp4" |
|
|
|
|
|
try: |
|
|
|
|
|
if isinstance(start_video, str): |
|
|
shutil.copy(start_video, str(input_path)) |
|
|
else: |
|
|
|
|
|
with open(input_path, "wb") as f: |
|
|
|
|
|
if hasattr(start_video, "name"): |
|
|
with open(start_video.name, "rb") as src: |
|
|
f.write(src.read()) |
|
|
else: |
|
|
|
|
|
import io |
|
|
if isinstance(start_video, io.IOBase): |
|
|
f.write(start_video.read()) |
|
|
|
|
|
|
|
|
cmd_add_audio = [ |
|
|
'ffmpeg', |
|
|
'-f', 'lavfi', |
|
|
'-i', 'anullsrc=channel_layout=stereo:sample_rate=44100', |
|
|
'-i', str(input_path), |
|
|
'-c:v', 'copy', |
|
|
'-c:a', 'aac', |
|
|
'-shortest', |
|
|
'-threads', '2', |
|
|
'-loglevel', 'error', |
|
|
'-y', |
|
|
str(audio_path) |
|
|
] |
|
|
|
|
|
await run_ffmpeg_async(cmd_add_audio, timeout=MAX_PROCESS_TIMEOUT) |
|
|
|
|
|
|
|
|
cmd_blur = [ |
|
|
'ffmpeg', |
|
|
'-i', str(audio_path), |
|
|
'-vf', 'gblur=sigma=25', |
|
|
'-c:a', 'copy', |
|
|
'-threads', '2', |
|
|
'-loglevel', 'error', |
|
|
'-y', |
|
|
str(blurred_path) |
|
|
] |
|
|
|
|
|
await run_ffmpeg_async(cmd_blur, timeout=MAX_PROCESS_TIMEOUT) |
|
|
|
|
|
|
|
|
return str(audio_path), str(blurred_path) |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Processing error for job {job_id}: {e}") |
|
|
|
|
|
|
|
|
try: |
|
|
shutil.rmtree(work_dir) |
|
|
except: |
|
|
pass |
|
|
|
|
|
raise |
|
|
finally: |
|
|
|
|
|
try: |
|
|
if input_path.exists(): |
|
|
input_path.unlink() |
|
|
except: |
|
|
pass |
|
|
|
|
|
async def finalise_video(start_video): |
|
|
"""Основная функция обработки видео""" |
|
|
job_id = str(uuid.uuid4()) |
|
|
logger.info(f"Starting video processing job: {job_id}") |
|
|
|
|
|
try: |
|
|
|
|
|
output_path1, output_path2 = await process_video_with_resources( |
|
|
start_video, job_id |
|
|
) |
|
|
|
|
|
logger.info(f"Completed video processing job: {job_id}") |
|
|
return output_path1, output_path2 |
|
|
|
|
|
except Exception as e: |
|
|
logger.error(f"Finalise video error: {e}") |
|
|
raise gr.Error(f"Ошибка обработки видео: {str(e)}") |
|
|
|
|
|
def check_ffmpeg(): |
|
|
"""Проверка доступности ffmpeg""" |
|
|
try: |
|
|
result = subprocess.run( |
|
|
['ffmpeg', '-version'], |
|
|
capture_output=True, |
|
|
text=True, |
|
|
timeout=5 |
|
|
) |
|
|
return result.returncode == 0 |
|
|
except Exception: |
|
|
return False |
|
|
|
|
|
def cleanup_old_files(): |
|
|
"""Фоновая очистка старых файлов""" |
|
|
try: |
|
|
import time |
|
|
current_time = time.time() |
|
|
for item in TEMP_BASE_DIR.iterdir(): |
|
|
try: |
|
|
if item.is_dir(): |
|
|
|
|
|
stat = item.stat() |
|
|
dir_age = current_time - stat.st_ctime |
|
|
if dir_age > 600: |
|
|
shutil.rmtree(item, ignore_errors=True) |
|
|
logger.info(f"Cleaned up old directory: {item}") |
|
|
except Exception as e: |
|
|
logger.warning(f"Failed to clean up {item}: {e}") |
|
|
except Exception as e: |
|
|
logger.error(f"Cleanup error: {e}") |
|
|
|
|
|
|
|
|
|
|
|
with gr.Blocks() as demo: |
|
|
gr.Markdown("# Видео Финализатор") |
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(scale=1): |
|
|
input_video = gr.Video( |
|
|
label="Входное видео", |
|
|
interactive=True |
|
|
) |
|
|
|
|
|
process_btn = gr.Button( |
|
|
"Обработать видео", |
|
|
variant="primary" |
|
|
) |
|
|
|
|
|
with gr.Column(scale=2): |
|
|
with gr.Row(): |
|
|
output1 = gr.Video( |
|
|
label="Видео с аудио", |
|
|
autoplay=True, |
|
|
interactive=False |
|
|
) |
|
|
output2 = gr.Video( |
|
|
label="Размытое видео", |
|
|
autoplay=True, |
|
|
interactive=False |
|
|
) |
|
|
|
|
|
|
|
|
process_btn.click( |
|
|
fn=finalise_video, |
|
|
inputs=[input_video], |
|
|
outputs=[output1, output2] |
|
|
) |
|
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
|
if not check_ffmpeg(): |
|
|
logger.error("FFmpeg не найден! Установите ffmpeg.") |
|
|
print("Ошибка: FFmpeg не установлен.") |
|
|
|
|
|
|
|
|
import threading |
|
|
import time |
|
|
|
|
|
def cleanup_thread(): |
|
|
while True: |
|
|
cleanup_old_files() |
|
|
time.sleep(600) |
|
|
|
|
|
cleanup_thread = threading.Thread(target=cleanup_thread, daemon=True) |
|
|
cleanup_thread.start() |
|
|
|
|
|
|
|
|
|
|
|
demo.queue(max_size=100).launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860 |
|
|
) |