bootphon / phonemizer

Simple text to phones converter for multiple languages
https://bootphon.github.io/phonemizer/
GNU General Public License v3.0
1.15k stars 163 forks source link

number of lines in input and output must be equal, we have: input=X, output=Y #162

Closed devidw closed 6 months ago

devidw commented 7 months ago

Describe the bug

number of lines in input and output must be equal, we have: input=2, output=4

Phonemizer version

phonemizer-3.2.1
available backends: espeak-1.48.3, segments-2.2.1
uninstalled backends: espeak-mbrola, festival

System

Linux a9033ed59475 5.4.0-153-generic #170-Ubuntu SMP Fri Jun 16 13:43:31 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

To reproduce

from phonemizer.backend import EspeakBackend

the_one = EspeakBackend(
    language="en-us",
    preserve_punctuation=True,
    with_stress=True,
    words_mismatch="ignore",
)

out = the_one.phonemize(
    [
        "You know where I am if you change your mind, right? Remember, bowl or eat tonight? …".strip().replace(
            '"', ""
        )
    ]
)

print(out[0])

Expected behavior

juː nˈoʊ wˌɛɹ aɪɐm ɪf juː tʃˈeɪndʒ jʊɹ mˈaɪnd, ɹˈaɪt? ɹɪmˈɛmbɚ, bˈoʊl ɔːɹ ˈiːt tənˈaɪt? … 

Additional context Running into this issue in context of a web server serving requests in parallel, the above script is a reproduction based on the code used within the server.

When I manually run the script, I can't reproduce the issue, however, in the server environment it pops up quite frequently.

Any ideas how to further debug this?

As far as I understand the setup, the input is not in alignment with what's expected for the output.

Could it be something messed up in communication with the underlying espeak backend.

Or some weird characters that can't be translated?

Thx a lot

mmmaat commented 6 months ago

Hi, espeak (and so phonemizer) does not support multi-threaded operations. If you want parallelism you need to use multiple processes instead. By so, there is a separate instance of the backend in each process.

import concurrent.futures

from phonemizer.backend import EspeakBackend

backend = EspeakBackend(
    language="en-us",
    preserve_punctuation=True,
    with_stress=True,
    words_mismatch="ignore")

def threads_run(sentences, ntimes, njobs):
    data = []
    with concurrent.futures.ThreadPoolExecutor(max_workers=njobs) as executor:
        futures = [
            executor.submit(backend.phonemize, sent)
            for sent in [sentences] * ntimes]
        for future in concurrent.futures.as_completed(futures):
            data.append(future.result())
    return data

def processes_run(sentences, ntimes, njobs):
    data = []
    with concurrent.futures.ProcessPoolExecutor(max_workers=njobs) as executor:
        futures = [
            executor.submit(backend.phonemize, sent)
            for sent in [sentences] * ntimes]
        for future in concurrent.futures.as_completed(futures):
            data.append(future.result())
    return data

if __name__ == '__main__':
    sentences = [
        "You know where I am if you change your mind, right?",
        "Remember, bowl or eat tonight? …"]

    # serial operation for reference
    ref = backend.phonemize(sentences)

    # repeat 100 times on 4 subprocesses. Ok.
    test2 = processes_run(sentences, 100, 4)
    assert all(t == ref for t in test2)

    # repeat 100 times on 4 threads. Will most likely fail.
    test3 = threads_run(sentences, 100, 4)
    assert all(t == ref for t in test3)
devidw commented 6 months ago

Thanks @mmmaat - that was it!