mirror of
https://github.com/jambonz/batch-speech-utils.git
synced 2026-01-25 02:08:27 +00:00
updated entry point and lib
This commit is contained in:
72
index.js
72
index.js
@@ -1,67 +1,9 @@
|
||||
const {audioProcess} = require('./lib/transcription');
|
||||
const axios = require('axios');
|
||||
const {redactSensitiveInfo} = require('./lib/audioRedaction');
|
||||
const path = require('path');
|
||||
const fs = require('fs-extra');
|
||||
require('dotenv').config();
|
||||
const AudioProcessor = require('./lib/utils');
|
||||
const DeepgramTranscriber = require('./lib/deepgramTranscriber');
|
||||
const DeepgramRedactor = require('./lib/deepgramRedactor');
|
||||
|
||||
const audioDir = process.env.AUDIO_DIR;
|
||||
const accountSid = process.env.ACCOUNT_SID;
|
||||
const callSid = process.env.CALL_SID;
|
||||
const jambonzApiToken = process.env.JAMBONZ_API_TOKEN;
|
||||
const recordingDate = '2024/06/06';
|
||||
|
||||
const audioURL = `https://jambonz.one/api/v1/Accounts/${accountSid}/RecentCalls/${callSid}/record/${recordingDate}/mp3`;
|
||||
console.log(audioURL);
|
||||
|
||||
const config = {
|
||||
responseType: 'stream',
|
||||
headers: {
|
||||
Authorization: `Bearer ${jambonzApiToken}`
|
||||
}
|
||||
module.exports = {
|
||||
AudioProcessor,
|
||||
DeepgramTranscriber,
|
||||
DeepgramRedactor
|
||||
};
|
||||
|
||||
// Function to download and process audio
|
||||
async function downloadAndProcessAudio() {
|
||||
try {
|
||||
const response = await axios.get(audioURL, config);
|
||||
const audioFilePath = path.join(audioDir, 'recording.mp3');
|
||||
const writer = fs.createWriteStream(audioFilePath);
|
||||
|
||||
response.data.pipe(writer);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
writer.on('finish', resolve);
|
||||
writer.on('error', reject);
|
||||
});
|
||||
|
||||
console.log('File downloaded successfully');
|
||||
await processAndRedactAudio(audioFilePath);
|
||||
} catch (error) {
|
||||
console.error('Error downloading the file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Function to process and redact audio
|
||||
async function processAndRedactAudio(audioFilePath) {
|
||||
try {
|
||||
const dG = new audioProcess(process.env.DEEPGRAM_API_KEY, audioFilePath);
|
||||
const results = await dG.processAudio();
|
||||
const jsonContent = JSON.stringify(results, null, 2);
|
||||
const outputPathJson = path.join(__dirname, 'redacts', `${path.basename(audioFilePath, '.mp3')}-redacts.json`);
|
||||
const outputPathAudio = path.join(__dirname, 'redacts', `${path.basename(audioFilePath, '.mp3')}-redacted.wav`);
|
||||
|
||||
await fs.ensureDir(path.dirname(outputPathJson));
|
||||
await fs.writeFile(outputPathJson, jsonContent, 'utf-8');
|
||||
console.dir({analysis: [results]}, {depth: null});
|
||||
|
||||
if (results && results.redactionTimestamps) {
|
||||
await redactSensitiveInfo(results.redactionTimestamps, audioFilePath, outputPathAudio);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error in processing audio:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the process
|
||||
downloadAndProcessAudio();
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
const fs = require('fs').promises;
|
||||
|
||||
|
||||
function redactSensitiveInfo(transcriptionData, audioPath, audioOutputPath, delta = 0.05) {
|
||||
const command = ffmpeg(audioPath)
|
||||
.outputFormat('wav'); // Ensure output format is WAV
|
||||
|
||||
// Iterate over transcription data to apply audio filters
|
||||
for (let i = 0; i < transcriptionData.length; i++) {
|
||||
const {word, start} = transcriptionData[i];
|
||||
let end = transcriptionData[i].end; // Default end time
|
||||
|
||||
// Check if the word needs redaction
|
||||
if (word.startsWith('[') && word.endsWith(']')) {
|
||||
// Find the start of the next non-redacted word
|
||||
for (let j = i + 1; j < transcriptionData.length; j++) {
|
||||
if (!(transcriptionData[j].word.startsWith('[') && transcriptionData[j].word.endsWith(']'))) {
|
||||
end = transcriptionData[j].start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the volume filter to silence from the start of the current word to the start of the next non-redacted word
|
||||
command.audioFilters({
|
||||
filter: 'volume',
|
||||
options: `volume=0:enable='between(t,${start - delta},${end})'` // Applying silence
|
||||
});
|
||||
|
||||
// Log the redacted segments
|
||||
console.log(`Redacting from ${start}s to ${end}s: "${word}"`);
|
||||
}
|
||||
}
|
||||
// Handlers for command execution
|
||||
command.on('end', () => {
|
||||
console.log(`Redacted audio saved at ${audioOutputPath}`);
|
||||
}).on('error', (err, stdout, stderr) => {
|
||||
console.error('Error processing audio file:', err.message);
|
||||
console.error('ffmpeg stdout:', stdout);
|
||||
console.error('ffmpeg stderr:', stderr);
|
||||
}).saveToFile(audioOutputPath);
|
||||
}
|
||||
|
||||
module.exports = {redactSensitiveInfo};
|
||||
42
lib/deepgramRedactor.js
Normal file
42
lib/deepgramRedactor.js
Normal file
@@ -0,0 +1,42 @@
|
||||
const Redactor = require('./redactor');
|
||||
const ffmpeg = require('fluent-ffmpeg');
|
||||
|
||||
class DeepGramRedactor extends Redactor {
|
||||
async redactAudio(transcriptionData, audioPath, audioOutputPath, { delta = 0.05 } = {}) {
|
||||
const command = ffmpeg(audioPath)
|
||||
.outputFormat('wav'); // Ensure output format is WAV
|
||||
|
||||
// Iterate over transcription data to apply audio filters
|
||||
for (let i = 0; i < transcriptionData.length; i++) {
|
||||
const {word, start} = transcriptionData[i];
|
||||
let end = transcriptionData[i].end; // Default end time
|
||||
|
||||
// Check if the word needs redaction
|
||||
if (word.startsWith('[') && word.endsWith(']')) {
|
||||
// Find the start of the next non-redacted word
|
||||
for (let j = i + 1; j < transcriptionData.length; j++) {
|
||||
if (!(transcriptionData[j].word.startsWith('[') && transcriptionData[j].word.endsWith(']'))) {
|
||||
end = transcriptionData[j].start;
|
||||
break;
|
||||
}
|
||||
}
|
||||
command.audioFilters({
|
||||
filter: 'volume',
|
||||
options: `volume=0:enable='between(t,${start - delta},${end})'` // Applying silence
|
||||
});
|
||||
|
||||
// Log the redacted segments
|
||||
console.log(`Redacting from ${start}s to ${end}s: "${word}"`);
|
||||
}
|
||||
}
|
||||
// Handlers for command execution
|
||||
command.on('end', () => {
|
||||
console.log(`Redacted audio saved at ${audioOutputPath}`);
|
||||
}).on('error', (err, stdout, stderr) => {
|
||||
console.error('Error processing audio file:', err.message);
|
||||
console.error('ffmpeg stdout:', stdout);
|
||||
console.error('ffmpeg stderr:', stderr);
|
||||
}).saveToFile(audioOutputPath);
|
||||
}
|
||||
}
|
||||
module.exports = DeepGramRedactor;
|
||||
66
lib/deepgramTranscriber.js
Normal file
66
lib/deepgramTranscriber.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// deepgramTranscriber.js
|
||||
const Transcriber = require('./transcriber');
|
||||
const { createClient } = require('@deepgram/sdk');
|
||||
const fs = require('fs');
|
||||
|
||||
class DeepgramTranscriber extends Transcriber {
|
||||
constructor(apiKey, audioFilePath) {
|
||||
super(audioFilePath);
|
||||
this.deepgram = createClient(apiKey);
|
||||
}
|
||||
|
||||
async transcribeFile(filePath) {
|
||||
const { result } = await this.deepgram.listen.prerecorded.transcribeFile(
|
||||
fs.readFileSync(filePath),
|
||||
{ model: 'nova-2', smart_format: true, detect_entities: true }
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async redactFile(filePath) {
|
||||
const { result } = await this.deepgram.listen.prerecorded.transcribeFile(
|
||||
fs.readFileSync(filePath),
|
||||
{ model: 'nova-2', smart_format: true, redact: 'pii' }
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async analyzeText(text) {
|
||||
const { result } = await this.deepgram.read.analyzeText(
|
||||
{ text },
|
||||
{ language: 'en', sentiment: true, intents: true, summarize: true }
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
async processAudio(filePath = this.audioDir) {
|
||||
try {
|
||||
const transcription = await this.transcribeFile(filePath);
|
||||
const redaction = await this.redactFile(filePath);
|
||||
const transcript = transcription.results.channels[0].alternatives[0].transcript;
|
||||
const timestamps = transcription.results.channels[0].alternatives[0].words;
|
||||
const redactionTimestamps = redaction.results.channels[0].alternatives[0].words;
|
||||
const redacted = redaction.results.channels[0].alternatives[0].transcript;
|
||||
const entities = transcription.results.channels[0].alternatives[0].entities;
|
||||
|
||||
const analysisResult = await this.analyzeText(transcript);
|
||||
const sentimentSegment = analysisResult.results.sentiments.segments[0];
|
||||
const sentiment = sentimentSegment.sentiment;
|
||||
const sentimentScore = sentimentSegment.sentiment_score;
|
||||
|
||||
return {
|
||||
transcript,
|
||||
timestamps,
|
||||
redactionTimestamps,
|
||||
redacted,
|
||||
sentiment,
|
||||
sentimentScore,
|
||||
entities
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DeepgramTranscriber;
|
||||
13
lib/redactor.js
Normal file
13
lib/redactor.js
Normal file
@@ -0,0 +1,13 @@
|
||||
class Redactor {
|
||||
constructor() {
|
||||
if (new.target === Redactor) {
|
||||
throw new TypeError('Cannot construct Redactor instances directly');
|
||||
}
|
||||
}
|
||||
|
||||
async redactAudio(transcriptionData, audioPath, audioOutputPath, options) {
|
||||
throw new Error("Method 'redactAudio' must be implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Redactor;
|
||||
27
lib/transcriber.js
Normal file
27
lib/transcriber.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// transcriber.js
|
||||
class Transcriber {
|
||||
constructor(audioFilePath) {
|
||||
if (new.target === Transcriber) {
|
||||
throw new TypeError('Cannot construct Transcriber instances directly');
|
||||
}
|
||||
this.audioDir = audioFilePath;
|
||||
}
|
||||
|
||||
async transcribeFile(filePath) {
|
||||
throw new Error("Method 'transcribeFile' must be implemented.");
|
||||
}
|
||||
|
||||
async redactFile(filePath) {
|
||||
throw new Error("Method 'redactFile' must be implemented.");
|
||||
}
|
||||
|
||||
async analyzeText(text) {
|
||||
throw new Error("Method 'analyzeText' must be implemented.");
|
||||
}
|
||||
|
||||
async processAudio(filePath) {
|
||||
throw new Error("Method 'processAudio' must be implemented.");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Transcriber;
|
||||
@@ -1,82 +0,0 @@
|
||||
const {createClient} = require("@deepgram/sdk");
|
||||
const fs = require("fs");
|
||||
|
||||
class audioProcess {
|
||||
constructor(apiKey, audioFilePath) {
|
||||
this.deepgram = createClient(apiKey);
|
||||
this.audioDir = audioFilePath
|
||||
}
|
||||
|
||||
async transcribeFile(filePath) {
|
||||
const {result, error} = await this.deepgram.listen.prerecorded.transcribeFile(
|
||||
fs.readFileSync(filePath),
|
||||
{
|
||||
model: "nova-2",
|
||||
smart_format: true,
|
||||
detect_entities: true
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async redactFile(filePath) {
|
||||
const {result, error} = await this.deepgram.listen.prerecorded.transcribeFile(
|
||||
fs.readFileSync(filePath),
|
||||
{
|
||||
model: "nova-2",
|
||||
smart_format: true,
|
||||
redact: "pii"
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async analyzeText(text) {
|
||||
const {result, error} = await this.deepgram.read.analyzeText(
|
||||
{
|
||||
text,
|
||||
},
|
||||
{
|
||||
language: "en",
|
||||
sentiment: true,
|
||||
intents: true,
|
||||
summarize: true
|
||||
}
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async processAudio(filePath = this.audioDir) {
|
||||
try {
|
||||
const transcription = await this.transcribeFile(filePath);
|
||||
const redaction = await this.redactFile(filePath);
|
||||
const transcript = transcription.results.channels[0].alternatives[0].transcript;
|
||||
const timestamps = transcription.results.channels[0].alternatives[0].words;
|
||||
const redactionTimestamps = redaction.results.channels[0].alternatives[0].words;
|
||||
const redacted = redaction.results.channels[0].alternatives[0].transcript;
|
||||
const entities = transcription.results.channels[0].alternatives[0].entities;
|
||||
|
||||
const analysisResult = await this.analyzeText(transcript);
|
||||
const sentimentSegment = analysisResult.results.sentiments.segments[0];
|
||||
const sentiment = sentimentSegment.sentiment;
|
||||
const sentimentScore = sentimentSegment.sentiment_score;
|
||||
|
||||
return {
|
||||
transcript,
|
||||
timestamps,
|
||||
redactionTimestamps,
|
||||
redacted,
|
||||
sentiment,
|
||||
sentimentScore,
|
||||
entities
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {audioProcess};
|
||||
26
lib/utils.js
Normal file
26
lib/utils.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const DeepgramTranscriber = require('./deepgramTranscriber');
|
||||
const DeepgramRedactor = require('./deepgramRedactor');
|
||||
|
||||
class AudioProcessing {
|
||||
static getTranscriber(serviceType,DEEPGRAM_API_KEY, audioFilePath) {
|
||||
switch (serviceType) {
|
||||
case 'deepgram':
|
||||
return new DeepgramTranscriber(DEEPGRAM_API_KEY, audioFilePath);
|
||||
|
||||
default:
|
||||
throw new Error('Unknown transcription service');
|
||||
}
|
||||
}
|
||||
|
||||
static getRedactor(serviceType, audioFilePath) {
|
||||
switch (serviceType) {
|
||||
case 'deepgram':
|
||||
return new DeepgramRedactor(audioFilePath);
|
||||
|
||||
default:
|
||||
throw new Error('Unknown redaction service');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AudioProcessing;
|
||||
Reference in New Issue
Block a user