imbecome commited on
Commit
3e38742
·
verified ·
1 Parent(s): 11b2481

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +252 -201
app.py CHANGED
@@ -1,40 +1,28 @@
1
- import uvicorn
2
- from fastapi import FastAPI, Request, Response, File, UploadFile, Form
3
- from fastapi.responses import JSONResponse, HTMLResponse
4
- from fastapi.middleware.cors import CORSMiddleware
5
- import httpx
6
  import random
7
  import re
8
- import os
9
- import base64
10
- import struct
11
  import shutil
12
  import tempfile
 
 
13
 
14
- # --- INISIALISASI NLP LOKAL (DistilBERT) ---
15
- MODEL_DIR = "/media/devmon/D A T A/project/models"
16
- DISTILBERT_PATH = os.path.join(MODEL_DIR, "distilbert")
17
 
18
- try:
19
- from transformers import pipeline
20
- if os.path.exists(DISTILBERT_PATH):
21
- nlp_engine = pipeline("sentiment-analysis", model=DISTILBERT_PATH, local_files_only=True)
22
- print("DistilBERT: Loaded from Disk.")
23
- else:
24
- nlp_engine = None
25
- print("DistilBERT: Folder not found.")
26
- except Exception as e:
27
- nlp_engine = None
28
- print(f"Transformers error: {e}")
29
 
30
- # --- INISIALISASI WHISPER LOKAL ---
31
- try:
32
- from faster_whisper import WhisperModel
33
- whisper_model = WhisperModel("tiny", device="cpu", compute_type="int8")
34
- print("Whisper-1: Local Engine Ready.")
35
- except Exception as e:
36
- whisper_model = None
37
- print(f"Whisper Local error (Switching to Cloud-Only Mode): {e}")
 
38
 
39
  app = FastAPI()
40
 
@@ -46,209 +34,272 @@ app.add_middleware(
46
  allow_headers=["*"],
47
  )
48
 
49
- # ================= KONFIGURASI API =================
50
- OLLAMA_URL = "http://127.0.0.1:11434/api/chat"
51
- LOCAL_MODEL = "deepseek-r1:1.5b"
52
- TIMEOUT = 900.0
53
-
54
- # 7 OpenRouter API Keys
55
- OR_KEYS = [
56
- "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",
57
- "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",
58
- "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",
59
- "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",
60
- "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",
61
- "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",
62
- "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"
63
- ]
64
-
65
- # 8 Cloudflare Accounts (ID & API Key)
66
- CF_ACCOUNTS = [
67
- {"id": "a3f0db8c35cad604bf7572d0303cf930", "token": "fnCi0UTHbygWNDraZut8EV3N4Tzmw-wagP33gmU2"},
68
- {"id": "ae24ffa3616016431a539f2c0ed3d884", "token": "nzpDfTUJw90I0cJQcePM58ZkGWrKruuudhR6L721"},
69
- {"id": "f0bc52c5fcd64c76b883965add9fe64a", "token": "vuUIjX5kKH7k_QtYWkejIzJcdtfw-lFGsDzh6BR7"},
70
- {"id": "6641f1bee075d1b12d3f5267b3df1424", "token": "BxKVqGpgkXtb2t0sKOguaurT4tkRGH4ZbRLGjG9t"},
71
- {"id": "c70d184eb2aae17ad02af3c13d489de1", "token": "2dk5Oj7yClLMU_qpJgxeWSJPy8pebouPPvMVtQzT"},
72
- {"id": "84039b443bb1e7dab62613cc39655c80", "token": "6VdcjF7_KtBPP46CEG2R82NzTSHwkBCV81rq0LVq"},
73
- {"id": "84bc6c610667a3d8988003dc36b585a4", "token": "Y8h_qYcaXFTbEQ-r-amzzHV_eL37sp-ATZPxiLZz"},
74
- {"id": "d8848c9ac069b6cc660a233ca3911331", "token": "ur_Jv30k7tPT6y-6O_TRODKIo05btJj_dfucUtkN"}
75
- ]
76
-
77
- # ================= HELPER FUNCTIONS =================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  def clean_ai_response(text):
80
- text = re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL)
81
- text = text.replace("```json", "").replace("```", "").strip()
82
- return text
83
-
84
- def pcm_to_wav(pcm_data, sample_rate=24000):
85
- num_channels = 1
86
- sample_width = 2
87
- wav_header = struct.pack('<4sI4s4sIHHIIHH4sI',
88
- b'RIFF', 36 + len(pcm_data), b'WAVE',
89
- b'fmt ', 16, 1, num_channels, sample_rate,
90
- sample_rate * num_channels * sample_width,
91
- num_channels * sample_width, sample_width * 8,
92
- b'data', len(pcm_data))
93
- return wav_header + pcm_data
94
-
95
- # ================= ROUTING =================
96
-
97
- @app.get("/", response_class=HTMLResponse)
98
  async def root():
99
- return """
100
- <html>
101
- <body style="font-family:sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; background:#0f172a; color:white;">
102
- <div style="background:#1e293b; padding:3rem; border-radius:2rem; box-shadow:0 25px 50px -12px rgba(0,0,0,0.5); text-align:center; border: 1px solid #334155;">
103
- <h1 style="color:#818cf8; margin-bottom:0.5rem;">STB AI Node v2.6</h1>
104
- <p style="color:#10b981; font-weight:bold;">● Hybrid Multi-Fallback Active</p>
105
- <div style="font-size: 0.8rem; color: #94a3b8; margin-top: 1.5rem; display:grid; grid-template-cols: 1fr 1fr; gap: 10px;">
106
- <div style="background:#0f172a; padding:10px; border-radius:10px;">LLM: DeepSeek / 7x OR / 8x CF</div>
107
- <div style="background:#0f172a; padding:10px; border-radius:10px;">Whisper: Local / 8x Cloud</div>
108
- <div style="background:#0f172a; padding:10px; border-radius:10px;">TTS: Gemini Pro</div>
109
- <div style="background:#0f172a; padding:10px; border-radius:10px;">NLP: DistilBERT</div>
110
- </div>
111
- </div>
112
- </body>
113
- </html>
114
- """
115
-
116
- @app.get("/v1/models")
117
- async def get_models():
118
- return {
119
- "data": [
120
- {"id": "gpt-4o-mini", "object": "model"},
121
- {"id": "distilbert-nlp", "object": "model"},
122
- {"id": LOCAL_MODEL, "object": "model"},
123
- {"id": "tts-1", "object": "model"},
124
- {"id": "whisper-1", "object": "model"}
125
- ]
126
- }
127
 
128
  @app.post("/v1/chat/completions")
129
- async def chat_completions(request: Request):
 
130
  body = await request.json()
131
- model_requested = body.get("model", LOCAL_MODEL)
132
  messages = body.get("messages", [])
133
- user_query = messages[-1].get("content", "") if messages else ""
134
-
135
- # 1. DISTILBERT LOCAL
136
- if model_requested == "distilbert-nlp" and nlp_engine:
137
- res = nlp_engine(user_query)
138
- return {"choices": [{"message": {"role": "assistant", "content": f"NLP: {res}"}}], "model": "distilbert", "provider": "Local"}
139
 
140
  async with httpx.AsyncClient(timeout=TIMEOUT) as client:
141
- # 2. OLLAMA LOCAL
 
 
 
 
142
  try:
143
- res = await client.post(OLLAMA_URL, json={"model": LOCAL_MODEL, "messages": messages, "stream": False})
 
 
 
 
 
 
 
 
144
  if res.status_code == 200:
145
- content = clean_ai_response(res.json()['message']['content'])
146
- return {"choices": [{"message": {"role": "assistant", "content": content}}], "model": LOCAL_MODEL, "provider": "Local Ollama"}
147
- except: pass
148
 
149
- # 3. OPENROUTER (7 KEYS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  shuffled_keys = list(OR_KEYS)
151
  random.shuffle(shuffled_keys)
 
152
  for key in shuffled_keys:
 
153
  try:
 
154
  res = await client.post(
155
  "https://openrouter.ai/api/v1/chat/completions",
156
- headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
157
- json={"model": "openai/gpt-4o-mini", "messages": messages}
 
 
 
 
 
 
158
  )
 
159
  if res.status_code == 200:
160
- data = res.json()
161
- content = clean_ai_response(data['choices'][0]['message']['content'])
162
- data['choices'][0]['message']['content'] = content
163
- return data
164
- except: continue
165
-
166
- # 4. CLOUDFLARE (8 ACCOUNTS)
 
 
167
  shuffled_cf = list(CF_ACCOUNTS)
168
  random.shuffle(shuffled_cf)
 
169
  for acc in shuffled_cf:
 
170
  try:
 
 
 
171
  res = await client.post(
172
- f"https://api.cloudflare.com/client/v4/accounts/{acc['id']}/ai/run/@cf/meta/llama-3-8b-instruct",
173
- headers={"Authorization": f"Bearer {acc['token']}"},
174
- json={"messages": messages}
175
- )
176
- if res.status_code == 200:
177
- data = res.json()
178
- content = clean_ai_response(data['result']['response'])
179
- return {"choices": [{"message": {"role": "assistant", "content": content}}], "model": "cf-llama-3", "provider": "Cloudflare"}
180
- except: continue
181
-
182
- return JSONResponse({"error": "All AI paths failed"}, status_code=500)
183
-
184
- @app.post("/v1/audio/speech")
185
- async def text_to_speech(request: Request):
186
- """Mendukung API tts-1 dengan Fallback Retry"""
187
- try:
188
- body = await request.json()
189
- text = body.get("input", "")
190
- voice = body.get("voice", "Kore")
191
-
192
- api_key = "" # Diisi oleh lingkungan
193
- url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-tts:generateContent?key={api_key}"
194
-
195
- async with httpx.AsyncClient(timeout=60.0) as client:
196
- for attempt in range(2):
197
- res = await client.post(url, json={
198
- "contents": [{"parts": [{"text": text}]}],
199
- "generationConfig": {
200
- "responseModalities": ["AUDIO"],
201
- "speechConfig": {"voiceConfig": {"prebuiltVoiceConfig": {"voiceName": voice}}}
202
  }
203
- })
 
204
  if res.status_code == 200:
205
- audio_base64 = res.json()['candidates'][0]['content']['parts'][0]['inlineData']['data']
206
- return Response(content=pcm_to_wav(base64.b64decode(audio_base64)), media_type="audio/wav")
207
-
208
- return JSONResponse({"error": "TTS Gemini Failed"}, status_code=500)
209
- except Exception as e:
210
- return JSONResponse({"error": str(e)}, status_code=500)
 
 
 
 
 
 
 
 
211
 
212
  @app.post("/v1/audio/transcriptions")
213
- async def transcribe_audio(
214
- file: UploadFile = File(...),
215
- model: str = Form("whisper-1")
216
- ):
217
- """Mendukung API whisper-1 (Lokal STB -> Fallback ke Cloudflare 8 Akun)"""
218
-
219
- with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as tmp:
 
 
 
220
  shutil.copyfileobj(file.file, tmp)
221
- tmp_path = tmp.name
222
-
223
- # 1. LOKAL WHISPER
224
- if whisper_model:
225
- try:
226
- segments, info = whisper_model.transcribe(tmp_path, beam_size=5)
227
- text = "".join([segment.text for segment in segments])
228
- os.remove(tmp_path)
229
- return {"text": text.strip(), "language": info.language, "provider": "local-stb"}
230
- except: pass
231
 
232
- # 2. CLOUDFLARE WHISPER FALLBACK (8 ACCOUNTS)
233
- try:
234
- with open(tmp_path, "rb") as audio_file:
235
- audio_data = audio_file.read()
236
 
237
- shuffled_cf = list(CF_ACCOUNTS)
238
- random.shuffle(shuffled_cf)
239
-
240
- async with httpx.AsyncClient(timeout=120.0) as client:
241
- for acc in shuffled_cf:
242
- cf_url = f"https://api.cloudflare.com/client/v4/accounts/{acc['id']}/ai/run/@cf/openai/whisper"
243
- res = await client.post(cf_url, headers={"Authorization": f"Bearer {acc['token']}"}, content=audio_data)
244
- if res.status_code == 200:
245
- data = res.json()
246
- os.remove(tmp_path)
247
- return {"text": data['result']['text'], "provider": "cloudflare-cloud"}
248
- except: pass
249
 
250
- if os.path.exists(tmp_path): os.remove(tmp_path)
251
- return JSONResponse({"error": "Transcription failed"}, status_code=500)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
252
 
253
  if __name__ == "__main__":
254
- uvicorn.run(app, host="0.0.0.0", port=8000)
 
 
 
 
 
 
1
+ import os
 
 
 
 
2
  import random
3
  import re
 
 
 
4
  import shutil
5
  import tempfile
6
+ import uvicorn
7
+ import httpx
8
 
9
+ from fastapi import FastAPI, Request, UploadFile, File
10
+ from fastapi.responses import JSONResponse
11
+ from fastapi.middleware.cors import CORSMiddleware
12
 
13
+ from transformers import pipeline
14
+ from faster_whisper import WhisperModel
15
+ from dotenv import load_dotenv
 
 
 
 
 
 
 
 
16
 
17
+ # =========================
18
+ # LOAD ENV
19
+ # =========================
20
+
21
+ load_dotenv()
22
+
23
+ # =========================
24
+ # FASTAPI INIT
25
+ # =========================
26
 
27
  app = FastAPI()
28
 
 
34
  allow_headers=["*"],
35
  )
36
 
37
+ # =========================
38
+ # MODEL PATH
39
+ # =========================
40
+
41
+ MODEL_DIR = "./models_cache"
42
+ DISTILBERT_PATH = os.path.join(MODEL_DIR, "distilbert")
43
+
44
+ # =========================
45
+ # LOAD DISTILBERT
46
+ # =========================
47
+
48
+ nlp_engine = None
49
+
50
+ try:
51
+ if os.path.exists(DISTILBERT_PATH):
52
+ nlp_engine = pipeline(
53
+ "sentiment-analysis",
54
+ model=DISTILBERT_PATH,
55
+ local_files_only=True
56
+ )
57
+ print("DistilBERT loaded")
58
+ except Exception as e:
59
+ print("DistilBERT failed:", e)
60
+
61
+ # =========================
62
+ # LOAD WHISPER
63
+ # =========================
64
+
65
+ whisper_model = None
66
+
67
+ try:
68
+ whisper_model = WhisperModel(
69
+ "tiny",
70
+ device="cpu",
71
+ compute_type="int8"
72
+ )
73
+ print("Whisper loaded")
74
+ except Exception as e:
75
+ print("Whisper failed:", e)
76
+
77
+ # =========================
78
+ # ENV CONFIG
79
+ # =========================
80
+
81
+ TIMEOUT = 120.0
82
+
83
+ OLLAMA_URL = os.getenv("OLLAMA_URL", "http://127.0.0.1:11434/api/chat")
84
+ LOCAL_MODEL = os.getenv("LOCAL_MODEL", "deepseek-r1:1.5b")
85
+
86
+ # =========================
87
+ # OPENROUTER KEYS
88
+ # =========================
89
+
90
+ OR_KEYS = os.getenv("OPENROUTER_KEYS", "").split(",")
91
+
92
+ # =========================
93
+ # CLOUDFLARE ACCOUNTS
94
+ # =========================
95
+
96
+ CF_ACCOUNTS = []
97
+
98
+ cf_env = os.getenv("CLOUDFLARE_ACCOUNTS", "")
99
+
100
+ for acc in cf_env.split(","):
101
+ if ":" in acc:
102
+ acc_id, token = acc.split(":")
103
+ CF_ACCOUNTS.append({
104
+ "id": acc_id,
105
+ "token": token
106
+ })
107
+
108
+ # =========================
109
+ # HELPER
110
+ # =========================
111
 
112
  def clean_ai_response(text):
113
+ text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL)
114
+ text = text.replace("```json", "").replace("```", "")
115
+ return text.strip()
116
+
117
+
118
+ # =========================
119
+ # ROOT
120
+ # =========================
121
+
122
+ @app.get("/")
 
 
 
 
 
 
 
 
123
  async def root():
124
+ return {"status": "AI Node running"}
125
+
126
+
127
+ # =========================
128
+ # CHAT COMPLETION
129
+ # =========================
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
  @app.post("/v1/chat/completions")
132
+ async def chat(request: Request):
133
+
134
  body = await request.json()
 
135
  messages = body.get("messages", [])
 
 
 
 
 
 
136
 
137
  async with httpx.AsyncClient(timeout=TIMEOUT) as client:
138
+
139
+ # =========================
140
+ # LOCAL OLLAMA
141
+ # =========================
142
+
143
  try:
144
+ res = await client.post(
145
+ OLLAMA_URL,
146
+ json={
147
+ "model": LOCAL_MODEL,
148
+ "messages": messages,
149
+ "stream": False
150
+ }
151
+ )
152
+
153
  if res.status_code == 200:
 
 
 
154
 
155
+ data = res.json()
156
+
157
+ content = clean_ai_response(
158
+ data["message"]["content"]
159
+ )
160
+
161
+ return {
162
+ "choices": [
163
+ {
164
+ "message": {
165
+ "role": "assistant",
166
+ "content": content
167
+ }
168
+ }
169
+ ]
170
+ }
171
+
172
+ except:
173
+ pass
174
+
175
+ # =========================
176
+ # OPENROUTER FALLBACK
177
+ # =========================
178
+
179
  shuffled_keys = list(OR_KEYS)
180
  random.shuffle(shuffled_keys)
181
+
182
  for key in shuffled_keys:
183
+
184
  try:
185
+
186
  res = await client.post(
187
  "https://openrouter.ai/api/v1/chat/completions",
188
+ headers={
189
+ "Authorization": f"Bearer {key}",
190
+ "Content-Type": "application/json"
191
+ },
192
+ json={
193
+ "model": "openai/gpt-4o-mini",
194
+ "messages": messages
195
+ }
196
  )
197
+
198
  if res.status_code == 200:
199
+ return res.json()
200
+
201
+ except:
202
+ continue
203
+
204
+ # =========================
205
+ # CLOUDFLARE FALLBACK
206
+ # =========================
207
+
208
  shuffled_cf = list(CF_ACCOUNTS)
209
  random.shuffle(shuffled_cf)
210
+
211
  for acc in shuffled_cf:
212
+
213
  try:
214
+
215
+ url = f"https://api.cloudflare.com/client/v4/accounts/{acc['id']}/ai/run/@cf/meta/llama-3-8b-instruct"
216
+
217
  res = await client.post(
218
+ url,
219
+ headers={
220
+ "Authorization": f"Bearer {acc['token']}"
221
+ },
222
+ json={
223
+ "messages": messages
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
224
  }
225
+ )
226
+
227
  if res.status_code == 200:
228
+ return res.json()
229
+
230
+ except:
231
+ continue
232
+
233
+ return JSONResponse(
234
+ {"error": "all AI providers failed"},
235
+ status_code=500
236
+ )
237
+
238
+
239
+ # =========================
240
+ # WHISPER TRANSCRIPTION
241
+ # =========================
242
 
243
  @app.post("/v1/audio/transcriptions")
244
+ async def transcribe_audio(file: UploadFile = File(...)):
245
+
246
+ if whisper_model is None:
247
+ return JSONResponse(
248
+ {"error": "whisper not loaded"},
249
+ status_code=500
250
+ )
251
+
252
+ with tempfile.NamedTemporaryFile(delete=False) as tmp:
253
+
254
  shutil.copyfileobj(file.file, tmp)
 
 
 
 
 
 
 
 
 
 
255
 
256
+ path = tmp.name
 
 
 
257
 
258
+ segments, info = whisper_model.transcribe(path)
259
+
260
+ text = ""
261
+
262
+ for s in segments:
263
+ text += s.text
 
 
 
 
 
 
264
 
265
+ os.remove(path)
266
+
267
+ return {
268
+ "text": text,
269
+ "language": info.language
270
+ }
271
+
272
+
273
+ # =========================
274
+ # SENTIMENT ANALYSIS
275
+ # =========================
276
+
277
+ @app.post("/v1/sentiment")
278
+ async def sentiment(request: Request):
279
+
280
+ if nlp_engine is None:
281
+ return JSONResponse(
282
+ {"error": "distilbert not loaded"},
283
+ status_code=500
284
+ )
285
+
286
+ body = await request.json()
287
+
288
+ text = body.get("text", "")
289
+
290
+ result = nlp_engine(text)
291
+
292
+ return result
293
+
294
+
295
+ # =========================
296
+ # SERVER
297
+ # =========================
298
 
299
  if __name__ == "__main__":
300
+
301
+ uvicorn.run(
302
+ app,
303
+ host="0.0.0.0",
304
+ port=8000
305
+ )