Files
soroban-abacus-flashcards/apps/web/scripts/generateTestWorksheet.ts
Thomas Hallock e2816ae88b feat(vision): improve remote camera calibration UX
- Add dual-stream calibration: phone sends both raw and cropped preview
  frames during calibration so users can see what practice will look like
- Add "Adjust" button to modify existing manual calibration without
  resetting to auto-detection first
- Hide calibration quad editor overlay when not in calibration mode
- Fix rotation buttons to update cropped preview immediately
- Add rate limiting (10fps) for cropped preview frames during calibration
- Fix multiple bugs preventing dual-stream mode from working:
  - Don't mark calibration as complete during preview mode
  - Don't stop detection loop when receiving preview calibration
  - Sync refs properly in frame mode change effects

Also includes accumulated formatting and cleanup changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:51:59 -06:00

127 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env tsx
/**
* Generate a test worksheet image for grading tests
*
* Usage:
* npx tsx scripts/generateTestWorksheet.ts
*
* Creates: data/uploads/test-worksheet.png
*
* This generates a simple addition worksheet image that can be used
* to test the GPT-5 grading pipeline without needing a real photo.
*/
import { createCanvas } from 'canvas'
import { writeFileSync, mkdirSync } from 'fs'
import { join } from 'path'
function generateTestWorksheet() {
// Create canvas (8.5" x 11" at 150 DPI)
const width = 1275
const height = 1650
const canvas = createCanvas(width, height)
const ctx = canvas.getContext('2d')
// White background
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, width, height)
// Title
ctx.fillStyle = '#000000'
ctx.font = 'bold 48px Arial'
ctx.fillText('Addition Practice Worksheet', 100, 100)
// Generate 20 problems (4 rows × 5 columns)
const problems = [
{ a: 45, b: 27, answer: 72, studentAnswer: 72 }, // Correct
{ a: 68, b: 45, answer: 113, studentAnswer: 103 }, // Incorrect (forgot to carry)
{ a: 23, b: 56, answer: 79, studentAnswer: 79 }, // Correct
{ a: 89, b: 34, answer: 123, studentAnswer: 123 }, // Correct
{ a: 57, b: 66, answer: 123, studentAnswer: 113 }, // Incorrect
{ a: 38, b: 47, answer: 85, studentAnswer: 85 }, // Correct
{ a: 74, b: 58, answer: 132, studentAnswer: 132 }, // Correct
{ a: 29, b: 83, answer: 112, studentAnswer: 102 }, // Incorrect
{ a: 91, b: 19, answer: 110, studentAnswer: 110 }, // Correct
{ a: 46, b: 78, answer: 124, studentAnswer: 124 }, // Correct
{ a: 63, b: 59, answer: 122, studentAnswer: 112 }, // Incorrect
{ a: 85, b: 27, answer: 112, studentAnswer: 112 }, // Correct
{ a: 34, b: 88, answer: 122, studentAnswer: 122 }, // Correct
{ a: 77, b: 65, answer: 142, studentAnswer: 132 }, // Incorrect
{ a: 52, b: 49, answer: 101, studentAnswer: 101 }, // Correct
{ a: 96, b: 37, answer: 133, studentAnswer: 133 }, // Correct
{ a: 41, b: 69, answer: 110, studentAnswer: 100 }, // Incorrect
{ a: 73, b: 58, answer: 131, studentAnswer: 131 }, // Correct
{ a: 28, b: 94, answer: 122, studentAnswer: 122 }, // Correct
{ a: 87, b: 76, answer: 163, studentAnswer: 153 }, // Incorrect
]
// Draw problems in grid
const startY = 200
const problemWidth = 240
const problemHeight = 280
const cols = 5
const rows = 4
problems.forEach((problem, index) => {
const col = index % cols
const row = Math.floor(index / cols)
const x = 80 + col * problemWidth
const y = startY + row * problemHeight
// Problem number
ctx.font = 'bold 20px Arial'
ctx.fillText(`${index + 1}.`, x, y)
// Draw problem in column format
ctx.font = '32px Arial'
const aStr = problem.a.toString().padStart(3, ' ')
const bStr = `+ ${problem.b.toString()}`
ctx.fillText(aStr, x + 40, y + 40)
ctx.fillText(bStr, x + 40, y + 80)
// Draw line
ctx.strokeStyle = '#000000'
ctx.lineWidth = 2
ctx.beginPath()
ctx.moveTo(x + 40, y + 90)
ctx.lineTo(x + 180, y + 90)
ctx.stroke()
// Student's answer (simulate handwriting with slight variation)
ctx.font = 'italic 32px Arial'
const answerStr = problem.studentAnswer.toString()
const xOffset = x + 40 + (3 - answerStr.length) * 20 // Right-align
ctx.fillText(answerStr, xOffset, y + 130)
})
// Footer
ctx.font = '18px Arial'
ctx.fillText('Score: 13/20 (65%) - Practice carrying in tens place', 100, height - 100)
// Save to file
const uploadDir = join(process.cwd(), 'data', 'uploads')
mkdirSync(uploadDir, { recursive: true })
const outputPath = join(uploadDir, 'test-worksheet.png')
const buffer = canvas.toBuffer('image/png')
writeFileSync(outputPath, buffer)
console.log(`✅ Test worksheet generated: ${outputPath}`)
console.log(` 13/20 correct (65%)`)
console.log(` 7 errors (mostly carrying mistakes)`)
}
// Check if canvas is available
try {
generateTestWorksheet()
} catch (error) {
if (error instanceof Error && error.message.includes('canvas')) {
console.error('❌ Canvas library not installed.')
console.error(' This is optional - you can use a real worksheet photo instead.')
console.error(' To install: npm install canvas')
} else {
throw error
}
}