310 lines
11 KiB
React
310 lines
11 KiB
React
import {
|
|
Globe,
|
|
SlidersHorizontal,
|
|
Settings2,
|
|
ChevronUp,
|
|
ChevronDown,
|
|
Play,
|
|
Square,
|
|
} from 'lucide-react';
|
|
import { Button, Progress } from '../../ui';
|
|
import SearchableSelect from '../SearchableSelect';
|
|
import ALL_LANGUAGES from '../../languages.json';
|
|
import { POPULAR_LANGS } from '../../utils/constants';
|
|
import { stopActivePlayback } from '../../utils/playback';
|
|
|
|
export default function ActionBar({
|
|
t,
|
|
showOverrides,
|
|
setShowOverrides,
|
|
cfg,
|
|
setCfg,
|
|
speed,
|
|
setSpeed,
|
|
tShift,
|
|
setTShift,
|
|
posTemp,
|
|
setPosTemp,
|
|
classTemp,
|
|
setClassTemp,
|
|
layerPenalty,
|
|
setLayerPenalty,
|
|
duration,
|
|
setDuration,
|
|
denoise,
|
|
setDenoise,
|
|
postprocess,
|
|
setPostprocess,
|
|
language,
|
|
setLanguage,
|
|
steps,
|
|
setSteps,
|
|
showHearDemo,
|
|
playDemoOutput,
|
|
demoAudioPlaying,
|
|
demoAudioRef,
|
|
demoReleaseRef,
|
|
setDemoAudioPlaying,
|
|
outputPlaying,
|
|
isGenerating,
|
|
handleGenerate,
|
|
cancelGeneration,
|
|
cancelAllPendingJobs,
|
|
generationTime,
|
|
wasGeneratingRef,
|
|
}) {
|
|
return (
|
|
<div className="studio-action-bar overflow-visible relative z-[10]">
|
|
{showOverrides && (
|
|
<div className="override-content">
|
|
<div className="grid [grid-template-columns:repeat(auto-fit,minmax(120px,1fr))] gap-[6px] max-[500px]:grid-cols-2">
|
|
<div>
|
|
<div className="label-row justify-between">
|
|
<span>CFG</span>
|
|
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
|
|
{cfg}
|
|
</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="1.0"
|
|
max="4.0"
|
|
step="0.1"
|
|
value={cfg}
|
|
onChange={(e) => setCfg(Number(e.target.value))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="label-row justify-between">
|
|
<span>{t('clone.speed')}</span>
|
|
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
|
|
{speed}x
|
|
</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0.5"
|
|
max="2.0"
|
|
step="0.1"
|
|
value={speed}
|
|
onChange={(e) => setSpeed(Number(e.target.value))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="label-row justify-between">
|
|
<span>{t('clone.tshift')}</span>
|
|
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
|
|
{tShift}
|
|
</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="1.0"
|
|
step="0.05"
|
|
value={tShift}
|
|
onChange={(e) => setTShift(Number(e.target.value))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="label-row justify-between">
|
|
<span>{t('clone.pos_temp')}</span>
|
|
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
|
|
{posTemp}
|
|
</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="10"
|
|
step="0.5"
|
|
value={posTemp}
|
|
onChange={(e) => setPosTemp(Number(e.target.value))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="label-row justify-between">
|
|
<span>{t('clone.class_temp')}</span>
|
|
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
|
|
{classTemp}
|
|
</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="2"
|
|
step="0.1"
|
|
value={classTemp}
|
|
onChange={(e) => setClassTemp(Number(e.target.value))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="label-row justify-between">
|
|
<span>{t('clone.layer_pen')}</span>
|
|
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
|
|
{layerPenalty}
|
|
</span>
|
|
</div>
|
|
<input
|
|
type="range"
|
|
min="0"
|
|
max="10"
|
|
step="0.5"
|
|
value={layerPenalty}
|
|
onChange={(e) => setLayerPenalty(Number(e.target.value))}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<div className="label-row">
|
|
<span>{t('clone.duration')}</span>
|
|
</div>
|
|
<input
|
|
type="text"
|
|
className="input-base text-[0.8rem]"
|
|
value={duration}
|
|
onChange={(e) => setDuration(e.target.value)}
|
|
placeholder={t('clone.auto')}
|
|
/>
|
|
</div>
|
|
<div className="flex flex-col gap-[6px]">
|
|
<label className="text-[0.75rem] flex items-center gap-[6px] cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={denoise}
|
|
onChange={(e) => setDenoise(e.target.checked)}
|
|
/>{' '}
|
|
{t('clone.denoise')}
|
|
</label>
|
|
<label className="text-[0.75rem] flex items-center gap-[6px] cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={postprocess}
|
|
onChange={(e) => setPostprocess(e.target.checked)}
|
|
/>{' '}
|
|
{t('clone.postprocess')}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Controls row: language · steps · overrides disclosure */}
|
|
<div className="flex items-center gap-[16px] min-w-0">
|
|
<div className="flex items-center gap-[6px] flex-[1_1_220px] min-w-[140px] [&>:last-child]:flex-1 [&>:last-child]:min-w-0">
|
|
<Globe size={12} className="label-icon" />
|
|
<SearchableSelect
|
|
value={language}
|
|
options={ALL_LANGUAGES}
|
|
popular={POPULAR_LANGS}
|
|
recentsKey="omnivoice.recents.genLang"
|
|
onChange={setLanguage}
|
|
/>
|
|
</div>
|
|
<label
|
|
className="flex items-center gap-[6px] flex-[1_1_160px] min-w-[120px] [&_input]:flex-1 [&_input]:min-w-[60px]"
|
|
title={t('clone.steps')}
|
|
>
|
|
<SlidersHorizontal size={12} className="label-icon" />
|
|
<input
|
|
type="range"
|
|
min="8"
|
|
max="64"
|
|
value={steps}
|
|
onChange={(e) => setSteps(Number(e.target.value))}
|
|
/>
|
|
<span className="text-[0.65rem] bg-black/35 px-[5px] py-px rounded-[3px] [border:1px_solid_rgba(255,255,255,0.04)] [font-variant-numeric:tabular-nums]">
|
|
{steps}
|
|
</span>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
className="inline-flex items-center gap-[4px] px-[10px] py-[4px] text-[0.7rem] text-[var(--chrome-fg-muted)] bg-transparent border border-transparent rounded-[var(--chrome-radius-pill)] cursor-pointer whitespace-nowrap flex-none transition-[color,border-color] duration-[var(--dur-fast)] hover:text-[var(--chrome-fg)] hover:border-transparent focus-visible:[outline:2px_solid_var(--chrome-accent)] focus-visible:[outline-offset:1px]"
|
|
onClick={() => setShowOverrides(!showOverrides)}
|
|
aria-expanded={showOverrides}
|
|
>
|
|
<Settings2 size={13} /> {t('clone.production_overrides')}
|
|
{showOverrides ? <ChevronDown size={12} /> : <ChevronUp size={12} />}
|
|
</button>
|
|
</div>
|
|
|
|
{showHearDemo ? (
|
|
<>
|
|
<Button
|
|
variant="primary"
|
|
block
|
|
onClick={playDemoOutput}
|
|
leading={<Play size={14} />}
|
|
className="mt-[6px]"
|
|
>
|
|
{demoAudioPlaying ? t('demo.stop_demo') : t('demo.hear_demo')}
|
|
</Button>
|
|
<div className="mt-[6px] px-[8px] py-[4px] text-[10px] text-center text-fg-muted bg-white/[0.03] rounded-md [border:1px_dashed_rgba(255,255,255,0.08)]">
|
|
{t('demo.prerendered_chip')}
|
|
</div>
|
|
<audio
|
|
ref={demoAudioRef}
|
|
onEnded={() => {
|
|
setDemoAudioPlaying(false);
|
|
demoReleaseRef.current?.();
|
|
demoReleaseRef.current = null;
|
|
}}
|
|
preload="none"
|
|
/>
|
|
</>
|
|
) : outputPlaying && !isGenerating ? (
|
|
/* Synthesized output is playing — the CTA becomes a Stop button
|
|
(#316) so playback can be halted immediately. */
|
|
<Button
|
|
variant="primary"
|
|
block
|
|
onClick={stopActivePlayback}
|
|
leading={<Square size={14} />}
|
|
className="mt-[6px]"
|
|
>
|
|
{t('clone.stop_playback')}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
variant="primary"
|
|
block
|
|
onClick={isGenerating ? cancelGeneration : handleGenerate}
|
|
leading={isGenerating ? <Square size={14} /> : <Play size={14} />}
|
|
className="mt-[6px]"
|
|
>
|
|
{isGenerating
|
|
? 'Cancel job'
|
|
: t('clone.synthesize')}
|
|
</Button>
|
|
)}
|
|
{isGenerating && (
|
|
<Progress
|
|
value={Math.min((generationTime / 8) * 100, 95)}
|
|
tone="brand"
|
|
size="sm"
|
|
className="mt-[6px]"
|
|
/>
|
|
)}
|
|
<Button
|
|
variant="ghost"
|
|
block
|
|
onClick={cancelAllPendingJobs}
|
|
className="mt-[4px]"
|
|
>
|
|
Cancel all pending jobs
|
|
</Button>
|
|
{/* 10x P4 a11y (spec §3): persistent polite live region — screen
|
|
readers hear generation start AND finish in-workspace, without
|
|
relying on the FloatingPill. sr-only keeps it out of the
|
|
action-bar flex flow; static text avoids per-second re-announces
|
|
from the ticking "Synthesizing… (Ns)" button label. */}
|
|
<div className="sr-only" role="status" aria-live="polite">
|
|
{isGenerating
|
|
? t('clone.generating_status', { defaultValue: 'Generating audio…' })
|
|
: wasGeneratingRef.current
|
|
? t('clone.generating_done_status', { defaultValue: 'Generation finished' })
|
|
: null}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|