zhalice2011 commited on
Commit
0b9e296
·
verified ·
1 Parent(s): 7ec0d0d

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. Dockerfile +14 -0
  2. README.md +4 -6
  3. main.py +173 -0
  4. requirements.txt +4 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ RUN apt-get update && \
4
+ apt-get install -y --no-install-recommends ffmpeg && \
5
+ rm -rf /var/lib/apt/lists/*
6
+
7
+ WORKDIR /app
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir -r requirements.txt
10
+
11
+ COPY main.py .
12
+
13
+ EXPOSE 7860
14
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,8 @@
1
  ---
2
  title: Media Processor
3
- emoji: 🌖
4
- colorFrom: green
5
- colorTo: blue
6
  sdk: docker
7
- pinned: false
8
  ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
  title: Media Processor
3
+ emoji: 🎬
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ app_port: 7860
8
  ---
 
 
main.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import subprocess
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ import httpx
8
+ from fastapi import FastAPI, HTTPException
9
+ from pydantic import BaseModel
10
+
11
+ app = FastAPI(title="Media Processor")
12
+
13
+
14
+ class ProcessRequest(BaseModel):
15
+ asset_id: str
16
+ video_url: str
17
+ api_url: str
18
+ api_key: str
19
+
20
+
21
+ @app.get("/health")
22
+ async def health():
23
+ ffmpeg_ok = subprocess.run(
24
+ ["ffmpeg", "-version"], capture_output=True
25
+ ).returncode == 0
26
+ return {"status": "ok", "ffmpeg": ffmpeg_ok}
27
+
28
+
29
+ @app.post("/process")
30
+ async def process_video(req: ProcessRequest):
31
+ tmp_dir = tempfile.mkdtemp()
32
+ video_path = Path(tmp_dir) / f"{req.asset_id}.mp4"
33
+ cover_path = Path(tmp_dir) / f"{req.asset_id}_cover.jpg"
34
+
35
+ try:
36
+ # 1. Download video
37
+ async with httpx.AsyncClient(timeout=120) as client:
38
+ async with client.stream("GET", req.video_url) as resp:
39
+ if resp.status_code != 200:
40
+ raise HTTPException(
41
+ status_code=400,
42
+ detail=f"Failed to download video: {resp.status_code}",
43
+ )
44
+ with open(video_path, "wb") as f:
45
+ async for chunk in resp.aiter_bytes(chunk_size=65536):
46
+ f.write(chunk)
47
+
48
+ # 2. Extract metadata with ffprobe
49
+ probe_result = subprocess.run(
50
+ [
51
+ "ffprobe", "-v", "quiet",
52
+ "-print_format", "json",
53
+ "-show_format", "-show_streams",
54
+ str(video_path),
55
+ ],
56
+ capture_output=True, text=True,
57
+ )
58
+ if probe_result.returncode != 0:
59
+ raise HTTPException(status_code=500, detail="ffprobe failed")
60
+
61
+ probe_data = json.loads(probe_result.stdout)
62
+ video_stream = next(
63
+ (s for s in probe_data.get("streams", []) if s.get("codec_type") == "video"),
64
+ None,
65
+ )
66
+ audio_stream = next(
67
+ (s for s in probe_data.get("streams", []) if s.get("codec_type") == "audio"),
68
+ None,
69
+ )
70
+ fmt = probe_data.get("format", {})
71
+
72
+ duration = float(fmt.get("duration", 0))
73
+ width = int(video_stream.get("width", 0)) if video_stream else 0
74
+ height = int(video_stream.get("height", 0)) if video_stream else 0
75
+
76
+ # Parse fps from r_frame_rate (e.g. "30/1")
77
+ fps = 0.0
78
+ if video_stream and video_stream.get("r_frame_rate"):
79
+ parts = video_stream["r_frame_rate"].split("/")
80
+ if len(parts) == 2 and int(parts[1]) != 0:
81
+ fps = round(int(parts[0]) / int(parts[1]), 2)
82
+
83
+ media_info = {
84
+ "format": fmt.get("format_name", ""),
85
+ "video_codec": video_stream.get("codec_name", "") if video_stream else "",
86
+ "audio_codec": audio_stream.get("codec_name", "") if audio_stream else "",
87
+ "bitrate": int(fmt.get("bit_rate", 0)),
88
+ "fps": fps,
89
+ }
90
+
91
+ # 3. Extract cover frame
92
+ seek_time = "1" if duration > 1 else "0"
93
+ ffmpeg_result = subprocess.run(
94
+ [
95
+ "ffmpeg", "-ss", seek_time,
96
+ "-i", str(video_path),
97
+ "-frames:v", "1", "-q:v", "2",
98
+ str(cover_path),
99
+ ],
100
+ capture_output=True,
101
+ )
102
+ if ffmpeg_result.returncode != 0:
103
+ # Fallback to 0s
104
+ subprocess.run(
105
+ [
106
+ "ffmpeg", "-ss", "0",
107
+ "-i", str(video_path),
108
+ "-frames:v", "1", "-q:v", "2",
109
+ str(cover_path),
110
+ ],
111
+ capture_output=True,
112
+ )
113
+
114
+ cover_url = None
115
+ if cover_path.exists():
116
+ # 4. Upload cover to asset-service
117
+ async with httpx.AsyncClient(timeout=60) as client:
118
+ with open(cover_path, "rb") as f:
119
+ upload_resp = await client.post(
120
+ f"{req.api_url}/api/assets/upload",
121
+ headers={"Authorization": f"Bearer {req.api_key}"},
122
+ files={"file": (f"{req.asset_id}_cover.jpg", f, "image/jpeg")},
123
+ data={
124
+ "type": "image",
125
+ "title": f"{req.asset_id}_cover",
126
+ "app": "media_processor",
127
+ },
128
+ )
129
+ if upload_resp.status_code in (200, 201):
130
+ cover_asset = upload_resp.json().get("asset", {})
131
+ cover_id = cover_asset.get("id")
132
+ if cover_id:
133
+ cover_url = f"{req.api_url}/api/assets/{cover_id}/file"
134
+
135
+ # 5. Update original asset with metadata
136
+ async with httpx.AsyncClient(timeout=30) as client:
137
+ update_body = {
138
+ "asset_details": {
139
+ "cover_url": cover_url,
140
+ "duration": duration,
141
+ "width": width,
142
+ "height": height,
143
+ "media_info": json.dumps(media_info),
144
+ }
145
+ }
146
+ await client.put(
147
+ f"{req.api_url}/api/assets/{req.asset_id}",
148
+ headers={
149
+ "Authorization": f"Bearer {req.api_key}",
150
+ "Content-Type": "application/json",
151
+ },
152
+ json=update_body,
153
+ )
154
+
155
+ return {
156
+ "success": True,
157
+ "cover_url": cover_url,
158
+ "duration": duration,
159
+ "width": width,
160
+ "height": height,
161
+ "media_info": media_info,
162
+ }
163
+
164
+ except HTTPException:
165
+ raise
166
+ except Exception as e:
167
+ return {"success": False, "error": str(e)}
168
+ finally:
169
+ # 6. Cleanup
170
+ for p in (video_path, cover_path):
171
+ if p.exists():
172
+ p.unlink()
173
+ Path(tmp_dir).rmdir() if Path(tmp_dir).exists() else None
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn==0.32.0
3
+ httpx==0.28.0
4
+ python-multipart==0.0.18