621f83c47b
Walk every speaking beat at export time, reuse current scene's beatAudioMap, and synth the rest via BYO TTS or /api/beat-audio with concurrency 4. Show a progress toast on the play page while collecting. Gallery export keeps audio in a sidecar localStorage key so the first paint is not blocked by JSON.parse-ing several MB of base64; the gallery lazy-loads it after the first scene image, then plays per-beat audio with a mute toggle persisted to localStorage. .infiplot share files embed audioByBeatId in the doc itself (v2); on import the data URIs survive scene swaps and feed back into the per-beat audio map so replayers hear the original voices for free. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
47 lines
1.5 KiB
TypeScript
47 lines
1.5 KiB
TypeScript
import { unpackDoc } from "@/lib/galleryCrypto";
|
|
|
|
export const runtime = "nodejs";
|
|
|
|
// Cap a bit above pack's MAX_DOC_BYTES — ciphertext adds the 16-byte GCM tag
|
|
// and the 17-byte header; some slack accommodates near-cap docs without
|
|
// rejecting them at unpack time. Bumped to fit pre-baked beat audio.
|
|
const MAX_FILE_BYTES = 13_000_000;
|
|
|
|
// Decrypt a `.infiplot` share file back to its doc JSON string. Returns the
|
|
// plaintext as a JSON field (not raw bytes) so the client can chain it through
|
|
// JSON.parse without sniffing the response type. Errors are deliberately
|
|
// generic — we don't distinguish "wrong key" from "tampered file" because the
|
|
// distinction would leak server config.
|
|
export async function POST(req: Request): Promise<Response> {
|
|
const secret = process.env.GALLERY_SECRET;
|
|
if (!secret) {
|
|
return Response.json(
|
|
{ error: "图集分享未启用 (GALLERY_SECRET 未配置)" },
|
|
{ status: 503 },
|
|
);
|
|
}
|
|
|
|
let ab: ArrayBuffer;
|
|
try {
|
|
ab = await req.arrayBuffer();
|
|
} catch {
|
|
return Response.json({ error: "Bad request body" }, { status: 400 });
|
|
}
|
|
if (ab.byteLength > MAX_FILE_BYTES) {
|
|
return Response.json({ error: "文件太大" }, { status: 413 });
|
|
}
|
|
if (ab.byteLength === 0) {
|
|
return Response.json({ error: "文件为空" }, { status: 400 });
|
|
}
|
|
|
|
try {
|
|
const docStr = await unpackDoc(new Uint8Array(ab), secret);
|
|
return Response.json({ docStr });
|
|
} catch (e) {
|
|
return Response.json(
|
|
{ error: e instanceof Error ? e.message : "解包失败" },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
}
|