Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -48,19 +48,37 @@ def cleanup_directory(directory_path: str):
|
|
| 48 |
shutil.rmtree(directory_path, ignore_errors=True)
|
| 49 |
print(f"--- CLEANUP: Successfully removed {directory_path} ---")
|
| 50 |
|
| 51 |
-
async def run_subprocess(command: str):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
print(f"Executing command: {command}")
|
| 53 |
process = await asyncio.create_subprocess_shell(
|
| 54 |
-
command,
|
|
|
|
|
|
|
| 55 |
)
|
| 56 |
stdout, stderr = await process.communicate()
|
| 57 |
|
| 58 |
if process.returncode != 0:
|
| 59 |
error_message = stderr.decode(errors='ignore')
|
| 60 |
-
print(f"
|
|
|
|
| 61 |
raise RuntimeError(f"FFmpeg command failed: {error_message}")
|
| 62 |
|
| 63 |
-
print("Command executed
|
|
|
|
|
|
|
| 64 |
|
| 65 |
# ==============================================================================
|
| 66 |
# 3. API ENDPOINTS
|
|
@@ -143,82 +161,37 @@ async def process_video_from_urls(payload: Dict = Body(...)):
|
|
| 143 |
async def get_duration(file: UploadFile = File(...)):
|
| 144 |
"""
|
| 145 |
Calculates the duration of a media file using FFprobe.
|
| 146 |
-
|
| 147 |
-
This endpoint accepts a binary file upload, saves it temporarily, runs
|
| 148 |
-
ffprobe to get its duration, and returns the duration in seconds.
|
| 149 |
-
It ensures temporary files are always cleaned up.
|
| 150 |
-
|
| 151 |
-
Args:
|
| 152 |
-
file (UploadFile): The media file (audio or video) uploaded by the client.
|
| 153 |
-
|
| 154 |
-
Returns:
|
| 155 |
-
JSONResponse: A JSON object containing the duration in seconds,
|
| 156 |
-
e.g., {"duration_seconds": 146.5}.
|
| 157 |
-
|
| 158 |
-
Raises:
|
| 159 |
-
HTTPException(400): If the duration cannot be parsed from ffprobe's output.
|
| 160 |
-
HTTPException(500): If ffprobe fails or an internal error occurs.
|
| 161 |
"""
|
| 162 |
-
# --- Setup Temporary Environment ---
|
| 163 |
-
# Create a unique filename to prevent conflicts
|
| 164 |
unique_id = str(uuid.uuid4())
|
| 165 |
temp_input_path = os.path.join(TEMP_DIR, f"{unique_id}_{file.filename}")
|
| 166 |
|
| 167 |
try:
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
with open(temp_input_path, "wb") as buffer:
|
| 172 |
-
shutil.copyfileobj(file.file, buffer)
|
| 173 |
-
print("File saved successfully.")
|
| 174 |
-
except Exception as e:
|
| 175 |
-
# Handle potential file writing errors
|
| 176 |
-
raise HTTPException(status_code=500, detail=f"Failed to write uploaded file to disk: {e}")
|
| 177 |
-
|
| 178 |
-
# --- Step 2: Construct and Execute FFprobe Command ---
|
| 179 |
-
# Using shlex.quote for security against command injection
|
| 180 |
quoted_path = shlex.quote(temp_input_path)
|
| 181 |
-
|
| 182 |
-
# This ffprobe command is optimized to return ONLY the duration number
|
| 183 |
command = f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {quoted_path}"
|
| 184 |
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
raise HTTPException(status_code=500, detail="FFprobe command failed. Check server logs.")
|
| 188 |
-
|
| 189 |
-
# Re-run the command to capture the output, as the helper only returns success/fail
|
| 190 |
-
process = await asyncio.create_subprocess_shell(
|
| 191 |
-
command,
|
| 192 |
-
stdout=asyncio.subprocess.PIPE,
|
| 193 |
-
stderr=asyncio.subprocess.PIPE
|
| 194 |
-
)
|
| 195 |
-
stdout, stderr = await process.communicate()
|
| 196 |
|
| 197 |
-
duration_str = stdout.
|
| 198 |
print(f"FFprobe output (duration): '{duration_str}'")
|
| 199 |
|
| 200 |
-
# --- Step 3: Parse and Return Duration ---
|
| 201 |
if not duration_str:
|
| 202 |
-
raise ValueError("FFprobe returned an empty string.")
|
| 203 |
|
| 204 |
duration = float(duration_str)
|
| 205 |
return JSONResponse(content={"duration_seconds": duration})
|
| 206 |
|
| 207 |
except ValueError:
|
| 208 |
-
# This catches errors from float() if ffprobe's output is not a number
|
| 209 |
raise HTTPException(status_code=400, detail="Could not parse a valid duration from the media file.")
|
| 210 |
-
|
| 211 |
except Exception as e:
|
| 212 |
-
#
|
| 213 |
-
print(f"An unexpected error occurred in get_duration: {e}")
|
| 214 |
raise HTTPException(status_code=500, detail=f"An internal server error occurred: {str(e)}")
|
| 215 |
-
|
| 216 |
finally:
|
| 217 |
-
# --- Final Step: Cleanup ---
|
| 218 |
-
# This block ALWAYS runs, ensuring no temporary files are left behind.
|
| 219 |
if os.path.exists(temp_input_path):
|
| 220 |
os.remove(temp_input_path)
|
| 221 |
-
print(f"Cleaned up temporary file: {temp_input_path}")
|
| 222 |
|
| 223 |
# ==============================================================================
|
| 224 |
# 4. ENDPOINT PARA ADICIONAR LEGENDAS
|
|
|
|
| 48 |
shutil.rmtree(directory_path, ignore_errors=True)
|
| 49 |
print(f"--- CLEANUP: Successfully removed {directory_path} ---")
|
| 50 |
|
| 51 |
+
async def run_subprocess(command: str) -> (str, str):
|
| 52 |
+
"""
|
| 53 |
+
Executes a shell command asynchronously and returns its outputs.
|
| 54 |
+
Raises an exception if the command fails.
|
| 55 |
+
|
| 56 |
+
Args:
|
| 57 |
+
command (str): The shell command to execute.
|
| 58 |
+
|
| 59 |
+
Returns:
|
| 60 |
+
tuple[str, str]: A tuple containing (stdout, stderr).
|
| 61 |
+
|
| 62 |
+
Raises:
|
| 63 |
+
RuntimeError: If the command returns a non-zero exit code.
|
| 64 |
+
"""
|
| 65 |
print(f"Executing command: {command}")
|
| 66 |
process = await asyncio.create_subprocess_shell(
|
| 67 |
+
command,
|
| 68 |
+
stdout=asyncio.subprocess.PIPE,
|
| 69 |
+
stderr=asyncio.subprocess.PIPE
|
| 70 |
)
|
| 71 |
stdout, stderr = await process.communicate()
|
| 72 |
|
| 73 |
if process.returncode != 0:
|
| 74 |
error_message = stderr.decode(errors='ignore')
|
| 75 |
+
print(f"ERROR: Command failed with exit code {process.returncode}\nStderr: {error_message}")
|
| 76 |
+
# Levanta uma exceção para que a função que a chamou possa tratá-la.
|
| 77 |
raise RuntimeError(f"FFmpeg command failed: {error_message}")
|
| 78 |
|
| 79 |
+
print("SUCCESS: Command executed.")
|
| 80 |
+
# Retorna os outputs para a função que a chamou.
|
| 81 |
+
return stdout.decode(errors='ignore'), stderr.decode(errors='ignore')
|
| 82 |
|
| 83 |
# ==============================================================================
|
| 84 |
# 3. API ENDPOINTS
|
|
|
|
| 161 |
async def get_duration(file: UploadFile = File(...)):
|
| 162 |
"""
|
| 163 |
Calculates the duration of a media file using FFprobe.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 164 |
"""
|
|
|
|
|
|
|
| 165 |
unique_id = str(uuid.uuid4())
|
| 166 |
temp_input_path = os.path.join(TEMP_DIR, f"{unique_id}_{file.filename}")
|
| 167 |
|
| 168 |
try:
|
| 169 |
+
with open(temp_input_path, "wb") as buffer:
|
| 170 |
+
shutil.copyfileobj(file.file, buffer)
|
| 171 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
quoted_path = shlex.quote(temp_input_path)
|
|
|
|
|
|
|
| 173 |
command = f"ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {quoted_path}"
|
| 174 |
|
| 175 |
+
# Agora, run_subprocess retorna o stdout se for bem-sucedido.
|
| 176 |
+
stdout, _ = await run_subprocess(command)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
+
duration_str = stdout.strip()
|
| 179 |
print(f"FFprobe output (duration): '{duration_str}'")
|
| 180 |
|
|
|
|
| 181 |
if not duration_str:
|
| 182 |
+
raise ValueError("FFprobe returned an empty string for duration.")
|
| 183 |
|
| 184 |
duration = float(duration_str)
|
| 185 |
return JSONResponse(content={"duration_seconds": duration})
|
| 186 |
|
| 187 |
except ValueError:
|
|
|
|
| 188 |
raise HTTPException(status_code=400, detail="Could not parse a valid duration from the media file.")
|
|
|
|
| 189 |
except Exception as e:
|
| 190 |
+
# Captura qualquer outro erro, incluindo o RuntimeError do run_subprocess.
|
|
|
|
| 191 |
raise HTTPException(status_code=500, detail=f"An internal server error occurred: {str(e)}")
|
|
|
|
| 192 |
finally:
|
|
|
|
|
|
|
| 193 |
if os.path.exists(temp_input_path):
|
| 194 |
os.remove(temp_input_path)
|
|
|
|
| 195 |
|
| 196 |
# ==============================================================================
|
| 197 |
# 4. ENDPOINT PARA ADICIONAR LEGENDAS
|