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:
@@ -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;
|
||||
|
||||
+314
-27
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user