feat: add OCR endpoint using OpenRouter Sonoma Sky Alpha model

This commit is contained in:
2025-09-10 17:50:47 -03:00
parent 322c84b389
commit da25ea9b03
18 changed files with 1358 additions and 316 deletions

View File

@@ -93,12 +93,8 @@
"name": "account_user_id_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -168,12 +164,8 @@
"name": "session_user_id_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": [
"user_id"
],
"columnsTo": [
"id"
],
"columnsFrom": ["user_id"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
@@ -183,9 +175,7 @@
"session_token_unique": {
"name": "session_token_unique",
"nullsNotDistinct": false,
"columns": [
"token"
]
"columns": ["token"]
}
},
"policies": {},
@@ -246,9 +236,7 @@
"user_email_unique": {
"name": "user_email_unique",
"nullsNotDistinct": false,
"columns": [
"email"
]
"columns": ["email"]
}
},
"policies": {},
@@ -316,4 +304,4 @@
"schemas": {},
"tables": {}
}
}
}

View File

@@ -10,4 +10,4 @@
"breakpoints": true
}
]
}
}

View File

@@ -3,6 +3,7 @@ import { execSync } from "node:child_process";
import { join } from "node:path";
import { trpcServer } from "@hono/trpc-server";
import { Hono } from "hono";
import { env } from "hono/adapter";
import { cors } from "hono/cors";
import { logger as honoLogger } from "hono/logger";
import { createContext } from "./lib/context";
@@ -44,6 +45,89 @@ app.use(
})
);
// Utilities for OpenRouter
async function openRouterChat(
apiKey: string,
body: unknown,
extra?: { referer?: string; title?: string }
) {
const headers: Record<string, string> = {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
};
if (extra?.referer) headers["HTTP-Referer"] = extra.referer;
if (extra?.title) headers["X-Title"] = extra.title;
const res = await fetch("https://openrouter.ai/api/v1/chat/completions", {
method: "POST",
headers,
body: JSON.stringify(body),
});
return res;
}
function arrayBufferToBase64(buf: ArrayBuffer) {
const bytes = new Uint8Array(buf);
let binary = "";
for (let i = 0; i < bytes.length; i++)
binary += String.fromCharCode(bytes[i]);
return Buffer.from(binary, "binary").toString("base64");
}
// OCR via OpenRouter Sonoma Sky Alpha
app.post("/ai/ocr", async (c) => {
const { OPENROUTER_API_KEY, OPENROUTER_SITE_URL, OPENROUTER_SITE_NAME, MOCK } =
env<{
OPENROUTER_API_KEY?: string;
OPENROUTER_SITE_URL?: string;
OPENROUTER_SITE_NAME?: string;
MOCK?: string;
}>(c);
if (!OPENROUTER_API_KEY) {
return c.json({ error: "Missing OPENROUTER_API_KEY" }, 500);
}
const blob = await c.req.blob();
const contentType = c.req.header("content-type") || "image/png";
const base64 = arrayBufferToBase64(await blob.arrayBuffer());
const dataUrl = `data:${contentType};base64,${base64}`;
const body = {
model: "openrouter/sonoma-sky-alpha",
messages: [
{
role: "system",
content:
"You are an OCR engine. Extract all text visible in the provided image. Respond with plain text only, preserving line breaks and reading order. Do not add commentary.",
},
{
role: "user",
content: [
{
type: "text",
text: "Extract text from this image and return only the text.",
},
{ type: "image_url", image_url: { url: dataUrl } },
],
},
],
};
const res = await openRouterChat(OPENROUTER_API_KEY as string, body, {
referer: OPENROUTER_SITE_URL,
title: OPENROUTER_SITE_NAME,
});
if (!res.ok) {
const err = await res.text();
return c.json(
{ error: `OpenRouter error ${res.status}`, details: err },
502
);
}
const json = (await res.json()) as any;
const text: string = json?.choices?.[0]?.message?.content ?? "";
return c.json({ text });
});
app.get("/", (c) => {
return c.text("OK");
});