diff --git a/.env.example b/.env.example index c098c48..382e8a1 100644 --- a/.env.example +++ b/.env.example @@ -25,7 +25,5 @@ S3_REGION= S3_BUCKET= S3_ENDPOINT= -TRANSCRIPTION_ENABLED= - MINIO_ROOT_USER= MINIO_ROOT_PASSWORD= \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index c033179..5f8375f 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,4 +1,18 @@ name: uxcaptain + +networks: + uxcaptain-network: + name: uxcaptain-network + driver: bridge + +volumes: + uxcaptain-database: + name: uxcaptain-database + + whisper-transcription-models: + name: whisper-transcription-models + + services: # server: # container_name: server @@ -18,13 +32,12 @@ services: # STRIPE_API_KEY: ${STRIPE_API_KEY} # STRIPE_WEBHOOK_SECRET: ${STRIPE_WEBHOOK_SECRET} # PORT: ${PORT} - # AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID} - # AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY} # S3_BUCKET: dev-analysis-entry-storage # S3_REGION: ${S3_REGION} # S3_ENDPOINT: ${S3_ENDPOINT} # MINIO_ROOT_USER=${MINIO_ROOT_USER} # MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD} + # TRANSCRIPTION_ENDPOINT= ${TRANSCRIPTION_ENDPOINT} # ports: # - 3000:3000 @@ -40,6 +53,15 @@ services: # condition: service_started # networks: # - uxcaptain-network + + # deploy: + # resources: + # limits: + # cpus: '1.0' + # memory: 512M + # reservations: + # cpus: '0.5' + # memory: 256M minio: image: minio/minio:latest @@ -74,11 +96,30 @@ services: networks: - uxcaptain-network -networks: - uxcaptain-network: - name: uxcaptain-network - driver: bridge + faster-whisper-transcribe: + image: onerahmet/openai-whisper-asr-webservice:latest + container_name: faster-whisper-asr + ports: + - 9007:9000 + volumes: + - whisper-transcription-models:/root/.cache # Model persistence (~2-5GB) + environment: + - ASR_MODEL=medium # tiny,base,small,medium,large https://ahmetoner.com/whisper-asr-webservice/environmental-variables/#configuring-the-model + - ASR_DEVICE=cpu # https://ahmetoner.com/whisper-asr-webservice/environmental-variables/#configuring-device-and-quantization + - ASR_ENGINE=faster_whisper # openai_whisper, faster_whisper, whisperx -- https://ahmetoner.com/whisper-asr-webservice/environmental-variables/#whisperx + - MODEL_IDLE_TIMEOUT=0 # in Seconds - Keep model loaded - https://ahmetoner.com/whisper-asr-webservice/environmental-variables/#configuring-the-model-unloading-timeout + - ASR_QUANTIZATION=int8 # https://ahmetoner.com/whisper-asr-webservice/environmental-variables/#configuring-device-and-quantization + restart: unless-stopped + depends_on: + - minio + networks: + - uxcaptain-network # Same as MinIO/monolith + deploy: + resources: + limits: + cpus: '4.0' + memory: 5000M + + -volumes: - uxcaptain-database: diff --git a/package-lock.json b/package-lock.json index ab8f1da..7e1b230 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "Proprietary", "dependencies": { "@aws-sdk/client-s3": "^3.936.0", - "@aws-sdk/client-transcribe": "^3.948.0", "@aws-sdk/s3-request-presigner": "^3.936.0", "@getbrevo/brevo": "^3.0.1", "@prisma/client": "^6.19.0", @@ -25,6 +24,7 @@ "express-session": "^1.18.2", "express-slow-down": "^3.0.1", "express-validator": "^7.3.1", + "form-data": "^4.0.5", "helmet": "^8.1.0", "npm": "^11.6.3", "passport": "^0.7.0", @@ -365,412 +365,6 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/client-transcribe": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-transcribe/-/client-transcribe-3.948.0.tgz", - "integrity": "sha512-EOPYaW/lL2UHZbsG6PxPeHu/Pcw8MTsUznrRW6z7svVHCgsQkGUoWJs9gxTr601r+TMPgt8rdv2bv+WgXeN/SQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-node": "3.948.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/client-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.948.0.tgz", - "integrity": "sha512-iWjchXy8bIAVBUsKnbfKYXRwhLgRg3EqCQ5FTr3JbR+QR75rZm4ZOYXlvHGztVTmtAZ+PQVA1Y4zO7v7N87C0A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/core": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.947.0.tgz", - "integrity": "sha512-Khq4zHhuAkvCFuFbgcy3GrZTzfSX7ZIjIcW1zRDxXRLZKRtuhnZdonqTUfaWi5K42/4OmxkYNpsO7X7trQOeHw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws-sdk/xml-builder": "3.930.0", - "@smithy/core": "^3.18.7", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/signature-v4": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.947.0.tgz", - "integrity": "sha512-VR2V6dRELmzwAsCpK4GqxUi6UW5WNhAXS9F9AzWi5jvijwJo3nH92YNJUP4quMpgFZxJHEWyXLWgPjh9u0zYOA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.947.0.tgz", - "integrity": "sha512-inF09lh9SlHj63Vmr5d+LmwPXZc2IbK8lAruhOr3KLsZAIHEgHgGPXWDC2ukTEMzg0pkexQ6FOhXXad6klK4RA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/util-stream": "^4.5.6", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.948.0.tgz", - "integrity": "sha512-Cl//Qh88e8HBL7yYkJNpF5eq76IO6rq8GsatKcfVBm7RFVxCqYEPSSBtkHdbtNwQdRQqAMXc6E/lEB/CZUDxnA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-login": "3.948.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.948.0", - "@aws-sdk/credential-provider-web-identity": "3.948.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/credential-provider-login": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.948.0.tgz", - "integrity": "sha512-gcKO2b6eeTuZGp3Vvgr/9OxajMrD3W+FZ2FCyJox363ZgMoYJsyNid1vuZrEuAGkx0jvveLXfwiVS0UXyPkgtw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.948.0.tgz", - "integrity": "sha512-ep5vRLnrRdcsP17Ef31sNN4g8Nqk/4JBydcUJuFRbGuyQtrZZrVT81UeH2xhz6d0BK6ejafDB9+ZpBjXuWT5/Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "3.947.0", - "@aws-sdk/credential-provider-http": "3.947.0", - "@aws-sdk/credential-provider-ini": "3.948.0", - "@aws-sdk/credential-provider-process": "3.947.0", - "@aws-sdk/credential-provider-sso": "3.948.0", - "@aws-sdk/credential-provider-web-identity": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/credential-provider-imds": "^4.2.5", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.947.0.tgz", - "integrity": "sha512-WpanFbHe08SP1hAJNeDdBDVz9SGgMu/gc0XJ9u3uNpW99nKZjDpvPRAdW7WLA4K6essMjxWkguIGNOpij6Do2Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.948.0.tgz", - "integrity": "sha512-gqLhX1L+zb/ZDnnYbILQqJ46j735StfWV5PbDjxRzBKS7GzsiYoaf6MyHseEopmWrez5zl5l6aWzig7UpzSeQQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/client-sso": "3.948.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/token-providers": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.948.0.tgz", - "integrity": "sha512-MvYQlXVoJyfF3/SmnNzOVEtANRAiJIObEUYYyjTqKZTmcRIVVky0tPuG26XnB8LmTYgtESwJIZJj/Eyyc9WURQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.948.0.tgz", - "integrity": "sha512-Qa8Zj+EAqA0VlAVvxpRnpBpIWJI9KUwaioY1vkeNVwXPlNaz9y9zCKVM9iU9OZ5HXpoUg6TnhATAHXHAE8+QsQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "3.936.0", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.947.0.tgz", - "integrity": "sha512-7rpKV8YNgCP2R4F9RjWZFcD2R+SO/0R4VHIbY9iZJdH2MzzJ8ZG7h8dZ2m8QkQd1fjx4wrFJGGPJUTYXPV3baA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@smithy/core": "^3.18.7", - "@smithy/protocol-http": "^5.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/nested-clients": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.948.0.tgz", - "integrity": "sha512-zcbJfBsB6h254o3NuoEkf0+UY1GpE9ioiQdENWv7odo69s8iaGBEQ4BDpsIMqcuiiUXw1uKIVNxCB1gUGYz8lw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.947.0", - "@aws-sdk/middleware-host-header": "3.936.0", - "@aws-sdk/middleware-logger": "3.936.0", - "@aws-sdk/middleware-recursion-detection": "3.948.0", - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/region-config-resolver": "3.936.0", - "@aws-sdk/types": "3.936.0", - "@aws-sdk/util-endpoints": "3.936.0", - "@aws-sdk/util-user-agent-browser": "3.936.0", - "@aws-sdk/util-user-agent-node": "3.947.0", - "@smithy/config-resolver": "^4.4.3", - "@smithy/core": "^3.18.7", - "@smithy/fetch-http-handler": "^5.3.6", - "@smithy/hash-node": "^4.2.5", - "@smithy/invalid-dependency": "^4.2.5", - "@smithy/middleware-content-length": "^4.2.5", - "@smithy/middleware-endpoint": "^4.3.14", - "@smithy/middleware-retry": "^4.4.14", - "@smithy/middleware-serde": "^4.2.6", - "@smithy/middleware-stack": "^4.2.5", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/node-http-handler": "^4.4.5", - "@smithy/protocol-http": "^5.3.5", - "@smithy/smithy-client": "^4.9.10", - "@smithy/types": "^4.9.0", - "@smithy/url-parser": "^4.2.5", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.13", - "@smithy/util-defaults-mode-node": "^4.2.16", - "@smithy/util-endpoints": "^3.2.5", - "@smithy/util-middleware": "^4.2.5", - "@smithy/util-retry": "^4.2.5", - "@smithy/util-utf8": "^4.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/token-providers": { - "version": "3.948.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.948.0.tgz", - "integrity": "sha512-V487/kM4Teq5dcr1t5K6eoUKuqlGr9FRWL3MIMukMERJXHZvio6kox60FZ/YtciRHRI75u14YUqm2Dzddcu3+A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "3.947.0", - "@aws-sdk/nested-clients": "3.948.0", - "@aws-sdk/types": "3.936.0", - "@smithy/property-provider": "^4.2.5", - "@smithy/shared-ini-file-loader": "^4.4.0", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@aws-sdk/client-transcribe/node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.947.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.947.0.tgz", - "integrity": "sha512-+vhHoDrdbb+zerV4noQk1DHaUMNzWFWPpPYjVTwW2186k5BEJIecAMChYkghRrBVJ3KPWP1+JnZwOd72F3d4rQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/middleware-user-agent": "3.947.0", - "@aws-sdk/types": "3.936.0", - "@smithy/node-config-provider": "^4.3.5", - "@smithy/types": "^4.9.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - }, - "peerDependencies": { - "aws-crt": ">=1.0.0" - }, - "peerDependenciesMeta": { - "aws-crt": { - "optional": true - } - } - }, "node_modules/@aws-sdk/core": { "version": "3.940.0", "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.940.0.tgz", diff --git a/package.json b/package.json index b89d886..208ebcf 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,6 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.936.0", - "@aws-sdk/client-transcribe": "^3.948.0", "@aws-sdk/s3-request-presigner": "^3.936.0", "@getbrevo/brevo": "^3.0.1", "@prisma/client": "^6.19.0", @@ -35,6 +34,7 @@ "express-session": "^1.18.2", "express-slow-down": "^3.0.1", "express-validator": "^7.3.1", + "form-data": "^4.0.5", "helmet": "^8.1.0", "npm": "^11.6.3", "passport": "^0.7.0", diff --git a/server/controllers/analysisController.js b/server/controllers/analysisController.js index 3f21a91..49ec4e1 100644 --- a/server/controllers/analysisController.js +++ b/server/controllers/analysisController.js @@ -7,7 +7,7 @@ import { from '../models/analysisModel.js'; import { createAnalysisEntryInDb } from '../models/analysisEntryModel.js'; -import { generateS3PutPresignedUrl } from '../integrations/aws/s3.js'; +import { generateS3PutPresignedUrl } from '../integrations/s3-client/s3.js'; export const createAnalysis = async (req, res) => { if (req.sanitizedErrors) { diff --git a/server/controllers/analysisEntryController.js b/server/controllers/analysisEntryController.js index 3fa70a5..935b19e 100644 --- a/server/controllers/analysisEntryController.js +++ b/server/controllers/analysisEntryController.js @@ -1,6 +1,7 @@ -import { generateS3GetPresignedUrl } from '../integrations/aws/s3.js'; +import { logError, logInfo } from '../config/loggerFunctions.js'; +import { generateS3GetPresignedUrl } from '../integrations/s3-client/s3.js'; import { createAnalysisEntryInDb, getAnalysisEntryDetailsById, markAnalysisEntryAsSubmitted } from '../models/analysisEntryModel.js'; -import { processTranscriptionRequest } from '../services/analysisService.js'; +import { insertTranscriptionJobInDb } from '../models/transcriptionModel.js'; export const createAnalysisEntry = async (req, res) => { const { analysisId } = req.body; @@ -19,14 +20,17 @@ export const updateAnalysisEntry = async (req, res) => { const updatedAnalysisEntry = await markAnalysisEntryAsSubmitted(analysisEntryId); - const transcriptionRequest = { + const transcriptionJob = { analysisEntryId: analysisEntryId, analysisId: updatedAnalysisEntry.analysis_id, - languageCode: 'es-ES', + languageCode: 'es', }; - if (process.env.TRANSCRIPTION_ENABLED === 'true') { - processTranscriptionRequest(transcriptionRequest); // Fire-and-forget + try { + await insertTranscriptionJobInDb(transcriptionJob); + logInfo(`Transcription job for ${transcriptionJob.analysisEntryId} stored in DB`, transcriptionJob); + } catch (error) { + logError(`error inserting ${transcriptionJob.analysisEntryId} analysisEntry's transcription request`); } return res.status(200).json({ diff --git a/server/controllers/transcriptionController.js b/server/controllers/transcriptionController.js new file mode 100644 index 0000000..2a6330e --- /dev/null +++ b/server/controllers/transcriptionController.js @@ -0,0 +1,43 @@ +import { logError, logInfo } from '../config/loggerFunctions.js'; +import { transcribeRecording } from '../integrations/whisper-asr-webservice/transcribe.js'; +import { + getPendingTranscriptionJobsFromDb, + storeNormalizedTranscriptionInDb, + updateStatusSingleTranscriptionJobInDb, +} from '../models/transcriptionModel.js'; +import { cleanUpTranscriptSegments } from '../utils/transcription/transcriptionNormalizer.js'; + +export const processPendingTranscriptionJobs = async () => { + const pendingTranscriptionJobs = await getPendingTranscriptionJobsFromDb(); + + if (pendingTranscriptionJobs.length === 0) { + logInfo('No pending transcription jobs found'); + return; + } + + for (const transcriptionJob of pendingTranscriptionJobs) { + logInfo(`Processing transcription job for analysis entry ID: ${transcriptionJob.analysis_entry_id}`); + + const { analysis_entry_id: analysisEntryId } = transcriptionJob; + + try { + // Mark job as IN_PROGRESS before making async call to prevent re-queuing + await updateStatusSingleTranscriptionJobInDb(analysisEntryId, 'IN_PROGRESS'); + logInfo(`Marked transcription job ${analysisEntryId} as IN_PROGRESS`); + + const transcriptionJobResult = await transcribeRecording(transcriptionJob); + + const { segments, text: fullText } = transcriptionJobResult; + + const cleanedUpSegments = await cleanUpTranscriptSegments(segments); + + await storeNormalizedTranscriptionInDb(analysisEntryId, fullText, cleanedUpSegments); + + logInfo(`Transcription job ${analysisEntryId} completed successfully`); + } catch (error) { + // Mark job back as PENDING to allow retry + await updateStatusSingleTranscriptionJobInDb(analysisEntryId, 'IN_PROGRESS'); + logError(`Error processing transcription job, ${analysisEntryId}`, error); + } + } +}; diff --git a/server/cron/getCompletedTranscriptionJobsScheduler.js b/server/cron/getCompletedTranscriptionJobsScheduler.js deleted file mode 100644 index 36a2f44..0000000 --- a/server/cron/getCompletedTranscriptionJobsScheduler.js +++ /dev/null @@ -1,12 +0,0 @@ -import { CronJob } from 'cron'; -import { handleCompletedVideoTranscriptionJobs } from '../services/analysisService.js'; -import { logError, logInfo } from '../config/loggerFunctions.js'; - -export const getCompletedTranscriptionJobsScheduler = new CronJob('15 * * * *', async () => { - try { - logInfo('Checking transcription job status'); - await handleCompletedVideoTranscriptionJobs(); - } catch (error) { - logError('Error checking transcription job status', error); - } -}); diff --git a/server/cron/getPendingTranscriptionJobScheduler.js b/server/cron/getPendingTranscriptionJobScheduler.js new file mode 100644 index 0000000..23ddb09 --- /dev/null +++ b/server/cron/getPendingTranscriptionJobScheduler.js @@ -0,0 +1,11 @@ +import { CronJob } from 'cron'; +import { processPendingTranscriptionJobs } from '../controllers/transcriptionController.js'; +import { logError } from '../config/loggerFunctions.js'; + +export const getPendingTranscriptionJobScheduler = new CronJob('15 * * * *', async () => { + try { + await processPendingTranscriptionJobs(); + } catch (error) { + logError('Error processing transcription request', error); + } +}); diff --git a/server/cron/jobsContainer.js b/server/cron/jobsContainer.js index 83f9ac1..eda5b71 100644 --- a/server/cron/jobsContainer.js +++ b/server/cron/jobsContainer.js @@ -1,20 +1,16 @@ -import { logError } from '../config/loggerFunctions.js'; +import { logError, logInfo } from '../config/loggerFunctions.js'; import { deletePasswordResetTokensScheduler } from './deletePasswordResetTokensScheduler.js'; -import { getCompletedTranscriptionJobsScheduler } from './getCompletedTranscriptionJobsScheduler.js'; +import { getPendingTranscriptionJobScheduler } from './getPendingTranscriptionJobScheduler.js'; import { markAnalysisEntriesAsCancelledScheduler } from './markAsCancelledAnalysisEntriesScheduler.js'; export const startCronJobs = () => { + logInfo('Starting cron jobs'); try { deletePasswordResetTokensScheduler.start(); - if (process.env.TRANSCRIPTION_ENABLED === true) { - getCompletedTranscriptionJobsScheduler.start(); - } + getPendingTranscriptionJobScheduler.start(); markAnalysisEntriesAsCancelledScheduler.start(); - - - console.log('Cron jobs started'); } catch (error) { logError('error on startCronJobs', error); } diff --git a/server/integrations/aws/Transcribe.js b/server/integrations/aws/Transcribe.js deleted file mode 100644 index 569f3ee..0000000 --- a/server/integrations/aws/Transcribe.js +++ /dev/null @@ -1,56 +0,0 @@ -import { - TranscribeClient, - StartTranscriptionJobCommand, - ListTranscriptionJobsCommand, - DeleteTranscriptionJobCommand, -} from '@aws-sdk/client-transcribe'; -import { getS3Object } from './s3.js'; -import { logInfo } from '../../config/loggerFunctions.js'; - -const transcribeClient = new TranscribeClient({ region: process.env.S3_REGION }); - -export const requestAnalysisEntryTranscriptionToAWSTranscribe = async (transcriptionRequest) => { - const command = new StartTranscriptionJobCommand({ - TranscriptionJobName: transcriptionRequest.analysisEntryId, - LanguageCode: transcriptionRequest.languageCode, - Media: { - MediaFileUri: `s3://${process.env.S3_BUCKET}/analysis/${transcriptionRequest.analysisId}/${transcriptionRequest.analysisEntryId}/recording.mp4`, - }, - OutputBucketName: process.env.S3_BUCKET, - OutputKey: `analysis/${transcriptionRequest.analysisId}/${transcriptionRequest.analysisEntryId}/transcription.json`, - }); - - await transcribeClient.send(command); -}; - -export const listCompletedTranscriptionJobsFromAWS = async () => { - const command = new ListTranscriptionJobsCommand({ - Status: 'COMPLETED', - MaxResults: 10, // Ensure memory is not hogged - if more are available, they will be processed in the next iteration - }); - - const completedTranscriptionJobs = await transcribeClient.send(command); - const completedTranscriptionJobsSummary = completedTranscriptionJobs.TranscriptionJobSummaries; // returns an array - - return completedTranscriptionJobsSummary; -}; - -export const fetchSingleTranscriptionJob = async (analysisId, analysisEntryId) => { - const key = `analysis/${analysisId}/${analysisEntryId}/transcription.json`; - - const transcriptionJobResult = await getS3Object(key); - - return transcriptionJobResult; -}; - -export const deleteCompletedTranscriptionJobFromAWS = async (transcriptionJobName) => { - logInfo(`deleting ${transcriptionJobName} from AWS Transcribe`); - const command = new DeleteTranscriptionJobCommand({ - TranscriptionJobName: transcriptionJobName, - }); - - const deletedTranscriptionJobs = await transcribeClient.send(command); - // returns an array - - return deletedTranscriptionJobs; -}; diff --git a/server/integrations/aws/s3.js b/server/integrations/s3-client/s3.js similarity index 89% rename from server/integrations/aws/s3.js rename to server/integrations/s3-client/s3.js index 94c1168..042b1f2 100644 --- a/server/integrations/aws/s3.js +++ b/server/integrations/s3-client/s3.js @@ -44,8 +44,8 @@ export const getS3Object = async (key) => { const s3Object = await s3client.send(command); - // Read the response body as a stream and convert to string so it is workable - const responseBody = await s3Object.Body.transformToString(); + // Read the response body as a buffer for binary file handling + const responseBody = await s3Object.Body.transformToByteArray(); - return responseBody; + return Buffer.from(responseBody); }; diff --git a/server/integrations/whisper-asr-webservice/transcribe.js b/server/integrations/whisper-asr-webservice/transcribe.js new file mode 100644 index 0000000..ff2e09d --- /dev/null +++ b/server/integrations/whisper-asr-webservice/transcribe.js @@ -0,0 +1,35 @@ +import axios from 'axios'; +import FormData from 'form-data'; +import { getS3Object } from '../s3-client/s3.js'; + +export const transcribeRecording = async (transcriptionJob) => { + // need to use S3 because S3 client (minIO) stores data in a compressed format and cant be accesed via bind mount + + const key = `analysis/${transcriptionJob.AnalysisEntry.analysis_id}/${transcriptionJob.analysis_entry_id}/recording.mp4`; + + const fileBuffer = await getS3Object(key); + + // Create query parameters + const params = new URLSearchParams({ + task: 'transcribe', + output: 'json', + word_timestamps: 'false', // Works with video audio + language: 'es', + vad_filter: 'true', + }); + + // Create form data with the buffer directly + const form = new FormData(); + form.append('audio_file', fileBuffer, { + filename: 'recording.mp4', + contentType: 'video/mp4', + }); + + // Make request with query parameters + const response = await axios.post(`${process.env.TRANSCRIPTION_ENDPOINT}/asr?${params.toString()}`, form, { + headers: { ...form.getHeaders() }, + timeout: 1200000, // 20min + }); + + return response.data; +}; diff --git a/server/models/transcriptionModel.js b/server/models/transcriptionModel.js index e87cf76..ba52834 100644 --- a/server/models/transcriptionModel.js +++ b/server/models/transcriptionModel.js @@ -2,7 +2,7 @@ import { PrismaClient } from '../config/generated/prisma/client/index.js'; const prisma = new PrismaClient(); -export const insertTranscriptionRequestInDb = async (transcriptionRequest) => { +export const insertTranscriptionJobInDb = async (transcriptionRequest) => { await prisma.transcriptionJob.create({ data: { analysis_entry_id: transcriptionRequest.analysisEntryId, @@ -12,7 +12,7 @@ export const insertTranscriptionRequestInDb = async (transcriptionRequest) => { }); }; -export const updateSingleTranscriptionRequestInDb = async (analysisEntryId) => { +export const updateStatusSingleTranscriptionJobInDb = async (analysisEntryId, status) => { const whereClause = { analysis_entry_id: analysisEntryId, }; @@ -20,46 +20,48 @@ export const updateSingleTranscriptionRequestInDb = async (analysisEntryId) => { await prisma.transcriptionJob.update({ where: whereClause, data: { - status: 'IN_PROGRESS', + status: status, }, }); }; -export const getSingleTranscriptionJobDetailsFromDb = async (transcriptionJobName) => { +export const storeNormalizedTranscriptionInDb = async (analysisEntryId, fullText, normalizedSegments) => { const whereClause = { - analysis_entry_id: transcriptionJobName, + id: analysisEntryId, }; - const transcriptionJobDetailsResult = await prisma.transcriptionJob.findUnique({ + await prisma.analysisEntry.update({ where: whereClause, - select: { - status: true, - AnalysisEntry: { - select: { - analysis_id: true, + data: { + full_transcript: fullText, + transcription_segments: normalizedSegments, + transcriptionJob: { + update: { + status: 'COMPLETED', }, }, }, }); - - return transcriptionJobDetailsResult; }; -export const storeNormalizedTranscriptionInDb = async (transcriptionJobName, normalizedTranscriptionJob, transcriptionJobResult) => { +export const getPendingTranscriptionJobsFromDb = async () => { const whereClause = { - id: transcriptionJobName, + status: 'PENDING', }; - await prisma.analysisEntry.update({ + const pendingTranscriptionJobs = await prisma.transcriptionJob.findMany({ where: whereClause, - data: { - full_transcript: transcriptionJobResult.results.transcripts[0].transcript, - transcription_segments: normalizedTranscriptionJob.results.segments, - transcriptionJob: { - update: { - status: 'COMPLETED', + select: { + analysis_entry_id: true, + language_code: true, + AnalysisEntry: { + select: { + analysis_id: true, }, }, }, + take: 10, }); + + return pendingTranscriptionJobs; }; diff --git a/server/services/analysisService.js b/server/services/analysisService.js deleted file mode 100644 index 4ce2f68..0000000 --- a/server/services/analysisService.js +++ /dev/null @@ -1,90 +0,0 @@ -import { logError, logInfo } from '../config/loggerFunctions.js'; -import { - insertTranscriptionRequestInDb, - updateSingleTranscriptionRequestInDb, - getSingleTranscriptionJobDetailsFromDb, - storeNormalizedTranscriptionInDb, -} from '../models/transcriptionModel.js'; -import { - requestAnalysisEntryTranscriptionToAWSTranscribe, deleteCompletedTranscriptionJobFromAWS, - fetchSingleTranscriptionJob, - listCompletedTranscriptionJobsFromAWS, -} from '../integrations/aws/Transcribe.js'; -import { normalizeTranscript } from '../utils/transcription/transcriptionNormalizer.js'; - -export const processTranscriptionRequest = async (transcriptionRequest) => { - try { - await insertTranscriptionRequestInDb(transcriptionRequest); - logInfo('Transcription request stored in DB', transcriptionRequest); - - try { // Handle errors gracefully - errors will be picked up by a cron job if failed - await requestAnalysisEntryTranscriptionToAWSTranscribe(transcriptionRequest); - logInfo('Transcription request sent to AWS Transcribe', transcriptionRequest); - - await updateSingleTranscriptionRequestInDb(transcriptionRequest.analysisEntryId); - logInfo('Transcription request updated in DB', transcriptionRequest); - } catch (error) { - logError('Error requesting transcription to AWS Transcribe', error); - } - } catch (error) { - logError('Error storing transcription request in DB', error); - } -}; - -const processSingleCompletedTranscriptionJob = async (transcriptionJob) => { - logInfo(`Processing completed transcription job: ${transcriptionJob.TranscriptionJobName}`); - - try { - // 1. Get transcription job details from database - const transcriptionJobDetails = await getSingleTranscriptionJobDetailsFromDb(transcriptionJob.TranscriptionJobName); - - // Delete from AWS Transcribe if already processed - Shouldnt happen if AWS Transcribe job deletion is working properly - - if (transcriptionJobDetails.status === 'COMPLETED') { // Handle duplicate entries to avoid normalization reprocessing - logInfo(`Deleting already processed job: ${transcriptionJob.TranscriptionJobName}`); - await deleteCompletedTranscriptionJobFromAWS(transcriptionJob.TranscriptionJobName); - } - - // 2. Construct S3 key and fetch transcription file from AWS - const transcriptionJobResultString = await fetchSingleTranscriptionJob(transcriptionJobDetails.AnalysisEntry.analysis_id, transcriptionJob.TranscriptionJobName); - - // 3. Parse the transcription job result (JSON string to object) - const transcriptionJobResult = JSON.parse(transcriptionJobResultString); - - // 4. Normalize transcription job result - const normalizedTranscriptionJob = await normalizeTranscript(transcriptionJobResult); - - // 5. Store normalized transcript in DB and update status to COMPLETED - await storeNormalizedTranscriptionInDb(transcriptionJob.TranscriptionJobName, normalizedTranscriptionJob, transcriptionJobResult); - - try { - await deleteCompletedTranscriptionJobFromAWS(transcriptionJob.TranscriptionJobName); - } catch (error) { - logError(`Error deleting transcription job ${transcriptionJob.TranscriptionJobName} from AWS Transcribe`, error); - // AWS Transcribe deletion failing is not an issue since it will be caught by a CRON-based retry mechanism - } - - logInfo(`Successfully processed transcription job: ${transcriptionJob.TranscriptionJobName}`); - } catch (error) { - logError(`Error processing transcription job ${transcriptionJob.TranscriptionJobName}`, error); - } -}; - -export const handleCompletedVideoTranscriptionJobs = async () => { - try { - // Get the completed Jobs from AWS Transcribe - logInfo('Retrieving completed transcription jobs'); - const completedTranscriptionJobsSummary = await listCompletedTranscriptionJobsFromAWS(); - - if (completedTranscriptionJobsSummary.length === 0) { - logInfo('no completed transcription jobs available to process'); - return; - } - - for (const transcriptionJob of completedTranscriptionJobsSummary) { - await processSingleCompletedTranscriptionJob(transcriptionJob); - } - } catch (error) { - logError('Error processing transcription jobs', error); - } -}; diff --git a/server/utils/transcription/transcriptionNormalizer.js b/server/utils/transcription/transcriptionNormalizer.js index 500fd11..a6e4c8d 100644 --- a/server/utils/transcription/transcriptionNormalizer.js +++ b/server/utils/transcription/transcriptionNormalizer.js @@ -1,10 +1,3 @@ -import { logError } from '../../config/loggerFunctions.js'; - -/** - * Converts string numbers to actual numbers - * @param {string|number} value - Value to convert - * @returns {number} Converted number or 0 if invalid - */ const convertToNumber = (value) => { if (value === undefined || value === null) { return 0; @@ -14,170 +7,41 @@ const convertToNumber = (value) => { return Number.isNaN(num) ? 0 : num; }; -/** - * Groups transcription items into meaningful segments with reduced cluttering - * @param {Array} items - Array of transcription items from AWS Transcribe - * @returns {Array} Array of segments with start_time, end_time, and transcript - */ -const createSegmentsFromItems = (items) => { - if (!items || items.length === 0) { +const createSegmentsFromItems = (segments) => { + if (!segments || segments.length === 0) { return []; } - const segments = []; - let currentSegment = null; - const SEGMENT_GAP_THRESHOLD = 2.0; // seconds - gap for natural pauses to start new segment - const SEGMENT_MAX_DURATION = 25.0; // seconds - maximum duration to avoid overly long segments - - for (let i = 0; i < items.length; i += 1) { - const item = items[i]; - - // Skip items without alternatives - if (!item.alternatives || item.alternatives.length === 0) { - // Skip this iteration - } else { - const alternative = item.alternatives[0]; - const content = alternative.content || ''; - - // Handle items without timing information (like some punctuation) - if (!item.start_time || !item.end_time) { - if (currentSegment && item.type === 'punctuation') { - currentSegment.transcript += content; - } - } else { - const startTime = convertToNumber(item.start_time); - const endTime = convertToNumber(item.end_time); - - // Start a new segment or continue current segment - if (!currentSegment) { - // Start first segment - currentSegment = { - start_time: startTime, - end_time: endTime, - transcript: content, - }; - } else { - // Check if we should start a new segment based on time gap or max duration - const timeGap = startTime - currentSegment.end_time; - const segmentDuration = startTime - currentSegment.start_time; - - // Start new segment if there's a significant gap OR if we've reached max duration - if (timeGap > SEGMENT_GAP_THRESHOLD || segmentDuration >= SEGMENT_MAX_DURATION) { - // Significant gap or max duration reached - finalize current segment and start new one - segments.push(currentSegment); - currentSegment = { - start_time: startTime, - end_time: endTime, - transcript: content, - }; - } else { - // Continue current segment - // Improved logic for adding spaces around punctuation - const currentText = currentSegment.transcript; - const newContent = content.trim(); - - // Don't add space if current text is empty - if (currentText.length === 0) { - currentSegment.transcript += newContent; - } else { - // Get the last character of current text and first character of new content - const lastChar = currentText[currentText.length - 1]; - const firstChar = newContent[0]; - - // Determine if we need to add space - let shouldAddSpace = false; - - // Add space if: - // 1. Current text doesn't end with punctuation and new content doesn't start with punctuation - // 2. Current text ends with punctuation (except quotes/brackets) and new content starts with a letter/number - // 3. Current text ends with letter/number and new content starts with punctuation (.,!?;:) - if (!/[.,!?;:)\]}'"]$/.test(lastChar) && !/^[.,!?;:([{'"]/.test(firstChar)) { - // Neither ends nor starts with punctuation - add space - shouldAddSpace = true; - } else if (/[.,!?;:)]'?]*$/.test(lastChar) && /^[A-Za-zÁÉÍÓÚÑÜáéíóúñü0-9]/.test(firstChar)) { - // Ends with punctuation and starts with letter/number - add space - shouldAddSpace = true; - } else if (/[A-Za-zÁÉÍÓÚÑÜáéíóúñü0-9]$/.test(lastChar) && /^[.,!?;:]/.test(firstChar)) { - // Ends with letter/number and starts with punctuation - don't add space - shouldAddSpace = false; - } else if (/[)\]}'"]$/.test(lastChar) && /^[A-Za-zÁÉÍÓÚÑÜáéíóúñü0-9]/.test(firstChar)) { - // Ends with closing bracket/quote and starts with letter/number - add space - shouldAddSpace = true; - } - - // Special handling for common Spanish patterns - // Add space after periods followed by capital letters (sentence boundaries) - if (/[.] $/.test(lastChar) && /^[A-ZÁÉÍÓÚÑÜ]/.test(firstChar)) { - shouldAddSpace = true; - } + const cleanedSegments = []; - // Add space after commas, semicolons, and colons - if (/[,;:]$/.test(lastChar)) { - shouldAddSpace = true; - } + for (let i = 0; i < segments.length; i += 1) { + const item = segments[i]; - if (shouldAddSpace) { - currentSegment.transcript += ' '; - } + // Process items with required fields + if (item.text && item.start !== undefined && item.end !== undefined) { + // Clean up the segment by removing unnecessary fields and renaming timing fields + const segment = { + start_time: convertToNumber(item.start), + end_time: convertToNumber(item.end), + transcript: item.text.trim(), + }; - currentSegment.transcript += newContent; - } - currentSegment.end_time = endTime; - } - } - } + cleanedSegments.push(segment); } } - // Add the last segment if it exists - if (currentSegment) { - segments.push(currentSegment); - } - - return segments; + return cleanedSegments; }; -/** - * Normalizes AWS Transcribe output to the required format - * @param {object} transcript - AWS Transcribe output as parsed object - * @returns {Promise} Normalized transcription data - */ -export const normalizeTranscript = async (transcript) => { - try { - // Handle missing or malformed data - if (!transcript) { - return { - status: 'FAILED', - results: { - segments: [], - }, - }; - } - - // Extract status - const status = transcript.status || 'UNKNOWN'; - - // Extract results section - const results = transcript.results || {}; +export const cleanUpTranscriptSegments = async (segments) => { + // Handle missing or malformed data + if (!segments || !Array.isArray(segments)) { + throw new Error('Invalid segments data'); + } - // Extract items array and create segments - const items = results.items || []; - const segments = createSegmentsFromItems(items); + // Create segments from items + const cleanedSegments = createSegmentsFromItems(segments); - // Return normalized structure matching the required format - return { - status, - results: { - segments, - }, - }; - } catch (error) { - logError(`Error normalizing transcript: ${error.message}`, error); - return { - status: 'FAILED', - results: { - segments: [], - }, - }; - } + // Return normalized structure matching the required format + return cleanedSegments; };