179 lines
4.8 KiB
TypeScript
179 lines
4.8 KiB
TypeScript
import { $ } from "bun"
|
|
import { Hono } from "hono"
|
|
import { openai, createFile } from "./src/openai"
|
|
import { serveStatic } from "hono/bun"
|
|
import fs from "fs"
|
|
|
|
|
|
const PROMPT = `
|
|
This image contains a corkboard with index cards on it - let's focus on that.
|
|
|
|
Please return the text content of each index card followed by a single line break, and nothing else.
|
|
|
|
Some humans write in ALL CAPS. Keep technical acronyms (DB, AI, HTML) in ALL CAPS, but convert everything else to Titlecase, Please.
|
|
|
|
Once you have gathered all the text, please scan it to make sure it looks like a human wrote it.
|
|
|
|
eg "It 's Steve Y'all !" => "It's Steve Y'all!"
|
|
`
|
|
const CAMERA = process.env.IAGO_CAMERA || "Set IAGO_CAMERA environment variable"
|
|
const IMAGE_PATH = "/tmp/iago.jpg"
|
|
const app = new Hono()
|
|
|
|
// Serve static files from public directory
|
|
app.use("/*", serveStatic({ root: "./public" }))
|
|
|
|
app.get("/", (c) => {
|
|
return c.json({ message: "Hello World" })
|
|
})
|
|
|
|
// you know
|
|
app.get("/status", async (c) => {
|
|
if (!(await checkImagesnap()))
|
|
return c.json({ status: "imagesnap not found", devices: [], camera: CAMERA })
|
|
|
|
const devices = await getDevices()
|
|
return c.json({ devices, camera: CAMERA, status: devices.includes(CAMERA) ? "camera found" : "camera not found" })
|
|
})
|
|
|
|
// take a picture with the camera
|
|
app.get("/capture", async (c) => {
|
|
try {
|
|
await runImagesnap()
|
|
const image = await Bun.file(IMAGE_PATH).arrayBuffer()
|
|
return new Response(image, {
|
|
headers: { "Content-Type": "image/jpeg" },
|
|
})
|
|
} catch (err: any) {
|
|
return c.json({ error: err.message }, 500)
|
|
}
|
|
})
|
|
|
|
// return the last captured image
|
|
app.get("/last.jpg", async (c) => {
|
|
const image = await Bun.file(IMAGE_PATH).arrayBuffer()
|
|
return new Response(image, {
|
|
headers: { "Content-Type": "image/jpeg" },
|
|
})
|
|
})
|
|
|
|
// capture and analyze image
|
|
app.get("/analyze", async (c) => {
|
|
try {
|
|
return c.json({ result: await analyze() })
|
|
} catch (err: any) {
|
|
return c.json({ error: err.message }, 500)
|
|
}
|
|
})
|
|
|
|
|
|
// capture and analyze image, return HTML
|
|
app.get("/analyze.html", async (c) => {
|
|
try {
|
|
const result = await analyze()
|
|
return c.html(`<h2>corkboard</h2><ul>${result.map(line => `<li>${line}</li>`).join("")}</ul>`)
|
|
} catch (err: any) {
|
|
return c.json({ error: err.message }, 500)
|
|
}
|
|
})
|
|
|
|
app.get("/speak", async (c) => {
|
|
const mp3 = await openai.audio.speech.create({
|
|
model: "gpt-4o-mini-tts",
|
|
voice: "ash",
|
|
instructions: "Speak in an upbeat and goofy tone!",
|
|
input: "I put the MAN in MAN-UH!"
|
|
})
|
|
|
|
const buffer = Buffer.from(await mp3.arrayBuffer())
|
|
|
|
return new Response(buffer, {
|
|
headers: {
|
|
"Content-Type": "audio/mpeg",
|
|
"Content-Disposition": "inline; filename=speak.mp3"
|
|
},
|
|
})
|
|
})
|
|
|
|
app.get("/talk", async (c) => {
|
|
return c.redirect("/talk.html")
|
|
})
|
|
|
|
app.post("/transcribe", async (c) => {
|
|
try {
|
|
const formData = await c.req.formData()
|
|
const audioFile = formData.get("audio") as File
|
|
|
|
if (!audioFile) {
|
|
return c.json({ error: "No audio file provided" }, 400)
|
|
}
|
|
|
|
// Convert File to Buffer
|
|
const arrayBuffer = await audioFile.arrayBuffer()
|
|
const buffer = Buffer.from(arrayBuffer)
|
|
|
|
// Create a temporary file
|
|
const tempPath = `./temp_audio_${Date.now()}.webm`
|
|
await Bun.write(tempPath, buffer)
|
|
|
|
const transcription = await openai.audio.transcriptions.create({
|
|
file: Bun.file(tempPath),
|
|
model: "gpt-4o-transcribe",
|
|
response_format: "text",
|
|
})
|
|
|
|
// Clean up temporary file
|
|
await fs.promises.unlink(tempPath)
|
|
|
|
console.log(transcription)
|
|
return c.json({ transcription })
|
|
} catch (err: any) {
|
|
console.error("Transcription error:", err)
|
|
return c.json({ error: err.message }, 500)
|
|
}
|
|
})
|
|
|
|
async function analyze(): Promise<string[]> {
|
|
await runImagesnap()
|
|
|
|
const fileId = await createFile(IMAGE_PATH)
|
|
|
|
const result = await openai.responses.create({
|
|
model: "gpt-4o",
|
|
input: [
|
|
{
|
|
role: "user",
|
|
content: [
|
|
{ type: "input_text", text: PROMPT },
|
|
{ type: "input_image", file_id: fileId, detail: "high" }
|
|
]
|
|
}
|
|
]
|
|
})
|
|
|
|
return result.output_text.split("\n").map(line => line.trim()).filter(line => line)
|
|
}
|
|
|
|
// Capture image using Bun.spawn
|
|
async function runImagesnap(): Promise<void> {
|
|
const proc = await $`imagesnap -d ${CAMERA} ${IMAGE_PATH}`
|
|
if (proc.exitCode !== 0) throw new Error("imagesnap failed")
|
|
}
|
|
|
|
// Check if imagesnap exists using Bun.spawnSync
|
|
async function checkImagesnap(): Promise<boolean> {
|
|
const result = await $`which imagesnap`
|
|
return result.exitCode === 0
|
|
}
|
|
|
|
// Get devices using imagesnap
|
|
async function getDevices(): Promise<string[]> {
|
|
const result = await $`imagesnap -l`
|
|
return result.stdout.toString().split("\n").filter(line => line.startsWith("=> ")).map(line => line.replace("=> ", ""))
|
|
}
|
|
|
|
export default {
|
|
port: 3000,
|
|
fetch: app.fetch,
|
|
idleTimeout: 255
|
|
} |