mirror of
https://github.com/OpenCut-app/OpenCut.git
synced 2026-07-13 21:52:53 +02:00
190 lines
5.5 KiB
TypeScript
190 lines
5.5 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { z } from "zod";
|
|
import { env } from "@/env";
|
|
import { baseRateLimit } from "@/lib/rate-limit";
|
|
import { isTranscriptionConfigured } from "@/lib/transcription-utils";
|
|
|
|
const transcribeRequestSchema = z.object({
|
|
filename: z.string().min(1, "Filename is required"),
|
|
language: z.string().optional().default("auto"),
|
|
decryptionKey: z.string().min(1, "Decryption key is required").optional(),
|
|
iv: z.string().min(1, "IV is required").optional(),
|
|
});
|
|
|
|
const modalResponseSchema = z.object({
|
|
text: z.string(),
|
|
segments: z.array(
|
|
z.object({
|
|
id: z.number(),
|
|
seek: z.number(),
|
|
start: z.number(),
|
|
end: z.number(),
|
|
text: z.string(),
|
|
tokens: z.array(z.number()),
|
|
temperature: z.number(),
|
|
avg_logprob: z.number(),
|
|
compression_ratio: z.number(),
|
|
no_speech_prob: z.number(),
|
|
})
|
|
),
|
|
language: z.string(),
|
|
});
|
|
|
|
const apiResponseSchema = z.object({
|
|
text: z.string(),
|
|
segments: z.array(
|
|
z.object({
|
|
id: z.number(),
|
|
seek: z.number(),
|
|
start: z.number(),
|
|
end: z.number(),
|
|
text: z.string(),
|
|
tokens: z.array(z.number()),
|
|
temperature: z.number(),
|
|
avg_logprob: z.number(),
|
|
compression_ratio: z.number(),
|
|
no_speech_prob: z.number(),
|
|
})
|
|
),
|
|
language: z.string(),
|
|
});
|
|
|
|
export async function POST(request: NextRequest) {
|
|
try {
|
|
// Rate limiting
|
|
const ip = request.headers.get("x-forwarded-for") ?? "anonymous";
|
|
const { success } = await baseRateLimit.limit(ip);
|
|
const origin = request.headers.get("origin");
|
|
|
|
if (!success) {
|
|
return NextResponse.json({ error: "Too many requests" }, { status: 429 });
|
|
}
|
|
|
|
// Check transcription configuration
|
|
const transcriptionCheck = isTranscriptionConfigured();
|
|
if (!transcriptionCheck.configured) {
|
|
console.error(
|
|
"Missing environment variables:",
|
|
JSON.stringify(transcriptionCheck.missingVars)
|
|
);
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: "Transcription not configured",
|
|
message: `Auto-captions require environment variables: ${transcriptionCheck.missingVars.join(", ")}. Check README for setup instructions.`,
|
|
},
|
|
{ status: 503 }
|
|
);
|
|
}
|
|
|
|
// Parse and validate request body
|
|
const rawBody = await request.json().catch(() => null);
|
|
if (!rawBody) {
|
|
return NextResponse.json(
|
|
{ error: "Invalid JSON in request body" },
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const validationResult = transcribeRequestSchema.safeParse(rawBody);
|
|
if (!validationResult.success) {
|
|
return NextResponse.json(
|
|
{
|
|
error: "Invalid request parameters",
|
|
details: validationResult.error.flatten().fieldErrors,
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
const { filename, language, decryptionKey, iv } = validationResult.data;
|
|
|
|
// Prepare request body for Modal
|
|
const modalRequestBody: any = {
|
|
filename,
|
|
language,
|
|
};
|
|
|
|
// Add encryption parameters if provided (zero-knowledge)
|
|
if (decryptionKey && iv) {
|
|
modalRequestBody.decryptionKey = decryptionKey;
|
|
modalRequestBody.iv = iv;
|
|
}
|
|
|
|
// Call Modal transcription service
|
|
const response = await fetch(env.MODAL_TRANSCRIPTION_URL, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(modalRequestBody),
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
console.error("Modal API error:", response.status, errorText);
|
|
|
|
let errorMessage = "Transcription service unavailable";
|
|
try {
|
|
const errorData = JSON.parse(errorText);
|
|
errorMessage = errorData.error || errorMessage;
|
|
} catch {
|
|
// Use default message if parsing fails
|
|
}
|
|
|
|
return NextResponse.json(
|
|
{
|
|
error: errorMessage,
|
|
message: "Failed to process transcription request",
|
|
},
|
|
{ status: response.status >= 500 ? 502 : response.status }
|
|
);
|
|
}
|
|
|
|
const rawResult = await response.json();
|
|
console.log("Raw Modal response:", JSON.stringify(rawResult, null, 2));
|
|
|
|
// Validate Modal response
|
|
const modalValidation = modalResponseSchema.safeParse(rawResult);
|
|
if (!modalValidation.success) {
|
|
console.error("Invalid Modal API response:", modalValidation.error);
|
|
return NextResponse.json(
|
|
{ error: "Invalid response from transcription service" },
|
|
{ status: 502 }
|
|
);
|
|
}
|
|
|
|
const result = modalValidation.data;
|
|
|
|
// Prepare and validate API response
|
|
const responseData = {
|
|
text: result.text,
|
|
segments: result.segments,
|
|
language: result.language,
|
|
};
|
|
|
|
const responseValidation = apiResponseSchema.safeParse(responseData);
|
|
if (!responseValidation.success) {
|
|
console.error(
|
|
"Invalid API response structure:",
|
|
responseValidation.error
|
|
);
|
|
return NextResponse.json(
|
|
{ error: "Internal response formatting error" },
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
|
|
return NextResponse.json(responseValidation.data);
|
|
} catch (error) {
|
|
console.error("Transcription API error:", error);
|
|
return NextResponse.json(
|
|
{
|
|
error: "Internal server error",
|
|
message: "An unexpected error occurred during transcription",
|
|
},
|
|
{ status: 500 }
|
|
);
|
|
}
|
|
}
|