add text chunking for long-form generation (Fixes #4)

This commit is contained in:
ANLGBOY
2025-11-19 18:08:30 +09:00
parent d31536d9fc
commit c31b6745e4
30 changed files with 1813 additions and 102 deletions

View File

@@ -2,6 +2,10 @@
High-performance text-to-speech inference using ONNX Runtime.
## 📰 Update News
**2025.11.19** - Added automatic text chunking for long-form inference. Long texts are split into chunks and synthesized with natural pauses.
## Requirements
- C++17 compiler, CMake 3.15+
@@ -62,14 +66,16 @@ Process multiple voice styles and texts at once:
```bash
./example_onnx \
--voice-style ../assets/voice_styles/M1.json,../assets/voice_styles/F1.json \
--text "The sun sets behind the mountains, painting the sky in shades of pink and orange.|The weather is beautiful and sunny outside. A gentle breeze makes the air feel fresh and pleasant."
--text "The sun sets behind the mountains, painting the sky in shades of pink and orange.|The weather is beautiful and sunny outside. A gentle breeze makes the air feel fresh and pleasant." \
--batch
```
This will:
- Use `--batch` flag to enable batch processing mode
- Generate speech for 2 different voice-text pairs
- Use male voice style (M1.json) for the first text
- Use female voice style (F1.json) for the second text
- Process both samples in a single batch
- Process both samples in a single batch (automatic text chunking disabled)
### Example 3: High Quality Inference
Increase denoising steps for better quality:
@@ -84,6 +90,22 @@ This will:
- Use 10 denoising steps instead of the default 5
- Produce higher quality output at the cost of slower inference
### Example 4: Long-Form Inference
For long texts, the system automatically chunks the text into manageable segments and generates a single audio file:
```bash
./example_onnx \
--voice-style ../assets/voice_styles/M1.json \
--text "Once upon a time, in a small village nestled between rolling hills, there lived a young artist named Clara. Every morning, she would wake up before dawn to capture the first light of day. The golden rays streaming through her window inspired countless paintings. Her work was known throughout the region for its vibrant colors and emotional depth. People from far and wide came to see her gallery, and many said her paintings could tell stories that words never could."
```
This will:
- Automatically split the long text into smaller chunks (max 300 characters by default)
- Process each chunk separately while maintaining natural speech flow
- Insert brief silences (0.3 seconds) between chunks for natural pacing
- Combine all chunks into a single output audio file
**Note**: When using batch mode (`--batch`), automatic text chunking is disabled. Use non-batch mode for long-form text synthesis.
## Available Arguments
| Argument | Type | Default | Description |
@@ -94,8 +116,10 @@ This will:
| `--voice-style` | str | `../assets/voice_styles/M1.json` | Voice style file path(s) (comma-separated for batch) |
| `--text` | str | (long default text) | Text(s) to synthesize (pipe-separated for batch) |
| `--save-dir` | str | `results` | Output directory |
| `--batch` | flag | False | Enable batch mode (disables automatic text chunking) |
## Notes
- **Batch Processing**: The number of `--voice-style` files must match the number of `--text` entries
- **Long-Form Inference**: Without `--batch` flag, long texts are automatically chunked and combined into a single audio file with natural pauses
- **Quality vs Speed**: Higher `--total-step` values produce better quality but take longer

View File

@@ -16,6 +16,7 @@ struct Args {
"This morning, I took a walk in the park, and the sound of the birds and the breeze was so pleasant that I stopped for a long time just to listen."
};
std::string save_dir = "results";
bool batch = false;
};
auto splitString = [](const std::string& str, char delim) {
@@ -39,6 +40,7 @@ Args parseArgs(int argc, char* argv[]) {
else if (arg == "--voice-style" && i + 1 < argc) args.voice_style = splitString(argv[++i], ',');
else if (arg == "--text" && i + 1 < argc) args.text = splitString(argv[++i], '|');
else if (arg == "--save-dir" && i + 1 < argc) args.save_dir = argv[++i];
else if (arg == "--batch") args.batch = true;
}
return args;
}
@@ -53,13 +55,13 @@ int main(int argc, char* argv[]) {
std::string save_dir = args.save_dir;
std::vector<std::string> voice_style_paths = args.voice_style;
std::vector<std::string> text_list = args.text;
bool batch = args.batch;
if (voice_style_paths.size() != text_list.size()) {
std::cerr << "Error: Number of voice styles (" << voice_style_paths.size()
<< ") must match number of texts (" << text_list.size() << ")\n";
return 1;
}
int bsz = voice_style_paths.size();
// --- 2. Load Text to Speech --- //
@@ -81,7 +83,11 @@ int main(int argc, char* argv[]) {
std::cout << "\n[" << (n + 1) << "/" << n_test << "] Starting synthesis...\n";
auto result = timer("Generating speech from text", [&]() {
return text_to_speech->call(memory_info, text_list, style, total_step);
if (batch) {
return text_to_speech->batch(memory_info, text_list, style, total_step);
} else {
return text_to_speech->call(memory_info, text_list[0], style, total_step);
}
});
int sample_rate = text_to_speech->getSampleRate();

View File

@@ -5,6 +5,7 @@
#include <algorithm>
#include <random>
#include <sstream>
#include <regex>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
@@ -155,7 +156,7 @@ void TextToSpeech::sampleNoisyLatent(
}
}
TextToSpeech::SynthesisResult TextToSpeech::call(
TextToSpeech::SynthesisResult TextToSpeech::_infer(
Ort::MemoryInfo& memory_info,
const std::vector<std::string>& text_list,
const Style& style,
@@ -364,6 +365,52 @@ TextToSpeech::SynthesisResult TextToSpeech::call(
return result;
}
TextToSpeech::SynthesisResult TextToSpeech::call(
Ort::MemoryInfo& memory_info,
const std::string& text,
const Style& style,
int total_step,
float silence_duration
) {
if (style.getTtlShape()[0] != 1) {
throw std::runtime_error("Single speaker text to speech only supports single style");
}
auto text_list = chunkText(text);
std::vector<float> wav_cat;
float dur_cat = 0.0f;
for (const auto& chunk : text_list) {
auto result = _infer(memory_info, {chunk}, style, total_step);
if (wav_cat.empty()) {
wav_cat = result.wav;
dur_cat = result.duration[0];
} else {
int silence_len = static_cast<int>(silence_duration * sample_rate_);
std::vector<float> silence(silence_len, 0.0f);
wav_cat.insert(wav_cat.end(), silence.begin(), silence.end());
wav_cat.insert(wav_cat.end(), result.wav.begin(), result.wav.end());
dur_cat += result.duration[0] + silence_duration;
}
}
SynthesisResult final_result;
final_result.wav = wav_cat;
final_result.duration = {dur_cat};
return final_result;
}
TextToSpeech::SynthesisResult TextToSpeech::batch(
Ort::MemoryInfo& memory_info,
const std::vector<std::string>& text_list,
const Style& style,
int total_step
) {
return _infer(memory_info, text_list, style, total_step);
}
// ============================================================================
// Utility functions
// ============================================================================
@@ -712,3 +759,92 @@ std::string sanitizeFilename(const std::string& text, int max_len) {
}
return result;
}
// ============================================================================
// Chunk text
// ============================================================================
static std::string trim(const std::string& str) {
size_t start = 0;
while (start < str.size() && std::isspace(static_cast<unsigned char>(str[start]))) {
start++;
}
size_t end = str.size();
while (end > start && std::isspace(static_cast<unsigned char>(str[end - 1]))) {
end--;
}
return str.substr(start, end - start);
}
std::vector<std::string> chunkText(const std::string& text, int max_len) {
std::vector<std::string> chunks;
// Split by paragraph (two or more newlines)
std::regex paragraph_regex(R"(\n\s*\n+)");
std::sregex_token_iterator iter(text.begin(), text.end(), paragraph_regex, -1);
std::sregex_token_iterator end;
std::vector<std::string> paragraphs;
for (; iter != end; ++iter) {
std::string para = trim(*iter);
if (!para.empty()) {
paragraphs.push_back(para);
}
}
// Split by sentence boundaries, excluding abbreviations
// This is a simplified version - C++ negative lookbehind is more complex
std::regex sentence_regex(R"([.!?]\s+)");
for (const auto& paragraph : paragraphs) {
std::sregex_token_iterator sent_iter(paragraph.begin(), paragraph.end(), sentence_regex, -1);
std::sregex_token_iterator sent_end;
std::vector<std::string> sentences;
std::string current = "";
for (; sent_iter != sent_end; ++sent_iter) {
std::string sentence = *sent_iter;
if (!sentence.empty()) {
// Add back the punctuation
if (sent_iter != sent_end) {
std::smatch match;
if (std::regex_search(sent_iter->first, paragraph.end(), match, sentence_regex)) {
sentence += match.str();
}
}
sentences.push_back(sentence);
}
}
// Combine sentences into chunks
std::string current_chunk = "";
for (const auto& sentence : sentences) {
if (static_cast<int>(current_chunk.length() + sentence.length() + 1) <= max_len) {
if (!current_chunk.empty()) {
current_chunk += " ";
}
current_chunk += sentence;
} else {
if (!current_chunk.empty()) {
chunks.push_back(trim(current_chunk));
}
current_chunk = sentence;
}
}
if (!current_chunk.empty()) {
chunks.push_back(trim(current_chunk));
}
}
// If no chunks were created, return the original text
if (chunks.empty()) {
chunks.push_back(trim(text));
}
return chunks;
}

View File

@@ -87,6 +87,14 @@ public:
};
SynthesisResult call(
Ort::MemoryInfo& memory_info,
const std::string& text,
const Style& style,
int total_step,
float silence_duration = 0.3f
);
SynthesisResult batch(
Ort::MemoryInfo& memory_info,
const std::vector<std::string>& text_list,
const Style& style,
@@ -96,6 +104,12 @@ public:
int getSampleRate() const { return sample_rate_; }
private:
SynthesisResult _infer(
Ort::MemoryInfo& memory_info,
const std::vector<std::string>& text_list,
const Style& style,
int total_step
);
Config cfgs_;
UnicodeProcessor* text_processor_;
Ort::Session* dp_ort_;
@@ -200,3 +214,6 @@ auto timer(const std::string& name, Func&& func) -> decltype(func()) {
// Sanitize filename
std::string sanitizeFilename(const std::string& text, int max_len);
// Chunk text into manageable segments
std::vector<std::string> chunkText(const std::string& text, int max_len = 300);