概述
我将字符串发送到一个文本到语音服务器,该服务器接受的最大长度为300个字符。由于网络延迟,返回的每一段语音之间可能会有延迟,所以我想尽可能以最"自然的停顿"来打断语音。
每个服务器请求都要花费我的钱,所以理想情况下,我会发送尽可能长的字符串,最多允许的字符数。
这是我目前的实现:
private static final boolean DEBUG = true;
private static final int MAX_UTTERANCE_LENGTH = 298;
private static final int MIN_UTTERANCE_LENGTH = 200;
private static final String FULL_STOP_SPACE = ". ";
private static final String QUESTION_MARK_SPACE = "? ";
private static final String EXCLAMATION_MARK_SPACE = "! ";
private static final String LINE_SEPARATOR = System.getProperty("line.separator");
private static final String COMMA_SPACE = ", ";
private static final String JUST_A_SPACE = " ";
public static ArrayList<String> splitUtteranceNaturalBreaks(String utterance) {
final long then = System.nanoTime();
final ArrayList<String> speakableUtterances = new ArrayList<String>();
int splitLocation = 0;
String success = null;
while (utterance.length() > MAX_UTTERANCE_LENGTH) {
splitLocation = utterance.lastIndexOf(FULL_STOP_SPACE, MAX_UTTERANCE_LENGTH);
if (DEBUG) {
System.out.println("(0 FULL STOP) - last index at: " + splitLocation);
}
if (splitLocation < MIN_UTTERANCE_LENGTH) {
if (DEBUG) {
System.out.println("(1 FULL STOP) - NOT_OK");
}
splitLocation = utterance.lastIndexOf(QUESTION_MARK_SPACE, MAX_UTTERANCE_LENGTH);
if (DEBUG) {
System.out.println("(1 QUESTION MARK) - last index at: " + splitLocation);
}
if (splitLocation < MIN_UTTERANCE_LENGTH) {
if (DEBUG) {
System.out.println("(2 QUESTION MARK) - NOT_OK");
}
splitLocation = utterance.lastIndexOf(EXCLAMATION_MARK_SPACE, MAX_UTTERANCE_LENGTH);
if (DEBUG) {
System.out.println("(2 EXCLAMATION MARK) - last index at: " + splitLocation);
}
if (splitLocation < MIN_UTTERANCE_LENGTH) {
if (DEBUG) {
System.out.println("(3 EXCLAMATION MARK) - NOT_OK");
}
splitLocation = utterance.lastIndexOf(LINE_SEPARATOR, MAX_UTTERANCE_LENGTH);
if (DEBUG) {
System.out.println("(3 SEPARATOR) - last index at: " + splitLocation);
}
if (splitLocation < MIN_UTTERANCE_LENGTH) {
if (DEBUG) {
System.out.println("(4 SEPARATOR) - NOT_OK");
}
splitLocation = utterance.lastIndexOf(COMMA_SPACE, MAX_UTTERANCE_LENGTH);
if (DEBUG) {
System.out.println("(4 COMMA) - last index at: " + splitLocation);
}
if (splitLocation < MIN_UTTERANCE_LENGTH) {
if (DEBUG) {
System.out.println("(5 COMMA) - NOT_OK");
}
splitLocation = utterance.lastIndexOf(JUST_A_SPACE, MAX_UTTERANCE_LENGTH);
if (DEBUG) {
System.out.println("(5 SPACE) - last index at: " + splitLocation);
}
if (splitLocation < MIN_UTTERANCE_LENGTH) {
if (DEBUG) {
System.out.println("(6 SPACE) - NOT_OK");
}
splitLocation = MAX_UTTERANCE_LENGTH;
if (DEBUG) {
System.out.println("(6 MAX_UTTERANCE_LENGTH) - last index at: " + splitLocation);
}
} else {
if (DEBUG) {
System.out.println("Accepted");
}
splitLocation -= 1;
}
}
} else {
if (DEBUG) {
System.out.println("Accepted");
}
splitLocation -= 1;
}
} else {
if (DEBUG) {
System.out.println("Accepted");
}
}
} else {
if (DEBUG) {
System.out.println("Accepted");
}
}
} else {
if (DEBUG) {
System.out.println("Accepted");
}
}
success = utterance.substring(0, (splitLocation + 2));
speakableUtterances.add(success.trim());
if (DEBUG) {
System.out.println("Split - Length: " + success.length() + " -:- " + success);
System.out.println("------------------------------");
}
utterance = utterance.substring((splitLocation + 2)).trim();
}
speakableUtterances.add(utterance);
if (DEBUG) {
System.out.println("Split - Length: " + utterance.length() + " -:- " + utterance);
final long now = System.nanoTime();
final long elapsed = now - then;
System.out.println("ELAPSED: " + TimeUnit.MILLISECONDS.convert(elapsed, TimeUnit.NANOSECONDS));
}
return speakableUtterances;
}
由于无法在lastIndexOf
中使用regex,所以它很难看。抛开丑陋不谈,它其实很快。
问题
理想情况下,我想使用regex,允许在我的首选分隔符之一上进行匹配:
private static final String firstChoice = "[.!?" + LINE_SEPARATOR + "]\s+";
private static final Pattern pFirstChoice = Pattern.compile(firstChoice);
然后使用匹配器来解决位置:
Matcher matcher = pFirstChoice.matcher(input);
if (matcher.find()) {
splitLocation = matcher.start();
}
在我当前的实现中,我的替代方案是存储每个分隔符的位置,然后选择最接近MAX_UTTERANCE_LENGTH
的
我尝试了各种方法来应用MIN_UTTERANCE_LENGTH
&MAX_UTTERANCE_LENGTH
到Pattern,所以它只捕获这些值之间的值,并使用查找来反向迭代?<=
,但这正是我的知识开始让我失望的地方:
private static final String poorEffort = "([.!?]{200, 298})\s+");
最后
我想知道你们中是否有regex大师能够实现我所追求的,并确认事实上,它是否会被证明更有效?
我提前感谢你。
参考文献:
- 在自然断开处拆分字符串(Python)
- 环视
- Regex以最小大小和分隔符拆分令牌
我会这样做:
Pattern p = Pattern.compile(".{1,299}(?:[.!?]\s+|\n|$)", Pattern.DOTALL);
Matcher matcher = p.matcher(text);
while (matcher.find()) {
speakableUtterances.add(matcher.group().trim());
}
正则表达式的解释:
.{1,299} any character between 1 and 299 times (matching the most amount possible)
(?:[.!?]\s+|\n|$) followed by either .!? and whitespaces, a newline or the end of the string
您可以考虑将标点符号扩展到p{Punct}
,请参阅javadoc for Pattern。
你可以在ideone上看到一个工作示例。
Unicode标准定义了如何将文本分解为句子和其他逻辑组件。以下是一些工作的伪代码:
// tests two consecutive codepoints within the text to detect the end of sentences
boolean continueSentence(Text text, Range range1, Range range2) {
Code code1 = text.code(range1), code2 = text.code(range2);
// 0.2 sot ÷
if (code1.isStartOfText())
return false;
// 0.3 ÷ eot
if (code2.isEndOfText())
return false;
// 3.0 CR × LF
if (code1.isCR() && code2.isLF())
return true;
// 4.0 (Sep | CR | LF) ÷
if (code1.isSep() || code1.isCR() || code1.isLF())
return false;
// 5.0 × [Format Extend]
if (code2.isFormat() || code2.isExtend())
return true;
// 6.0 ATerm × Numeric
if (code1.isATerm() && (code2.isDigit() || code2.isDecimal() || code2.isNumeric()))
return true;
// 7.0 Upper ATerm × Upper
if (code2.isUppercase() && code1.isATerm()) {
Range range = text.previousCode(range1);
if (range.isValid() && text.code(range).isUppercase())
return true;
}
boolean allow_STerm = true, return_value = true;
// 8.0 ATerm Close* Sp* × [^ OLetter Upper Lower Sep CR LF STerm ATerm]* Lower
Range range = range2;
Code code = code2;
while (!code.isOLetter() && !code.isUppercase() && !code.isLowercase() && !code.isSep() && !code.isCR() && !code.isLF() && !code.isSTerm() && !code.isATerm()) {
if (!(range = text.nextCode(range)).isValid())
break;
code = text.code(range);
}
range = range1;
if (code.isLowercase()) {
code = code1;
allow_STerm = true;
goto Sp_Close_ATerm;
}
code = code1;
// 8.1 (STerm | ATerm) Close* Sp* × (SContinue | STerm | ATerm)
if (code2.isSContinue() || code2.isSTerm() || code2.isATerm())
goto Sp_Close_ATerm;
// 9.0 ( STerm | ATerm ) Close* × ( Close | Sp | Sep | CR | LF )
if (code2.isClose())
goto Close_ATerm;
// 10.0 ( STerm | ATerm ) Close* Sp* × ( Sp | Sep | CR | LF )
if (code2.isSp() || code2.isSep() || code2.isCR() || code2.isLF())
goto Sp_Close_ATerm;
// 11.0 ( STerm | ATerm ) Close* Sp* (Sep | CR | LF)? ÷
return_value = false;
// allow Sep, CR, or LF zero or one times
for (int iteration = 1; iteration != 0; iteration--) {
if (!code.isSep() && !code.isCR() && !code.isLF()) goto Sp_Close_ATerm;
if (!(range = text.previousCode(range)).isValid()) goto Sp_Close_ATerm;
code = text.code(range);
}
Sp_Close_ATerm:
// allow zero or more Sp
while (code.isSp() && (range = text.previousCode(range)).isValid())
code = text.code(range);
Close_ATerm:
// allow zero or more Close
while (code.isClose() && (range = text.previousCode(range)).isValid())
code = text.code(range);
// require STerm or ATerm
if (code.isATerm() || (allow_STerm && code.isSTerm()))
return return_value;
// 12.0 × Any
return true;
}
然后你可以重复如下句子:
// pass in a range of (0, 0) to get the range of the first sentence
// returns a range with a length of 0 if there are no more sentences
Range nextSentence(Text text, Range range) {
try_again:
range = text.nextCode(new Range(range.start + range.length, 0));
if (!range.isValid())
return range;
Range next = text.nextCode(range);
long start = range.start;
while (next.isValid()) && text.continueSentence(range, next))
next = text.nextCode(range = next);
range = new Range(start, range.start + range.length - start);
Range range2 = text.trimRange(range);
if (!range2.isValid())
goto try_again;
return range2;
}
其中:
- 范围被定义为从>=start到<起始+长度
- text.trimRange删除空白字符(可选)
- 所有Code.is[Type]函数都是对Unicode字符数据库的查找。例如,您将在其中一些文件中看到一些代码点被定义为"CR"、"Sep"、"StartOfText"等
- Text.code(range)对range.start处的文本中的代码点进行解码。未使用长度
- Text.nextCode和Text.previousCode根据当前代码点的范围返回字符串中下一个或上一个代码点的区域。如果在该方向上没有代码点,则返回一个无效的范围,即长度为0的范围
该标准还定义了对单词、行和字符进行迭代的方法。