rany2 / edge-tts

Use Microsoft Edge's online text-to-speech service from Python WITHOUT needing Microsoft Edge or Windows or an API key
https://pypi.org/project/edge-tts/
GNU General Public License v3.0
4.21k stars 444 forks source link

Communicate blocks the asyncio loop briefly at the start #223

Closed lanzz closed 1 month ago

lanzz commented 1 month ago

Test case:

import asyncio
import edge_tts

TEXT = (
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt '
    'ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco '
    'laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in '
    'voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat '
    'cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
)

async def edge_request():
    req = edge_tts.Communicate(TEXT)
    return b''.join([chunk['data'] async for chunk in req.stream() if chunk['type'] == 'audio'])

async def dummy_sleep():
    await asyncio.sleep(2)

async def test(fn) -> None:
    print(f'testing {fn!r}')
    edge_task = asyncio.create_task(fn())
    while not edge_task.done():
        await asyncio.wait({edge_task}, timeout=.1)
        print('tick')
    print('done')

asyncio.run(test(edge_request))
asyncio.run(test(dummy_sleep))

It runs the test twice, once executing an edge-tts request, and a second time executing just a regular asyncio.sleep(). You can see that with the sleep there's no pauses between the printed "tick"-s, but with the edge-tts request there is a brief, but noticeable pause before the first tick (so that's when await asyncio.wait yields the execution to the loop and the edge_request task starts to run, before the first asyncio.wait timeout), then it seems that after the stream is started the rest of the ticks get printed without blocks.

I can work around this by running the edge-tts request in an asyncio.to_thread executor, but it seems to indicate something not really async happening in edge-tts.

rany2 commented 1 month ago

I modified your script slightly and can't repro. I don't notice any blocks either. This is what I used to confirm this:

import asyncio
import edge_tts
import timeit

TEXT = (
    'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt '
    'ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco '
    'laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in '
    'voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat '
    'cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
)

async def edge_request():
    req = edge_tts.Communicate(TEXT)
    return b''.join([chunk['data'] async for chunk in req.stream() if chunk['type'] == 'audio'])

async def dummy_sleep():
    await asyncio.sleep(2)

async def test(fn) -> None:
    print(f'testing {fn!r}')
    x = 0
    y = timeit.default_timer()
    i = 0
    edge_task = asyncio.create_task(fn())
    while not edge_task.done():
        await asyncio.wait({edge_task}, timeout=.1)
        x = y
        y = timeit.default_timer()
        i += 1
        print(f'elapsed time: {y - x:.2f} seconds ({i} iterations)')

asyncio.run(test(edge_request))
asyncio.run(test(dummy_sleep))
rany2 commented 1 month ago

I know that async on Windows is pretty buggy, so maybe you're using that platform?

lanzz commented 1 month ago

Yeah, I can confirm that this only occurs on Windows, tested on macOS and Linux and there's no block there. Oh well.