├── .gitignore ├── docs ├── solution-architecture.png ├── cards-google-attributed.pptx ├── cards │ ├── de-DE-carla-google.png │ ├── de-DE-dora-google.png │ ├── de-DE-emma-google.png │ ├── de-DE-filiz-google.png │ ├── de-DE-ines-google.png │ ├── de-DE-liv-google.png │ ├── de-DE-lotte-google.png │ ├── de-DE-maja-google.png │ ├── de-DE-naja-google.png │ ├── de-DE-salli-google.png │ ├── de-DE-astrid-google.png │ ├── de-DE-carmen-google.png │ ├── de-DE-celine-google.png │ ├── de-DE-dora-microsoft.png │ ├── de-DE-emma-microsoft.png │ ├── de-DE-gwyneth-google.png │ ├── de-DE-ines-microsoft.png │ ├── de-DE-joanna-google.png │ ├── de-DE-liv-microsoft.png │ ├── de-DE-maja-microsoft.png │ ├── de-DE-marlene-google.png │ ├── de-DE-mizuki-google.png │ ├── de-DE-naja-microsoft.png │ ├── de-DE-nicole-google.png │ ├── de-DE-tatyana-google.png │ ├── en-GB-dora-microsoft.png │ ├── en-GB-emma-microsoft.png │ ├── en-GB-ines-microsoft.png │ ├── en-GB-liv-microsoft.png │ ├── en-GB-maja-microsoft.png │ ├── en-GB-naja-microsoft.png │ ├── en-US-dora-microsoft.png │ ├── en-US-emma-microsoft.png │ ├── en-US-ines-microsoft.png │ ├── en-US-live-microsoft.png │ ├── en-US-maja-microsoft.png │ ├── en-US-naja-microsoft.png │ ├── de-DE-astrid-microsoft.png │ ├── de-DE-carla-microsoft.png │ ├── de-DE-carmen-microsoft.png │ ├── de-DE-celine-microsoft.png │ ├── de-DE-filiz-microsoft.png │ ├── de-DE-gwyneth-microsoft.png │ ├── de-DE-joanna-microsoft.png │ ├── de-DE-lotte-microsoft.png │ ├── de-DE-marlene-microsoft.png │ ├── de-DE-mizuki-microsoft.png │ ├── de-DE-nicole-microsoft.png │ ├── de-DE-penelope-google.png │ ├── de-DE-salli-microsoft.png │ ├── de-DE-tatyana-microsoft.png │ ├── en-GB-astrid-microsoft.png │ ├── en-GB-carla-microsoft.png │ ├── en-GB-carmen-microsoft.png │ ├── en-GB-celine-microsoft.png │ ├── en-GB-filiz-microsoft.png │ ├── en-GB-gwyneth-microsoft.png │ ├── en-GB-joanna-microsoft.png │ ├── en-GB-lotte-microsoft.png │ ├── en-GB-marlene-microsoft.png │ ├── en-GB-mizuki-microsoft.png │ ├── en-GB-nicole-microsoft.png │ ├── en-GB-salli-microsoft.png │ ├── en-GB-tatyana-microsoft.png │ ├── en-US-astrid-microsoft.png │ ├── en-US-carla-microsoft.png │ ├── en-US-carmen-microsoft.png │ ├── en-US-celine-microsoft.png │ ├── en-US-filiz-microsoft.png │ ├── en-US-gwyneth-microsoft.png │ ├── en-US-joanna-microsoft.png │ ├── en-US-lotte-microsoft.png │ ├── en-US-marlene-microsoft.png │ ├── en-US-mizuki-microsoft.png │ ├── en-US-nicole-microsoft.png │ ├── en-US-salli-microsoft.png │ ├── en-US-tatyana-microsoft.png │ ├── de-DE-penelope-microsoft.png │ ├── en-GB-penelope-microsoft.png │ └── en-US-penelope-microsoft.png ├── solution-architecture.pptx └── cards-microsoft-attributed.pptx ├── server ├── .mvn │ └── wrapper │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── src │ └── main │ │ ├── resources │ │ └── application.yml │ │ ├── java │ │ └── io │ │ │ └── klerch │ │ │ └── alexa │ │ │ └── translator │ │ │ ├── Application.java │ │ │ ├── config │ │ │ └── JerseyConfig.java │ │ │ ├── util │ │ │ └── FFmpegUtils.java │ │ │ └── service │ │ │ └── ConvertService.java │ │ └── docker │ │ └── Dockerfile ├── .gitignore ├── pom.xml ├── mvnw.cmd └── mvnw ├── skill ├── src │ ├── main │ │ ├── resources │ │ │ ├── in │ │ │ │ ├── de-DE │ │ │ │ │ ├── utterances.txt │ │ │ │ │ ├── customSlot-SupportedLanguages │ │ │ │ │ ├── customSlot-SupportedTerms │ │ │ │ │ └── schema.json │ │ │ │ ├── en-GB │ │ │ │ │ ├── utterances.txt │ │ │ │ │ ├── customSlot-SupportedTerms │ │ │ │ │ ├── customSlot-SupportedLanguages │ │ │ │ │ └── schema.json │ │ │ │ └── en-US │ │ │ │ │ ├── utterances.txt │ │ │ │ │ ├── customSlot-SupportedTerms │ │ │ │ │ ├── customSlot-SupportedLanguages │ │ │ │ │ └── schema.json │ │ │ ├── log4j.properties │ │ │ ├── out │ │ │ │ ├── en-GB │ │ │ │ │ ├── languages-azure.yml │ │ │ │ │ ├── languages-google.yml │ │ │ │ │ ├── voices.yml │ │ │ │ │ └── utterances.yml │ │ │ │ ├── en-US │ │ │ │ │ ├── languages-azure.yml │ │ │ │ │ ├── languages-google.yml │ │ │ │ │ ├── voices.yml │ │ │ │ │ └── utterances.yml │ │ │ │ └── de-DE │ │ │ │ │ ├── languages-azure.yml │ │ │ │ │ ├── languages-google.yml │ │ │ │ │ ├── voices.yml │ │ │ │ │ └── utterances.yml │ │ │ └── app.properties │ │ └── java │ │ │ └── io │ │ │ └── klerch │ │ │ └── alexa │ │ │ └── translator │ │ │ └── skill │ │ │ ├── translate │ │ │ ├── Translator.java │ │ │ ├── TranslatorFactory.java │ │ │ ├── GoogleTranslator.java │ │ │ ├── AbstractTranslator.java │ │ │ └── MicrosoftTranslator.java │ │ │ ├── model │ │ │ ├── SessionState.java │ │ │ ├── LastTextToSpeech.java │ │ │ └── TextToSpeech.java │ │ │ ├── TranslatorSpeechletHandler.java │ │ │ ├── handler │ │ │ ├── HelpHandler.java │ │ │ ├── StopCancelNoHandler.java │ │ │ ├── YesHandler.java │ │ │ ├── TranslateHandler.java │ │ │ ├── LaunchHandler.java │ │ │ ├── RepeatHandler.java │ │ │ └── AbstractIntentHandler.java │ │ │ ├── tts │ │ │ ├── Mp3Converter.java │ │ │ └── TextToSpeechConverter.java │ │ │ └── SkillConfig.java │ └── test │ │ ├── resources │ │ ├── log4j.properties │ │ └── de-DE │ │ │ ├── askForHelpOneShot.xml │ │ │ ├── noLanguageTranslation.xml │ │ │ ├── unsupportedLanguageTranslation.xml │ │ │ ├── singleTranslationOneShot.xml │ │ │ └── singleTranslationWithRepeat.xml │ │ └── java │ │ └── TranslatorSpeechletHandlerTest.java ├── .gitignore └── pom.xml ├── client ├── src │ ├── main │ │ ├── resources │ │ │ ├── app.properties │ │ │ ├── log4j.properties │ │ │ └── my.app.properties │ │ └── java │ │ │ └── io │ │ │ └── klerch │ │ │ └── alexa │ │ │ └── translator │ │ │ └── client │ │ │ ├── TranslatorClientRequestHandler.java │ │ │ ├── AppConfig.java │ │ │ └── SkillClient.java │ └── test │ │ └── java │ │ └── StressTest.java ├── .gitignore ├── README.md └── pom.xml ├── README.md └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | ### IntelliJ IDEA ### 2 | .idea 3 | *.iws 4 | *.iml 5 | *.ipr -------------------------------------------------------------------------------- /docs/solution-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/solution-architecture.png -------------------------------------------------------------------------------- /docs/cards-google-attributed.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards-google-attributed.pptx -------------------------------------------------------------------------------- /docs/cards/de-DE-carla-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-carla-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-dora-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-dora-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-emma-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-emma-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-filiz-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-filiz-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-ines-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-ines-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-liv-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-liv-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-lotte-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-lotte-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-maja-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-maja-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-naja-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-naja-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-salli-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-salli-google.png -------------------------------------------------------------------------------- /docs/solution-architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/solution-architecture.pptx -------------------------------------------------------------------------------- /docs/cards/de-DE-astrid-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-astrid-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-carmen-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-carmen-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-celine-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-celine-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-dora-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-dora-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-emma-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-emma-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-gwyneth-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-gwyneth-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-ines-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-ines-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-joanna-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-joanna-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-liv-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-liv-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-maja-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-maja-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-marlene-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-marlene-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-mizuki-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-mizuki-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-naja-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-naja-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-nicole-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-nicole-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-tatyana-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-tatyana-google.png -------------------------------------------------------------------------------- /docs/cards/en-GB-dora-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-dora-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-emma-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-emma-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-ines-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-ines-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-liv-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-liv-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-maja-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-maja-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-naja-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-naja-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-dora-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-dora-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-emma-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-emma-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-ines-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-ines-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-live-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-live-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-maja-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-maja-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-naja-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-naja-microsoft.png -------------------------------------------------------------------------------- /docs/cards-microsoft-attributed.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards-microsoft-attributed.pptx -------------------------------------------------------------------------------- /docs/cards/de-DE-astrid-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-astrid-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-carla-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-carla-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-carmen-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-carmen-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-celine-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-celine-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-filiz-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-filiz-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-gwyneth-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-gwyneth-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-joanna-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-joanna-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-lotte-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-lotte-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-marlene-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-marlene-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-mizuki-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-mizuki-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-nicole-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-nicole-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-penelope-google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-penelope-google.png -------------------------------------------------------------------------------- /docs/cards/de-DE-salli-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-salli-microsoft.png -------------------------------------------------------------------------------- /docs/cards/de-DE-tatyana-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-tatyana-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-astrid-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-astrid-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-carla-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-carla-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-carmen-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-carmen-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-celine-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-celine-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-filiz-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-filiz-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-gwyneth-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-gwyneth-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-joanna-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-joanna-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-lotte-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-lotte-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-marlene-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-marlene-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-mizuki-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-mizuki-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-nicole-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-nicole-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-salli-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-salli-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-tatyana-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-tatyana-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-astrid-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-astrid-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-carla-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-carla-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-carmen-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-carmen-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-celine-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-celine-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-filiz-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-filiz-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-gwyneth-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-gwyneth-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-joanna-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-joanna-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-lotte-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-lotte-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-marlene-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-marlene-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-mizuki-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-mizuki-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-nicole-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-nicole-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-salli-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-salli-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-tatyana-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-tatyana-microsoft.png -------------------------------------------------------------------------------- /server/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/server/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /docs/cards/de-DE-penelope-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/de-DE-penelope-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-GB-penelope-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-GB-penelope-microsoft.png -------------------------------------------------------------------------------- /docs/cards/en-US-penelope-microsoft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KayLerch/alexa-meets-polly/HEAD/docs/cards/en-US-penelope-microsoft.png -------------------------------------------------------------------------------- /server/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.3.9/apache-maven-3.3.9-bin.zip 2 | -------------------------------------------------------------------------------- /server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9091 3 | 4 | security: 5 | user: 6 | name: api 7 | password: api 8 | 9 | logging: 10 | file: logs/application.log 11 | -------------------------------------------------------------------------------- /skill/src/main/resources/in/de-DE/utterances.txt: -------------------------------------------------------------------------------- 1 | Translate {termA} in {language} 2 | Translate {termA} auf {language} 3 | Translate {termA} {termB} in {language} 4 | Translate {termA} {termB} auf {language} 5 | -------------------------------------------------------------------------------- /skill/src/main/resources/in/en-GB/utterances.txt: -------------------------------------------------------------------------------- 1 | Translate {termA} in {language} 2 | Translate {termA} to {language} 3 | Translate {termA} into {language} 4 | Translate {termA} {termB} in {language} 5 | Translate {termA} {termB} to {language} 6 | Translate {termA} {termB} into {language} -------------------------------------------------------------------------------- /skill/src/main/resources/in/en-US/utterances.txt: -------------------------------------------------------------------------------- 1 | Translate {termA} in {language} 2 | Translate {termA} to {language} 3 | Translate {termA} into {language} 4 | Translate {termA} {termB} in {language} 5 | Translate {termA} {termB} to {language} 6 | Translate {termA} {termB} into {language} -------------------------------------------------------------------------------- /client/src/main/resources/app.properties: -------------------------------------------------------------------------------- 1 | AlexaAppId=amzn1.ask.skill.xxx 2 | AlexaUserId=amzn1.ask.account.xxx 3 | AlexaSessionId=SessionId.ed089995-34b6-4cbf-9e14-2530c2287435 4 | AlexaRequestId=EdwRequestId.96b30ad7-1a92-4708-b3b6-111182ac2d05 5 | Locale=de-DE 6 | LambdaName=io-klerch-alexa-translator 7 | TestPhrase=tag -------------------------------------------------------------------------------- /skill/src/main/resources/in/de-DE/customSlot-SupportedLanguages: -------------------------------------------------------------------------------- 1 | dänisch 2 | niederländisch 3 | holländisch 4 | englisch 5 | britisch 6 | australisch 7 | walisisch 8 | französisch 9 | deutsch 10 | isländisch 11 | italienisch 12 | japanisch 13 | norwegisch 14 | polnisch 15 | portugiesisch 16 | rumänisch 17 | russisch 18 | spanisch 19 | schwedisch 20 | türkisch -------------------------------------------------------------------------------- /skill/src/main/resources/in/en-GB/customSlot-SupportedTerms: -------------------------------------------------------------------------------- 1 | abcqqdef 2 | fsdfsdf dfgdwew 3 | xcvxcv asdasd fgfdd 4 | sdasdad xyloxvxcvphone asqqas xfsdfsd 5 | cxvxv xvxcvefds fdfvxvcv xvxcvsdfs xvxcvxc 6 | xvxcvxcv sdfs sdfsdf sfdsgfdg jhgjgh trefsdf 7 | sfsdf xcvcx xcvxcvw ytryrty gfdgdf daddsda ewqeqw 8 | vcvb cvbccvbc sdfsdf qweqw asdasda qweqwe gdfgdfg dsfdsfsdfs -------------------------------------------------------------------------------- /skill/src/main/resources/in/en-US/customSlot-SupportedTerms: -------------------------------------------------------------------------------- 1 | abcqqdef 2 | fsdfsdf dfgdwew 3 | xcvxcv asdasd fgfdd 4 | sdasdad xyloxvxcvphone asqqas xfsdfsd 5 | cxvxv xvxcvefds fdfvxvcv xvxcvsdfs xvxcvxc 6 | xvxcvxcv sdfs sdfsdf sfdsgfdg jhgjgh trefsdf 7 | sfsdf xcvcx xcvxcvw ytryrty gfdgdf daddsda ewqeqw 8 | vcvb cvbccvbc sdfsdf qweqw asdasda qweqwe gdfgdfg dsfdsfsdfs -------------------------------------------------------------------------------- /client/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log = . 2 | log4j.rootLogger = INFO, LAMBDA 3 | 4 | #Define the LAMBDA appender 5 | log4j.appender.LAMBDA=com.amazonaws.services.lambda.runtime.log4j.LambdaAppender 6 | log4j.appender.LAMBDA.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.LAMBDA.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss} <%X{AWSRequestId}> %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /skill/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log = . 2 | log4j.rootLogger = INFO, LAMBDA 3 | 4 | #Define the LAMBDA appender 5 | log4j.appender.LAMBDA=com.amazonaws.services.lambda.runtime.log4j.LambdaAppender 6 | log4j.appender.LAMBDA.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.LAMBDA.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss} <%X{AWSRequestId}> %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /skill/src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | log = . 2 | log4j.rootLogger = INFO, LAMBDA 3 | 4 | #Define the LAMBDA appender 5 | log4j.appender.LAMBDA=com.amazonaws.services.lambda.runtime.log4j.LambdaAppender 6 | log4j.appender.LAMBDA.layout=org.apache.log4j.PatternLayout 7 | log4j.appender.LAMBDA.layout.conversionPattern=%d{yyyy-MM-dd HH:mm:ss} <%X{AWSRequestId}> %-5p %c{1}:%L - %m%n -------------------------------------------------------------------------------- /skill/src/main/resources/in/en-GB/customSlot-SupportedLanguages: -------------------------------------------------------------------------------- 1 | danish 2 | dutch 3 | english 4 | british 5 | british english 6 | american 7 | american english 8 | australian 9 | australian english 10 | welsh 11 | french 12 | german 13 | icelandic 14 | italian 15 | japanese 16 | norwegian 17 | polish 18 | portuguese 19 | romanian 20 | russian 21 | spanish 22 | swedish 23 | turkish -------------------------------------------------------------------------------- /skill/src/main/resources/in/en-US/customSlot-SupportedLanguages: -------------------------------------------------------------------------------- 1 | danish 2 | dutch 3 | english 4 | british 5 | british english 6 | american 7 | american english 8 | australian 9 | australian english 10 | welsh 11 | french 12 | german 13 | icelandic 14 | italian 15 | japanese 16 | norwegian 17 | polish 18 | portuguese 19 | romanian 20 | russian 21 | spanish 22 | swedish 23 | turkish -------------------------------------------------------------------------------- /server/src/main/java/io/klerch/alexa/translator/Application.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class Application { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(Application.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | src/main/resources/application-prod.yml 2 | logs/ 3 | src/test/ 4 | target/ 5 | .mvn/wrapper/maven-wrapper.jar 6 | 7 | ### STS ### 8 | .apt_generated 9 | .classpath 10 | .factorypath 11 | .project 12 | .settings 13 | .springBeans 14 | 15 | ### IntelliJ IDEA ### 16 | .idea 17 | *.iws 18 | *.iml 19 | *.ipr 20 | 21 | ### NetBeans ### 22 | nbproject/private/ 23 | build/ 24 | nbbuild/ 25 | dist/ 26 | nbdist/ 27 | .nb-gradle/ -------------------------------------------------------------------------------- /skill/src/main/resources/in/de-DE/customSlot-SupportedTerms: -------------------------------------------------------------------------------- 1 | steuererklärung 2 | fröhliche weihnachten 3 | xylophone xylophone xylophone 4 | xylophone xylophone xylophone xylophone 5 | xylophone xylophone xylophone xylophone xylophone 6 | xylophone xylophone xylophone xylophone xylophone xylophone 7 | xylophone xylophone xylophone xylophone xylophone xylophone xylophone 8 | xylophone xylophone xylophone xylophone xylophone xylophone xylophone xylophone 9 | -------------------------------------------------------------------------------- /server/src/main/java/io/klerch/alexa/translator/config/JerseyConfig.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.config; 2 | 3 | import io.klerch.alexa.translator.service.ConvertService; 4 | import org.glassfish.jersey.server.ResourceConfig; 5 | import org.springframework.stereotype.Component; 6 | 7 | @Component 8 | public class JerseyConfig extends ResourceConfig { 9 | 10 | public JerseyConfig() { 11 | register(ConvertService.class); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /skill/.gitignore: -------------------------------------------------------------------------------- 1 | src/main/java/io/klerch/alexa/translator/skill/Program.java 2 | src/main/resources/my.app.properties 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | src/main/java/io/klerch/alexa/translator/skill/Program.java 2 | src/main/resources/my.app.properties 3 | target/ 4 | !.mvn/wrapper/maven-wrapper.jar 5 | 6 | ### STS ### 7 | .apt_generated 8 | .classpath 9 | .factorypath 10 | .project 11 | .settings 12 | .springBeans 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | nbproject/private/ 22 | build/ 23 | nbbuild/ 24 | dist/ 25 | nbdist/ 26 | .nb-gradle/ -------------------------------------------------------------------------------- /skill/src/main/resources/out/en-GB/languages-azure.yml: -------------------------------------------------------------------------------- 1 | danish: "DA" 2 | dutch: "NL" 3 | english: "EN" 4 | british: "EN" 5 | british_english: "EN" 6 | american: "EN" 7 | american_english: "EN" 8 | australian: "EN" 9 | australian_english: "EN" 10 | welsh: "EN" 11 | french: "FR" 12 | german: "DE" 13 | italian: "IT" 14 | japanese: "JA" 15 | norwegian: "NO" 16 | polish: "PL" 17 | portuguese: "PT" 18 | romanian: "RO" 19 | russian: "RU" 20 | spanish: "ES" 21 | swedish: "SV" 22 | turkish: "TR" -------------------------------------------------------------------------------- /skill/src/main/resources/out/en-US/languages-azure.yml: -------------------------------------------------------------------------------- 1 | danish: "DA" 2 | dutch: "NL" 3 | english: "EN" 4 | british: "EN" 5 | british_english: "EN" 6 | american: "EN" 7 | american_english: "EN" 8 | australian: "EN" 9 | australian_english: "EN" 10 | welsh: "EN" 11 | french: "FR" 12 | german: "DE" 13 | italian: "IT" 14 | japanese: "JA" 15 | norwegian: "NO" 16 | polish: "PL" 17 | portuguese: "PT" 18 | romanian: "RO" 19 | russian: "RU" 20 | spanish: "ES" 21 | swedish: "SV" 22 | turkish: "TR" -------------------------------------------------------------------------------- /skill/src/main/resources/out/de-DE/languages-azure.yml: -------------------------------------------------------------------------------- 1 | dänisch: "DA" 2 | niederländisch: "NL" 3 | holländisch: "NL" 4 | englisch: "EN" 5 | britisch: "EN" 6 | british: "EN" 7 | amerikanisch: "EN" 8 | australisch: "EN" 9 | walisisch: "EN" 10 | französisch: "FR" 11 | deutsch: "DE" 12 | italienisch: "IT" 13 | japanisch: "JA" 14 | norwegisch: "NO" 15 | polnisch: "PL" 16 | portugiesisch: "PT" 17 | romänisch: "RO" 18 | rumänisch: "RO" 19 | russisch: "RU" 20 | spanisch: "ES" 21 | schwedisch: "SV" 22 | türkisch: "TR" -------------------------------------------------------------------------------- /skill/src/main/resources/out/en-GB/languages-google.yml: -------------------------------------------------------------------------------- 1 | danish: "DA" 2 | dutch: "NL" 3 | english: "EN" 4 | british: "EN" 5 | british_english: "EN" 6 | american: "EN" 7 | american_english: "EN" 8 | australian: "EN" 9 | australian_english: "EN" 10 | welsh: "EN" 11 | french: "FR" 12 | german: "DE" 13 | icelandic: "IS" 14 | italian: "IT" 15 | japanese: "JA" 16 | norwegian: "NO" 17 | polish: "PL" 18 | portuguese: "PT-PT" 19 | romanian: "RO" 20 | russian: "RU" 21 | spanish: "ES" 22 | swedish: "SV" 23 | turkish: "TR" -------------------------------------------------------------------------------- /skill/src/main/resources/out/en-US/languages-google.yml: -------------------------------------------------------------------------------- 1 | danish: "DA" 2 | dutch: "NL" 3 | english: "EN" 4 | british: "EN" 5 | british_english: "EN" 6 | american: "EN" 7 | american_english: "EN" 8 | australian: "EN" 9 | australian_english: "EN" 10 | welsh: "EN" 11 | french: "FR" 12 | german: "DE" 13 | icelandic: "IS" 14 | italian: "IT" 15 | japanese: "JA" 16 | norwegian: "NO" 17 | polish: "PL" 18 | portuguese: "PT-PT" 19 | romanian: "RO" 20 | russian: "RU" 21 | spanish: "ES" 22 | swedish: "SV" 23 | turkish: "TR" -------------------------------------------------------------------------------- /skill/src/main/resources/out/de-DE/languages-google.yml: -------------------------------------------------------------------------------- 1 | dänisch: "DA" 2 | niederländisch: "NL" 3 | holländisch: "NL" 4 | englisch: "EN" 5 | britisch: "EN" 6 | british: "EN" 7 | amerikanisch: "EN" 8 | australisch: "EN" 9 | walisisch: "EN" 10 | französisch: "FR" 11 | deutsch: "DE" 12 | isländisch: "IS" 13 | italienisch: "IT" 14 | japanisch: "JA" 15 | norwegisch: "NO" 16 | polnisch: "PL" 17 | portugiesisch: "PT-PT" 18 | romänisch: "RO" 19 | rumänisch: "RO" 20 | russisch: "RU" 21 | spanisch: "ES" 22 | schwedisch: "SV" 23 | türkisch: "TR" -------------------------------------------------------------------------------- /client/src/main/resources/my.app.properties: -------------------------------------------------------------------------------- 1 | AlexaAppId=amzn1.ask.skill.b69e4a8f-35ed-4584-86bf-a2ff452b8aa1 2 | AlexaUserId=amzn1.ask.account.AGLIXNZIB3AZSEWCAC222EGCPAEAK65KBUXU5WCV6OIXSXTJYFM3HJWWYTWDDRK7PGZXBRCI2O75NNASY53TJSA7ERLPZAPXZRCTYB3I54GGBRDHXTUGK3ZCYXSCBOCO2GK5NH3CRWXWWS5DYGFKYW7YDPE56CN2DHGC3QQKSOIDM33OCTNWOMS7ZCD56KV57V4IHW7BM7CDLFY 3 | AlexaSessionId=SessionId.ed089995-34b6-4cbf-9e14-2530c2287435 4 | AlexaRequestId=EdwRequestId.96b30ad7-1a92-4708-b3b6-111182ac2d05 5 | Locale=de-DE 6 | LambdaName=io-klerch-alexa-translator 7 | TestPhrase=tag -------------------------------------------------------------------------------- /skill/src/main/resources/app.properties: -------------------------------------------------------------------------------- 1 | AlexaAppId=amzn1.ask.skill.xxx 2 | S3BucketUrl=https://s3.amazonaws.com/bucketName/ 3 | S3BucketName=bucketName 4 | S3CardFolder=cards 5 | DynamoTableName=translator 6 | GoogleApiKey=abcXYZ 7 | GoogleProjectName=MyProject 8 | MicrosoftSubscriptionKey=a06294fxxxxxxxxxxxxxxxxxxxxxxxxx 9 | TranslatorService=Microsoft 10 | TranslatorConvertServiceUrl=http://localhost:9091/convert 11 | TranslatorConvertServiceUser=api 12 | TranslatorConvertServicePass=api 13 | SkipMp3Conversion=false 14 | AlwaysRoundTripPhrase=tag -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/translate/Translator.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.translate; 2 | 3 | import java.util.Optional; 4 | 5 | public interface Translator { 6 | Optional getTargetLangCodeIfSupported(final String language); 7 | /** 8 | * Translate the given text into the given language. 9 | * @param text The text to translate 10 | * @param language The language to translate the text into 11 | * @return The translated text 12 | */ 13 | Optional translate(final String text, final String language); 14 | } 15 | -------------------------------------------------------------------------------- /skill/src/main/resources/out/en-US/voices.yml: -------------------------------------------------------------------------------- 1 | danish: "Naja" 2 | dutch: "Lotte" 3 | english: "Salli" 4 | british: "Emma" 5 | british_english: "Emma" 6 | american: "Joanna" 7 | american_english: "Joanna" 8 | australian: "Nicole" 9 | australian_english: "Nicole" 10 | welsh: "Gwyneth" 11 | french: "Celine" 12 | german: "Marlene" 13 | icelandic: "Dora" 14 | italian: "Carla" 15 | japanese: "Mizuki" 16 | norwegian: "Liv" 17 | polish: "Maja" 18 | portuguese: "Ines" 19 | romanian: "Carmen" 20 | russian: "Tatyana" 21 | spanish: "Penelope" 22 | swedish: "Astrid" 23 | turkish: "Filiz" 24 | 25 | PREFIXES_TO_REMOVE: 26 | - "what is " 27 | - "translate " -------------------------------------------------------------------------------- /skill/src/main/resources/out/en-GB/voices.yml: -------------------------------------------------------------------------------- 1 | danish: "Naja" 2 | dutch: "Lotte" 3 | englisch: "Salli" 4 | british: "Emma" 5 | british_english: "Emma" 6 | american: "Joanna" 7 | american_english: "Joanna" 8 | australian: "Nicole" 9 | australian_english: "Nicole" 10 | welsh: "Gwyneth" 11 | french: "Celine" 12 | german: "Marlene" 13 | icelandic: "Dora" 14 | italian: "Carla" 15 | japanese: "Mizuki" 16 | norwegian: "Liv" 17 | polish: "Maja" 18 | portuguese: "Ines" 19 | romanian: "Carmen" 20 | russian: "Tatyana" 21 | spanish: "Penelope" 22 | swedish: "Astrid" 23 | turkish: "Filiz" 24 | 25 | PREFIXES_TO_REMOVE: 26 | - "what is " 27 | - "translate " 28 | 29 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/model/SessionState.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.model; 2 | 3 | import io.klerch.alexa.state.model.AlexaScope; 4 | import io.klerch.alexa.state.model.AlexaStateModel; 5 | import io.klerch.alexa.state.model.AlexaStateSave; 6 | 7 | @AlexaStateSave(Scope = AlexaScope.SESSION) 8 | public class SessionState extends AlexaStateModel { 9 | private Boolean isConversation = false; 10 | 11 | public SessionState() {} 12 | 13 | public Boolean getConversation() { 14 | return isConversation; 15 | } 16 | 17 | public void setConversation(final Boolean conversation) { 18 | isConversation = conversation; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Lambda Test Client 2 | 3 | This code is an implementation of a Lambda function whose duty it is 4 | to periodically invoke the skill Lambda function. It is scheduled 5 | by a CloudWatch rule every for five minutes. There are two reasons for 6 | this: 7 | 8 | 1. This Lambda function validates the response of the Translator skill to 9 | ensure it is working properly. The test requests a translation and expects 10 | to see a specific value string in the skill's response. 11 | 12 | 2. The skill is implemented in Java. Lambda functions in Java have a bad 13 | cold start performance which impacts the skill user experience. By invoking the skill Lambda function every five 14 | minutes it never idles and won't have cold starts anymore. -------------------------------------------------------------------------------- /skill/src/main/resources/out/de-DE/voices.yml: -------------------------------------------------------------------------------- 1 | dänisch: "Naja" 2 | niederländisch: "Lotte" 3 | holländisch: "Lotte" 4 | englisch: "Salli" 5 | britisch: "Emma" 6 | british: "Emma" 7 | amerikanisch: "Joanna" 8 | australisch: "Nicole" 9 | walisisch: "Gwyneth" 10 | französisch: "Celine" 11 | deutsch: "Marlene" 12 | isländisch: "Dora" 13 | italienisch: "Carla" 14 | japanisch: "Mizuki" 15 | norwegisch: "Liv" 16 | polnisch: "Maja" 17 | portugiesisch: "Ines" 18 | rumänisch: "Carmen" 19 | romänisch: "Carmen" 20 | russisch: "Tatyana" 21 | spanisch: "Penelope" 22 | schwedisch: "Astrid" 23 | türkisch: "Filiz" 24 | 25 | PREFIXES_TO_REMOVE: 26 | - "was heißt " 27 | - "was heisst " 28 | - "was bedeutet " 29 | - "was ist " 30 | - "übersetze " 31 | - "frage wörterbuch nach " -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/TranslatorSpeechletHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill; 2 | 3 | import io.klerch.alexa.tellask.model.wrapper.AlexaRequestStreamHandler; 4 | import io.klerch.alexa.tellask.schema.UtteranceReader; 5 | import io.klerch.alexa.tellask.util.resource.ResourceUtteranceReader; 6 | 7 | import java.util.Collections; 8 | import java.util.Set; 9 | 10 | public class TranslatorSpeechletHandler extends AlexaRequestStreamHandler { 11 | @Override 12 | public Set getSupportedApplicationIds() { 13 | return Collections.singleton(SkillConfig.getAlexaAppId()); 14 | } 15 | 16 | @Override 17 | public UtteranceReader getUtteranceReader() { 18 | return new ResourceUtteranceReader("out/"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/model/LastTextToSpeech.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.model; 2 | 3 | import io.klerch.alexa.state.model.AlexaScope; 4 | import io.klerch.alexa.state.model.AlexaStateModel; 5 | import io.klerch.alexa.state.model.AlexaStateSave; 6 | 7 | @AlexaStateSave(Scope= AlexaScope.USER) 8 | public class LastTextToSpeech extends AlexaStateModel { 9 | // refers to TextToSpeech object in the dictionary 10 | private String ttsId; 11 | 12 | public LastTextToSpeech() { 13 | // keep this empty constructor. it is important for the magic reflection of the state handlers 14 | } 15 | 16 | public LastTextToSpeech(final TextToSpeech tts) { 17 | ttsId = tts.getId(); 18 | } 19 | 20 | public String getTtsId() { 21 | return ttsId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /skill/src/test/resources/de-DE/askForHelpOneShot.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skill/src/main/resources/in/de-DE/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "AMAZON.HelpIntent" 5 | }, 6 | { 7 | "intent": "AMAZON.CancelIntent" 8 | }, 9 | { 10 | "intent": "AMAZON.StopIntent" 11 | }, 12 | { 13 | "intent": "AMAZON.RepeatIntent" 14 | }, 15 | { 16 | "intent": "AMAZON.YesIntent" 17 | }, 18 | { 19 | "intent": "AMAZON.NoIntent" 20 | }, 21 | { 22 | "intent": "Translate", 23 | "slots": [ 24 | { 25 | "name": "termA", 26 | "type": "SupportedTerms" 27 | }, 28 | { 29 | "name": "termB", 30 | "type": "SupportedTerms" 31 | }, 32 | { 33 | "name": "language", 34 | "type": "SupportedLanguages" 35 | } 36 | ] 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /skill/src/main/resources/in/en-GB/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "AMAZON.HelpIntent" 5 | }, 6 | { 7 | "intent": "AMAZON.CancelIntent" 8 | }, 9 | { 10 | "intent": "AMAZON.StopIntent" 11 | }, 12 | { 13 | "intent": "AMAZON.RepeatIntent" 14 | }, 15 | { 16 | "intent": "AMAZON.YesIntent" 17 | }, 18 | { 19 | "intent": "AMAZON.NoIntent" 20 | }, 21 | { 22 | "intent": "Translate", 23 | "slots": [ 24 | { 25 | "name": "termA", 26 | "type": "SupportedTerms" 27 | }, 28 | { 29 | "name": "termB", 30 | "type": "SupportedTerms" 31 | }, 32 | { 33 | "name": "language", 34 | "type": "SupportedLanguages" 35 | } 36 | ] 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /skill/src/main/resources/in/en-US/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "intent": "AMAZON.HelpIntent" 5 | }, 6 | { 7 | "intent": "AMAZON.CancelIntent" 8 | }, 9 | { 10 | "intent": "AMAZON.StopIntent" 11 | }, 12 | { 13 | "intent": "AMAZON.RepeatIntent" 14 | }, 15 | { 16 | "intent": "AMAZON.YesIntent" 17 | }, 18 | { 19 | "intent": "AMAZON.NoIntent" 20 | }, 21 | { 22 | "intent": "Translate", 23 | "slots": [ 24 | { 25 | "name": "termA", 26 | "type": "SupportedTerms" 27 | }, 28 | { 29 | "name": "termB", 30 | "type": "SupportedTerms" 31 | }, 32 | { 33 | "name": "language", 34 | "type": "SupportedLanguages" 35 | } 36 | ] 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/handler/HelpHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.handler; 2 | 3 | import io.klerch.alexa.state.utils.AlexaStateException; 4 | import io.klerch.alexa.tellask.model.AlexaInput; 5 | import io.klerch.alexa.tellask.model.AlexaOutput; 6 | import io.klerch.alexa.tellask.schema.annotation.AlexaIntentListener; 7 | import io.klerch.alexa.tellask.schema.type.AlexaIntentType; 8 | import io.klerch.alexa.tellask.util.AlexaRequestHandlerException; 9 | 10 | @AlexaIntentListener(builtInIntents = AlexaIntentType.INTENT_HELP) 11 | public class HelpHandler extends AbstractIntentHandler { 12 | @Override 13 | public AlexaOutput handleRequest(AlexaInput input) throws AlexaRequestHandlerException, AlexaStateException { 14 | return AlexaOutput.ask("SayHelp").withReprompt(true).build(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/handler/StopCancelNoHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.handler; 2 | 3 | import io.klerch.alexa.state.utils.AlexaStateException; 4 | import io.klerch.alexa.tellask.model.AlexaInput; 5 | import io.klerch.alexa.tellask.model.AlexaOutput; 6 | import io.klerch.alexa.tellask.schema.annotation.AlexaIntentListener; 7 | import io.klerch.alexa.tellask.schema.type.AlexaIntentType; 8 | import io.klerch.alexa.tellask.util.AlexaRequestHandlerException; 9 | 10 | @AlexaIntentListener(builtInIntents = {AlexaIntentType.INTENT_CANCEL, AlexaIntentType.INTENT_STOP, AlexaIntentType.INTENT_NO}) 11 | public class StopCancelNoHandler extends AbstractIntentHandler { 12 | @Override 13 | public AlexaOutput handleRequest(final AlexaInput input) throws AlexaRequestHandlerException, AlexaStateException { 14 | return AlexaOutput.tell("SayOk").build(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/handler/YesHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.handler; 2 | 3 | import io.klerch.alexa.state.utils.AlexaStateException; 4 | import io.klerch.alexa.tellask.model.AlexaInput; 5 | import io.klerch.alexa.tellask.model.AlexaOutput; 6 | import io.klerch.alexa.tellask.schema.annotation.AlexaIntentListener; 7 | import io.klerch.alexa.tellask.schema.type.AlexaIntentType; 8 | import io.klerch.alexa.tellask.util.AlexaRequestHandlerException; 9 | 10 | @AlexaIntentListener(builtInIntents = AlexaIntentType.INTENT_YES) 11 | public class YesHandler extends AbstractIntentHandler { 12 | @Override 13 | public AlexaOutput handleRequest(final AlexaInput input) throws AlexaRequestHandlerException, AlexaStateException { 14 | return AlexaOutput.ask("SayTranslateRequest") 15 | .withReprompt(true) 16 | .build(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /skill/src/test/resources/de-DE/noLanguageTranslation.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skill/src/test/resources/de-DE/unsupportedLanguageTranslation.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/translate/TranslatorFactory.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.translate; 2 | 3 | import io.klerch.alexa.translator.skill.SkillConfig; 4 | import org.apache.commons.lang3.Validate; 5 | 6 | public class TranslatorFactory { 7 | /** 8 | * Gets the appropriate translator set up in the app configuration 9 | * @param locale the locale coming in with a speechlet request 10 | * @return appropriate translator set up in the app configuration 11 | */ 12 | public static Translator getTranslator(final String locale) { 13 | final String translatorId = SkillConfig.getTranslatorService(); 14 | final Translator translator = 15 | "Microsoft".equalsIgnoreCase(translatorId) ? new MicrosoftTranslator(locale) : 16 | "Google".equalsIgnoreCase(translatorId) ? new GoogleTranslator(locale) : null; 17 | Validate.notNull(translator, "Invalid TranslatorService set up in the configuration."); 18 | return translator; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/main/java/io/klerch/alexa/translator/client/TranslatorClientRequestHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.client; 2 | 3 | import com.amazonaws.services.lambda.runtime.Context; 4 | import com.amazonaws.services.lambda.runtime.RequestHandler; 5 | import org.apache.commons.lang3.Validate; 6 | 7 | import java.util.Map; 8 | 9 | public class TranslatorClientRequestHandler implements RequestHandler, String> { 10 | private static String testPhrase = AppConfig.getTestPhrase(); 11 | private static String locale = AppConfig.getLocale(); 12 | 13 | @Override 14 | public String handleRequest(final Map input, final Context context) { 15 | final String response = new SkillClient(locale).translate(testPhrase, "englisch"); 16 | 17 | Validate.notNull(response, "Skill was not invoked. See log details with error message."); 18 | Validate.matchesPattern(response, "(?i:.*de-DE/Salli/" + testPhrase +".mp3.*)", "Unexpected response. " + response); 19 | 20 | return "1"; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/handler/TranslateHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.handler; 2 | 3 | import io.klerch.alexa.state.utils.AlexaStateException; 4 | import io.klerch.alexa.tellask.model.AlexaInput; 5 | import io.klerch.alexa.tellask.model.AlexaOutput; 6 | import io.klerch.alexa.tellask.schema.annotation.AlexaIntentListener; 7 | import io.klerch.alexa.tellask.util.AlexaRequestHandlerException; 8 | import org.apache.log4j.Logger; 9 | 10 | @AlexaIntentListener(customIntents = "Translate") 11 | public class TranslateHandler extends AbstractIntentHandler { 12 | @Override 13 | public AlexaOutput handleRequest(final AlexaInput input) throws AlexaRequestHandlerException, AlexaStateException { 14 | final StringBuilder sb = new StringBuilder(); 15 | 16 | if (input.hasSlotNotBlank("termA")) { 17 | sb.append(input.getSlotValue("termA")); 18 | } 19 | if (input.hasSlotNotBlank("termB")) { 20 | sb.append(" ").append(input.getSlotValue("termB")); 21 | } 22 | return sayTranslate(input, sb.toString().trim()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /skill/src/test/resources/de-DE/singleTranslationOneShot.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/handler/LaunchHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.handler; 2 | 3 | import io.klerch.alexa.state.utils.AlexaStateException; 4 | import io.klerch.alexa.tellask.model.AlexaInput; 5 | import io.klerch.alexa.tellask.model.AlexaOutput; 6 | import io.klerch.alexa.tellask.schema.AlexaLaunchHandler; 7 | import io.klerch.alexa.tellask.schema.annotation.AlexaLaunchListener; 8 | import io.klerch.alexa.tellask.util.AlexaRequestHandlerException; 9 | import io.klerch.alexa.translator.skill.model.SessionState; 10 | 11 | @AlexaLaunchListener 12 | public class LaunchHandler implements AlexaLaunchHandler { 13 | 14 | public AlexaOutput handleRequest(final AlexaInput input) throws AlexaRequestHandlerException, AlexaStateException { 15 | final SessionState sessionState = input.getSessionStateHandler().createModel(SessionState.class); 16 | // remember this skill was started as a conversation (rather than in with a one-shot) 17 | sessionState.setConversation(true); 18 | 19 | return AlexaOutput.ask("SayWelcome") 20 | .withReprompt(true) 21 | .putState(sessionState) 22 | .build(); 23 | } 24 | 25 | public AlexaOutput handleError(final AlexaRequestHandlerException exception) { 26 | return AlexaOutput.tell("SaySorry").build(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/handler/RepeatHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.handler; 2 | 3 | import io.klerch.alexa.state.handler.AWSDynamoStateHandler; 4 | import io.klerch.alexa.state.utils.AlexaStateException; 5 | import io.klerch.alexa.tellask.model.AlexaInput; 6 | import io.klerch.alexa.tellask.model.AlexaOutput; 7 | import io.klerch.alexa.tellask.schema.annotation.AlexaIntentListener; 8 | import io.klerch.alexa.tellask.schema.type.AlexaIntentType; 9 | import io.klerch.alexa.tellask.util.AlexaRequestHandlerException; 10 | import io.klerch.alexa.translator.skill.SkillConfig; 11 | import io.klerch.alexa.translator.skill.model.LastTextToSpeech; 12 | import io.klerch.alexa.translator.skill.model.TextToSpeech; 13 | 14 | import java.util.Optional; 15 | 16 | @AlexaIntentListener(builtInIntents = AlexaIntentType.INTENT_REPEAT) 17 | public class RepeatHandler extends AbstractIntentHandler { 18 | @Override 19 | public AlexaOutput handleRequest(final AlexaInput input) throws AlexaRequestHandlerException, AlexaStateException { 20 | final AWSDynamoStateHandler dynamoHandler = new AWSDynamoStateHandler(input.getSessionStateHandler().getSession(), SkillConfig.getDynamoTableName()); 21 | // try get last result 22 | final Optional lastTts = dynamoHandler.readModel(LastTextToSpeech.class); 23 | 24 | if (lastTts.isPresent()) { 25 | final Optional tts = dynamoHandler.readModel(TextToSpeech.class, lastTts.get().getTtsId()); 26 | if (tts.isPresent()) { 27 | // avoid to rewrite model to dynamo 28 | tts.get().setHandler(input.getSessionStateHandler()); 29 | return sayTranslate(input, tts.get()); 30 | } 31 | } 32 | return isConversation(input) ? 33 | AlexaOutput.ask("SayNothingToRepeatAndElse").withReprompt(true).build() : 34 | AlexaOutput.tell("SayNothingToRepeat").build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client/src/test/java/StressTest.java: -------------------------------------------------------------------------------- 1 | import io.klerch.alexa.translator.client.AppConfig; 2 | import io.klerch.alexa.translator.client.SkillClient; 3 | import org.junit.Ignore; 4 | import org.junit.Test; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.concurrent.ExecutorService; 9 | import java.util.concurrent.Executors; 10 | 11 | public class StressTest { 12 | final List testPhrases = Arrays.asList("öl", "gut", "fuß", "lob", "tor", "rad", "tee", "rot", "not", "see", "fee", "zeh"); 13 | final String language = "schwedisch"; 14 | 15 | @Test 16 | @Ignore 17 | public void stress() { 18 | final ExecutorService executor = Executors.newFixedThreadPool(testPhrases.size()); 19 | final String locale = AppConfig.getLocale(); 20 | 21 | testPhrases.forEach(testPhrase -> { 22 | final Runnable worker = new LambdaRunner(locale, testPhrase, language); 23 | executor.execute(worker); 24 | }); 25 | executor.shutdown(); 26 | // Wait until all threads are finish 27 | while (!executor.isTerminated()) { 28 | 29 | } 30 | } 31 | 32 | public static class LambdaRunner implements Runnable { 33 | private final SkillClient client; 34 | private final String testPhrase; 35 | private final String language; 36 | 37 | LambdaRunner(final String locale, final String testPhrase, final String language) { 38 | this.client = new SkillClient(locale); 39 | this.testPhrase = testPhrase; 40 | this.language = language; 41 | } 42 | 43 | @Override 44 | public void run() { 45 | System.out.println("STARTED: " + testPhrase); 46 | long startTime = System.currentTimeMillis(); 47 | final String response = client.translate(testPhrase, language); 48 | long stopTime = System.currentTimeMillis(); 49 | long elapsedTime = stopTime - startTime; 50 | System.out.println("FINISHED: " + testPhrase + " in " + elapsedTime + "ms : " + response); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /skill/src/test/resources/de-DE/singleTranslationWithRepeat.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/tts/Mp3Converter.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.tts; 2 | 3 | import io.klerch.alexa.translator.skill.SkillConfig; 4 | import org.apache.commons.io.IOUtils; 5 | import org.apache.http.HttpEntity; 6 | import org.apache.http.HttpResponse; 7 | import org.apache.http.auth.AuthScope; 8 | import org.apache.http.auth.UsernamePasswordCredentials; 9 | import org.apache.http.client.CredentialsProvider; 10 | import org.apache.http.client.methods.HttpGet; 11 | import org.apache.http.client.utils.URIBuilder; 12 | import org.apache.http.impl.client.BasicCredentialsProvider; 13 | import org.apache.http.impl.client.HttpClientBuilder; 14 | 15 | import java.io.IOException; 16 | import java.net.URISyntaxException; 17 | 18 | public class Mp3Converter { 19 | public static String convertMp3(final String mp3Path) throws URISyntaxException, IOException { 20 | // get credentials for webservice from application config 21 | final String apiKey = SkillConfig.getTranslatorConvertServiceUser(); 22 | final String apiPass = SkillConfig.getTranslatorConvertServicePass(); 23 | // build uri 24 | final String bucketName = SkillConfig.getS3BucketName(); 25 | final URIBuilder uri = new URIBuilder(SkillConfig.getTranslatorConvertServiceUrl()).addParameter("bucket", bucketName).addParameter("path", mp3Path); 26 | // set up web request 27 | final HttpGet httpGet = new HttpGet(uri.build()); 28 | httpGet.setHeader("Content-Type", "text/plain"); 29 | // set up credentials 30 | final CredentialsProvider provider = new BasicCredentialsProvider(); 31 | final UsernamePasswordCredentials credentials = new UsernamePasswordCredentials(apiKey, apiPass); 32 | provider.setCredentials(AuthScope.ANY, credentials); 33 | // send request to convert webservice 34 | final HttpResponse response = 35 | HttpClientBuilder.create().setDefaultCredentialsProvider(provider).build().execute(httpGet); 36 | 37 | //Validate.inclusiveBetween(200, 399, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); 38 | // work on response 39 | final HttpEntity entity = response.getEntity(); 40 | return IOUtils.toString(entity.getContent(), "UTF-8"); 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /skill/src/test/java/TranslatorSpeechletHandlerTest.java: -------------------------------------------------------------------------------- 1 | import io.klerch.alexa.test.client.AlexaClient; 2 | import io.klerch.alexa.test.client.endpoint.AlexaEndpoint; 3 | import io.klerch.alexa.test.client.endpoint.AlexaRequestStreamHandlerEndpoint; 4 | import io.klerch.alexa.translator.skill.SkillConfig; 5 | import io.klerch.alexa.translator.skill.TranslatorSpeechletHandler; 6 | 7 | import org.junit.Test; 8 | import java.util.Locale; 9 | 10 | public class TranslatorSpeechletHandlerTest { 11 | public AlexaClient givenClient() throws Exception { 12 | final AlexaEndpoint endpoint = AlexaRequestStreamHandlerEndpoint.create(TranslatorSpeechletHandler.class).build(); 13 | return AlexaClient.create(endpoint) 14 | .withLocale(Locale.GERMANY) 15 | .withApplicationId(SkillConfig.getAlexaAppId()) 16 | .build(); 17 | } 18 | 19 | public AlexaClient givenScriptClient(final String filePath) throws Exception { 20 | return AlexaClient.create(this.getClass().getResourceAsStream(filePath)) 21 | .withApplicationId(SkillConfig.getAlexaAppId()) 22 | .build(); 23 | } 24 | 25 | @Test 26 | public void doConversation() throws Exception { 27 | final AlexaClient client = AlexaClient.create(this.getClass().getResourceAsStream("de-DE/singleTranslationOneShot.xml")) 28 | .withApplicationId(SkillConfig.getAlexaAppId()) 29 | .build(); 30 | 31 | client.startScript(); 32 | } 33 | 34 | @Test 35 | public void askForHelpOneShot() throws Exception { 36 | givenScriptClient("de-DE/askForHelpOneShot.xml").startScript(); 37 | } 38 | 39 | @Test 40 | public void singleTranslationOneShot() throws Exception { 41 | givenScriptClient("de-DE/singleTranslationOneShot.xml").startScript(); 42 | } 43 | 44 | @Test 45 | public void singleTranslationWithRepeat() throws Exception { 46 | givenScriptClient("de-DE/singleTranslationWithRepeat.xml").startScript(); 47 | } 48 | 49 | @Test 50 | public void unsupportedLanguageTranslation() throws Exception { 51 | givenScriptClient("de-DE/unsupportedLanguageTranslation.xml").startScript(); 52 | } 53 | 54 | @Test 55 | public void noLanguageTranslation() throws Exception { 56 | givenScriptClient("de-DE/noLanguageTranslation.xml").startScript(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /client/src/main/java/io/klerch/alexa/translator/client/AppConfig.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.client; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.Properties; 8 | 9 | /** 10 | * Encapsulates access to application-wide property values 11 | */ 12 | public class AppConfig { 13 | private static Properties properties = new Properties(); 14 | private static final String defaultPropertiesFile = "app.properties"; 15 | private static final String customPropertiesFile = "my.app.properties"; 16 | 17 | /** 18 | * Static block does the bootstrapping of all configuration properties with 19 | * reading out values from different resource files 20 | */ 21 | static { 22 | final String propertiesFile = 23 | AppConfig.class.getClassLoader().getResource(customPropertiesFile) != null ? 24 | customPropertiesFile : defaultPropertiesFile; 25 | final InputStream propertiesStream = AppConfig.class.getClassLoader().getResourceAsStream(propertiesFile); 26 | try { 27 | properties.load(propertiesStream); 28 | } catch (IOException e) { 29 | e.printStackTrace(); 30 | } finally { 31 | if (propertiesStream != null) { 32 | try { 33 | propertiesStream.close(); 34 | } catch (IOException e) { 35 | e.printStackTrace(); 36 | } 37 | } 38 | } 39 | } 40 | 41 | public static String getAlexaAppId() { 42 | return properties.getProperty("AlexaAppId"); 43 | } 44 | 45 | public static String getAlexaUserId() { 46 | return properties.getProperty("AlexaUserId"); 47 | } 48 | 49 | public static String getAlexaSessionId() { 50 | return properties.getProperty("AlexaSessionId"); 51 | } 52 | 53 | public static String getAlexaRequestId() { 54 | return properties.getProperty("AlexaRequestId"); 55 | } 56 | 57 | public static String getLocale() { 58 | return properties.getProperty("Locale"); 59 | } 60 | 61 | public static String getLambdaName() { 62 | return properties.getProperty("LambdaName"); 63 | } 64 | 65 | public static String getTestPhrase() { 66 | return properties.getProperty("TestPhrase"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/translate/GoogleTranslator.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.translate; 2 | 3 | import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; 4 | import com.google.api.client.json.gson.GsonFactory; 5 | import com.google.api.services.translate.Translate; 6 | import com.google.api.services.translate.model.TranslationsListResponse; 7 | import io.klerch.alexa.translator.skill.SkillConfig; 8 | import org.apache.commons.lang3.StringEscapeUtils; 9 | 10 | import java.io.IOException; 11 | import java.security.GeneralSecurityException; 12 | import java.util.Collections; 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | 16 | public class GoogleTranslator extends AbstractTranslator { 17 | private static Logger log = Logger.getLogger(GoogleTranslator.class.getName()); 18 | private Translate translator; 19 | 20 | public GoogleTranslator(final String locale) { 21 | super(locale, "/languages-google.yml"); 22 | 23 | try { 24 | this.translator = new Translate.Builder( 25 | GoogleNetHttpTransport.newTrustedTransport(), 26 | GsonFactory.getDefaultInstance(), null) 27 | .setApplicationName(SkillConfig.getGoogleProjectName()) 28 | .build(); 29 | } catch (final GeneralSecurityException | IOException ex) { 30 | log.log(Level.SEVERE, null, ex); 31 | } 32 | } 33 | 34 | @Override 35 | public String doTranslate(final String text, final String targetLanguageCode) { 36 | if (targetLanguageCode.equalsIgnoreCase(sourceLanguageCode)) { 37 | return text; 38 | } 39 | 40 | try { 41 | final Translate.Translations.List list = translator.new Translations().list( 42 | Collections.singletonList(text), targetLanguageCode); 43 | list.setKey(SkillConfig.getGoogleApiKey()); 44 | list.setSource(sourceLanguageCode); 45 | final TranslationsListResponse response = list.execute(); 46 | 47 | if (!response.isEmpty() && !response.getTranslations().isEmpty()) { 48 | return StringEscapeUtils.unescapeHtml4(response.getTranslations().get(0).getTranslatedText()); 49 | } 50 | } catch (IOException e) { 51 | log.severe(e.getMessage()); 52 | } 53 | return null; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/translate/AbstractTranslator.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.translate; 2 | 3 | import com.amazonaws.util.StringUtils; 4 | import io.klerch.alexa.tellask.util.resource.ResourceUtteranceReader; 5 | import io.klerch.alexa.tellask.util.resource.YamlReader; 6 | 7 | import java.util.Optional; 8 | 9 | public abstract class AbstractTranslator implements Translator { 10 | final YamlReader yamlReader; 11 | final String locale; 12 | final String sourceLanguageCode; 13 | 14 | public AbstractTranslator(final String locale) { 15 | throw new RuntimeException("Translator must override and implement the default constructor."); 16 | } 17 | 18 | AbstractTranslator(final String locale, final String yamlFile) { 19 | // the locale is coming with the speechlet request and indicates to source language to translate from 20 | this.locale = locale; 21 | this.sourceLanguageCode = this.locale.split("-")[0]; 22 | 23 | final ResourceUtteranceReader reader = new ResourceUtteranceReader("/out", yamlFile); 24 | // the yaml reader reads values from YAML file to get a short code for a language 25 | this.yamlReader = new YamlReader(reader, locale); 26 | } 27 | 28 | @Override 29 | public final Optional getTargetLangCodeIfSupported(final String language) { 30 | return Optional.ofNullable(language) 31 | // map language to target-language-code 32 | .map(l -> this.yamlReader.getRandomUtterance(l.toLowerCase().replace(" ", "_")).orElse("")) 33 | // if source and target language are equal target language is not supported 34 | .filter(c -> !StringUtils.isNullOrEmpty(c) && !c.equalsIgnoreCase(sourceLanguageCode)); 35 | } 36 | 37 | @Override 38 | public final Optional translate(final String term, final String language) { 39 | return getTargetLangCodeIfSupported(language) 40 | // return text as is or delegate translation to child in case source and target language differ 41 | .map(targetLanguageCode -> targetLanguageCode.equalsIgnoreCase(sourceLanguageCode) ? term : doTranslate(term, targetLanguageCode)) 42 | // translation must be not null or empty 43 | .filter(translation -> !StringUtils.isNullOrEmpty(translation)); 44 | } 45 | 46 | /** 47 | * Delegates the actual translation to the child implementation. 48 | * @param term text to translate 49 | * @param targetLanguageCode target language code 50 | * @return translated text 51 | */ 52 | abstract String doTranslate(final String term, final String targetLanguageCode); 53 | } 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Alexa meets AWS Polly 2 | 3 | This project demonstrates an integration of [AWS Polly](https://aws.amazon.com/polly/) into an Alexa skill which translates phrases into different languages. 4 | Polly is Amazon's new text-to-speech cloud service and is a perfect fit for Alexa skills aiming for playing back foreign voice. 5 | 6 | This project combines the [Alexa Skills Kit](https://developer.amazon.com/alexa-skills-kit), [AWS Polly](https://aws.amazon.com/polly/) and a Translator API to translate common phrases into 7 | 17 different languages. 8 | 9 | __Important note__ 10 | Polly now provides the dynamic range compression SSML-tag and aligned bitrates of audiostreams. This removes the burden of manually converting Polly-mp3 using ffmpeg in order to 11 | comply with audio setting and volume requirements of Alexa. That being said, step 7 to 9 aren't necessary anymore. 12 | 13 | ![](docs/solution-architecture.png) 14 | 15 | 1. User speaks to an Alexa device and asks for e.g. _"What is "Good Morning" in Polish?"_ 16 | 17 | 2. NLU of Alexa triggers the Translate-intent and passes in a language-slot with 18 | value _Polish_ and a term-slot having the value _Good Morning_. An [AWS Lambda](https://aws.amazon.com/lambda) function whose code is contained in this 19 | Repo implements a Speechlet that handles the request and returns the translation. 20 | 21 | 3. Before this skill uses the translation API and TTS service of Polly, it first looks into its own dictionary where all the 22 | previous translations are stored. If it finds a record for _Good Morning_ in Polish in the database it will skip the 23 | entire round-trip (step 4 to 9) and uses the S3 audio-file referenced in the Dynamo record (learn how it got there in step 10.) 24 | 25 | 4. However, if _Good Morning_ in Polish has never been translated before the skill requests _Good Morning_ 26 | in Polish from Microsoft Translator API (or interchangeably from Google Translate). 27 | 28 | 5. The returned translation is then passed to AWS Polly. Polly responds with 29 | an MP3 bitstream with the spoken translation. 30 | 31 | 6. The stream is persisted in [AWS S3](https://aws.amazon.com/s3) as an mp3-file. 32 | 33 | 7.-9. _No custom conversion of Polly-mp3 necessary anymore as it's now aligned to Alexa requirements._ 34 | 35 | 10. Finally, a record is created for _Good Morning_ in Polish in the Dynamo dictionary. Another record that references the new 36 | dictionary entry is created for the user so Alexa keeps in mind the last translation. This is how a user can 37 | request Alexa to repeat the most recent translation. 38 | 39 | 11. The skill creates the output-speech text and squeezes in an audio-SSML tag with the mp3-url. 40 | 41 | 12. Output-speech is returned to the Alexa device. Alexa speaks and plays back the translated text with one of 42 | Polly's voices. A card is returned to the Alexa app providing the written translation. 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /server/src/main/java/io/klerch/alexa/translator/util/FFmpegUtils.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.util; 2 | 3 | import net.bramp.ffmpeg.FFmpeg; 4 | import net.bramp.ffmpeg.FFmpegExecutor; 5 | import net.bramp.ffmpeg.builder.FFmpegBuilder; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.io.InputStreamReader; 11 | import java.net.URLEncoder; 12 | import java.util.UUID; 13 | 14 | public class FFmpegUtils { 15 | 16 | public static File convertUrlToMp3(final String url) throws IOException { 17 | final String mp3Filename = UUID.randomUUID().toString() + ".mp3"; 18 | final String escFilename = URLEncoder.encode(url.substring(url.lastIndexOf("/") + 1), "UTF-8"); 19 | final String escUrl = url.substring(0, url.lastIndexOf("/") + 1) + escFilename; 20 | // build a configuration according to what Alexa expects from an MP3 it supports 21 | // see: https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit/docs/speech-synthesis-markup-language-ssml-reference#audio 22 | final FFmpegBuilder builder = new FFmpegBuilder() 23 | .setInput(escUrl) 24 | .overrideOutputFiles(true) 25 | .addOutput(mp3Filename) 26 | .setAudioCodec("libmp3lame") 27 | .setAudioChannels(FFmpeg.AUDIO_MONO) 28 | .setAudioBitRate(FFmpeg.AUDIO_SAMPLE_48000) 29 | .setAudioSampleRate(FFmpeg.AUDIO_SAMPLE_16000) 30 | .done(); 31 | 32 | // ensure ffmpeg is a valid command on the system that service runs on 33 | // or at least make sure FFMPEG and FFPROBE environment variables are set to correct path 34 | new FFmpegExecutor().createJob(builder).run(); 35 | return new File(mp3Filename); 36 | } 37 | 38 | public static File convertUrlToMp3Cmd(final String url) throws IOException, InterruptedException { 39 | final String mp3Filename = UUID.randomUUID().toString() + ".mp3"; 40 | final String escFilename = URLEncoder.encode(url.substring(url.lastIndexOf("/") + 1), "UTF-8"); 41 | final String escUrl = url.substring(0, url.lastIndexOf("/") + 1) + escFilename; 42 | // ffmpeg -i https://s3.amazonaws.com/io.klerch.alexa.translator/en-GB/Marlene/good_morning.mp3 -ac 2 -codec:a libmp3lame -b:a 48k -ar 16000 -af volume=10dB sample.mp3 43 | final String cmd = "ffmpeg -i " + escUrl + " -ac 2 -codec:a libmp3lame -b:a 48k -ar 16000 -af volume=10dB " + mp3Filename; 44 | System.out.println(cmd); 45 | final Process p = Runtime.getRuntime().exec(cmd); 46 | p.waitFor(); 47 | 48 | final BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); 49 | 50 | String line = ""; 51 | while ((line = reader.readLine())!= null) { 52 | System.out.println(line); 53 | } 54 | return new File(mp3Filename); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /skill/src/main/resources/out/en-GB/utterances.yml: -------------------------------------------------------------------------------- 1 | Languages: "[Russian|German|Spanish|Italian|Turkish|Polish|Portuguese|Japanese|Swedish|Norwegian|Romanian|Dutch|Danish|French]" 2 | SamplePhrases: "[Good morning|Hello|How are you|What's your name|Train station|football]" 3 | Sorry: "[Excuse me|Sorry|I am sorry]" 4 | 5 | SayWelcome: 6 | Utterances: 7 | - "Hi. What do you want me to translate?" 8 | Reprompts: 9 | - "Say something like

${SamplePhrases}

in ${Languages} or ask me for help." 10 | 11 | SayTranslate: 12 | - "{text} in {language} [is|translates to] {mp3}" 13 | 14 | SayTranslateAndElse: 15 | Utterances: 16 | - "{text} in {language} [is|translates to] {mp3} What else?" 17 | Reprompts: 18 | - "You can ask me to repeat the last translation or let me translate something else. Request help if you are lost." 19 | 20 | SayNoTranslation: 21 | - "Sorry, I [could not|was not able to] translate {text} into {language}." 22 | 23 | SayNoTranslationAndElse: 24 | Utterances: 25 | - "Sorry, I [could not|was not able to] translate {text} into {language}. Anything else?" 26 | Reprompts: 27 | - "Say something like

${SamplePhrases}

in ${Languages} or ask me for help." 28 | 29 | SayNoLanguage: 30 | Utterances: 31 | - "I need to know the language. Ask me something like, What is {text}, in ${Languages}." 32 | Reprompts: 33 | - "Ask me something like, {text}, in ${Languages}." 34 | 35 | SayUnsupportedLanguages: 36 | - "${Sorry}, currently I cannot translate to {language} but maybe this is something I will learn soon." 37 | 38 | SayUnsupportedLanguagesAndElse: 39 | Utterances: 40 | - "${Sorry}, currently I cannot translate to {language} but maybe this is something I will learn soon. What else?" 41 | Reprompts: 42 | - "For example say

${SamplePhrases}

in ${Languages}" 43 | 44 | SayTranslateRequest: 45 | Utterances: 46 | - "Provide a word or phrase and the language I should translate into." 47 | Reprompts: 48 | - "For example say

${SamplePhrases}

in ${Languages}" 49 | 50 | SayOk: 51 | - "Ok" 52 | 53 | SayNothingToRepeat: 54 | - "${Sorry}, but there is nothing for me to repeat. Let me translate something for you and I will keep it in mind for later repeating." 55 | 56 | SayNothingToRepeatAndElse: 57 | Utterances: 58 | - "${Sorry}, but there is nothing for me to repeat. Let me translate something for you and I will keep it in mind for later repeating. Anything else?" 59 | Reprompts: 60 | - "For example say

${SamplePhrases}

in ${Languages}" 61 | 62 | SayHelp: 63 | Utterances: 64 | - "I can translate common terms of your language into a lot of foreign languages. Say something like

${SamplePhrases}

in ${Languages}" 65 | Reprompts: 66 | - "Say something like

${SamplePhrases}

in ${Languages}" 67 | 68 | SaySorry: 69 | Utterances: 70 | - "${Sorry}, I could not understand what you were saying." 71 | Reprompts: 72 | - "Say something like

${SamplePhrases}

in ${Languages}" -------------------------------------------------------------------------------- /skill/src/main/resources/out/en-US/utterances.yml: -------------------------------------------------------------------------------- 1 | Languages: "[Russian|German|Spanish|Italian|Turkish|Polish|Portuguese|Japanese|Swedish|Norwegian|Romanian|Dutch|Danish|French]" 2 | SamplePhrases: "[Good morning|Hello|How are you|What's your name|Train station|football]" 3 | 4 | SayWelcome: 5 | Utterances: 6 | - "Hi. What do you want me to translate?" 7 | Reprompts: 8 | - "Say something like

${SamplePhrases}

in ${Languages} or ask me for help." 9 | 10 | SayTranslate: 11 | - "{text} in {language} [is|translates to] {mp3}" 12 | 13 | SayNoTranslation: 14 | - "Sorry, I [could not|was not able to] translate {text} into {language}." 15 | 16 | SayNoTranslationAndElse: 17 | Utterances: 18 | - "Sorry, I [could not|was not able to] translate {text} into {language}." 19 | Reprompts: 20 | - "Say something like

${SamplePhrases}

in ${Languages} or ask me for help." 21 | 22 | SayTranslateAndElse: 23 | Utterances: 24 | - "{text} in {language} [is|translates to] {mp3} Anything else?" 25 | Reprompts: 26 | - "You can ask me to repeat the last translation or let me translate something else. Request help if you are lost." 27 | 28 | SayNoLanguage: 29 | Utterances: 30 | - "I need to know the language. Ask me something like, What is {text}, in ${Languages}." 31 | Reprompts: 32 | - "Ask me something like, {text}, in ${Languages}." 33 | 34 | SayUnsupportedLanguages: 35 | - "${Sorry}, currently I cannot translate to {language} but maybe this is something I will learn soon." 36 | 37 | SayUnsupportedLanguagesAndElse: 38 | Utterances: 39 | - "${Sorry}, currently I cannot translate to {language} but maybe this is something I will learn soon. What else?" 40 | Reprompts: 41 | - "For example say

${SamplePhrases}

in ${Languages}" 42 | 43 | SayTranslateRequest: 44 | Utterances: 45 | - "Provide a word or phrase and the language I should translate into." 46 | Reprompts: 47 | - "For example say

${SamplePhrases}

in ${Languages}" 48 | 49 | SayOk: 50 | - "Ok" 51 | 52 | SayNothingToRepeat: 53 | - "[Excuse me|Sorry|I am sorry], but there is nothing for me to repeat. Let me translate something for you and I will keep it in mind for later repeating." 54 | 55 | SayNothingToRepeatAndElse: 56 | Utterances: 57 | - "[Excuse me|Sorry|I am sorry], but there is nothing for me to repeat. Let me translate something for you and I will keep it in mind for later repeating. Anything else?" 58 | Reprompts: 59 | - "For example say

${SamplePhrases}

in ${Languages}" 60 | 61 | SayHelp: 62 | Utterances: 63 | - "I can translate common terms of your language into a lot of foreign languages. Say something like

${SamplePhrases}

in ${Languages}" 64 | Reprompts: 65 | - "Say something like

${SamplePhrases}

in ${Languages}" 66 | 67 | SaySorry: 68 | Utterances: 69 | - "[Excuse me|Sorry|I am sorry], I could not understand what you were saying." 70 | Reprompts: 71 | - "Say something like

${SamplePhrases}

in ${Languages}" -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/SkillConfig.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.util.Properties; 8 | 9 | /** 10 | * Encapsulates access to application-wide property values 11 | */ 12 | public class SkillConfig { 13 | private static Properties properties = new Properties(); 14 | private static final String defaultPropertiesFile = "app.properties"; 15 | private static final String customPropertiesFile = "my.app.properties"; 16 | 17 | /** 18 | * Static block does the bootstrapping of all configuration properties with 19 | * reading out values from different resource files 20 | */ 21 | static { 22 | final String propertiesFile = 23 | SkillConfig.class.getClassLoader().getResource(customPropertiesFile) != null ? 24 | customPropertiesFile : defaultPropertiesFile; 25 | final InputStream propertiesStream = SkillConfig.class.getClassLoader().getResourceAsStream(propertiesFile); 26 | try { 27 | properties.load(propertiesStream); 28 | } catch (IOException e) { 29 | e.printStackTrace(); 30 | } finally { 31 | if (propertiesStream != null) { 32 | try { 33 | propertiesStream.close(); 34 | } catch (IOException e) { 35 | e.printStackTrace(); 36 | } 37 | } 38 | } 39 | } 40 | 41 | public static String getAlexaAppId() { 42 | return properties.getProperty("AlexaAppId"); 43 | } 44 | 45 | public static String getS3BucketUrl() { 46 | return properties.getProperty("S3BucketUrl"); 47 | } 48 | 49 | public static String getS3BucketName() { 50 | return properties.getProperty("S3BucketName"); 51 | } 52 | 53 | public static String getGoogleProjectName() { 54 | return properties.getProperty("GoogleProjectName"); 55 | } 56 | 57 | public static String getGoogleApiKey() { 58 | return properties.getProperty("GoogleApiKey"); 59 | } 60 | 61 | public static String getMicrosoftSubscriptionKey() { 62 | return properties.getProperty("MicrosoftSubscriptionKey"); 63 | } 64 | 65 | public static String getDynamoTableName() { 66 | return properties.getProperty("DynamoTableName"); 67 | } 68 | 69 | public static String getTranslatorConvertServiceUrl() { 70 | return properties.getProperty("TranslatorConvertServiceUrl"); 71 | } 72 | 73 | public static String getTranslatorConvertServiceUser() { 74 | return properties.getProperty("TranslatorConvertServiceUser"); 75 | } 76 | 77 | public static String getTranslatorConvertServicePass() { 78 | return properties.getProperty("TranslatorConvertServicePass"); 79 | } 80 | 81 | public static String getS3CardFolderUrl() { 82 | return getS3BucketUrl() + properties.getProperty("S3CardFolder"); 83 | } 84 | 85 | public static Boolean shouldSkipMp3Conversion() { 86 | return StringUtils.equalsIgnoreCase("true", properties.getProperty("SkipMp3Conversion")); 87 | } 88 | 89 | public static String getTranslatorService() { 90 | return properties.getProperty("TranslatorService"); 91 | } 92 | 93 | public static String getAlwaysRoundTripPhrase() { 94 | return properties.getProperty("AlwaysRoundTripPhrase"); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /skill/src/main/resources/out/de-DE/utterances.yml: -------------------------------------------------------------------------------- 1 | Languages: "[Russisch|Englisch|Spanisch|Italienisch|Türkisch|Polnisch|Portugiesisch|Japanisch|Schwedisch|Norwegisch|Rumanisch|Niederländisch|Dänisch|Französisch]" 2 | SamplePhrases: "[Guten Tag|Guten Abend|Hallo|Wie geht es dir|Wie heißt du|Bahnhof|Fußball]" 3 | 4 | Hello: "[aloha,|bonjour,|na?|willkommen,]" 5 | Etwas: "etwas" 6 | Sorry: "[achje|ähm|argh|au weia|huch|oje|schachmatt|schade|verflixt|]" 7 | 8 | SayWelcome: 9 | Utterances: 10 | - "${Hello} Was möchtest du mich übersetzen lassen?" 11 | Reprompts: 12 | - "Sage zum Beispiel

${SamplePhrases}

[auf|in] ${Languages}" 13 | 14 | SayTranslate: 15 | - "{text} [auf|in] {language} [bedeutet|heißt|klingt so] {mp3}" 16 | 17 | SayTranslateAndElse: 18 | Utterances: 19 | - "{text} [auf|in] {language} [bedeutet|heißt|klingt so] {mp3} Noch ${Etwas}?" 20 | Reprompts: 21 | - "Du kannst mich wiederholen lassen oder etwas anderes übersetzen lassen. Wenn du nicht weiter weißt, frage mich nach Hilfe." 22 | 23 | SayUnsupportedLanguages: 24 | - "${Sorry}, ich kann noch nichts nach {language} übersetzen aber das lerne ich sicher bald." 25 | 26 | SayUnsupportedLanguagesAndElse: 27 | Utterances: 28 | - "${Sorry}, ich kann noch nichts nach {language} übersetzen aber das lerne ich sicher bald. Noch ${Etwas}?" 29 | Reprompts: 30 | - "Sage zum Beispiel

${SamplePhrases}

[auf|in] ${Languages}" 31 | 32 | SayTranslateRequest: 33 | Utterances: 34 | - "Gib mir ein Wort und eine Sprache, in die ich es übersetzen soll." 35 | Reprompts: 36 | - "Sage zum Beispiel

${SamplePhrases}

[auf|in] ${Languages}" 37 | 38 | SayNoTranslation: 39 | - "${Sorry}, aber {text} konnte ich nicht nach {language} übersetzen." 40 | 41 | SayNoTranslationAndElse: 42 | Utterances: 43 | - "${Sorry}, aber {text} konnte ich nicht nach {language} übersetzen. Noch ${Etwas}?" 44 | Reprompts: 45 | - "Sage zum Beispiel

${SamplePhrases}

[auf|in] ${Languages}" 46 | 47 | SayNoLanguage: 48 | Utterances: 49 | - "Ich brauche eine Sprache in die ich übersetzen soll. Frage zum Beispiel, Was ist {text}, [auf|in] ${Languages}." 50 | Reprompts: 51 | - "Frage zum Beispiel Was ist {text}, [auf|in] ${Languages}." 52 | 53 | SayOk: 54 | - "[ade|alles klar|arrivederci|bis bald|bon voyage|mach's gut|null problemo|tschö|wie du meinst]" 55 | 56 | SayNothingToRepeat: 57 | - "${Sorry}, es gibt für mich nichts zum Wiederholen. Sobald ich etwas für dich übersetzt habe, merke ich es mir und wiederhole später gern." 58 | 59 | SayNothingToRepeatAndElse: 60 | Utterances: 61 | - "${Sorry}, es gibt für mich nichts zum Wiederholen. Sobald ich etwas für dich übersetzt habe, merke ich es mir und wiederhole später gern. Noch ${Etwas}?" 62 | Reprompts: 63 | - "Sage zum Beispiel

${SamplePhrases}

[auf|in] ${Languages}" 64 | 65 | SayHelp: 66 | Utterances: 67 | - "Ich kann dir viele Wörter aus dem Deutschen in fremde Sprachen übersetzen. Sage dazu beispielsweise

Übersetze ${SamplePhrases}

[auf|in] ${Languages}" 68 | Reprompts: 69 | - "Sage zum Beispiel

${SamplePhrases}

[auf|in] ${Languages}" 70 | 71 | SaySorry: 72 | Utterances: 73 | - "${Sorry}, das [probiere bitte nochmal|musst du nochmal probieren|versuche bitte erneut|musst du nochmal versuchen]." 74 | Reprompts: 75 | - "Sage zum Beispiel

${SamplePhrases}

[auf|in] ${Languages}" -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/translate/MicrosoftTranslator.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.translate; 2 | 3 | import io.klerch.alexa.translator.skill.SkillConfig; 4 | import org.apache.commons.io.IOUtils; 5 | import org.apache.commons.lang3.StringEscapeUtils; 6 | import org.apache.commons.lang3.Validate; 7 | import org.apache.http.HttpEntity; 8 | import org.apache.http.HttpResponse; 9 | import org.apache.http.client.methods.HttpGet; 10 | import org.apache.http.client.methods.HttpPost; 11 | import org.apache.http.client.utils.URIBuilder; 12 | import org.apache.http.impl.client.HttpClientBuilder; 13 | 14 | import java.io.IOException; 15 | import java.net.URISyntaxException; 16 | import java.util.logging.Logger; 17 | 18 | public class MicrosoftTranslator extends AbstractTranslator { 19 | private static Logger log = Logger.getLogger(MicrosoftTranslator.class.getName()); 20 | private static final String ServiceEndpointIssueToken = "https://api.cognitive.microsoft.com/sts/v1.0/issueToken"; 21 | private static final String ServiceEndpointTranslate = "https://api.microsofttranslator.com/v2/http.svc/Translate"; 22 | 23 | public MicrosoftTranslator(final String locale) { 24 | super(locale, "/languages-azure.yml"); 25 | } 26 | 27 | @Override 28 | public String doTranslate(final String text, final String targetLanguageCode) { 29 | // if source and target language are the same return original term 30 | if (targetLanguageCode.equalsIgnoreCase(sourceLanguageCode)) { 31 | return text; 32 | } 33 | 34 | try { 35 | final String accessToken = String.format("Bearer %1$s", getAccessToken()); 36 | final URIBuilder uri = new URIBuilder(ServiceEndpointTranslate) 37 | .addParameter("appid", accessToken) 38 | .addParameter("from", StringEscapeUtils.escapeHtml4(sourceLanguageCode)) 39 | .addParameter("to", StringEscapeUtils.escapeHtml4(targetLanguageCode)) 40 | .addParameter("contentType", "text/plain") 41 | .addParameter("text", StringEscapeUtils.escapeHtml4(text)); 42 | final HttpGet httpGet = new HttpGet(uri.build()); 43 | final HttpResponse response = HttpClientBuilder.create().build().execute(httpGet); 44 | 45 | Validate.inclusiveBetween(200, 399, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); 46 | 47 | // work on response 48 | final HttpEntity entity = response.getEntity(); 49 | return IOUtils.toString(entity.getContent(), "UTF-8").replaceAll("<[^>]*>", ""); 50 | } catch (final IOException | URISyntaxException e) { 51 | log.severe(e.getMessage()); 52 | } 53 | return null; 54 | } 55 | 56 | private String getAccessToken() throws URISyntaxException, IOException { 57 | // build uri 58 | final URIBuilder uri = new URIBuilder(ServiceEndpointIssueToken); 59 | final HttpPost httpPost = new HttpPost(uri.build()); 60 | httpPost.setHeader("Content-Type", "text/plain"); 61 | httpPost.setHeader("Ocp-Apim-Subscription-Key", SkillConfig.getMicrosoftSubscriptionKey()); 62 | final HttpResponse response = HttpClientBuilder.create().build().execute(httpPost); 63 | 64 | Validate.inclusiveBetween(200, 399, response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase()); 65 | 66 | // work on response 67 | final HttpEntity entity = response.getEntity(); 68 | return IOUtils.toString(entity.getContent(), "UTF-8"); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/model/TextToSpeech.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.model; 2 | 3 | import io.klerch.alexa.state.model.AlexaScope; 4 | import io.klerch.alexa.state.model.AlexaStateModel; 5 | import io.klerch.alexa.state.model.AlexaStateSave; 6 | import io.klerch.alexa.tellask.schema.annotation.AlexaSlotSave; 7 | import io.klerch.alexa.tellask.schema.type.AlexaOutputFormat; 8 | import org.apache.commons.lang3.Validate; 9 | 10 | @AlexaStateSave(Scope = AlexaScope.APPLICATION) 11 | public class TextToSpeech extends AlexaStateModel { 12 | @AlexaSlotSave(slotName = "translatedText") 13 | private String translatedText; 14 | @AlexaSlotSave(slotName = "mp3", formatAs = AlexaOutputFormat.AUDIO) 15 | private String mp3; 16 | @AlexaSlotSave(slotName = "voice") 17 | private String voice; 18 | @AlexaSlotSave(slotName = "language") 19 | private String language; 20 | @AlexaSlotSave(slotName = "text") 21 | private String text; 22 | 23 | public TextToSpeech() { 24 | // keep this empty constructor. it is important for the magic reflection of the state handlers 25 | } 26 | 27 | private TextToSpeech(final TextToSpeechBuilder builder) { 28 | this.translatedText = builder.translatedText; 29 | this.text = builder.text; 30 | this.voice = builder.voice; 31 | this.language = builder.language; 32 | this.mp3 = builder.mp3; 33 | } 34 | 35 | /** 36 | * @return the translated text 37 | */ 38 | public String getTranslatedText() { 39 | return translatedText; 40 | } 41 | 42 | /** 43 | * @return the original text before it was translated 44 | */ 45 | public String getText() { 46 | return text; 47 | } 48 | 49 | /** 50 | * @return the language coming from the user input (e.g. 'russian' or 'russisch') 51 | */ 52 | public String getLanguage() { 53 | return language; 54 | } 55 | 56 | /** 57 | * @return Url to an MP3 file having the output speech of the translated text 58 | */ 59 | public String getMp3() { 60 | return mp3; 61 | } 62 | 63 | /** 64 | * @return id of the Polly voice used to convert translated text to speech 65 | */ 66 | public String getVoice() { 67 | return voice; 68 | } 69 | 70 | public static TextToSpeechBuilder create() { 71 | return new TextToSpeechBuilder(); 72 | } 73 | 74 | public static class TextToSpeechBuilder { 75 | private String translatedText; 76 | private String mp3; 77 | private String voice; 78 | private String language; 79 | private String text; 80 | 81 | TextToSpeechBuilder() { 82 | } 83 | 84 | public TextToSpeechBuilder withTranslatedText(final String translatedText) { 85 | this.translatedText = translatedText; 86 | return this; 87 | } 88 | 89 | public TextToSpeechBuilder withVoice(final String voice) { 90 | this.voice = voice; 91 | return this; 92 | } 93 | 94 | public TextToSpeechBuilder withMp3(final String mp3Url) { 95 | this.mp3 = mp3Url; 96 | return this; 97 | } 98 | 99 | public TextToSpeechBuilder withText(final String text) { 100 | this.text = text; 101 | return this; 102 | } 103 | 104 | public TextToSpeechBuilder withLanguage(final String language) { 105 | this.language = language; 106 | return this; 107 | } 108 | 109 | public TextToSpeech build() { 110 | Validate.notBlank(text, "Text must not be blank."); 111 | Validate.notBlank(voice, "Voice must not be blank."); 112 | Validate.notBlank(mp3, "Mp3 must not be blank."); 113 | return new TextToSpeech(this); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /server/src/main/java/io/klerch/alexa/translator/service/ConvertService.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.service; 2 | 3 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 4 | import com.amazonaws.auth.BasicSessionCredentials; 5 | import com.amazonaws.regions.Regions; 6 | import com.amazonaws.services.s3.AmazonS3; 7 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 8 | import com.amazonaws.services.s3.model.CannedAccessControlList; 9 | import com.amazonaws.services.s3.model.PutObjectRequest; 10 | import com.amazonaws.services.securitytoken.AWSSecurityTokenService; 11 | import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; 12 | import com.amazonaws.services.securitytoken.model.AssumeRoleRequest; 13 | import com.amazonaws.services.securitytoken.model.Credentials; 14 | import com.amazonaws.util.StringUtils; 15 | import io.klerch.alexa.translator.util.FFmpegUtils; 16 | import org.apache.commons.lang3.Validate; 17 | import org.springframework.stereotype.Component; 18 | 19 | import javax.ws.rs.GET; 20 | import javax.ws.rs.Path; 21 | import javax.ws.rs.Produces; 22 | import javax.ws.rs.QueryParam; 23 | import javax.ws.rs.core.MediaType; 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.util.logging.Logger; 27 | 28 | @Component 29 | @Path("/convert") 30 | public class ConvertService { 31 | private static Logger logger = Logger.getLogger(ConvertService.class.getName()); 32 | 33 | @GET 34 | @Produces(MediaType.APPLICATION_JSON) 35 | public String convertMp3(@QueryParam("bucket") String bucket, @QueryParam("path") String filePath, @QueryParam("roleArn") String roleArn, @QueryParam("region") String region) { 36 | Validate.notBlank(bucket, "Bucket must not be null or empty."); 37 | Validate.notBlank(filePath, "File path must not be null or empty."); 38 | Validate.isTrue(filePath.toLowerCase().endsWith(".mp3"), "Path does not end with '.mp3'."); 39 | 40 | try { 41 | final String cleanedPath = filePath.trim().startsWith("/") ? filePath.trim().substring(1) : filePath; 42 | final String absoluteUrl = String.format("https://s3.amazonaws.com/%1$s/%2$s", bucket, filePath); 43 | final File mp3File = FFmpegUtils.convertUrlToMp3Cmd(absoluteUrl); 44 | uploadFileToS3(mp3File, bucket, cleanedPath, region, roleArn); 45 | // return absolute url to converted file 46 | return absoluteUrl; 47 | } catch (final IOException | InterruptedException e) { 48 | logger.severe(e.getMessage()); 49 | } 50 | return ""; 51 | } 52 | 53 | public void uploadFileToS3(final File file, final String bucket, final String path, final String region, final String roleArn) { 54 | // upload mp3 to S3 bucket 55 | final PutObjectRequest s3Put = new PutObjectRequest(bucket, path, file).withCannedAcl(CannedAccessControlList.PublicRead); 56 | getS3Client(region, roleArn).putObject(s3Put); 57 | 58 | if (!file.delete()) { 59 | logger.warning("Could not delete mp3 temporary audio file."); 60 | } 61 | } 62 | 63 | public static AmazonS3 getS3Client(final String region, final String roleArn) { 64 | final Regions awsRegion = StringUtils.isNullOrEmpty(region) ? Regions.US_EAST_1 : Regions.fromName(region); 65 | 66 | if (StringUtils.isNullOrEmpty(roleArn)) { 67 | return AmazonS3ClientBuilder.standard().withRegion(awsRegion).build(); 68 | } else { 69 | final AssumeRoleRequest assumeRole = new AssumeRoleRequest().withRoleArn(roleArn).withRoleSessionName("io-klerch-mp3-converter"); 70 | 71 | final AWSSecurityTokenService sts = AWSSecurityTokenServiceClientBuilder.standard().withRegion(awsRegion).build(); 72 | final Credentials credentials = sts.assumeRole(assumeRole).getCredentials(); 73 | 74 | final BasicSessionCredentials sessionCredentials = new BasicSessionCredentials( 75 | credentials.getAccessKeyId(), 76 | credentials.getSecretAccessKey(), 77 | credentials.getSessionToken()); 78 | 79 | return AmazonS3ClientBuilder.standard().withRegion(awsRegion).withCredentials(new AWSStaticCredentialsProvider(sessionCredentials)).build(); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | io.klerch 7 | alexa.translator.server 8 | 2.0.0 9 | jar 10 | 11 | alexa.translator.server 12 | 13 | 14 | org.springframework.boot 15 | spring-boot-starter-parent 16 | 1.4.3.RELEASE 17 | 18 | 19 | 20 | UTF-8 21 | UTF-8 22 | 1.8 23 | io.klerch.alexa.translator 24 | latest 25 | 661661179496.dkr.ecr.us-east-1.amazonaws.com 26 | 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-jersey 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | net.bramp.ffmpeg 39 | ffmpeg 40 | 0.6.1 41 | 42 | 43 | com.amazonaws 44 | aws-java-sdk-s3 45 | 1.11.95 46 | 47 | 48 | com.amazonaws 49 | aws-java-sdk-sts 50 | 1.11.95 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-security 55 | 56 | 57 | 58 | 59 | 60 | 61 | org.springframework.boot 62 | spring-boot-maven-plugin 63 | 64 | 65 | com.spotify 66 | docker-maven-plugin 67 | 0.4.12 68 | 69 | false 70 | 72 | ${docker.image.name}:${docker.image.version} 73 | src/main/docker 74 | 75 | 76 | / 77 | ${project.build.directory} 78 | ${project.build.finalName}.jar 79 | 80 | 81 | 82 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.klerch 8 | alexa.translator.client 9 | 1.0 10 | 11 | 12 | 13 | io.lerch.repo 14 | s3://io.klerch.maven.repo/snapshot 15 | 16 | 17 | io.lerch.repo 18 | s3://io.klerch.maven.repo/release 19 | 20 | 21 | 22 | 23 | 2.16 24 | UTF-8 25 | 1.11.63 26 | 1.1.0 27 | 1.0.0 28 | 2.8.2 29 | 1.2 30 | 31 | 32 | 33 | 34 | 35 | org.kuali.maven.wagons 36 | maven-s3-wagon 37 | 1.2.1 38 | 39 | 40 | 41 | 42 | src/main/resources 43 | true 44 | 45 | 46 | 47 | 48 | maven-compiler-plugin 49 | 3.5.1 50 | 51 | 1.8 52 | 1.8 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-shade-plugin 58 | 2.4.3 59 | 60 | false 61 | 62 | 63 | 64 | package 65 | 66 | shade 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | com.fasterxml.jackson.core 77 | jackson-core 78 | ${jackson.version} 79 | 80 | 81 | com.fasterxml.jackson.core 82 | jackson-databind 83 | ${jackson.version} 84 | 85 | 86 | com.fasterxml.jackson.core 87 | jackson-annotations 88 | ${jackson.version} 89 | 90 | 91 | com.amazon.alexa 92 | alexa-skills-kit 93 | ${alexa-skillskit-version} 94 | 95 | 96 | com.amazonaws 97 | aws-lambda-java-core 98 | ${aws.lambda.version} 99 | 100 | 101 | com.amazonaws 102 | aws-java-sdk-lambda 103 | ${aws.version} 104 | 105 | 106 | com.amazonaws 107 | aws-lambda-java-log4j 108 | ${aws.log4j.version} 109 | 110 | 111 | junit 112 | junit 113 | RELEASE 114 | test 115 | 116 | 117 | -------------------------------------------------------------------------------- /server/mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /skill/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.klerch 8 | alexa.translator.skill 9 | 1.4.0 10 | 11 | 12 | 2.16 13 | UTF-8 14 | 0.2.3 15 | 0.1.1 16 | 1.11.190 17 | 1.1.0 18 | 1.0.0 19 | 4.12 20 | 21 | 22 | 23 | 24 | io.lerch.repo 25 | s3://io.klerch.maven.repo/snapshot 26 | 27 | 28 | io.lerch.repo 29 | s3://io.klerch.maven.repo/release 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.kuali.maven.wagons 37 | maven-s3-wagon 38 | 1.2.1 39 | 40 | 41 | 42 | 43 | src/main/resources 44 | true 45 | 46 | 47 | 48 | 49 | maven-compiler-plugin 50 | 3.5.1 51 | 52 | 1.8 53 | 1.8 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-shade-plugin 59 | 2.4.3 60 | 61 | false 62 | 63 | 64 | 65 | package 66 | 67 | shade 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | com.amazonaws 78 | aws-lambda-java-core 79 | ${aws.lambda.version} 80 | 81 | 82 | io.klerch 83 | alexa-skills-kit-tellask-java 84 | ${io.klerch.tellask.version} 85 | 86 | 87 | com.amazonaws 88 | aws-java-sdk-polly 89 | ${aws.version} 90 | 91 | 92 | com.amazonaws 93 | aws-java-sdk-s3 94 | ${aws.version} 95 | 96 | 97 | com.amazonaws 98 | aws-java-sdk-dynamodb 99 | ${aws.version} 100 | 101 | 102 | com.amazonaws 103 | aws-java-sdk-kms 104 | 105 | 106 | 107 | 108 | com.amazonaws 109 | aws-lambda-java-log4j 110 | ${aws.log4j.version} 111 | 112 | 113 | com.google.apis 114 | google-api-services-translate 115 | v2-rev48-1.22.0 116 | provided 117 | 118 | 119 | com.google.http-client 120 | google-http-client-gson 121 | 1.22.0 122 | provided 123 | 124 | 125 | junit 126 | junit 127 | ${junit.version} 128 | test 129 | 130 | 131 | io.klerch 132 | alexa-skills-kit-tester-java 133 | ${io.klerch.tester.version} 134 | test 135 | 136 | 137 | org.jooq 138 | joox 139 | 1.4.0 140 | test 141 | 142 | 143 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/handler/AbstractIntentHandler.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.handler; 2 | 3 | import com.amazon.speech.ui.Image; 4 | import com.amazon.speech.ui.StandardCard; 5 | import io.klerch.alexa.state.handler.AWSDynamoStateHandler; 6 | import io.klerch.alexa.state.utils.AlexaStateException; 7 | import io.klerch.alexa.tellask.model.AlexaInput; 8 | import io.klerch.alexa.tellask.model.AlexaOutput; 9 | import io.klerch.alexa.tellask.schema.AlexaIntentHandler; 10 | import io.klerch.alexa.tellask.util.AlexaRequestHandlerException; 11 | import io.klerch.alexa.translator.skill.SkillConfig; 12 | import io.klerch.alexa.translator.skill.model.LastTextToSpeech; 13 | import io.klerch.alexa.translator.skill.model.SessionState; 14 | import io.klerch.alexa.translator.skill.model.TextToSpeech; 15 | import io.klerch.alexa.translator.skill.tts.TextToSpeechConverter; 16 | import org.apache.commons.lang3.StringUtils; 17 | import org.apache.log4j.Logger; 18 | 19 | import java.util.Optional; 20 | 21 | abstract class AbstractIntentHandler implements AlexaIntentHandler { 22 | private static final Logger log = Logger.getLogger(AbstractIntentHandler.class); 23 | 24 | @Override 25 | public boolean verify(final AlexaInput input) { 26 | return true; 27 | } 28 | 29 | @Override 30 | public abstract AlexaOutput handleRequest(final AlexaInput input) throws AlexaRequestHandlerException, AlexaStateException; 31 | 32 | @Override 33 | public AlexaOutput handleError(final AlexaRequestHandlerException exception) { 34 | log.error("ERROR: " + exception.getMessage()); 35 | if (exception.getCause() != null) { 36 | log.error("ERROR: " + exception.getCause().getMessage()); 37 | } 38 | 39 | return isConversation(exception.getInput()) ? 40 | AlexaOutput.ask("SaySorry").withReprompt(true).build() : 41 | AlexaOutput.tell("SaySorry").build(); 42 | } 43 | 44 | AlexaOutput sayTranslate(final AlexaInput input, final TextToSpeech tts) { 45 | final StandardCard card = new StandardCard(); 46 | card.setTitle(StringUtils.capitalize(tts.getText()) + " : " + StringUtils.capitalize(tts.getTranslatedText())); 47 | card.setText("http://aka.ms/MicrosoftTranslatorAttribution"); 48 | 49 | final String imgUrl = String.format("%1$s/%2$s-%3$s-%4$s.png", SkillConfig.getS3CardFolderUrl(), input.getLocale(), tts.getVoice().toLowerCase(), SkillConfig.getTranslatorService().toLowerCase()); 50 | 51 | final Image img = new Image(); 52 | img.setLargeImageUrl(imgUrl); 53 | img.setSmallImageUrl(imgUrl); 54 | card.setImage(img); 55 | 56 | // remember the current tts as last tts of user 57 | final AWSDynamoStateHandler dynamoStateHandler = new AWSDynamoStateHandler(input.getSessionStateHandler().getSession(), SkillConfig.getDynamoTableName()); 58 | final LastTextToSpeech lastTts = new LastTextToSpeech(tts); 59 | 60 | // if no one-shot this conversation keeps open and user can go on with other options 61 | if (isConversation(input)) { 62 | return AlexaOutput.ask("SayTranslateAndElse") 63 | .withCard(card) 64 | .withReprompt(true) 65 | .putState(tts, lastTts.withHandler(dynamoStateHandler)) 66 | .build(); 67 | } else { 68 | // a one-shot invocation returns the translation and ends the session 69 | return AlexaOutput.tell("SayTranslate") 70 | .withCard(card) 71 | .putState(tts, lastTts.withHandler(dynamoStateHandler)) 72 | .build(); 73 | } 74 | } 75 | 76 | AlexaOutput sayTranslate(final AlexaInput input, final String text) throws AlexaStateException { 77 | if (!input.hasSlotNotBlank("language")) { 78 | return AlexaOutput.ask("SayNoLanguage") 79 | .putSlot("text", text) 80 | .withReprompt(true) 81 | .build(); 82 | } 83 | 84 | final TextToSpeechConverter ttsConverter = new TextToSpeechConverter(input); 85 | 86 | if (ttsConverter.hasSupportedLanguage()) { 87 | return ttsConverter.textToSpeech(text).map(tts -> sayTranslate(input, tts)).orElse( 88 | isConversation(input) ? 89 | AlexaOutput.ask("SayNoTranslationAndElse") 90 | .putSlot("text", text) 91 | .putSlot("language", ttsConverter.getLanguage()) 92 | .withReprompt(true) 93 | .build() : 94 | AlexaOutput.tell("SayNoTranslation") 95 | .putSlot("text", text) 96 | .putSlot("language", ttsConverter.getLanguage()) 97 | .build()); 98 | } else { 99 | return isConversation(input) ? 100 | AlexaOutput.ask("SayUnsupportedLanguagesAndElse") 101 | .putSlot("language", ttsConverter.getLanguage()) 102 | .withReprompt(true) 103 | .build() : 104 | AlexaOutput.tell("SayUnsupportedLanguages") 105 | .putSlot("language", ttsConverter.getLanguage()) 106 | .build(); 107 | } 108 | } 109 | 110 | boolean isConversation(final AlexaInput input) { 111 | // find out if this is no one-shot 112 | Optional sessionState = Optional.empty(); 113 | try { 114 | sessionState = input.getSessionStateHandler().readModel(SessionState.class); 115 | } catch (final AlexaStateException e) { 116 | log.error("Unable to read session state.", e); 117 | } 118 | return sessionState.isPresent() && sessionState.get().getConversation(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /client/src/main/java/io/klerch/alexa/translator/client/SkillClient.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.client; 2 | 3 | import com.amazon.speech.json.SpeechletRequestEnvelope; 4 | import com.amazon.speech.slu.Intent; 5 | import com.amazon.speech.slu.Slot; 6 | import com.amazon.speech.speechlet.*; 7 | import com.amazonaws.services.lambda.AWSLambdaClient; 8 | import com.amazonaws.services.lambda.model.InvocationType; 9 | import com.amazonaws.services.lambda.model.InvokeRequest; 10 | import com.amazonaws.services.lambda.model.InvokeResult; 11 | import com.fasterxml.jackson.core.JsonProcessingException; 12 | import com.fasterxml.jackson.databind.ObjectMapper; 13 | import org.apache.log4j.Logger; 14 | 15 | import java.util.Date; 16 | import java.util.HashMap; 17 | import java.util.Locale; 18 | import java.util.Map; 19 | 20 | public class SkillClient { 21 | private static final Logger log = Logger.getLogger(SkillClient.class); 22 | 23 | private static String requestId = AppConfig.getAlexaRequestId(); 24 | private static String sessionId = AppConfig.getAlexaSessionId(); 25 | private static String appId = AppConfig.getAlexaAppId(); 26 | private static String userId = AppConfig.getAlexaUserId(); 27 | private final String locale; 28 | private static String lambdaName = AppConfig.getLambdaName(); 29 | 30 | public SkillClient(final String locale) { 31 | this.locale = locale; 32 | } 33 | 34 | public String getLocale() { 35 | return this.locale; 36 | } 37 | 38 | public String translate(final String testPhrase, final String language) { 39 | final Map slots = new HashMap<>(); 40 | slots.put("termA", Slot.builder().withName("termA").withValue(testPhrase).build()); 41 | slots.put("termB", Slot.builder().withName("termB").build()); 42 | slots.put("language", Slot.builder().withName("language").withValue(language).build()); 43 | final SpeechletRequestEnvelope envelope = givenIntentSpeechletRequestEnvelope("Translate", slots); 44 | final ObjectMapper mapper = new ObjectMapper(); 45 | String response = null; 46 | try { 47 | final AWSLambdaClient awsLambda = new AWSLambdaClient(); 48 | final InvokeRequest invokeRequest = new InvokeRequest() 49 | .withInvocationType(InvocationType.RequestResponse) 50 | .withFunctionName(lambdaName) 51 | .withPayload(mapper.writeValueAsString(envelope)); 52 | final InvokeResult invokeResult = awsLambda.invoke(invokeRequest); 53 | response = new String(invokeResult.getPayload().array()); 54 | } catch (JsonProcessingException e) { 55 | log.error(e.getMessage()); 56 | } 57 | return response; 58 | } 59 | 60 | private SpeechletRequestEnvelope givenLaunchSpeechletRequestEnvelope() { 61 | return givenLaunchSpeechletRequestEnvelope(appId); 62 | } 63 | 64 | private SpeechletRequestEnvelope givenLaunchSpeechletRequestEnvelope(final String applicationId) { 65 | return SpeechletRequestEnvelope.builder() 66 | .withRequest(givenLaunchRequest()) 67 | .withSession(givenSession(applicationId)) 68 | .withVersion("1.0.0") 69 | .build(); 70 | } 71 | 72 | private SpeechletRequestEnvelope givenIntentSpeechletRequestEnvelope(final String intentName) { 73 | return givenIntentSpeechletRequestEnvelope(intentName, null); 74 | } 75 | 76 | private SpeechletRequestEnvelope givenIntentSpeechletRequestEnvelope(final String intentName, final Map slots) { 77 | return SpeechletRequestEnvelope.builder() 78 | .withRequest(givenIntentRequest(intentName, slots)) 79 | .withSession(givenSession(appId)) 80 | .withVersion("1.0.0") 81 | .build(); 82 | } 83 | 84 | private Session givenSession() { 85 | return givenSession(appId); 86 | } 87 | 88 | private Session givenSession(final String applicationId) { 89 | final Application application = new Application(applicationId); 90 | final User user = User.builder().withUserId(userId).withAccessToken("accessToken").build(); 91 | return Session.builder().withSessionId(sessionId) 92 | .withApplication(application).withUser(user).build(); 93 | } 94 | 95 | private IntentRequest givenIntentRequest(final String intentName, final Map slots) { 96 | Map slotsForSure = slots != null ? slots : new HashMap<>(); 97 | final Intent intent = Intent.builder() 98 | .withName(intentName) 99 | .withSlots(slotsForSure) 100 | .build(); 101 | return IntentRequest.builder() 102 | .withRequestId(requestId) 103 | .withTimestamp(new Date()) 104 | .withIntent(intent) 105 | .withLocale(Locale.forLanguageTag(locale)) 106 | .build(); 107 | } 108 | 109 | private IntentRequest givenIntentRequest(final String intentName) { 110 | return givenIntentRequest(intentName, null); 111 | } 112 | 113 | private LaunchRequest givenLaunchRequest() { 114 | return LaunchRequest.builder() 115 | .withRequestId(requestId) 116 | .withTimestamp(new Date()) 117 | .withLocale(Locale.forLanguageTag(locale)) 118 | .build(); 119 | } 120 | 121 | private SessionStartedRequest givenSessionStartedRequest() { 122 | return SessionStartedRequest.builder() 123 | .withRequestId(requestId) 124 | .withLocale(Locale.forLanguageTag(locale)) 125 | .withTimestamp(new Date()).build(); 126 | } 127 | 128 | private SessionEndedRequest givenSessionEndedRequest() { 129 | return SessionEndedRequest.builder() 130 | .withRequestId(requestId) 131 | .withLocale(Locale.forLanguageTag(locale)) 132 | .withReason(SessionEndedRequest.Reason.USER_INITIATED) 133 | .withTimestamp(new Date()).build(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /server/src/main/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM frolvlad/alpine-oraclejdk8:slim 2 | VOLUME /tmp 3 | WORKDIR /tmp/workdir 4 | ADD alexa.translator.server-1.0.0.jar app.jar 5 | RUN sh -c 'touch /app.jar' 6 | ENV JAVA_OPTS="" \ 7 | FFMPEG_VERSION=3.2.2 \ 8 | FDKAAC_VERSION=0.1.4 \ 9 | LAME_VERSION=3.99.5 \ 10 | OGG_VERSION=1.3.2 \ 11 | OPUS_VERSION=1.1.1 \ 12 | THEORA_VERSION=1.1.1 \ 13 | YASM_VERSION=1.3.0 \ 14 | VORBIS_VERSION=1.3.5 \ 15 | VPX_VERSION=1.6.0 \ 16 | XVID_VERSION=1.3.4 \ 17 | X265_VERSION=2.0 \ 18 | X264_VERSION=20160826-2245-stable \ 19 | PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ 20 | SRC=/usr/local 21 | 22 | ENTRYPOINT [ "sh", "-c", "java $JAVA_OPTS -Dspring.profiles.active=prod -Djava.security.egd=file:/dev/./urandom -jar /app.jar" ] 23 | 24 | RUN buildDeps="autoconf \ 25 | automake \ 26 | bash \ 27 | binutils \ 28 | bzip2 \ 29 | cmake \ 30 | curl \ 31 | coreutils \ 32 | g++ \ 33 | gcc \ 34 | libtool \ 35 | make \ 36 | openssl-dev \ 37 | tar \ 38 | yasm \ 39 | zlib-dev" && \ 40 | export MAKEFLAGS="-j$(($(grep -c ^processor /proc/cpuinfo) + 1))" && \ 41 | apk add --update ${buildDeps} libgcc libstdc++ ca-certificates && \ 42 | 43 | DIR=$(mktemp -d) && cd ${DIR} && \ 44 | ## x264 http://www.videolan.org/developers/x264.html 45 | curl -sL https://ftp.videolan.org/pub/videolan/x264/snapshots/x264-snapshot-${X264_VERSION}.tar.bz2 | \ 46 | tar -jx --strip-components=1 && \ 47 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --enable-pic --enable-shared --disable-cli && \ 48 | make && \ 49 | make install && \ 50 | rm -rf ${DIR} && \ 51 | DIR=$(mktemp -d) && cd ${DIR} && \ 52 | ## x265 http://x265.org/ 53 | curl -sL https://download.videolan.org/pub/videolan/x265/x265_${X265_VERSION}.tar.gz | \ 54 | tar -zx && \ 55 | cd x265_${X265_VERSION}/build/linux && \ 56 | ./multilib.sh && \ 57 | make -C 8bit install && \ 58 | rm -rf ${DIR} && \ 59 | DIR=$(mktemp -d) && cd ${DIR} && \ 60 | ## libogg https://www.xiph.org/ogg/ 61 | curl -sL http://downloads.xiph.org/releases/ogg/libogg-${OGG_VERSION}.tar.gz | \ 62 | tar -zx --strip-components=1 && \ 63 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-static --datarootdir=${DIR} && \ 64 | make && \ 65 | make install && \ 66 | rm -rf ${DIR} && \ 67 | DIR=$(mktemp -d) && cd ${DIR} && \ 68 | ## libopus https://www.opus-codec.org/ 69 | curl -sL http://downloads.xiph.org/releases/opus/opus-${OPUS_VERSION}.tar.gz | \ 70 | tar -zx --strip-components=1 && \ 71 | autoreconf -fiv && \ 72 | ./configure --prefix="${SRC}" --disable-static --datadir="${DIR}" && \ 73 | make && \ 74 | make install && \ 75 | rm -rf ${DIR} && \ 76 | DIR=$(mktemp -d) && cd ${DIR} && \ 77 | ## libvorbis https://xiph.org/vorbis/ 78 | curl -sL http://downloads.xiph.org/releases/vorbis/libvorbis-${VORBIS_VERSION}.tar.gz | \ 79 | tar -zx --strip-components=1 && \ 80 | ./configure --prefix="${SRC}" --with-ogg="${SRC}" --bindir="${SRC}/bin" \ 81 | --disable-static --datadir="${DIR}" && \ 82 | make && \ 83 | make install && \ 84 | rm -rf ${DIR} && \ 85 | DIR=$(mktemp -d) && cd ${DIR} && \ 86 | ## libtheora http://www.theora.org/ 87 | curl -sL http://downloads.xiph.org/releases/theora/libtheora-${THEORA_VERSION}.tar.bz2 | \ 88 | tar -jx --strip-components=1 && \ 89 | ./configure --prefix="${SRC}" --with-ogg="${SRC}" --bindir="${SRC}/bin" \ 90 | --disable-static --datadir="${DIR}" && \ 91 | make && \ 92 | make install && \ 93 | rm -rf ${DIR} && \ 94 | DIR=$(mktemp -d) && cd ${DIR} && \ 95 | ## libvpx https://www.webmproject.org/code/ 96 | curl -sL https://codeload.github.com/webmproject/libvpx/tar.gz/v${VPX_VERSION} | \ 97 | tar -zx --strip-components=1 && \ 98 | ./configure --prefix="${SRC}" --enable-vp8 --enable-vp9 --enable-pic --disable-debug --disable-examples --disable-docs --disable-install-bins --enable-shared && \ 99 | make && \ 100 | make install && \ 101 | rm -rf ${DIR} && \ 102 | DIR=$(mktemp -d) && cd ${DIR} && \ 103 | ## libmp3lame http://lame.sourceforge.net/ 104 | curl -sL https://downloads.sf.net/project/lame/lame/${LAME_VERSION%.*}/lame-${LAME_VERSION}.tar.gz | \ 105 | tar -zx --strip-components=1 && \ 106 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --disable-static --enable-nasm --datarootdir="${DIR}" && \ 107 | make && \ 108 | make install && \ 109 | rm -rf ${DIR} && \ 110 | DIR=$(mktemp -d) && cd ${DIR} && \ 111 | ## xvid https://www.xvid.com/ 112 | curl -sL http://downloads.xvid.org/downloads/xvidcore-${XVID_VERSION}.tar.gz | \ 113 | tar -zx && \ 114 | cd xvidcore/build/generic && \ 115 | ./configure --prefix="${SRC}" --bindir="${SRC}/bin" --datadir="${DIR}" --disable-static --enable-shared && \ 116 | make && \ 117 | make install && \ 118 | rm -rf ${DIR} && \ 119 | DIR=$(mktemp -d) && cd ${DIR} && \ 120 | ## fdk-aac https://github.com/mstorsjo/fdk-aac 121 | curl -sL https://github.com/mstorsjo/fdk-aac/archive/v${FDKAAC_VERSION}.tar.gz | \ 122 | tar -zx --strip-components=1 && \ 123 | autoreconf -fiv && \ 124 | ./configure --prefix="${SRC}" --disable-static --datadir="${DIR}" && \ 125 | make && \ 126 | make install && \ 127 | make distclean && \ 128 | rm -rf ${DIR} && \ 129 | DIR=$(mktemp -d) && cd ${DIR} && \ 130 | ## ffmpeg https://ffmpeg.org/ 131 | curl -sL http://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz | \ 132 | tar -zx --strip-components=1 && \ 133 | ./configure --prefix="${SRC}" \ 134 | --extra-cflags="-I${SRC}/include" \ 135 | --extra-ldflags="-L${SRC}/lib" \ 136 | --bindir="${SRC}/bin" \ 137 | --disable-doc \ 138 | --disable-static \ 139 | --enable-shared \ 140 | --disable-ffplay \ 141 | --extra-libs=-ldl \ 142 | --enable-version3 \ 143 | --enable-libfdk_aac \ 144 | --enable-libmp3lame \ 145 | --enable-libopus \ 146 | --enable-libtheora \ 147 | --enable-libvorbis \ 148 | --enable-libvpx \ 149 | --enable-libx264 \ 150 | --enable-libx265 \ 151 | --enable-libxvid \ 152 | --enable-gpl \ 153 | --enable-avresample \ 154 | --enable-postproc \ 155 | --enable-nonfree \ 156 | --disable-debug \ 157 | --enable-small \ 158 | --enable-openssl && \ 159 | make && \ 160 | make install && \ 161 | make distclean && \ 162 | hash -r && \ 163 | cd tools && \ 164 | make qt-faststart && \ 165 | cp qt-faststart ${SRC}/bin && \ 166 | rm -rf ${DIR} && \ 167 | # cleanup 168 | cd && \ 169 | apk del ${buildDeps} && \ 170 | rm -rf /var/cache/apk/* /usr/local/include && \ 171 | ffmpeg -buildconf 172 | 173 | -------------------------------------------------------------------------------- /server/mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /skill/src/main/java/io/klerch/alexa/translator/skill/tts/TextToSpeechConverter.java: -------------------------------------------------------------------------------- 1 | package io.klerch.alexa.translator.skill.tts; 2 | 3 | import com.amazonaws.services.polly.AmazonPolly; 4 | import com.amazonaws.services.polly.AmazonPollyClientBuilder; 5 | import com.amazonaws.services.polly.model.OutputFormat; 6 | import com.amazonaws.services.polly.model.SynthesizeSpeechRequest; 7 | import com.amazonaws.services.polly.model.SynthesizeSpeechResult; 8 | import com.amazonaws.services.polly.model.TextType; 9 | import com.amazonaws.services.s3.AmazonS3; 10 | import com.amazonaws.services.s3.AmazonS3ClientBuilder; 11 | import com.amazonaws.services.s3.model.CannedAccessControlList; 12 | import com.amazonaws.services.s3.model.ObjectMetadata; 13 | import com.amazonaws.services.s3.model.PutObjectRequest; 14 | import io.klerch.alexa.state.handler.AWSDynamoStateHandler; 15 | import io.klerch.alexa.state.handler.AlexaStateHandler; 16 | import io.klerch.alexa.state.utils.AlexaStateException; 17 | import io.klerch.alexa.tellask.model.AlexaInput; 18 | import io.klerch.alexa.tellask.util.resource.ResourceUtteranceReader; 19 | import io.klerch.alexa.tellask.util.resource.YamlReader; 20 | import io.klerch.alexa.translator.skill.SkillConfig; 21 | import io.klerch.alexa.translator.skill.model.TextToSpeech; 22 | import io.klerch.alexa.translator.skill.translate.Translator; 23 | import io.klerch.alexa.translator.skill.translate.TranslatorFactory; 24 | import org.apache.commons.lang3.StringUtils; 25 | import org.apache.commons.lang3.Validate; 26 | import org.apache.log4j.Logger; 27 | 28 | import java.io.IOException; 29 | import java.net.URISyntaxException; 30 | import java.util.List; 31 | import java.util.Optional; 32 | 33 | public class TextToSpeechConverter { 34 | private static final Logger log = Logger.getLogger(TextToSpeechConverter.class); 35 | 36 | private final String locale; 37 | private final Translator translator; 38 | private final String language; 39 | private final YamlReader yamlReader; 40 | private final AmazonPolly awsPolly; 41 | private final AmazonS3 awsS3; 42 | private final String voiceId; 43 | private final AlexaStateHandler dynamoStateHandler; 44 | private final AlexaStateHandler sessionStateHandler; 45 | private final List prefixesToRemove; 46 | 47 | public TextToSpeechConverter(final AlexaInput input) { 48 | // the locale is coming with the speechlet request and indicates to source language to translate from 49 | this.locale = input.getLocale(); 50 | // get translator 51 | this.translator = TranslatorFactory.getTranslator(this.locale); 52 | // the language is taken from the user input (slot value) and indicates to language to translate to 53 | this.language = input.getSlotValue("language"); 54 | final ResourceUtteranceReader reader = new ResourceUtteranceReader("/out", "/voices.yml"); 55 | // the yaml reader reads values from YAML file to get a Polly voiceId for a language 56 | this.yamlReader = new YamlReader(reader, locale); 57 | // Polly client to request speech of a translated text 58 | this.awsPolly = AmazonPollyClientBuilder.standard().build(); 59 | // S3 client to store MP3 with speech of a translated text 60 | this.awsS3 = AmazonS3ClientBuilder.standard().build(); 61 | // session state handler to read/write skill state information to Alexa session 62 | this.sessionStateHandler = input.getSessionStateHandler(); 63 | // dynamo state handler to read/write skill state information to DynamoDB 64 | this.dynamoStateHandler = new AWSDynamoStateHandler(input.getSessionStateHandler().getSession(), SkillConfig.getDynamoTableName()); 65 | // retrieve voiceId from YAML file that maps to the language given by the user 66 | voiceId = language != null ? yamlReader.getRandomUtterance(language.toLowerCase().replace(" ", "_")).orElse("") : ""; 67 | // language-specific prefix phrases that accidently made it into the text slot and should be removed 68 | prefixesToRemove = yamlReader.getUtterances("PREFIXES_TO_REMOVE"); 69 | } 70 | 71 | /** 72 | * Generates the id unique per text, locale and voice. This id is used to store 73 | * translation information in DynamoDB 74 | * @param text The text (not the translation) 75 | * @return id of translation in dictionary 76 | */ 77 | private String getDictionaryId(final String text) { 78 | final String escapedText = text.replace(" ", "_") 79 | .replaceAll("(?i)ö", "oe") 80 | .replaceAll("(?i)ä", "ae") 81 | .replaceAll("(?i)ü", "ue") 82 | .replaceAll("(?i)ß", "ss") 83 | .replaceAll("[^a-zA-Z0-9_\\-]", ""); 84 | return String.format("%1$s-%2$s_%3$s", locale, voiceId, escapedText); 85 | } 86 | 87 | /** 88 | * Generates the path to an mp3 having the speech of the translation 89 | * @param text The text (not the translation) 90 | * @return path to an mp3 having the speech of the translation 91 | */ 92 | private String getMp3Path(final String text) { 93 | return String.format("%1$s/%2$s/%3$s.mp3", locale, voiceId, text.replace(" ", "_")); 94 | } 95 | 96 | /** 97 | * Generates the url to an mp3 having the speech of the translation 98 | * @param text The text (not the translation) 99 | * @return path to an mp3 having the speech of the translation 100 | */ 101 | private String getMp3Url(final String text) { 102 | return SkillConfig.getS3BucketUrl() + getMp3Path(text); 103 | } 104 | 105 | public boolean hasSupportedLanguage() { 106 | return translator.getTargetLangCodeIfSupported(language).isPresent(); 107 | } 108 | 109 | /** 110 | * Returns text-to-speech of a translation of a given text. Before translating the text, 111 | * requesting speech from AWS Polly and storing the resulting MP3 to S3 this method looks 112 | * up previous translation of the same text. Once found it will avoid doing the aforementioned 113 | * roundtrip but rather will use the data of the previous translation. 114 | * @param text text to translate and convert to speech 115 | * @return text to speech information 116 | * @throws AlexaStateException error reading or writing state to Dynamo dictionary 117 | */ 118 | public Optional textToSpeech(final String text) throws AlexaStateException { 119 | // remove invalid prefixes that accidently made it into the slots 120 | final String textToTranslate = prefixesToRemove.stream() 121 | .filter(prefix -> StringUtils.startsWithIgnoreCase(text, prefix)) 122 | .findFirst() 123 | .map(prefix -> text.replaceFirst(prefix, "")) 124 | // if none of these prefixes exist in the text, keep the text as is 125 | .orElse(text); 126 | 127 | // look up previous translation in dictionary 128 | Optional tts = dynamoStateHandler.readModel(TextToSpeech.class, getDictionaryId(textToTranslate)); 129 | // if there was a previous tts for this text return immediately (exception for the roundtrip-phrase used by the test-client) 130 | if (tts.isPresent() && !StringUtils.equalsIgnoreCase(textToTranslate, SkillConfig.getAlwaysRoundTripPhrase())) { 131 | // set handler to session to avoid writing back to dynamo (nothing changed) 132 | tts.get().setHandler(sessionStateHandler); 133 | return tts; 134 | } 135 | 136 | // translate term by leveraging a Translator implementation provided by the factory 137 | final Optional translated = translator.translate(textToTranslate, language); 138 | 139 | if (translated.isPresent()) { 140 | // without a voiceId there's not chance to fulfill the translation request 141 | Validate.notBlank(voiceId, "No voiceId is associated with given language."); 142 | // form the SSML by embedding the translated text 143 | final String ssml = String.format("%1$s", translated.get()); 144 | // build a Polly request to get speech with desired voice and SSML 145 | final SynthesizeSpeechRequest synthRequest = new SynthesizeSpeechRequest() 146 | .withText(ssml) 147 | .withOutputFormat(OutputFormat.Mp3) 148 | .withVoiceId(voiceId) 149 | .withTextType(TextType.Ssml) 150 | .withSampleRate("22050"); 151 | // fire request to Polly 152 | final SynthesizeSpeechResult synthResult = awsPolly.synthesizeSpeech(synthRequest); 153 | 154 | try { 155 | // store audio stream of Polly to S3 as an MP3 file 156 | final PutObjectRequest s3Put = new PutObjectRequest(SkillConfig.getS3BucketName(), getMp3Path(textToTranslate), synthResult.getAudioStream(), new ObjectMetadata()) 157 | .withCannedAcl(CannedAccessControlList.PublicRead); 158 | awsS3.putObject(s3Put); 159 | // as long as Polly output does not comply with Alexa MP3 format restriction we need to convert the MP3 160 | if (!SkillConfig.shouldSkipMp3Conversion()) { 161 | // call the REST service that encapsualtes the FFMPEG conversion on a server 162 | final String mp3ConvertedUrl = Mp3Converter.convertMp3(getMp3Path(textToTranslate)); 163 | // validate this service returned a url (equal to success) 164 | Validate.notBlank(mp3ConvertedUrl, "Conversion service did not return proper return value"); 165 | } 166 | // build the TTS object with all the information needed to return output speech 167 | return Optional.of(getTTS(textToTranslate, translated.get())); 168 | } catch (final IOException | URISyntaxException e) { 169 | log.error("Error while generating mp3. " + e.getMessage()); 170 | } 171 | } 172 | return Optional.empty(); 173 | } 174 | 175 | /** 176 | * Does the TTS object creation for you as all related information can be generated 177 | * from a given text and its translation. 178 | * @param text the original text 179 | * @param translatedText the translated text 180 | * @return TTS object holding all information 181 | */ 182 | private TextToSpeech getTTS(final String text, final String translatedText) { 183 | final TextToSpeech tts = TextToSpeech.create() 184 | .withLanguage(language) 185 | .withText(text) 186 | .withMp3(getMp3Url(text)) 187 | .withVoice(voiceId) 188 | .withTranslatedText(translatedText).build(); 189 | // this object needs to be saved in dynamo dictionary 190 | tts.setHandler(dynamoStateHandler); 191 | // using an unique identifier 192 | tts.setId(getDictionaryId(text)); 193 | return tts; 194 | } 195 | 196 | /** 197 | * @return the language a text is translated to. This is taken directly from the 198 | * user input so it contains the spoken version like 'russian' or 'russisch' in the 199 | * locale-specific format. 200 | */ 201 | public String getLanguage() { 202 | return this.language; 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Kay Lerch 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------