Skip to content

Commit

Permalink
Split up modules and improve naming
Browse files Browse the repository at this point in the history
  • Loading branch information
AlecRust committed Aug 9, 2023
1 parent 8fb954b commit 9035583
Show file tree
Hide file tree
Showing 13 changed files with 200 additions and 170 deletions.
45 changes: 45 additions & 0 deletions src/addHeadingToImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const { createCanvas, loadImage } = require('canvas')
const { calculateAvgColor } = require('./utils')

const addHeadingToImage = async (
imageBuffer,
text,
targetWidth,
targetHeight,
) => {
const canvas = createCanvas(targetWidth, targetHeight)
const ctx = canvas.getContext('2d')
const image = await loadImage(imageBuffer)
ctx.drawImage(image, 0, 0, targetWidth, targetHeight)

const avgColor = calculateAvgColor(
ctx.getImageData(
targetWidth / 4,
(targetHeight * 9) / 32,
targetWidth / 2,
(targetHeight * 9) / 16,
),
)
ctx.fillStyle = avgColor > 128 ? '#000000' : '#FFFFFF'
ctx.font = 'bold 48px sans-serif'
ctx.textAlign = 'center'

const words = text.split(' ')
let line = '',
y = targetHeight / 2
for (const word of words) {
const testLine = line + word + ' '
if (ctx.measureText(testLine).width > targetWidth * 0.9) {
ctx.fillText(line, targetWidth / 2, y)
line = word + ' '
y += 50
} else {
line = testLine
}
}
ctx.fillText(line, targetWidth / 2, y)

return canvas.toBuffer('image/png')
}

module.exports = addHeadingToImage
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const convertTextToSpeech = require('./audioGenerator')
const convertTextToSpeech = require('./convertTextToSpeech')
const textToSpeech = require('@google-cloud/text-to-speech')
const fs = require('fs-extra')

describe('audioGenerator', () => {
describe('convertTextToSpeech', () => {
// Mock console.error and console.log to suppress the error message in the test output
beforeAll(() => {
jest.spyOn(console, 'error').mockImplementation(() => {})
Expand Down
32 changes: 2 additions & 30 deletions src/videoGenerator.js → src/createAndConcatenateVideos.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,7 @@ const ffmpeg = require('fluent-ffmpeg')
const fs = require('fs-extra')
const path = require('path')
const naturalSort = require('natural-sort')
const { getAudioDuration } = require('./utils')

const createVideo = async (imageFile, audioFile, videoOutput) => {
console.log(`🎥 Creating video from ${imageFile} and ${audioFile}...`)
const audioDuration = await getAudioDuration(audioFile)
const additionalSeconds = 0

return new Promise((resolve, reject) => {
ffmpeg()
.input(imageFile)
.input(audioFile)
.outputOptions([
'-c:v libx264',
'-profile:v baseline',
'-level 3.0',
'-pix_fmt yuv420p',
'-c:a aac',
'-movflags faststart',
'-filter_complex',
`[0:v]loop=loop=${
Math.ceil(audioDuration + additionalSeconds) * 25
}:size=1:start=0[v];[v][1:a]concat=n=1:v=1:a=1`,
])
.format('mp4')
.saveToFile(videoOutput)
.on('end', resolve)
.on('error', reject)
})
}
const createVideoFromParts = require('./createVideoFromParts')

const concatenateVideos = async (videoFiles, outputPath) => {
console.log(`Concatenating videos to ${outputPath}...`)
Expand Down Expand Up @@ -78,7 +50,7 @@ const createAndConcatenateVideos = async (audioDir, imageDir, outputPath) => {
const videoFiles = await Promise.all(
audioFiles.map(async (audioFile, i) => {
const videoOutput = path.join(videoOutputDirectory, `${i}.mp4`)
await createVideo(imageFiles[i], audioFile, videoOutput)
await createVideoFromParts(imageFiles[i], audioFile, videoOutput)
return videoOutput
}),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const ffmpeg = require('fluent-ffmpeg')
const fs = require('fs-extra')
const path = require('path')
const utils = require('./utils')
const createAndConcatenateVideos = require('./videoGenerator')
const createAndConcatenateVideos = require('./createAndConcatenateVideos')

jest.mock('fluent-ffmpeg', () => {
const mockFfmpeg = {
Expand All @@ -19,7 +19,7 @@ jest.mock('fluent-ffmpeg', () => {
return jest.fn(() => mockFfmpeg)
})

describe('videoGenerator', () => {
describe('createAndConcatenateVideos', () => {
const audioDir = 'audio'
const imageDir = 'images'
const outputPath = 'output.mp4'
Expand Down
32 changes: 32 additions & 0 deletions src/createVideoFromParts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const ffmpeg = require('fluent-ffmpeg')
const { getAudioDuration } = require('./utils')

const createVideoFromParts = async (imageFile, audioFile, videoOutput) => {
console.log(`🎥 Creating video from ${imageFile} and ${audioFile}...`)
const audioDuration = await getAudioDuration(audioFile)
const additionalSeconds = 0

return new Promise((resolve, reject) => {
ffmpeg()
.input(imageFile)
.input(audioFile)
.outputOptions([
'-c:v libx264',
'-profile:v baseline',
'-level 3.0',
'-pix_fmt yuv420p',
'-c:a aac',
'-movflags faststart',
'-filter_complex',
`[0:v]loop=loop=${
Math.ceil(audioDuration + additionalSeconds) * 25
}:size=1:start=0[v];[v][1:a]concat=n=1:v=1:a=1`,
])
.format('mp4')
.saveToFile(videoOutput)
.on('end', resolve)
.on('error', reject)
})
}

module.exports = createVideoFromParts
31 changes: 31 additions & 0 deletions src/cropAndScaleImage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { createCanvas, loadImage } = require('canvas')

const cropAndScaleImage = async (imageBuffer, targetWidth, targetHeight) => {
const canvas = createCanvas(targetWidth, targetHeight)
const ctx = canvas.getContext('2d')
const image = await loadImage(imageBuffer)

const originalWidth = 1024 // Original width of the image
const originalHeight = 1024 // Original height of the image

// Calculate the scaling factor to retain aspect ratio
const scaleFactor = Math.max(
targetWidth / originalWidth,
targetHeight / originalHeight,
)

// Calculate new width and height based on the scaling factor
const newWidth = originalWidth * scaleFactor
const newHeight = originalHeight * scaleFactor

// Calculate the position to center the image on the canvas
const x = (targetWidth - newWidth) / 2
const y = (targetHeight - newHeight) / 2

// Draw the image on the canvas, scaling it and positioning it to fit the target dimensions
ctx.drawImage(image, x, y, newWidth, newHeight)

return canvas.toBuffer('image/png')
}

module.exports = cropAndScaleImage
65 changes: 65 additions & 0 deletions src/generateImageFromText.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const axios = require('axios')
const fs = require('fs-extra')
const cropAndScaleImage = require('./cropAndScaleImage')
const addHeadingToImage = require('./addHeadingToImage')

const targetWidth = 1920
const targetHeight = 1080

const generatePrompt = async (content, openaiInstance) => {
const response = await openaiInstance.createChatCompletion({
model: 'gpt-3.5-turbo',
messages: [
{
role: 'system',
content:
'You are ChatGPT, and your task is to summarize the essence of the user\'s text in less than a dozen words, in a way that would guide the creation of an image e.g. "a teddy bear on a skateboard in Times Square".',
},
{
role: 'user',
content: content,
},
],
})
return response.data.choices[0].message.content.trim()
}

const generateImageFromText = async (line, outputPath, openaiInstance) => {
try {
const imagePrompt = await generatePrompt(line.content, openaiInstance)
console.log('🖼️ Image prompt:', imagePrompt)

const createImageResponse = await openaiInstance.createImage({
prompt: `An illustration of ${imagePrompt}`,
n: 1,
size: '1024x1024',
})

const imageUrl = createImageResponse.data.data[0].url
const imageResponse = await axios.get(imageUrl, {
responseType: 'arraybuffer',
})

let finalImageBuffer = await cropAndScaleImage(
imageResponse.data,
targetWidth,
targetHeight,
)
if (line.type === 'heading') {
finalImageBuffer = await addHeadingToImage(
finalImageBuffer,
line.content,
targetWidth,
targetHeight,
)
}

await fs.outputFile(outputPath, Buffer.from(finalImageBuffer, 'binary'))
console.log(`✅ Image saved to: ${outputPath}`)
} catch (error) {
console.error(`Failed to generate image from text: ${error.message}`)
throw error
}
}

module.exports = generateImageFromText
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
const axios = require('axios')
const fs = require('fs-extra')
const generateImageFromText = require('./imageGenerator')
const generateImageFromText = require('./generateImageFromText')
const { createCanvas } = require('canvas')
const { OpenAIApi, Configuration } = require('openai')

jest.mock('axios')
jest.mock('fs-extra')
jest.mock('openai')

describe('imageGenerator', () => {
describe('generateImageFromText', () => {
let openaiInstance

// Suppress console.log
Expand Down
File renamed without changes.
17 changes: 11 additions & 6 deletions src/scriptGenerator.test.js → src/generateScriptForTopic.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const scriptGenerator = require('./scriptGenerator')
const generateScriptForTopic = require('./generateScriptForTopic')
const fs = require('fs-extra')

jest.mock('fs-extra')

describe('scriptGenerator', () => {
describe('generateScriptForTopic', () => {
let openaiInstance

// Suppress console.log and console.error
Expand Down Expand Up @@ -40,8 +40,13 @@ describe('scriptGenerator', () => {
},
})

// Call the scriptGenerator function with the topic and mock dependencies
await scriptGenerator(topic, outputPath, openaiInstance, fs.outputFile)
// Call the generateScriptForTopic function with the topic and mock dependencies
await generateScriptForTopic(
topic,
outputPath,
openaiInstance,
fs.outputFile,
)

// Check if the content starts with the expected title
expect(fs.outputFile).toHaveBeenCalledWith(
Expand Down Expand Up @@ -71,9 +76,9 @@ describe('scriptGenerator', () => {
new Error('API Error'),
)

// Call the scriptGenerator function with the topic and mock dependencies and expect an error
// Call the generateScriptForTopic function with the topic and mock dependencies and expect an error
await expect(
scriptGenerator(topic, outputPath, openaiInstance, fs.outputFile),
generateScriptForTopic(topic, outputPath, openaiInstance, fs.outputFile),
).rejects.toThrow('API Error')
})
})
Loading

0 comments on commit 9035583

Please sign in to comment.