FarelDeveloper commited on
Commit
1ddd8a3
·
verified ·
1 Parent(s): 00df5c8

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +95 -233
main.py CHANGED
@@ -1,270 +1,132 @@
1
- from fastapi import FastAPI, Query, Request
2
- from fastapi.responses import JSONResponse, FileResponse, Response, StreamingResponse
3
  from playwright.async_api import async_playwright
4
- import os, uuid, shutil, asyncio, time, stat
5
- from collections import defaultdict
6
 
7
  app = FastAPI(
8
- title="BRAT GEN - API",
9
- description="API for generating BRAT-style text images & videos.",
10
  version="1.0.0",
11
  )
12
 
13
- def _ensure_dir(path: str) -> str:
14
- """
15
- Ensures the directory can be created and written to. If it fails, falls back to /tmp.
16
- Sets 0777 permissions for safety in non-root containers.
17
- """
18
- try:
19
- os.makedirs(path, exist_ok=True)
20
- os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) # 0o777
21
- return path
22
- except Exception:
23
- fallback = os.path.join("/tmp", os.path.basename(path))
24
- os.makedirs(fallback, exist_ok=True)
25
- os.chmod(fallback, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
26
- return fallback
27
 
28
- OUTPUT_DIR = _ensure_dir(os.environ.get("OUTPUT_DIR", os.path.join(os.getcwd(), "output")))
29
- TMP_DIR = _ensure_dir(os.environ.get("TMP_DIR", os.path.join(os.getcwd(), "tmp_brat")))
30
 
31
- REQUEST_LIMIT = 10 # Max requests allowed
32
- TIME_WINDOW = 60 # Time window in seconds (1 minute)
33
- BAN_DURATION = 300 # Ban duration in seconds (5 minutes)
34
-
35
- request_logs: dict[str, list[float]] = defaultdict(list)
36
- banned_ips: dict[str, float] = {}
37
-
38
- def get_client_ip(request: Request) -> str:
39
- cf = request.headers.get("CF-Connecting-IP")
40
- if cf:
41
- return cf.strip()
42
- xff = request.headers.get("X-Forwarded-For")
43
- if xff:
44
- return xff.split(",")[0].strip()
45
- return (request.client.host or "").strip()
46
-
47
- @app.middleware("http")
48
- async def anti_ddos_middleware(request: Request, call_next):
49
- ip = get_client_ip(request)
50
- now = time.time()
51
-
52
- if ip in banned_ips:
53
- if now < banned_ips[ip]:
54
- return JSONResponse(
55
- status_code=429,
56
- content={"error": "IP is blocked for 5 minutes due to too many requests."},
57
- )
58
- else:
59
- del banned_ips[ip]
60
-
61
- window = [ts for ts in request_logs[ip] if now - ts < TIME_WINDOW]
62
- window.append(now)
63
- request_logs[ip] = window
64
-
65
- if len(request_logs[ip]) > REQUEST_LIMIT:
66
- banned_ips[ip] = now + BAN_DURATION
67
- return JSONResponse(
68
- status_code=429,
69
- content={"error": "Too many requests. IP is blocked for 5 minutes."},
70
- )
71
-
72
- return await call_next(request)
73
-
74
-
75
- async def delete_file_after_delay(filepath: str, delay: int = 600):
76
  await asyncio.sleep(delay)
77
- try:
78
- if os.path.exists(filepath):
79
- os.remove(filepath)
80
- except Exception:
81
- pass
82
-
83
- @app.get("/api/brat", tags=["MAIN"], summary="Generate BRAT text image")
84
- async def generate_brat(
85
- request: Request,
86
- text: str = Query(..., description="Text to be inserted into the BRAT image"),
87
- background: str | None = Query(None, description="Background color (e.g., #000000)"),
88
- color: str | None = Query(None, description="Text color (e.g., #FFFFFF)"),
89
- ):
90
- text = (text or "").strip()
91
  if not text:
92
- return JSONResponse(status_code=400, content={"error": "Text cannot be empty."})
93
-
94
- try:
95
- async with async_playwright() as p:
96
- browser = await p.chromium.launch(args=["--no-sandbox"])
97
- context = await browser.new_context(viewport={"width": 1536, "height": 695})
98
- page = await context.new_page()
99
- await page.goto("https://www.bratgenerator.com/", wait_until="domcontentloaded")
100
 
101
- try:
102
- await page.click("text=Accept", timeout=3000)
103
- except Exception:
104
- pass
105
 
106
- await page.click("#toggleButtonWhite")
107
- await page.click("#textOverlay")
108
- await page.click("#textInput")
109
- await page.fill("#textInput", text)
110
 
111
- await page.evaluate(
112
- """(data) => {
113
- if (data.background) $('.node__content.clearfix').css('background-color', data.background);
114
- if (data.color) $('.textFitted').css('color', data.color);
115
- }""",
116
- {"background": background, "color": color},
117
- )
118
 
119
- await asyncio.sleep(0.5)
 
 
 
120
 
121
- element = await page.query_selector("#textOverlay")
122
- if not element:
123
- await browser.close()
124
- return JSONResponse(status_code=500, content={"error": "Target element not found."})
125
- box = await element.bounding_box()
126
- if not box:
127
- await browser.close()
128
- return JSONResponse(status_code=500, content={"error": "Failed to read element bounding box."})
129
 
130
- filename = f"ditss-{uuid.uuid4().hex[:8]}.png"
131
- filepath = os.path.join(OUTPUT_DIR, filename)
 
132
 
133
- screenshot = await page.screenshot(
134
- clip={"x": box["x"], "y": box["y"], "width": 500, "height": 440}
135
- )
136
- with open(filepath, "wb") as f:
137
- f.write(screenshot)
138
 
139
- await context.close()
140
- await browser.close()
141
 
142
- asyncio.create_task(delete_file_after_delay(filepath))
143
- base_url = str(request.base_url).rstrip("/")
144
- return {"status": 200, "URL": f"{base_url}/download/file/{filename}"}
145
 
146
- except Exception as e:
147
- return JSONResponse(status_code=500, content={"error": f"Failed to generate image: {str(e)}"})
148
 
149
- @app.get("/api/bratvid", tags=["MAIN"], summary="Create animated video from BRAT text")
150
- async def generate_brat_video(
151
- request: Request,
152
- text: str = Query(..., description="Sentence to be animated (space-separated)"),
153
- background: str | None = Query(None, description="Background color (e.g., #000000)"),
154
- color: str | None = Query(None, description="Text color (e.g., #FFFFFF)"),
155
- ):
156
- text = (text or "").strip()
157
  if not text:
158
- return JSONResponse(status_code=400, content={"error": "Text cannot be empty."})
159
 
160
  words = text.split()
161
- if not words:
162
- return JSONResponse(status_code=400, content={"error": "Text must contain at least one word."})
163
-
164
- temp_dir = _ensure_dir(os.path.join(TMP_DIR, str(uuid.uuid4())))
165
-
166
- try:
167
- async with async_playwright() as p:
168
- browser = await p.chromium.launch(args=["--no-sandbox"])
169
- context = await browser.new_context(viewport={"width": 1536, "height": 695})
170
- page = await context.new_page()
171
- await page.goto("https://www.bratgenerator.com/", wait_until="domcontentloaded")
172
-
173
- try:
174
- await page.click("text=Accept", timeout=3000)
175
- except Exception:
176
- pass
177
-
178
- await page.click("#toggleButtonWhite")
179
- await page.click("#textOverlay")
180
- await page.click("#textInput")
181
-
182
- for i in range(len(words)):
183
- partial_text = " ".join(words[: i + 1])
184
- await page.fill("#textInput", partial_text)
185
 
186
- await page.evaluate(
187
- """(data) => {
188
- if (data.background) $('.node__content.clearfix').css('background-color', data.background);
189
- if (data.color) $('.textFitted').css('color', data.color);
190
- }""",
191
- {"background": background, "color": color},
192
- )
193
 
194
- await asyncio.sleep(0.2)
 
 
 
 
195
 
196
- element = await page.query_selector("#textOverlay")
197
- if not element:
198
- await context.close()
199
- await browser.close()
200
- shutil.rmtree(temp_dir, ignore_errors=True)
201
- return JSONResponse(status_code=500, content={"error": "Target element not found."})
202
- box = await element.bounding_box()
203
- if not box:
204
- await context.close()
205
- await browser.close()
206
- shutil.rmtree(temp_dir, ignore_errors=True)
207
- return JSONResponse(status_code=500, content={"error": "Failed to read element bounding box."})
208
 
209
- screenshot = await page.screenshot(
210
- clip={"x": box["x"], "y": box["y"], "width": 500, "height": 440}
211
- )
212
- frame_path = os.path.join(temp_dir, f"frame{i:03d}.png")
213
- with open(frame_path, "wb") as f:
214
- f.write(screenshot)
215
 
216
- await context.close()
217
- await browser.close()
218
-
219
- # Render video with ffmpeg
220
- output_filename = f"ditss-{uuid.uuid4().hex[:8]}.mp4"
221
- output_path = os.path.join(OUTPUT_DIR, output_filename)
222
 
223
- ffmpeg_cmd = [
224
- "ffmpeg",
225
- "-y",
226
- "-framerate", "1.428",
227
- "-i", os.path.join(temp_dir, "frame%03d.png"),
228
- "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2,fps=30",
229
- "-c:v", "libx264",
230
- "-preset", "ultrafast",
231
- "-pix_fmt", "yuv420p",
232
- output_path,
233
- ]
234
 
235
- process = await asyncio.create_subprocess_exec(
236
- *ffmpeg_cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
237
- )
238
- _, stderr = await process.communicate()
239
 
240
- if process.returncode != 0:
241
- shutil.rmtree(temp_dir, ignore_errors=True)
242
- return JSONResponse(status_code=500, content={"error": stderr.decode()})
243
 
244
- asyncio.create_task(delete_file_after_delay(output_path))
245
- base_url = str(request.base_url).rstrip("/")
246
- return {"status": 200, "URL": f"{base_url}/download/file/{output_filename}"}
247
 
248
- except Exception as e:
249
- return JSONResponse(status_code=500, content={"error": str(e)})
250
- finally:
251
- shutil.rmtree(temp_dir, ignore_errors=True)
 
 
 
 
252
 
253
- @app.get("/", summary="Root Endpoint", tags=["MISC"], description="Displaying the main page.")
254
- async def root():
255
- # take absolute path based on main.py file location
256
- html_path = os.path.join(os.path.dirname(__file__), "index.html")
 
 
257
 
258
- if os.path.exists(html_path):
259
- return FileResponse(html_path)
260
- return JSONResponse(status_code=404, content={"error": "index.html file not found"})
261
 
262
- @app.get("/download/file/{filename}", tags=["MISC"])
263
- async def download_file(filename: str):
264
- filepath = os.path.join(OUTPUT_DIR, filename)
265
- if not os.path.exists(filepath):
266
- return JSONResponse(status_code=404, content={"error": "File not found"})
267
-
268
- with open(filepath, "rb") as f:
269
- data = f.read()
270
- return Response(data, media_type="application/octet-stream")
 
1
+ from fastapi import FastAPI, Query
2
+ from fastapi.responses import FileResponse, JSONResponse
3
  from playwright.async_api import async_playwright
4
+ import os, uuid, asyncio, shutil, stat
 
5
 
6
  app = FastAPI(
7
+ title="Brat-Api Farel",
8
+ description="Free BRAT Image & Video Generator API",
9
  version="1.0.0",
10
  )
11
 
12
+ def ensure_dir(path: str):
13
+ os.makedirs(path, exist_ok=True)
14
+ os.chmod(path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
15
+ return path
 
 
 
 
 
 
 
 
 
 
16
 
17
+ OUTPUT_DIR = ensure_dir(os.path.join(os.getcwd(), "output"))
18
+ TMP_DIR = ensure_dir(os.path.join(os.getcwd(), "tmp"))
19
 
20
+ async def delete_later(path: str, delay: int = 300):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  await asyncio.sleep(delay)
22
+ if os.path.exists(path):
23
+ os.remove(path)
24
+
25
+ # =========================
26
+ # BRAT IMAGE
27
+ # =========================
28
+ @app.get("/api/brat")
29
+ async def brat_image(text: str = Query(...)):
30
+ text = text.strip()
 
 
 
 
 
31
  if not text:
32
+ return JSONResponse(status_code=400, content={"error": "text is required"})
 
 
 
 
 
 
 
33
 
34
+ filename = f"brat-{uuid.uuid4().hex}.png"
35
+ filepath = os.path.join(OUTPUT_DIR, filename)
 
 
36
 
37
+ async with async_playwright() as p:
38
+ browser = await p.chromium.launch(args=["--no-sandbox"])
39
+ context = await browser.new_context(viewport={"width": 1536, "height": 695})
40
+ page = await context.new_page()
41
 
42
+ await page.goto("https://www.bratgenerator.com/", wait_until="domcontentloaded")
43
+ try:
44
+ await page.click("text=Accept", timeout=3000)
45
+ except:
46
+ pass
 
 
47
 
48
+ await page.click("#toggleButtonWhite")
49
+ await page.click("#textOverlay")
50
+ await page.fill("#textInput", text)
51
+ await asyncio.sleep(0.4)
52
 
53
+ element = await page.query_selector("#textOverlay")
54
+ box = await element.bounding_box()
 
 
 
 
 
 
55
 
56
+ img = await page.screenshot(
57
+ clip={"x": box["x"], "y": box["y"], "width": 500, "height": 440}
58
+ )
59
 
60
+ with open(filepath, "wb") as f:
61
+ f.write(img)
 
 
 
62
 
63
+ await browser.close()
 
64
 
65
+ asyncio.create_task(delete_later(filepath))
 
 
66
 
67
+ return FileResponse(filepath, media_type="image/png", filename=filename)
 
68
 
69
+ # =========================
70
+ # BRAT VIDEO
71
+ # =========================
72
+ @app.get("/api/bratvid")
73
+ async def brat_video(text: str = Query(...)):
74
+ text = text.strip()
 
 
75
  if not text:
76
+ return JSONResponse(status_code=400, content={"error": "text is required"})
77
 
78
  words = text.split()
79
+ temp_dir = ensure_dir(os.path.join(TMP_DIR, uuid.uuid4().hex))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
 
81
+ async with async_playwright() as p:
82
+ browser = await p.chromium.launch(args=["--no-sandbox"])
83
+ context = await browser.new_context(viewport={"width": 1536, "height": 695})
84
+ page = await context.new_page()
 
 
 
85
 
86
+ await page.goto("https://www.bratgenerator.com/", wait_until="domcontentloaded")
87
+ try:
88
+ await page.click("text=Accept", timeout=3000)
89
+ except:
90
+ pass
91
 
92
+ await page.click("#toggleButtonWhite")
93
+ await page.click("#textOverlay")
 
 
 
 
 
 
 
 
 
 
94
 
95
+ for i in range(len(words)):
96
+ await page.fill("#textInput", " ".join(words[: i + 1]))
97
+ await asyncio.sleep(0.2)
 
 
 
98
 
99
+ element = await page.query_selector("#textOverlay")
100
+ box = await element.bounding_box()
 
 
 
 
101
 
102
+ img = await page.screenshot(
103
+ clip={"x": box["x"], "y": box["y"], "width": 500, "height": 440}
104
+ )
 
 
 
 
 
 
 
 
105
 
106
+ with open(os.path.join(temp_dir, f"frame{i:03d}.png"), "wb") as f:
107
+ f.write(img)
 
 
108
 
109
+ await browser.close()
 
 
110
 
111
+ output_video = os.path.join(OUTPUT_DIR, f"bratvid-{uuid.uuid4().hex}.mp4")
 
 
112
 
113
+ ffmpeg_cmd = [
114
+ "ffmpeg", "-y",
115
+ "-framerate", "1.4",
116
+ "-i", os.path.join(temp_dir, "frame%03d.png"),
117
+ "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2,fps=30",
118
+ "-pix_fmt", "yuv420p",
119
+ output_video
120
+ ]
121
 
122
+ process = await asyncio.create_subprocess_exec(
123
+ *ffmpeg_cmd,
124
+ stdout=asyncio.subprocess.DEVNULL,
125
+ stderr=asyncio.subprocess.DEVNULL
126
+ )
127
+ await process.wait()
128
 
129
+ shutil.rmtree(temp_dir, ignore_errors=True)
130
+ asyncio.create_task(delete_later(output_video))
 
131
 
132
+ return FileResponse(output_video, media_type="video/mp4", filename=os.path.basename(output_video))