Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
42 changes: 10 additions & 32 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ workflows:
matrix:
parameters:
version:
- "3.7"
- "3.8"
- "3.9"
- "3.10"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should still support Python 3.10, its EOL is set to 2026-10: https://devguide.python.org/versions/#versions

- "3.11"
- "3.12"
Expand Down Expand Up @@ -41,37 +38,31 @@ jobs:

- restore_cache:
keys:
- <<parameters.version>>-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }}
- <<parameters.version>>-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-{{ checksum "setup.py" }}-
- <<parameters.version>>-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-{{ checksum "pyproject.toml" }}-{{ checksum "uv.lock" }}
- <<parameters.version>>-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-{{ checksum "pyproject.toml" }}-
- <<parameters.version>>-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-
- <<parameters.version>>-{{ .Environment.CACHE_VERSION }}-

- run:
name: install requirements
command: |
python -m venv venv
. venv/bin/activate
pip install tox
pip install -e .[examples]
tox --notest # Install all tox dependencies
uv sync --all-extras

- save_cache:
paths:
- venv
- .tox
key: <<parameters.version>>-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-{{ checksum "setup.py" }}-{{ checksum "tox.ini" }}
- .venv
key: <<parameters.version>>-{{ .Environment.CACHE_VERSION }}-{{ .Branch }}-{{ checksum "pyproject.toml" }}-{{ checksum "uv.lock" }}

- run:
name: run linters
command: |
. venv/bin/activate
tox -e mypy,linter
uv run ruff check src examples tests
uv run mypy src examples tests

- run:
name: run tests
command: |
. venv/bin/activate
tox -e py3
uv run python -m unittest discover

deploy:
resource_class: small
Expand All @@ -83,25 +74,12 @@ jobs:
steps:
- checkout

- run:
name: init .pypirc and build env
command: |
python -m venv venv
. venv/bin/activate
pip install -U pip
pip install wheel twine build
echo -e "[pypi]" >> ~/.pypirc
echo -e "username = __token__" >> ~/.pypirc
echo -e "password = $PYPI_TOKEN" >> ~/.pypirc

- run:
name: create packages
command: |
. venv/bin/activate
python -m build
uv build

- run:
name: upload to pypi
command: |
. venv/bin/activate
twine upload dist/*
uv publish --username __token__ --password $PYPI_TOKEN
64 changes: 56 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,70 @@ applications written in the Python language.

Read the [full documentation](https://python-uhlive-sdk.netlify.app/).

## Requirements

### Installation from source
## Installation from source

Install with `pip install .[examples]` to install the the library and all the dependencies necessary to run the examples.
This project uses [`uv`](https://docs.astral.sh/uv/).

### Installation from Pypi
`uv sync --all-extras`

## Installation from Pypi

```
pip install uhlive
```

or as a dependency to a project managed by `uv`:

```
uv add uhlive
```

## Tools

If you have [`just`](https://just.systems/man/en/) and `uv` installed, you have a convenient way to run the tooling.
Otherwise, you can run the commands in the `justfile` manually.

### Format the sources

```
just format
```

Will run `isort` & `black`

### Lint the sources

```
just lint
```

Will run `ruff` & `mypy`

### Run the tests

```
just test
```

### Compile the docs to html

```
just docs
```

### Run format, lint and tests in one go

```
just
```

Contrary to `tox`, it will stop at the first error. So that we're not drown in (duplicate) error messages.

## Usage

See the `README.md` in each of the example folders.

### Audio files

To play with the examples, you should have a raw audio file.
Expand All @@ -25,7 +77,3 @@ using a source audio file in wav format using the following command:
```
sox audio_file.wav -t raw -c 1 -b 16 -r 8k -e signed-integer audio_file.raw
```

## Usage

See the `README.md` in each of the example folders.
9 changes: 8 additions & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,16 @@ Also known as the human to bot (H2B) stream API.

## Changelog

### v1.6.0
### v2.0.0

* Support for the H2H (conversation) protocol version 2 based on universal annotations.
* Legacy H2H protocol v1 is not supported anymore.
* Drop support for Python 3.9 and below

### v1.6.1

* Support for phones.
* Last release to support the H2H protocol version 1.

### v1.5.1

Expand Down
3 changes: 2 additions & 1 deletion examples/conversation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ export UHLIVE_API_SECRET=secret-pass-code
And then:

```
python examples/<python_script>.py -h
uv run python examples/<python_script>.py -h
```



Note that you must use the right token for the right entrypoint URL.
30 changes: 16 additions & 14 deletions examples/recognition/async_bot_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
from aiohttp import ClientSession # type: ignore

from uhlive.auth import build_authentication_request
from uhlive.stream.recognition import Closed
from uhlive.stream.recognition import (
Closed,
)
from uhlive.stream.recognition import CompletionCause as CC
from uhlive.stream.recognition import (
DefaultParams,
Expand Down Expand Up @@ -80,19 +82,6 @@ def callback(indata, frame_count, time_info, status):
class Bot:
TTF_CACHE: Dict[str, bytes] = {}

def __init__(self, google_ttf_key):
self.client = Recognizer()
self.session = None
self.socket = None
self.google_ttf_key = google_ttf_key

async def stream_mic(self):
try:
async for block in inputstream_generator(blocksize=960):
await self.socket.send_bytes(self.client.send_audio_chunk(block))
except asyncio.CancelledError:
pass

async def _ttf(self, text) -> bytes:
if text in self.TTF_CACHE:
return self.TTF_CACHE[text]
Expand All @@ -114,6 +103,19 @@ async def say(self, text):
audio = await self._ttf(text)
await play_buffer(audio)

def __init__(self, google_ttf_key):
self.client = Recognizer()
self.session = None
self.socket = None
self.google_ttf_key = google_ttf_key

async def stream_mic(self):
try:
async for block in inputstream_generator(blocksize=960):
await self.socket.send_bytes(self.client.send_audio_chunk(block))
except asyncio.CancelledError:
pass

async def expect(self, *event_classes, ignore=None):
while True:
msg = await self.socket.receive()
Expand Down
4 changes: 3 additions & 1 deletion examples/recognition/basic_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
from aiohttp import ClientSession # type: ignore

from uhlive.auth import build_authentication_request
from uhlive.stream.recognition import Closed
from uhlive.stream.recognition import (
Closed,
)
from uhlive.stream.recognition import CompletionCause as CC
from uhlive.stream.recognition import (
GrammarDefined,
Expand Down
4 changes: 3 additions & 1 deletion examples/recognition/basic_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
import websocket as ws # type: ignore

from uhlive.auth import build_authentication_request
from uhlive.stream.recognition import Closed
from uhlive.stream.recognition import (
Closed,
)
from uhlive.stream.recognition import CompletionCause as CC
from uhlive.stream.recognition import (
GrammarDefined,
Expand Down
49 changes: 45 additions & 4 deletions examples/recognition/desktop-bot_async.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@
from uhlive.stream.recognition import CompletionCause as CC
from uhlive.stream.recognition import RecognitionComplete, StartOfInput

COUNTRY_LANG = {"france": "fr-FR", "belgique": "fr-BE", "usa": "en-US"}


class DemoBot(Bot):
async def set_defaults(self):
await self.set_params(
speech_language="fr",
speech_language="fr-FR",
no_input_timeout=5000,
recognition_timeout=20000,
speech_complete_timeout=1200,
speech_complete_timeout=1000,
speech_incomplete_timeout=2000,
speech_nomatch_timeout=3000,
)
Expand All @@ -23,7 +25,10 @@ async def set_defaults(self):
"speech/keywords?alternatives=allo\\-media", "activation"
)
await self.define_grammar(
"speech/keywords?alternatives=adresse|multi|arrêt", "menu"
"speech/keywords?alternatives=adresse|multi|arrêt|téléphone", "menu"
)
await self.define_grammar(
"speech/keywords?alternatives=france|belgique|usa", "country"
)
await self.define_grammar(
"speech/spelling/mixed?regex=[a-z][0-9]{3}[a-z]", "subs_num"
Expand Down Expand Up @@ -79,6 +84,40 @@ async def demo_address(self):
await say("tu prononces tellement mal!")
print(result.asr.transcript)

async def demo_phone(self):
say = self.say
while True:
country = await self.ask_until_success(
"Quel pays ?",
"session:country",
recognition_mode="hotword",
)
speech_language = COUNTRY_LANG[country.value]
await say("Composez un numéro de téléphone")
await say(f"à partir de {country.value}")
await self.recognize(
"builtin:speech/spelling/phone_number", speech_language=speech_language
)
resp = await self.expect(RecognitionComplete, ignore=(StartOfInput,))
print(resp.completion_cause)
result = resp.body
if result.asr is None:
await say("Je n'ai rien entendu.")
elif result.nlu is None:
await say(
"Je ne reconnais pas de numéro de téléphone valide dans ce que vous avez dit."
)
print("user said", result.asr.transcript)
else:
phone_number = result.nlu.value
await say("Voici le numéro que j'ai compris au format E164:")
print(phone_number)
confirm = await self.confirm(
"On recommence ?",
)
if not confirm:
break

async def demo_multi(self):
say = self.say
while True:
Expand Down Expand Up @@ -126,7 +165,7 @@ async def scenario(self):
# dialogue
while True:
nlu = await self.ask_until_success(
"Que voulez vous tester ? Adresse ou multi grammaire ?",
"Que voulez vous tester ? Adresse, téléphone ou multi grammaire ?",
"session:menu",
hotword_max_duration=10000,
no_input_timeout=5000,
Expand All @@ -138,6 +177,8 @@ async def scenario(self):
await self.demo_address()
elif keyword == "multi":
await self.demo_multi()
elif keyword == "téléphone":
await self.demo_phone()
elif keyword == "arrêt":
break

Expand Down
4 changes: 3 additions & 1 deletion examples/recognition/sync_bot_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
import websocket as ws # type: ignore

from uhlive.auth import build_authentication_request
from uhlive.stream.recognition import Closed
from uhlive.stream.recognition import (
Closed,
)
from uhlive.stream.recognition import CompletionCause as CC
from uhlive.stream.recognition import (
DefaultParams,
Expand Down
4 changes: 3 additions & 1 deletion examples/recognition/transcribe.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
from basic_sync import AudioStreamer

from uhlive.auth import build_authentication_request
from uhlive.stream.recognition import Closed
from uhlive.stream.recognition import (
Closed,
)
from uhlive.stream.recognition import CompletionCause as CC
from uhlive.stream.recognition import (
Opened,
Expand Down
15 changes: 15 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
default: format lint test

test:
uv run python -m unittest discover

format:
uv run isort --profile black src examples tests
uv run black src examples tests

lint:
uv run ruff check src examples tests
uv run mypy src examples tests

docs:
uv run mkdocs build
Loading