| """ |
| OpenAI Audio API Proxy Server to Pollinations |
| Forwards OpenAI-style audio requests to Pollinations.ai API |
| |
| Usage: |
| export POLLINATIONS_TOKEN="your_token_here" |
| export ALLOWED_API_KEYS="key1,key2,key3" # Optional - comma separated keys |
| python mock_openai_audio_api.py |
| |
| Environment Variables: |
| POLLINATIONS_TOKEN - Your Pollinations API token (required for forwarding) |
| ALLOWED_API_KEYS - Comma-separated list of allowed API keys for auth (optional) |
| If not set, any Bearer token is accepted |
| """ |
|
|
| from flask import Flask, request, jsonify, send_file, Response |
| import requests |
| import io |
| import base64 |
| import json |
| import os |
| from datetime import datetime |
|
|
| app = Flask(__name__) |
|
|
| |
| POLLINATIONS_TOKEN = os.getenv("POLLINATIONS_TOKEN") |
| ALLOWED_API_KEYS = os.getenv("ALLOWED_API_KEYS", "").split(",") if os.getenv("ALLOWED_API_KEYS") else None |
| POLLINATIONS_API_URL = "https://gen.pollinations.ai/v1/chat/completions" |
|
|
| |
| SUPPORTED_VOICES = ["alloy", "ash", "ballad", "coral", "echo", "fable", |
| "onyx", "nova", "sage", "shimmer", "verse", "marin", "cedar"] |
| TTS_MODELS = ["tts-1", "tts-1-hd", "gpt-4o-mini-tts", "gpt-4o-mini-tts-2025-12-15"] |
|
|
| def validate_api_key(): |
| """Validate API key from Authorization header""" |
| auth_header = request.headers.get('Authorization', '') |
| if not auth_header.startswith('Bearer '): |
| return False, "Missing or invalid Authorization header" |
| |
| api_key = auth_header[7:] |
| |
| |
| if ALLOWED_API_KEYS: |
| if api_key not in ALLOWED_API_KEYS: |
| return False, "Invalid API key" |
| else: |
| |
| if not api_key: |
| return False, "Invalid API key" |
| |
| return True, None |
|
|
| @app.route('/v1/audio/speech', methods=['POST']) |
| def create_speech(): |
| """Text-to-Speech endpoint - forwards to Pollinations""" |
| |
| is_valid, error_msg = validate_api_key() |
| if not is_valid: |
| return jsonify({"error": {"message": error_msg, "type": "invalid_request_error"}}), 401 |
| |
| data = request.get_json() |
| |
| |
| if not data: |
| return jsonify({"error": {"message": "Request body is required", "type": "invalid_request_error"}}), 400 |
| |
| input_text = data.get('input') |
| model = data.get('model') |
| voice = data.get('voice') |
| |
| if not input_text: |
| return jsonify({"error": {"message": "Missing required parameter: input", "type": "invalid_request_error"}}), 400 |
| if not model: |
| return jsonify({"error": {"message": "Missing required parameter: model", "type": "invalid_request_error"}}), 400 |
| if not voice: |
| return jsonify({"error": {"message": "Missing required parameter: voice", "type": "invalid_request_error"}}), 400 |
| |
| |
| if len(input_text) > 4096: |
| return jsonify({"error": {"message": "Input text exceeds maximum length of 4096 characters", "type": "invalid_request_error"}}), 400 |
| |
| |
| if model not in TTS_MODELS: |
| return jsonify({"error": {"message": f"Invalid model: {model}", "type": "invalid_request_error"}}), 400 |
| |
| |
| voice_id = voice |
| if isinstance(voice, str): |
| if voice not in SUPPORTED_VOICES: |
| return jsonify({"error": {"message": f"Invalid voice: {voice}", "type": "invalid_request_error"}}), 400 |
| elif isinstance(voice, dict): |
| voice_id = voice.get('id', 'alloy') |
| |
| |
| response_format = data.get('response_format', 'mp3') |
| speed = data.get('speed', 1.0) |
| instructions = data.get('instructions', '') |
| stream_format = data.get('stream_format', 'audio') |
| |
| |
| emotion = instructions if instructions else "neutral" |
| system_instruction = f"Only repeat what I say. Now say with proper emphasis in a \"{emotion}\" emotion this statement." |
| |
| |
| pollinations_headers = { |
| "Content-Type": "application/json", |
| } |
| |
| |
| if POLLINATIONS_TOKEN: |
| pollinations_headers["Authorization"] = f"Bearer {POLLINATIONS_TOKEN}" |
| |
| pollinations_payload = { |
| "model": "openai-audio", |
| "modalities": ["text", "audio"], |
| "audio": { |
| "voice": voice_id if isinstance(voice_id, str) else voice_id, |
| "format": response_format |
| }, |
| "messages": [ |
| {"role": "system", "content": system_instruction}, |
| {"role": "user", "content": input_text} |
| ] |
| } |
| |
| try: |
| |
| response = requests.post( |
| POLLINATIONS_API_URL, |
| headers=pollinations_headers, |
| json=pollinations_payload, |
| timeout=60 |
| ) |
| |
| if response.status_code != 200: |
| |
| error_message = f"Pollinations API error: {response.status_code}" |
| if response.status_code == 402: |
| error_message = "Rate limit exceeded. Please try again later or use a premium API key." |
| elif response.status_code == 429: |
| error_message = "Too many requests. Please slow down." |
| elif response.status_code == 401: |
| error_message = "Invalid Pollinations token." |
| |
| return jsonify({ |
| "error": { |
| "message": error_message, |
| "type": "api_error", |
| "pollinations_status": response.status_code |
| } |
| }), response.status_code |
| |
| |
| pollinations_data = response.json() |
| try: |
| audio_b64 = pollinations_data['choices'][0]['message']['audio']['data'] |
| audio_bytes = base64.b64decode(audio_b64) |
| except (KeyError, IndexError) as e: |
| return jsonify({ |
| "error": { |
| "message": "Invalid response from Pollinations API", |
| "type": "api_error" |
| } |
| }), 500 |
| |
| |
| return send_file( |
| io.BytesIO(audio_bytes), |
| mimetype=f'audio/{response_format}', |
| as_attachment=True, |
| download_name=f'speech.{response_format}' |
| ) |
| |
| except requests.exceptions.RequestException as e: |
| return jsonify({ |
| "error": { |
| "message": f"Network error: {str(e)}", |
| "type": "api_error" |
| } |
| }), 503 |
|
|
| @app.route('/v1/audio/transcriptions', methods=['POST']) |
| def create_transcription(): |
| """Speech-to-Text endpoint - returns mock data (not yet implemented for Pollinations)""" |
| |
| is_valid, error_msg = validate_api_key() |
| if not is_valid: |
| return jsonify({"error": {"message": error_msg, "type": "invalid_request_error"}}), 401 |
| |
| return jsonify({ |
| "error": { |
| "message": "Transcription endpoint not yet implemented. This proxy currently only supports text-to-speech.", |
| "type": "not_implemented_error" |
| } |
| }), 501 |
|
|
| @app.route('/v1/audio/translations', methods=['POST']) |
| def create_translation(): |
| """Audio Translation endpoint - returns mock data (not yet implemented)""" |
| is_valid, error_msg = validate_api_key() |
| if not is_valid: |
| return jsonify({"error": {"message": error_msg, "type": "invalid_request_error"}}), 401 |
| |
| return jsonify({ |
| "error": { |
| "message": "Translation endpoint not yet implemented. This proxy currently only supports text-to-speech.", |
| "type": "not_implemented_error" |
| } |
| }), 501 |
|
|
| @app.route('/v1/audio/voices', methods=['POST']) |
| def create_voice(): |
| """Create custom voice endpoint - not supported""" |
| return jsonify({ |
| "error": { |
| "message": "Custom voices not supported by Pollinations proxy", |
| "type": "not_implemented_error" |
| } |
| }), 501 |
|
|
| @app.route('/v1/audio/voice_consents', methods=['POST', 'GET']) |
| def voice_consents(): |
| """Voice consents endpoint - not supported""" |
| return jsonify({ |
| "error": { |
| "message": "Voice consents not supported by Pollinations proxy", |
| "type": "not_implemented_error" |
| } |
| }), 501 |
|
|
| @app.route('/', methods=['GET']) |
| def index(): |
| """Main page showing API status""" |
| base_url = request.host_url.rstrip('/') |
| |
| html = f""" |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <title>OpenAI Audio API Proxy</title> |
| <style> |
| body {{ |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| max-width: 800px; |
| margin: 50px auto; |
| padding: 20px; |
| background: #f5f5f5; |
| }} |
| .container {{ |
| background: white; |
| padding: 30px; |
| border-radius: 10px; |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
| }} |
| h1 {{ |
| color: #10a37f; |
| margin-top: 0; |
| }} |
| .status {{ |
| background: #e8f5e9; |
| border-left: 4px solid #4caf50; |
| padding: 15px; |
| margin: 20px 0; |
| border-radius: 4px; |
| }} |
| .base-url {{ |
| background: #f5f5f5; |
| padding: 10px; |
| border-radius: 4px; |
| font-family: monospace; |
| font-size: 14px; |
| margin: 10px 0; |
| }} |
| .endpoint {{ |
| background: #f9f9f9; |
| padding: 10px; |
| margin: 5px 0; |
| border-radius: 4px; |
| border-left: 3px solid #10a37f; |
| }} |
| .config {{ |
| margin: 20px 0; |
| }} |
| .config-item {{ |
| padding: 8px 0; |
| border-bottom: 1px solid #eee; |
| }} |
| .badge {{ |
| display: inline-block; |
| padding: 4px 8px; |
| border-radius: 4px; |
| font-size: 12px; |
| font-weight: bold; |
| }} |
| .badge-success {{ background: #4caf50; color: white; }} |
| .badge-warning {{ background: #ff9800; color: white; }} |
| code {{ |
| background: #f5f5f5; |
| padding: 2px 6px; |
| border-radius: 3px; |
| font-size: 13px; |
| }} |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <h1>ποΈ OpenAI Audio API Proxy</h1> |
| |
| <div class="status"> |
| <strong>β
API is running at:</strong> |
| <div class="base-url">{base_url}</div> |
| </div> |
| |
| <h2>π Configuration</h2> |
| <div class="config"> |
| <div class="config-item"> |
| <strong>Pollinations Token:</strong> |
| {"<span class='badge badge-success'>β Configured</span>" if POLLINATIONS_TOKEN else "<span class='badge badge-warning'>β Not Set</span>"} |
| </div> |
| <div class="config-item"> |
| <strong>Authentication:</strong> |
| {"<span class='badge badge-success'>Restricted</span>" if ALLOWED_API_KEYS else "<span class='badge badge-warning'>Open (any token)</span>"} |
| </div> |
| </div> |
| |
| <h2>π Available Endpoints</h2> |
| <div class="endpoint"> |
| <strong>POST</strong> <code>/v1/audio/speech</code> - Text-to-Speech |
| </div> |
| <div class="endpoint"> |
| <strong>GET</strong> <code>/health</code> - Health check |
| </div> |
| |
| <h2>π Example Usage</h2> |
| <pre style="background: #f5f5f5; padding: 15px; border-radius: 4px; overflow-x: auto;"> |
| curl {base_url}/v1/audio/speech \\ |
| -H "Authorization: Bearer YOUR_API_KEY" \\ |
| -H "Content-Type: application/json" \\ |
| -d '{{ |
| "model": "gpt-4o-mini-tts", |
| "input": "Hello world!", |
| "voice": "alloy" |
| }}' \\ |
| --output speech.mp3</pre> |
| |
| <h2>π΅ Supported Voices</h2> |
| <p>{", ".join(SUPPORTED_VOICES)}</p> |
| |
| <p style="margin-top: 30px; color: #666; font-size: 14px;"> |
| Powered by <a href="https://pollinations.ai" target="_blank">Pollinations.ai</a> |
| </p> |
| </div> |
| </body> |
| </html> |
| """ |
| return html |
|
|
| @app.route('/health', methods=['GET']) |
| def health_check(): |
| """Health check endpoint""" |
| pollinations_status = "not_checked" |
| |
| |
| try: |
| if POLLINATIONS_TOKEN: |
| test_response = requests.get( |
| "https://gen.pollinations.ai", |
| timeout=5 |
| ) |
| pollinations_status = "reachable" if test_response.status_code < 500 else "error" |
| else: |
| pollinations_status = "no_token" |
| except: |
| pollinations_status = "unreachable" |
| |
| return jsonify({ |
| "status": "ok", |
| "timestamp": datetime.now().isoformat(), |
| "pollinations_token_configured": POLLINATIONS_TOKEN is not None, |
| "pollinations_status": pollinations_status, |
| "auth_mode": "key_list" if ALLOWED_API_KEYS else "open", |
| "supported_endpoints": ["/v1/audio/speech"] |
| }) |
|
|
| @app.errorhandler(404) |
| def not_found(error): |
| return jsonify({ |
| "error": { |
| "message": "Not found. Available endpoint: POST /v1/audio/speech", |
| "type": "invalid_request_error" |
| } |
| }), 404 |
|
|
| @app.errorhandler(500) |
| def internal_error(error): |
| return jsonify({ |
| "error": { |
| "message": "Internal server error", |
| "type": "api_error" |
| } |
| }), 500 |
|
|
| if __name__ == '__main__': |
| print("=" * 70) |
| print("OpenAI Audio API Proxy to Pollinations.ai") |
| print("=" * 70) |
| |
| |
| print("\nπ Configuration:") |
| if POLLINATIONS_TOKEN: |
| print(f" β
Pollinations Token: Configured ({POLLINATIONS_TOKEN[:10]}...)") |
| else: |
| print(" β οΈ Pollinations Token: NOT SET (requests will use free tier)") |
| print(" Set with: export POLLINATIONS_TOKEN='your_token'") |
| |
| if ALLOWED_API_KEYS: |
| print(f" β
Auth: Restricted to {len(ALLOWED_API_KEYS)} API key(s)") |
| else: |
| print(" β οΈ Auth: Open (any Bearer token accepted)") |
| print(" Set with: export ALLOWED_API_KEYS='key1,key2,key3'") |
| |
| print("\nπ Available endpoints:") |
| print(" POST /v1/audio/speech - Text-to-Speech (forwards to Pollinations)") |
| print(" GET /health - Health check") |
| |
| print("\nπ Example usage:") |
| print(""" |
| curl http://localhost:5000/v1/audio/speech \\ |
| -H "Authorization: Bearer YOUR_KEY" \\ |
| -H "Content-Type: application/json" \\ |
| -d '{ |
| "model": "gpt-4o-mini-tts", |
| "input": "Hello world!", |
| "voice": "alloy" |
| }' \\ |
| --output speech.mp3 |
| """) |
| |
| print("\nπ Starting server on http://localhost:7860") |
| print(" Visit http://localhost:7860 in your browser to see API status") |
| print("=" * 70) |
| |
| app.run(host='0.0.0.0', port=7860, debug=True) |