sdafd commited on
Commit
5da8662
·
verified ·
1 Parent(s): f83fd1d

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +171 -0
app.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import shlex
4
+ import tempfile
5
+ import uuid
6
+ from typing import List
7
+
8
+ from fastapi import FastAPI, File, UploadFile, Form, HTTPException
9
+ from fastapi.responses import FileResponse, JSONResponse
10
+ import logging
11
+
12
+ # Configure logging
13
+ logging.basicConfig(level=logging.INFO)
14
+ logger = logging.getLogger(__name__)
15
+
16
+ # --- Define the core ffmpeg function (adapted for API context) ---
17
+ # Note: We pass paths directly now, as they are managed within the API call.
18
+ # We also return stderr for debugging purposes in case of failure.
19
+ def run_ffmpeg_concatenation(input_files: List[str], output_file: str, ffmpeg_executable: str = "ffmpeg"):
20
+ """
21
+ Runs the FFmpeg concatenation process.
22
+ Returns (success: bool, message: str, stderr: str)
23
+ """
24
+ if not input_files:
25
+ return False, "Error: Cannot concatenate, file list is empty.", ""
26
+
27
+ logger.info(f"Starting re-encode concatenation of {len(input_files)} videos into {output_file}...")
28
+ logger.info("This will re-encode the video and may take some time.")
29
+
30
+ input_args = []
31
+ for video_file in input_files:
32
+ input_args.extend(['-i', video_file])
33
+
34
+ filter_parts = []
35
+ for i in range(len(input_files)):
36
+ filter_parts.append(f"[{i}:v]")
37
+ filter_parts.append(f"[{i}:a]")
38
+ filter_string = "".join(filter_parts) + f"concat=n={len(input_files)}:v=1:a=1[outv][outa]"
39
+
40
+ command = [
41
+ ffmpeg_executable,
42
+ *input_args,
43
+ '-filter_complex', filter_string,
44
+ '-map', '[outv]',
45
+ '-map', '[outa]',
46
+ '-vsync', 'vfr',
47
+ '-movflags', '+faststart',
48
+ '-y', # Overwrite output without asking
49
+ output_file
50
+ ]
51
+
52
+ logger.info("\nRunning FFmpeg command:")
53
+ try:
54
+ cmd_str = shlex.join(command)
55
+ logger.info(cmd_str)
56
+ except AttributeError: # shlex.join is Python 3.8+
57
+ cmd_str = ' '.join(f'"{arg}"' if ' ' in arg else arg for arg in command)
58
+ logger.info(cmd_str)
59
+
60
+ logger.info("\n--- FFmpeg Execution Start ---")
61
+ stderr_output = ""
62
+ try:
63
+ # Using Popen for potentially better handling of large outputs if needed,
64
+ # but run is simpler here. Ensure encoding/errors are handled.
65
+ process = subprocess.run(
66
+ command,
67
+ check=True, # Raises CalledProcessError on non-zero exit
68
+ capture_output=True,
69
+ text=True,
70
+ encoding='utf-8',
71
+ errors='replace' # Handle potential encoding errors in ffmpeg output
72
+ )
73
+ stderr_output = process.stderr
74
+ logger.info("--- FFmpeg Execution End ---")
75
+ # logger.info("\nSTDOUT:") # Often empty for concat
76
+ # logger.info(process.stdout)
77
+ logger.info("\nSTDERR (Progress/Info):\n" + stderr_output)
78
+ msg = f"Successfully concatenated videos into {os.path.basename(output_file)}"
79
+ logger.info(msg)
80
+ return True, msg, stderr_output # Return success, message, and stderr
81
+ except subprocess.CalledProcessError as e:
82
+ stderr_output = e.stderr + "\n" + e.stdout # Combine stderr/stdout on error
83
+ logger.error("--- FFmpeg Execution End ---")
84
+ logger.error(f"\nError during FFmpeg execution (return code {e.returncode}):")
85
+ logger.error("\nSTDERR/STDOUT:\n" + stderr_output)
86
+ return False, f"FFmpeg failed with return code {e.returncode}.", stderr_output
87
+ except FileNotFoundError:
88
+ err_msg = f"Error: '{ffmpeg_executable}' command not found on the server."
89
+ logger.error(err_msg)
90
+ return False, err_msg, ""
91
+ except Exception as e:
92
+ err_msg = f"An unexpected server error occurred during ffmpeg processing: {e}"
93
+ logger.exception(err_msg) # Log full traceback
94
+ return False, err_msg, stderr_output # Include any captured stderr
95
+
96
+ # --- FastAPI App Definition ---
97
+ app = FastAPI()
98
+
99
+ @app.post("/concatenate/")
100
+ async def concatenate_videos_api(
101
+ files: List[UploadFile] = File(..., description="List of video files to concatenate"),
102
+ output_filename: str = Form("concatenated_video.mp4", description="Desired output filename (e.g., final.mp4)")
103
+ ):
104
+ """
105
+ API endpoint to concatenate uploaded video files using FFmpeg re-encoding.
106
+ """
107
+ if not files:
108
+ raise HTTPException(status_code=400, detail="No files were uploaded.")
109
+ if not output_filename.lower().endswith(('.mp4', '.mov', '.avi', '.mkv')): # Basic validation
110
+ raise HTTPException(status_code=400, detail="Output filename must have a common video extension (mp4, mov, avi, mkv).")
111
+
112
+ logger.info(f"Received {len(files)} files for concatenation. Output name: {output_filename}")
113
+
114
+ # Create a temporary directory to store uploaded files and the output
115
+ with tempfile.TemporaryDirectory() as temp_dir:
116
+ input_file_paths = []
117
+ original_filenames = []
118
+
119
+ try:
120
+ # Save uploaded files to the temporary directory
121
+ for uploaded_file in files:
122
+ original_filenames.append(uploaded_file.filename)
123
+ # Create a unique-ish temp filename to avoid clashes, keep extension
124
+ _, ext = os.path.splitext(uploaded_file.filename)
125
+ temp_input_path = os.path.join(temp_dir, f"{uuid.uuid4()}{ext}")
126
+ logger.info(f"Saving uploaded file '{uploaded_file.filename}' to '{temp_input_path}'")
127
+ with open(temp_input_path, "wb") as buffer:
128
+ buffer.write(await uploaded_file.read())
129
+ input_file_paths.append(temp_input_path)
130
+ await uploaded_file.close() # Close the file handle
131
+
132
+ logger.info(f"Saved files: {original_filenames}")
133
+
134
+ # Define the output path within the temporary directory
135
+ temp_output_path = os.path.join(temp_dir, output_filename)
136
+
137
+ # Run the FFmpeg concatenation logic
138
+ success, message, ffmpeg_stderr = run_ffmpeg_concatenation(input_file_paths, temp_output_path)
139
+
140
+ if success:
141
+ logger.info(f"Concatenation successful. Preparing file response for: {temp_output_path}")
142
+ # Return the concatenated file
143
+ return FileResponse(
144
+ path=temp_output_path,
145
+ filename=output_filename, # Send back with the desired name
146
+ media_type='video/mp4' # Adjust if needed based on output_filename extension
147
+ )
148
+ else:
149
+ logger.error(f"Concatenation failed: {message}")
150
+ # Return a JSON error response including ffmpeg's stderr if available
151
+ return JSONResponse(
152
+ status_code=500,
153
+ content={
154
+ "detail": f"Video concatenation failed: {message}",
155
+ "ffmpeg_stderr": ffmpeg_stderr,
156
+ "input_files": original_filenames
157
+ }
158
+ )
159
+
160
+ except Exception as e:
161
+ logger.exception("An unexpected error occurred in the API endpoint.")
162
+ raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
163
+
164
+ @app.get("/")
165
+ async def read_root():
166
+ return {"message": "Video Concatenation API is running. Use the /concatenate/ endpoint (POST) to process videos."}
167
+
168
+ # Example of how to run locally (Hugging Face Spaces handles this automatically)
169
+ # if __name__ == "__main__":
170
+ # import uvicorn
171
+ # uvicorn.run(app, host="0.0.0.0", port=8000)