sharktide commited on
Commit
2c6dd2f
·
verified ·
1 Parent(s): 0004b0e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +7 -840
app.py CHANGED
@@ -28,12 +28,7 @@ from helper.subscriptions import (
28
  )
29
  from typing import Optional
30
  from helper.keywords import *
31
- from helper.assets import (
32
- save_base64_image,
33
- cleanup_image,
34
- is_base64_image,
35
- asset_router,
36
- )
37
 
38
  from helper.ratelimit import (
39
  enforce_rate_limit,
@@ -55,6 +50,9 @@ from helper.ratelimit import (
55
  get_usage_snapshot_for_subject,
56
  )
57
 
 
 
 
58
  app = FastAPI()
59
 
60
  WEBSOCKET_KEY = os.getenv("WEBSOCKET_KEY")
@@ -71,7 +69,10 @@ app.add_middleware(
71
  allow_methods=["GET", "POST", "HEAD"],
72
  allow_headers=["*"],
73
  )
 
74
  app.include_router(asset_router)
 
 
75
 
76
  def check_ws_auth_rate_limit(ip: str):
77
  now = time.time()
@@ -98,250 +99,6 @@ async def reroute_to_home():
98
  OLLAMA_LIBRARY_URL = "https://ollama.com/library"
99
 
100
 
101
- def is_complex_reasoning(prompt: str) -> bool:
102
- if len(prompt) > 800:
103
- return True
104
-
105
- for kw in REASONING_KEYWORDS:
106
- if kw in prompt:
107
- return True
108
-
109
- if re.search(r"\b(if|therefore|assume|let x|given that)\b", prompt):
110
- return True
111
-
112
- return False
113
-
114
-
115
- def is_lightweight(prompt: str) -> bool:
116
- if len(prompt) < 100:
117
- for kw in LIGHTWEIGHT_KEYWORDS:
118
- if kw in prompt:
119
- return True
120
- return False
121
-
122
-
123
- def is_cinematic_image_prompt(prompt: str) -> bool:
124
- for kw in CREATIVE_KEYWORDS:
125
- if kw in prompt.lower():
126
- return True
127
- return False
128
-
129
- PKEY = os.getenv("POLLINATIONS_KEY", "")
130
- PKEY2 = os.getenv("POLLINATIONS2_KEY", "")
131
- PKEY3 = os.getenv("POLLINATIONS3_KEY", "")
132
-
133
- GROQ_TOOL_MODELS = [
134
- "openai/gpt-oss-120b",
135
- "openai/gpt-oss-20b",
136
- "meta-llama/llama-4-scout-17b-16e-instruct",
137
- "qwen/qwen3-32b",
138
- "moonshotai/kimi-k2-instruct",
139
- ]
140
-
141
- GROQ_NORMAL_MODELS = [
142
- "llama-3.1-8b-instant",
143
- "llama-3.3-70b-versatile",
144
- "meta-llama/llama-4-maverick-17b-128e-instruct",
145
- "meta-llama/llama-guard-4-12b",
146
- "openai/gpt-oss-safeguard-20b",
147
- "qwen/qwen3-32b",
148
- ]
149
-
150
- CEREBRAS_MODELS = [
151
- "gpt-oss-120b",
152
- "llama3.1-8b",
153
- "qwen-3-235b-a22b-instruct-2507",
154
- "zai-glm-4.7",
155
- ]
156
-
157
-
158
- async def check_chat_rate_limit(
159
- request: Request,
160
- authorization: Optional[str],
161
- client_id: Optional[str] = None,
162
- ):
163
- return await enforce_rate_limit(request, authorization, "cloudChatDaily", client_id)
164
-
165
-
166
- @app.head("/status/sfx")
167
- async def head_sfx():
168
- return Response(
169
- status_code=200,
170
- headers={
171
- "Content-Type": "audio/mpeg",
172
- "Accept-Ranges": "bytes",
173
- },
174
- )
175
-
176
-
177
- @app.head("/status/image")
178
- async def head_image():
179
- return Response(
180
- status_code=200,
181
- headers={
182
- "Content-Type": "image/jpeg",
183
- "Accept-Ranges": "bytes",
184
- },
185
- )
186
-
187
-
188
- @app.head("/status/video")
189
- async def head_video():
190
- return Response(
191
- status_code=200,
192
- headers={
193
- "Content-Type": "video/mp4",
194
- "Accept-Ranges": "bytes",
195
- },
196
- )
197
-
198
-
199
- @app.head("/status/text")
200
- async def head_text():
201
- return Response(
202
- status_code=200,
203
- headers={
204
- "Content-Type": "application/json",
205
- "Accept-Ranges": "bytes",
206
- },
207
- )
208
-
209
-
210
- @app.get("/status")
211
- async def get_status():
212
- notify = ""
213
- services = {
214
- "Video Generation": {"code": 200, "state": "ok", "message": "Running Normally"},
215
- "Image Generation": {"code": 200, "state": "ok", "message": "Running Normally"},
216
- "Lightning-Text v2": {
217
- "code": 200,
218
- "state": "ok",
219
- "message": "Running normally",
220
- },
221
- "Music/SFX Generation": {
222
- "code": 200,
223
- "state": "ok",
224
- "message": "Running normally",
225
- },
226
- }
227
-
228
- overall_state = (
229
- "ok" if all(s["state"] == "ok" for s in services.values()) else "degraded"
230
- )
231
-
232
- return JSONResponse(
233
- status_code=200,
234
- content={
235
- "state": overall_state,
236
- "services": services,
237
- "notifications": notify,
238
- "latest": "2.9.1",
239
- },
240
- )
241
-
242
- @app.post("/gen/image")
243
- @app.get("/genimg/{prompt}")
244
- async def generate_image(
245
- request: Request,
246
- prompt: str = None,
247
- authorization: Optional[str] = Header(None),
248
- x_client_id: Optional[str] = Header(None),
249
- ):
250
- """
251
- Image generation endpoint.
252
- --------------------------------------------------------------
253
- • Accepts a plain‑text prompt (GET or JSON body).
254
- • Optional JSON fields:
255
- - mode: "fantasy" | "realistic" (keeps current behaviour)
256
- - image_urls: list of up to 2 image URLs or base‑64 strings
257
- • If *any* image is supplied we always use the Pollinations
258
- model **flux-2-dev** (the “editing” model). Otherwise the
259
- original heuristic (flux / zimage) is retained.
260
- • Base‑64 images are saved temporarily with the helper
261
- `save_base64_image` and served from the asset CDN exactly
262
- like the video endpoint does.
263
- --------------------------------------------------------------
264
- """
265
- timeout = httpx.Timeout(300.0, read=300.0)
266
- payload: Dict[str, Any] = {}
267
-
268
- if prompt is None:
269
- payload = await request.json()
270
- prompt = payload.get("prompt")
271
- mode = payload.get("mode")
272
- image_urls = payload.get("image_urls")
273
- else:
274
- mode = request.query_params.get("mode")
275
- image_urls = request.query_params.getlist("image_urls")
276
- payload = {}
277
-
278
- prompt = normalize_prompt_value(prompt, "prompt")
279
- enforce_prompt_size(
280
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Image prompt"
281
- )
282
- await check_image_rate_limit(request, authorization, x_client_id)
283
-
284
- chosen_model = "zimage"
285
- if is_cinematic_image_prompt(prompt):
286
- chosen_model = "flux"
287
-
288
- if isinstance(mode, str):
289
- normalized_mode = mode.strip().lower()
290
- if normalized_mode == "fantasy":
291
- chosen_model = "flux"
292
- elif normalized_mode == "realistic":
293
- chosen_model = "zimage"
294
-
295
- has_input_image = False
296
- temp_assets: List[str] = []
297
- if image_urls:
298
- if not isinstance(image_urls, list):
299
- raise HTTPException(400, "image_urls must be a list")
300
- if len(image_urls) > 4:
301
- raise HTTPException(400, "Maximum of four image URLs allowed")
302
- has_input_image = True
303
-
304
- if has_input_image:
305
- chosen_model = "klein"
306
-
307
- params = {
308
- "model": chosen_model,
309
- "key": PKEY2,
310
- }
311
-
312
- if has_input_image:
313
- processed_urls: List[str] = []
314
- for img in image_urls[:2]:
315
- if is_base64_image(img):
316
- image_id = save_base64_image(img)
317
- temp_assets.append(image_id)
318
- served_url = f"{request.base_url}asset-cdn/assets/{image_id}"
319
- processed_urls.append(served_url)
320
- else:
321
- processed_urls.append(img)
322
-
323
- params["image"] = "|".join(processed_urls)
324
-
325
- encoded_prompt = quote(prompt, safe="")
326
- query_string = "&".join(f"{k}={quote(str(v), safe='')}" for k, v in params.items())
327
- url = f"https://gen.pollinations.ai/image/{encoded_prompt}?{query_string}"
328
-
329
- try:
330
- async with httpx.AsyncClient(timeout=timeout) as client:
331
- response = await client.get(url)
332
- finally:
333
- for aid in temp_assets:
334
- cleanup_image(aid)
335
-
336
- if response.status_code != 200:
337
- raise HTTPException(
338
- status_code=500,
339
- detail=f"Pollinations error: {response.status_code}",
340
- )
341
-
342
- return Response(content=response.content, media_type="image/jpeg")
343
-
344
-
345
  @app.head("/models")
346
  @app.get("/models")
347
  async def get_models() -> List[Dict]:
@@ -382,591 +139,6 @@ async def get_models() -> List[Dict]:
382
 
383
  return models
384
 
385
-
386
- @app.post("/gen/chat/completions")
387
- async def generate_text(
388
- request: Request,
389
- authorization: Optional[str] = Header(None),
390
- x_client_id: Optional[str] = Header(None),
391
- ):
392
- body = await request.json()
393
- messages = body.get("messages", [])
394
- if not isinstance(messages, list) or len(messages) == 0:
395
- raise HTTPException(400, "messages[] is required")
396
-
397
- total_chars, total_bytes = calculate_messages_size(messages)
398
- # if total_chars > MAX_CHAT_PROMPT_CHARS or total_bytes > MAX_CHAT_PROMPT_BYTES:
399
- # raise HTTPException(
400
- # status_code=413,
401
- # detail=(
402
- # f"Prompt context too large ({total_chars} chars, {total_bytes} bytes). "
403
- # f"Max allowed is {MAX_CHAT_PROMPT_CHARS} chars or {MAX_CHAT_PROMPT_BYTES} bytes."
404
- # ),
405
- # )
406
-
407
- prompt_text = extract_user_text(messages)
408
-
409
- uses_tools = (
410
- "tools" in body and isinstance(body["tools"], list) and len(body["tools"]) > 0
411
- ) or ("tool_choice" in body and body["tool_choice"] not in [None, "none"])
412
-
413
- long_context = is_long_context(messages)
414
- code_present = contains_code(prompt_text)
415
- math_heavy = is_math_heavy(prompt_text)
416
- structured_task = is_structured_task(prompt_text)
417
- multi_q = multiple_questions(prompt_text)
418
- code_heavy = is_code_heavy(prompt_text, code_present, long_context)
419
-
420
- score = 0
421
-
422
- if long_context:
423
- score += 3
424
-
425
- if math_heavy:
426
- score += 3
427
-
428
- if structured_task:
429
- score += 2
430
-
431
- if code_present:
432
- score += 2
433
-
434
- if multi_q:
435
- score += 1
436
-
437
- for kw in REASONING_KEYWORDS:
438
- if kw in prompt_text:
439
- score += 1
440
-
441
- chosen_model = "llama-3.1-8b-instant"
442
- provider = "groq"
443
- has_images = contains_images(messages)
444
-
445
- if has_images:
446
- chosen_model = "meta-llama/llama-4-scout-17b-16e-instruct"
447
- provider = "groq"
448
- else:
449
- if score > 10:
450
- score = 10
451
- if uses_tools:
452
- if score >= 4:
453
- chosen_model = "openai/gpt-oss-120b"
454
- else:
455
- chosen_model = "openai/gpt-oss-20b"
456
- provider = "groq"
457
-
458
- elif code_present:
459
-
460
- if code_heavy and score >= 6:
461
- chosen_model = "qwen-3-235b-a22b-instruct-2507"
462
- provider = "cerebras"
463
-
464
- elif score >= 4:
465
- chosen_model = "llama-3.3-70b-versatile"
466
- provider = "groq"
467
-
468
- elif score >= 4:
469
- chosen_model = "meta-llama/llama-4-scout-17b-16e-instruct"
470
- provider = "groq"
471
-
472
- if provider == "groq" and (
473
- total_chars > MAX_GROQ_PROMPT_CHARS or total_bytes > MAX_GROQ_PROMPT_BYTES
474
- ):
475
- provider = "cerebras"
476
- chosen_model = "qwen-3-235b-a22b-instruct-2507"
477
-
478
- await check_chat_rate_limit(request, authorization, x_client_id)
479
-
480
- body["model"] = chosen_model
481
- print(
482
- f"""
483
- [ADVANCED ROUTER]
484
- Score: {score}
485
- Uses tools: {uses_tools}
486
- Long context: {long_context}
487
- Code present: {code_present}
488
- Math heavy: {math_heavy}
489
- Structured: {structured_task}
490
- Multi-question: {multi_q}
491
- MULTIMODAL REQUIRED: {has_images}
492
- → Selected: {chosen_model} ({provider})
493
- """
494
- )
495
-
496
- stream = body.get("stream", False)
497
-
498
- if provider == "groq":
499
- groq_keys = os.getenv("GROQ_KEY", "")
500
- print(f"ENV VAR: {groq_keys}")
501
- groq_keys_list = [k.strip() for k in groq_keys.split(",") if k.strip()]
502
- print(f"PARSED ENV VAR LIST: {groq_keys_list}")
503
- if not groq_keys_list:
504
- raise HTTPException(500, "Missing GROQ_KEY(s)")
505
- API_KEY = random.choice(groq_keys_list)
506
- print(f"SELECTED API KEY: {API_KEY}")
507
- url = "https://api.groq.com/openai/v1/chat/completions"
508
-
509
- elif provider == "cerebras":
510
- cer_keys = os.getenv("CER_KEY", "")
511
- cer_keys_list = [k.strip() for k in cer_keys.split(",") if k.strip()]
512
- if not cer_keys_list:
513
- raise HTTPException(500, "Missing CER_KEY(s)")
514
- API_KEY = random.choice(cer_keys_list)
515
-
516
- url = "https://api.cerebras.ai/v1/chat/completions"
517
-
518
- else:
519
- raise HTTPException(500, "Unknown provider routing error")
520
-
521
- headers = {"Authorization": f"Bearer {API_KEY}"}
522
-
523
- if stream:
524
- body["stream"] = True
525
-
526
- async def event_generator():
527
- try:
528
- async with httpx.AsyncClient(timeout=None) as client:
529
- async with client.stream(
530
- "POST",
531
- url,
532
- json=body,
533
- headers=headers,
534
- ) as r:
535
- if r.status_code >= 400:
536
- error_payload = ""
537
- try:
538
- error_payload = (
539
- (await r.aread()).decode("utf-8", errors="replace")
540
- )[:800]
541
- except Exception:
542
- error_payload = ""
543
- safe_error_payload = (
544
- error_payload.replace("\\", "\\\\")
545
- .replace('"', '\\"')
546
- .replace("\n", " ")
547
- .replace("\r", " ")
548
- )
549
- yield (
550
- 'data: {"error": '
551
- f'"Upstream provider error ({r.status_code}): {safe_error_payload}"'
552
- "}\n\n"
553
- )
554
- return
555
-
556
- async for line in r.aiter_lines():
557
- if line == "":
558
- yield "\n"
559
- continue
560
-
561
- yield line + "\n"
562
-
563
- except asyncio.CancelledError:
564
- return
565
- except Exception as e:
566
- yield f'data: {{"error": "{str(e)}"}}\n\n'
567
-
568
- return StreamingResponse(
569
- event_generator(),
570
- media_type="text/event-stream",
571
- headers={
572
- "Cache-Control": "no-cache",
573
- "Connection": "keep-alive",
574
- "X-Accel-Buffering": "no", # critical for nginx
575
- },
576
- )
577
- else:
578
- async with httpx.AsyncClient(timeout=None) as client:
579
- r = await client.post(url, json=body, headers=headers)
580
- content_type = (r.headers.get("content-type") or "").lower()
581
- if "application/json" in content_type:
582
- try:
583
- payload = r.json()
584
- except Exception:
585
- payload = {"error": "Upstream returned invalid JSON"}
586
- else:
587
- payload = {
588
- "error": "Upstream returned non-JSON response",
589
- "status_code": r.status_code,
590
- "message": r.text[:1000],
591
- }
592
-
593
- return JSONResponse(status_code=r.status_code, content=payload)
594
-
595
- raise HTTPException(500, "Unknown provider routing error")
596
-
597
-
598
- @app.get("/gen/sfx/{prompt}")
599
- @app.post("/gen/sfx")
600
- async def gensfx(
601
- request: Request,
602
- prompt: str = None,
603
- authorization: Optional[str] = Header(None),
604
- x_client_id: Optional[str] = Header(None),
605
- ):
606
- payload: Dict[str, Any] = {}
607
- if prompt is None:
608
- payload = await request.json()
609
- prompt = payload.get("prompt")
610
- prompt = normalize_prompt_value(prompt, "prompt")
611
- enforce_prompt_size(
612
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Audio prompt"
613
- )
614
- await check_audio_rate_limit(request, authorization, x_client_id)
615
- url = f"https://gen.pollinations.ai/audio/{prompt}?model=acestep&key={PKEY}"
616
- async with httpx.AsyncClient(timeout=None) as client:
617
- response = await client.get(url)
618
- body_text = ""
619
- try:
620
- body_text = response.text
621
- except Exception:
622
- pass
623
- if response.status_code != 200:
624
- return JSONResponse(
625
- status_code=response.status_code,
626
- content={
627
- "success": False,
628
- "error": "Upstream music/sfx generation failed",
629
- "status_code": response.status_code,
630
- "message": body_text[:1000],
631
- },
632
- )
633
- return Response(response.content, media_type="audio/mpeg")
634
-
635
-
636
- @app.get("/gen/tts/{prompt}")
637
- @app.post("/gen/tts")
638
- async def gensfx(
639
- request: Request,
640
- prompt: str = None,
641
- authorization: Optional[str] = Header(None),
642
- x_client_id: Optional[str] = Header(None),
643
- ):
644
- payload: Dict[str, Any] = {}
645
- if prompt is None:
646
- payload = await request.json()
647
- prompt = payload.get("prompt")
648
- prompt = normalize_prompt_value(prompt, "prompt")
649
- enforce_prompt_size(
650
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Audio prompt"
651
- )
652
- await check_audio_rate_limit(request, authorization, x_client_id)
653
- url = f"https://gen.pollinations.ai/audio/{prompt}?key={PKEY3}"
654
- async with httpx.AsyncClient(timeout=None) as client:
655
- response = await client.get(url)
656
- body_text = ""
657
- try:
658
- body_text = response.text
659
- except Exception:
660
- pass
661
- if response.status_code != 200:
662
- return JSONResponse(
663
- status_code=response.status_code,
664
- content={
665
- "success": False,
666
- "error": "Upstream audio generation failed",
667
- "status_code": response.status_code,
668
- "message": body_text[:1000],
669
- },
670
- )
671
- return Response(response.content, media_type="audio/mpeg")
672
-
673
-
674
- @app.get("/gen/video/{prompt}")
675
- @app.post("/gen/video")
676
- @app.head("/gen/video")
677
- async def genvideo_airforce(
678
- request: Request,
679
- prompt: str = None,
680
- authorization: Optional[str] = Header(None),
681
- x_client_id: Optional[str] = Header(None),
682
- ):
683
- if request.method == "HEAD":
684
- return Response(
685
- status_code=200,
686
- headers={
687
- "Y-prompt": "string — required. The text prompt used to generate the video.",
688
- "Y-ratio": "string — optional. Aspect ratio of the output video.",
689
- "Y-ratio-values": "3:2,2:3,1:1",
690
- "Y-ratio-default": "3:2",
691
- "Y-mode": "string — optional. Controls generation style.",
692
- "Y-mode-values": "normal,fun",
693
- "Y-mode-default": "normal",
694
- "Y-duration": "integer — optional. Duration in seconds (1–10).",
695
- "Y-duration-default": "5",
696
- "Y-image_urls": "array<string> — optional. Up to 2 image URLs for conditioning.",
697
- "Y-image_urls-max": "2",
698
- "Y-response_format": "video/mp4",
699
- "Y-model": "grok-video",
700
- },
701
- )
702
-
703
- aspectRatio = "3:2"
704
- inputMode = "normal"
705
- duration = 5
706
- image_urls = None
707
- ratio = None
708
- mode = None
709
-
710
- if prompt is None:
711
- user_body = await request.json()
712
- prompt = user_body.get("prompt")
713
- ratio = user_body.get("ratio")
714
- mode = user_body.get("mode")
715
- image_urls = user_body.get("image_urls")
716
- duration = user_body.get("duration", 5)
717
-
718
- if ratio not in valid_ratios:
719
- raise HTTPException(
720
- status_code=400,
721
- detail=f"Invalid aspect ratio '{ratio}'. Must be one of 3:2, 2:3, or 1:1.",
722
- )
723
- if ratio in ratios:
724
- aspectRatio = ratio
725
-
726
- if mode not in valid_modes:
727
- raise HTTPException(
728
- status_code=400,
729
- detail=f"Invalid mode '{mode}'. Must be 'normal' or 'fun'.",
730
- )
731
- if mode in modes:
732
- inputMode = mode
733
-
734
- if image_urls:
735
- if not isinstance(image_urls, list):
736
- raise HTTPException(400, "image_urls must be a list")
737
- if len(image_urls) > 2:
738
- raise HTTPException(400, "You may provide at most two image URLs")
739
-
740
- # Clamp duration
741
- try:
742
- duration = max(1, min(10, int(duration)))
743
- except (TypeError, ValueError):
744
- duration = 5
745
-
746
- prompt = normalize_prompt_value(prompt, "prompt")
747
- enforce_prompt_size(
748
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt"
749
- )
750
- await check_video_rate_limit(request, authorization, x_client_id)
751
-
752
- RATIO_MAP = {
753
- "3:2": "16:9",
754
- "2:3": "9:16",
755
- "1:1": "9:16",
756
- }
757
- pollinations_ratio = RATIO_MAP.get(aspectRatio, "16:9")
758
-
759
- encoded_prompt = quote(prompt, safe="")
760
- params = {
761
- "model": "ltx-2",
762
- "duration": duration,
763
- "aspectRatio": pollinations_ratio,
764
- "seed": -1,
765
- }
766
-
767
- temp_assets = []
768
-
769
- if image_urls:
770
- processed_urls = []
771
-
772
- for img in image_urls[:2]:
773
- if is_base64_image(img):
774
- image_id = save_base64_image(img)
775
- temp_assets.append(image_id)
776
-
777
- served_url = f"{request.base_url}asset-cdn/assets/{image_id}"
778
- processed_urls.append(served_url)
779
- else:
780
- processed_urls.append(img)
781
-
782
- params["image"] = "|".join(processed_urls)
783
-
784
- if inputMode == "fun":
785
- params["enhance"] = "true"
786
-
787
- query_string = "&".join(f"{k}={quote(str(v), safe='')}" for k, v in params.items())
788
- url = f"https://gen.pollinations.ai/image/{encoded_prompt}?{query_string}"
789
-
790
- print(f"[VIDEO GEN] Pollinations URL: {url}")
791
- url = url + f"&key={PKEY}"
792
- resp = None
793
- try:
794
- async with httpx.AsyncClient(timeout=600) as client:
795
- resp = await client.get(url)
796
- finally:
797
- for aid in temp_assets:
798
- cleanup_image(aid)
799
- if resp is None:
800
- raise HTTPException(502, "Video generation request failed")
801
- if resp.status_code != 200:
802
- body_text = ""
803
- try:
804
- body_text = resp.text
805
- except Exception:
806
- pass
807
- return JSONResponse(
808
- status_code=resp.status_code,
809
- content={
810
- "success": False,
811
- "error": "Upstream video generation failed",
812
- "status_code": resp.status_code,
813
- "message": body_text[:1000],
814
- },
815
- )
816
-
817
- if not resp.content:
818
- raise HTTPException(502, "Pollinations returned empty response")
819
-
820
- return Response(
821
- content=resp.content,
822
- media_type="video/mp4",
823
- headers={
824
- "Content-Length": str(len(resp.content)),
825
- "Accept-Ranges": "bytes",
826
- },
827
- )
828
-
829
-
830
- AIRFORCE_KEY = os.getenv("AIRFORCE")
831
- AIRFORCE_VIDEO_MODEL = "grok-imagine-video"
832
- AIRFORCE_API_URL = "https://api.airforce/v1/images/generations"
833
-
834
- valid_ratios = {"3:2", "2:3", "1:1", "", None}
835
- ratios = {"3:2", "2:3", "1:1"}
836
-
837
- valid_modes = {"normal", "fun", "", None}
838
- modes = {"normal", "fun"}
839
-
840
- MAX_VIDEO_RETRIES = 6
841
-
842
-
843
- @app.get("/gen/video/airforce/{prompt}")
844
- @app.post("/gen/video/airforce")
845
- @app.head("/gen/video/airforce")
846
- async def genvideo_airforce(
847
- request: Request,
848
- prompt: str = None,
849
- authorization: Optional[str] = Header(None),
850
- x_client_id: Optional[str] = Header(None),
851
- ):
852
- if request.method == "HEAD":
853
- return Response(
854
- status_code=200,
855
- headers={
856
- # Required field
857
- "Y-prompt": "string — required. The text prompt used to generate the video.",
858
- # Optional fields
859
- "Y-ratio": "string — optional. Aspect ratio of the output video.",
860
- "Y-ratio-values": "3:2,2:3,1:1",
861
- "Y-ratio-default": "3:2",
862
- "Y-mode": "string — optional. Controls generation style.",
863
- "Y-mode-values": "normal,fun",
864
- "Y-mode-default": "normal",
865
- "Y-duration": "integer — optional. Duration in seconds.",
866
- "Y-duration-default": "5",
867
- "Y-image_urls": "array<string> — optional. Up to 2 image URLs for conditioning.",
868
- "Y-image_urls-max": "2",
869
- # Response format
870
- "Y-response_format": "video/mp4",
871
- # Model info
872
- "Y-model": "grok-imagine-video",
873
- },
874
- )
875
-
876
- aspectRatio = "3:2"
877
- inputMode = "normal"
878
- image_urls = None
879
- ratio = None
880
- mode = None
881
-
882
- user_body = {}
883
- if prompt is None:
884
- user_body = await request.json()
885
- prompt = user_body.get("prompt")
886
- ratio = user_body.get("ratio")
887
- mode = user_body.get("mode")
888
- image_urls = user_body.get("image_urls")
889
-
890
- if ratio not in valid_ratios:
891
- raise HTTPException(
892
- status_code=400,
893
- detail=f"Invalid aspect ratio {ratio}. Must be one of 3:2, 2:3, or 1:1. Default is 3:2",
894
- )
895
- if ratio in ratios:
896
- aspectRatio = ratio
897
-
898
- if mode not in valid_modes:
899
- raise HTTPException(
900
- status_code=400,
901
- detail=f"Invalid mode {mode}. Must be 'normal' or 'fun'. Default is normal",
902
- )
903
- if mode in modes:
904
- inputMode = mode
905
-
906
- if image_urls:
907
- if not isinstance(image_urls, list):
908
- raise HTTPException(400, "image_urls must be a list")
909
-
910
- if len(image_urls) > 2:
911
- raise HTTPException(400, "You may provide at most two image URLs")
912
-
913
- prompt = normalize_prompt_value(prompt, "prompt")
914
- enforce_prompt_size(
915
- prompt, MAX_MEDIA_PROMPT_CHARS, MAX_MEDIA_PROMPT_BYTES, "Video prompt"
916
- )
917
- await check_video_rate_limit(request, authorization, x_client_id)
918
-
919
- payload = {
920
- "model": AIRFORCE_VIDEO_MODEL,
921
- "prompt": prompt,
922
- "n": 1,
923
- "size": "1024x1024",
924
- "response_format": "b64_json",
925
- "sse": False,
926
- "mode": inputMode,
927
- "aspectRatio": aspectRatio,
928
- }
929
-
930
- if image_urls:
931
- payload["image_urls"] = image_urls
932
-
933
- async with httpx.AsyncClient(timeout=600) as client:
934
- resp = await client.post(
935
- AIRFORCE_API_URL,
936
- headers={
937
- "Authorization": f"Bearer {AIRFORCE_KEY}",
938
- "Content-Type": "application/json",
939
- },
940
- json=payload,
941
- )
942
-
943
- if resp.status_code != 200:
944
- return JSONResponse(status_code=resp.status_code, content=resp.json())
945
-
946
- if not resp.content:
947
- raise HTTPException(502, "api.airforce returned empty response")
948
-
949
- try:
950
- result = resp.json()
951
- b64_video = result["data"][0]["b64_json"]
952
- except Exception:
953
- raise HTTPException(502, f"Invalid api.airforce response: {resp.text[:500]}")
954
-
955
- if not b64_video:
956
- raise HTTPException(502, "Airforce returned empty b64_json")
957
-
958
- video_bytes = base64.b64decode(b64_video)
959
-
960
- return Response(
961
- content=video_bytes,
962
- media_type="video/mp4",
963
- headers={
964
- "Content-Length": str(len(video_bytes)),
965
- "Accept-Ranges": "bytes",
966
- },
967
- )
968
-
969
-
970
  @app.get("/subscription")
971
  async def get_subscription(authorization: Optional[str] = Header(None)):
972
  if not authorization or not authorization.startswith("Bearer "):
@@ -1084,23 +256,19 @@ async def websocket_chat(ws: WebSocket):
1084
  await ws.close(code=4403)
1085
  return
1086
 
1087
- # Auth success
1088
  await ws.send_json({
1089
  "type": "auth",
1090
  "status": "ok"
1091
  })
1092
 
1093
- # Internal endpoint (avoids Hugging Face proxy redirect)
1094
  internal_url = "http://127.0.0.1:7860/gen/chat/completions"
1095
 
1096
- # Persistent HTTP client for streaming
1097
  async with httpx.AsyncClient(
1098
  timeout=None,
1099
  follow_redirects=False
1100
  ) as client:
1101
  request_counter = 0
1102
 
1103
- # Background task to handle incoming requests
1104
  async def handle_incoming_requests():
1105
  nonlocal request_counter
1106
  while True:
@@ -1220,4 +388,3 @@ async def redirect_to_protal(request: Request):
1220
  )
1221
  else:
1222
  return JSONResponse({"redirect_url": (base_url + "?prefilled_email=" + email)})
1223
-
 
28
  )
29
  from typing import Optional
30
  from helper.keywords import *
31
+ from helper.assets import asset_router
 
 
 
 
 
32
 
33
  from helper.ratelimit import (
34
  enforce_rate_limit,
 
50
  get_usage_snapshot_for_subject,
51
  )
52
 
53
+ from status import router as status_router
54
+ from gen import router as gen_router
55
+
56
  app = FastAPI()
57
 
58
  WEBSOCKET_KEY = os.getenv("WEBSOCKET_KEY")
 
69
  allow_methods=["GET", "POST", "HEAD"],
70
  allow_headers=["*"],
71
  )
72
+
73
  app.include_router(asset_router)
74
+ app.include_router(status_router)
75
+ app.include_router(gen_router)
76
 
77
  def check_ws_auth_rate_limit(ip: str):
78
  now = time.time()
 
99
  OLLAMA_LIBRARY_URL = "https://ollama.com/library"
100
 
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  @app.head("/models")
103
  @app.get("/models")
104
  async def get_models() -> List[Dict]:
 
139
 
140
  return models
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  @app.get("/subscription")
143
  async def get_subscription(authorization: Optional[str] = Header(None)):
144
  if not authorization or not authorization.startswith("Bearer "):
 
256
  await ws.close(code=4403)
257
  return
258
 
 
259
  await ws.send_json({
260
  "type": "auth",
261
  "status": "ok"
262
  })
263
 
 
264
  internal_url = "http://127.0.0.1:7860/gen/chat/completions"
265
 
 
266
  async with httpx.AsyncClient(
267
  timeout=None,
268
  follow_redirects=False
269
  ) as client:
270
  request_counter = 0
271
 
 
272
  async def handle_incoming_requests():
273
  nonlocal request_counter
274
  while True:
 
388
  )
389
  else:
390
  return JSONResponse({"redirect_url": (base_url + "?prefilled_email=" + email)})