forked from Yara724/api
206 lines
6.0 KiB
TypeScript
206 lines
6.0 KiB
TypeScript
import { createReadStream, existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import {
|
|
HttpException,
|
|
HttpStatus,
|
|
Injectable,
|
|
OnModuleInit,
|
|
} from "@nestjs/common";
|
|
import { ConfigService } from "@nestjs/config";
|
|
import axios, { AxiosRequestConfig } from "axios";
|
|
import * as FormData from "form-data";
|
|
|
|
@Injectable()
|
|
export class AiService implements OnModuleInit {
|
|
private apiKey: string;
|
|
private accessToken: string = null;
|
|
|
|
// These configurations are for authentication and getting the API key.
|
|
private readonly loginOptions: AxiosRequestConfig = {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
url: `${this.configService.get<string>("AI_URL_V2")}/auth/login`,
|
|
data: {
|
|
username: this.configService.get<string>("AI_USERNAME"),
|
|
password: this.configService.get<string>("AI_PASSWORD"),
|
|
},
|
|
timeout: 1000, // TODO: Make this ENV
|
|
};
|
|
|
|
constructor(private readonly configService: ConfigService) {}
|
|
|
|
private get profileOptions(): AxiosRequestConfig {
|
|
return {
|
|
method: "GET",
|
|
url: `${this.configService.get<string>("AI_URL_V2")}/auth/profile`,
|
|
headers: {
|
|
Authorization: `Bearer ${this.accessToken}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
// This getter dynamically creates the base options for the image processing request.
|
|
private get imageProcessOptions(): AxiosRequestConfig {
|
|
return {
|
|
method: "POST",
|
|
url: `${process.env.AI_URL_V2}/services/car-damage/detector?version=ai-v7`,
|
|
headers: {
|
|
Authorization: `Bearer ${this.accessToken}`,
|
|
"gateway-api-key": `${this.apiKey}`,
|
|
},
|
|
};
|
|
}
|
|
|
|
async onModuleInit() {
|
|
try {
|
|
const res = await this.login();
|
|
if (res?.accessToken) {
|
|
this.accessToken = res.accessToken;
|
|
await this.getApiKey();
|
|
}
|
|
} catch (error) {
|
|
// Don't prevent app startup if AI service is temporarily unavailable
|
|
// The service will attempt to re-authenticate when aiRequestImage is called
|
|
// Reset tokens so re-authentication will be attempted
|
|
this.accessToken = null;
|
|
this.apiKey = null;
|
|
}
|
|
}
|
|
|
|
private async login() {
|
|
const loginResponse = await axios.request(this.loginOptions);
|
|
return loginResponse.data;
|
|
}
|
|
|
|
private async getApiKey() {
|
|
const profileResponse = await axios.request(this.profileOptions);
|
|
this.apiKey = profileResponse.data.apiKey.key;
|
|
return this.apiKey;
|
|
}
|
|
|
|
public async aiRequestImage(file: {
|
|
path: string;
|
|
fileName?: string;
|
|
}): Promise<any> {
|
|
// Ensure authentication is set up
|
|
if (!this.accessToken || !this.apiKey) {
|
|
try {
|
|
const res = await this.login();
|
|
if (res?.accessToken) {
|
|
this.accessToken = res.accessToken;
|
|
await this.getApiKey();
|
|
} else {
|
|
throw new HttpException(
|
|
"AI Service authentication failed",
|
|
HttpStatus.UNAUTHORIZED,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
throw new HttpException(
|
|
"AI Service authentication failed",
|
|
HttpStatus.UNAUTHORIZED,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Resolve relative paths to absolute paths
|
|
const filePath = file.path.startsWith("/")
|
|
? file.path
|
|
: join(process.cwd(), file.path.replace(/^\.\//, ""));
|
|
|
|
// Check if file exists
|
|
if (!existsSync(filePath)) {
|
|
throw new HttpException(
|
|
`File not found: ${file.path}`,
|
|
HttpStatus.NOT_FOUND,
|
|
);
|
|
}
|
|
|
|
const form = new FormData();
|
|
const fileStream = createReadStream(filePath);
|
|
|
|
// Append file with filename if available
|
|
if (file.fileName) {
|
|
form.append("images", fileStream, file.fileName);
|
|
} else {
|
|
// Extract filename from path if not provided
|
|
const pathParts = filePath.split("/");
|
|
const extractedFileName = pathParts[pathParts.length - 1];
|
|
form.append("images", fileStream, extractedFileName);
|
|
}
|
|
|
|
try {
|
|
const requestHeaders = {
|
|
...this.imageProcessOptions.headers,
|
|
...form.getHeaders(),
|
|
};
|
|
|
|
const fs = require("fs");
|
|
const stats = fs.statSync(filePath);
|
|
|
|
const response = await axios.request({
|
|
...this.imageProcessOptions,
|
|
headers: requestHeaders,
|
|
data: form,
|
|
maxContentLength: Infinity,
|
|
maxBodyLength: Infinity,
|
|
});
|
|
|
|
// Validate response structure
|
|
if (!response.data) {
|
|
throw new HttpException(
|
|
"AI Service returned empty response",
|
|
HttpStatus.BAD_GATEWAY,
|
|
);
|
|
}
|
|
|
|
// Check for error in response first (AI service returns 201 with error in body)
|
|
if (response.data.error) {
|
|
throw new HttpException(
|
|
`AI Service error: ${response.data.error}`,
|
|
HttpStatus.BAD_GATEWAY,
|
|
);
|
|
}
|
|
|
|
// Check for processed image (downloadLink)
|
|
if (!response.data.downloadLink) {
|
|
throw new HttpException(
|
|
"AI Service did not return processed image (downloadLink missing)",
|
|
HttpStatus.BAD_GATEWAY,
|
|
);
|
|
}
|
|
|
|
return response.data;
|
|
} catch (er) {
|
|
// Determine error source
|
|
let errorSource = "UNKNOWN";
|
|
let errorMessage = er.message;
|
|
let errorDetails = "No error details available";
|
|
|
|
if (er.response) {
|
|
errorSource = "AI_SERVICE_RESPONSE";
|
|
errorMessage =
|
|
er.response?.data?.message ||
|
|
er.message ||
|
|
`HTTP ${er.response.status}`;
|
|
errorDetails = er.response?.data
|
|
? JSON.stringify(er.response.data, null, 2)
|
|
: `Status: ${er.response.status}, StatusText: ${er.response.statusText}`;
|
|
} else if (er.request) {
|
|
errorSource = "NETWORK_ERROR";
|
|
errorMessage = "Network error - AI service did not respond";
|
|
errorDetails = "Request was made but no response received";
|
|
} else {
|
|
errorSource = "REQUEST_SETUP_ERROR";
|
|
errorMessage = er.message || "Error setting up request";
|
|
}
|
|
|
|
// Re-throw with detailed error information
|
|
throw new HttpException(
|
|
`[${errorSource}] ${errorMessage}`,
|
|
er.response?.status || HttpStatus.INTERNAL_SERVER_ERROR,
|
|
);
|
|
}
|
|
}
|
|
}
|