Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
File size: 3,742 Bytes
3165afb 2cae2a9 3165afb 2cae2a9 d5d9687 2cae2a9 3165afb 2cae2a9 3165afb d5d9687 2cae2a9 3165afb 2cae2a9 d5d9687 3165afb 2cae2a9 3165afb 2cae2a9 3165afb 2cae2a9 3165afb 74dba9a 2cae2a9 3165afb 2cae2a9 3165afb 2cae2a9 3165afb 2cae2a9 3165afb 2cae2a9 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 |
import { existsSync } from "node:fs"
import path from "node:path"
import { v4 as uuidv4 } from "uuid"
import ffmpeg, { FfmpegCommand } from "fluent-ffmpeg"
import { getRandomDirectory, removeTemporaryFiles, writeBase64ToFile } from "@aitube/io"
import { addBase64Header } from "@aitube/encoders"
import { getMediaInfo } from "../analyze/getMediaInfo"
export type ConcatenateAudioOptions = {
// those are base64 audio strings!
audioTracks?: string[]; // base64
audioFilePaths?: string[]; // path
crossfadeDurationInSec?: number;
outputFormat?: string; // "wav" or "mp3"
output?: string;
}
export type ConcatenateAudioOutput = {
filepath: string;
durationInSec: number;
}
export async function concatenateAudio({
output,
audioTracks = [],
audioFilePaths = [],
crossfadeDurationInSec = 10,
outputFormat = "mp3"
}: ConcatenateAudioOptions): Promise<ConcatenateAudioOutput> {
if (!Array.isArray(audioTracks)) {
throw new Error("Audios must be provided in an array");
}
const tempDir = await getRandomDirectory()
// console.log(" |- created tmp dir")
// trivial case: there is only one audio to concatenate!
if (audioTracks.length === 1 && audioTracks[0]) {
const audioTrack = audioTracks[0]
const outputFilePath = path.join(tempDir, `audio_0.${outputFormat}`)
await writeBase64ToFile(addBase64Header(audioTrack, outputFormat), outputFilePath)
// console.log(" |- there is only one track! so.. returning that")
const { durationInSec } = await getMediaInfo(outputFilePath)
return { filepath: outputFilePath, durationInSec }
}
if (audioFilePaths.length === 1) {
throw new Error("concatenating a single audio file path is not implemented yet")
}
try {
let i = 0
for (const track of audioTracks) {
if (!track) { continue }
const audioFilePath = path.join(tempDir, `audio_${++i}.${outputFormat}`);
await writeBase64ToFile(addBase64Header(track, outputFormat), audioFilePath)
audioFilePaths.push(audioFilePath);
}
// TODO: convert this to an async filter using promises
audioFilePaths = audioFilePaths.filter((audio) => existsSync(audio))
const outputFilePath = output ?? path.join(tempDir, `${uuidv4()}.${outputFormat}`);
let filterComplex = "";
let prevLabel = "0";
for (let i = 0; i < audioFilePaths.length - 1; i++) {
const nextLabel = `a${i}`;
filterComplex += `[${prevLabel}][${i + 1}]acrossfade=d=${crossfadeDurationInSec}:c1=tri:c2=tri[${nextLabel}];`;
prevLabel = nextLabel;
}
/*
console.log(" |- concatenateAudio(): DEBUG:", {
tempDir,
audioFilePaths,
outputFilePath,
filterComplex,
prevLabel
})
*/
let cmd: FfmpegCommand = ffmpeg() // .outputOptions('-vn');
audioFilePaths.forEach((audio, i) => {
cmd = cmd.input(audio)
})
const promise = new Promise<ConcatenateAudioOutput>((resolve, reject) => {
cmd = cmd
.on('error', reject)
.on('end', async () => {
try {
const { durationInSec } = await getMediaInfo(outputFilePath);
// console.log("concatenation ended! see ->", outputFilePath)
resolve({ filepath: outputFilePath, durationInSec })
} catch (err) {
reject(err)
}
})
.complexFilter(filterComplex, prevLabel)
.save(outputFilePath);
});
const result = await promise
return result
} catch (error) {
console.error(`Failed to assemble audio!`)
console.error(error)
throw new Error(`Failed to assemble audio: ${(error as Error)?.message || error}`);
} finally {
await removeTemporaryFiles(audioFilePaths)
}
}
|