diff --git a/api/transcribe.php b/api/transcribe.php index e9787f5..040805b 100644 --- a/api/transcribe.php +++ b/api/transcribe.php @@ -6,9 +6,12 @@ require_once __DIR__ . '/../includes/LegalTools.php'; dbnToolsRequireMethod('POST'); dbnToolsRequireAuth(); +set_time_limit(0); +ignore_user_abort(true); + // ── 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'))); if (!in_array($language, $validLangs, true)) $language = 'auto'; diff --git a/assets/css/tools.css b/assets/css/tools.css index 644ee6b..fe125b9 100644 --- a/assets/css/tools.css +++ b/assets/css/tools.css @@ -1300,6 +1300,42 @@ p { 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 { font-size: 0.78rem; padding: 0.2rem 0.6rem; diff --git a/assets/js/tools.js b/assets/js/tools.js index 99a1b3a..a0769ae 100644 --- a/assets/js/tools.js +++ b/assets/js/tools.js @@ -9,11 +9,299 @@ let lastTranscriptData = null; 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', - 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', 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 = { ask: { kind: 'Source-grounded Legal Ask', @@ -137,6 +425,10 @@ document.addEventListener('DOMContentLoaded', () => { setupAudio(); setupTranscribeControls(); setupVocabPresets(); + document.querySelectorAll('.lang-btn').forEach((btn) => { + btn.addEventListener('click', () => applyTranscribeI18n(btn.dataset.lang)); + }); + applyTranscribeI18n(uiLang); els.results.addEventListener('click', (e) => { if (e.target.closest('#exportCsvBtn')) exportTimelineCSV(lastTimelineEvents); if (e.target.closest('#dlTxt')) downloadTranscriptTxt(); @@ -443,8 +735,8 @@ function setBusy(isBusy) { const button = document.querySelector('#runButton'); button.disabled = isBusy; button.textContent = isBusy - ? (state.activeTool === 'transcribe' ? 'Transkriberer...' : 'Kjører...') - : 'Kjør'; + ? (state.activeTool === 'transcribe' ? currentUiT('running') : currentUiT('runningOther')) + : currentUiT('run'); } function currentLanguage() { @@ -591,7 +883,7 @@ function currentTask() { async function runTranscribe() { if (!audioQueue.length) { - els.status.textContent = 'Velg minst én lydfil før transkripsjon.'; + els.status.textContent = currentUiT('noFileSelected'); return; } @@ -600,19 +892,19 @@ async function runTranscribe() { if (engine === 'openai') { const key = document.getElementById('openaiKeyInput')?.value?.trim(); 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; } const oversized = audioQueue.find((item) => item.file.size > 25 * 1024 * 1024); 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; } } if (engine === 'azure') { const key = document.getElementById('azureKeyInput')?.value?.trim(); if (!key) { - els.status.textContent = 'Legg inn Azure Speech API-nøkkel før du kjører.'; + els.status.textContent = currentUiT('missingAzureKey'); return; } } @@ -642,7 +934,7 @@ async function runTranscribe() { const startTime = Date.now(); let elapsed = 0; - const clipLabel = total > 1 ? `Klipp ${i + 1}/${total}` : 'Transkriberer'; + const clipLabel = currentUiT('clipLabel', i + 1, total); els.status.textContent = `${clipLabel}…`; const timer = setInterval(() => { @@ -684,7 +976,7 @@ async function runTranscribe() { }); const data = await resp.json().catch(() => ({})); 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); @@ -708,7 +1000,7 @@ async function runTranscribe() { item.status = 'error'; renderAudioQueue(); 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); return; } @@ -733,28 +1025,23 @@ async function runTranscribe() { const totalMin = Math.floor(totalSec / 60); const remSec = totalSec % 60; const durLabel = totalMin > 0 ? `${totalMin}m ${remSec}s` : `${totalSec}s`; - const clipCount = total > 1 ? ` · ${total} klipp` : ''; - els.status.textContent = `Ferdig${clipCount} · Total lyd: ${durLabel}`; + els.status.textContent = currentUiT('done', total, durLabel); 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'; let label, detail; if (elapsed < 10) { - label = `${clipLabel} — laster opp til ${engineLabel}`; - detail = engine === 'gpu' ? 'Sender lyd til cuttlefish GPU…' : `Sender lyd til ${engineLabel}…`; + label = currentUiT('traceUploadLabel', clipLabel, engineLabel); + detail = currentUiT('traceUploadDetail', engine); } else if (elapsed < 60) { - label = `${clipLabel} — ${engineLabel} transkriberer`; - detail = engine === 'gpu' - ? '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.`; + label = currentUiT('traceProcessingLabel', clipLabel, engineLabel); + detail = currentUiT('traceProcessingDetail', engine); } else { - label = `${clipLabel} — behandler fortsatt…`; - detail = `${Math.floor(elapsed / 60)} min ${pad2(elapsed % 60)}s — lange opptak tar flere minutter.`; + label = currentUiT('traceStillLabel', clipLabel); + detail = currentUiT('traceStillDetail', elapsed); } renderTrace([{ label, detail, status: 'running' }]); } @@ -955,7 +1242,7 @@ function handleAudioFiles(fileList) { } const sizeMB = file.size / 1024 / 1024; if (sizeMB > 200) { - skipped.push(`${file.name} (${sizeMB.toFixed(1)} MB — maks 200 MB)`); + skipped.push(currentUiT('fileSizeExceeded', file.name, sizeMB.toFixed(1))); return; } audioQueue.push({ file, status: 'pending', result: null }); @@ -963,9 +1250,9 @@ function handleAudioFiles(fileList) { }); if (skipped.length) { - els.status.textContent = `Hoppet over: ${skipped.join(', ')}`; + els.status.textContent = currentUiT('filesSkipped', skipped.join(', ')); } 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(); diff --git a/transcribe.php b/transcribe.php index b5a0a81..4d35b27 100644 --- a/transcribe.php +++ b/transcribe.php @@ -8,97 +8,106 @@ require_once __DIR__ . '/includes/layout.php'; ?>
+
+ + + + +
+
- Engine - - - + Engine + + +
- Model - - - + Model + + +
- Språk + Audio language + + - +
- Talere - - Antall - + Speakers + + Count +
- Ordliste - - - - + Vocabulary + + + +
- -

Hjelper Whisper gjenkjenne fagtermer. Ikke inkludert i utskriften.

+ +

Helps Whisper recognise technical terms. Not included in the transcript.

-
+
-

Slipp lydfil(er) her, eller

-

MP3, WAV, OGG, M4A, FLAC, WEBM — maks 200 MB per fil

+

Drop audio file(s) here, or

+

MP3, WAV, OGG, M4A, FLAC, WEBMmax 200 MB per file

- Ekspertinnstillinger + Advanced settings
- Oppgave - - + Task + +
- Beam size - + Beam size + - +
- VAD-filter - + VAD filter +
@@ -120,14 +129,14 @@ require_once __DIR__ . '/includes/layout.php';
-

Klar

-

Velg et verktøy, kjør en forespørsel, og svaret vises her.

+

Ready

+

Select a tool, run a request, and the result appears here.