Skip to content
Open
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
9 changes: 9 additions & 0 deletions libpiper/include/piper_impl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ Ort::Env ort_env{ORT_LOGGING_LEVEL_WARNING, "piper"};

#define CLAUSE_TYPE_CLAUSE 0x00040000
#define CLAUSE_TYPE_SENTENCE 0x00080000
#define CLAUSE_TYPE_EOF 0x00010000

#define CLAUSE_INTONATION_TYPE 0x00007000
#define CLAUSE_PAUSE 0x00000FFF

#define CLAUSE_PERIOD (40 | CLAUSE_INTONATION_FULL_STOP | CLAUSE_TYPE_SENTENCE)
#define CLAUSE_COMMA (20 | CLAUSE_INTONATION_COMMA | CLAUSE_TYPE_CLAUSE)
Expand All @@ -54,6 +58,11 @@ Ort::Env ort_env{ORT_LOGGING_LEVEL_WARNING, "piper"};
#define CLAUSE_COLON (30 | CLAUSE_INTONATION_FULL_STOP | CLAUSE_TYPE_CLAUSE)
#define CLAUSE_SEMICOLON (30 | CLAUSE_INTONATION_COMMA | CLAUSE_TYPE_CLAUSE)

#define EXCLAMATION_PAUSE (CLAUSE_EXCLAMATION & CLAUSE_PAUSE)
#define PERIOD_PAUSE (CLAUSE_PERIOD & CLAUSE_PAUSE)
#define SEMICOLON_PAUSE (CLAUSE_SEMICOLON & CLAUSE_PAUSE)
#define COMMA_PAUSE (CLAUSE_COMMA & CLAUSE_PAUSE)

struct piper_synthesizer {
// From config JSON file
std::string espeak_voice;
Expand Down
41 changes: 24 additions & 17 deletions libpiper/src/piper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,29 @@ piper_default_synthesize_options(piper_synthesizer *synth) {
return options;
}

std::string categorize_terminator(int terminator) {
if ((terminator & CLAUSE_TYPE_EOF) != CLAUSE_TYPE_EOF) {
int pause = terminator & CLAUSE_PAUSE;

if (pause >= PERIOD_PAUSE) {
switch (terminator & CLAUSE_INTONATION_TYPE) {
case CLAUSE_INTONATION_EXCLAMATION: return "!";
case CLAUSE_INTONATION_QUESTION: return "?";
default: return ".";
}
} else if (pause >= SEMICOLON_PAUSE) {
switch (terminator & CLAUSE_INTONATION_TYPE) {
case CLAUSE_INTONATION_FULL_STOP: return ":";
default: return ";";
}
} else if (pause >= COMMA_PAUSE) {
return ",";
}
}

return "";
}

int piper_synthesize_start(struct piper_synthesizer *synth, const char *text,
const piper_synthesize_options *options) {
if (!synth) {
Expand Down Expand Up @@ -163,7 +186,6 @@ int piper_synthesize_start(struct piper_synthesizer *synth, const char *text,
const void *text_ptr = text;
while (text_ptr != nullptr) {
int terminator = 0;
std::string terminator_str = "";

const char *phonemes = espeak_TextToPhonemesWithTerminator(
&text_ptr, espeakCHARS_AUTO, espeakPHONEMES_IPA, &terminator);
Expand All @@ -172,22 +194,7 @@ int piper_synthesize_start(struct piper_synthesizer *synth, const char *text,
sentence_phonemes[current_idx] += phonemes;
}

// Categorize terminator
terminator &= 0x000FFFFF;

if (terminator == CLAUSE_PERIOD) {
terminator_str = ".";
} else if (terminator == CLAUSE_QUESTION) {
terminator_str = "?";
} else if (terminator == CLAUSE_EXCLAMATION) {
terminator_str = "!";
} else if (terminator == CLAUSE_COMMA) {
terminator_str = ", ";
} else if (terminator == CLAUSE_COLON) {
terminator_str = ": ";
} else if (terminator == CLAUSE_SEMICOLON) {
terminator_str = "; ";
}
std::string terminator_str = categorize_terminator(terminator);

sentence_phonemes[current_idx] += terminator_str;

Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ exclude = '''
\.pyi$
)
'''

[tool.pytest.ini_options]
disable_test_id_escaping_and_forfeit_all_rights_to_community_support = true
50 changes: 33 additions & 17 deletions src/piper/espeakbridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

#define CLAUSE_TYPE_CLAUSE 0x00040000
#define CLAUSE_TYPE_SENTENCE 0x00080000
#define CLAUSE_TYPE_EOF 0x00010000

#define CLAUSE_INTONATION_TYPE 0x00007000
#define CLAUSE_PAUSE 0x00000FFF

#define CLAUSE_PERIOD (40 | CLAUSE_INTONATION_FULL_STOP | CLAUSE_TYPE_SENTENCE)
#define CLAUSE_COMMA (20 | CLAUSE_INTONATION_COMMA | CLAUSE_TYPE_CLAUSE)
Expand All @@ -20,6 +24,11 @@
#define CLAUSE_COLON (30 | CLAUSE_INTONATION_FULL_STOP | CLAUSE_TYPE_CLAUSE)
#define CLAUSE_SEMICOLON (30 | CLAUSE_INTONATION_COMMA | CLAUSE_TYPE_CLAUSE)

#define EXCLAMATION_PAUSE (CLAUSE_EXCLAMATION & CLAUSE_PAUSE)
#define PERIOD_PAUSE (CLAUSE_PERIOD & CLAUSE_PAUSE)
#define SEMICOLON_PAUSE (CLAUSE_SEMICOLON & CLAUSE_PAUSE)
#define COMMA_PAUSE (CLAUSE_COMMA & CLAUSE_PAUSE)

static PyObject *py_initialize(PyObject *self, PyObject *args) {
const char *data_dir;
if (!PyArg_ParseTuple(args, "s", &data_dir)) {
Expand Down Expand Up @@ -48,6 +57,29 @@ static PyObject *py_set_voice(PyObject *self, PyObject *args) {
Py_RETURN_NONE;
}

static char *categorize_terminator(int terminator) {
if ((terminator & CLAUSE_TYPE_EOF) != CLAUSE_TYPE_EOF) {
int pause = terminator & CLAUSE_PAUSE;

if (pause >= PERIOD_PAUSE) {
switch (terminator & CLAUSE_INTONATION_TYPE) {
case CLAUSE_INTONATION_EXCLAMATION: return "!";
case CLAUSE_INTONATION_QUESTION: return "?";
default: return ".";
}
} else if (pause >= SEMICOLON_PAUSE) {
switch (terminator & CLAUSE_INTONATION_TYPE) {
case CLAUSE_INTONATION_FULL_STOP: return ":";
default: return ";";
}
} else if (pause >= COMMA_PAUSE) {
return ",";
}
}

return "";
}

static PyObject *py_get_phonemes(PyObject *self, PyObject *args) {
const char *text;
if (!PyArg_ParseTuple(args, "s", &text)) {
Expand All @@ -58,28 +90,12 @@ static PyObject *py_get_phonemes(PyObject *self, PyObject *args) {

while (text != NULL) {
int terminator = 0;
char *terminator_str = "";

const char *phonemes = espeak_TextToPhonemesWithTerminator(
(const void **)&text, espeakCHARS_AUTO, espeakPHONEMES_IPA,
&terminator);

// Categorize terminator
terminator &= 0x000FFFFF;

if (terminator == CLAUSE_PERIOD) {
terminator_str = ".";
} else if (terminator == CLAUSE_QUESTION) {
terminator_str = "?";
} else if (terminator == CLAUSE_EXCLAMATION) {
terminator_str = "!";
} else if (terminator == CLAUSE_COMMA) {
terminator_str = ",";
} else if (terminator == CLAUSE_COLON) {
terminator_str = ":";
} else if (terminator == CLAUSE_SEMICOLON) {
terminator_str = ";";
}
char *terminator_str = categorize_terminator(terminator);

PyList_Append(phonemes_and_terminators,
Py_BuildValue("(ssO)", phonemes, terminator_str,
Expand Down
24 changes: 24 additions & 0 deletions tests/test_espeakbridge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pytest

from piper import espeakbridge

terminator_tests = [
# exact matches for punctuation are retained verbatim
("en-us", "Hello, world! Colons: important? Maybe; maybe not.", ",!:?;."),
# ellipses get normalized to semicolons
("en-us", "An ellipsis… Could it be true?", ";?"),
("en-us", "An ASCII ellipsis... Could it be true?", ";?"),
("en-us", "A long ellipsis.... Could it be true?", ";?"),
# em dashes get normalized to semicolons
("en-us", "Em dashes — for all your punctuation needs.", ";."),
# Chinese/fullwidth punctuation gets normalized to ASCII equivalents
("cmn", "你好,世界!冒号:重要?可能;与否。一、二、三……", ",!:?;.,,;"),
]


@pytest.mark.parametrize("locale,text,expect", terminator_tests)
def test_get_phonemes_terminator(locale: str, text: str, expect: list[str]):
espeakbridge.set_voice(locale)
result = espeakbridge.get_phonemes(text)

assert [terminator for _, terminator, *_ in result] == list(expect)