// worker.js — Cloudflare Workers export default { async fetch(request, env) { const cors = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "Content-Type, Authorization", "Access-Control-Allow-Methods": "POST, OPTIONS, GET", "Vary": "Origin" }; if (request.method === "OPTIONS") return new Response(null, { headers: cors }); const url = new URL(request.url); try { if (url.pathname === "/" || url.pathname === "/health") { return j({ ok: true }, cors); } // --- 音声 -> 文字起こし --- if (url.pathname === "/transcribe" && request.method === "POST") { const form = await request.formData(); const file = form.get("audio"); if (!file) return j({ error: 'no "audio" file' }, cors, 400); const out = new FormData(); out.append("model", "gpt-4o-transcribe"); out.append("file", file, file.name || "chunk.webm"); // 固定するなら: out.append("language", "ja"); // or "en" const r = await fetch("https://api.openai.com/v1/audio/transcriptions", { method: "POST", headers: { Authorization: `Bearer ${env.OPENAI_API_KEY}` }, body: out }); if (!r.ok) { return j({ error: "openai transcribe error", detail: await r.text() }, cors, 502); } const data = await r.json(); return j({ text: data.text || "" }, cors); } // --- 翻訳 + (英語の質問なら)ドラフト --- if (url.pathname === "/translate" && request.method === "POST") { const body = await safeJson(request); const text = (body.text || "").trim(); const direction = body.direction || "auto"; const glossary = Array.isArray(body.glossary) ? body.glossary : []; if (!text) return j({ error: "Missing text" }, cors, 400); // 入力が英語っぽい? const englishish = isEnglish(text); // どっちへ翻訳するかを明示 const target = direction === "ja-en" ? "English" : direction === "en-ja" ? "Japanese" : englishish ? "Japanese" : "English"; // auto のとき const glue = (glossary.map(([a,b]) => `- "${a}" ⇄ "${b}"`).join("\n")) || "- (none)"; const sys = [ `You are a lightning-fast interpreter.`, `Translate the user's message into ${target}.`, `- Output ONLY the translation; no brackets or commentary.`, `- Keep sentences short; crisp business tone.`, `- Enforce the glossary consistently:\n${glue}` ].join("\n"); const tr = await openaiResponses(env.OPENAI_API_KEY, { model: "gpt-4.1-mini", input: [ { role: "system", content: sys }, { role: "user", content: text } ] }); if (tr.error) return j({ error: "openai translate error", detail: tr.error }, cors, 502); const translation = tr.text; // 英語の質問なら 25語以内の英語ドラフトも let draft = ""; if (englishish && looksQuestion(text)) { const dsys = [ `You assist in business meetings.`, `For an ENGLISH question, produce a ONE-SENTENCE, direct ENGLISH reply under 25 words,`, `polite and concrete. If info is missing, add a short clarifying option.`, `Enforce glossary if relevant:\n${glue}` ].join(" "); const dr = await openaiResponses(env.OPENAI_API_KEY, { model: "gpt-4.1-mini", input: [ { role: "system", content: dsys }, { role: "user", content: text } ] }); if (!dr.error) draft = dr.text; } return j({ translation: translation.trim(), draft: draft.trim() }, cors); } return j({ error: "not found" }, cors, 404); } catch (e) { return j({ error: String(e?.message || e) }, cors, 500); } } }; // ---- Helpers ---- function j(data, headers = {}, status = 200) { return new Response(JSON.stringify(data), { status, headers: { "Content-Type": "application/json", ...headers } }); } async function safeJson(req) { const t = await req.text(); try { return JSON.parse(t || "{}"); } catch { return {}; } } function isEnglish(s) { // 記号・数字を除外して英字比率で判定 const letters = (s.match(/[A-Za-z]/g) || []).length; const cjk = (s.match(/[\u3040-\u30ff\u3400-\u9fff]/g) || []).length; return letters > cjk; } function looksQuestion(s) { if (!s) return false; if (/\?\s*$/.test(s)) return true; return /\b(what|why|how|when|where|which|who|whom|whose|can|could|may|might|should|would|do|does|did|is|are|am|was|were|will|shall)\b/i.test(s); } // OpenAI Responses API を堅牢に呼ぶ async function openaiResponses(API_KEY, payload) { try { const resp = await fetch("https://api.openai.com/v1/responses", { method: "POST", headers: { Authorization: `Bearer ${API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify(payload) }); if (!resp.ok) return { error: await resp.text() }; const data = await resp.json(); // 取り出しを頑丈に let text = ""; if (typeof data.output_text === "string") text = data.output_text; else if (Array.isArray(data.output)) { const first = data.output[0]; const piece = first?.content?.[0]?.text || first?.content?.[0]?.string_value; if (typeof piece === "string") text = piece; } else if (Array.isArray(data.choices)) { const msg = data.choices[0]?.message?.content; if (typeof msg === "string") text = msg; } return { text: text || "" }; } catch (e) { return { error: String(e?.message || e) }; } }