| | """ |
| | 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) |