before changes

This commit is contained in:
2026-10-05 16:14:53 -04:00
parent dd6e6f7cfd
commit f39555ce15
94 changed files with 3285321 additions and 1 deletions

View File

@@ -0,0 +1,332 @@
const fs = require('fs')
const https = require('https')
const exec = require('child_process').exec
const AWS = require('aws-sdk')
const Bugsnag = require('@bugsnag/js')
const mongoose = require('mongoose')
const version = require('./package.json').version
const sqs = require('../app/services/sqs')
const s3 = require('../app/services/s3')
const voiceCloningService = require('./voice_cloning')
const userAudioProfileService = require('./user_audio_profile')
AWS.config.update({ region: 'us-west-2' })
const sqsQueueUrl = process.env.SQS_URL
const mongoUriDev = process.env.MONGODB_URI_DEV
const mongoUriStaging = process.env.MONGODB_URI_STAGING
const mongoUriProd = process.env.MONGODB_URI_PROD
let throttleMessageFetching = true
const APP_ENV = process.env.POTION_APP_ENV
const cloudFrontUrlProd = process.env.CLOUDFRONT_URL_PROD
const cloudFrontUrlDev = process.env.CLOUDFRONT_URL_DEV
const cloudFrontUrlStaging = process.env.CLOUDFRONT_URL_STAGING
const updateUrl = (str, cloudFrontUrl) => {
const host = new URL(str).host
return str.replace(`https://${host}`, cloudFrontUrl)
}
function connectDB(dbUri, retryCount = 0) {
return new Promise((resolve, reject) => {
console.log('Connection Attempt : ', retryCount)
mongoose.set('strictQuery', true)
mongoose
.connect(dbUri)
.then((msg) => {
console.log('Connected to Mongo DB !')
resolve()
})
.catch((err) => {
console.log('Failed to connect dns mongo: ', err)
if (retryCount < 6) {
retryCount++
connectDB(dbUri, retryCount)
}
})
})
}
function execShellCommand(cmd, logPath) {
// const exec = require("child_process").exec;
return new Promise((resolve, reject) => {
exec(cmd, { maxBuffer: 1024 * 1000000 }, async (error, stdout, stderr) => {
if (error) {
console.log('Error while proccessing python command', error)
reject(error)
}
// console.log('Stdout --- ', stdout)
// console.log('Stderror --- ', stderr)
await fs.promises.writeFile(`${logPath}/error.log`, stderr)
await fs.promises.writeFile(`${logPath}/info.log`, stdout)
resolve()
})
})
}
async function getFile(waveUrl, path) {
return new Promise((resolve) => {
https.get(waveUrl, (res) => {
const writeStream = fs.createWriteStream(path)
res.pipe(writeStream)
writeStream.on('finish', () => {
writeStream.close()
resolve()
})
})
})
}
function pad(s) {
while (s.length < 3) s = '0' + s // IN future we will need padding to 4
return s
}
const processQueue = () => {
/* eslint-disable no-async-promise-executor */
return new Promise(async (resolve, reject) => {
try {
const response = await sqs.fetchMessageFromSQS(sqsQueueUrl)
if (
typeof response.Messages !== 'undefined' &&
response.Messages.length > 0
) {
throttleMessageFetching = false
const job = JSON.parse(response.Messages[0].Body)
const receiptHandle = response.Messages[0].ReceiptHandle
console.log('job===', job)
const { metadata, input, _id, userAudioProfileId } = job._doc
console.log('userAudioProfileId', userAudioProfileId)
console.log('_id', _id)
const { env } = job
console.log('env', env)
console.log('metadata------', metadata)
console.log('input', input)
const DB_URI =
env === 'production'
? mongoUriProd
: env === 'staging'
? mongoUriStaging
: mongoUriDev
console.log('DB_URI ', DB_URI)
await connectDB(DB_URI)
const cloudFrontUrl =
env === 'production'
? cloudFrontUrlProd
: env === 'staging'
? cloudFrontUrlStaging
: cloudFrontUrlDev
try {
await sqs.deleteMessageFromSQS(sqsQueueUrl, receiptHandle)
const { directoryName } = metadata
console.log('directoryName', directoryName)
const logPath = `/mnt/efs/potion-voice/${env}/${directoryName}`
if (!fs.existsSync(logPath)) {
fs.mkdirSync(logPath, { recursive: true })
}
// update the db model to processing
await voiceCloningService.update({ _id, status: 'processing' })
await userAudioProfileService.update({
_id: userAudioProfileId,
status: 'processing',
})
// create directory for userid-useraudioprofileid if not exist
const rootPath = `/tmp/${directoryName}`
const wavePath = `${rootPath}/wav48/1`
if (!fs.existsSync(wavePath)) {
fs.mkdirSync(wavePath, { recursive: true })
}
const txtPath = `${rootPath}/txt/1`
if (!fs.existsSync(txtPath)) {
fs.mkdirSync(txtPath, { recursive: true })
}
// download the training data files and put it in respective directories
for (let index = 0; index < input.length; index++) {
const item = input[index]
const { waveUrl, originalText } = item
// download wave file
const waveFilePath = `${wavePath}/1_${pad('' + (index + 1))}.wav`
await getFile(updateUrl(waveUrl, cloudFrontUrl), waveFilePath)
const txtFilePath = `${txtPath}/1_${pad('' + (index + 1))}.txt`
await fs.promises.writeFile(txtFilePath, originalText)
}
const zipFileName = directoryName + '.tgz'
// /tmp/directoryName.tgz
await execShellCommand(
`cd /tmp && tar czvf ${zipFileName} ${directoryName}`,
logPath
)
console.log('ZIP created ', zipFileName)
// re-sample audio
const SAMPLING_LABEL = `Time Taken for re-sampling ${directoryName}`
console.time(SAMPLING_LABEL)
const outputPath = `/mnt/efs/potion-voice/${env}/${directoryName}`
const samplingCommand = `python3 ../voice-cloning/prepare_datasets.py --dataset_preset potion_voice_cloning --dataset_archive_path /tmp/${zipFileName} --output_path ${outputPath}`
console.log('samplingCommand ', samplingCommand)
const samplingResponse = await execShellCommand(
samplingCommand,
logPath
)
console.timeEnd(SAMPLING_LABEL)
// /mnt/efs/potion-voice/${env}/speakrs.pth
// /mnt/efs/potion-voice/${env}/txt
// /mnt/efs/potion-voice/${env}/${directoryName}/wav
const outPath = `/mnt/efs/potion-voice/${env}/${directoryName}/sr22050/${directoryName}`
const resultsPath = outPath + '/results'
//update pth file for cloning
// clone the voice
const VOICE_CLONING_LABEL = `Time Taken for voice cloning ${directoryName}`
console.time(VOICE_CLONING_LABEL)
const trainingModelCommand = `python3 ../voice-cloning/clone_voice.py --baseline_model_path ../voice-cloning/pretrained-models/checkpoint_365000.pth --speaker_dataset_path ${outPath} --speaker_embeddings_path ${
outPath + '/speakers.pth'
} --output_path ${resultsPath}`
console.log('Training Model Command', trainingModelCommand)
const trainingResponse = await execShellCommand(
trainingModelCommand,
logPath
)
console.timeEnd(VOICE_CLONING_LABEL)
let generatedDirectoryName = ''
fs.readdirSync(`${resultsPath}/`).forEach((file) => {
if (file.includes('vits_potion_clone'))
// use output from above to get right path and directory name
generatedDirectoryName = file
})
// minimize cloning model
const VOICE_MINIMIZE_LABEL = `Time Taken for voice minimizing cloning ${directoryName}`
console.time(VOICE_MINIMIZE_LABEL)
const minimizeCloningModelCommand = `python3 ../voice-cloning/minimize_cloned_voice_model.py --voice_model_asset_path ${
resultsPath + '/' + generatedDirectoryName + '/'
} --voice_model_name checkpoint_365200.pth`
console.log(
'Minimize Cloning Model Command',
minimizeCloningModelCommand
)
const minimizeCloning = await execShellCommand(
minimizeCloningModelCommand,
logPath
)
console.timeEnd(VOICE_MINIMIZE_LABEL)
// Add the code to update location of generated model and status into DB
await voiceCloningService.update({ _id, status: 'completed' })
const training_model_path = {
voice_model_path: `${resultsPath}/${generatedDirectoryName}/checkpoint_365200.pth`,
voice_model_config_path: `${resultsPath}/${generatedDirectoryName}/config.json`,
voice_model_speakers_file_path: `${outPath}/speakers.pth`, // TODO update the name to voice model speakers embeddings
voice_model_light_path: `${resultsPath}/${generatedDirectoryName}/checkpoint_365200_light.pth`,
voice_model_config_light_path: `${resultsPath}/${generatedDirectoryName}/config_light.json`,
}
await userAudioProfileService.update({
_id: userAudioProfileId,
status: 'completed',
training_model_path,
})
// add code to put that model into S3
let keys = Object.keys(training_model_path)
const training_model_s3_path = {}
for (let index = 0; index < keys.length; index++) {
const path = training_model_path[keys[index]]
const s3Path = await s3.upload({
filePath: path,
fileName: `${directoryName}/${path.split('/').pop()}`,
bucket: `potion-voice-users-training-model/${env}`,
})
training_model_s3_path[keys[index]] = s3Path
}
// add S3 path to user audio profile model
await userAudioProfileService.update({
_id: userAudioProfileId,
training_model_s3_path,
})
} catch (error) {
console.log('error********************', error)
Bugsnag.notify(
new Error(
`Unable to train for voice cloning videos ` + JSON.stringify(job)
)
)
Bugsnag.notify(error)
// update the db to set status as error
await voiceCloningService.update({ _id, status: 'error' })
await userAudioProfileService.update({
_id: userAudioProfileId,
status: 'error',
})
resolve() // to continue working on new jobs
}
} else {
throttleMessageFetching = true
}
resolve()
} catch (error) {
console.error('Error while training voice clone', { error })
Bugsnag.notify(error)
resolve() // to continue working on new jobs
} finally {
mongoose.connection.close()
}
})
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms)
})
}
const init = async () => {
console.log('potion Voice Clone Process Started')
Bugsnag.start({
appVersion: APP_ENV + version,
apiKey: process.env.BUGSNAG_BACKEND_KEY,
releaseStage: process.env.NODE_ENV,
})
try {
while (true) {
await processQueue()
if (throttleMessageFetching) await sleep(2000)
}
} catch (error) {
Bugsnag.notify(error)
}
}
init()

View File

@@ -0,0 +1,24 @@
{
"name": "voice-cloning-job-handler",
"version": "1.0.0",
"description": "This will handle the voice cloning jobs",
"main": "index.js",
"scripts": {
"deploy-production": "npx dotenv-cli -e ./app-scripts/env-aws-code-deploy/.env.production.aws-code-deploy node ./app-scripts/deploy-scripts/deploy-production.js",
"deploy-staging": "npx dotenv-cli -e ./app-scripts/env-aws-code-deploy/.env.staging.aws-code-deploy node ./app-scripts/deploy-scripts/deploy-staging.js"
},
"dependencies": {
"@bugsnag/js": "^7.3.5",
"aws-sdk": "^2.752.0",
"fs-extra": "^9.0.1",
"mongoose": "^6.8.0",
"pm2": "^5.2.0",
"rimraf": "^3.0.2",
"uuid": "^8.3.2"
},
"devDependencies": {
"aws-code-deploy": "^1.0.11"
},
"author": "potion Team",
"license": "ISC"
}

View File

@@ -0,0 +1,18 @@
apps:
- name: training-model
script: index.js
watch: false
autorestart: true
instances: 1
time: true
env:
NODE_ENV: 'staging'
POTION_APP_ENV: 'staging'
SQS_URL: 'https://sqs.us-west-2.amazonaws.com/[REDACTED_AWS_ACCOUNT_1961]/potion-voice-clone-ai-staging.fifo'
BUGSNAG_BACKEND_KEY: '[REDACTED_generic-api-key]'
MONGODB_URI_DEV: 'mongodb+srv://[REDACTED_MONGO_USER_deve]:scrubbed_1@example.com7.mongodb.net/potion_development?retryWrites=true&w=majority'
MONGODB_URI_STAGING: 'mongodb+srv://[REDACTED_MONGO_USER_stag]:scrubbed_2@example.com7.mongodb.net/potion_staging?retryWrites=true&w=majority'
MONGODB_URI_PROD: ''
CLOUDFRONT_URL_PROD: 'https://videoassets.sendpotion.com'
CLOUDFRONT_URL_STAGING: ''
CLOUDFRONT_URL_DEV: 'https://d2rmbzmoml90gd.cloudfront.net'

View File

@@ -0,0 +1,18 @@
apps:
- name: training-model
script: index.js
watch: false
autorestart: true
instances: 1
time: true
env:
NODE_ENV: 'production'
POTION_APP_ENV: 'production'
SQS_URL: 'https://sqs.us-west-2.amazonaws.com/[REDACTED_AWS_ACCOUNT_1961]/potion-voice-clone-ai-production.fifo'
BUGSNAG_BACKEND_KEY: '[REDACTED_generic-api-key]'
MONGODB_URI_DEV: 'mongodb+srv://[REDACTED_MONGO_USER_deve]:scrubbed_1@example.com7.mongodb.net/potion_development?retryWrites=true&w=majority'
MONGODB_URI_STAGING: 'mongodb+srv://[REDACTED_MONGO_USER_stag]:scrubbed_2@example.com7.mongodb.net/potion_staging?retryWrites=true&w=majority'
MONGODB_URI_PROD: 'mongodb+srv://[REDACTED_MONGO_USER_prod]:scrubbed_3@example.com.net/potion_production?retryWrites=true&w=majority'
CLOUDFRONT_URL_PROD: 'https://videoassets.sendpotion.com'
CLOUDFRONT_URL_STAGING: ''
CLOUDFRONT_URL_DEV: 'https://d2rmbzmoml90gd.cloudfront.net'

View File

@@ -0,0 +1,4 @@
const UserAudioProfile = require('./user_audio_profile_model')
const UserAudioProfileService = require('./user_audio_profile_service')
module.exports = UserAudioProfileService(UserAudioProfile)

View File

@@ -0,0 +1,40 @@
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const UserAudioProfileSchema = Schema(
{
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
name: {
type: String,
required: true,
default: '',
},
status: {
type: String,
required: false,
default: 'created',
},
training_model_path: {
type: Schema.Types.Mixed,
default: null,
},
training_model_s3_path: {
type: Schema.Types.Mixed,
default: null,
},
deleted: {
type: Boolean,
required: true,
default: false,
},
},
{
timestamps: true,
}
)
module.exports = mongoose.model('UserAudioProfile', UserAudioProfileSchema)

View File

@@ -0,0 +1,140 @@
const StringifyUtils = require('../../app/services/utils/logService')
const create = (UserAudioProfileModel) => async (data) => {
try {
const newModel = new UserAudioProfileModel({ ...data })
const savedModel = await newModel.save()
return savedModel
} catch (error) {
const details = { data }
console.log(
'ERROR - USER AUDIO PROFILE SERVICE > create',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const insertMany = (UserAudioProfileModel) => async (data) => {
try {
const inserted = await UserAudioProfileModel.insertMany(data)
return inserted
} catch (error) {
const details = { data }
console.log(
'ERROR - USER AUDIO PROFILE SERVICE > insertMany',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const read = (UserAudioProfileModel) => async (filter) => {
try {
const foundModel = await UserAudioProfileModel.findOne({
...filter,
deleted: false,
})
return foundModel
} catch (error) {
const details = { filter }
console.log(
'ERROR - USER AUDIO PROFILE SERVICE > read',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const find = (UserAudioProfileModel) => async (filter) => {
try {
const foundModels = await UserAudioProfileModel.find({
...filter,
deleted: false,
})
return foundModels
} catch (error) {
const details = { filter }
console.log(
'ERROR - USER AUDIO PROFILE SERVICE > find',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const update = (UserAudioProfileModel) => async (data) => {
try {
const updatedModel = await UserAudioProfileModel.findOneAndUpdate(
{ _id: data._id },
data,
{
new: true,
}
)
return updatedModel
} catch (error) {
const details = { data }
console.log(
'ERROR - USER AUDIO PROFILE SERVICE > update',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const remove = (UserAudioProfileModel) => async (filter) => {
try {
const updatedModel = await UserAudioProfileModel.findOneAndUpdate(
{ ...filter },
{
$set: {
deleted: true,
},
},
{ new: true }
)
return updatedModel
} catch (error) {
const details = { filter }
console.log(
'ERROR - USER AUDIO PROFILE SERVICE > remove',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const removeMany = (UserAudioProfileModel) => async (filter) => {
try {
const updatedModel = await UserAudioProfileModel.updateMany(
{ ...filter },
{
$set: {
deleted: true,
},
},
{ new: true }
)
return updatedModel
} catch (error) {
const details = { filter }
console.log(
'ERROR - USER AUDIO PROFILE SERVICE > removeMany',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
module.exports = (UserAudioProfileModel) => {
return {
create: create(UserAudioProfileModel),
insertMany: insertMany(UserAudioProfileModel),
read: read(UserAudioProfileModel),
remove: remove(UserAudioProfileModel),
removeMany: removeMany(UserAudioProfileModel),
update: update(UserAudioProfileModel),
find: find(UserAudioProfileModel),
}
}

View File

@@ -0,0 +1,4 @@
const VoiceCloning = require('./voice_cloning_model')
const VoiceCloningService = require('./voice_cloning_service')
module.exports = VoiceCloningService(VoiceCloning)

View File

@@ -0,0 +1,44 @@
const mongoose = require('mongoose')
const Schema = mongoose.Schema
const VoiceCloningSchema = Schema(
{
userId: {
type: Schema.Types.ObjectId,
ref: 'User',
required: true,
},
userAudioProfileId: {
type: Schema.Types.ObjectId,
ref: 'UserAudioProfile',
required: true,
},
status: {
type: String,
required: false,
default: 'created',
},
input: {
type: Schema.Types.Mixed,
default: null,
},
training_model: {
type: Schema.Types.Mixed,
default: null,
},
metadata: {
type: Schema.Types.Mixed,
default: null,
},
deleted: {
type: Boolean,
required: true,
default: false,
},
},
{
timestamps: true,
}
)
module.exports = mongoose.model('VoiceCloning', VoiceCloningSchema)

View File

@@ -0,0 +1,141 @@
const StringifyUtils = require('../../app/services/utils/logService')
const create = (VoiceCloningModel) => async (data) => {
try {
const newModel = new VoiceCloningModel({ ...data })
const savedModel = await newModel.save()
return savedModel
} catch (error) {
const details = { data }
console.log(
'ERROR - VOICE CLONING SERVICE > create',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const insertMany = (VoiceCloningModel) => async (data) => {
try {
const inserted = await VoiceCloningModel.insertMany(data)
return inserted
} catch (error) {
const details = { data }
console.log(
'ERROR - VOICE CLONING SERVICE > insertMany',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const read = (VoiceCloningModel) => async (filter) => {
try {
const foundModel = await VoiceCloningModel.findOne({
...filter,
deleted: false,
})
return foundModel
} catch (error) {
const details = { filter }
console.log(
'ERROR - VOICE CLONING SERVICE > read',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const find = (VoiceCloningModel) => async (filter) => {
try {
const foundModels = await VoiceCloningModel.find({
...filter,
deleted: false,
})
return foundModels
} catch (error) {
const details = { filter }
console.log(
'ERROR - VOICE CLONING SERVICE > find',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const update = (VoiceCloningModel) => async (data) => {
try {
const updatedModel = await VoiceCloningModel.findOneAndUpdate(
{ _id: data._id },
data,
{
new: true,
}
)
return updatedModel
} catch (error) {
const details = { data }
console.log(
'ERROR - VOICE CLONING SERVICE > update',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const remove = (VoiceCloningModel) => async (filter) => {
try {
const updatedModel = await VoiceCloningModel.findOneAndUpdate(
{ ...filter },
{
$set: {
deleted: true,
},
},
{ new: true }
)
return updatedModel
} catch (error) {
const details = { filter }
console.log(
'ERROR - VOICE CLONING SERVICE > remove',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
const removeMany = (VoiceCloningModel) => async (filter) => {
try {
const updatedModel = await VoiceCloningModel.updateMany(
{ ...filter },
{
$set: {
deleted: true,
},
},
{ new: true }
)
return updatedModel
} catch (error) {
const details = { filter }
console.log(
'ERROR - VOICE CLONING SERVICE > removeMany',
StringifyUtils.potionErrorObj(error, details)
)
throw error
}
}
module.exports = (VoiceCloningModel) => {
return {
create: create(VoiceCloningModel),
insertMany: insertMany(VoiceCloningModel),
read: read(VoiceCloningModel),
remove: remove(VoiceCloningModel),
removeMany: removeMany(VoiceCloningModel),
update: update(VoiceCloningModel),
find: find(VoiceCloningModel),
}
}