diff --git a/package-lock.json b/package-lock.json index 167ccdd8..1be75051 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4524,6 +4524,299 @@ "fxparser": "src/cli/cli.js" } }, + "node_modules/@aws-sdk/client-polly": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-polly/-/client-polly-3.629.0.tgz", + "integrity": "sha512-x/RUDuXExCnMy+ATCJIOLPLBebWPSVH/BGdevW2l+NEZMOcDj//ztrxNNBgDoo2t2w+ZX2PtyTStOJ79Ewg5Bw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.629.0", + "@aws-sdk/client-sts": "3.629.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.629.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-stream": "^3.1.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/client-sso": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.629.0.tgz", + "integrity": "sha512-2w8xU4O0Grca5HmT2dXZ5fF0g39RxODtmoqHJDsK5DSt750LqDG4w3ktmBvQs3+SrpkkJOjlX5v/hb2PCxVbww==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/client-sso-oidc": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.629.0.tgz", + "integrity": "sha512-3if0LauNJPqubGYf8vnlkp+B3yAeKRuRNxfNbHlE6l510xWGcKK/ZsEmiFmfePzKKSRrDh/cxMFMScgOrXptNg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.629.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.629.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/client-sts": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.629.0.tgz", + "integrity": "sha512-RjOs371YwnSVGxhPjuluJKaxl4gcPYTAky0nPjwBime0i9/iS9nI8R8l5j7k7ec9tpFWjBPvNnThCU07pvjdzw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/client-sso-oidc": "3.629.0", + "@aws-sdk/core": "3.629.0", + "@aws-sdk/credential-provider-node": "3.629.0", + "@aws-sdk/middleware-host-header": "3.620.0", + "@aws-sdk/middleware-logger": "3.609.0", + "@aws-sdk/middleware-recursion-detection": "3.620.0", + "@aws-sdk/middleware-user-agent": "3.620.0", + "@aws-sdk/region-config-resolver": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@aws-sdk/util-endpoints": "3.614.0", + "@aws-sdk/util-user-agent-browser": "3.609.0", + "@aws-sdk/util-user-agent-node": "3.614.0", + "@smithy/config-resolver": "^3.0.5", + "@smithy/core": "^2.3.2", + "@smithy/fetch-http-handler": "^3.2.4", + "@smithy/hash-node": "^3.0.3", + "@smithy/invalid-dependency": "^3.0.3", + "@smithy/middleware-content-length": "^3.0.5", + "@smithy/middleware-endpoint": "^3.1.0", + "@smithy/middleware-retry": "^3.0.14", + "@smithy/middleware-serde": "^3.0.3", + "@smithy/middleware-stack": "^3.0.3", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/node-http-handler": "^3.1.4", + "@smithy/protocol-http": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/url-parser": "^3.0.3", + "@smithy/util-base64": "^3.0.0", + "@smithy/util-body-length-browser": "^3.0.0", + "@smithy/util-body-length-node": "^3.0.0", + "@smithy/util-defaults-mode-browser": "^3.0.14", + "@smithy/util-defaults-mode-node": "^3.0.14", + "@smithy/util-endpoints": "^2.0.5", + "@smithy/util-middleware": "^3.0.3", + "@smithy/util-retry": "^3.0.3", + "@smithy/util-utf8": "^3.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/core": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.629.0.tgz", + "integrity": "sha512-+/ShPU/tyIBM3oY1cnjgNA/tFyHtlWq+wXF9xEKRv19NOpYbWQ+xzNwVjGq8vR07cCRqy/sDQLWPhxjtuV/FiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^2.3.2", + "@smithy/node-config-provider": "^3.1.4", + "@smithy/property-provider": "^3.1.3", + "@smithy/protocol-http": "^4.1.0", + "@smithy/signature-v4": "^4.1.0", + "@smithy/smithy-client": "^3.1.12", + "@smithy/types": "^3.3.0", + "@smithy/util-middleware": "^3.0.3", + "fast-xml-parser": "4.4.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.629.0.tgz", + "integrity": "sha512-r9fI7BABARvVDp77DBUImQzYdvarAIdhbvpCEZib0rlpvfWu3zxE9KZcapCAAi0MPjxeDfb7RMehFQIkAP7mYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.629.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-sts": "^3.629.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.629.0.tgz", + "integrity": "sha512-868hnVOLlXOBHk91Rl0jZIRgr/M4WJCa0nOrW9A9yidsQxuZp9P0vshDmm4hMvNZadmPIfo0Rra2MpA4RELoCw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.620.1", + "@aws-sdk/credential-provider-http": "3.622.0", + "@aws-sdk/credential-provider-ini": "3.629.0", + "@aws-sdk/credential-provider-process": "3.620.1", + "@aws-sdk/credential-provider-sso": "3.629.0", + "@aws-sdk/credential-provider-web-identity": "3.621.0", + "@aws-sdk/types": "3.609.0", + "@smithy/credential-provider-imds": "^3.2.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-sdk/client-polly/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.629.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.629.0.tgz", + "integrity": "sha512-Lf4XOuj6jamxgGZGrVojERh5S+NS2t2S4CUOnAu6tJ5U0GPlpjhINUKlcVxJBpsIXudMGW1nkumAd3+kazCPig==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.629.0", + "@aws-sdk/token-providers": "3.614.0", + "@aws-sdk/types": "3.609.0", + "@smithy/property-provider": "^3.1.3", + "@smithy/shared-ini-file-loader": "^3.1.4", + "@smithy/types": "^3.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@aws-sdk/client-s3": { "version": "3.622.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.622.0.tgz", @@ -11149,6 +11442,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, "license": "ISC", "engines": { "node": ">= 4.0.0" @@ -11732,6 +12026,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -12384,6 +12679,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, "license": "MIT" }, "node_modules/console-browserify": { @@ -14206,6 +14502,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, "license": "MIT", "dependencies": { "at-least-node": "^1.0.0", @@ -14829,6 +15126,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 4" @@ -16862,6 +17160,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -17971,6 +18270,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -18967,6 +19267,7 @@ "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver" @@ -21633,6 +21934,7 @@ } }, "packages/types": { + "name": "@types/generative-ai-use-cases-jp", "dependencies": { "@aws-sdk/client-bedrock-agent-runtime": "^3.549.0", "@aws-sdk/client-kendra": "^3.549.0" @@ -21644,6 +21946,7 @@ "@aws-sdk/client-cognito-identity": "^3.549.0", "@aws-sdk/client-kendra": "^3.549.0", "@aws-sdk/client-lambda": "^3.549.0", + "@aws-sdk/client-polly": "^3.549.0", "@aws-sdk/client-transcribe": "^3.549.0", "@aws-sdk/client-transcribe-streaming": "^3.549.0", "@aws-sdk/credential-provider-cognito-identity": "^3.549.0", diff --git a/packages/cdk/lib/construct/auth.ts b/packages/cdk/lib/construct/auth.ts index 7faf5d4c..abc0526a 100644 --- a/packages/cdk/lib/construct/auth.ts +++ b/packages/cdk/lib/construct/auth.ts @@ -89,6 +89,18 @@ export class Auth extends Construct { ); } + idPool.authenticatedRole.attachInlinePolicy( + new Policy(this, 'PollyPolicy', { + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + resources: ['*'], + actions: ['polly:SynthesizeSpeech'], + }), + ], + }) + ); + // Lambda if (props.allowedSignUpEmailDomains) { const checkEmailDomainFunction = new NodejsFunction( diff --git a/packages/web/package.json b/packages/web/package.json index 0933b90e..de50c8ab 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -13,6 +13,7 @@ "@aws-sdk/client-cognito-identity": "^3.549.0", "@aws-sdk/client-kendra": "^3.549.0", "@aws-sdk/client-lambda": "^3.549.0", + "@aws-sdk/client-polly": "^3.549.0", "@aws-sdk/client-transcribe": "^3.549.0", "@aws-sdk/client-transcribe-streaming": "^3.549.0", "@aws-sdk/credential-provider-cognito-identity": "^3.549.0", diff --git a/packages/web/src/hooks/useSpeach.ts b/packages/web/src/hooks/useSpeach.ts new file mode 100644 index 00000000..8003ed0f --- /dev/null +++ b/packages/web/src/hooks/useSpeach.ts @@ -0,0 +1,58 @@ +import { useState } from 'react'; +import { fetchAuthSession } from 'aws-amplify/auth'; +import { fromCognitoIdentityPool } from '@aws-sdk/credential-provider-cognito-identity'; +import { CognitoIdentityClient } from '@aws-sdk/client-cognito-identity'; +import { Polly, SynthesizeSpeechCommand } from '@aws-sdk/client-polly'; + +const useSpeach = () => { + const [loading, setLoading] = useState(false); + + return { + loading, + synthesizeSpeach: async (text: string): Promise => { + setLoading(true); + + const token = (await fetchAuthSession()).tokens?.idToken?.toString(); + + if (!token) { + setLoading(false); + throw new Error('認証されていません。'); + } + + const region = import.meta.env.VITE_APP_REGION; + const userPoolId = import.meta.env.VITE_APP_USER_POOL_ID; + const idPoolId = import.meta.env.VITE_APP_IDENTITY_POOL_ID; + const cognito = new CognitoIdentityClient({ region }); + const providerName = `cognito-idp.${region}.amazonaws.com/${userPoolId}`; + + const polly = new Polly({ + region, + credentials: fromCognitoIdentityPool({ + client: cognito, + identityPoolId: idPoolId, + logins: { + [providerName]: token, + }, + }), + }); + + const command = new SynthesizeSpeechCommand({ + Text: text, + OutputFormat: 'mp3', + VoiceId: 'Joanna', // TODO: 多言語対応 + }); + + const response = await polly.send(command); + const audioStream = response.AudioStream!.transformToWebStream(); + + const audioBlob = await new Response(audioStream).blob(); + const audioUrl = URL.createObjectURL(audioBlob); + + setLoading(false); + + return audioUrl; + }, + }; +}; + +export default useSpeach; diff --git a/packages/web/src/pages/TranslatePage.tsx b/packages/web/src/pages/TranslatePage.tsx index 2935f78c..1aa1b230 100644 --- a/packages/web/src/pages/TranslatePage.tsx +++ b/packages/web/src/pages/TranslatePage.tsx @@ -1,7 +1,14 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useState, + useRef, +} from 'react'; import { useLocation } from 'react-router-dom'; import Card from '../components/Card'; import Button from '../components/Button'; +import ButtonIcon from '../components/ButtonIcon'; import Textarea from '../components/Textarea'; import ExpandableField from '../components/ExpandableField'; import Select from '../components/Select'; @@ -12,13 +19,19 @@ import useChat from '../hooks/useChat'; import useMicrophone from '../hooks/useMicrophone'; import useTyping from '../hooks/useTyping'; import useLocalStorageBoolean from '../hooks/useLocalStorageBoolean'; -import { PiMicrophoneBold, PiStopCircleBold } from 'react-icons/pi'; +import { + PiMicrophoneBold, + PiStopCircleBold, + PiSpeakerSimpleHigh, + PiSpeakerSimpleHighFill, +} from 'react-icons/pi'; import { create } from 'zustand'; import debounce from 'lodash.debounce'; import { TranslatePageQueryParams } from '../@types/navigate'; import { MODELS } from '../hooks/useModel'; import { getPrompter } from '../prompts'; import queryString from 'query-string'; +import useSpeach from '../hooks/useSpeach'; const languages = [ '英語', @@ -118,6 +131,7 @@ const TranslatePage: React.FC = () => { const stopReason = getStopReason(); const [auto, setAuto] = useLocalStorageBoolean('Auto_Translate', true); const [audio, setAudioInput] = useState(false); // 音声入力フラグ + const { synthesizeSpeach, loading: speachIsLoading } = useSpeach(); useEffect(() => { updateSystemContextByModel(); @@ -262,6 +276,44 @@ const TranslatePage: React.FC = () => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const audioRef = useRef(null); + const [isSpeachPlaying, setIsSpeachPlaying] = useState(false); + + const handleSpeachEnded = useCallback(() => { + setIsSpeachPlaying(false); + }, [setIsSpeachPlaying]); + + const startOrStopSpeach = useCallback(async () => { + if (speachIsLoading) return; + + // 再生中の場合は止める + if (isSpeachPlaying && audioRef.current) { + setIsSpeachPlaying(false); + + audioRef.current.pause(); + audioRef.current.currentTime = 0; + audioRef.current = null; + return; + } + + setIsSpeachPlaying(true); + + const speachUrl = await synthesizeSpeach(translatedSentence); + const audio = new Audio(speachUrl!); + + audioRef.current = audio; + audio.addEventListener('ended', handleSpeachEnded); + audio.play(); + }, [ + translatedSentence, + synthesizeSpeach, + audioRef, + setIsSpeachPlaying, + isSpeachPlaying, + handleSpeachEnded, + speachIsLoading, + ]); + return (
@@ -283,7 +335,7 @@ const TranslatePage: React.FC = () => {
-
+
言語を自動検出
{audio && ( @@ -310,50 +362,67 @@ const TranslatePage: React.FC = () => { value={sentence} onChange={setSentence} maxHeight={-1} + rows={5} /> + + +