Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
*.tsbuildinfo
.idea
26 changes: 14 additions & 12 deletions apps/web/src/app/api/transcription/final/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { NextRequest } from "next/server"
import { toPipelineError } from "@pipeline-errors"
import { parseWavHeader, resolveTranscriptionProvider, transcribeWithResolvedProvider } from "@transcription"
import { transcriptionSessionStore } from "@transcript-assembly"
import { writeAuditEntry } from "@storage/audit-log"

export const runtime = "nodejs"

function jsonError(status: number, code: string, message: string) {
return new Response(JSON.stringify({ error: { code, message } }), {
function jsonError(status: number, code: string, message: string, recoverable: boolean) {
return new Response(JSON.stringify({ error: { code, message, recoverable } }), {
status,
headers: { "Content-Type": "application/json" },
})
Expand All @@ -19,7 +20,7 @@ export async function POST(req: NextRequest) {
const file = formData.get("file")

if (typeof sessionId !== "string" || !(file instanceof Blob)) {
return jsonError(400, "validation_error", "Missing session_id or file")
return jsonError(400, "validation_error", "Missing session_id or file", false)
}

transcriptionSessionStore.setStatus(sessionId, "finalizing")
Expand All @@ -29,11 +30,11 @@ export async function POST(req: NextRequest) {
try {
wavInfo = parseWavHeader(arrayBuffer)
} catch (error) {
return jsonError(400, "validation_error", error instanceof Error ? error.message : "Invalid WAV file")
return jsonError(400, "validation_error", error instanceof Error ? error.message : "Invalid WAV file", true)
}

if (wavInfo.sampleRate !== 16000 || wavInfo.numChannels !== 1 || wavInfo.bitDepth !== 16) {
return jsonError(400, "validation_error", "Final recording must be 16kHz mono 16-bit PCM WAV")
return jsonError(400, "validation_error", "Final recording must be 16kHz mono 16-bit PCM WAV", true)
}

try {
Expand Down Expand Up @@ -67,11 +68,12 @@ export async function POST(req: NextRequest) {
} catch (error) {
console.error("Final transcription failed", error)
const resolvedProvider = resolveTranscriptionProvider()
transcriptionSessionStore.emitError(
sessionId,
"api_error",
error instanceof Error ? error.message : "Transcription API failure",
)
const pipelineError = toPipelineError(error, {
code: "api_error",
message: "Transcription API failure",
recoverable: true,
})
transcriptionSessionStore.emitError(sessionId, pipelineError)

// Audit log: final transcription failed
await writeAuditEntry({
Expand All @@ -85,10 +87,10 @@ export async function POST(req: NextRequest) {
},
})

return jsonError(502, "api_error", "Transcription API failed")
return jsonError(502, pipelineError.code, pipelineError.message, pipelineError.recoverable)
}
} catch (error) {
console.error("Final recording ingestion failed", error)
return jsonError(500, "storage_error", "Failed to process final recording")
return jsonError(500, "storage_error", "Failed to process final recording", false)
}
}
28 changes: 15 additions & 13 deletions apps/web/src/app/api/transcription/segment/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import type { NextRequest } from "next/server"
import { toPipelineError } from "@pipeline-errors"
import { parseWavHeader, resolveTranscriptionProvider, transcribeWithResolvedProvider } from "@transcription"
import { transcriptionSessionStore } from "@transcript-assembly"
import { writeAuditEntry } from "@storage/audit-log"

export const runtime = "nodejs"

function jsonError(status: number, code: string, message: string) {
return new Response(JSON.stringify({ error: { code, message } }), {
function jsonError(status: number, code: string, message: string, recoverable: boolean) {
return new Response(JSON.stringify({ error: { code, message, recoverable } }), {
status,
headers: { "Content-Type": "application/json" },
})
Expand All @@ -32,23 +33,23 @@ export async function POST(req: NextRequest) {
Number.isNaN(overlapMs) ||
!(file instanceof Blob)
) {
return jsonError(400, "validation_error", "Missing required metadata or file")
return jsonError(400, "validation_error", "Missing required metadata or file", false)
}

const arrayBuffer = await file.arrayBuffer()
let wavInfo
try {
wavInfo = parseWavHeader(arrayBuffer)
} catch (error) {
return jsonError(400, "validation_error", error instanceof Error ? error.message : "Invalid WAV file")
return jsonError(400, "validation_error", error instanceof Error ? error.message : "Invalid WAV file", true)
}

if (wavInfo.sampleRate !== 16000 || wavInfo.numChannels !== 1 || wavInfo.bitDepth !== 16) {
return jsonError(400, "validation_error", "Segments must be 16kHz mono 16-bit PCM WAV")
return jsonError(400, "validation_error", "Segments must be 16kHz mono 16-bit PCM WAV", true)
}

if (wavInfo.durationMs < 8000 || wavInfo.durationMs > 12000) {
return jsonError(400, "validation_error", "Segment duration must be between 8s and 12s")
return jsonError(400, "validation_error", "Segment duration must be between 8s and 12s", true)
}

try {
Expand Down Expand Up @@ -85,11 +86,12 @@ export async function POST(req: NextRequest) {
} catch (error) {
console.error("Segment transcription failed", error)
const resolvedProvider = resolveTranscriptionProvider()
transcriptionSessionStore.emitError(
sessionId,
"api_error",
error instanceof Error ? error.message : "Transcription API failure",
)
const pipelineError = toPipelineError(error, {
code: "api_error",
message: "Transcription API failure",
recoverable: true,
})
transcriptionSessionStore.emitError(sessionId, pipelineError)

// Audit log: segment transcription failed
await writeAuditEntry({
Expand All @@ -104,10 +106,10 @@ export async function POST(req: NextRequest) {
},
})

return jsonError(502, "api_error", "Transcription API failed")
return jsonError(502, pipelineError.code, pipelineError.message, pipelineError.recoverable)
}
} catch (error) {
console.error("Segment ingestion failed", error)
return jsonError(500, "storage_error", "Failed to process audio segment")
return jsonError(500, "storage_error", "Failed to process audio segment", false)
}
}
Loading