Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding api-inference-community to huggingface_hub. #48

Merged
merged 9 commits into from
May 25, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/python-api-quality.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: Check code quality
Narsil marked this conversation as resolved.
Show resolved Hide resolved

on: push
Narsil marked this conversation as resolved.
Show resolved Hide resolved

jobs:
run_tests:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v1
with:
python-version: 3.8
- name: Install dependencies
run: |
pip install black isort flake8 mypy
- name: Make quality
workdir: api-inference-community
run: |
make quality
33 changes: 33 additions & 0 deletions .github/workflows/python-api-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Python-tests
Narsil marked this conversation as resolved.
Show resolved Hide resolved

on:
push:
branches:
- "*"
Narsil marked this conversation as resolved.
Show resolved Hide resolved

jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8"]

steps:
- run: |
sudo apt-get install ffmpeg

- uses: actions/checkout@v2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}

- name: Install dependencies
workdir: api-inference-community
run: |
pip install --upgrade pip
pip install pytest pillow httpx
pip install -e .
- run: make test
workdir: api-inference-community
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,10 @@ We add a custom "Use in Asteroid" button.

When clicked you get a library-specific code sample that you'll be able to specify. 🔥

## API integration into the huggingface.co hub
Narsil marked this conversation as resolved.
Show resolved Hide resolved

In order to get functional widgets on the hub for your models check out this [doc](https://github.com/huggingface/huggingface_hub/tree/master/api-inference-community)
Narsil marked this conversation as resolved.
Show resolved Hide resolved

<br>

## Feedback (feature requests, bugs, etc.) is super welcome 💙💚💛💜♥️🧡
9 changes: 9 additions & 0 deletions api-inference-community/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
__pycache__/
*.py[cod]

# Speechbrain artefact.
pretrained_checkpoints

*.egg-info/
build/
dist/
64 changes: 64 additions & 0 deletions api-inference-community/.pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
repos:
- repo: https://github.com/ambv/black
rev: 20.8b1
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.3
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.7.0 # Use the revision sha / tag you want to point at
hooks:
- id: isort
- repo: https://github.com/pre-commit/mirrors-mypy
rev: "b84ce099a2fd3c5216b6ccf3fd176c3828b075fb" # Use the sha / tag you want to point at
hooks:
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
files: ^docker_images/common/
entry: mypy docker_images/common/
pass_filenames: false
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
files: ^docker_images/speechbrain/
entry: mypy docker_images/speechbrain/
pass_filenames: false
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
files: ^docker_images/kasteroid/
entry: mypy docker_images/asteroid/
pass_filenames: false
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
files: ^docker_images/allennlp/
entry: mypy docker_images/allennlp/
pass_filenames: false
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
files: ^docker_images/espnet/
entry: mypy docker_images/espnet/
pass_filenames: false
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
files: ^docker_images/timm/
entry: mypy docker_images/timm/
pass_filenames: false
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
files: ^docker_images/flair/
entry: mypy docker_images/flair/
pass_filenames: false
- id: mypy
args: [--ignore-missing-imports]
additional_dependencies: [tokenize-rt==3.2.0]
files: ^docker_images/sentence_transformers/
entry: mypy docker_images/sentence_transformers/
pass_filenames: false
1 change: 1 addition & 0 deletions api-inference-community/MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
include README.md requirements.txt
20 changes: 20 additions & 0 deletions api-inference-community/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.PHONY: quality style


check_dirs := .



quality:
black --check $(check_dirs)
isort --check-only $(check_dirs)
flake8 $(check_dirs)

style:
black $(check_dirs)
isort $(check_dirs)


test:
pytest -sv tests/

59 changes: 59 additions & 0 deletions api-inference-community/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@

This repositories enable third-party libraries integrated with [huggingface_hub](https://github.com/huggingface/huggingface_hub/) to create
their own docker so that the widgets on the hub can work as the `transformers` one do.

The hardware to run the API will be provided by Hugging Face for now.

The `docker_images/common` folder is intended to be a starter point for all new libs that
want to be integrated.

### Adding a new container from a new lib.


1. Copy the `docker_images/common` folder into your library's name `docker_images/example`.
2. Edit:
- `docker_images/example/requirements.txt`
- `docker_images/example/app/main.py`
- `docker_images/example/app/pipelines/{task_name}.py`
to implement the desired functionnality. All required code is marked with `IMPLEMENT_THIS` markup.
3. Feel free to customize anything required by your lib everywhere you want. The only real requirements, are to honor the HTTP endpoints, in the same fashion as the `common` folder for all your supported tasks.
4. Edit `example/tests/test_api.py` to add TESTABLE_MODELS.
5. Pass the test suite `pytest -sv --rootdir docker_images/example/ docker_images/example/`
6. Enjoy !

### Developping while updating `api-inference-community`.

If you ever come across a bug within `api-inference-community` or want to update it
the developpement process is slightly more involved.

- First, make sure you need to change this package, each framework is very autonomous
so if your code can get away by being standalone go that way first as it's much simpler.
- If you can make the change only in `api-inference-community` without depending on it
that's also a great option. Make sure to add the proper tests to your PR.
- Finally, the best way to go is to develop locally using `manage.py` command:
- Do the necessary modifications within `api-inference-community` first.
- Install it locally in your environment with `pip install -e .`
- Install your package dependencies locally.
- Run your webserver locally: `./manage.py start --framework example --task audio-source-separation --model-id MY_MODEL`
- When everything is working, you will need to split your PR in two, 1 for the `api-inference-community` part.
The second one will be for your package specific modifications and will only land once the `api-inference-community`
tag has landed.
- This workflow is still work in progress, don't hesitate to ask questions to maintainers.

Another similar command `./manage.py docker --framework example --task audio-source-separation --model-id MY_MODEL`
Will launch the server, but this time in a protected, controlled docker environment making sure the behavior
will be exactly the one in the API.



### Available tasks

- **Automatic speech recognition**: Input is a file, output is a dict of understood words being said within the file
- **Text generation**: Input is a text, output is a dict of generated text
- **Image recognition**: Input is an image, output is a dict of generated text
- **Question answering**: Input is a question + some context, output is a dict containing necessary information to locate the answer to the `question` within the `context`.
- **Audio source separation**: Input is some audio, and the output is n audio files that sum up to the original audio but contain individual soures of sound (either speakers or instruments for instant).
- **Token classification**: Input is some text, and the output is a list of entities mentionned in the text. Entities can be anything remarquable like locations, organisations, persons, times etc...
- **Text to speech**: Input is some text, and the output is an audio file saying the text...
- **Sentence Similarity**: Input is some sentence and a list of reference sentences, and the list of similarity scores.

113 changes: 113 additions & 0 deletions api-inference-community/api_inference_community/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import logging
import os
import time
from typing import Any, Dict

from api_inference_community.validation import ffmpeg_convert, normalize_payload
from pydantic import ValidationError
from starlette.requests import Request
from starlette.responses import JSONResponse, Response


HF_HEADER_COMPUTE_TIME = "x-compute-time"
HF_HEADER_COMPUTE_TYPE = "x-compute-type"
HF_HEADER_COMPUTE_CHARACTERS = "x-compute-characters"
COMPUTE_TYPE = os.getenv("COMPUTE_TYPE", "cpu")

logger = logging.getLogger(__name__)


async def pipeline_route(request: Request) -> Response:
start = time.time()
payload = await request.body()
task = os.environ["TASK"]
pipe = request.app.pipeline
try:
sampling_rate = pipe.sampling_rate
except Exception:
sampling_rate = None
try:
inputs, params = normalize_payload(payload, task, sampling_rate=sampling_rate)
except ValidationError as e:
errors = []
for error in e.errors():
errors.append(f'{error["msg"]}: `{error["loc"][0]}` in `parameters`')
return JSONResponse({"error": errors}, status_code=400)
except (EnvironmentError, ValueError) as e:
return JSONResponse({"error": str(e)}, status_code=400)
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)

return call_pipe(pipe, inputs, params, start)


def call_pipe(pipe: Any, inputs, params: Dict, start: float) -> Response:
root_logger = logging.getLogger()
warnings = set()

class RequestsHandler(logging.Handler):
def emit(self, record):
"""Send the log records (created by loggers) to
the appropriate destination.
"""
warnings.add(record.getMessage())

handler = RequestsHandler()
handler.setLevel(logging.WARNING)
root_logger.addHandler(handler)
for _logger in logging.root.manager.loggerDict.values(): # type: ignore
try:
_logger.addHandler(handler)
except Exception:
pass

status_code = 200
n_characters = 0
try:
outputs = pipe(inputs)
n_characters = get_input_characters(inputs)
except (AssertionError, ValueError) as e:
outputs = {"error": str(e)}
status_code = 400
except Exception as e:
outputs = {"error": "unknown error"}
status_code = 500
logger.error(f"There was an inference error: {e}")

if warnings and isinstance(outputs, dict):
outputs["warnings"] = list(sorted(warnings))

compute_type = COMPUTE_TYPE
headers = {
HF_HEADER_COMPUTE_TIME: "{:.3f}".format(time.time() - start),
HF_HEADER_COMPUTE_TYPE: compute_type,
# https://stackoverflow.com/questions/43344819/reading-response-headers-with-fetch-api/44816592#44816592
"access-control-expose-headers": f"{HF_HEADER_COMPUTE_TYPE}, {HF_HEADER_COMPUTE_TIME}",
}
if status_code == 200:
headers[HF_HEADER_COMPUTE_CHARACTERS] = f"{n_characters}"
if os.getenv("TASK") in {"text-to-speech", "audio-source-separation"}:
# Special case, right now everything is flac audio we can output
waveform, sampling_rate = outputs
data = ffmpeg_convert(waveform, sampling_rate)
headers["content-type"] = "audio/flac"
return Response(data, headers=headers, status_code=status_code)
return JSONResponse(
outputs,
headers=headers,
status_code=status_code,
)


def get_input_characters(inputs) -> int:
if isinstance(inputs, str):
return len(inputs)
elif isinstance(inputs, (tuple, list)):
return sum(get_input_characters(input_) for input_ in inputs)
elif isinstance(inputs, dict):
return sum(get_input_characters(input_) for input_ in inputs.values())
return -1


async def status_ok(request):
return JSONResponse({"ok": "ok"})
Loading