Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
031c3c6
Bump jinja2 from 3.1.4 to 3.1.5
dependabot[bot] Dec 24, 2024
64b06c6
Merge pull request #37 from uhlive/dependabot/pip/jinja2-3.1.5
merwan Dec 30, 2024
b7a44fe
Bump jinja2 from 3.1.5 to 3.1.6
dependabot[bot] Mar 11, 2025
9057e48
Merge pull request #38 from uhlive/dependabot/pip/jinja2-3.1.6
merwan Mar 11, 2025
d63c1ad
Bump requests from 2.32.3 to 2.32.4
dependabot[bot] Jun 10, 2025
ff1559d
Merge pull request #39 from uhlive/dependabot/pip/requests-2.32.4
merwan Jun 10, 2025
d243406
Bump urllib3 from 2.2.2 to 2.5.0
dependabot[bot] Jun 19, 2025
de46fd1
Merge pull request #40 from uhlive/dependabot/pip/urllib3-2.5.0
rtxm Jun 19, 2025
91017e2
Bump aiohttp from 3.11.7 to 3.12.14
dependabot[bot] Jul 15, 2025
93fd827
Fix aiohttp dependencies
merwan Jul 15, 2025
02d0a4f
Merge pull request #41 from uhlive/dependabot/pip/aiohttp-3.12.14
merwan Jul 15, 2025
eb16088
New demo: phone numbers.
rtxm Oct 23, 2025
08a3c70
Ported project to 'uv'
rtxm Oct 23, 2025
552a310
Formatted by black.
rtxm Oct 23, 2025
6d9cfdc
Replaced 'tox' by 'just'.
rtxm Oct 23, 2025
42821ad
Universal annotations.
rtxm Oct 24, 2025
994a3d2
Next release is 2.0.0.
rtxm Oct 24, 2025
b8af191
Updated README.
rtxm Oct 24, 2025
aa96049
Custom netlify build script.
rtxm Oct 24, 2025
2e0796e
Netlify: try another installation method.
rtxm Oct 24, 2025
7b85e63
Updated changelog.
rtxm Oct 24, 2025
1ba42e9
Run the examples with 'uv'.
rtxm Oct 24, 2025
8b1a54c
Update docs/index.md
rtxm Oct 30, 2025
4eeb30a
Update .circleci/config.yml
rtxm Oct 30, 2025
9e0c5e7
Update justfile
rtxm Oct 30, 2025
34f76ea
Update examples/recognition/desktop-bot_async.py
rtxm Oct 30, 2025
2c4b6f9
Update pyproject.toml
rtxm Oct 30, 2025
07aa93b
Support python 3.10 still.
rtxm Oct 30, 2025
caadf66
Merge pull request #42 from uhlive/feature/sc-61334/universal-annotat…
rtxm Oct 31, 2025
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"
- "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
Loading