diff --git a/.github/workflows/mediatranslation.yaml b/.github/workflows/mediatranslation.yaml new file mode 100644 index 0000000000..8fd636337c --- /dev/null +++ b/.github/workflows/mediatranslation.yaml @@ -0,0 +1,67 @@ +name: mediatranslation +on: + push: + branches: + - main + paths: + - 'mediatranslation/**' + pull_request: + paths: + - 'mediatranslation/**' + pull_request_target: + types: [labeled] + schedule: + - cron: '0 0 * * 0' +jobs: + test: + if: ${{ github.event.action != 'labeled' || github.event.label.name == 'actions:force-run' }} + runs-on: ubuntu-latest + timeout-minutes: 60 + permissions: + contents: 'write' + pull-requests: 'write' + id-token: 'write' + steps: + - uses: actions/checkout@v3.1.0 + with: + ref: ${{github.event.pull_request.head.ref}} + repository: ${{github.event.pull_request.head.repo.full_name}} + - uses: 'google-github-actions/auth@v0.8.3' + with: + workload_identity_provider: 'projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider' + service_account: 'kokoro-system-test@long-door-651.iam.gserviceaccount.com' + create_credentials_file: 'true' + access_token_lifetime: 600s + - uses: actions/setup-node@v3.5.1 + with: + node-version: 16 + - run: npm install + working-directory: mediatranslation + - run: npm test + working-directory: mediatranslation + env: + MOCHA_REPORTER_SUITENAME: mediatranslation + MOCHA_REPORTER_OUTPUT: mediatranslation_sponge_log.xml + MOCHA_REPORTER: xunit + - if: ${{ github.event.action == 'labeled' && github.event.label.name == 'actions:force-run' }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + try { + await github.rest.issues.removeLabel({ + name: 'actions:force-run', + owner: 'GoogleCloudPlatform', + repo: 'nodejs-docs-samples', + issue_number: context.payload.pull_request.number + }); + } catch (e) { + if (!e.message.includes('Label does not exist')) { + throw e; + } + } + - if: ${{ github.event_name == 'schedule'}} + run: | + curl https://github.com/googleapis/repo-automation-bots/releases/download/flakybot-1.1.0/flakybot -o flakybot -s -L + chmod +x ./flakybot + ./flakybot --repo GoogleCloudPlatform/nodejs-docs-samples --commit_hash ${{github.sha}} --build_url https://github.com/${{github.repository}}/actions/runs/${{github.run_id}} diff --git a/.github/workflows/workflows.json b/.github/workflows/workflows.json index 149b84bbb7..5fc1908212 100644 --- a/.github/workflows/workflows.json +++ b/.github/workflows/workflows.json @@ -40,6 +40,7 @@ "healthcare/dicom", "healthcare/fhir", "healthcare/hl7v2", + "mediatranslation", "monitoring/opencensus", "monitoring/prometheus", "datacatalog/cloud-client", diff --git a/mediatranslation/package.json b/mediatranslation/package.json new file mode 100644 index 0000000000..3ffacb174d --- /dev/null +++ b/mediatranslation/package.json @@ -0,0 +1,24 @@ +{ + "name": "mediatranslation-samples", + "private": true, + "license": "Apache-2.0", + "author": "Google LLC", + "engines": { + "node": ">=12.0.0" + }, + "files": [ + "*.js" + ], + "scripts": { + "test": "c8 mocha --timeout 600000 test/*.js" + }, + "dependencies": { + "@google-cloud/media-translation": "^3.0.4", + "node-record-lpcm16": "1.0.1" + }, + "devDependencies": { + "c8": "^7.0.0", + "chai": "^4.2.0", + "mocha": "^8.0.0" + } +} \ No newline at end of file diff --git a/mediatranslation/quickstart.js b/mediatranslation/quickstart.js new file mode 100644 index 0000000000..7e73b0f228 --- /dev/null +++ b/mediatranslation/quickstart.js @@ -0,0 +1,101 @@ +// Copyright 2020, Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +/** + * Translate text from an audio file. + * @param {string} filename local path to + * @param {string} encoding the encoding of the audio rate, e.g. Linear16 + * @param {string} sourceLanguage language translating from, as BCP-47 code + * @param {string} targetLanguage languate translating to, as BCP-47 code + */ +function main(filename, encoding, sourceLanguage, targetLanguage) { + // [START mediatranslation_quickstart] + const fs = require('fs'); + + // Imports the CLoud Media Translation client library + const { + SpeechTranslationServiceClient, + } = require('@google-cloud/media-translation'); + + // Creates a client + const client = new SpeechTranslationServiceClient(); + + async function quickstart() { + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const filename = 'Local path to audio file, e.g. /path/to/audio.raw'; + // const encoding = 'Encoding of the audio file, e.g. LINEAR16'; + // const sourceLanguage = 'BCP-47 source language code, e.g. en-US'; + // const targetLanguage = 'BCP-47 target language code, e.g. es-ES'; + + const config = { + audioConfig: { + audioEncoding: encoding, + sourceLanguageCode: sourceLanguage, + targetLanguageCode: targetLanguage, + }, + }; + + // First request needs to have only a streaming config, no data. + const initialRequest = { + streamingConfig: config, + audioContent: null, + }; + + const readStream = fs.createReadStream(filename, { + highWaterMark: 4096, + encoding: 'base64', + }); + + const chunks = []; + readStream + .on('data', chunk => { + const request = { + streamingConfig: config, + audioContent: chunk.toString(), + }; + chunks.push(request); + }) + .on('close', () => { + // Config-only request should be first in stream of requests + stream.write(initialRequest); + for (let i = 0; i < chunks.length; i++) { + stream.write(chunks[i]); + } + stream.end(); + }); + + const stream = client.streamingTranslateSpeech().on('data', response => { + const {result} = response; + if (result.textTranslationResult.isFinal) { + console.log( + `\nFinal translation: ${result.textTranslationResult.translation}` + ); + console.log(`Final recognition result: ${result.recognitionResult}`); + } else { + console.log( + `\nPartial translation: ${result.textTranslationResult.translation}` + ); + console.log(`Partial recognition result: ${result.recognitionResult}`); + } + }); + + // [END mediatranslation_quickstart] + } + quickstart(); +} + +main(...process.argv.slice(2)); diff --git a/mediatranslation/resources/audio.raw b/mediatranslation/resources/audio.raw new file mode 100644 index 0000000000..5ebf79d3c9 Binary files /dev/null and b/mediatranslation/resources/audio.raw differ diff --git a/mediatranslation/test/quickstart.test.js b/mediatranslation/test/quickstart.test.js new file mode 100644 index 0000000000..c031c44129 --- /dev/null +++ b/mediatranslation/test/quickstart.test.js @@ -0,0 +1,31 @@ +// Copyright 2017 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const path = require('path'); +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const execSync = require('child_process').execSync; +const cmd = 'node quickstart.js'; + +const filePath = path.join(__dirname, '..', 'resources/audio.raw'); +const exec = cmd => execSync(cmd, {encoding: 'utf-8'}); + +describe('Quickstart', () => { + it('should translate from a streamed file', async () => { + const stdout = exec(`${cmd} ${filePath} linear16 en-US es-ES`); + assert.include(stdout, 'Partial translation'); + }); +}); diff --git a/mediatranslation/test/translate_from_file.test.js b/mediatranslation/test/translate_from_file.test.js new file mode 100644 index 0000000000..193b3b6555 --- /dev/null +++ b/mediatranslation/test/translate_from_file.test.js @@ -0,0 +1,31 @@ +// Copyright 2017 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +const path = require('path'); +const {assert} = require('chai'); +const {describe, it} = require('mocha'); +const execSync = require('child_process').execSync; +const cmd = 'node translate_from_file.js'; + +const filePath = path.join(__dirname, '..', 'resources/audio.raw'); +const exec = cmd => execSync(cmd, {encoding: 'utf-8'}); + +describe('MediaTranslation', () => { + it('should translate from a streamed file', async () => { + const stdout = exec(`${cmd} ${filePath} linear16 en-US es-ES`); + assert.include(stdout, 'Partial translation'); + }); +}); diff --git a/mediatranslation/translate_from_file.js b/mediatranslation/translate_from_file.js new file mode 100644 index 0000000000..4fa6b718b1 --- /dev/null +++ b/mediatranslation/translate_from_file.js @@ -0,0 +1,102 @@ +// Copyright 2020, Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +/** + * Translate text from an audio file. + * @param {string} filename local path to + * @param {string} encoding the encoding of the audio rate, e.g. Linear16 + * @param {string} sourceLanguage language translating from, as BCP-47 code + * @param {string} targetLanguage languate translating to, as BCP-47 code + */ +function main(filename, encoding, sourceLanguage, targetLanguage) { + // [START mediatranslation_translate_from_file] + const fs = require('fs'); + + // Imports the CLoud Media Translation client library + const { + SpeechTranslationServiceClient, + } = require('@google-cloud/media-translation'); + + // Creates a client + const client = new SpeechTranslationServiceClient(); + + async function translate_from_file() { + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + // const filename = 'Local path to audio file, e.g. /path/to/audio.raw'; + // const encoding = 'Encoding of the audio file, e.g. LINEAR16'; + // const sourceLanguage = 'BCP-47 source language code, e.g. en-US'; + // const targetLanguage = 'BCP-47 target language code, e.g. es-ES'; + + const config = { + audioConfig: { + audioEncoding: encoding, + sourceLanguageCode: sourceLanguage, + targetLanguageCode: targetLanguage, + }, + single_utterance: true, + }; + + // First request needs to have only a streaming config, no data. + const initialRequest = { + streamingConfig: config, + audioContent: null, + }; + + const readStream = fs.createReadStream(filename, { + highWaterMark: 4096, + encoding: 'base64', + }); + + const chunks = []; + readStream + .on('data', chunk => { + const request = { + streamingConfig: config, + audioContent: chunk.toString(), + }; + chunks.push(request); + }) + .on('close', () => { + // Config-only request should be first in stream of requests + stream.write(initialRequest); + for (let i = 0; i < chunks.length; i++) { + stream.write(chunks[i]); + } + stream.end(); + }); + + const stream = client.streamingTranslateSpeech().on('data', response => { + const {result} = response; + if (result.textTranslationResult.isFinal) { + console.log( + `\nFinal translation: ${result.textTranslationResult.translation}` + ); + console.log(`Final recognition result: ${result.recognitionResult}`); + } else { + console.log( + `\nPartial translation: ${result.textTranslationResult.translation}` + ); + console.log(`Partial recognition result: ${result.recognitionResult}`); + } + }); + + // [END mediatranslation_translate_from_file] + } + translate_from_file(); +} + +main(...process.argv.slice(2)); diff --git a/mediatranslation/translate_from_mic.js b/mediatranslation/translate_from_mic.js new file mode 100644 index 0000000000..37d18f578e --- /dev/null +++ b/mediatranslation/translate_from_mic.js @@ -0,0 +1,155 @@ +// Copyright 2020, Google LLC. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +'use strict'; + +/** + * This application demonstrates how to perform basic translation operations + * with the Google Cloud Media Translation API. + * + * Note: Correct microphone settings is required: check enclosed link, and make + * sure the following conditions are met: + * 1. SoX must be installed and available in your $PATH- it can be found here: + * http://sox.sourceforge.net/ + * 2. Microphone must be working + * 3. Encoding, sampleRateHertz, and # of channels must match header of audio file you're + * recording to. + * 4. Get Node-Record-lpcm16 https://www.npmjs.com/package/node-record-lpcm16 + * More Info: https://cloud.google.com/speech-to-text/docs/streaming-recognize + */ + +/** + * Translates audio streaming from a microphone + * @param {string} encoding the audio encoding codec + * @param {string} sampleRateHertz the sampling rate of the audio stream + * @param {string} sourceLanguageCode the language to translate from + * @param {string} targetLanguageCode the language to translate to + */ +function main(encoding, sampleRateHertz, sourceLanguage, targetLanguage) { + sampleRateHertz = Number(sampleRateHertz); + + // [START mediatranslation_translate_from_mic] + + // Allow user input from terminal + const readline = require('readline'); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); + + function doTranslationLoop() { + rl.question("Press any key to translate or 'q' to quit: ", answer => { + if (answer.toLowerCase() === 'q') { + rl.close(); + } else { + translateFromMicrophone(); + } + }); + } + + // Node-Record-lpcm16 + const recorder = require('node-record-lpcm16'); + + // Imports the Cloud Media Translation client library + const { + SpeechTranslationServiceClient, + } = require('@google-cloud/media-translation'); + + // Creates a client + const client = new SpeechTranslationServiceClient(); + + function translateFromMicrophone() { + /** + * TODO(developer): Uncomment the following lines before running the sample. + */ + //const encoding = 'linear16'; + //const sampleRateHertz = 16000; + //const sourceLanguage = 'Language to translate from, as BCP-47 locale'; + //const targetLanguage = 'Language to translate to, as BCP-47 locale'; + console.log('Begin speaking ...'); + + const config = { + audioConfig: { + audioEncoding: encoding, + sourceLanguageCode: sourceLanguage, + targetLanguageCode: targetLanguage, + }, + singleUtterance: true, + }; + + // First request needs to have only a streaming config, no data. + const initialRequest = { + streamingConfig: config, + audioContent: null, + }; + + let currentTranslation = ''; + let currentRecognition = ''; + // Create a recognize stream + const stream = client + .streamingTranslateSpeech() + .on('error', e => { + if (e.code && e.code === 4) { + console.log('Streaming translation reached its deadline.'); + } else { + console.log(e); + } + }) + .on('data', response => { + const {result, speechEventType} = response; + if (speechEventType === 'END_OF_SINGLE_UTTERANCE') { + console.log(`\nFinal translation: ${currentTranslation}`); + console.log(`Final recognition result: ${currentRecognition}`); + + stream.destroy(); + recording.stop(); + } else { + currentTranslation = result.textTranslationResult.translation; + currentRecognition = result.recognitionResult; + console.log(`\nPartial translation: ${currentTranslation}`); + console.log(`Partial recognition result: ${currentRecognition}`); + } + }); + + let isFirst = true; + // Start recording and send microphone input to the Media Translation API + const recording = recorder.record({ + sampleRateHertz: sampleRateHertz, + threshold: 0, //silence threshold + recordProgram: 'rec', + silence: '5.0', //seconds of silence before ending + }); + recording + .stream() + .on('data', chunk => { + if (isFirst) { + stream.write(initialRequest); + isFirst = false; + } + const request = { + streamingConfig: config, + audioContent: chunk.toString('base64'), + }; + if (!stream.destroyed) { + stream.write(request); + } + }) + .on('close', () => { + doTranslationLoop(); + }); + } + + doTranslationLoop(); + // [END mediatranslation_translate_from_mic] +} +main(...process.argv.slice(2));