2014-04-29 3 views
6

ÜbersichtSplit String in natürlicher Sprache bricht

I Strings zu einem Text-to-Speech-Server senden, die eine maximale Länge von 300 Zeichen akzeptiert. Aufgrund der Netzwerklatenz kann es zwischen den einzelnen zurückgesendeten Sprachabschnitten zu Verzögerungen kommen. Deshalb möchte ich die Sprachausgabe möglichst bei den "natürlichsten Pausen" unterbrechen.

Jede Serveranfrage kostet mich Geld, also würde ich idealerweise die längste mögliche Zeichenfolge senden, bis zu den maximal zulässigen Zeichen.

Hier ist meine aktuelle Implementierung:

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; 
} 

Es ist hässlich, da auf sie nicht in der Lage regex innerhalb lastIndexOf zu verwenden. Hässlich beiseite, es ist eigentlich ziemlich schnell.

Probleme

Im Idealfall würde Ich mag Regex zu verwenden, die auf einem meiner ersten Wahl Trennzeichen für ein Spiel erlaubt:

private static final String firstChoice = "[.!?" + LINE_SEPARATOR + "]\\s+"; 
private static final Pattern pFirstChoice = Pattern.compile(firstChoice); 

Und dann eine Matcher verwenden, um die Position zu beheben:

Matcher matcher = pFirstChoice.matcher(input); 

    if (matcher.find()) { 
     splitLocation = matcher.start(); 
    } 

Meine Alternative in meiner aktuellen Implementierung ist, den Speicherort jedes Trennzeichens zu speichern und dann das nächste auszuwählen MAX_UTTERANCE_LENGTH

Ich habe verschiedene Methoden versucht, die MIN_UTTERANCE_LENGTH & MAX_UTTERANCE_LENGTH auf das Muster anwenden, so fängt es nur zwischen diesen Werten und lookarounds mit Iterierte umkehren ?<=, aber das ist, wo mein Wissen beginnt mich zu scheitern:

private static final String poorEffort = "([.!?]{200, 298})\\s+");

Schließlich

ich frage mich, ob jemand von euch kann regex Meister erreichen, was ich nach und con fest, wenn es sich tatsächlich als effizienter erweist?

Ich danke Ihnen im Voraus.

Referenzen:

+3

Wow, große Frage. Ich hoffe, dass einige Antworten bald kommen werden, da dies genau die Art von Herausforderung zu sein scheint, die die 'regex'-Tag-Jungs lieben. – MattSizzle

Antwort

1

Ich würde so etwas tun:

Pattern p = Pattern.compile(".{1,299}(?:[.!?]\\s+|\\n|$)", Pattern.DOTALL); 
Matcher matcher = p.matcher(text); 
while (matcher.find()) { 
    speakableUtterances.add(matcher.group().trim()); 
} 

Exp lanation der Regex:

.{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 

Sie könnte in Erwägung ziehen, die Zeichensetzung \p{Punct} finden javadoc für Pattern zu verlängern.

Sie können eine funktionierende Probe unter ideone sehen.

+0

Das ist fantastisch! Sehr schnell auch, danke. Das einzige Problem ist die minimale Länge von 200 Zeichen. Ich denke, der einzige Weg, den ich erreichen könnte, wäre, die Länge der 'group()' zu überprüfen und auf meine Spiele der zweiten Wahl zurückzugreifen, indem ein alternatives Muster verwendet wird. – brandall

+0

Ja, ich denke, wenn du zurückfallen willst, musst du so etwas tun. Aber es wird komplizierter, da Sie den anderen Matcher anpassen müssen. Sie könnten 'find (index)', 'start()' und 'end()' verwenden. – morja

0

Die Unicode standard definiert, wie Sie Text in Sätze und andere logische Komponenten brechen sollten. Hier einige Arbeits Pseudo-Code:

// 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; 
} 

Dann können Sie über die Sätze wiederholen, wie so:

// 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; 
} 

Wo:

  • Bereich als ein Bereich definiert ist, von> = Start und < Start + Länge
  • text.trimRange löscht die Leerzeichen (optional)
  • Alle Code.is [Type] -Funktionen sind Lookups in Unicode character database. Zum Beispiel sehen Sie in einigen dieser Dateien, dass einige Codepunkte als "CR", "Sep", "StartOfText" usw. definiert sind.
  • Text.code (Bereich) dekodiert den Codepunkt im Text im Bereich. Anfang. Die Länge wird nicht verwendet.
  • Text.nextCode und Text.previousCode Rück den Bereich des nächsten oder vorherigen Codepunkt innerhalb der Zeichenfolge, basierend auf dem Bereich des aktuellen Codepunkt. Wenn es kein Codepunkt in dieser Richtung ist, gibt es einen ungültigen Bereich, der auch 0.

Der Standard einen Bereich mit einer Länge von ist es, Wege über words, iterieren definiert lines und characters.