From 1b422ba1c2f47bab8231f24b8fed347fe63c89d8 Mon Sep 17 00:00:00 2001
From: Z8MB1E
Date: Mon, 7 Sep 2026 04:37:08 -0400
Subject: [PATCH] feat(voiceover): collect take descriptions and surface
reviewer feedback
Submitters can attach an optional 300-char note to each take, and their own takes list now shows the description plus the reviewer feedback that was previously admin-only.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)
Co-authored-by: Sisyphus
---
src/app/(frontend)/voiceover/actions.ts | 9 +++++++++
.../frontend/voiceover/VoiceCapture.tsx | 20 +++++++++++++++++--
.../frontend/voiceover/VoiceoverRecorder.tsx | 6 +++++-
.../frontend/voiceover/VoiceoverTakes.tsx | 18 ++++++++++++++++-
src/lib/voiceover/index.ts | 4 ++++
5 files changed, 53 insertions(+), 4 deletions(-)
diff --git a/src/app/(frontend)/voiceover/actions.ts b/src/app/(frontend)/voiceover/actions.ts
index 070aed8..620d215 100644
--- a/src/app/(frontend)/voiceover/actions.ts
+++ b/src/app/(frontend)/voiceover/actions.ts
@@ -21,6 +21,8 @@ const MAX_AUDIO_BYTES = 9 * 1024 * 1024;
const MAX_TAKES_PER_SCRIPT = 5;
+const MAX_DESCRIPTION_CHARS = 300;
+
async function authenticate() {
const payloadConfig = await config;
const payload = await getPayload({ config: payloadConfig });
@@ -99,6 +101,12 @@ export async function submitVoiceRecording(
};
}
+ const rawDescription = formData.get("description");
+ const description =
+ typeof rawDescription === "string" && rawDescription.trim().length > 0
+ ? rawDescription.trim().slice(0, MAX_DESCRIPTION_CHARS)
+ : null;
+
const file = formData.get("file");
if (!(file instanceof File)) {
return { success: false, error: "No audio file provided." };
@@ -119,6 +127,7 @@ export async function submitVoiceRecording(
script: scriptId,
user: user.id,
status: "pending",
+ description,
},
file: {
data: Buffer.from(await file.arrayBuffer()),
diff --git a/src/components/frontend/voiceover/VoiceCapture.tsx b/src/components/frontend/voiceover/VoiceCapture.tsx
index 28effd9..a03a323 100644
--- a/src/components/frontend/voiceover/VoiceCapture.tsx
+++ b/src/components/frontend/voiceover/VoiceCapture.tsx
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
const MAX_AUDIO_BYTES = 9 * 1024 * 1024;
@@ -16,7 +17,7 @@ const PREFERRED_MIME_TYPE = "audio/webm;codecs=opus";
export type VoiceCaptureSource = Blob | File;
interface VoiceCaptureProps {
- readonly onSubmitTake: (source: VoiceCaptureSource) => Promise;
+ readonly onSubmitTake: (source: VoiceCaptureSource, description: string) => Promise;
}
function formatDuration(seconds: number): string {
@@ -56,6 +57,7 @@ export function VoiceCapture({ onSubmitTake }: VoiceCaptureProps) {
const [reviewBlob, setReviewBlob] = useState(null);
const [reviewUrl, setReviewUrl] = useState(null);
const [selectedFile, setSelectedFile] = useState(null);
+ const [description, setDescription] = useState("");
const stopLevelMeter = useCallback(() => {
if (meterAnimationRef.current !== null) {
@@ -221,12 +223,13 @@ export function VoiceCapture({ onSubmitTake }: VoiceCaptureProps) {
}
setIsSubmitting(true);
- const submitted = await onSubmitTake(source);
+ const submitted = await onSubmitTake(source, description.trim());
setIsSubmitting(false);
if (!submitted) return;
clearReview();
setSelectedFile(null);
+ setDescription("");
if (fileInputRef.current) fileInputRef.current.value = "";
}
@@ -277,6 +280,19 @@ export function VoiceCapture({ onSubmitTake }: VoiceCaptureProps) {
+
+
+
+
{support === "unsupported" && (
{
+ async function handleSubmitTake(
+ source: VoiceCaptureSource,
+ description: string,
+ ): Promise {
const formData = new FormData();
if (source instanceof File) {
formData.append("file", source, source.name);
} else {
formData.append("file", source, "take.webm");
}
+ if (description) formData.append("description", description);
try {
const result = await submitVoiceRecording(script.id, formData);
diff --git a/src/components/frontend/voiceover/VoiceoverTakes.tsx b/src/components/frontend/voiceover/VoiceoverTakes.tsx
index 5121eb9..b563550 100644
--- a/src/components/frontend/voiceover/VoiceoverTakes.tsx
+++ b/src/components/frontend/voiceover/VoiceoverTakes.tsx
@@ -1,7 +1,7 @@
"use client";
import { useState } from "react";
-import { CheckIcon, Clock3Icon, Trash2Icon, XIcon } from "lucide-react";
+import { CheckIcon, Clock3Icon, MessageSquareQuoteIcon, Trash2Icon, XIcon } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -81,6 +81,10 @@ export function VoiceoverTakes({ submissions, onWithdraw }: VoiceoverTakesProps)
+ {submission.description && (
+ {submission.description}
+ )}
+
{submission.audioUrl ? (
)}
+ {submission.reviewNote && (
+
+
+
+
+ Reviewer feedback
+
+
{submission.reviewNote}
+
+
+ )}
+
{canWithdraw && (