├── requirements.txt ├── a4f_local ├── providers │ ├── __init__.py │ ├── provider_1 │ │ ├── tts │ │ │ ├── __init__.py │ │ │ └── engine.py │ │ └── __init__.py │ └── _discovery.py ├── types │ ├── __init__.py │ ├── audio.py │ └── chat.py ├── __init__.py └── client.py ├── docs ├── requirements.txt ├── make.bat ├── Makefile ├── index.rst └── conf.py ├── setup.py ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── workflows │ ├── test.yml │ └── publish.yml └── PULL_REQUEST_TEMPLATE.md ├── CHANGELOG.md ├── tests └── test_tts.py ├── CONTRIBUTING.md ├── pyproject.toml ├── .gitignore ├── README.md ├── LICENSE └── CODE_OF_CONDUCT.md /requirements.txt: -------------------------------------------------------------------------------- 1 | pydantic==2.11.3 2 | Requests==2.32.3 3 | setuptools==70.0.0 4 | -------------------------------------------------------------------------------- /a4f_local/providers/__init__.py: -------------------------------------------------------------------------------- 1 | # This file makes the 'providers' directory a Python package. 2 | # It can optionally contain common provider logic or initialization code. 3 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # Documentation dependencies 2 | sphinx>=4.0.0 3 | sphinx-rtd-theme>=1.0.0 4 | # Add other Sphinx extensions here if needed, e.g.: 5 | # sphinx-autodoc-typehints 6 | # myst-parser 7 | 8 | pydantic==2.11.3 9 | Requests==2.32.3 10 | setuptools==70.0.0 11 | -------------------------------------------------------------------------------- /a4f_local/providers/provider_1/tts/__init__.py: -------------------------------------------------------------------------------- 1 | # This file makes 'tts' a package within 'provider_1'. 2 | # It exports the main engine function for this capability. 3 | 4 | from .engine import create_speech 5 | 6 | __all__ = ['create_speech'] # Required for the discovery mechanism convention 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # setup.py shim 2 | # Allows editable installs and provides compatibility for older tools 3 | # that don't fully support pyproject.toml. 4 | # Configuration should primarily live in pyproject.toml. 5 | 6 | from setuptools import setup 7 | 8 | if __name__ == "__main__": 9 | setup() 10 | -------------------------------------------------------------------------------- /a4f_local/providers/provider_1/__init__.py: -------------------------------------------------------------------------------- 1 | # Provider metadata for provider_1 (OpenAI.fm TTS) 2 | 3 | # REQUIRED by the discovery mechanism 4 | CAPABILITIES: list[str] = ["tts"] 5 | 6 | # OPTIONAL: Human-readable name for logging/debugging 7 | PROVIDER_NAME: str = "OpenAI.fm TTS (Reverse-Engineered)" 8 | 9 | # This file marks 'provider_1' as a Python package. 10 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 16 | goto end 17 | 18 | :help 19 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 20 | 21 | :end 22 | popd 23 | -------------------------------------------------------------------------------- /a4f_local/types/__init__.py: -------------------------------------------------------------------------------- 1 | # This file makes the 'types' directory a Python package. 2 | # It should export the defined Pydantic models/dataclasses. 3 | 4 | from .audio import SpeechCreateRequest 5 | # from .chat import ChatCompletionRequest, ChatCompletion # Uncomment when chat.py is implemented 6 | 7 | __all__ = [ 8 | "SpeechCreateRequest", 9 | # "ChatCompletionRequest", # Uncomment when chat.py is implemented 10 | # "ChatCompletion", # Uncomment when chat.py is implemented 11 | ] 12 | -------------------------------------------------------------------------------- /a4f_local/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | a4f-local: A unified wrapper for various reverse-engineered AI provider APIs. 3 | """ 4 | __version__ = "0.1.0" 5 | 6 | # Import the main client class to make it available directly, e.g., `from a4f_local import A4F` 7 | from .client import A4F 8 | 9 | # Optionally, pre-run discovery when the package is imported 10 | # from .providers import _discovery 11 | # _discovery.find_providers() # Ensure registry is populated early 12 | 13 | # Define what gets imported with 'from a4f_local import *' 14 | __all__ = ['A4F'] 15 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = a4f-local 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest an idea for this project 4 | title: "[FEAT] Brief description of the feature" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. "I'm always frustrated when..." 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: Create a report to help us improve a4f-local 4 | title: "[BUG] Brief description of the bug" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Code snippet: 16 | ```python 17 | # Your minimal reproducible code here 18 | from a4f_local import A4F 19 | # ... 20 | ``` 21 | 2. Input data (if applicable) 22 | 3. Expected behavior 23 | 4. Actual behavior (including full traceback if there's an error) 24 | 25 | **Environment (please complete the following information):** 26 | - OS: [e.g. Windows 11, Ubuntu 22.04] 27 | - Python version: [e.g. 3.10] 28 | - a4f-local version: [e.g. 0.1.0 or commit hash] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest # Or specify other OS like windows-latest, macos-latest 12 | strategy: 13 | matrix: 14 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] # Test against multiple Python versions 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - name: Install dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install .[test] # Install package with 'test' extras defined in pyproject.toml 29 | 30 | - name: Run tests with pytest 31 | run: | 32 | pytest tests/ 33 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project are documented here. 4 | 5 | ## [0.1.1] - 2025-04-14 6 | 7 | - Added support for new OpenAI TTS voices: ash, coral, and sage. These voices can now be used in TTS requests if supported by the backend API. 8 | - Updated type validation and provider engine to accept the new voices. 9 | - Improved README with clearer installation instructions and updated roadmap. 10 | 11 | ## [0.1.0] - 2025-04-10 12 | 13 | - Initial release of a4f-local. 14 | - Introduced the core A4F client class for unified access to reverse-engineered AI provider APIs. 15 | - Implemented provider discovery mechanism. 16 | - Added Pydantic types for TTS requests. 17 | - Included example TTS provider (provider_1) using OpenAI.fm reverse-engineered API. 18 | - Added basic import-based test script (tests/test_tts.py). 19 | - Provided pyproject.toml for packaging and distribution. 20 | - Added LICENSE file with custom license. 21 | - Included comprehensive README and .gitignore. 22 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.md 2 | :parser: myst_parser.sphinx_ 3 | 4 | .. note:: 5 | If the above include doesn't render the README correctly, you might need to install and enable `myst-parser` in `docs/requirements.txt` and `docs/conf.py`. 6 | 7 | Welcome to a4f-local's documentation! 8 | ===================================== 9 | 10 | .. toctree:: 11 | :maxdepth: 2 12 | :caption: Contents: 13 | 14 | api 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | 23 | API Documentation 24 | ================= 25 | 26 | .. automodule:: a4f_local 27 | :members: 28 | :undoc-members: 29 | :show-inheritance: 30 | 31 | .. automodule:: a4f_local.client 32 | :members: 33 | :undoc-members: 34 | :show-inheritance: 35 | 36 | .. automodule:: a4f_local.providers.provider_1.tts.engine 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | 41 | .. Add more automodule directives for other modules as needed 42 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package to PyPI 2 | 3 | on: 4 | release: 5 | types: [published] # Trigger only when a release is published on GitHub 6 | 7 | permissions: 8 | contents: read 9 | # IMPORTANT: This permission is needed for uploading to PyPI using trusted publishing. 10 | id-token: write 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.x' # Use a recent Python 3 version for building 24 | 25 | - name: Install build dependencies 26 | run: | 27 | python -m pip install --upgrade pip 28 | pip install build twine 29 | 30 | - name: Build package 31 | run: python -m build 32 | 33 | - name: Publish package to PyPI 34 | # Uses PyPI's trusted publishing feature (OIDC) - requires setup on PyPI. 35 | # See: https://docs.pypi.org/trusted-publishers/ 36 | # You need to configure this project as a trusted publisher on PyPI. 37 | uses: pypa/gh-action-pypi-publish@release/v1 38 | # No API token needed here if trusted publishing is configured. 39 | # If not using trusted publishing, you would need to add secrets: 40 | # with: 41 | # user: __token__ 42 | # password: ${{ secrets.PYPI_API_TOKEN }} # Store your PyPI token as a GitHub secret 43 | -------------------------------------------------------------------------------- /tests/test_tts.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | # Ensure the package root is in the Python path for direct script execution 5 | project_root = Path(__file__).parent.parent 6 | sys.path.insert(0, str(project_root)) 7 | 8 | from a4f_local import A4F 9 | 10 | def tts(): 11 | """Runs a simple TTS test compatible with pytest.""" 12 | try: 13 | client = A4F() 14 | 15 | test_text = "This is a test of the text-to-speech system using the a4f-local package and provider 1." 16 | test_voice = "echo" 17 | output_filename = "test_output.mp3" 18 | output_path = Path(__file__).parent / output_filename 19 | 20 | print(f"Requesting TTS for voice '{test_voice}'...") 21 | audio_bytes = client.audio.speech.create( 22 | model="tts-1", 23 | input=test_text, 24 | voice=test_voice 25 | ) 26 | 27 | if isinstance(audio_bytes, bytes) and len(audio_bytes) > 100: 28 | print(f"SUCCESS: Received {len(audio_bytes)} bytes of audio.") 29 | with open(output_path, "wb") as f: 30 | f.write(audio_bytes) 31 | print(f"Saved output to: {output_path}") 32 | else: 33 | print(f"FAILED: Invalid audio data received (Type: {type(audio_bytes)}).") 34 | 35 | except Exception as e: 36 | print(f"FAILED: An error occurred during the test: {e}") 37 | raise # Re-raise exception for pytest to capture failure 38 | 39 | # if __name__ == "__main__": 40 | # tts() 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to a4f-local 2 | 3 | First off, thank you for considering contributing to `a4f-local`! Your help is appreciated. 4 | 5 | ## How Can I Contribute? 6 | 7 | ### Reporting Bugs 8 | 9 | - Ensure the bug was not already reported by searching on GitHub under [Issues](https://github.com/Devs-Do-Code/a4f-local/issues). 10 | - If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/Devs-Do-Code/a4f-local/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. Use the "Bug Report" template if available. 11 | 12 | ### Suggesting Enhancements 13 | 14 | - Open a new issue using the "Feature Request" template. 15 | - Clearly describe the enhancement and the motivation for it. Include examples if possible. 16 | 17 | ### Pull Requests 18 | 19 | 1. Fork the repository and create your branch from `main`. 20 | 2. If you've added code that should be tested, add tests. 21 | 3. Ensure the test suite passes (details on running tests TBD). 22 | 4. Make sure your code lints (details on linting TBD). 23 | 5. Issue that pull request! 24 | 25 | ## Code of Conduct 26 | 27 | Please note that this project is released with a Contributor Code of Conduct. By participating in this project you agree to abide by its terms. See `CODE_OF_CONDUCT.md`. 28 | 29 | ## Any questions? 30 | 31 | Feel free to open an issue if you have questions about contributing. 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Please include a summary of the change and which issue is fixed (if any). Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue number) 6 | 7 | ## Type of change 8 | 9 | Please delete options that are not relevant. 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 14 | - [ ] This change requires a documentation update 15 | - [ ] Code refactoring or cleanup 16 | - [ ] Other (please describe): 17 | 18 | ## How Has This Been Tested? 19 | 20 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. 21 | 22 | - [ ] Test A 23 | - [ ] Test B 24 | 25 | **Test Configuration**: 26 | * OS: 27 | * Python Version: 28 | * `a4f-local` Version: 29 | 30 | ## Checklist: 31 | 32 | - [ ] My code follows the style guidelines of this project 33 | - [ ] I have performed a self-review of my own code 34 | - [ ] I have commented my code, particularly in hard-to-understand areas 35 | - [ ] I have made corresponding changes to the documentation 36 | - [ ] My changes generate no new warnings 37 | - [ ] I have added tests that prove my fix is effective or that my feature works 38 | - [ ] New and existing unit tests pass locally with my changes 39 | - [ ] Any dependent changes have been merged and published in downstream modules 40 | -------------------------------------------------------------------------------- /a4f_local/types/audio.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import Literal, Optional 3 | 4 | # Based on OpenAI's API documentation for /v1/audio/speech 5 | 6 | class SpeechCreateRequest(BaseModel): 7 | """ 8 | Pydantic model for TTS request, mirroring OpenAI's structure. 9 | """ 10 | model: str = Field(..., description="One of the available TTS models, e.g., tts-1 or tts-1-hd") 11 | input: str = Field(..., max_length=4096, description="The text to generate audio from. The maximum length is 4096 characters.") 12 | voice: Literal["alloy", "echo", "fable", "onyx", "nova", "shimmer", "ash", "coral", "sage"] = Field(..., description="The voice to use for synthesis.") 13 | response_format: Optional[Literal["mp3", "opus", "aac", "flac", "wav", "pcm"]] = Field( 14 | default="mp3", 15 | description="The format to audio in. Supported formats are mp3, opus, aac, flac, wav, and pcm." 16 | ) 17 | speed: Optional[float] = Field( 18 | default=1.0, 19 | ge=0.25, 20 | le=4.0, 21 | description="The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default." 22 | ) 23 | 24 | # Allow extra fields to accommodate potential provider-specific parameters 25 | # passed via **kwargs in the client method, although ideally these should 26 | # be handled explicitly during translation if possible. 27 | class Config: 28 | extra = 'allow' 29 | 30 | # Note: The response from OpenAI's API is typically the raw audio bytes. 31 | # If a structured response object were needed (e.g., containing metadata), 32 | # it would be defined here as well (e.g., class AudioResponse(BaseModel): ...). 33 | # For now, the client expects raw bytes directly from the engine. 34 | -------------------------------------------------------------------------------- /a4f_local/types/chat.py: -------------------------------------------------------------------------------- 1 | from pydantic import BaseModel, Field 2 | from typing import List, Optional, Dict, Any, Literal 3 | 4 | # Placeholder models based loosely on OpenAI's Chat Completion API 5 | # These should be refined when implementing a chat provider. 6 | 7 | class ChatCompletionMessageParam(BaseModel): 8 | role: Literal["system", "user", "assistant", "tool"] 9 | content: str | List[Dict[str, Any]] # Content can be string or complex (e.g., for vision) 10 | # name: Optional[str] = None # Optional name for tool/assistant roles 11 | # tool_call_id: Optional[str] = None # For tool responses 12 | 13 | class ChatCompletionRequest(BaseModel): 14 | model: str 15 | messages: List[ChatCompletionMessageParam] 16 | # Add other common parameters like temperature, max_tokens, stream, etc. as needed 17 | temperature: Optional[float] = Field(default=1.0) 18 | max_tokens: Optional[int] = None 19 | stream: Optional[bool] = Field(default=False) 20 | 21 | class Config: 22 | extra = 'allow' # Allow provider-specific params 23 | 24 | # Placeholder for the response structure 25 | class ChoiceDelta(BaseModel): 26 | content: Optional[str] = None 27 | role: Optional[Literal["assistant"]] = None 28 | # tool_calls: Optional[List[Any]] = None # Placeholder 29 | 30 | class Choice(BaseModel): 31 | index: int 32 | message: Optional[ChatCompletionMessageParam] = None # For non-streaming 33 | delta: Optional[ChoiceDelta] = None # For streaming 34 | finish_reason: Optional[str] = None 35 | 36 | class ChatCompletion(BaseModel): 37 | id: str 38 | object: Literal["chat.completion", "chat.completion.chunk"] 39 | created: int 40 | model: str 41 | choices: List[Choice] 42 | # usage: Optional[CompletionUsage] = None # Placeholder for token usage info 43 | 44 | class Config: 45 | extra = 'allow' 46 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "a4f-local" 7 | version = "0.1.1" 8 | description = "A unified wrapper for various reverse-engineered AI provider APIs, aiming for OpenAI compatibility." 9 | readme = "README.md" 10 | requires-python = ">=3.8" 11 | license = { file = "LICENSE" } 12 | authors = [ 13 | { name = "DevsDoCode" }, 14 | ] 15 | keywords = ["openai", "tts", "ai", "wrapper", "reverse-engineering", "api", "text-to-speech"] 16 | classifiers = [ 17 | "Development Status :: 3 - Alpha", # Initial development stage 18 | "Programming Language :: Python :: 3", 19 | "License :: Other/Proprietary License", # Custom license 20 | "Operating System :: OS Independent", 21 | "Intended Audience :: Developers", 22 | "Topic :: Software Development :: Libraries :: Python Modules", 23 | "Topic :: Scientific/Engineering :: Artificial Intelligence", 24 | ] 25 | dependencies = [ 26 | "requests>=2.20.0", 27 | "pydantic>=2.0.0", 28 | ] 29 | 30 | [project.urls] 31 | Homepage = "https://github.com/Devs-Do-Code/a4f-local" 32 | Repository = "https://github.com/Devs-Do-Code/a4f-local" 33 | YouTube = "https://www.youtube.com/@DevsDoCode" 34 | Instagram = "https://www.instagram.com/sree.shades_/" 35 | "Twitter/X" = "https://x.com/Anand_Sreejan" 36 | Telegram = "https://t.me/devsdocode" 37 | Discord = "https://discord.com/invite/4gGcqsWWde" 38 | LinkedIn = "https://www.linkedin.com/in/developer-sreejan/" 39 | "Personal GitHub" = "https://github.com/SreejanPersonal" 40 | 41 | [project.optional-dependencies] 42 | test = [ 43 | "pytest>=6.0", 44 | ] 45 | docs = [ 46 | "sphinx>=4.0.0", 47 | "sphinx-rtd-theme>=1.0.0", 48 | # Add other docs dependencies from docs/requirements.txt if needed 49 | ] 50 | dev = [ 51 | "a4f-local[test,docs]", # Includes test and docs dependencies 52 | # Add other dev tools like linters (flake8, black, mypy), build tools etc. 53 | "build", 54 | "twine", 55 | ] 56 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | cover/ 54 | 55 | # Translations 56 | *.mo 57 | *.pot 58 | 59 | # Django stuff: 60 | *.log 61 | local_settings.py 62 | db.sqlite3 63 | db.sqlite3-journal 64 | 65 | # Flask stuff: 66 | instance/ 67 | .webassets-cache 68 | 69 | # Scrapy stuff: 70 | .scrapy 71 | 72 | # Sphinx documentation 73 | docs/_build/ 74 | docs/build/ # Add this too just in case 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # IPython 83 | profile_default/ 84 | ipython_config.py 85 | 86 | # pyenv 87 | .python-version 88 | 89 | # pipenv 90 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 91 | # Pipfile 92 | 93 | # PEP 582; used by PDM, Flit and potentially other packaging tools. 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | # pytype static analysis results 131 | .pytype/ 132 | 133 | # Cython debug symbols 134 | cython_debug/ 135 | 136 | # Test outputs 137 | *.mp3 138 | tests/*.mp3 139 | tests/*.wav 140 | tests/*.log 141 | 142 | # Project-specific ignores 143 | /bin/ 144 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | import os 7 | import sys 8 | # Point to the project root to find the package 9 | sys.path.insert(0, os.path.abspath('..')) 10 | 11 | # -- Project information ----------------------------------------------------- 12 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 13 | 14 | project = 'a4f-local' 15 | copyright = '2025, DevsDoCode' # Updated copyright holder 16 | author = 'DevsDoCode' # Updated author 17 | 18 | # Attempt to get the version from the package __init__ 19 | try: 20 | from a4f_local import __version__ as release 21 | except ImportError: 22 | release = '0.1.0' # Fallback version 23 | 24 | version = '.'.join(release.split('.')[:2]) # Short X.Y version 25 | 26 | # -- General configuration --------------------------------------------------- 27 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 28 | 29 | extensions = [ 30 | 'sphinx.ext.autodoc', # Include documentation from docstrings 31 | 'sphinx.ext.napoleon', # Support NumPy and Google style docstrings 32 | 'sphinx.ext.intersphinx', # Link to other projects' documentation 33 | 'sphinx.ext.viewcode', # Add links to source code 34 | 'sphinx_rtd_theme', # Use the Read the Docs theme 35 | # Add other extensions here if needed 36 | ] 37 | 38 | templates_path = ['_templates'] 39 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 40 | 41 | # Intersphinx mapping allows linking to other documentation 42 | intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} 43 | 44 | # Autodoc settings 45 | autodoc_member_order = 'bysource' # Keep source order for members 46 | 47 | # Napoleon settings (if using Google/NumPy style docstrings) 48 | napoleon_google_docstring = True 49 | napoleon_numpy_docstring = True 50 | napoleon_include_init_with_doc = True 51 | napoleon_include_private_with_doc = False 52 | napoleon_include_special_with_doc = True 53 | napoleon_use_admonition_for_examples = False 54 | napoleon_use_admonition_for_notes = False 55 | napoleon_use_admonition_for_references = False 56 | napoleon_use_ivar = False 57 | napoleon_use_param = True 58 | napoleon_use_rtype = True 59 | 60 | # -- Options for HTML output ------------------------------------------------- 61 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 62 | 63 | html_theme = 'sphinx_rtd_theme' 64 | html_static_path = ['_static'] 65 | 66 | # Theme options are theme-specific 67 | # html_theme_options = {} 68 | 69 | # Add any paths that contain custom static files (such as style sheets) here, 70 | # relative to this directory. They are copied after the builtin static files, 71 | # so a file named "default.css" will overwrite the builtin "default.css". 72 | # html_static_path = ['_static'] 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # a4f-local 2 | 3 | [![PyPI version](https://img.shields.io/pypi/v/a4f-local.svg?style=flat-square)](https://pypi.org/project/a4f-local/) 4 | [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/a4f-local.svg?style=flat-square)](https://pypi.org/project/a4f-local/) 5 | 6 | A unified, professional Python wrapper for various reverse-engineered AI provider APIs, designed to be **OpenAI-compatible** and **easy to use**. 7 | 8 | --- 9 | 10 | ## Key Features 11 | 12 | - **Unified API:** Call multiple unofficial AI providers with a single, OpenAI-like interface. 13 | - **Modular:** Easily extend with new providers and capabilities. 14 | - **Supports Text-to-Speech (TTS):** Initial implementation includes OpenAI.fm reverse-engineered TTS. 15 | - **OpenAI-Compatible:** Accepts and returns data in the same format as OpenAI's official API. 16 | - **Simple to Use:** Designed for developers of all skill levels. 17 | 18 | --- 19 | 20 | ## Installation 21 | 22 | Install directly from PyPI: 23 | 24 | ```bash 25 | pip install -U a4f-local 26 | ``` 27 | 28 | --- 29 | 30 | ## Usage Example 31 | 32 | ```python 33 | from a4f_local import A4F 34 | 35 | client = A4F() 36 | 37 | try: 38 | audio_bytes = client.audio.speech.create( 39 | model="tts-1", # Model name (currently informational) 40 | input="Hello from a4f-local!", 41 | voice="alloy" # Choose a supported voice 42 | ) 43 | with open("output.mp3", "wb") as f: 44 | f.write(audio_bytes) 45 | print("Generated output.mp3") 46 | except Exception as e: 47 | print(f"An error occurred: {e}") 48 | ``` 49 | 50 | --- 51 | 52 | ## Supported Voices 53 | 54 | The following voice names are supported (mapped to OpenAI's official voices): 55 | 56 | - `alloy` 57 | - `echo` 58 | - `fable` 59 | - `onyx` 60 | - `nova` 61 | - `shimmer` 62 | 63 | Use one of these as the `voice` parameter in your TTS requests. 64 | 65 | --- 66 | 67 | ## How It Works 68 | 69 | - You interact with the `A4F` client **just like OpenAI's Python SDK**. 70 | - The client **automatically discovers** available providers and their capabilities. 71 | - When you call a method (e.g., `client.audio.speech.create()`), it **routes the request** to the appropriate provider. 72 | - The provider **translates** the OpenAI-compatible request into its own API format, calls the API, and **translates the response back**. 73 | 74 | --- 75 | 76 | ## Roadmap 77 | 78 | - **More Providers:** Support for chat, image generation, and other AI capabilities. 79 | - **Configuration:** Easier ways to configure API keys and provider preferences. 80 | - **Async Support:** Async versions of API calls. 81 | - **Better Error Handling:** More informative error messages and exceptions. 82 | 83 | --- 84 | 85 | ## License 86 | 87 | See the [LICENSE](LICENSE) file for details. This software is **not** open source in the traditional sense. Please review the license terms carefully before use. 88 | 89 | --- 90 | 91 | ## Disclaimer 92 | 93 | This package uses **reverse-engineered, unofficial APIs**. These may break or change at any time. Use at your own risk. 94 | 95 | --- 96 | 97 | ## More Information 98 | 99 | More detailed documentation, tutorials, and examples will be published here and on PyPI. 100 | 101 | For now, refer to the example above and the source code for guidance. 102 | 103 | --- 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | a4f-local - Software License Agreement 2 | Copyright (c) 2025 DevsDoCode 3 | All rights reserved. 4 | 5 | Preamble: 6 | This software, a4f-local, is the intellectual property of DevsDoCode. It is made available to the public under the terms of this license to encourage collaboration and innovation while ensuring the original author receives full recognition, control, and, where applicable, compensation. The following terms are binding on all users, distributors, and modifiers of this software. 7 | 8 | 1. Definitions 9 | - "Software" refers to the a4f-local project, including all source code, documentation, and associated files provided by the Author. 10 | - "Author" refers to DevsDoCode, the original creator of the Software. 11 | - "User" refers to any individual or entity exercising permissions granted by this License. 12 | - "Derivative Work" refers to any modification, adaptation, or extension of the Software created by a User. 13 | 14 | 2. Grant of License 15 | Permission is hereby granted, free of charge, to any User to use, copy, modify, and distribute the Software, subject to the conditions outlined in this License. 16 | 17 | 3. Attribution Requirements 18 | - Any User of the Software or Derivative Work must provide prominent attribution to the Author in all copies, distributions, and public uses. Attribution must include: 19 | a. The full text: "Originally created by DevsDoCode - all rights reserved." 20 | b. A hyperlink to the Author's GitHub profile: https://github.com/Devs-Do-Code 21 | c. A clear statement acknowledging the Author’s role as the original creator in relevant documentation, user interfaces (if applicable), README files, and any associated marketing materials. 22 | - Failure to provide this attribution in a conspicuous manner voids the User’s rights under this License. 23 | 24 | 4. Open-Source Mandate (Share-Alike) 25 | - Any Derivative Work based on the Software must be distributed under the exact terms of this License. 26 | - The complete source code of any Derivative Work must be made publicly available alongside its distribution, at no additional cost beyond reasonable distribution costs. 27 | - Users creating Derivative Works must notify the Author via LinkedIn (https://www.linkedin.com/in/developer-sreejan/) within 30 days of the first public distribution, providing a link to the Derivative Work’s publicly accessible source code repository. 28 | 29 | 5. Restriction on Commercial Use 30 | - The Software and any Derivative Work may not be used for commercial purposes (defined as any use primarily intended for or directed towards commercial advantage or monetary compensation) without explicit prior written permission from the Author. 31 | - To request permission for commercial use, contact the Author via LinkedIn: https://www.linkedin.com/in/developer-sreejan/ 32 | - If commercial use is permitted by the Author in writing: 33 | a. The User must strictly adhere to the attribution requirements specified in Section 3. 34 | b. The User agrees to pay the Author a royalty of 25% of all net income derived directly from the commercial use of the Software or Derivative Work. "Net income" is defined as gross revenue minus direct operational expenses related to the Software/Derivative Work. Royalties are payable quarterly. 35 | c. The User must submit a written report to the Author every quarter detailing the income generated and relevant expenses incurred, along with the royalty payment. Reports and payments should be arranged via the contact method specified above (LinkedIn). 36 | - Unauthorized commercial use constitutes a violation of this License, automatically terminates all rights granted herein, and may subject the User to legal action. 37 | 38 | 6. Modifications and Derivative Works 39 | - Users may modify the Software, provided that: 40 | a. All modifications are clearly documented, distinguishing them from the original Software. The original Author's foundational role must remain clear. 41 | b. Derivative Works do not state or imply endorsement by the Author unless explicit written consent is obtained. 42 | c. The Author reserves the right to request the removal of their name or attribution from any Derivative Work if, in the Author's sole judgment, the Derivative Work significantly misrepresents or deviates negatively from the original intent or quality of the Software. 43 | 44 | 7. Disclaimer of Warranty and Limitation of Liability 45 | - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 46 | - IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 47 | 48 | 8. Termination 49 | - This License and the rights granted hereunder will terminate automatically if the User fails to comply with any of its terms. 50 | - Upon termination, the User must immediately cease all use, copying, modification, and distribution of the Software and destroy all copies in their possession or control. 51 | 52 | 9. Governing Law 53 | - This License shall be governed by and construed in accordance with the laws of the jurisdiction in which the Author resides, without regard to its conflict of law provisions. 54 | -------------------------------------------------------------------------------- /a4f_local/client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Optional, Type 3 | 4 | # Import discovery functions (will be created soon) 5 | from .providers import _discovery 6 | # Import type hints for request/response objects (will be created soon) 7 | from .types.audio import SpeechCreateRequest # Example for TTS 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | class Speech: 12 | """Handles Text-to-Speech related API calls.""" 13 | def __init__(self, client: 'A4F'): 14 | self._client = client 15 | 16 | def create(self, *, model: str, input: str, voice: str, **kwargs: Any) -> bytes: 17 | """ 18 | Generates audio from the input text. Mimics OpenAI's audio.speech.create. 19 | 20 | Args: 21 | model: The model to use (e.g., "tts-1"). This might influence provider selection. 22 | input: The text to synthesize. 23 | voice: The voice to use (e.g., "alloy"). 24 | **kwargs: Additional provider-specific arguments. 25 | 26 | Returns: 27 | The generated audio content as bytes. 28 | 29 | Raises: 30 | NotImplementedError: If no provider supports the 'tts' capability. 31 | Exception: If the selected provider's engine fails. 32 | """ 33 | capability = "tts" 34 | logger.debug(f"Attempting to find provider for capability: {capability}") 35 | 36 | # 1. Find a provider for the capability 37 | # TODO: Enhance provider selection based on model or other criteria if needed. 38 | provider_name = _discovery.get_provider_for_capability(capability) 39 | if not provider_name: 40 | logger.error(f"No provider found supporting capability: {capability}") 41 | raise NotImplementedError(f"No configured provider supports the '{capability}' capability.") 42 | 43 | logger.info(f"Selected provider '{provider_name}' for capability '{capability}'") 44 | 45 | # 2. Get the engine function from the selected provider 46 | engine_func = _discovery.get_engine(provider_name, capability) 47 | if not engine_func: 48 | # This should ideally not happen if discovery worked correctly 49 | logger.error(f"Provider '{provider_name}' found but engine function for '{capability}' is missing.") 50 | raise RuntimeError(f"Internal error: Engine function missing for {provider_name}.{capability}") 51 | 52 | # 3. Prepare the request object (using Pydantic model recommended) 53 | # We assume the engine function expects an object matching the type hint 54 | # For simplicity here, we pass required args directly, but using the Pydantic 55 | # model is better for validation and structure. 56 | # request_obj = SpeechCreateRequest(model=model, input=input, voice=voice, **kwargs) 57 | 58 | # 4. Call the provider's engine function 59 | logger.debug(f"Calling engine function for {provider_name}.{capability}") 60 | try: 61 | # Pass arguments consistent with OpenAI's structure. 62 | # The engine function is responsible for translating these. 63 | # Using a structured request object is preferred: 64 | # return engine_func(request=request_obj) 65 | 66 | # Simplified call for now, assuming engine accepts kwargs or specific args: 67 | # Note: This assumes the engine function signature matches these named args. 68 | # A better approach is to pass a single request object. 69 | request_data = SpeechCreateRequest(model=model, input=input, voice=voice, **kwargs) 70 | return engine_func(request=request_data) 71 | 72 | except Exception as e: 73 | logger.exception(f"Error executing engine function for {provider_name}.{capability}: {e}") 74 | # Re-raise the exception for the caller to handle 75 | raise e 76 | 77 | class Audio: 78 | """Groups audio-related capabilities.""" 79 | def __init__(self, client: 'A4F'): 80 | self.speech = Speech(client) 81 | # Add other audio capabilities here (e.g., transcriptions) 82 | # self.transcriptions = Transcriptions(client) 83 | 84 | # --- Placeholder for other capabilities --- 85 | # class ChatCompletions: ... 86 | # class Chat: def __init__(self, client): self.completions = ChatCompletions(client) 87 | # class Images: ... 88 | 89 | class A4F: 90 | """ 91 | Main client class for interacting with various AI providers through a unified, 92 | OpenAI-compatible interface. 93 | """ 94 | def __init__( 95 | self, 96 | # Potential future args: api_key, base_url (if needed for a default provider) 97 | # provider_config: Optional[Dict[str, Any]] = None # For passing config down 98 | ): 99 | """ 100 | Initializes the A4F client. 101 | 102 | Currently, initialization is simple. It might be extended later to handle 103 | authentication or specific provider configurations. 104 | """ 105 | # Ensure providers are discovered when the client is instantiated 106 | # (can be moved to package __init__ if preferred) 107 | _discovery.find_providers() 108 | logger.info(f"A4F Client Initialized. Discovered providers: {list(_discovery.PROVIDER_CAPABILITIES.keys())}") 109 | 110 | # Instantiate capability groups 111 | self.audio = Audio(self) 112 | # self.chat = Chat(self) 113 | # self.images = Images(self) 114 | # ... add other top-level capabilities like 'models', 'files' etc. 115 | 116 | # Potentially add methods for listing models, etc., if needed 117 | # def list_models(self): ... 118 | -------------------------------------------------------------------------------- /a4f_local/providers/provider_1/tts/engine.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | from ....types.audio import SpeechCreateRequest 4 | 5 | # Optional: Import from provider_1.config if URL/headers/secrets were stored there 6 | # from ..config import PROVIDER_URL, PROVIDER_HEADERS # Example if config.py existed 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | # Define standard OpenAI voices supported by this specific provider (OpenAI.fm) 11 | # Includes standard voices and potentially newer ones if the backend supports them. 12 | SUPPORTED_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer", "ash", "coral", "sage"] 13 | 14 | # Headers based on the reverse-engineered request in the guide 15 | # Consider moving sensitive or frequently changing parts to a config file or env vars later 16 | PROVIDER_HEADERS = { 17 | "accept": "*/*", 18 | "accept-encoding": "gzip, deflate, br, zstd", 19 | "accept-language": "en-US,en;q=0.9,hi;q=0.8", 20 | "dnt": "1", 21 | "origin": "https://www.openai.fm", 22 | "referer": "https://www.openai.fm/", # Simplified referer, adjust if needed 23 | # Using a generic user-agent might be less likely to break than a specific worker JS referer 24 | "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" 25 | } 26 | PROVIDER_URL = "https://www.openai.fm/api/generate" 27 | 28 | def create_speech(*, request: SpeechCreateRequest) -> bytes: 29 | """ 30 | Generates speech using the OpenAI.fm reverse-engineered API. 31 | This function acts as the engine for the 'tts' capability of 'provider_1'. 32 | 33 | Args: 34 | request: An object matching OpenAI's SpeechCreateRequest schema, 35 | containing input text, voice, model, etc. 36 | 37 | Returns: 38 | MP3 audio content as bytes on success. 39 | 40 | Raises: 41 | ValueError: If the requested voice is not supported by this provider. 42 | requests.exceptions.RequestException: If the API call fails (network issue, bad status code). 43 | # Consider adding custom exceptions for specific API errors if identifiable 44 | """ 45 | logger.info(f"Provider 1 (OpenAI.fm) received TTS request for voice: {request.voice}") 46 | 47 | # --- Input Validation --- 48 | if request.voice not in SUPPORTED_VOICES: 49 | logger.error(f"Unsupported voice requested for Provider 1: {request.voice}") 50 | raise ValueError(f"Provider 'OpenAI.fm TTS' does not support voice: {request.voice}. Supported: {SUPPORTED_VOICES}") 51 | 52 | # --- Input Translation (OpenAI Schema -> Provider API Schema) --- 53 | # The OpenAI.fm API requires a 'prompt' field which isn't standard in OpenAI's TTS. 54 | # We need to construct it. The guide suggests a placeholder. 55 | # Future improvement: Allow passing custom prompts via kwargs or infer based on input/speed/etc. 56 | # Note: OpenAI's 'speed' parameter isn't directly supported by the example API call. 57 | if request.speed != 1.0: 58 | logger.warning(f"Provider 1 (OpenAI.fm) does not support the 'speed' parameter (requested: {request.speed}). Using default speed.") 59 | 60 | # Constructing the 'prompt' based on the guide's simplified example 61 | voice_prompt = f"Voice: {request.voice}. Standard clear voice." 62 | logger.debug(f"Constructed voice prompt for OpenAI.fm: '{voice_prompt}'") 63 | 64 | payload = { 65 | "input": request.input, 66 | "prompt": voice_prompt, 67 | "voice": request.voice, # Directly maps from the validated OpenAI voice 68 | "vibe": "null" # As per the reverse-engineered request 69 | } 70 | logger.debug(f"Payload for OpenAI.fm API: {payload}") 71 | 72 | # --- API Call --- 73 | try: 74 | logger.info(f"Calling OpenAI.fm API at {PROVIDER_URL}") 75 | response = requests.post(PROVIDER_URL, headers=PROVIDER_HEADERS, data=payload, timeout=60) # Added timeout 76 | response.raise_for_status() # Raises HTTPError for 4xx/5xx responses 77 | 78 | # --- Output Translation (Provider API Response -> OpenAI Expected Output) --- 79 | # The OpenAI.fm API returns raw MP3 bytes directly in the response body on success. 80 | # This matches the expected output format (bytes) for the client's speech.create method. 81 | # No further translation is needed for the success case here. 82 | audio_content = response.content 83 | logger.info(f"Successfully received {len(audio_content)} bytes of audio data from OpenAI.fm.") 84 | return audio_content 85 | 86 | except requests.exceptions.Timeout: 87 | logger.error(f"Timeout occurred while calling OpenAI.fm API: {PROVIDER_URL}") 88 | raise # Re-raise the timeout exception 89 | except requests.exceptions.HTTPError as http_err: 90 | # Log specific HTTP errors 91 | error_body = http_err.response.text 92 | logger.error(f"HTTP error occurred calling OpenAI.fm API: {http_err.response.status_code} - {http_err.response.reason}. Response: {error_body[:500]}") # Log first 500 chars 93 | # Re-raise the original exception, potentially wrap in a custom one later 94 | raise http_err 95 | except requests.exceptions.RequestException as req_err: 96 | # Catch other request errors (DNS failure, connection refused, etc.) 97 | logger.error(f"Network error occurred calling OpenAI.fm API: {req_err}") 98 | raise req_err # Re-raise 99 | except Exception as e: 100 | # Catch any other unexpected errors during the process 101 | logger.exception(f"An unexpected error occurred in Provider 1 TTS engine: {e}") 102 | raise e # Re-raise 103 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement via LinkedIn at 63 | https://www.linkedin.com/in/developer-sreejan/. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder][mozilla ladder]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][faq]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [mozilla ladder]: https://github.com/mozilla/diversity 131 | [faq]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /a4f_local/providers/_discovery.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import pkgutil 3 | import os 4 | import logging 5 | from typing import Dict, List, Callable, Any, Optional 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | # Registry to hold discovered engine functions: { 'provider_1': {'tts': create_speech_func}, ... } 10 | PROVIDER_REGISTRY: Dict[str, Dict[str, Callable]] = {} 11 | # Registry to hold discovered capabilities per provider: { 'provider_1': ['tts'], ... } 12 | PROVIDER_CAPABILITIES: Dict[str, List[str]] = {} 13 | # Optional: Store provider metadata like human-readable names 14 | PROVIDER_METADATA: Dict[str, Dict[str, Any]] = {} 15 | 16 | def find_providers(): 17 | """ 18 | Scans the 'providers' directory dynamically, imports provider modules, 19 | reads their capabilities, imports corresponding engine modules, and populates 20 | the PROVIDER_REGISTRY and PROVIDER_CAPABILITIES. 21 | """ 22 | global PROVIDER_REGISTRY, PROVIDER_CAPABILITIES, PROVIDER_METADATA 23 | # Clear registries to allow for potential re-discovery if needed 24 | PROVIDER_REGISTRY.clear() 25 | PROVIDER_CAPABILITIES.clear() 26 | PROVIDER_METADATA.clear() 27 | 28 | providers_package_path = os.path.dirname(__file__) # Path to the 'providers' directory 29 | logger.info(f"Scanning for providers in: {providers_package_path}") 30 | 31 | # Use the package context for relative imports 32 | current_package = __package__ or 'a4f_local.providers' 33 | 34 | for _, provider_name, is_pkg in pkgutil.iter_modules([providers_package_path]): 35 | # Look for subdirectories that are packages and follow the naming convention 36 | if is_pkg and provider_name.startswith("provider_"): 37 | logger.debug(f"Found potential provider package: {provider_name}") 38 | try: 39 | # Import the provider's __init__.py to get capabilities and metadata 40 | provider_module_path = f".{provider_name}" # Relative import path 41 | provider_init = importlib.import_module(provider_module_path, package=current_package) 42 | 43 | if hasattr(provider_init, 'CAPABILITIES') and isinstance(provider_init.CAPABILITIES, list): 44 | capabilities = provider_init.CAPABILITIES 45 | PROVIDER_CAPABILITIES[provider_name] = capabilities 46 | PROVIDER_REGISTRY[provider_name] = {} # Initialize registry for this provider 47 | PROVIDER_METADATA[provider_name] = { 48 | 'name': getattr(provider_init, 'PROVIDER_NAME', provider_name) # Get optional name 49 | } 50 | logger.info(f"Discovered provider '{provider_name}' with capabilities: {capabilities}") 51 | 52 | # Dynamically import the engine module for each declared capability 53 | for capability in capabilities: 54 | try: 55 | capability_module_path = f".{provider_name}.{capability}" 56 | capability_module = importlib.import_module(capability_module_path, package=current_package) 57 | 58 | # --- Engine Function Discovery Convention --- 59 | # Assume the capability module's __init__.py exports the main engine function, 60 | # OR look for a conventionally named function (e.g., create_{capability}). 61 | # Option 1: Check __all__ if defined in capability/__init__.py 62 | engine_func = None 63 | if hasattr(capability_module, '__all__') and capability_module.__all__: 64 | engine_func_name = capability_module.__all__[0] # Assume first is the entry point 65 | if hasattr(capability_module, engine_func_name): 66 | engine_func = getattr(capability_module, engine_func_name) 67 | else: 68 | # Option 2: Fallback to convention if __all__ is not helpful 69 | conventional_func_name = f"create_{capability}" # e.g., create_tts, create_chat 70 | if hasattr(capability_module, conventional_func_name): 71 | engine_func = getattr(capability_module, conventional_func_name) 72 | 73 | if engine_func and callable(engine_func): 74 | PROVIDER_REGISTRY[provider_name][capability] = engine_func 75 | logger.debug(f" Registered engine for '{capability}' capability.") 76 | else: 77 | logger.warning(f"Could not find or register callable engine function for {provider_name}.{capability}") 78 | 79 | except ImportError as e_cap: 80 | logger.warning(f"Could not import capability module '{capability}' for provider '{provider_name}': {e_cap}") 81 | except Exception as e_eng: 82 | logger.error(f"Error processing engine for {provider_name}.{capability}: {e_eng}") 83 | 84 | else: 85 | logger.warning(f"Skipping '{provider_name}': Does not have a valid 'CAPABILITIES' list in its __init__.py.") 86 | 87 | except ImportError as e_prov: 88 | logger.warning(f"Could not import provider module '{provider_name}': {e_prov}") 89 | except Exception as e_gen: 90 | logger.error(f"Error processing provider '{provider_name}': {e_gen}") 91 | 92 | logger.info(f"Provider discovery complete. Registry: {list(PROVIDER_REGISTRY.keys())}") 93 | 94 | 95 | def get_provider_for_capability(capability: str) -> Optional[str]: 96 | """ 97 | Finds the first discovered provider that supports a given capability. 98 | Note: This uses a simple first-match strategy. Could be enhanced later 99 | (e.g., based on configuration, model compatibility, or priority). 100 | """ 101 | for provider_name, capabilities in PROVIDER_CAPABILITIES.items(): 102 | if capability in capabilities: 103 | logger.debug(f"Found provider '{provider_name}' for capability '{capability}'") 104 | return provider_name 105 | logger.warning(f"No provider found supporting capability: {capability}") 106 | return None 107 | 108 | def get_engine(provider_name: str, capability: str) -> Optional[Callable]: 109 | """Gets the registered engine function for a specific provider and capability.""" 110 | provider_engines = PROVIDER_REGISTRY.get(provider_name) 111 | if provider_engines: 112 | engine_func = provider_engines.get(capability) 113 | if engine_func: 114 | return engine_func 115 | else: 116 | logger.error(f"Capability '{capability}' not found in registry for provider '{provider_name}'") 117 | else: 118 | logger.error(f"Provider '{provider_name}' not found in registry.") 119 | return None 120 | 121 | # --- Auto-run discovery when this module is imported --- 122 | # This ensures the registry is populated when the client or other parts of the package need it. 123 | find_providers() 124 | --------------------------------------------------------------------------------