Skip to content

Commit e166c5b

Browse files
authored
Merge pull request #1547 from ace-step/feat/issue-1096-voice-verification
feat: add Voice Identity Verification for voice cloning consent (#1096)
2 parents 37513d5 + 9781023 commit e166c5b

14 files changed

Lines changed: 1389 additions & 21 deletions

src/components/layout/EditorShell.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const AudioAnalysisPanel = lazy(() => import('../generation/AudioAnalysisPanel')
5757
const StemSeparationModal = lazy(() => import('../generation/StemSeparationModal').then(m => ({ default: m.StemSeparationModal })));
5858
const AudioToMidiModal = lazy(() => import('../generation/AudioToMidiModal').then(m => ({ default: m.AudioToMidiModal })));
5959
const HumToSongModal = lazy(() => import('../generation/HumToSongModal').then(m => ({ default: m.HumToSongModal })));
60+
const VoiceVerificationModal = lazy(() => import('../voice/VoiceVerificationModal').then(m => ({ default: m.VoiceVerificationModal })));
6061
const VocalReplacementModal = lazy(() => import('../generation/VocalReplacementModal').then(m => ({ default: m.VocalReplacementModal })));
6162

6263
// Lazy-loaded heavy panels (code-split, loaded on first use)
@@ -229,6 +230,7 @@ export function EditorShell() {
229230
<StemSeparationModal />
230231
<AudioToMidiModal />
231232
<HumToSongModal />
233+
<VoiceVerificationModal />
232234
<VocalReplacementModal />
233235
<ShareDialog />
234236
<VideoExportDialog />

src/components/voice/VoiceCard.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ export function VoiceCard({
130130
<span className={`text-[8px] px-1 py-px rounded ${SKILL_COLORS[voice.skillLevel] ?? 'bg-zinc-700 text-zinc-400'}`}>
131131
{voice.skillLevel}
132132
</span>
133+
<span
134+
className={`text-[8px] px-1 py-px rounded ${
135+
voice.verificationStatus === 'verified'
136+
? 'bg-emerald-900/50 text-emerald-300'
137+
: 'bg-zinc-800 text-zinc-500'
138+
}`}
139+
title={voice.verificationStatus === 'verified' ? 'Voice identity verified' : 'Voice identity unverified'}
140+
>
141+
{voice.verificationStatus === 'verified' ? 'verified' : 'unverified'}
142+
</span>
133143
</div>
134144
</div>
135145

src/components/voice/VoiceLibraryPanel.tsx

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { useCallback, useEffect, useRef, useState } from 'react';
22
import type { ChangeEvent } from 'react';
3+
import type { AddVoiceInput } from '../../store/voiceStore';
34
import { useVoiceStore } from '../../store/voiceStore';
5+
import { useVoiceVerificationStore } from '../../store/voiceVerificationStore';
6+
import { useUIStore } from '../../store/uiStore';
47
import { VoiceCard } from './VoiceCard';
58
import { VoiceEditDialog } from './VoiceEditDialog';
69
import { Button } from '../ui/Button';
@@ -10,7 +13,7 @@ import {
1013
processVoiceAudioFile,
1114
isVoiceUploadError,
1215
} from '../../services/voiceUploadService';
13-
import { toastError, toastSuccess } from '../../hooks/useToast';
16+
import { toastError, toastInfo, toastSuccess } from '../../hooks/useToast';
1417

1518
export function VoiceLibraryPanel() {
1619
const voices = useVoiceStore((s) => s.voices);
@@ -21,11 +24,12 @@ export function VoiceLibraryPanel() {
2124
const selectedVoiceId = useVoiceStore((s) => s.selectedVoiceId);
2225
const selectVoice = useVoiceStore((s) => s.selectVoice);
2326
const deselectVoice = useVoiceStore((s) => s.deselectVoice);
24-
const addVoice = useVoiceStore((s) => s.addVoice);
2527
const deleteVoice = useVoiceStore((s) => s.deleteVoice);
2628
const getFilteredVoices = useVoiceStore((s) => s.getFilteredVoices);
2729
const getAllTags = useVoiceStore((s) => s.getAllTags);
2830
const loadAudioBlob = useVoiceStore((s) => s.loadAudioBlob);
31+
const beginVerification = useVoiceVerificationStore((s) => s.beginVerification);
32+
const setShowVoiceVerificationModal = useUIStore((s) => s.setShowVoiceVerificationModal);
2933

3034
const [editingVoiceId, setEditingVoiceId] = useState<string | null>(null);
3135
const [playingVoiceId, setPlayingVoiceId] = useState<string | null>(null);
@@ -57,6 +61,15 @@ export function VoiceLibraryPanel() {
5761
fileInputRef.current?.click();
5862
}, []);
5963

64+
const beginVoiceCreationVerification = useCallback(
65+
(input: AddVoiceInput, audioBlob: Blob) => {
66+
beginVerification(input, audioBlob);
67+
setShowVoiceVerificationModal(true);
68+
toastInfo('Verify voice identity to add this voice to the library');
69+
},
70+
[beginVerification, setShowVoiceVerificationModal],
71+
);
72+
6073
const handleFileSelect = useCallback(
6174
async (e: ChangeEvent<HTMLInputElement>) => {
6275
const file = e.target.files?.[0];
@@ -73,7 +86,7 @@ export function VoiceLibraryPanel() {
7386
return;
7487
}
7588

76-
addVoice(
89+
beginVoiceCreationVerification(
7790
{
7891
name: result.name,
7992
durationSeconds: result.durationSeconds,
@@ -84,7 +97,6 @@ export function VoiceLibraryPanel() {
8497
},
8598
result.blob,
8699
);
87-
toastSuccess(`Voice "${result.name}" added to library`);
88100
} catch {
89101
toastError('Failed to process voice file');
90102
} finally {
@@ -94,7 +106,7 @@ export function VoiceLibraryPanel() {
94106
if (fileInputRef.current) fileInputRef.current.value = '';
95107
}
96108
},
97-
[addVoice],
109+
[beginVoiceCreationVerification],
98110
);
99111

100112
// ─── Preview playback ────────────────────────────────────
@@ -212,7 +224,7 @@ export function VoiceLibraryPanel() {
212224
Voice Library
213225
</span>
214226
<span className="text-[9px] text-zinc-600 mr-2">{voices.length}</span>
215-
<VoiceRecordButton />
227+
<VoiceRecordButton onCapturedVoice={beginVoiceCreationVerification} />
216228
<Button
217229
variant="ghost"
218230
size="sm"

src/components/voice/VoiceRecordButton.tsx

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,19 @@
11
import { useCallback, useEffect, useRef, useState } from 'react';
22
import { recordingEngine } from '../../engine/RecordingEngine';
33
import { audioBufferToWavBlob } from '../../utils/wav';
4+
import type { AddVoiceInput } from '../../store/voiceStore';
45
import { useVoiceStore } from '../../store/voiceStore';
56
import { computeSimplePeaks } from '../../services/voiceUploadService';
67
import { toastError, toastSuccess, toastInfo } from '../../hooks/useToast';
78
import { VOICE_MIN_DURATION_SECONDS } from '../../services/voiceUploadService';
89

910
const VOICE_RECORD_TRACK_ID = '__voice-recording__';
1011

11-
export function VoiceRecordButton() {
12+
interface VoiceRecordButtonProps {
13+
onCapturedVoice?: (input: AddVoiceInput, audioBlob: Blob) => void;
14+
}
15+
16+
export function VoiceRecordButton({ onCapturedVoice }: VoiceRecordButtonProps) {
1217
const addVoice = useVoiceStore((s) => s.addVoice);
1318
const [isRecording, setIsRecording] = useState(false);
1419
const [isStopping, setIsStopping] = useState(false);
@@ -71,20 +76,21 @@ export function VoiceRecordButton() {
7176
const blob = audioBufferToWavBlob(result.audioBuffer);
7277
const peaks = computeSimplePeaks(result.audioBuffer, 64);
7378
const name = `Recording ${new Date().toLocaleTimeString()}`;
79+
const input: AddVoiceInput = {
80+
name,
81+
durationSeconds: result.duration,
82+
skillLevel: 'intermediate',
83+
source: 'recording',
84+
tags: [],
85+
waveformPeaks: peaks,
86+
};
7487

75-
addVoice(
76-
{
77-
name,
78-
durationSeconds: result.duration,
79-
skillLevel: 'intermediate',
80-
source: 'recording',
81-
tags: [],
82-
waveformPeaks: peaks,
83-
},
84-
blob,
85-
);
86-
87-
toastSuccess(`Voice recording "${name}" added (${Math.round(result.duration)}s)`);
88+
if (onCapturedVoice) {
89+
onCapturedVoice(input, blob);
90+
} else {
91+
addVoice(input, blob);
92+
toastSuccess(`Voice recording "${name}" added (${Math.round(result.duration)}s)`);
93+
}
8894
setRecordingDuration(0);
8995
} catch {
9096
clearRecordingTimer();
@@ -94,7 +100,7 @@ export function VoiceRecordButton() {
94100
} finally {
95101
setIsStopping(false);
96102
}
97-
}, [addVoice, clearRecordingTimer]);
103+
}, [addVoice, clearRecordingTimer, onCapturedVoice]);
98104

99105
return (
100106
<button

0 commit comments

Comments
 (0)