feat: add core domain types for Photo Gallery SDK including media items, albums, and annotations

This commit is contained in:
2026-07-23 07:39:01 +05:30
parent 7bcd1a2a2d
commit 2613c767a6
107 changed files with 20699 additions and 7 deletions
@@ -0,0 +1,86 @@
'use client';
import { useRef, useState } from 'react';
import { blobToWavBase64, startRecording, wavBase64ToBlob, type Recorder } from '../../lib/audioCapture';
import { Icon } from '../../icons';
import { useAIProvider } from '../aiContext';
/**
* Reusable dictation button: record → (optional AI denoise) → transcribe → onText.
* Renders nothing if the AI provider can't transcribe. Used by the image-markup
* Text tool, the video Text overlay, and the Info comment box.
*/
export function VoiceButton({
onText,
denoise = false,
label = 'Speak',
size = 14,
}: {
onText: (text: string) => void;
denoise?: boolean;
label?: string;
size?: number;
}) {
const provider = useAIProvider();
const [recording, setRecording] = useState(false);
const [status, setStatus] = useState<string | null>(null);
const recRef = useRef<Recorder | null>(null);
if (!provider?.transcribeAudio) return null;
const start = async () => {
setStatus(null);
try {
recRef.current = await startRecording();
setRecording(true);
} catch (e) {
setStatus(e instanceof Error ? e.message : 'Microphone unavailable.');
}
};
const stop = async () => {
const rec = recRef.current;
recRef.current = null;
setRecording(false);
if (!rec || !provider.transcribeAudio) return;
try {
const blob = await rec.stop();
let wav: string;
if (denoise && provider.denoiseAudio) {
setStatus('Reducing noise…');
const w48 = await blobToWavBase64(blob, 48000);
const cleaned = await provider.denoiseAudio(w48);
wav = await blobToWavBase64(wavBase64ToBlob(cleaned), 16000);
} else {
wav = await blobToWavBase64(blob, 16000);
}
setStatus('Transcribing…');
const t = (await provider.transcribeAudio(wav)).trim();
if (t) onText(t);
setStatus(null);
} catch (e) {
setStatus(e instanceof Error ? e.message : 'Could not transcribe.');
}
};
return (
<span className="apg-voice">
<button
type="button"
className={`apg-btn apg-btn--small apg-voice__mic${recording ? ' apg-voice__mic--rec' : ''}`}
onClick={recording ? stop : start}
aria-label={recording ? 'Stop recording' : 'Dictate text'}
title={recording ? 'Stop & transcribe' : 'Speak to type'}
>
<Icon name={recording ? 'check' : 'mic'} size={size} />
{recording ? 'Stop' : label}
</button>
{recording || status ? (
<span className="apg-voice__status" aria-live="polite">
{recording ? '● Listening…' : status}
</span>
) : null}
</span>
);
}