y4shg commited on
Commit
98231f3
Β·
verified Β·
1 Parent(s): 6ea7c95

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +445 -0
app.py ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ OpenAI Audio API Proxy Server to Pollinations
3
+ Forwards OpenAI-style audio requests to Pollinations.ai API
4
+
5
+ Usage:
6
+ export POLLINATIONS_TOKEN="your_token_here"
7
+ export ALLOWED_API_KEYS="key1,key2,key3" # Optional - comma separated keys
8
+ python mock_openai_audio_api.py
9
+
10
+ Environment Variables:
11
+ POLLINATIONS_TOKEN - Your Pollinations API token (required for forwarding)
12
+ ALLOWED_API_KEYS - Comma-separated list of allowed API keys for auth (optional)
13
+ If not set, any Bearer token is accepted
14
+ """
15
+
16
+ from flask import Flask, request, jsonify, send_file, Response
17
+ import requests
18
+ import io
19
+ import base64
20
+ import json
21
+ import os
22
+ from datetime import datetime
23
+
24
+ app = Flask(__name__)
25
+
26
+ # Configuration from environment
27
+ POLLINATIONS_TOKEN = os.getenv("POLLINATIONS_TOKEN")
28
+ ALLOWED_API_KEYS = os.getenv("ALLOWED_API_KEYS", "").split(",") if os.getenv("ALLOWED_API_KEYS") else None
29
+ POLLINATIONS_API_URL = "https://gen.pollinations.ai/v1/chat/completions"
30
+
31
+ # Supported voices and models
32
+ SUPPORTED_VOICES = ["alloy", "ash", "ballad", "coral", "echo", "fable",
33
+ "onyx", "nova", "sage", "shimmer", "verse", "marin", "cedar"]
34
+ TTS_MODELS = ["tts-1", "tts-1-hd", "gpt-4o-mini-tts", "gpt-4o-mini-tts-2025-12-15"]
35
+
36
+ def validate_api_key():
37
+ """Validate API key from Authorization header"""
38
+ auth_header = request.headers.get('Authorization', '')
39
+ if not auth_header.startswith('Bearer '):
40
+ return False, "Missing or invalid Authorization header"
41
+
42
+ api_key = auth_header[7:] # Remove 'Bearer '
43
+
44
+ # If ALLOWED_API_KEYS is set, validate against the list
45
+ if ALLOWED_API_KEYS:
46
+ if api_key not in ALLOWED_API_KEYS:
47
+ return False, "Invalid API key"
48
+ else:
49
+ # If no specific keys are set, just check it's not empty
50
+ if not api_key:
51
+ return False, "Invalid API key"
52
+
53
+ return True, None
54
+
55
+ @app.route('/v1/audio/speech', methods=['POST'])
56
+ def create_speech():
57
+ """Text-to-Speech endpoint - forwards to Pollinations"""
58
+ # Validate API key
59
+ is_valid, error_msg = validate_api_key()
60
+ if not is_valid:
61
+ return jsonify({"error": {"message": error_msg, "type": "invalid_request_error"}}), 401
62
+
63
+ data = request.get_json()
64
+
65
+ # Validate required parameters
66
+ if not data:
67
+ return jsonify({"error": {"message": "Request body is required", "type": "invalid_request_error"}}), 400
68
+
69
+ input_text = data.get('input')
70
+ model = data.get('model')
71
+ voice = data.get('voice')
72
+
73
+ if not input_text:
74
+ return jsonify({"error": {"message": "Missing required parameter: input", "type": "invalid_request_error"}}), 400
75
+ if not model:
76
+ return jsonify({"error": {"message": "Missing required parameter: model", "type": "invalid_request_error"}}), 400
77
+ if not voice:
78
+ return jsonify({"error": {"message": "Missing required parameter: voice", "type": "invalid_request_error"}}), 400
79
+
80
+ # Validate input length
81
+ if len(input_text) > 4096:
82
+ return jsonify({"error": {"message": "Input text exceeds maximum length of 4096 characters", "type": "invalid_request_error"}}), 400
83
+
84
+ # Validate model
85
+ if model not in TTS_MODELS:
86
+ return jsonify({"error": {"message": f"Invalid model: {model}", "type": "invalid_request_error"}}), 400
87
+
88
+ # Validate voice
89
+ voice_id = voice
90
+ if isinstance(voice, str):
91
+ if voice not in SUPPORTED_VOICES:
92
+ return jsonify({"error": {"message": f"Invalid voice: {voice}", "type": "invalid_request_error"}}), 400
93
+ elif isinstance(voice, dict):
94
+ voice_id = voice.get('id', 'alloy')
95
+
96
+ # Optional parameters
97
+ response_format = data.get('response_format', 'mp3')
98
+ speed = data.get('speed', 1.0)
99
+ instructions = data.get('instructions', '')
100
+ stream_format = data.get('stream_format', 'audio')
101
+
102
+ # Build system prompt with emotion/instructions
103
+ emotion = instructions if instructions else "neutral"
104
+ system_instruction = f"Only repeat what I say. Now say with proper emphasis in a \"{emotion}\" emotion this statement."
105
+
106
+ # Prepare Pollinations API request
107
+ pollinations_headers = {
108
+ "Content-Type": "application/json",
109
+ }
110
+
111
+ # Add Pollinations token if available
112
+ if POLLINATIONS_TOKEN:
113
+ pollinations_headers["Authorization"] = f"Bearer {POLLINATIONS_TOKEN}"
114
+
115
+ pollinations_payload = {
116
+ "model": "openai-audio",
117
+ "modalities": ["text", "audio"],
118
+ "audio": {
119
+ "voice": voice_id if isinstance(voice_id, str) else voice_id,
120
+ "format": response_format
121
+ },
122
+ "messages": [
123
+ {"role": "system", "content": system_instruction},
124
+ {"role": "user", "content": input_text}
125
+ ]
126
+ }
127
+
128
+ try:
129
+ # Forward request to Pollinations
130
+ response = requests.post(
131
+ POLLINATIONS_API_URL,
132
+ headers=pollinations_headers,
133
+ json=pollinations_payload,
134
+ timeout=60
135
+ )
136
+
137
+ if response.status_code != 200:
138
+ # Handle Pollinations errors
139
+ error_message = f"Pollinations API error: {response.status_code}"
140
+ if response.status_code == 402:
141
+ error_message = "Rate limit exceeded. Please try again later or use a premium API key."
142
+ elif response.status_code == 429:
143
+ error_message = "Too many requests. Please slow down."
144
+ elif response.status_code == 401:
145
+ error_message = "Invalid Pollinations token."
146
+
147
+ return jsonify({
148
+ "error": {
149
+ "message": error_message,
150
+ "type": "api_error",
151
+ "pollinations_status": response.status_code
152
+ }
153
+ }), response.status_code
154
+
155
+ # Extract audio from Pollinations response
156
+ pollinations_data = response.json()
157
+ try:
158
+ audio_b64 = pollinations_data['choices'][0]['message']['audio']['data']
159
+ audio_bytes = base64.b64decode(audio_b64)
160
+ except (KeyError, IndexError) as e:
161
+ return jsonify({
162
+ "error": {
163
+ "message": "Invalid response from Pollinations API",
164
+ "type": "api_error"
165
+ }
166
+ }), 500
167
+
168
+ # Return audio in OpenAI format
169
+ return send_file(
170
+ io.BytesIO(audio_bytes),
171
+ mimetype=f'audio/{response_format}',
172
+ as_attachment=True,
173
+ download_name=f'speech.{response_format}'
174
+ )
175
+
176
+ except requests.exceptions.RequestException as e:
177
+ return jsonify({
178
+ "error": {
179
+ "message": f"Network error: {str(e)}",
180
+ "type": "api_error"
181
+ }
182
+ }), 503
183
+
184
+ @app.route('/v1/audio/transcriptions', methods=['POST'])
185
+ def create_transcription():
186
+ """Speech-to-Text endpoint - returns mock data (not yet implemented for Pollinations)"""
187
+ # Validate API key
188
+ is_valid, error_msg = validate_api_key()
189
+ if not is_valid:
190
+ return jsonify({"error": {"message": error_msg, "type": "invalid_request_error"}}), 401
191
+
192
+ return jsonify({
193
+ "error": {
194
+ "message": "Transcription endpoint not yet implemented. This proxy currently only supports text-to-speech.",
195
+ "type": "not_implemented_error"
196
+ }
197
+ }), 501
198
+
199
+ @app.route('/v1/audio/translations', methods=['POST'])
200
+ def create_translation():
201
+ """Audio Translation endpoint - returns mock data (not yet implemented)"""
202
+ is_valid, error_msg = validate_api_key()
203
+ if not is_valid:
204
+ return jsonify({"error": {"message": error_msg, "type": "invalid_request_error"}}), 401
205
+
206
+ return jsonify({
207
+ "error": {
208
+ "message": "Translation endpoint not yet implemented. This proxy currently only supports text-to-speech.",
209
+ "type": "not_implemented_error"
210
+ }
211
+ }), 501
212
+
213
+ @app.route('/v1/audio/voices', methods=['POST'])
214
+ def create_voice():
215
+ """Create custom voice endpoint - not supported"""
216
+ return jsonify({
217
+ "error": {
218
+ "message": "Custom voices not supported by Pollinations proxy",
219
+ "type": "not_implemented_error"
220
+ }
221
+ }), 501
222
+
223
+ @app.route('/v1/audio/voice_consents', methods=['POST', 'GET'])
224
+ def voice_consents():
225
+ """Voice consents endpoint - not supported"""
226
+ return jsonify({
227
+ "error": {
228
+ "message": "Voice consents not supported by Pollinations proxy",
229
+ "type": "not_implemented_error"
230
+ }
231
+ }), 501
232
+
233
+ @app.route('/', methods=['GET'])
234
+ def index():
235
+ """Main page showing API status"""
236
+ base_url = request.host_url.rstrip('/')
237
+
238
+ html = f"""
239
+ <!DOCTYPE html>
240
+ <html>
241
+ <head>
242
+ <title>OpenAI Audio API Proxy</title>
243
+ <style>
244
+ body {{
245
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
246
+ max-width: 800px;
247
+ margin: 50px auto;
248
+ padding: 20px;
249
+ background: #f5f5f5;
250
+ }}
251
+ .container {{
252
+ background: white;
253
+ padding: 30px;
254
+ border-radius: 10px;
255
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
256
+ }}
257
+ h1 {{
258
+ color: #10a37f;
259
+ margin-top: 0;
260
+ }}
261
+ .status {{
262
+ background: #e8f5e9;
263
+ border-left: 4px solid #4caf50;
264
+ padding: 15px;
265
+ margin: 20px 0;
266
+ border-radius: 4px;
267
+ }}
268
+ .base-url {{
269
+ background: #f5f5f5;
270
+ padding: 10px;
271
+ border-radius: 4px;
272
+ font-family: monospace;
273
+ font-size: 14px;
274
+ margin: 10px 0;
275
+ }}
276
+ .endpoint {{
277
+ background: #f9f9f9;
278
+ padding: 10px;
279
+ margin: 5px 0;
280
+ border-radius: 4px;
281
+ border-left: 3px solid #10a37f;
282
+ }}
283
+ .config {{
284
+ margin: 20px 0;
285
+ }}
286
+ .config-item {{
287
+ padding: 8px 0;
288
+ border-bottom: 1px solid #eee;
289
+ }}
290
+ .badge {{
291
+ display: inline-block;
292
+ padding: 4px 8px;
293
+ border-radius: 4px;
294
+ font-size: 12px;
295
+ font-weight: bold;
296
+ }}
297
+ .badge-success {{ background: #4caf50; color: white; }}
298
+ .badge-warning {{ background: #ff9800; color: white; }}
299
+ code {{
300
+ background: #f5f5f5;
301
+ padding: 2px 6px;
302
+ border-radius: 3px;
303
+ font-size: 13px;
304
+ }}
305
+ </style>
306
+ </head>
307
+ <body>
308
+ <div class="container">
309
+ <h1>πŸŽ™οΈ OpenAI Audio API Proxy</h1>
310
+
311
+ <div class="status">
312
+ <strong>βœ… API is running at:</strong>
313
+ <div class="base-url">{base_url}</div>
314
+ </div>
315
+
316
+ <h2>πŸ“‹ Configuration</h2>
317
+ <div class="config">
318
+ <div class="config-item">
319
+ <strong>Pollinations Token:</strong>
320
+ {"<span class='badge badge-success'>βœ“ Configured</span>" if POLLINATIONS_TOKEN else "<span class='badge badge-warning'>⚠ Not Set</span>"}
321
+ </div>
322
+ <div class="config-item">
323
+ <strong>Authentication:</strong>
324
+ {"<span class='badge badge-success'>Restricted</span>" if ALLOWED_API_KEYS else "<span class='badge badge-warning'>Open (any token)</span>"}
325
+ </div>
326
+ </div>
327
+
328
+ <h2>πŸš€ Available Endpoints</h2>
329
+ <div class="endpoint">
330
+ <strong>POST</strong> <code>/v1/audio/speech</code> - Text-to-Speech
331
+ </div>
332
+ <div class="endpoint">
333
+ <strong>GET</strong> <code>/health</code> - Health check
334
+ </div>
335
+
336
+ <h2>πŸ“ Example Usage</h2>
337
+ <pre style="background: #f5f5f5; padding: 15px; border-radius: 4px; overflow-x: auto;">
338
+ curl {base_url}/v1/audio/speech \\
339
+ -H "Authorization: Bearer YOUR_API_KEY" \\
340
+ -H "Content-Type: application/json" \\
341
+ -d '{{
342
+ "model": "gpt-4o-mini-tts",
343
+ "input": "Hello world!",
344
+ "voice": "alloy"
345
+ }}' \\
346
+ --output speech.mp3</pre>
347
+
348
+ <h2>🎡 Supported Voices</h2>
349
+ <p>{", ".join(SUPPORTED_VOICES)}</p>
350
+
351
+ <p style="margin-top: 30px; color: #666; font-size: 14px;">
352
+ Powered by <a href="https://pollinations.ai" target="_blank">Pollinations.ai</a>
353
+ </p>
354
+ </div>
355
+ </body>
356
+ </html>
357
+ """
358
+ return html
359
+
360
+ @app.route('/health', methods=['GET'])
361
+ def health_check():
362
+ """Health check endpoint"""
363
+ pollinations_status = "not_checked"
364
+
365
+ # Check if we can reach Pollinations
366
+ try:
367
+ if POLLINATIONS_TOKEN:
368
+ test_response = requests.get(
369
+ "https://gen.pollinations.ai",
370
+ timeout=5
371
+ )
372
+ pollinations_status = "reachable" if test_response.status_code < 500 else "error"
373
+ else:
374
+ pollinations_status = "no_token"
375
+ except:
376
+ pollinations_status = "unreachable"
377
+
378
+ return jsonify({
379
+ "status": "ok",
380
+ "timestamp": datetime.now().isoformat(),
381
+ "pollinations_token_configured": POLLINATIONS_TOKEN is not None,
382
+ "pollinations_status": pollinations_status,
383
+ "auth_mode": "key_list" if ALLOWED_API_KEYS else "open",
384
+ "supported_endpoints": ["/v1/audio/speech"]
385
+ })
386
+
387
+ @app.errorhandler(404)
388
+ def not_found(error):
389
+ return jsonify({
390
+ "error": {
391
+ "message": "Not found. Available endpoint: POST /v1/audio/speech",
392
+ "type": "invalid_request_error"
393
+ }
394
+ }), 404
395
+
396
+ @app.errorhandler(500)
397
+ def internal_error(error):
398
+ return jsonify({
399
+ "error": {
400
+ "message": "Internal server error",
401
+ "type": "api_error"
402
+ }
403
+ }), 500
404
+
405
+ if __name__ == '__main__':
406
+ print("=" * 70)
407
+ print("OpenAI Audio API Proxy to Pollinations.ai")
408
+ print("=" * 70)
409
+
410
+ # Configuration status
411
+ print("\nπŸ“‹ Configuration:")
412
+ if POLLINATIONS_TOKEN:
413
+ print(f" βœ… Pollinations Token: Configured ({POLLINATIONS_TOKEN[:10]}...)")
414
+ else:
415
+ print(" ⚠️ Pollinations Token: NOT SET (requests will use free tier)")
416
+ print(" Set with: export POLLINATIONS_TOKEN='your_token'")
417
+
418
+ if ALLOWED_API_KEYS:
419
+ print(f" βœ… Auth: Restricted to {len(ALLOWED_API_KEYS)} API key(s)")
420
+ else:
421
+ print(" ⚠️ Auth: Open (any Bearer token accepted)")
422
+ print(" Set with: export ALLOWED_API_KEYS='key1,key2,key3'")
423
+
424
+ print("\nπŸš€ Available endpoints:")
425
+ print(" POST /v1/audio/speech - Text-to-Speech (forwards to Pollinations)")
426
+ print(" GET /health - Health check")
427
+
428
+ print("\nπŸ“ Example usage:")
429
+ print("""
430
+ curl http://localhost:5000/v1/audio/speech \\
431
+ -H "Authorization: Bearer YOUR_KEY" \\
432
+ -H "Content-Type: application/json" \\
433
+ -d '{
434
+ "model": "gpt-4o-mini-tts",
435
+ "input": "Hello world!",
436
+ "voice": "alloy"
437
+ }' \\
438
+ --output speech.mp3
439
+ """)
440
+
441
+ print("\n🌐 Starting server on http://localhost:7860")
442
+ print(" Visit http://localhost:7860 in your browser to see API status")
443
+ print("=" * 70)
444
+
445
+ app.run(host='0.0.0.0', port=7860, debug=True)