vision-pipeline-cost-aware.service.ts 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. import { Injectable, Logger } from '@nestjs/common';
  2. import { ConfigService } from '@nestjs/config';
  3. import * as fs from 'fs/promises';
  4. import * as path from 'path';
  5. import { LibreOfficeService } from '../libreoffice/libreoffice.service';
  6. import { Pdf2ImageService } from '../pdf2image/pdf2image.service';
  7. import { VisionService } from '../vision/vision.service';
  8. import { ElasticsearchService } from '../elasticsearch/elasticsearch.service';
  9. import { ModelConfigService } from '../model-config/model-config.service';
  10. import { PreciseModeOptions, PipelineResult, ProcessingStatus, ModeRecommendation } from './vision-pipeline.interface';
  11. import { VisionModelConfig, VisionAnalysisResult } from '../vision/vision.interface';
  12. import { CostControlService } from './cost-control.service';
  13. @Injectable()
  14. export class VisionPipelineCostAwareService {
  15. private readonly logger = new Logger(VisionPipelineCostAwareService.name);
  16. constructor(
  17. private libreOffice: LibreOfficeService,
  18. private pdf2Image: Pdf2ImageService,
  19. private vision: VisionService,
  20. private elasticsearch: ElasticsearchService,
  21. private modelConfigService: ModelConfigService,
  22. private configService: ConfigService,
  23. private costControl: CostControlService,
  24. ) { }
  25. async processPreciseMode(
  26. filePath: string,
  27. options: PreciseModeOptions,
  28. ): Promise<PipelineResult> {
  29. const startTime = Date.now();
  30. const results: VisionAnalysisResult[] = [];
  31. let processedPages = 0;
  32. let failedPages = 0;
  33. let totalCost = 0;
  34. let pdfPath = filePath;
  35. let imagesToProcess: any[] = [];
  36. this.logger.log(
  37. `Starting precise mode processing for ${options.fileName} (user: ${options.userId})`
  38. );
  39. try {
  40. this.updateStatus('converting', 10, 'Converting document format...');
  41. pdfPath = await this.convertToPDF(filePath);
  42. this.updateStatus('splitting', 30, 'Converting PDF to image...');
  43. const conversionResult = await this.pdf2Image.convertToImages(pdfPath, {
  44. density: 300,
  45. quality: 85,
  46. format: 'jpeg',
  47. });
  48. if (conversionResult.images.length === 0) {
  49. throw new Error('PDF to image conversion failed. No image was generated');
  50. }
  51. imagesToProcess = options.maxPages
  52. ? conversionResult.images.slice(0, options.maxPages)
  53. : conversionResult.images;
  54. const pageCount = imagesToProcess.length;
  55. this.updateStatus('checking', 40, 'Checking quotas and estimating costs...');
  56. const costEstimate = this.costControl.estimateCost(pageCount);
  57. this.logger.log(
  58. `Estimated cost: $${costEstimate.estimatedCost.toFixed(2)}, Estimated time: ${this.costControl.formatTime(costEstimate.estimatedTime)}`
  59. );
  60. const quotaCheck = await this.costControl.checkQuota(
  61. options.userId,
  62. costEstimate.estimatedCost,
  63. );
  64. if (!quotaCheck.allowed) {
  65. throw new Error(quotaCheck.reason);
  66. }
  67. const warning = await this.costControl.checkWarningThreshold(options.userId);
  68. if (warning.shouldWarn) {
  69. this.logger.warn(warning.message);
  70. }
  71. const modelConfig = await this.getVisionModelConfig(options.userId, options.modelId, options.tenantId);
  72. this.updateStatus('analyzing', 50, 'Analyzing the page using the vision model...');
  73. const batchResult = await this.vision.batchAnalyze(
  74. imagesToProcess.map(img => img.path),
  75. modelConfig,
  76. {
  77. startIndex: 1,
  78. skipQualityCheck: options.skipQualityCheck,
  79. }
  80. );
  81. totalCost = batchResult.estimatedCost;
  82. processedPages = batchResult.successCount;
  83. failedPages = batchResult.failedCount;
  84. results.push(...batchResult.results);
  85. if (totalCost > 0) {
  86. await this.costControl.deductQuota(options.userId, totalCost);
  87. this.logger.log(`Actual deducted cost: $${totalCost.toFixed(2)}`);
  88. }
  89. this.updateStatus('completed', 100, 'Processing completed. Cleaning up temporary files...');
  90. await this.pdf2Image.cleanupImages(imagesToProcess);
  91. if (pdfPath !== filePath) {
  92. try {
  93. await fs.unlink(pdfPath);
  94. } catch (error) {
  95. this.logger.warn(`Failed to cleanup converted PDF: ${error.message}`);
  96. }
  97. }
  98. const duration = (Date.now() - startTime) / 1000;
  99. this.logger.log(
  100. `Precise mode completed: ${processedPages} pages processed, ` +
  101. `cost: $${totalCost.toFixed(2)}, duration: ${duration.toFixed(1)}s`
  102. );
  103. return {
  104. success: true,
  105. fileId: options.fileId,
  106. fileName: options.fileName,
  107. totalPages: conversionResult.totalPages,
  108. processedPages,
  109. failedPages,
  110. results,
  111. cost: totalCost,
  112. duration,
  113. mode: 'precise',
  114. };
  115. } catch (error) {
  116. this.logger.error(`Precise mode failed: ${error.message}`);
  117. try {
  118. if (pdfPath !== filePath && pdfPath !== filePath) {
  119. await fs.unlink(pdfPath);
  120. }
  121. if (imagesToProcess.length > 0) {
  122. await this.pdf2Image.cleanupImages(imagesToProcess);
  123. }
  124. } catch { }
  125. return {
  126. success: false,
  127. fileId: options.fileId,
  128. fileName: options.fileName,
  129. totalPages: 0,
  130. processedPages,
  131. failedPages,
  132. results: [],
  133. cost: totalCost,
  134. duration: (Date.now() - startTime) / 1000,
  135. mode: 'precise',
  136. };
  137. }
  138. }
  139. private async getVisionModelConfig(userId: string, modelId: string, tenantId?: string): Promise<VisionModelConfig> {
  140. const config = await this.modelConfigService.findOne(modelId, userId, tenantId || 'default');
  141. if (!config) {
  142. throw new Error(`Model configuration not found: ${modelId}`);
  143. }
  144. // API key is optional - allow local models
  145. return {
  146. baseUrl: config.baseUrl || '',
  147. apiKey: config.apiKey || '',
  148. modelId: config.modelId,
  149. };
  150. }
  151. private async convertToPDF(filePath: string): Promise<string> {
  152. const ext = path.extname(filePath).toLowerCase();
  153. if (ext === '.pdf') {
  154. return filePath;
  155. }
  156. return await this.libreOffice.convertToPDF(filePath);
  157. }
  158. async recommendMode(filePath: string): Promise<ModeRecommendation> {
  159. const ext = path.extname(filePath).toLowerCase();
  160. const stats = await fs.stat(filePath);
  161. const sizeMB = stats.size / (1024 * 1024);
  162. const supportedFormats = ['.pdf', '.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx'];
  163. const preciseFormats = ['.pdf', '.doc', '.docx', '.ppt', '.pptx'];
  164. if (!supportedFormats.includes(ext)) {
  165. return {
  166. recommendedMode: 'fast',
  167. reason: `Unsupported file format: ${ext}`,
  168. warnings: ['Using Fast Mode (text extraction only)'],
  169. };
  170. }
  171. if (!preciseFormats.includes(ext)) {
  172. return {
  173. recommendedMode: 'fast',
  174. reason: `Format ${ext} does not support Precise Mode`,
  175. warnings: ['Using Fast Mode (text extraction only)'],
  176. };
  177. }
  178. const estimatedPages = Math.max(1, Math.ceil(sizeMB * 2));
  179. const costEstimate = this.costControl.estimateCost(estimatedPages);
  180. if (sizeMB > 50) {
  181. return {
  182. recommendedMode: 'precise',
  183. reason: 'Due to large files, Precise Mode is recommended to retain complete information',
  184. estimatedCost: costEstimate.estimatedCost,
  185. estimatedTime: costEstimate.estimatedTime,
  186. warnings: ['Processing time may be longer', 'API charges may apply'],
  187. };
  188. }
  189. return {
  190. recommendedMode: 'precise',
  191. reason: 'Precise Mode is available. Can hold mixed content of text and images',
  192. estimatedCost: costEstimate.estimatedCost,
  193. estimatedTime: costEstimate.estimatedTime,
  194. warnings: ['API charges will apply'],
  195. };
  196. }
  197. async getUserQuotaInfo(userId: string) {
  198. const quota = await this.costControl.getUserQuota(userId);
  199. const report = await this.costControl.getCostReport(userId);
  200. return {
  201. ...quota,
  202. report,
  203. warnings: await this.costControl.checkWarningThreshold(userId),
  204. };
  205. }
  206. private updateStatus(status: ProcessingStatus['status'], progress: number, message: string): void {
  207. this.logger.log(`[${status}] ${progress}% - ${message}`);
  208. }
  209. }