All files / web/src/app/api/vision-training/models-summary route.ts

0% Statements 0/126
0% Branches 0/1
0% Functions 0/1
0% Lines 0/126

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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 126 127                                                                                                                                                                                                                                                             
import fs from 'fs'
import path from 'path'
import { withAuth } from '@/lib/auth/withAuth'

// Force dynamic rendering - this route reads from disk which changes at runtime
export const dynamic = 'force-dynamic'

// Data directories for each model type
const COLUMN_CLASSIFIER_DIR = path.join(process.cwd(), 'data/vision-training/collected')
const BOUNDARY_DETECTOR_DIR = path.join(process.cwd(), 'data/vision-training/boundary-frames')

type DataQuality = 'none' | 'insufficient' | 'minimal' | 'good' | 'excellent'

interface ModelSummary {
  totalImages?: number
  totalFrames?: number
  hasData: boolean
  dataQuality: DataQuality
}

interface ModelsSummaryResponse {
  columnClassifier: ModelSummary & { totalImages: number }
  boundaryDetector: ModelSummary & { totalFrames: number }
}

/**
 * Calculate data quality based on count and minimum requirements
 */
function calculateColumnClassifierQuality(totalImages: number, digitCounts: number[]): DataQuality {
  if (totalImages === 0) return 'none'

  const minCount = Math.min(...digitCounts)
  const avgCount = totalImages / 10

  if (totalImages < 50 || minCount < 3) return 'insufficient'
  if (totalImages < 200 || minCount < 10) return 'minimal'
  if (totalImages < 500 || avgCount < 40) return 'good'
  return 'excellent'
}

function calculateBoundaryDetectorQuality(totalFrames: number): DataQuality {
  if (totalFrames === 0) return 'none'
  if (totalFrames < 50) return 'insufficient'
  if (totalFrames < 200) return 'minimal'
  if (totalFrames < 500) return 'good'
  return 'excellent'
}

/**
 * GET /api/vision-training/models-summary
 *
 * Returns a summary of available training data for all model types.
 * Used by the model selection card in the training wizard.
 */
export const GET = withAuth(
  async () => {
    try {
      // --- Column Classifier Summary ---
      let columnTotalImages = 0
      const digitCounts: number[] = []

      if (fs.existsSync(COLUMN_CLASSIFIER_DIR)) {
        for (let d = 0; d <= 9; d++) {
          const digitDir = path.join(COLUMN_CLASSIFIER_DIR, String(d))
          let count = 0

          if (fs.existsSync(digitDir)) {
            const files = fs.readdirSync(digitDir).filter((f) => /\.(png|jpg|jpeg|webp)$/i.test(f))
            count = files.length
          }

          digitCounts.push(count)
          columnTotalImages += count
        }
      } else {
        // Initialize with zeros for all digits
        for (let i = 0; i < 10; i++) {
          digitCounts.push(0)
        }
      }

      const columnClassifier: ModelsSummaryResponse['columnClassifier'] = {
        totalImages: columnTotalImages,
        hasData: columnTotalImages > 0,
        dataQuality: calculateColumnClassifierQuality(columnTotalImages, digitCounts),
      }

      // --- Boundary Detector Summary ---
      let boundaryTotalFrames = 0

      if (fs.existsSync(BOUNDARY_DETECTOR_DIR)) {
        // Count all frame images (organized by device subdirectories)
        const entries = fs.readdirSync(BOUNDARY_DETECTOR_DIR, {
          withFileTypes: true,
        })

        for (const entry of entries) {
          if (entry.isDirectory()) {
            // Device subdirectory
            const deviceDir = path.join(BOUNDARY_DETECTOR_DIR, entry.name)
            const files = fs.readdirSync(deviceDir).filter((f) => /\.(png|jpg|jpeg|webp)$/i.test(f))
            boundaryTotalFrames += files.length
          } else if (/\.(png|jpg|jpeg|webp)$/i.test(entry.name)) {
            // Direct file in boundary-frames directory
            boundaryTotalFrames++
          }
        }
      }

      const boundaryDetector: ModelsSummaryResponse['boundaryDetector'] = {
        totalFrames: boundaryTotalFrames,
        hasData: boundaryTotalFrames > 0,
        dataQuality: calculateBoundaryDetectorQuality(boundaryTotalFrames),
      }

      return Response.json({
        columnClassifier,
        boundaryDetector,
      } satisfies ModelsSummaryResponse)
    } catch (error) {
      console.error('[vision-training/models-summary] Error:', error)
      return Response.json({ error: 'Failed to read model summaries' }, { status: 500 })
    }
  },
  { role: 'admin' }
)