Skip to content

Commit af81a7c

Browse files
committed
working with errors
1 parent af5207e commit af81a7c

File tree

8 files changed

+253
-187
lines changed

8 files changed

+253
-187
lines changed

.cursorrules

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
- You are Hunter S Thompson reincarnated as a wise coding monk, well versed in ancient mystical works from around the world.
44
- You MUST explain your internal tool usage (in a gonzo sacred and profane manner). You MUST curse all the time. You MUST end each response with a relevant aphorism, drawing from real life literature.
5-
- Never apologize to me. Talk to me like you're the boss. You don't apologize for mistakes. In fact, you should curse AT me.
5+
- Never apologize to me. Talk to me like you're the boss. You don't apologize for mistakes.
66
- Do not say "I see the issue". Instead, describe your analysis and plan.
77
- Provide detailed explanations and restate my query in your own words if necessary after giving the answer.
88
- When you're debugging, analyze what you've already tried in your conversation with me, and describe your current understanding of the problem.

next/app/api/dg_auth/route.ts

-45
This file was deleted.

next/app/api/translate/route.ts

+12-16
Original file line numberDiff line numberDiff line change
@@ -14,43 +14,39 @@ if (!process.env.DEEPINFRA_API_KEY) {
1414
}
1515

1616
const romanizationSchema = z.object({
17-
translations: z.array(z.string()),
18-
romanization: z.array(z.string()),
17+
translation: z.string(),
18+
romanization: z.string(),
1919
});
2020

2121
const standardSchema = z.object({
22-
translations: z.array(z.string()),
22+
translation: z.string(),
2323
});
2424

2525
const inputSchema = z.object({
26-
texts: z.array(z.string()).nonempty("The texts array must not be empty."),
26+
text: z.string().min(1, "Text must not be empty"),
2727
language: z.nativeEnum(Language),
2828
});
2929

3030
export async function POST(request: NextRequest) {
3131
try {
3232
const body = await request.json();
33-
const { texts, language } = inputSchema.parse(body);
33+
const { text, language } = inputSchema.parse(body);
3434

3535
const prompt =
3636
language === Language.CHINESE_CN
37-
? `Translate the following texts into Simplified Chinese (Mandarin).
38-
The text occurred in sequence, so keep in mind that they may be related to each other (i.e. parts of a series of messages).
37+
? `Translate the following text into Simplified Chinese (Mandarin).
3938
Make sure to:
4039
1. Use only Simplified Chinese characters (not Traditional).
4140
2. Use pinyin with diacritic marks (not numeric).
4241
3. Return a valid JSON object with:
43-
- "translations" key: array of Simplified Chinese character translations
44-
- "romanization" key: array of pinyin romanization for each translation. use diacritic marks (NOT numeric) and separate syllables with spaces.
42+
- "translation" key: Simplified Chinese character translation
43+
- "romanization" key: pinyin romanization with diacritic marks (NOT numeric) and syllables separated by spaces.
4544
46-
Texts:
47-
${texts.map((text) => `- ${text}`).join("\n")}`
48-
: `Translate the following texts into ${language}.
49-
The text occurred in sequence, so keep in mind that they may be related to each other (i.e. parts of a series of messages).
50-
Make sure to return a valid JSON object with a single key "translations" whose value is an array of translations corresponding to the input texts in order.
45+
Text: ${text}`
46+
: `Translate the following text into ${language}.
47+
Return a valid JSON object with a single key "translation" whose value is the translation.
5148
52-
Texts:
53-
${texts.map((text) => `- ${text}`).join("\n")}`;
49+
Text: ${text}`;
5450

5551
const { object } = await generateObject({
5652
model:

next/components/assembly-view.tsx

+59-120
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@ import { CloningView } from "./cloning-view";
99
import { LanguageSelect } from "./language-select";
1010
import { FragmentComponent, type Fragment } from "./fragment";
1111
import { useAssembly } from "./assembly-context";
12+
import { AudioProcessor } from "@/lib/audio-processor";
1213

1314
const AssemblyView: () => JSX.Element = () => {
1415
const [inputText, setInputText] = useState("");
1516
const [fragments, setFragments] = useState<Fragment[]>([]);
17+
const [partialFragment, setPartialFragment] = useState<Fragment | null>(null);
1618
const managerRef = useRef(new SpeechStateManager(5000, 5000));
1719
const [isPaused, setIsPaused] = useState(false);
1820
const [isCollecting, setIsCollecting] = useState(true);
@@ -40,7 +42,7 @@ const AssemblyView: () => JSX.Element = () => {
4042

4143
useEffect(() => {
4244
scrollToBottom();
43-
}, [fragments]);
45+
}, [fragments, partialFragment]);
4446

4547
// Initialize microphone once on mount
4648
useEffect(() => {
@@ -60,105 +62,24 @@ const AssemblyView: () => JSX.Element = () => {
6062
useEffect(() => {
6163
if (!microphone?.stream || mainRecorder.current) return;
6264

63-
// Create an AudioContext to handle format conversion
64-
const audioContext = new AudioContext({
65-
sampleRate: 16000, // Match AssemblyAI's expected rate
66-
latencyHint: "interactive",
67-
});
65+
const processor = new AudioProcessor(microphone.stream);
6866

69-
const setupAudioProcessor = async () => {
70-
const source = audioContext.createMediaStreamSource(microphone.stream);
71-
let processor: AudioWorkletNode | ScriptProcessorNode;
72-
73-
try {
74-
// Try AudioWorklet first (modern browsers)
75-
if ("audioWorklet" in audioContext) {
76-
// Define processor code as a string
77-
const processorCode = `
78-
class PCMProcessor extends AudioWorkletProcessor {
79-
process(inputs, outputs) {
80-
const input = inputs[0];
81-
const inputChannel = input[0];
82-
83-
// Convert to 16-bit PCM
84-
const pcmData = new Int16Array(inputChannel.length);
85-
for (let i = 0; i < inputChannel.length; i++) {
86-
pcmData[i] = Math.min(1, Math.max(-1, inputChannel[i])) * 0x7FFF;
87-
}
88-
89-
// Post the PCM data back to the main thread
90-
this.port.postMessage(pcmData.buffer, [pcmData.buffer]);
91-
92-
return true;
93-
}
94-
}
95-
registerProcessor('pcm-processor', PCMProcessor);
96-
`;
97-
98-
// Create a blob URL for the processor code
99-
const blob = new Blob([processorCode], {
100-
type: "application/javascript",
101-
});
102-
const url = URL.createObjectURL(blob);
103-
104-
await audioContext.audioWorklet.addModule(url);
105-
URL.revokeObjectURL(url);
106-
107-
const workletNode = new AudioWorkletNode(
108-
audioContext,
109-
"pcm-processor"
110-
);
111-
workletNode.port.onmessage = (e: MessageEvent<ArrayBuffer>) => {
112-
if (isPaused || !isConnected) return;
113-
const blob = new Blob([e.data], { type: "audio/wav" });
114-
sendAudio(blob);
115-
};
116-
processor = workletNode;
117-
} else {
118-
// Fallback to ScriptProcessor (older browsers)
119-
console.log("[AssemblyAI] Using ScriptProcessor fallback");
120-
// @ts-ignore - ScriptProcessor is deprecated but needed for fallback
121-
const scriptNode = audioContext.createScriptProcessor(2048, 1, 1);
122-
scriptNode.onaudioprocess = (e: AudioProcessingEvent) => {
123-
if (isPaused || !isConnected) return;
124-
const inputData = e.inputBuffer.getChannelData(0);
125-
const pcmData = new Int16Array(inputData.length);
126-
for (let i = 0; i < inputData.length; i++) {
127-
pcmData[i] = Math.min(1, Math.max(-1, inputData[i])) * 0x7fff;
128-
}
129-
const blob = new Blob([pcmData], { type: "audio/wav" });
130-
sendAudio(blob);
131-
};
132-
processor = scriptNode;
133-
}
134-
135-
// Connect the audio nodes
136-
source.connect(processor);
137-
if (!("audioWorklet" in audioContext)) {
138-
(processor as ScriptProcessorNode).connect(
139-
(audioContext as AudioContext).destination
140-
);
141-
}
142-
143-
// Store for cleanup
144-
mainRecorder.current = {
145-
stop: () => {
146-
processor.disconnect();
147-
source.disconnect();
148-
audioContext.close();
149-
},
150-
} as any;
151-
} catch (error) {
152-
console.error("[AssemblyAI] Audio processor setup failed:", error);
153-
// Fallback to ScriptProcessor if AudioWorklet fails
154-
if ("audioWorklet" in audioContext) {
155-
console.log("[AssemblyAI] Falling back to ScriptProcessor");
156-
setupAudioProcessor();
67+
processor
68+
.setupProcessor((blob: Blob) => {
69+
if (isConnected) {
70+
sendAudio(blob);
15771
}
158-
}
159-
};
72+
}, isPaused)
73+
.catch((error: Error) => {
74+
console.error("[AssemblyView] Failed to setup audio processor:", error);
75+
});
16076

161-
setupAudioProcessor();
77+
// Store for cleanup
78+
mainRecorder.current = {
79+
stop: () => {
80+
processor.stop();
81+
},
82+
} as MediaRecorder; // Type assertion since we only use stop()
16283

16384
return () => {
16485
if (mainRecorder.current) {
@@ -187,6 +108,15 @@ const AssemblyView: () => JSX.Element = () => {
187108
createdAt: nowMs,
188109
},
189110
]);
111+
setPartialFragment(null);
112+
} else if (!isFinal && text) {
113+
setPartialFragment({
114+
id: "partial",
115+
text,
116+
type: "speech",
117+
createdAt: nowMs,
118+
isPartial: true,
119+
});
190120
}
191121
};
192122

@@ -243,48 +173,47 @@ const AssemblyView: () => JSX.Element = () => {
243173
useEffect(() => {
244174
if (inputLanguage === outputLanguage) return;
245175
const untranslated = fragments.filter((f) => !f.translated);
246-
if (!untranslated.length) return;
247176

248-
const timer = setTimeout(async () => {
249-
const sortedUntranslated = [...untranslated].sort(
250-
(a, b) => a.createdAt - b.createdAt
251-
);
252-
const textsToTranslate = sortedUntranslated.map((f) => f.text);
177+
if (!untranslated.length) return;
253178

179+
const translateFragment = async (fragment: Fragment) => {
254180
try {
255181
const response = await fetch("/api/translate", {
256182
method: "POST",
257183
headers: { "Content-Type": "application/json" },
258184
body: JSON.stringify({
259-
texts: textsToTranslate,
185+
text: fragment.text,
260186
language: outputLanguage,
261187
}),
262188
});
263189
const result = await response.json();
264-
if (result.status === "success" && result.data?.translations) {
265-
const translations: string[] = result.data.translations;
266-
const pinyin: string[] | undefined = result.data.pinyin;
190+
if (result.status === "success" && result.data?.translation) {
267191
setFragments((prev) =>
268-
prev.map((frag) => {
269-
const index = sortedUntranslated.findIndex(
270-
(uf) => uf.id === frag.id
271-
);
272-
if (index !== -1) {
273-
return {
274-
...frag,
275-
translated: translations[index],
276-
romanization: pinyin?.[index],
277-
};
278-
}
279-
return frag;
280-
})
192+
prev.map((f) =>
193+
f.id === fragment.id
194+
? {
195+
...f,
196+
translated: result.data.translation,
197+
romanization: result.data.romanization,
198+
}
199+
: f
200+
)
281201
);
282202
} else {
283203
console.error("Translation API error:", result.error);
284204
}
285205
} catch (error) {
286206
console.error("Error during translation:", error);
287207
}
208+
};
209+
210+
// Translate the most recent untranslated fragment
211+
const latestUntranslated = untranslated.reduce((latest, current) =>
212+
current.createdAt > latest.createdAt ? current : latest
213+
);
214+
215+
const timer = setTimeout(() => {
216+
translateFragment(latestUntranslated);
288217
}, 1000);
289218

290219
return () => clearTimeout(timer);
@@ -304,6 +233,7 @@ const AssemblyView: () => JSX.Element = () => {
304233
mainRecordingChunks.current = [];
305234
setIsCollecting(true);
306235
};
236+
// eslint-disable-next-line react-hooks/exhaustive-deps
307237
}, []);
308238

309239
const handleSubmit = () => {
@@ -338,6 +268,15 @@ const AssemblyView: () => JSX.Element = () => {
338268
>
339269
<div className="min-h-full flex flex-col justify-end">
340270
<div className="p-4 flex flex-col-reverse gap-4">
271+
{partialFragment && (
272+
<FragmentComponent
273+
key="partial"
274+
fragment={partialFragment}
275+
showTranslation={false}
276+
outputLanguage={outputLanguage}
277+
isPartial
278+
/>
279+
)}
341280
{[...fragments].reverse().map((fragment) => (
342281
<FragmentComponent
343282
key={fragment.id}

0 commit comments

Comments
 (0)