mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 09:57:57 -04:00
feat(react-ui): localize MediaHistory, AudioTransform, and Sound components (#10802)
- Replace hardcoded text with useTranslation hook in UI components - Add localization support for both English (en) and Indonesian (id) locales Signed-off-by: Dedy F. Setyawan <dedyfajars@gmail.com>
This commit is contained in:
@@ -264,14 +264,22 @@
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Describe the sound you want to generate...",
|
||||
"instrumental": "Instrumental",
|
||||
"bpm": "BPM",
|
||||
"duration": "Duration (seconds)",
|
||||
"language": "Language",
|
||||
"timesignature": "Time Signature",
|
||||
"keyscale": "Key/Scale",
|
||||
"vocalLanguage": "Vocal language",
|
||||
"lyrics": "Lyrics (optional)",
|
||||
"vocalLanguagePlaceholder": "e.g. English",
|
||||
"caption": "Caption",
|
||||
"lyrics": "Lyrics",
|
||||
"lyricsPlaceholder": "Lyrics for vocal generation",
|
||||
"advanced": "Advanced Settings",
|
||||
"simple": "Simple",
|
||||
"advanced": "Advanced",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Random"
|
||||
"seedPlaceholder": "Random",
|
||||
"thinkMode": "Think mode"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Generate",
|
||||
@@ -284,6 +292,45 @@
|
||||
"generateFailed": "Generation failed"
|
||||
}
|
||||
},
|
||||
"audioTransform": {
|
||||
"title": "Audio Transform",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"audio": "Audio (required)",
|
||||
"reference": "Reference (optional)",
|
||||
"referenceHelp": "Loopback / far-end signal for echo cancellation, target speaker for voice conversion. Leave empty for unconditional transform.",
|
||||
"advancedParameters": "Advanced parameters",
|
||||
"advancedParametersHelp": "backend-specific (one key=value per line, e.g. noise_gate=true)",
|
||||
"advancedParametersPlaceholder": "# Optional. For LocalVQE:\n# noise_gate=true\n# noise_gate_threshold_dbfs=-50"
|
||||
},
|
||||
"input": {
|
||||
"upload": "Upload",
|
||||
"uploadDescription": "Drop a file here or",
|
||||
"uploadBrowse": "browse",
|
||||
"record": "Record",
|
||||
"startRecording": "Start recording",
|
||||
"stop": "Stop",
|
||||
"encoding": "Encoding...",
|
||||
"clear": "Clear",
|
||||
"microphoneUnavailable": "Microphone capture is unavailable in this browser.",
|
||||
"echoNotice": "Browsers often apply their own WebRTC echo cancellation and noise suppression by default. This usually results in worse performance than running LocalVQE on the raw audio.",
|
||||
"echoTest": "Echo test (record mic while playing reference)",
|
||||
"stopEchoTest": "Stop echo test"
|
||||
},
|
||||
"result": {
|
||||
"audio": "Audio",
|
||||
"reference": "Reference",
|
||||
"inputSpectrum": "Input spectrum",
|
||||
"outputSpectrum": "Output spectrum",
|
||||
"outputSpectrumHint": "Transform to compare attenuation",
|
||||
"output": "Output"
|
||||
},
|
||||
"actions": {
|
||||
"transform": "Transform",
|
||||
"processing": "Processing..."
|
||||
},
|
||||
"empty": "Choose an audio file (and optional reference) to transform"
|
||||
},
|
||||
"talk": {
|
||||
"title": "Talk",
|
||||
"subtitle": "Realtime voice conversation",
|
||||
|
||||
@@ -96,26 +96,73 @@
|
||||
"model": "Model",
|
||||
"prompt": "Prompt",
|
||||
"promptPlaceholder": "Deskripsikan suara yang ingin Anda buat...",
|
||||
"instrumental": "Instrumental",
|
||||
"bpm": "Tempo",
|
||||
"duration": "Durasi (detik)",
|
||||
"language": "Bahasa",
|
||||
"timesignature": "Tanda birama",
|
||||
"keyscale": "Tangga nada",
|
||||
"vocalLanguage": "Bahasa vokal",
|
||||
"lyrics": "Lirik (opsional)",
|
||||
"lyricsPlaceholder": "Lirik untuk generasi vokal",
|
||||
"advanced": "Pengaturan Tingkat Lanjutan",
|
||||
"vocalLanguagePlaceholder": "cth. Bahasa Indonesia",
|
||||
"caption": "Takarir",
|
||||
"lyrics": "Lirik",
|
||||
"lyricsPlaceholder": "Lirik untuk menghasilkan vokal",
|
||||
"simple": "Sederhana",
|
||||
"advanced": "Tingkat Lanjutan",
|
||||
"seed": "Seed",
|
||||
"seedPlaceholder": "Acak"
|
||||
"seedPlaceholder": "Acak",
|
||||
"thinkMode": "Mode penalaran"
|
||||
},
|
||||
"actions": {
|
||||
"generate": "Hasilkan",
|
||||
"generating": "Menghasilkan..."
|
||||
},
|
||||
"empty": "Audio yang dihasilkan akan muncul di sini",
|
||||
"empty": "Suara yang dihasilkan akan muncul di sini",
|
||||
"toasts": {
|
||||
"noPrompt": "Silahkan masukkan prompt",
|
||||
"noModel": "Silahkan pilih model",
|
||||
"generateFailed": "Gagal menghasilkan"
|
||||
}
|
||||
},
|
||||
"audioTransform": {
|
||||
"title": "Transformasi Audio",
|
||||
"labels": {
|
||||
"model": "Model",
|
||||
"audio": "Audio (wajib)",
|
||||
"reference": "Referensi (opsional)",
|
||||
"referenceHelp": "Sinyal loopback / ujung jauh untuk pembatalan gema, atau pembicara target untuk konversi suara. Kosongkan untuk transformasi tanpa syarat.",
|
||||
"advancedParameters": "Parameter tingkat lanjut",
|
||||
"advancedParametersHelp": "Spesifik backend (satu key=value per baris, contoh: noise_gate=true)",
|
||||
"advancedParametersPlaceholder": "# Opsional. Untuk LocalVQE:\n# noise_gate=true\n# noise_gate_threshold_dbfs=-50"
|
||||
},
|
||||
"input": {
|
||||
"upload": "Unggah",
|
||||
"uploadDescription": "Tarik file di sini atau",
|
||||
"uploadBrowse": "jelajahi",
|
||||
"record": "Rekam",
|
||||
"startRecording": "Mulai merekam",
|
||||
"stop": "Selesai",
|
||||
"encoding": "Pengenkodean...",
|
||||
"clear": "Hapus",
|
||||
"microphoneUnavailable": "Perekaman mikrofon tidak tersedia di peramban ini.",
|
||||
"echoNotice": "Peramban sering kali menerapkan pembatalan gema dan penekanan derau WebRTC secara bawaan. Hal ini biasanya menghasilkan performa yang lebih buruk dibandingkan menjalankan LocalVQE pada audio mentah.",
|
||||
"echoTest": "Tes gema (rekam mik saat memutar referensi)",
|
||||
"stopEchoTest": "Hentikan tes gema"
|
||||
},
|
||||
"result": {
|
||||
"audio": "Audio",
|
||||
"reference": "Referensi",
|
||||
"inputSpectrum": "Input spektrum",
|
||||
"outputSpectrum": "Output spektrum",
|
||||
"outputSpectrumHint": "Transformasi untuk membandingkan atenuasi",
|
||||
"output": "Output"
|
||||
},
|
||||
"actions": {
|
||||
"transform": "Transformasi",
|
||||
"processing": "Memproses..."
|
||||
},
|
||||
"empty": "Pilih file audio (dan referensi opsional) untuk ditransformasikan"
|
||||
},
|
||||
"talk": {
|
||||
"title": "Percakapan",
|
||||
"subtitle": "Percakapan suara realtime",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { memo, useState } from 'react'
|
||||
import { relativeTime } from '../utils/format'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ICONS = {
|
||||
image: 'fa-image',
|
||||
@@ -10,6 +11,7 @@ const ICONS = {
|
||||
}
|
||||
|
||||
export default memo(function MediaHistory({ entries, selectedId, onSelect, onDelete, onClearAll, mediaType }) {
|
||||
const { t } = useTranslation('media')
|
||||
const [expanded, setExpanded] = useState(true)
|
||||
|
||||
return (
|
||||
@@ -20,11 +22,11 @@ export default memo(function MediaHistory({ entries, selectedId, onSelect, onDel
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
>
|
||||
<i className="fas fa-chevron-right" />
|
||||
<span style={{ flex: 1 }}>History ({entries.length})</span>
|
||||
<span style={{ flex: 1 }}>{t('history.title')} ({entries.length})</span>
|
||||
{entries.length > 0 && (
|
||||
<button
|
||||
className="media-history-clear-btn"
|
||||
title="Clear all"
|
||||
title={t('history.clearTitle')}
|
||||
onClick={(e) => { e.stopPropagation(); onClearAll() }}
|
||||
>
|
||||
<i className="fas fa-trash" />
|
||||
@@ -34,7 +36,7 @@ export default memo(function MediaHistory({ entries, selectedId, onSelect, onDel
|
||||
{expanded && (
|
||||
<div className="media-history-list">
|
||||
{entries.length === 0 ? (
|
||||
<div className="media-history-empty">No history yet</div>
|
||||
<div className="media-history-empty">{t('history.empty')}</div>
|
||||
) : (
|
||||
entries.map(entry => (
|
||||
<div
|
||||
@@ -59,7 +61,7 @@ export default memo(function MediaHistory({ entries, selectedId, onSelect, onDel
|
||||
</div>
|
||||
<button
|
||||
className="media-history-item-delete"
|
||||
title="Delete"
|
||||
title={t('history.deleteEntry')}
|
||||
onClick={(e) => { e.stopPropagation(); onDelete(entry.id) }}
|
||||
data-testid="media-history-delete"
|
||||
>
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useMediaCapture } from '../hooks/useMediaCapture'
|
||||
import useObjectUrl from '../hooks/useObjectUrl'
|
||||
import { useMediaHistory } from '../hooks/useMediaHistory'
|
||||
import MediaHistory from '../components/MediaHistory'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
// AudioTransform — Studio tab for the audio_transform capability. Takes a
|
||||
// primary audio file plus an optional reference (loopback for AEC, target
|
||||
@@ -22,6 +23,7 @@ import MediaHistory from '../components/MediaHistory'
|
||||
// bleed of the reference, giving the user a real (mic, ref) pair to test
|
||||
// echo cancellation against.
|
||||
export default function AudioTransform() {
|
||||
const { t } = useTranslation('media')
|
||||
const { model: urlModel } = useParams()
|
||||
const { addToast } = useOutletContext()
|
||||
|
||||
@@ -167,22 +169,22 @@ export default function AudioTransform() {
|
||||
return (
|
||||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<PageHeader title={<><i className="fas fa-wave-square" /> Audio Transform</>} />
|
||||
<PageHeader title={<><i className="fas fa-wave-square" /> {t('audioTransform.title')}</>} />
|
||||
|
||||
<form onSubmit={handleProcess}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Model</label>
|
||||
<label className="form-label">{t('audioTransform.labels.model')}</label>
|
||||
<ModelSelector value={model} onChange={setModel} capability={CAP_AUDIO_TRANSFORM} />
|
||||
</div>
|
||||
|
||||
<AudioInput
|
||||
label="Audio (required)"
|
||||
label={t('audioTransform.labels.audio')}
|
||||
file={audioFile}
|
||||
onChange={setAudioFile}
|
||||
/>
|
||||
<AudioInput
|
||||
label="Reference (optional)"
|
||||
help="Loopback / far-end signal for echo cancellation, target speaker for voice conversion. Leave empty for unconditional transform."
|
||||
label={t('audioTransform.labels.reference')}
|
||||
help={t('audioTransform.labels.referenceHelp')}
|
||||
file={referenceFile}
|
||||
onChange={setReferenceFile}
|
||||
/>
|
||||
@@ -192,9 +194,7 @@ export default function AudioTransform() {
|
||||
<p className="audio-transform-echo__notice" role="note">
|
||||
<i className="fas fa-circle-info" aria-hidden="true" />
|
||||
<span>
|
||||
Browsers often apply their own WebRTC echo cancellation and
|
||||
noise suppression by default. This usually results in worse
|
||||
performance than running LocalVQE on the raw audio.
|
||||
{t('audioTransform.input.echoNotice')}
|
||||
</span>
|
||||
</p>
|
||||
<div className="audio-transform-echo__row">
|
||||
@@ -204,8 +204,8 @@ export default function AudioTransform() {
|
||||
onClick={echoActive ? stopEchoTest : startEchoTest}
|
||||
>
|
||||
{echoActive
|
||||
? <><i className="fas fa-stop" /> Stop echo test</>
|
||||
: <><i className="fas fa-headphones-alt" /> Echo test (record mic while playing reference)</>}
|
||||
? <><i className="fas fa-stop" /> {t('audioTransform.input.stopEchoTest')}</>
|
||||
: <><i className="fas fa-headphones-alt" /> {t('audioTransform.input.echoTest')}</>}
|
||||
</button>
|
||||
{echoActive && echoCap.recording && (
|
||||
<span className="audio-transform-echo__elapsed">
|
||||
@@ -222,20 +222,20 @@ export default function AudioTransform() {
|
||||
|
||||
<div className="form-group">
|
||||
<label className="form-label">
|
||||
Advanced parameters
|
||||
<span className="form-help"> — backend-specific (one <code>key=value</code> per line, e.g. <code>noise_gate=true</code>)</span>
|
||||
{t('audioTransform.labels.advancedParameters')}
|
||||
<span className="form-help"> — {t('audioTransform.labels.advancedParametersHelp')}</span>
|
||||
</label>
|
||||
<textarea
|
||||
className="textarea"
|
||||
value={paramsText}
|
||||
onChange={(e) => setParamsText(e.target.value)}
|
||||
placeholder={`# Optional. For LocalVQE:\n# noise_gate=true\n# noise_gate_threshold_dbfs=-50`}
|
||||
placeholder={t('audioTransform.labels.advancedParametersPlaceholder')}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? <><LoadingSpinner size="sm" /> Processing...</> : <><i className="fas fa-wand-magic-sparkles" /> Transform</>}
|
||||
{loading ? <><LoadingSpinner size="sm" /> {t('audioTransform.actions.processing')}</> : <><i className="fas fa-wand-magic-sparkles" /> {t('audioTransform.actions.transform')}</>}
|
||||
</button>
|
||||
</form>
|
||||
<MediaHistory {...historyProps} />
|
||||
@@ -251,7 +251,7 @@ export default function AudioTransform() {
|
||||
<WaveformPlayer
|
||||
key={r.kind || r.url}
|
||||
src={r.url}
|
||||
label={resultLabel(r)}
|
||||
label={resultLabel(r, t)}
|
||||
height={r.kind === 'output' ? 120 : 96}
|
||||
dimmed={r.kind === 'reference'}
|
||||
download={r.kind === 'output' ? `audio-transform-${selectedEntry.model || 'output'}.wav` : undefined}
|
||||
@@ -263,28 +263,28 @@ export default function AudioTransform() {
|
||||
<div className="audio-transform-stack">
|
||||
{audioUrl && (
|
||||
<div className="audio-spectrogram-pair">
|
||||
<Spectrogram src={audioUrl} label="Input spectrum" testId="spectrogram-input" />
|
||||
<Spectrogram src={audioUrl} label={t('audioTransform.result.inputSpectrum')} testId="spectrogram-input" />
|
||||
{outputUrl ? (
|
||||
<Spectrogram src={outputUrl} label="Output spectrum" testId="spectrogram-output" />
|
||||
<Spectrogram src={outputUrl} label={t('audioTransform.result.outputSpectrum')} testId="spectrogram-output" />
|
||||
) : (
|
||||
<div className="audio-spectrogram">
|
||||
<div className="audio-spectrogram__label">Output spectrum</div>
|
||||
<div className="audio-spectrogram__label">{t('audioTransform.result.outputSpectrum')}</div>
|
||||
<div
|
||||
className="audio-spectrogram__canvas-wrap audio-spectrogram__canvas-wrap--empty"
|
||||
style={{ height: 140 }}
|
||||
>
|
||||
<span className="audio-spectrogram__hint">Transform to compare attenuation</span>
|
||||
<span className="audio-spectrogram__hint">{t('audioTransform.result.outputSpectrumHint')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<WaveformPlayer src={audioUrl} label="Audio" height={96} />
|
||||
<WaveformPlayer src={referenceUrl} label="Reference" height={96} dimmed={!referenceFile} />
|
||||
<WaveformPlayer src={audioUrl} label={t('audioTransform.result.audio')} height={96} />
|
||||
<WaveformPlayer src={referenceUrl} label={t('audioTransform.result.reference')} height={96} dimmed={!referenceFile} />
|
||||
{outputUrl && (
|
||||
<WaveformPlayer
|
||||
src={outputUrl}
|
||||
label="Output"
|
||||
label={t('audioTransform.result.output')}
|
||||
height={120}
|
||||
download={`audio-transform-${model || 'output'}-${new Date().toISOString().slice(0, 10)}.wav`}
|
||||
/>
|
||||
@@ -292,7 +292,7 @@ export default function AudioTransform() {
|
||||
{!audioUrl && !outputUrl && (
|
||||
<div className="media-empty">
|
||||
<i className="fas fa-wave-square media-empty__icon" />
|
||||
<p>Choose an audio file (and optional reference) to transform</p>
|
||||
<p>{t('audioTransform.empty')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -324,6 +324,7 @@ function resultLabel(r) {
|
||||
// as `File([blob], 'recording-XXX.wav', { type: 'audio/wav' })` so callers
|
||||
// can treat them identically to uploaded files).
|
||||
function AudioInput({ label, help, file, onChange }) {
|
||||
const { t } = useTranslation('media')
|
||||
const [tab, setTab] = useState('upload') // 'upload' | 'record'
|
||||
const cap = useMediaCapture('audio')
|
||||
const [recordPending, setRecordPending] = useState(false)
|
||||
@@ -371,7 +372,7 @@ function AudioInput({ label, help, file, onChange }) {
|
||||
className={`audio-transform-input__tab${tab === 'upload' ? ' active' : ''}`}
|
||||
onClick={() => setTab('upload')}
|
||||
>
|
||||
<i className="fas fa-upload" /> Upload
|
||||
<i className="fas fa-upload" /> {t('audioTransform.input.upload')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
@@ -380,7 +381,7 @@ function AudioInput({ label, help, file, onChange }) {
|
||||
className={`audio-transform-input__tab${tab === 'record' ? ' active' : ''}`}
|
||||
onClick={() => setTab('record')}
|
||||
>
|
||||
<i className="fas fa-microphone" /> Record
|
||||
<i className="fas fa-microphone" /> {t('audioTransform.input.record')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -395,14 +396,14 @@ function AudioInput({ label, help, file, onChange }) {
|
||||
{hasFile ? (
|
||||
<div className="audio-transform-drop__file">
|
||||
<i className="fas fa-file-audio" /> {file.name}
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={() => onChange(null)}>Clear</button>
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={() => onChange(null)}>{t('audioTransform.input.clear')}</button>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<i className="fas fa-upload" /> Drop a file here or
|
||||
<i className="fas fa-upload" /> {t('audioTransform.input.uploadDescription')}
|
||||
<label className="audio-transform-drop__pick">
|
||||
<input type="file" accept="audio/*" onChange={onPick} hidden />
|
||||
browse
|
||||
{t('audioTransform.input.uploadBrowse')}
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
@@ -413,23 +414,23 @@ function AudioInput({ label, help, file, onChange }) {
|
||||
<div className="audio-transform-rec">
|
||||
{!cap.supported && (
|
||||
<div className="audio-transform-rec__notice">
|
||||
<i className="fas fa-circle-info" /> Microphone capture is unavailable in this browser.
|
||||
<i className="fas fa-circle-info" /> {t('audioTransform.input.microphoneUnavailable')}
|
||||
</div>
|
||||
)}
|
||||
{cap.supported && (
|
||||
<>
|
||||
{!cap.recording && !recordPending && (
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={startRecord}>
|
||||
<i className="fas fa-circle" style={{ color: '#e25555' }} /> Start recording
|
||||
<i className="fas fa-circle" style={{ color: '#e25555' }} /> {t('audioTransform.input.startRecording')}
|
||||
</button>
|
||||
)}
|
||||
{cap.recording && (
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={stopRecord}>
|
||||
<i className="fas fa-stop" /> Stop ({cap.elapsed.toFixed(1)}s)
|
||||
<i className="fas fa-stop" /> {t('audioTransform.input.stop')} ({cap.elapsed.toFixed(1)}s)
|
||||
</button>
|
||||
)}
|
||||
{recordPending && !cap.recording && (
|
||||
<div className="audio-transform-rec__pending">Encoding…</div>
|
||||
<div className="audio-transform-rec__pending">{t('audioTransform.input.encoding')}</div>
|
||||
)}
|
||||
{cap.error && (
|
||||
<div className="audio-transform-rec__notice audio-transform-rec__notice--error">
|
||||
@@ -439,7 +440,7 @@ function AudioInput({ label, help, file, onChange }) {
|
||||
{hasFile && !cap.recording && (
|
||||
<div className="audio-transform-drop__file" style={{ marginTop: 'var(--spacing-sm)' }}>
|
||||
<i className="fas fa-file-audio" /> {file.name}
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={() => onChange(null)}>Clear</button>
|
||||
<button type="button" className="btn btn-secondary btn-sm" onClick={() => onChange(null)}>{t('audioTransform.input.clear')}</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -10,8 +10,10 @@ import MediaHistory from '../components/MediaHistory'
|
||||
import WaveformPlayer from '../components/audio/WaveformPlayer'
|
||||
import { soundApi } from '../utils/api'
|
||||
import { useMediaHistory } from '../hooks/useMediaHistory'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export default function Sound() {
|
||||
const { t } = useTranslation('media')
|
||||
const { model: urlModel } = useParams()
|
||||
const { addToast } = useOutletContext()
|
||||
const [model, setModel] = useState(urlModel || '')
|
||||
@@ -79,63 +81,63 @@ export default function Sound() {
|
||||
return (
|
||||
<div className="media-layout">
|
||||
<div className="media-controls">
|
||||
<PageHeader title={<><i className="fas fa-music" /> Sound Generation</>} />
|
||||
<PageHeader title={<><i className="fas fa-music" /> {t('sound.title')}</>} />
|
||||
|
||||
<form onSubmit={handleGenerate}>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Model</label>
|
||||
<label className="form-label">{t('sound.labels.model')}</label>
|
||||
<ModelSelector value={model} onChange={setModel} capability={CAP_SOUND_GENERATION} />
|
||||
</div>
|
||||
|
||||
{/* Mode toggle */}
|
||||
<div className="segmented">
|
||||
<button type="button" className={`segmented__item${mode === 'simple' ? ' is-active' : ''}`} onClick={() => setMode('simple')}>Simple</button>
|
||||
<button type="button" className={`segmented__item${mode === 'advanced' ? ' is-active' : ''}`} onClick={() => setMode('advanced')}>Advanced</button>
|
||||
<button type="button" className={`segmented__item${mode === 'simple' ? ' is-active' : ''}`} onClick={() => setMode('simple')}>{t('sound.labels.simple')}</button>
|
||||
<button type="button" className={`segmented__item${mode === 'advanced' ? ' is-active' : ''}`} onClick={() => setMode('advanced')}>{t('sound.labels.advanced')}</button>
|
||||
</div>
|
||||
|
||||
{mode === 'simple' ? (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Description</label>
|
||||
<textarea className="textarea" value={text} onChange={(e) => setText(e.target.value)} placeholder="Describe the sound..." rows={3} />
|
||||
<label className="form-label">{t('sound.labels.prompt')}</label>
|
||||
<textarea className="textarea" value={text} onChange={(e) => setText(e.target.value)} placeholder={t('sound.labels.promptPlaceholder')} rows={3} />
|
||||
</div>
|
||||
<div className="form-grid-2col">
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={instrumental} onChange={(e) => setInstrumental(e.target.checked)} />
|
||||
<span>Instrumental</span>
|
||||
<span>{t('sound.labels.instrumental')}</span>
|
||||
</label>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Vocal language</label>
|
||||
<input className="input" value={vocalLanguage} onChange={(e) => setVocalLanguage(e.target.value)} placeholder="e.g. English" />
|
||||
<label className="form-label">{t('sound.labels.vocalLanguage')}</label>
|
||||
<input className="input" value={vocalLanguage} onChange={(e) => setVocalLanguage(e.target.value)} placeholder={t('sound.labels.vocalLanguagePlaceholder')} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Caption</label>
|
||||
<label className="form-label">{t('sound.labels.caption')}</label>
|
||||
<textarea className="textarea" value={caption} onChange={(e) => setCaption(e.target.value)} rows={2} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="form-label">Lyrics</label>
|
||||
<textarea className="textarea" value={lyrics} onChange={(e) => setLyrics(e.target.value)} rows={3} />
|
||||
<label className="form-label">{t('sound.labels.lyrics')}</label>
|
||||
<textarea className="textarea" value={lyrics} onChange={(e) => setLyrics(e.target.value)} placeholder={t('sound.labels.lyricsPlaceholder')} rows={3} />
|
||||
</div>
|
||||
<div className="form-grid-2col">
|
||||
<div className="form-group"><label className="form-label">BPM</label><input className="input" type="number" value={bpm} onChange={(e) => setBpm(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Duration (s)</label><input className="input" type="number" step="0.1" value={duration} onChange={(e) => setDuration(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Key/Scale</label><input className="input" value={keyscale} onChange={(e) => setKeyscale(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Language</label><input className="input" value={language} onChange={(e) => setLanguage(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">Time Signature</label><input className="input" value={timesignature} onChange={(e) => setTimesignature(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">{t('sound.labels.bpm')}</label><input className="input" type="number" value={bpm} onChange={(e) => setBpm(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">{t('sound.labels.duration')}</label><input className="input" type="number" step="0.1" value={duration} onChange={(e) => setDuration(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">{t('sound.labels.keyscale')}</label><input className="input" value={keyscale} onChange={(e) => setKeyscale(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">{t('sound.labels.language')}</label><input className="input" value={language} onChange={(e) => setLanguage(e.target.value)} /></div>
|
||||
<div className="form-group"><label className="form-label">{t('sound.labels.timesignature')}</label><input className="input" value={timesignature} onChange={(e) => setTimesignature(e.target.value)} /></div>
|
||||
</div>
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" checked={think} onChange={(e) => setThink(e.target.checked)} />
|
||||
<span>Think mode</span>
|
||||
<span>{t('sound.labels.thinkMode')}</span>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
|
||||
<button type="submit" className="btn btn-primary btn-full" disabled={loading}>
|
||||
{loading ? <><LoadingSpinner size="sm" /> Generating...</> : <><i className="fas fa-music" /> Generate Sound</>}
|
||||
{loading ? <><LoadingSpinner size="sm" /> {t('sound.actions.generating')}</> : <><i className="fas fa-music" /> {t('sound.actions.generate')}</>}
|
||||
</button>
|
||||
</form>
|
||||
<MediaHistory {...historyProps} />
|
||||
@@ -163,7 +165,7 @@ export default function Sound() {
|
||||
) : (
|
||||
<div className="media-empty">
|
||||
<i className="fas fa-music media-empty__icon" />
|
||||
<p>Generated sound will appear here</p>
|
||||
<p>{t('sound.empty')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user