harivarshannn commited on
Commit
0dcc655
·
1 Parent(s): 81e5d2d

Add Supabase-backed auth/history and speech-to-text APIs

Browse files
Files changed (2) hide show
  1. README.md +26 -0
  2. app.py +439 -1
README.md CHANGED
@@ -15,6 +15,13 @@ This folder is prepared to be pushed as a standalone Hugging Face Docker Space.
15
  It exposes a Flask API for:
16
  - `POST /api/v1/ask`
17
  - `POST /api/v1/upload`
 
 
 
 
 
 
 
18
  - `GET /api/status`
19
 
20
  ## Space Secrets
@@ -22,6 +29,9 @@ It exposes a Flask API for:
22
  Set these in your Hugging Face Space settings:
23
 
24
  - `GROQ_API_KEY`: required
 
 
 
25
 
26
  Optional variables:
27
 
@@ -30,6 +40,22 @@ Optional variables:
30
  - `MODEL_DOWNLOAD_URL=https://.../efficientnet_b0.pt`
31
  - `CLASSES_PATH=/app/models/classes.json`
32
  - `UPLOAD_FOLDER=/tmp/agrogpt_uploads`
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
  ## Model Size Strategy
35
 
 
15
  It exposes a Flask API for:
16
  - `POST /api/v1/ask`
17
  - `POST /api/v1/upload`
18
+ - `POST /api/v1/auth/signup`
19
+ - `POST /api/v1/auth/login`
20
+ - `GET /api/v1/auth/profile`
21
+ - `GET /api/v1/history`
22
+ - `POST /api/v1/history`
23
+ - `DELETE /api/v1/history/<entry_id>`
24
+ - `POST /api/v1/speech-to-text`
25
  - `GET /api/status`
26
 
27
  ## Space Secrets
 
29
  Set these in your Hugging Face Space settings:
30
 
31
  - `GROQ_API_KEY`: required
32
+ - `SUPABASE_URL`: required for auth/history
33
+ - `SUPABASE_SERVICE_ROLE_KEY`: required for auth/history using `public.user`
34
+ - `APP_AUTH_SECRET`: required for signing backend auth tokens
35
 
36
  Optional variables:
37
 
 
40
  - `MODEL_DOWNLOAD_URL=https://.../efficientnet_b0.pt`
41
  - `CLASSES_PATH=/app/models/classes.json`
42
  - `UPLOAD_FOLDER=/tmp/agrogpt_uploads`
43
+ - `SUPABASE_HISTORY_TABLE=chat_history`
44
+ - `GROQ_TRANSCRIBE_MODEL=whisper-large-v3-turbo`
45
+ - `AUTH_TOKEN_MAX_AGE_SECONDS=604800`
46
+
47
+ ## Supabase Table (History)
48
+
49
+ Create a table named `chat_history` (or set `SUPABASE_HISTORY_TABLE`) with columns:
50
+
51
+ - `id uuid primary key default gen_random_uuid()`
52
+ - `user_id integer not null` (references `public.user.id`)
53
+ - `entry_type text not null default 'ask'`
54
+ - `question text`
55
+ - `answer text`
56
+ - `created_at timestamptz default now()`
57
+
58
+ Auth endpoints in this backend use your existing `public.user` table (`email` + `password_hash`) and return a backend Bearer token.
59
 
60
  ## Model Size Strategy
61
 
app.py CHANGED
@@ -2,10 +2,13 @@ import os
2
  import sys
3
  import platform
4
  import tempfile
 
5
  from flask import Flask, request, jsonify
6
  from flask_cors import CORS
7
  import threading
8
  from werkzeug.utils import secure_filename
 
 
9
  import uuid
10
 
11
  # Import the new model functionality and prompts
@@ -19,9 +22,22 @@ CORS(app)
19
  # Configuration for file uploads
20
  UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER", os.path.join(tempfile.gettempdir(), "agrogpt_uploads"))
21
  ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp'}
 
22
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
23
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
24
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  # Create uploads directory if it doesn't exist
26
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
27
 
@@ -32,6 +48,145 @@ status_message = "Initializing..."
32
  def allowed_file(filename):
33
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  def print_header() -> None:
36
  print("AgroGPT Mobile Backend starting...", flush=True)
37
  print(f"Python: {platform.python_version()} ({sys.executable})", flush=True)
@@ -135,7 +290,12 @@ def index():
135
  "endpoints": {
136
  "health": "/api/status",
137
  "ask_question": "/api/v1/ask",
138
- "analyze_image": "/api/v1/upload"
 
 
 
 
 
139
  }
140
  })
141
 
@@ -182,6 +342,12 @@ def ask_question_v1():
182
 
183
  if "error" in response_json:
184
  return api_error("Error from AI model", details=response_json["error"], status_code=500)
 
 
 
 
 
 
185
 
186
  return api_success(response_json)
187
 
@@ -229,6 +395,278 @@ def upload_image_v1():
229
 
230
  return api_error("Invalid file type")
231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  def main():
233
  print_header()
234
  check_backend_status()
 
2
  import sys
3
  import platform
4
  import tempfile
5
+ import requests
6
  from flask import Flask, request, jsonify
7
  from flask_cors import CORS
8
  import threading
9
  from werkzeug.utils import secure_filename
10
+ from werkzeug.security import generate_password_hash, check_password_hash
11
+ from itsdangerous import URLSafeTimedSerializer, BadSignature, SignatureExpired
12
  import uuid
13
 
14
  # Import the new model functionality and prompts
 
22
  # Configuration for file uploads
23
  UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER", os.path.join(tempfile.gettempdir(), "agrogpt_uploads"))
24
  ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif', 'bmp'}
25
+ ALLOWED_AUDIO_EXTENSIONS = {'wav', 'mp3', 'm4a', 'ogg', 'webm', 'flac', 'aac'}
26
  app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
27
  app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size
28
 
29
+ # Supabase and speech configuration
30
+ SUPABASE_URL = os.getenv("SUPABASE_URL", "").rstrip("/")
31
+ SUPABASE_ANON_KEY = os.getenv("SUPABASE_ANON_KEY", "")
32
+ SUPABASE_SERVICE_ROLE_KEY = os.getenv("SUPABASE_SERVICE_ROLE_KEY", "")
33
+ SUPABASE_HISTORY_TABLE = os.getenv("SUPABASE_HISTORY_TABLE", "chat_history")
34
+ GROQ_API_KEY = os.getenv("GROQ_API_KEY", "")
35
+ GROQ_TRANSCRIBE_MODEL = os.getenv("GROQ_TRANSCRIBE_MODEL", "whisper-large-v3-turbo")
36
+ APP_AUTH_SECRET = os.getenv("APP_AUTH_SECRET", GROQ_API_KEY or "agrogpt-dev-secret")
37
+ AUTH_TOKEN_MAX_AGE_SECONDS = int(os.getenv("AUTH_TOKEN_MAX_AGE_SECONDS", "604800"))
38
+
39
+ token_serializer = URLSafeTimedSerializer(APP_AUTH_SECRET)
40
+
41
  # Create uploads directory if it doesn't exist
42
  os.makedirs(UPLOAD_FOLDER, exist_ok=True)
43
 
 
48
  def allowed_file(filename):
49
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
50
 
51
+ def allowed_audio_file(filename):
52
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
53
+
54
+ def supabase_enabled() -> bool:
55
+ return bool(SUPABASE_URL and (SUPABASE_ANON_KEY or SUPABASE_SERVICE_ROLE_KEY))
56
+
57
+ def supabase_api_key() -> str:
58
+ return SUPABASE_SERVICE_ROLE_KEY or SUPABASE_ANON_KEY
59
+
60
+ def supabase_headers() -> dict:
61
+ api_key = supabase_api_key()
62
+ headers = {
63
+ "apikey": api_key,
64
+ "Authorization": f"Bearer {api_key}",
65
+ "Content-Type": "application/json"
66
+ }
67
+ return headers
68
+
69
+ def extract_bearer_token() -> str | None:
70
+ auth_header = request.headers.get("Authorization", "")
71
+ if auth_header.lower().startswith("bearer "):
72
+ return auth_header.split(" ", 1)[1].strip() or None
73
+ return None
74
+
75
+ def create_auth_token(user_row: dict) -> str:
76
+ return token_serializer.dumps({
77
+ "user_id": user_row["id"],
78
+ "email": user_row.get("email", "")
79
+ })
80
+
81
+ def decode_auth_token(token: str):
82
+ try:
83
+ payload = token_serializer.loads(token, max_age=AUTH_TOKEN_MAX_AGE_SECONDS)
84
+ return payload, None
85
+ except SignatureExpired:
86
+ return None, "Session expired"
87
+ except BadSignature:
88
+ return None, "Invalid token"
89
+
90
+ def sanitize_user(user_row: dict) -> dict:
91
+ return {
92
+ "id": user_row.get("id"),
93
+ "email": user_row.get("email"),
94
+ "full_name": user_row.get("full_name"),
95
+ "profession": user_row.get("profession")
96
+ }
97
+
98
+ def find_user_by_email(email: str):
99
+ if not supabase_enabled():
100
+ return None, "Supabase is not configured"
101
+
102
+ try:
103
+ res = requests.get(
104
+ f"{SUPABASE_URL}/rest/v1/user",
105
+ headers=supabase_headers(),
106
+ params={
107
+ "select": "id,email,full_name,profession,password_hash",
108
+ "email": f"eq.{email}",
109
+ "limit": "1"
110
+ },
111
+ timeout=15
112
+ )
113
+ if res.status_code >= 400:
114
+ return None, f"Failed to query user ({res.status_code})"
115
+
116
+ rows = res.json() if res.content else []
117
+ if not rows:
118
+ return None, None
119
+
120
+ return rows[0], None
121
+ except Exception as e:
122
+ return None, f"Failed to query user: {str(e)}"
123
+
124
+ def find_user_by_id(user_id: int):
125
+ if not supabase_enabled():
126
+ return None, "Supabase is not configured"
127
+
128
+ try:
129
+ res = requests.get(
130
+ f"{SUPABASE_URL}/rest/v1/user",
131
+ headers=supabase_headers(),
132
+ params={
133
+ "select": "id,email,full_name,profession,password_hash",
134
+ "id": f"eq.{user_id}",
135
+ "limit": "1"
136
+ },
137
+ timeout=15
138
+ )
139
+ if res.status_code >= 400:
140
+ return None, f"Failed to load profile ({res.status_code})"
141
+
142
+ rows = res.json() if res.content else []
143
+ if not rows:
144
+ return None, "User not found"
145
+ return rows[0], None
146
+ except Exception as e:
147
+ return None, f"Failed to load profile: {str(e)}"
148
+
149
+ def get_authenticated_user(optional: bool = False):
150
+ user_token = extract_bearer_token()
151
+ if not user_token:
152
+ if optional:
153
+ return None, None
154
+ return None, "Missing Bearer token"
155
+
156
+ claims, token_error = decode_auth_token(user_token)
157
+ if token_error:
158
+ return None, token_error
159
+
160
+ user_row, user_error = find_user_by_id(claims.get("user_id"))
161
+ if user_error:
162
+ return None, user_error
163
+
164
+ return user_row, None
165
+
166
+ def save_history_entry(user_id: int, question: str, answer_text: str):
167
+ if not supabase_enabled() or not user_id:
168
+ return
169
+
170
+ payload = {
171
+ "user_id": user_id,
172
+ "entry_type": "ask",
173
+ "question": question,
174
+ "answer": answer_text
175
+ }
176
+
177
+ try:
178
+ requests.post(
179
+ f"{SUPABASE_URL}/rest/v1/{SUPABASE_HISTORY_TABLE}",
180
+ headers={
181
+ **supabase_headers(),
182
+ "Prefer": "return=minimal"
183
+ },
184
+ json=payload,
185
+ timeout=15
186
+ )
187
+ except Exception as e:
188
+ print(f"Warning: Failed to store history entry: {e}", flush=True)
189
+
190
  def print_header() -> None:
191
  print("AgroGPT Mobile Backend starting...", flush=True)
192
  print(f"Python: {platform.python_version()} ({sys.executable})", flush=True)
 
290
  "endpoints": {
291
  "health": "/api/status",
292
  "ask_question": "/api/v1/ask",
293
+ "analyze_image": "/api/v1/upload",
294
+ "signup": "/api/v1/auth/signup",
295
+ "login": "/api/v1/auth/login",
296
+ "profile": "/api/v1/auth/profile",
297
+ "history": "/api/v1/history",
298
+ "speech_to_text": "/api/v1/speech-to-text"
299
  }
300
  })
301
 
 
342
 
343
  if "error" in response_json:
344
  return api_error("Error from AI model", details=response_json["error"], status_code=500)
345
+
346
+ # Optional history persistence when a valid auth token is provided.
347
+ user_data, _ = get_authenticated_user(optional=True)
348
+ if user_data and user_data.get("id"):
349
+ answer_text = format_to_rich_text(response_json)
350
+ save_history_entry(user_data["id"], question, answer_text)
351
 
352
  return api_success(response_json)
353
 
 
395
 
396
  return api_error("Invalid file type")
397
 
398
+ @app.route('/api/v1/auth/signup', methods=['POST'])
399
+ def auth_signup_v1():
400
+ if not supabase_enabled():
401
+ return api_error("Supabase is not configured", status_code=500)
402
+
403
+ data = request.get_json() or {}
404
+ email = (data.get("email") or "").strip().lower()
405
+ password = data.get("password") or ""
406
+ full_name = (data.get("full_name") or "").strip()
407
+ profession = (data.get("profession") or "").strip()
408
+
409
+ if not email or not password:
410
+ return api_error("Email and password are required")
411
+
412
+ existing_user, query_error = find_user_by_email(email)
413
+ if query_error:
414
+ return api_error("Signup failed", details=query_error, status_code=500)
415
+ if existing_user:
416
+ return api_error("Signup failed", details="Email already exists", status_code=409)
417
+
418
+ payload = [{
419
+ "email": email,
420
+ "full_name": full_name,
421
+ "profession": profession,
422
+ "password_hash": generate_password_hash(password)
423
+ }]
424
+
425
+ try:
426
+ res = requests.post(
427
+ f"{SUPABASE_URL}/rest/v1/user",
428
+ headers=supabase_headers(),
429
+ params={
430
+ "select": "id,email,full_name,profession"
431
+ },
432
+ json=payload,
433
+ timeout=20
434
+ )
435
+ body = res.json() if res.content else []
436
+ if res.status_code >= 400:
437
+ return api_error("Signup failed", details=body, status_code=res.status_code)
438
+ if not body:
439
+ return api_error("Signup failed", details="User creation returned empty response", status_code=500)
440
+
441
+ user_row = body[0]
442
+ token = create_auth_token(user_row)
443
+ return jsonify({
444
+ "status": "success",
445
+ "message": "Signup successful",
446
+ "data": {
447
+ "token": token,
448
+ "user": sanitize_user(user_row)
449
+ }
450
+ })
451
+ except Exception as e:
452
+ return api_error("Signup request failed", details=str(e), status_code=500)
453
+
454
+ @app.route('/api/v1/auth/login', methods=['POST'])
455
+ def auth_login_v1():
456
+ if not supabase_enabled():
457
+ return api_error("Supabase is not configured", status_code=500)
458
+
459
+ data = request.get_json() or {}
460
+ email = (data.get("email") or "").strip().lower()
461
+ password = data.get("password") or ""
462
+
463
+ if not email or not password:
464
+ return api_error("Email and password are required")
465
+
466
+ user_row, query_error = find_user_by_email(email)
467
+ if query_error:
468
+ return api_error("Login failed", details=query_error, status_code=500)
469
+ if not user_row:
470
+ return api_error("Login failed", details="Invalid email or password", status_code=401)
471
+
472
+ if not check_password_hash(user_row.get("password_hash") or "", password):
473
+ return api_error("Login failed", details="Invalid email or password", status_code=401)
474
+
475
+ token = create_auth_token(user_row)
476
+ return jsonify({
477
+ "status": "success",
478
+ "message": "Login successful",
479
+ "data": {
480
+ "token": token,
481
+ "user": sanitize_user(user_row)
482
+ }
483
+ })
484
+
485
+ @app.route('/api/v1/auth/profile', methods=['GET'])
486
+ def auth_profile_v1():
487
+ if not supabase_enabled():
488
+ return api_error("Supabase is not configured", status_code=500)
489
+
490
+ user_data, err = get_authenticated_user()
491
+ if err:
492
+ return api_error("Unauthorized", details=err, status_code=401)
493
+
494
+ return jsonify({
495
+ "status": "success",
496
+ "data": {
497
+ "user": sanitize_user(user_data)
498
+ }
499
+ })
500
+
501
+ @app.route('/api/v1/history', methods=['GET'])
502
+ def history_list_v1():
503
+ if not supabase_enabled():
504
+ return api_error("Supabase is not configured", status_code=500)
505
+
506
+ user_data, err = get_authenticated_user()
507
+ if err:
508
+ return api_error("Unauthorized", details=err, status_code=401)
509
+
510
+ limit = request.args.get("limit", "30")
511
+ try:
512
+ limit_int = max(1, min(int(limit), 100))
513
+ except ValueError:
514
+ limit_int = 30
515
+
516
+ try:
517
+ res = requests.get(
518
+ f"{SUPABASE_URL}/rest/v1/{SUPABASE_HISTORY_TABLE}",
519
+ headers=supabase_headers(),
520
+ params={
521
+ "select": "id,entry_type,question,answer,created_at",
522
+ "user_id": f"eq.{user_data['id']}",
523
+ "order": "created_at.desc",
524
+ "limit": str(limit_int)
525
+ },
526
+ timeout=20
527
+ )
528
+ body = res.json() if res.content else []
529
+ if res.status_code >= 400:
530
+ return api_error("Failed to fetch history", details=body, status_code=res.status_code)
531
+
532
+ return jsonify({
533
+ "status": "success",
534
+ "data": {
535
+ "items": body
536
+ }
537
+ })
538
+ except Exception as e:
539
+ return api_error("History query failed", details=str(e), status_code=500)
540
+
541
+ @app.route('/api/v1/history', methods=['POST'])
542
+ def history_create_v1():
543
+ if not supabase_enabled():
544
+ return api_error("Supabase is not configured", status_code=500)
545
+
546
+ user_data, err = get_authenticated_user()
547
+ if err:
548
+ return api_error("Unauthorized", details=err, status_code=401)
549
+
550
+ data = request.get_json() or {}
551
+ question = (data.get("question") or "").strip()
552
+ answer = (data.get("answer") or "").strip()
553
+ entry_type = (data.get("entry_type") or "ask").strip()[:30]
554
+
555
+ if not question and not answer:
556
+ return api_error("Either question or answer is required")
557
+
558
+ payload = {
559
+ "user_id": user_data["id"],
560
+ "entry_type": entry_type,
561
+ "question": question,
562
+ "answer": answer
563
+ }
564
+
565
+ try:
566
+ res = requests.post(
567
+ f"{SUPABASE_URL}/rest/v1/{SUPABASE_HISTORY_TABLE}",
568
+ headers={
569
+ **supabase_headers(),
570
+ "Prefer": "return=representation"
571
+ },
572
+ json=payload,
573
+ timeout=20
574
+ )
575
+ body = res.json() if res.content else []
576
+ if res.status_code >= 400:
577
+ return api_error("Failed to save history", details=body, status_code=res.status_code)
578
+
579
+ return jsonify({
580
+ "status": "success",
581
+ "data": {
582
+ "item": body[0] if body else payload
583
+ }
584
+ })
585
+ except Exception as e:
586
+ return api_error("History save failed", details=str(e), status_code=500)
587
+
588
+ @app.route('/api/v1/history/<entry_id>', methods=['DELETE'])
589
+ def history_delete_v1(entry_id):
590
+ if not supabase_enabled():
591
+ return api_error("Supabase is not configured", status_code=500)
592
+
593
+ user_data, err = get_authenticated_user()
594
+ if err:
595
+ return api_error("Unauthorized", details=err, status_code=401)
596
+
597
+ try:
598
+ res = requests.delete(
599
+ f"{SUPABASE_URL}/rest/v1/{SUPABASE_HISTORY_TABLE}",
600
+ headers=supabase_headers(),
601
+ params={
602
+ "id": f"eq.{entry_id}",
603
+ "user_id": f"eq.{user_data['id']}"
604
+ },
605
+ timeout=20
606
+ )
607
+ if res.status_code >= 400:
608
+ body = res.json() if res.content else {}
609
+ return api_error("Failed to delete history", details=body, status_code=res.status_code)
610
+
611
+ return jsonify({
612
+ "status": "success",
613
+ "message": "History item deleted"
614
+ })
615
+ except Exception as e:
616
+ return api_error("History delete failed", details=str(e), status_code=500)
617
+
618
+ @app.route('/api/v1/speech-to-text', methods=['POST'])
619
+ def speech_to_text_v1():
620
+ if not GROQ_API_KEY:
621
+ return api_error("Speech service unavailable", details="GROQ_API_KEY is not configured", status_code=503)
622
+
623
+ if 'audio' not in request.files:
624
+ return api_error("No audio file provided")
625
+
626
+ audio_file = request.files['audio']
627
+ if not audio_file or audio_file.filename == '':
628
+ return api_error("No audio file selected")
629
+
630
+ if not allowed_audio_file(audio_file.filename):
631
+ return api_error("Invalid audio file type")
632
+
633
+ language = (request.form.get("language") or "en").strip()[:10]
634
+ filename = secure_filename(audio_file.filename)
635
+
636
+ try:
637
+ files = {
638
+ "file": (filename, audio_file.stream, audio_file.mimetype or "application/octet-stream")
639
+ }
640
+ data = {
641
+ "model": GROQ_TRANSCRIBE_MODEL,
642
+ "language": language,
643
+ "response_format": "json"
644
+ }
645
+ headers = {
646
+ "Authorization": f"Bearer {GROQ_API_KEY}"
647
+ }
648
+
649
+ res = requests.post(
650
+ "https://api.groq.com/openai/v1/audio/transcriptions",
651
+ headers=headers,
652
+ files=files,
653
+ data=data,
654
+ timeout=45
655
+ )
656
+
657
+ body = res.json() if res.content else {}
658
+ if res.status_code >= 400:
659
+ return api_error("Speech transcription failed", details=body, status_code=res.status_code)
660
+
661
+ return jsonify({
662
+ "status": "success",
663
+ "data": {
664
+ "text": body.get("text", "")
665
+ }
666
+ })
667
+ except Exception as e:
668
+ return api_error("Speech transcription request failed", details=str(e), status_code=500)
669
+
670
  def main():
671
  print_header()
672
  check_backend_status()