feat(transcribe): English UI default, language switcher (NO/UK/PL), fix 504 timeout
- Default UI language to English; lang switcher (EN/NO/UK/PL) persisted in localStorage - Rename 'rettssak/tingrett' preset to 'Mediation / legal meeting' — court recording is illegal - Add Ukrainian (uk) and Polish (pl) as selectable audio transcription languages - TRANSCRIBE_I18N translation object drives all status messages, labels, and trace text - Apache ProxyTimeout raised to 1800s on server (was 300s — caused 504 on large files) - set_time_limit(0) + ignore_user_abort(true) in api/transcribe.php - applyTranscribeI18n() patches data-i18n / data-i18n-placeholder / data-i18n-aria attrs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -6,9 +6,12 @@ require_once __DIR__ . '/../includes/LegalTools.php';
|
|||||||
dbnToolsRequireMethod('POST');
|
dbnToolsRequireMethod('POST');
|
||||||
dbnToolsRequireAuth();
|
dbnToolsRequireAuth();
|
||||||
|
|
||||||
|
set_time_limit(0);
|
||||||
|
ignore_user_abort(true);
|
||||||
|
|
||||||
// ── Common params ─────────────────────────────────────────────────────────────
|
// ── Common params ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
$validLangs = ['auto', 'no', 'nn', 'en', 'sv', 'da', 'de', 'fr', 'es', 'pl', 'fi', 'nl', 'it', 'pt', 'ru', 'ar', 'tr', 'zh', 'ja', 'ko'];
|
$validLangs = ['auto', 'no', 'nn', 'en', 'sv', 'da', 'de', 'fr', 'es', 'pl', 'uk', 'fi', 'nl', 'it', 'pt', 'ru', 'ar', 'tr', 'zh', 'ja', 'ko'];
|
||||||
$language = strtolower(trim((string)($_POST['language'] ?? 'auto')));
|
$language = strtolower(trim((string)($_POST['language'] ?? 'auto')));
|
||||||
if (!in_array($language, $validLangs, true)) $language = 'auto';
|
if (!in_array($language, $validLangs, true)) $language = 'auto';
|
||||||
|
|
||||||
|
|||||||
@@ -1300,6 +1300,42 @@ p {
|
|||||||
margin-bottom: 0.35rem;
|
margin-bottom: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── UI Language switcher ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.lang-switcher {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
padding-bottom: 0.625rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 9999px;
|
||||||
|
color: var(--muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
transition: background 0.15s, border-color 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn:hover {
|
||||||
|
background: var(--soft-teal);
|
||||||
|
border-color: var(--teal);
|
||||||
|
color: var(--teal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lang-btn.is-active {
|
||||||
|
background: var(--teal);
|
||||||
|
border-color: var(--teal);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.vocab-btn {
|
.vocab-btn {
|
||||||
font-size: 0.78rem;
|
font-size: 0.78rem;
|
||||||
padding: 0.2rem 0.6rem;
|
padding: 0.2rem 0.6rem;
|
||||||
|
|||||||
+314
-27
@@ -9,11 +9,299 @@ let lastTranscriptData = null;
|
|||||||
|
|
||||||
const VOCAB_PRESETS = {
|
const VOCAB_PRESETS = {
|
||||||
barnerett: 'Barnevernet, Fylkesnemnda, barnevernloven, barneloven, barnets beste, samvær, foreldreansvar, omsorgsovertakelse, sakkyndig, advokat, prosessfullmektig, dommer, vitne, tolk, bistandsadvokat, fosterforeldre, fosterhjem, akuttvedtak, statsforvalter, Bufetat, saksbehandler, rettslig medhold, begjæring, samtykke, tilsynsfører',
|
barnerett: 'Barnevernet, Fylkesnemnda, barnevernloven, barneloven, barnets beste, samvær, foreldreansvar, omsorgsovertakelse, sakkyndig, advokat, prosessfullmektig, dommer, vitne, tolk, bistandsadvokat, fosterforeldre, fosterhjem, akuttvedtak, statsforvalter, Bufetat, saksbehandler, rettslig medhold, begjæring, samtykke, tilsynsfører',
|
||||||
rettssak: 'Tingretten, lagmannsretten, Høyesterett, statsadvokat, aktor, forsvarer, tiltalte, fornærmede, stevning, tilsvar, prosesskriv, rettsbok, bevisføring, anke, dom, kjennelse, rettsmekling, forlik, saksøker, saksøkte, vitne, ed, prosessfullmektig',
|
mediation: 'rettsmekling, meklingsnemnd, familievernkontor, mekling, forlik, meklingsprotokoll, foreldreplan, avtale, prosessfullmektig, advokat, dommer, vitne, sakkyndig, statsforvalter, barnevernloven, barneloven, barnets beste, samvær, foreldreansvar, omsorgsovertakelse, tilsvar, stevning, kjennelse, anke',
|
||||||
generell: 'bokmål, nynorsk, statsforvalter, kommunen, forvaltning, klage, vedtak, rettigheter, plikter, protokoll, referat, rapport, dokumentasjon, velferd',
|
generell: 'bokmål, nynorsk, statsforvalter, kommunen, forvaltning, klage, vedtak, rettigheter, plikter, protokoll, referat, rapport, dokumentasjon, velferd',
|
||||||
custom: '',
|
custom: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let uiLang = localStorage.getItem('dbn-ui-lang') || 'en';
|
||||||
|
|
||||||
|
const TRANSCRIBE_I18N = {
|
||||||
|
en: {
|
||||||
|
engine: 'Engine',
|
||||||
|
engineGpuLabel: 'GPU (cuttlefish RTX 3060)',
|
||||||
|
engineOpenaiLabel: 'OpenAI Whisper API',
|
||||||
|
engineAzureLabel: 'Azure AI Speech (nb-NO)',
|
||||||
|
apiKey: 'API Key',
|
||||||
|
apiKeyHint: 'Used for this request only, never stored. Max 25 MB.',
|
||||||
|
region: 'Region',
|
||||||
|
model: 'Model',
|
||||||
|
modelFastest: 'Fastest',
|
||||||
|
modelBalanced: 'Balanced',
|
||||||
|
modelBest: 'Best quality',
|
||||||
|
transcribeLang: 'Audio language',
|
||||||
|
autoDetectHint: '(may confuse nb/da/sv)',
|
||||||
|
speakers: 'Speakers',
|
||||||
|
identifySpeakers: 'Identify speakers',
|
||||||
|
speakersCount: 'Count',
|
||||||
|
speakersPlaceholder: 'auto',
|
||||||
|
speakersAriaLabel: 'Expected number of speakers',
|
||||||
|
vocabulary: 'Vocabulary',
|
||||||
|
vocabPresetChildWelfare: 'Child welfare / CPS',
|
||||||
|
vocabPresetMediation: 'Mediation / legal meeting',
|
||||||
|
vocabPresetGeneral: 'General Norwegian',
|
||||||
|
vocabPresetCustom: 'Custom',
|
||||||
|
vocabPlaceholder: 'Technical terms and names for Whisper to recognise, e.g. Barnevernet, mediation, family services…',
|
||||||
|
vocabHint: 'Helps Whisper recognise technical terms. Not included in the transcript.',
|
||||||
|
uploadAria: 'Audio upload',
|
||||||
|
uploadDrop: 'Drop audio file(s) here, or',
|
||||||
|
uploadBrowse: 'browse',
|
||||||
|
uploadHint: 'max 200 MB per file',
|
||||||
|
uploadAddFiles: '+ Add files',
|
||||||
|
uploadClearQueue: '× Clear queue',
|
||||||
|
expertSettings: 'Advanced settings',
|
||||||
|
task: 'Task',
|
||||||
|
taskTranscribe: 'Transcribe',
|
||||||
|
taskTranslate: 'Translate to English',
|
||||||
|
beamSize: 'Beam size',
|
||||||
|
beamFastest: '(fastest)',
|
||||||
|
beamBest: '(best)',
|
||||||
|
vadFilter: 'VAD filter',
|
||||||
|
vadFilterLabel: 'Remove silence',
|
||||||
|
run: 'Run',
|
||||||
|
running: 'Transcribing…',
|
||||||
|
runningOther: 'Running…',
|
||||||
|
readyTitle: 'Ready',
|
||||||
|
readyDesc: 'Select a tool, run a request, and the result appears here.',
|
||||||
|
noFileSelected: 'Select at least one audio file before transcribing.',
|
||||||
|
missingOpenaiKey: 'Enter a valid OpenAI API key (sk-…) before running.',
|
||||||
|
openaiFileTooLarge: (f) => `OpenAI Whisper has a 25 MB limit. Use the GPU engine for ${f}.`,
|
||||||
|
missingAzureKey: 'Enter an Azure Speech API key before running.',
|
||||||
|
clipLabel: (i, total) => total > 1 ? `Clip ${i}/${total}` : 'Transcribing',
|
||||||
|
transcribeFailed: (s) => `Transcription failed (HTTP ${s}).`,
|
||||||
|
errorLabel: (clip) => `Error – ${clip}`,
|
||||||
|
filesSkipped: (names) => `Skipped: ${names}`,
|
||||||
|
fileSizeExceeded: (name, mb) => `${name} (${mb} MB — max 200 MB)`,
|
||||||
|
filesInQueue: (n) => `${n} file${n !== 1 ? 's' : ''} in queue.`,
|
||||||
|
done: (n, dur) => n > 1 ? `Done · ${n} clips · Total audio: ${dur}` : `Done · Audio: ${dur}`,
|
||||||
|
traceUploadLabel: (clip, eng) => `${clip} — uploading to ${eng}`,
|
||||||
|
traceUploadDetail: (eng) => eng === 'gpu' ? 'Sending audio to cuttlefish GPU…' : `Sending audio to ${eng}…`,
|
||||||
|
traceProcessingLabel: (clip, eng) => `${clip} — ${eng} transcribing`,
|
||||||
|
traceProcessingDetail: (eng) => eng === 'gpu' ? 'Whisper transcribing. Large files take 1–3 minutes.' : `${eng} processing audio.`,
|
||||||
|
traceStillLabel: (clip) => `${clip} — still processing…`,
|
||||||
|
traceStillDetail: (e) => { const m = Math.floor(e / 60), s = e % 60; return m > 0 ? `${m}m ${s}s elapsed — working through the audio.` : `${e}s elapsed — processing.`; },
|
||||||
|
},
|
||||||
|
no: {
|
||||||
|
engine: 'Motor',
|
||||||
|
engineGpuLabel: 'GPU (cuttlefish RTX 3060)',
|
||||||
|
engineOpenaiLabel: 'OpenAI Whisper API',
|
||||||
|
engineAzureLabel: 'Azure AI Speech (nb-NO)',
|
||||||
|
apiKey: 'API-nøkkel',
|
||||||
|
apiKeyHint: 'Brukes kun for denne forespørselen, lagres aldri. Maks 25 MB.',
|
||||||
|
region: 'Region',
|
||||||
|
model: 'Modell',
|
||||||
|
modelFastest: 'Raskest',
|
||||||
|
modelBalanced: 'Balansert',
|
||||||
|
modelBest: 'Beste kvalitet',
|
||||||
|
transcribeLang: 'Språk i lydfil',
|
||||||
|
autoDetectHint: '(kan forveksle nb/da/sv)',
|
||||||
|
speakers: 'Talere',
|
||||||
|
identifySpeakers: 'Skill ut talere',
|
||||||
|
speakersCount: 'Antall',
|
||||||
|
speakersPlaceholder: 'auto',
|
||||||
|
speakersAriaLabel: 'Forventet antall talere',
|
||||||
|
vocabulary: 'Ordliste',
|
||||||
|
vocabPresetChildWelfare: 'Barnerett / CPS',
|
||||||
|
vocabPresetMediation: 'Mekling / møter',
|
||||||
|
vocabPresetGeneral: 'Generell norsk',
|
||||||
|
vocabPresetCustom: 'Egendefinert',
|
||||||
|
vocabPlaceholder: 'Fagord og navn Whisper skal gjenkjenne, f.eks. Barnevernet, Fylkesnemnda, mekling…',
|
||||||
|
vocabHint: 'Hjelper Whisper gjenkjenne fagtermer. Ikke inkludert i utskriften.',
|
||||||
|
uploadAria: 'Lydopplasting',
|
||||||
|
uploadDrop: 'Slipp lydfil(er) her, eller',
|
||||||
|
uploadBrowse: 'bla',
|
||||||
|
uploadHint: 'maks 200 MB per fil',
|
||||||
|
uploadAddFiles: '+ Legg til filer',
|
||||||
|
uploadClearQueue: '× Tøm kø',
|
||||||
|
expertSettings: 'Ekspertinnstillinger',
|
||||||
|
task: 'Oppgave',
|
||||||
|
taskTranscribe: 'Transkriber',
|
||||||
|
taskTranslate: 'Oversett til engelsk',
|
||||||
|
beamSize: 'Beam size',
|
||||||
|
beamFastest: '(raskest)',
|
||||||
|
beamBest: '(best)',
|
||||||
|
vadFilter: 'VAD-filter',
|
||||||
|
vadFilterLabel: 'Fjern stillhet',
|
||||||
|
run: 'Kjør',
|
||||||
|
running: 'Transkriberer…',
|
||||||
|
runningOther: 'Kjører…',
|
||||||
|
readyTitle: 'Klar',
|
||||||
|
readyDesc: 'Velg et verktøy, kjør en forespørsel, og svaret vises her.',
|
||||||
|
noFileSelected: 'Velg minst én lydfil før transkripsjon.',
|
||||||
|
missingOpenaiKey: 'Legg inn en gyldig OpenAI API-nøkkel (sk-…) før du kjører.',
|
||||||
|
openaiFileTooLarge: (f) => `OpenAI Whisper har 25 MB-grense. Bruk GPU-motor for ${f}.`,
|
||||||
|
missingAzureKey: 'Legg inn Azure Speech API-nøkkel før du kjører.',
|
||||||
|
clipLabel: (i, total) => total > 1 ? `Klipp ${i}/${total}` : 'Transkriberer',
|
||||||
|
transcribeFailed: (s) => `Transkripsjon feilet (HTTP ${s}).`,
|
||||||
|
errorLabel: (clip) => `Feil – ${clip}`,
|
||||||
|
filesSkipped: (names) => `Hoppet over: ${names}`,
|
||||||
|
fileSizeExceeded: (name, mb) => `${name} (${mb} MB — maks 200 MB)`,
|
||||||
|
filesInQueue: (n) => `${n} fil${n !== 1 ? 'er' : ''} i køen.`,
|
||||||
|
done: (n, dur) => n > 1 ? `Ferdig · ${n} klipp · Total lyd: ${dur}` : `Ferdig · Lyd: ${dur}`,
|
||||||
|
traceUploadLabel: (clip, eng) => `${clip} — laster opp til ${eng}`,
|
||||||
|
traceUploadDetail: (eng) => eng === 'gpu' ? 'Sender lyd til cuttlefish GPU…' : `Sender lyd til ${eng}…`,
|
||||||
|
traceProcessingLabel: (clip, eng) => `${clip} — ${eng} transkriberer`,
|
||||||
|
traceProcessingDetail: (eng) => eng === 'gpu' ? 'Whisper transkriberer. Store filer tar 1–3 minutter.' : `${eng} behandler lyden.`,
|
||||||
|
traceStillLabel: (clip) => `${clip} — behandler fortsatt…`,
|
||||||
|
traceStillDetail: (e) => { const m = Math.floor(e / 60), s = e % 60; return m > 0 ? `${m} min ${s}s gått — jobber gjennom lyden.` : `${e}s gått — behandler.`; },
|
||||||
|
},
|
||||||
|
uk: {
|
||||||
|
engine: 'Рушій',
|
||||||
|
engineGpuLabel: 'GPU (cuttlefish RTX 3060)',
|
||||||
|
engineOpenaiLabel: 'OpenAI Whisper API',
|
||||||
|
engineAzureLabel: 'Azure AI Speech (nb-NO)',
|
||||||
|
apiKey: 'API-ключ',
|
||||||
|
apiKeyHint: 'Використовується лише для цього запиту, ніколи не зберігається. Макс 25 МБ.',
|
||||||
|
region: 'Регіон',
|
||||||
|
model: 'Модель',
|
||||||
|
modelFastest: 'Найшвидша',
|
||||||
|
modelBalanced: 'Збалансована',
|
||||||
|
modelBest: 'Найкраща якість',
|
||||||
|
transcribeLang: 'Мова аудіо',
|
||||||
|
autoDetectHint: '(може плутати nb/da/sv)',
|
||||||
|
speakers: 'Мовці',
|
||||||
|
identifySpeakers: 'Визначити мовців',
|
||||||
|
speakersCount: 'Кількість',
|
||||||
|
speakersPlaceholder: 'auto',
|
||||||
|
speakersAriaLabel: 'Очікувана кількість мовців',
|
||||||
|
vocabulary: 'Словник',
|
||||||
|
vocabPresetChildWelfare: 'Охорона дітей / CPS',
|
||||||
|
vocabPresetMediation: 'Медіація / зустріч',
|
||||||
|
vocabPresetGeneral: 'Загальна норвезька',
|
||||||
|
vocabPresetCustom: 'Власний',
|
||||||
|
vocabPlaceholder: 'Терміни та імена для Whisper, напр. Barnevernet, медіація…',
|
||||||
|
vocabHint: 'Допомагає Whisper розпізнати терміни. Не включається до транскрипту.',
|
||||||
|
uploadAria: 'Завантаження аудіо',
|
||||||
|
uploadDrop: 'Перетягніть файл(u) сюди, або',
|
||||||
|
uploadBrowse: 'огляд',
|
||||||
|
uploadHint: 'макс 200 МБ на файл',
|
||||||
|
uploadAddFiles: '+ Додати файли',
|
||||||
|
uploadClearQueue: '× Очистити чергу',
|
||||||
|
expertSettings: 'Розширені налаштування',
|
||||||
|
task: 'Завдання',
|
||||||
|
taskTranscribe: 'Транскрибувати',
|
||||||
|
taskTranslate: 'Перекласти англійською',
|
||||||
|
beamSize: 'Розмір пучка',
|
||||||
|
beamFastest: '(найшвидший)',
|
||||||
|
beamBest: '(найкращий)',
|
||||||
|
vadFilter: 'VAD-фільтр',
|
||||||
|
vadFilterLabel: 'Видалити тишу',
|
||||||
|
run: 'Запустити',
|
||||||
|
running: 'Транскрибування…',
|
||||||
|
runningOther: 'Виконання…',
|
||||||
|
readyTitle: 'Готово',
|
||||||
|
readyDesc: 'Виберіть інструмент, запустіть запит — результат з'явиться тут.',
|
||||||
|
noFileSelected: 'Виберіть хоч б один аудіофайл перед транскрибуванням.',
|
||||||
|
missingOpenaiKey: 'Введіть дійсний ключ OpenAI API (sk-…) перед запуском.',
|
||||||
|
openaiFileTooLarge: (f) => `OpenAI Whisper має обмеження 25 МБ. Використовуйте GPU для ${f}.`,
|
||||||
|
missingAzureKey: 'Введіть ключ Azure Speech API перед запуском.',
|
||||||
|
clipLabel: (i, total) => total > 1 ? `Кліп ${i}/${total}` : 'Транскрибування',
|
||||||
|
transcribeFailed: (s) => `Транскрибування не вдалося (HTTP ${s}).`,
|
||||||
|
errorLabel: (clip) => `Помилка – ${clip}`,
|
||||||
|
filesSkipped: (names) => `Пропущено: ${names}`,
|
||||||
|
fileSizeExceeded: (name, mb) => `${name} (${mb} МБ — макс 200 МБ)`,
|
||||||
|
filesInQueue: (n) => `${n} файл${n !== 1 ? 'ів' : ''} у черзі.`,
|
||||||
|
done: (n, dur) => n > 1 ? `Готово · ${n} кліпи · Загальне аудіо: ${dur}` : `Готово · Аудіо: ${dur}`,
|
||||||
|
traceUploadLabel: (clip, eng) => `${clip} — завантаження до ${eng}`,
|
||||||
|
traceUploadDetail: (eng) => eng === 'gpu' ? 'Відправка аудіо на cuttlefish GPU…' : `Відправка аудіо до ${eng}…`,
|
||||||
|
traceProcessingLabel: (clip, eng) => `${clip} — ${eng} транскрибує`,
|
||||||
|
traceProcessingDetail: (eng) => eng === 'gpu' ? 'Whisper транскрибує. Великі файли займають 1–3 хвилини.' : `${eng} обробляє аудіо.`,
|
||||||
|
traceStillLabel: (clip) => `${clip} — ще обробляється…`,
|
||||||
|
traceStillDetail: (e) => { const m = Math.floor(e / 60), s = e % 60; return m > 0 ? `Минуло ${m} хв ${s} с — обробка.` : `Минуло ${e} с — обробка.`; },
|
||||||
|
},
|
||||||
|
pl: {
|
||||||
|
engine: 'Silnik',
|
||||||
|
engineGpuLabel: 'GPU (cuttlefish RTX 3060)',
|
||||||
|
engineOpenaiLabel: 'OpenAI Whisper API',
|
||||||
|
engineAzureLabel: 'Azure AI Speech (nb-NO)',
|
||||||
|
apiKey: 'Klucz API',
|
||||||
|
apiKeyHint: 'Używany tylko dla tego żądania, nigdy nie przechowywany. Maks 25 MB.',
|
||||||
|
region: 'Region',
|
||||||
|
model: 'Model',
|
||||||
|
modelFastest: 'Najszybszy',
|
||||||
|
modelBalanced: 'Zrównoważony',
|
||||||
|
modelBest: 'Najlepsza jakość',
|
||||||
|
transcribeLang: 'Język audio',
|
||||||
|
autoDetectHint: '(może mylić nb/da/sv)',
|
||||||
|
speakers: 'Mówcy',
|
||||||
|
identifySpeakers: 'Rozróżnij mówców',
|
||||||
|
speakersCount: 'Liczba',
|
||||||
|
speakersPlaceholder: 'auto',
|
||||||
|
speakersAriaLabel: 'Oczekiwana liczba mówców',
|
||||||
|
vocabulary: 'Słownik',
|
||||||
|
vocabPresetChildWelfare: 'Opieka nad dziećmi / CPS',
|
||||||
|
vocabPresetMediation: 'Mediacja / spotkanie',
|
||||||
|
vocabPresetGeneral: 'Ogólny norweski',
|
||||||
|
vocabPresetCustom: 'Własny',
|
||||||
|
vocabPlaceholder: 'Terminy i nazwy dla Whisper, np. Barnevernet, mediacja…',
|
||||||
|
vocabHint: 'Pomaga Whisper rozpoznać terminy. Nie jest uwzględniony w transkrypcji.',
|
||||||
|
uploadAria: 'Prześylanie audio',
|
||||||
|
uploadDrop: 'Upuść plik(i) audio tutaj lub',
|
||||||
|
uploadBrowse: 'przeglądaj',
|
||||||
|
uploadHint: 'maks 200 MB na plik',
|
||||||
|
uploadAddFiles: '+ Dodaj pliki',
|
||||||
|
uploadClearQueue: '× Wyczyść kolejkę',
|
||||||
|
expertSettings: 'Ustawienia zaawansowane',
|
||||||
|
task: 'Zadanie',
|
||||||
|
taskTranscribe: 'Transkrybuj',
|
||||||
|
taskTranslate: 'Przetłumacz na angielski',
|
||||||
|
beamSize: 'Rozmiar wiązki',
|
||||||
|
beamFastest: '(najszybszy)',
|
||||||
|
beamBest: '(najlepszy)',
|
||||||
|
vadFilter: 'Filtr VAD',
|
||||||
|
vadFilterLabel: 'Usuń ciszę',
|
||||||
|
run: 'Uruchom',
|
||||||
|
running: 'Transkrybowanie…',
|
||||||
|
runningOther: 'Uruchamianie…',
|
||||||
|
readyTitle: 'Gotowe',
|
||||||
|
readyDesc: 'Wybierz narzędzie, uruchom żądanie — wynik pojawi się tutaj.',
|
||||||
|
noFileSelected: 'Wybierz co najmniej jeden plik audio przed transkrypcją.',
|
||||||
|
missingOpenaiKey: 'Wprowadź prawidłowy klucz API OpenAI (sk-…) przed uruchomieniem.',
|
||||||
|
openaiFileTooLarge: (f) => `OpenAI Whisper ma limit 25 MB. Użyj silnika GPU dla ${f}.`,
|
||||||
|
missingAzureKey: 'Wprowadź klucz Azure Speech API przed uruchomieniem.',
|
||||||
|
clipLabel: (i, total) => total > 1 ? `Klip ${i}/${total}` : 'Transkrybowanie',
|
||||||
|
transcribeFailed: (s) => `Transkrypcja nie powiodła się (HTTP ${s}).`,
|
||||||
|
errorLabel: (clip) => `Błąd – ${clip}`,
|
||||||
|
filesSkipped: (names) => `Pominięto: ${names}`,
|
||||||
|
fileSizeExceeded: (name, mb) => `${name} (${mb} MB — maks 200 MB)`,
|
||||||
|
filesInQueue: (n) => `${n} plik${n !== 1 ? 'i' : ''} w kolejce.`,
|
||||||
|
done: (n, dur) => n > 1 ? `Gotowe · ${n} klipy · Łączne audio: ${dur}` : `Gotowe · Audio: ${dur}`,
|
||||||
|
traceUploadLabel: (clip, eng) => `${clip} — przesyłanie do ${eng}`,
|
||||||
|
traceUploadDetail: (eng) => eng === 'gpu' ? 'Wysyłanie audio do cuttlefish GPU…' : `Wysyłanie audio do ${eng}…`,
|
||||||
|
traceProcessingLabel: (clip, eng) => `${clip} — ${eng} transkrybuje`,
|
||||||
|
traceProcessingDetail: (eng) => eng === 'gpu' ? 'Whisper transkrybuje. Duże pliki zajmują 1–3 minuty.' : `${eng} przetwarza audio.`,
|
||||||
|
traceStillLabel: (clip) => `${clip} — nadal przetwarza…`,
|
||||||
|
traceStillDetail: (e) => { const m = Math.floor(e / 60), s = e % 60; return m > 0 ? `Minęło ${m} min ${s} s — przetwarzanie audio.` : `Minęło ${e} s — przetwarzanie.`; },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function currentUiT(key, ...args) {
|
||||||
|
const t = TRANSCRIBE_I18N[uiLang] || TRANSCRIBE_I18N.en;
|
||||||
|
const val = (key in t) ? t[key] : TRANSCRIBE_I18N.en[key];
|
||||||
|
if (typeof val === 'function') return val(...args);
|
||||||
|
return val ?? key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTranscribeI18n(lang) {
|
||||||
|
uiLang = lang;
|
||||||
|
localStorage.setItem('dbn-ui-lang', lang);
|
||||||
|
document.querySelectorAll('[data-i18n]').forEach((el) => {
|
||||||
|
const text = currentUiT(el.dataset.i18n);
|
||||||
|
if (text != null) el.textContent = text;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-i18n-placeholder]').forEach((el) => {
|
||||||
|
const text = currentUiT(el.dataset.i18nPlaceholder);
|
||||||
|
if (text != null) el.placeholder = text;
|
||||||
|
});
|
||||||
|
document.querySelectorAll('[data-i18n-aria]').forEach((el) => {
|
||||||
|
const text = currentUiT(el.dataset.i18nAria);
|
||||||
|
if (text != null) el.setAttribute('aria-label', text);
|
||||||
|
});
|
||||||
|
document.querySelectorAll('.lang-btn').forEach((btn) => {
|
||||||
|
btn.classList.toggle('is-active', btn.dataset.lang === lang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const tools = {
|
const tools = {
|
||||||
ask: {
|
ask: {
|
||||||
kind: 'Source-grounded Legal Ask',
|
kind: 'Source-grounded Legal Ask',
|
||||||
@@ -137,6 +425,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
setupAudio();
|
setupAudio();
|
||||||
setupTranscribeControls();
|
setupTranscribeControls();
|
||||||
setupVocabPresets();
|
setupVocabPresets();
|
||||||
|
document.querySelectorAll('.lang-btn').forEach((btn) => {
|
||||||
|
btn.addEventListener('click', () => applyTranscribeI18n(btn.dataset.lang));
|
||||||
|
});
|
||||||
|
applyTranscribeI18n(uiLang);
|
||||||
els.results.addEventListener('click', (e) => {
|
els.results.addEventListener('click', (e) => {
|
||||||
if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents);
|
if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents);
|
||||||
if (e.target.closest('#dlTxt')) downloadTranscriptTxt();
|
if (e.target.closest('#dlTxt')) downloadTranscriptTxt();
|
||||||
@@ -443,8 +735,8 @@ function setBusy(isBusy) {
|
|||||||
const button = document.querySelector('#runButton');
|
const button = document.querySelector('#runButton');
|
||||||
button.disabled = isBusy;
|
button.disabled = isBusy;
|
||||||
button.textContent = isBusy
|
button.textContent = isBusy
|
||||||
? (state.activeTool === 'transcribe' ? 'Transkriberer...' : 'Kjører...')
|
? (state.activeTool === 'transcribe' ? currentUiT('running') : currentUiT('runningOther'))
|
||||||
: 'Kjør';
|
: currentUiT('run');
|
||||||
}
|
}
|
||||||
|
|
||||||
function currentLanguage() {
|
function currentLanguage() {
|
||||||
@@ -591,7 +883,7 @@ function currentTask() {
|
|||||||
|
|
||||||
async function runTranscribe() {
|
async function runTranscribe() {
|
||||||
if (!audioQueue.length) {
|
if (!audioQueue.length) {
|
||||||
els.status.textContent = 'Velg minst én lydfil før transkripsjon.';
|
els.status.textContent = currentUiT('noFileSelected');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -600,19 +892,19 @@ async function runTranscribe() {
|
|||||||
if (engine === 'openai') {
|
if (engine === 'openai') {
|
||||||
const key = document.getElementById('openaiKeyInput')?.value?.trim();
|
const key = document.getElementById('openaiKeyInput')?.value?.trim();
|
||||||
if (!key || !key.startsWith('sk-')) {
|
if (!key || !key.startsWith('sk-')) {
|
||||||
els.status.textContent = 'Legg inn en gyldig OpenAI API-nøkkel (sk-…) før du kjører.';
|
els.status.textContent = currentUiT('missingOpenaiKey');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const oversized = audioQueue.find((item) => item.file.size > 25 * 1024 * 1024);
|
const oversized = audioQueue.find((item) => item.file.size > 25 * 1024 * 1024);
|
||||||
if (oversized) {
|
if (oversized) {
|
||||||
els.status.textContent = `OpenAI Whisper har 25 MB-grense. Bruk GPU-motor for ${oversized.file.name}.`;
|
els.status.textContent = currentUiT('openaiFileTooLarge', oversized.file.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (engine === 'azure') {
|
if (engine === 'azure') {
|
||||||
const key = document.getElementById('azureKeyInput')?.value?.trim();
|
const key = document.getElementById('azureKeyInput')?.value?.trim();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
els.status.textContent = 'Legg inn Azure Speech API-nøkkel før du kjører.';
|
els.status.textContent = currentUiT('missingAzureKey');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -642,7 +934,7 @@ async function runTranscribe() {
|
|||||||
|
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
let elapsed = 0;
|
let elapsed = 0;
|
||||||
const clipLabel = total > 1 ? `Klipp ${i + 1}/${total}` : 'Transkriberer';
|
const clipLabel = currentUiT('clipLabel', i + 1, total);
|
||||||
els.status.textContent = `${clipLabel}…`;
|
els.status.textContent = `${clipLabel}…`;
|
||||||
|
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@@ -684,7 +976,7 @@ async function runTranscribe() {
|
|||||||
});
|
});
|
||||||
const data = await resp.json().catch(() => ({}));
|
const data = await resp.json().catch(() => ({}));
|
||||||
if (!resp.ok || !data.ok) {
|
if (!resp.ok || !data.ok) {
|
||||||
throw new Error(data.error?.message || `Transkripsjon feilet (HTTP ${resp.status}).`);
|
throw new Error(data.error?.message || currentUiT('transcribeFailed', resp.status));
|
||||||
}
|
}
|
||||||
|
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
@@ -708,7 +1000,7 @@ async function runTranscribe() {
|
|||||||
item.status = 'error';
|
item.status = 'error';
|
||||||
renderAudioQueue();
|
renderAudioQueue();
|
||||||
els.status.textContent = `${clipLabel}: ${err.message}`;
|
els.status.textContent = `${clipLabel}: ${err.message}`;
|
||||||
renderTrace([{ label: `Feil – ${clipLabel}`, detail: err.message, status: 'warning' }]);
|
renderTrace([{ label: currentUiT('errorLabel', clipLabel), detail: err.message, status: 'warning' }]);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -733,28 +1025,23 @@ async function runTranscribe() {
|
|||||||
const totalMin = Math.floor(totalSec / 60);
|
const totalMin = Math.floor(totalSec / 60);
|
||||||
const remSec = totalSec % 60;
|
const remSec = totalSec % 60;
|
||||||
const durLabel = totalMin > 0 ? `${totalMin}m ${remSec}s` : `${totalSec}s`;
|
const durLabel = totalMin > 0 ? `${totalMin}m ${remSec}s` : `${totalSec}s`;
|
||||||
const clipCount = total > 1 ? ` · ${total} klipp` : '';
|
els.status.textContent = currentUiT('done', total, durLabel);
|
||||||
els.status.textContent = `Ferdig${clipCount} · Total lyd: ${durLabel}`;
|
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateTranscribeTrace(elapsed, engine, clipLabel = 'Transkriberer') {
|
function updateTranscribeTrace(elapsed, engine, clipLabel) {
|
||||||
|
if (!clipLabel) clipLabel = currentUiT('clipLabel', 1, 1);
|
||||||
const engineLabel = engine === 'openai' ? 'OpenAI API' : engine === 'azure' ? 'Azure Speech' : 'Whisper GPU';
|
const engineLabel = engine === 'openai' ? 'OpenAI API' : engine === 'azure' ? 'Azure Speech' : 'Whisper GPU';
|
||||||
let label, detail;
|
let label, detail;
|
||||||
if (elapsed < 10) {
|
if (elapsed < 10) {
|
||||||
label = `${clipLabel} — laster opp til ${engineLabel}`;
|
label = currentUiT('traceUploadLabel', clipLabel, engineLabel);
|
||||||
detail = engine === 'gpu' ? 'Sender lyd til cuttlefish GPU…' : `Sender lyd til ${engineLabel}…`;
|
detail = currentUiT('traceUploadDetail', engine);
|
||||||
} else if (elapsed < 60) {
|
} else if (elapsed < 60) {
|
||||||
label = `${clipLabel} — ${engineLabel} transkriberer`;
|
label = currentUiT('traceProcessingLabel', clipLabel, engineLabel);
|
||||||
detail = engine === 'gpu'
|
detail = currentUiT('traceProcessingDetail', engine);
|
||||||
? 'Whisper transkriberer. Store filer tar 1–3 minutter.'
|
|
||||||
: `${engineLabel} behandler lyden.`;
|
|
||||||
} else if (elapsed < 120) {
|
|
||||||
label = `${clipLabel} — behandler fortsatt…`;
|
|
||||||
detail = `${Math.floor(elapsed / 60)} min gått — ${engineLabel} jobber gjennom lyden.`;
|
|
||||||
} else {
|
} else {
|
||||||
label = `${clipLabel} — behandler fortsatt…`;
|
label = currentUiT('traceStillLabel', clipLabel);
|
||||||
detail = `${Math.floor(elapsed / 60)} min ${pad2(elapsed % 60)}s — lange opptak tar flere minutter.`;
|
detail = currentUiT('traceStillDetail', elapsed);
|
||||||
}
|
}
|
||||||
renderTrace([{ label, detail, status: 'running' }]);
|
renderTrace([{ label, detail, status: 'running' }]);
|
||||||
}
|
}
|
||||||
@@ -955,7 +1242,7 @@ function handleAudioFiles(fileList) {
|
|||||||
}
|
}
|
||||||
const sizeMB = file.size / 1024 / 1024;
|
const sizeMB = file.size / 1024 / 1024;
|
||||||
if (sizeMB > 200) {
|
if (sizeMB > 200) {
|
||||||
skipped.push(`${file.name} (${sizeMB.toFixed(1)} MB — maks 200 MB)`);
|
skipped.push(currentUiT('fileSizeExceeded', file.name, sizeMB.toFixed(1)));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
audioQueue.push({ file, status: 'pending', result: null });
|
audioQueue.push({ file, status: 'pending', result: null });
|
||||||
@@ -963,9 +1250,9 @@ function handleAudioFiles(fileList) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (skipped.length) {
|
if (skipped.length) {
|
||||||
els.status.textContent = `Hoppet over: ${skipped.join(', ')}`;
|
els.status.textContent = currentUiT('filesSkipped', skipped.join(', '));
|
||||||
} else if (added > 0) {
|
} else if (added > 0) {
|
||||||
els.status.textContent = `${audioQueue.length} fil${audioQueue.length !== 1 ? 'er' : ''} i køen.`;
|
els.status.textContent = currentUiT('filesInQueue', audioQueue.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
renderAudioQueue();
|
renderAudioQueue();
|
||||||
|
|||||||
+51
-42
@@ -8,97 +8,106 @@ require_once __DIR__ . '/includes/layout.php';
|
|||||||
?>
|
?>
|
||||||
<form id="toolForm" class="tool-form">
|
<form id="toolForm" class="tool-form">
|
||||||
|
|
||||||
|
<div class="lang-switcher" id="uiLangSwitcher" role="group" aria-label="UI language">
|
||||||
|
<button type="button" class="lang-btn is-active" data-lang="en">🇬🇧 EN</button>
|
||||||
|
<button type="button" class="lang-btn" data-lang="no">🇳🇴 NO</button>
|
||||||
|
<button type="button" class="lang-btn" data-lang="uk">🇺🇦 UK</button>
|
||||||
|
<button type="button" class="lang-btn" data-lang="pl">🇵🇱 PL</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="control-row" id="engineControl">
|
<div class="control-row" id="engineControl">
|
||||||
<span class="control-label">Engine</span>
|
<span class="control-label" data-i18n="engine">Engine</span>
|
||||||
<label><input type="radio" name="engine" value="gpu" checked id="engineGpu"> GPU (cuttlefish RTX 3060)</label>
|
<label><input type="radio" name="engine" value="gpu" checked id="engineGpu"> <span data-i18n="engineGpuLabel">GPU (cuttlefish RTX 3060)</span></label>
|
||||||
<label><input type="radio" name="engine" value="openai" id="engineOpenai"> OpenAI Whisper API</label>
|
<label><input type="radio" name="engine" value="openai" id="engineOpenai"> <span data-i18n="engineOpenaiLabel">OpenAI Whisper API</span></label>
|
||||||
<label><input type="radio" name="engine" value="azure" id="engineAzure"> Azure AI Speech (nb-NO)</label>
|
<label><input type="radio" name="engine" value="azure" id="engineAzure"> <span data-i18n="engineAzureLabel">Azure AI Speech (nb-NO)</span></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-row is-hidden" id="openaiKeyControl">
|
<div class="control-row is-hidden" id="openaiKeyControl">
|
||||||
<span class="control-label">API Key</span>
|
<span class="control-label" data-i18n="apiKey">API Key</span>
|
||||||
<input type="password" id="openaiKeyInput" name="openai_key" placeholder="sk-…" class="byok-input" autocomplete="off">
|
<input type="password" id="openaiKeyInput" name="openai_key" placeholder="sk-…" class="byok-input" autocomplete="off">
|
||||||
<small class="control-hint inline-hint">Used for this request only, never stored. Max 25 MB.</small>
|
<small class="control-hint inline-hint" data-i18n="apiKeyHint">Used for this request only, never stored. Max 25 MB.</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-row is-hidden" id="azureKeyControl">
|
<div class="control-row is-hidden" id="azureKeyControl">
|
||||||
<span class="control-label">API Key</span>
|
<span class="control-label" data-i18n="apiKey">API Key</span>
|
||||||
<input type="password" id="azureKeyInput" name="azure_key" placeholder="Azure Speech key" class="byok-input" autocomplete="off">
|
<input type="password" id="azureKeyInput" name="azure_key" placeholder="Azure Speech key" class="byok-input" autocomplete="off">
|
||||||
<span class="control-label" style="margin-left:1.25rem">Region</span>
|
<span class="control-label" style="margin-left:1.25rem" data-i18n="region">Region</span>
|
||||||
<input type="text" id="azureRegionInput" name="azure_region" placeholder="norwayeast" class="byok-input byok-input--short" value="norwayeast">
|
<input type="text" id="azureRegionInput" name="azure_region" placeholder="norwayeast" class="byok-input byok-input--short" value="norwayeast">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-row" id="modelControl">
|
<div class="control-row" id="modelControl">
|
||||||
<span class="control-label">Model</span>
|
<span class="control-label" data-i18n="model">Model</span>
|
||||||
<label><input type="radio" name="model" value="small"> Raskest <small class="control-hint">(small)</small></label>
|
<label><input type="radio" name="model" value="small"> <span data-i18n="modelFastest">Fastest</span> <small class="control-hint">(small)</small></label>
|
||||||
<label><input type="radio" name="model" value="medium"> Balansert <small class="control-hint">(medium)</small></label>
|
<label><input type="radio" name="model" value="medium"> <span data-i18n="modelBalanced">Balanced</span> <small class="control-hint">(medium)</small></label>
|
||||||
<label><input type="radio" name="model" value="large-v3" checked> Beste kvalitet ★ <small class="control-hint">(large-v3)</small></label>
|
<label><input type="radio" name="model" value="large-v3" checked> <span data-i18n="modelBest">Best quality</span> ★ <small class="control-hint">(large-v3)</small></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-row" id="transcribeLangControl">
|
<div class="control-row" id="transcribeLangControl">
|
||||||
<span class="control-label">Språk</span>
|
<span class="control-label" data-i18n="transcribeLang">Audio language</span>
|
||||||
<label><input type="radio" name="transcribeLang" value="no" checked> Norsk (nb)</label>
|
<label><input type="radio" name="transcribeLang" value="no" checked> Norsk (nb)</label>
|
||||||
<label><input type="radio" name="transcribeLang" value="nn"> Nynorsk</label>
|
<label><input type="radio" name="transcribeLang" value="nn"> Nynorsk</label>
|
||||||
<label><input type="radio" name="transcribeLang" value="en"> English</label>
|
<label><input type="radio" name="transcribeLang" value="en"> English</label>
|
||||||
|
<label><input type="radio" name="transcribeLang" value="pl"> Polski</label>
|
||||||
|
<label><input type="radio" name="transcribeLang" value="uk"> Українська</label>
|
||||||
<label><input type="radio" name="transcribeLang" value="sv"> Svenska</label>
|
<label><input type="radio" name="transcribeLang" value="sv"> Svenska</label>
|
||||||
<label><input type="radio" name="transcribeLang" value="da"> Dansk</label>
|
<label><input type="radio" name="transcribeLang" value="da"> Dansk</label>
|
||||||
<label><input type="radio" name="transcribeLang" value="de"> Deutsch</label>
|
<label><input type="radio" name="transcribeLang" value="de"> Deutsch</label>
|
||||||
<label><input type="radio" name="transcribeLang" value="fr"> Français</label>
|
<label><input type="radio" name="transcribeLang" value="fr"> Français</label>
|
||||||
<label><input type="radio" name="transcribeLang" value="auto"> Auto-detect <small class="control-hint">(kan forveksle nb/da/sv)</small></label>
|
<label><input type="radio" name="transcribeLang" value="auto"> Auto-detect <small class="control-hint" data-i18n="autoDetectHint">(may confuse nb/da/sv)</small></label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="control-row" id="diarizeControl">
|
<div class="control-row" id="diarizeControl">
|
||||||
<span class="control-label">Talere</span>
|
<span class="control-label" data-i18n="speakers">Speakers</span>
|
||||||
<label><input type="checkbox" id="diarizeCheck" name="diarize"> Skill ut talere</label>
|
<label><input type="checkbox" id="diarizeCheck" name="diarize"> <span data-i18n="identifySpeakers">Identify speakers</span></label>
|
||||||
<span class="control-label" style="margin-left:1.25rem">Antall</span>
|
<span class="control-label" style="margin-left:1.25rem" data-i18n="speakersCount">Count</span>
|
||||||
<input type="number" id="numSpeakersInput" name="num_speakers" min="2" max="20" placeholder="auto" class="num-speakers-input" aria-label="Forventet antall talere">
|
<input type="number" id="numSpeakersInput" name="num_speakers" min="2" max="20" placeholder="auto" class="num-speakers-input" data-i18n-placeholder="speakersPlaceholder" data-i18n-aria="speakersAriaLabel" aria-label="Expected number of speakers">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="expert-field" id="vocabControl">
|
<div class="expert-field" id="vocabControl">
|
||||||
<div class="vocab-presets" id="vocabPresets">
|
<div class="vocab-presets" id="vocabPresets">
|
||||||
<span class="control-label">Ordliste</span>
|
<span class="control-label" data-i18n="vocabulary">Vocabulary</span>
|
||||||
<button type="button" class="vocab-btn" data-preset="barnerett">Barnerett / CPS</button>
|
<button type="button" class="vocab-btn" data-preset="barnerett" data-i18n="vocabPresetChildWelfare">Child welfare / CPS</button>
|
||||||
<button type="button" class="vocab-btn" data-preset="rettssak">Rettssak / tingrett</button>
|
<button type="button" class="vocab-btn" data-preset="mediation" data-i18n="vocabPresetMediation">Mediation / legal meeting</button>
|
||||||
<button type="button" class="vocab-btn" data-preset="generell">Generell norsk</button>
|
<button type="button" class="vocab-btn" data-preset="generell" data-i18n="vocabPresetGeneral">General Norwegian</button>
|
||||||
<button type="button" class="vocab-btn" data-preset="custom">Egendefinert</button>
|
<button type="button" class="vocab-btn" data-preset="custom" data-i18n="vocabPresetCustom">Custom</button>
|
||||||
</div>
|
</div>
|
||||||
<textarea id="initPromptInput" name="initial_prompt" rows="2" placeholder="Fagord og navn Whisper skal gjenkjenne, f.eks. Barnevernet, Fylkesnemnda, advokat, tingrett…" class="prompt-textarea"></textarea>
|
<textarea id="initPromptInput" name="initial_prompt" rows="2" placeholder="Technical terms and names for Whisper to recognise, e.g. Barnevernet, mediation, family services…" class="prompt-textarea" data-i18n-placeholder="vocabPlaceholder"></textarea>
|
||||||
<p class="upload-hint">Hjelper Whisper gjenkjenne fagtermer. Ikke inkludert i utskriften.</p>
|
<p class="upload-hint" data-i18n="vocabHint">Helps Whisper recognise technical terms. Not included in the transcript.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="upload-zone" id="audioZone" role="region" aria-label="Audio upload">
|
<div class="upload-zone" id="audioZone" role="region" aria-label="Audio upload" data-i18n-aria="uploadAria">
|
||||||
<input type="file" id="audioInput" accept="audio/*,video/mp4,video/webm" multiple aria-label="Choose audio files">
|
<input type="file" id="audioInput" accept="audio/*,video/mp4,video/webm" multiple aria-label="Choose audio files">
|
||||||
<div id="audioPrompt" class="upload-prompt">
|
<div id="audioPrompt" class="upload-prompt">
|
||||||
<span class="upload-icon" aria-hidden="true">▶</span>
|
<span class="upload-icon" aria-hidden="true">▶</span>
|
||||||
<p>Slipp lydfil(er) her, eller <label for="audioInput" class="upload-browse">bla</label></p>
|
<p><span data-i18n="uploadDrop">Drop audio file(s) here, or</span> <label for="audioInput" class="upload-browse" data-i18n="uploadBrowse">browse</label></p>
|
||||||
<p class="upload-hint"><strong>MP3</strong>, <strong>WAV</strong>, <strong>OGG</strong>, <strong>M4A</strong>, <strong>FLAC</strong>, <strong>WEBM</strong> — maks 200 MB per fil</p>
|
<p class="upload-hint"><strong>MP3</strong>, <strong>WAV</strong>, <strong>OGG</strong>, <strong>M4A</strong>, <strong>FLAC</strong>, <strong>WEBM</strong> — <span data-i18n="uploadHint">max 200 MB per file</span></p>
|
||||||
</div>
|
</div>
|
||||||
<div id="audioFileInfo" class="upload-file is-hidden">
|
<div id="audioFileInfo" class="upload-file is-hidden">
|
||||||
<ol id="audioQueueList" class="audio-queue-list"></ol>
|
<ol id="audioQueueList" class="audio-queue-list"></ol>
|
||||||
<div class="audio-queue-actions">
|
<div class="audio-queue-actions">
|
||||||
<label for="audioInput" class="upload-browse">+ Legg til filer</label>
|
<label for="audioInput" class="upload-browse" data-i18n="uploadAddFiles">+ Add files</label>
|
||||||
<button type="button" id="audioClear" class="upload-clear" aria-label="Tøm kø">× Tøm kø</button>
|
<button type="button" id="audioClear" class="upload-clear" data-i18n="uploadClearQueue">× Clear queue</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<details class="expert-settings" id="expertSettings">
|
<details class="expert-settings" id="expertSettings">
|
||||||
<summary class="expert-summary">Ekspertinnstillinger</summary>
|
<summary class="expert-summary" data-i18n="expertSettings">Advanced settings</summary>
|
||||||
<div class="expert-body">
|
<div class="expert-body">
|
||||||
<div class="control-row">
|
<div class="control-row">
|
||||||
<span class="control-label">Oppgave</span>
|
<span class="control-label" data-i18n="task">Task</span>
|
||||||
<label><input type="radio" name="task" value="transcribe" checked> Transkriber</label>
|
<label><input type="radio" name="task" value="transcribe" checked> <span data-i18n="taskTranscribe">Transcribe</span></label>
|
||||||
<label><input type="radio" name="task" value="translate"> Oversett til engelsk</label>
|
<label><input type="radio" name="task" value="translate"> <span data-i18n="taskTranslate">Translate to English</span></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-row">
|
<div class="control-row">
|
||||||
<span class="control-label">Beam size</span>
|
<span class="control-label" data-i18n="beamSize">Beam size</span>
|
||||||
<label><input type="radio" name="beam_size" value="1"> 1 <small class="control-hint">(raskest)</small></label>
|
<label><input type="radio" name="beam_size" value="1"> 1 <small class="control-hint" data-i18n="beamFastest">(fastest)</small></label>
|
||||||
<label><input type="radio" name="beam_size" value="3"> 3</label>
|
<label><input type="radio" name="beam_size" value="3"> 3</label>
|
||||||
<label><input type="radio" name="beam_size" value="5" checked> 5 <small class="control-hint">(best)</small></label>
|
<label><input type="radio" name="beam_size" value="5" checked> 5 <small class="control-hint" data-i18n="beamBest">(best)</small></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="control-row">
|
<div class="control-row">
|
||||||
<span class="control-label">VAD-filter</span>
|
<span class="control-label" data-i18n="vadFilter">VAD filter</span>
|
||||||
<label><input type="checkbox" name="vad_filter" id="vadFilterCheck" value="1" checked> Fjern stillhet</label>
|
<label><input type="checkbox" name="vad_filter" id="vadFilterCheck" value="1" checked> <span data-i18n="vadFilterLabel">Remove silence</span></label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -120,14 +129,14 @@ require_once __DIR__ . '/includes/layout.php';
|
|||||||
|
|
||||||
<div class="form-footer">
|
<div class="form-footer">
|
||||||
<p id="toolStatus" class="form-status" role="status" aria-live="polite"></p>
|
<p id="toolStatus" class="form-status" role="status" aria-live="polite"></p>
|
||||||
<button id="runButton" type="submit">Kjør</button>
|
<button id="runButton" type="submit" data-i18n="run">Run</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<section id="results" class="results" aria-live="polite">
|
<section id="results" class="results" aria-live="polite">
|
||||||
<div class="empty-state">
|
<div class="empty-state">
|
||||||
<h3>Klar</h3>
|
<h3 data-i18n="readyTitle">Ready</h3>
|
||||||
<p>Velg et verktøy, kjør en forespørsel, og svaret vises her.</p>
|
<p data-i18n="readyDesc">Select a tool, run a request, and the result appears here.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<?php require_once __DIR__ . '/includes/layout_footer.php'; ?>
|
<?php require_once __DIR__ . '/includes/layout_footer.php'; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user