diff options
Diffstat (limited to 'src/server')
-rw-r--r-- | src/server/ApiManagers/FireflyManager.ts | 224 | ||||
-rw-r--r-- | src/server/ApiManagers/FlashcardManager.ts | 161 | ||||
-rw-r--r-- | src/server/ApiManagers/UploadManager.ts | 7 | ||||
-rw-r--r-- | src/server/DashSession/DashSessionAgent.ts | 4 | ||||
-rw-r--r-- | src/server/DashUploadUtils.ts | 21 | ||||
-rw-r--r-- | src/server/index.ts | 2 |
6 files changed, 310 insertions, 109 deletions
diff --git a/src/server/ApiManagers/FireflyManager.ts b/src/server/ApiManagers/FireflyManager.ts index e75ede9df..e934e635b 100644 --- a/src/server/ApiManagers/FireflyManager.ts +++ b/src/server/ApiManagers/FireflyManager.ts @@ -6,6 +6,7 @@ import * as path from 'path'; import { DashUserModel } from '../authentication/DashUserModel'; import { DashUploadUtils } from '../DashUploadUtils'; import { _error, _invalid, _success, Method } from '../RouteManager'; +import { Upload } from '../SharedMediaTypes'; import { Directory, filesDirectory } from '../SocketData'; import ApiManager, { Registration } from './ApiManager'; @@ -70,54 +71,44 @@ export default class FireflyManager extends ApiManager { ); uploadImageToDropbox = (fileUrl: string, user: DashUserModel | undefined, dbx = new Dropbox({ accessToken: user?.dropboxToken || '' })) => - new Promise<string | Error>((res, rej) => + new Promise<string>((resolve, reject) => { fs.readFile(path.join(filesDirectory, `${Directory.images}/${path.basename(fileUrl)}`), undefined, (err, contents) => { if (err) { - console.log('Error: ', err); - rej(); - } else { - dbx.filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents }) - .then(response => { - dbx.filesGetTemporaryLink({ path: response.result.path_display ?? '' }) - .then(link => res(link.result.link)) - .catch(e => res(new Error(e.toString()))); - }) - .catch(e => { + return reject(new Error('Error reading file:' + err.message)); + } + + const uploadToDropbox = (dropboxClient: Dropbox) => + dropboxClient + .filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents }) + .then(response => + dropboxClient + .filesGetTemporaryLink({ path: response.result.path_display ?? '' }) + .then(link => resolve(link.result.link)) + .catch(linkErr => reject(new Error('Failed to get temporary link: ' + linkErr.message))) + ) + .catch(uploadErr => { if (user?.dropboxRefresh) { - console.log('*********** try refresh dropbox for: ' + user.email + ' ***********'); - this.refreshDropboxToken(user).then(token => { - if (!token) { - console.log('Dropbox error: cannot refresh token'); - res(new Error(e.toString())); - } else { - const dbxNew = new Dropbox({ accessToken: user.dropboxToken || '' }); - dbxNew - .filesUpload({ path: `/Apps/browndash/${path.basename(fileUrl)}`, contents }) - .then(response => { - dbxNew - .filesGetTemporaryLink({ path: response.result.path_display ?? '' }) - .then(link => res(link.result.link)) - .catch(linkErr => res(new Error(linkErr.toString()))); - }) - .catch(uploadErr => { - console.log('Dropbox error:', uploadErr); - res(new Error(uploadErr.toString())); - }); - } - }); + console.log('Attempting to refresh Dropbox token for user:', user.email); + this.refreshDropboxToken(user) + .then(token => { + if (!token) return reject(new Error('Failed to refresh Dropbox token.' + user.email)); + + const dbxNew = new Dropbox({ accessToken: token }); + uploadToDropbox(dbxNew).catch(finalErr => reject(new Error('Failed to refresh Dropbox token:' + finalErr.message))); + }) + .catch(refreshErr => reject(new Error('Failed to refresh Dropbox token: ' + refreshErr.message))); } else { - console.log('Dropbox error:', e); - res(new Error(e.toString())); + reject(new Error('Dropbox error: ' + uploadErr.message)); } }); - } - }) - ); + + uploadToDropbox(dbx); + }); + }); generateImage = (prompt: string = 'a realistic illustration of a cat coding', width: number = 2048, height: number = 2048, seed?: number) => { let body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}} }`; if (seed) { - console.log('RECEIVED SEED', seed); body = `{ "prompt": "${prompt}", "size": { "width": ${width}, "height": ${height}}, "seeds": [${seed}]}`; } const fetched = this.getBearerToken().then(response => @@ -290,44 +281,116 @@ export default class FireflyManager extends ApiManager { register({ method: Method.POST, subscription: '/queryFireflyImageFromStructure', + secureHandler: ({ req, res }) => + new Promise<void>(resolver => + (req.body.styleUrl + ? this.uploadImageToDropbox(req.body.styleUrl, req.user as DashUserModel) + : Promise.resolve(undefined) + ) + .then(styleUrl => + this.uploadImageToDropbox(req.body.structureUrl, req.user as DashUserModel) + .then(dropboxStructureUrl => + ({ styleUrl, structureUrl: dropboxStructureUrl }) + ) + + ) + .then(uploads => + this.generateImageFromStructure( + req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl + ).then(images => + Promise.all((images ?? [new Error('no images were generated')]).map(fire => (fire instanceof Error ? fire : DashUploadUtils.UploadImage(fire.url)))) + .then(dashImages => + (dashImages.every(img => img instanceof Error)) + ? _invalid(res, dashImages[0]!.message) + : _success(res, JSON.stringify(dashImages.filter(img => !(img instanceof Error)))) + ) + ) + ) + .catch(e => { + _invalid(res, e.message); + resolver(); + }) + ), // prettier-ignore + }); + + register({ + method: Method.POST, + subscription: '/outpaintImage', secureHandler: ({ req, res }) => - new Promise<void>(resolver => { - (req.body.styleUrl ? this.uploadImageToDropbox(req.body.styleUrl, req.user as DashUserModel) : Promise.resolve(undefined)) - .then(styleUrl => { - if (styleUrl instanceof Error) { - _invalid(res, styleUrl.message); - throw new Error('Error uploading images to dropbox'); - } - this.uploadImageToDropbox(req.body.structureUrl, req.user as DashUserModel) - .then(dropboxStructureUrl => { - if (dropboxStructureUrl instanceof Error) { - _invalid(res, dropboxStructureUrl.message); - throw new Error('Error uploading images to dropbox'); - } - return { styleUrl, structureUrl: dropboxStructureUrl }; - }) - .then(uploads => - this.generateImageFromStructure(req.body.prompt, req.body.width, req.body.height, uploads.structureUrl, req.body.strength, req.body.presets, uploads.styleUrl) - .then(images => { - Promise.all((images ?? [new Error('no images were generated')]).map(fire => (fire instanceof Error ? fire : DashUploadUtils.UploadImage(fire.url)))) - .then(dashImages => { - if (dashImages.every(img => img instanceof Error)) _invalid(res, dashImages[0]!.message); - else _success(res, JSON.stringify(dashImages.filter(img => !(img instanceof Error)))); - }) - .then(resolver); - }) - .catch(e => { - _invalid(res, e.message); - resolver(); + new Promise<void>(resolver => + this.uploadImageToDropbox(req.body.imageUrl, req.user as DashUserModel) + .then(uploadUrl => + this.getBearerToken() + .then(tokenResponse => tokenResponse?.json()) + .then((tokenData: { access_token: string }) => + fetch('https://firefly-api.adobe.io/v3/images/expand', { + method: 'POST', + headers: [ + ['Content-Type', 'application/json'], + ['Accept', 'application/json'], + ['x-api-key', process.env._CLIENT_FIREFLY_CLIENT_ID ?? ''], + ['Authorization', `Bearer ${tokenData.access_token}`], + ], + body: JSON.stringify({ + image: { + source: { url: uploadUrl }, + }, + size: { + width: Math.round(req.body.newDimensions.width), + height: Math.round(req.body.newDimensions.height), + }, + prompt: req.body.prompt ?? '', + numVariations: 1, + placement: { + inset: { + left: 0, // Math.round((req.body.newDimensions.width - req.body.originalDimensions.width) / 2), + top: 0, // Math.round((req.body.newDimensions.height - req.body.originalDimensions.height) / 2), + right: 0, // Math.round((req.body.newDimensions.width - req.body.originalDimensions.width) / 2), + bottom: 0, // Math.round((req.body.newDimensions.height - req.body.originalDimensions.height) / 2), + }, + alignment: { + horizontal: req.body.halignment || 'center', + vertical: req.body.valignment || 'center', + }, + }, + }), + }) + .then(expandResp => expandResp?.json()) + .then(expandData => { + if (expandData.error_code || !expandData.outputs?.[0]?.image?.url) { + console.error('Firefly validation error:', expandData); + _error(res, expandData.message ?? 'Failed to generate image'); + } else { + return DashUploadUtils.UploadImage(expandData.outputs[0].image.url) + .then((info: Upload.ImageInformation | Error) => { + if (info instanceof Error) { + _invalid(res, info.message); + } else { + _success(res, { url: info.accessPaths.agnostic.client }); + } + }) + .catch(uploadErr => { + console.error('DashUpload Error:', uploadErr); + _error(res, 'Failed to upload generated image.'); + }); + } }) - ); - }) - .catch(() => { - /* do nothing */ + ) + ) + .catch(e => { + _invalid(res, e.message); resolver(); - }); - }), + }) + ), }); + + /* register({ + method: Method.POST + subscription: '/queryFireflyOutpaint', + secureHandler: ({req, res}) => + this.outpaintImage() + })*/ + register({ method: Method.POST, subscription: '/queryFireflyImage', @@ -354,23 +417,6 @@ export default class FireflyManager extends ApiManager { ) ), }); - register({ - method: Method.POST, - subscription: '/expandImage', - secureHandler: ({ req, res }) => - this.uploadImageToDropbox(req.body.file, req.user as DashUserModel).then(uploadUrl => - uploadUrl instanceof Error - ? _invalid(res, uploadUrl.message) - : this.expandImage(uploadUrl, req.body.prompt).then(text => { - if (text.error_code) _error(res, text.message); - else - DashUploadUtils.UploadImage(text.outputs[0].image.url).then(info => { - if (info instanceof Error) _invalid(res, info.message); - else _success(res, info); - }); - }) - ), - }); // construct this url and send user to it. It will allow them to authorize their dropbox account and will send the resulting token to our endpoint /refreshDropbox // https://www.dropbox.com/oauth2/authorize?client_id=DROPBOX_CLIENT_ID&response_type=code&token_access_type=offline&redirect_uri=http://localhost:1050/refreshDropbox diff --git a/src/server/ApiManagers/FlashcardManager.ts b/src/server/ApiManagers/FlashcardManager.ts new file mode 100644 index 000000000..fd7c42437 --- /dev/null +++ b/src/server/ApiManagers/FlashcardManager.ts @@ -0,0 +1,161 @@ +/** + * @file FlashcardManager.ts + * @description This file defines the FlashcardManager class, responsible for managing API routes + * related to flashcard creation and manipulation. It provides functionality for handling file processing, + * running Python scripts in a virtual environment, and managing dependencies. + */ + +import { spawn } from 'child_process'; +import * as fs from 'fs'; +import * as path from 'path'; +import { Method } from '../RouteManager'; +import ApiManager, { Registration } from './ApiManager'; + +/** + * Runs a Python script using the provided virtual environment and passes file and option arguments. + * @param {string} venvPath - Path to the virtual environment. + * @param {string} scriptPath - Path to the Python script. + * @param {string} [file] - Optional file to pass to the Python script. + * @param {string} [drag] - Optional argument to control drag mode. + * @param {string} [smart] - Optional argument to control smart mode. + * @returns {Promise<string>} - Resolves with the output from the Python script, or rejects on error. + */ +function runPythonScript(venvPath: string, scriptPath: string, file?: string, drag?: string, smart?: string): Promise<string> { + return new Promise((resolve, reject) => { + const pythonPath = process.platform === 'win32' ? path.join(venvPath, 'Scripts', 'python.exe') : path.join(venvPath, 'bin', 'python3'); + + const tempFilePath = path.join(__dirname, `temp_data.txt`); // Unique temp file name + + if (file) { + // Write the raw file data to the temp file without conversion + fs.writeFileSync(tempFilePath, file, 'utf8'); + } + + const pythonProcess = spawn( + pythonPath, + [scriptPath, file ? tempFilePath : undefined, drag, smart].filter(arg => arg !== undefined) + ); + + let pythonOutput = ''; + let stderrOutput = ''; + + pythonProcess.stdout.on('data', data => { + pythonOutput += data.toString(); + }); + + pythonProcess.stderr.on('data', data => { + stderrOutput += data.toString(); + }); + + pythonProcess.on('close', code => { + if (code === 0) { + resolve(pythonOutput); + } else { + reject(`Python process exited with code ${code}: ${stderrOutput}`); + } + }); + }); +} + +/** + * Installs Python dependencies using pip in the specified virtual environment. + * @param {string} venvPath - Path to the virtual environment. + * @param {string} requirementsPath - Path to the requirements.txt file. + * @returns {Promise<void>} - Resolves when dependencies are successfully installed, rejects on failure. + */ +function installDependencies(venvPath: string, requirementsPath: string): Promise<void> { + return new Promise((resolve, reject) => { + const pipPath = process.platform === 'win32' ? path.join(venvPath, 'Scripts', 'pip.exe') : path.join(venvPath, 'bin', 'pip3'); + + const installProcess = spawn(pipPath, ['install', '-r', requirementsPath]); + + installProcess.stdout.on('data', data => { + console.log(`pip stdout: ${data}`); + }); + + installProcess.stderr.on('data', data => { + console.error(`pip stderr: ${data}`); + }); + + installProcess.on('close', code => { + if (code !== 0) { + reject(`Failed to install dependencies. Exit code: ${code}`); + } else { + resolve(); + } + }); + }); +} + +/** + * Creates a new Python virtual environment. + * @param {string} venvPath - Path to the virtual environment that will be created. + * @returns {Promise<void>} - Resolves when the virtual environment is successfully created, rejects on failure. + */ +function createVirtualEnvironment(venvPath: string): Promise<void> { + return new Promise((resolve, reject) => { + const createVenvProcess = spawn('python3', ['-m', 'venv', venvPath]); + + createVenvProcess.on('close', code => { + if (code !== 0) { + reject(`Failed to create virtual environment. Exit code: ${code}`); + } else { + resolve(); + } + }); + }); +} + +/** + * Manages the creation of the virtual environment, installation of dependencies, and running of the Python script. + * @param {string} [file] - Optional file data to be processed by the Python script. + * @param {string} [drag] - Optional argument controlling drag mode. + * @param {string} [smart] - Optional argument controlling smart mode. + * @returns {Promise<string>} - Resolves with the Python script output, or rejects on failure. + */ +async function manageVenvAndRunScript(file?: string, drag?: string, smart?: string): Promise<string> { + const venvPath = path.join(__dirname, '../flashcard/venv'); // Virtual environment path + const requirementsPath = path.join(__dirname, '../flashcard/requirements.txt'); + const pythonScriptPath = path.join(__dirname, '../flashcard/labels.py'); + console.log('venvPath:', venvPath); + + // Check if the virtual environment exists + if (!fs.existsSync(path.join(venvPath, 'bin', 'python3')) && !fs.existsSync(path.join(venvPath, 'Scripts', 'python.exe'))) { + await createVirtualEnvironment(venvPath); + + await installDependencies(venvPath, requirementsPath); + } + + return runPythonScript(venvPath, pythonScriptPath, file, drag, smart); +} + +/** + * FlashcardManager class responsible for managing API routes related to flashcard functionality. + * It initializes API routes for handling YouTube subscriptions and label creation using a Python backend. + */ +export default class FlashcardManager extends ApiManager { + /** + * Initializes the API routes for the FlashcardManager class. + * @param {Registration} register - The registration function for defining API routes. + */ + protected initialize(register: Registration): void { + register({ + method: Method.POST, + subscription: '/labels', + secureHandler: async ({ req, res }) => { + const { file, drag, smart } = req.body; + + try { + // Run the Python process + const result = await manageVenvAndRunScript(file, drag, smart); + res.status(200).send({ result }); + } catch (error) { + console.error('Error initiating document creation:', error); + res.status(500).send({ + error: 'Failed to initiate document creation', + }); + } + }, + }); + } +} diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index c9d5df547..7c55e4a42 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -126,11 +126,8 @@ export default class UploadManager extends ApiManager { secureHandler: async ({ req, res }) => { const { sources } = req.body; if (Array.isArray(sources)) { - const results = await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source))); - res.send(results); - return; - } - res.send(); + res.send(await Promise.all(sources.map(source => DashUploadUtils.UploadImage(source)))); + } else res.send(); }, }); diff --git a/src/server/DashSession/DashSessionAgent.ts b/src/server/DashSession/DashSessionAgent.ts index 891316b80..8688ec049 100644 --- a/src/server/DashSession/DashSessionAgent.ts +++ b/src/server/DashSession/DashSessionAgent.ts @@ -213,9 +213,9 @@ export class DashSessionAgent extends AppliedSessionAgent { // indicate success or failure mainLog(`${error === null ? green('successfully dispatched') : red('failed to dispatch')} ${zipName} to ${cyan(to)}`); error && mainLog(red(error.message)); - } catch (error: any) { + } catch (error: unknown) { mainLog(red('unable to dispatch zipped backup...')); - mainLog(red(error.message)); + mainLog(red((error as { message: string }).message)); } } } diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index a2747257a..f76371b0d 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -221,9 +221,7 @@ export namespace DashUploadUtils { const parseExifData = async (source: string) => { const image = await request.get(source, { encoding: null }); const { /* data, */ error } = await new Promise<{ data: ExifData; error: string | undefined }>(resolve => { - new ExifImage({ image }, (exifError, data) => { - resolve({ data, error: exifError?.message }); - }); + new ExifImage({ image }, (exifError, data) => resolve({ data, error: exifError?.message })); }); return error ? { data: undefined, error } : { data: await exifr.parse(image), error }; }; @@ -295,7 +293,7 @@ export namespace DashUploadUtils { try { // Compute the native width and height ofthe image with an npm module - const { width: nativeWidth, height: nativeHeight } = await requestImageSize(resolvedUrl); + const { width: nativeWidth, height: nativeHeight } = await requestImageSize(resolvedUrl).catch(() => ({ width: 0, height: 0 })); // Bundle up the information into an object return { source, @@ -307,7 +305,6 @@ export namespace DashUploadUtils { ...results, }; } catch (e: unknown) { - console.log(e); return new Error(e ? e.toString?.() : 'unkown error'); } }; @@ -450,14 +447,12 @@ export namespace DashUploadUtils { * 3) the size of the image, in bytes (4432130) * 4) the content type of the image, i.e. image/(jpeg | png | ...) */ - export const UploadImage = async (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => { - const result = await InspectImage(source); - if (result instanceof Error) { - return { name: result.name, message: result.message }; - } - const outputFile = filename || result.filename || ''; - return UploadInspectedImage(result, outputFile, prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false); - }; + export const UploadImage = (source: string, filename?: string, prefix: string = ''): Promise<Upload.ImageInformation | Error> => + InspectImage(source).then(async result => + result instanceof Error + ? ({ name: result.name, message: result.message } as Error) // + : UploadInspectedImage(result, filename || result.filename || '', prefix, isLocal().exec(source) || source.startsWith('data:') ? true : false) + ); type md5 = 'md5'; type falsetype = false; diff --git a/src/server/index.ts b/src/server/index.ts index 1f9af9ee0..3b77359ec 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -4,6 +4,7 @@ import * as mobileDetect from 'mobile-detect'; import * as path from 'path'; import { logExecution } from './ActionUtilities'; import AssistantManager from './ApiManagers/AssistantManager'; +import FlashcardManager from './ApiManagers/FlashcardManager'; import DataVizManager from './ApiManagers/DataVizManager'; import DeleteManager from './ApiManagers/DeleteManager'; import DownloadManager from './ApiManagers/DownloadManager'; @@ -72,6 +73,7 @@ function routeSetter({ addSupervisedRoute, logRegistrationOutcome }: RouteManage new GeneralGoogleManager(), /* new GooglePhotosManager(), */ new DataVizManager(), new AssistantManager(), + new FlashcardManager(), new FireflyManager(), ]; |