Himba → English SRT Studio
Upload one full video → Gemini detects every spoken language → common languages are translated automatically → Himba/Herero stay for human verification → export one final SRT.
VIDEO → SRT
1 · Choose videoUpload or paste a direct link
2 · Create clipsAutomatic timestamps
3 · TranslateWatch, pause, type
4 · Export SRTOne final subtitle file
1

START HERE — Choose the full video

You only need to do this once. Upload the complete video below, or paste a direct video link if the file is too large to send.

1. Gemini API key

For this private/local tool, the key is kept only in this page's memory. Do not hard-code a key into a website you give to the client. For production, put the key on a server. Gemini is used for language detection, automatic translation of all reliably identified non-Himba/Herero languages, and final batch proofreading.

Video source

Choose the video source Use Gallery for normal videos, or Browse Files for MOV/MP4 files in Downloads, Files, WhatsApp, SD card, etc.
No video selected.
Browse device files uses the browser's native file picker when supported and starts in Downloads. If the browser does not support it, Android may use its normal picker.
Best: a direct downloadable video URL. A normal webpage/SwissTransfer/Google Drive page may require a login or may block browser access. If the link allows browser access, the site downloads it into memory and then treats it exactly like an uploaded video.
No video link loaded.
2

AUTOMATIC CLIPS — Let the site split the video

After the video loads, the site analyzes the speech in order, detects the language of each spoken section, and creates timestamp-aligned work segments. Italian is translated automatically; Himba and Herero are left for the human translator.
What happens next
  1. The full video is divided into short clips.
  2. Every clip keeps its exact original timestamp.
  3. You work through the clips one by one below.
🎬 Translator workspaceeasy mode
Upload the video and run the language analysis. The segments will then appear in exact chronological order.
3

TRANSLATE — Watch each clip and type the English meaning

Every speech segment gets a detected language. Italian and other reliably identified common languages are translated automatically. Himba and Herero stay empty for the human translator. Everything remains in the original chronological order with the detected speech timestamps.
4

FINISH — Create the final SRT

When you are ready, use the button below. The site first proofreads every English translation, removes speaker labels, separates clearly labeled speakers, validates the timestamps, and then creates one clean professional SRT. It will not get stuck on the old “finish one translation” check.
Complete your translations first. The final step automatically checks ALL completed translations together, removes speaker labels, separates clear speaker changes, validates timestamps, and creates one SRT.

Advanced / AI tools

These tools are optional. The simple translator workflow above is the recommended way to have a fluent Himba/OtjiHimba speaker verify the meaning.

Gemini API key

Add as many short clips as you want. Added clips stay in this list until you delete them. Set the exact original-video start/end time for each clip, then generate a single ZIP pack containing the clips and a ready-to-use translator page. Your translator only opens the page, presses play, types the English meaning under each clip, and downloads the final SRT.
No clips added yet.
Your translator does not need to import the videos. Build the Translator Pack after you finish editing the list; the ZIP contains the videos and translator page together.

Advanced: AI video analysis

`; html+=`
`; }); box.innerHTML=html; box.querySelectorAll('.reviewtext').forEach(el=>el.oninput=()=>reviewNotes[Number(el.dataset.i)].text=el.value); box.querySelectorAll('.removeNote').forEach(el=>el.onclick=()=>{reviewNotes.splice(Number(el.dataset.i),1);renderReviewNotes();}); box.querySelectorAll('.transcribeNote').forEach(el=>el.onclick=()=>transcribeReviewNote(Number(el.dataset.i))); const use=$("useReviewSrt"); if(use) use.disabled=!reviewNotes.some(n=>n.text&&n.text.trim()); } async function startReviewRecording(){ if(!selectedFile) throw new Error('Choose a video first.'); if(!navigator.mediaDevices?.getUserMedia) throw new Error('This browser does not allow microphone recording. Try Chrome and allow microphone access.'); const stream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}}); const types=['audio/webm;codecs=opus','audio/webm','audio/mp4']; const mime=types.find(x=>MediaRecorder.isTypeSupported(x))||''; recorder=new MediaRecorder(stream,mime?{mimeType:mime}:undefined); recordChunks=[]; const v=$("reviewVideo"); recordStartTime=Number(v.currentTime)||0; recordBlob=null; recorder.ondataavailable=e=>{if(e.data?.size) recordChunks.push(e.data);}; recorder.onstop=()=>{ recordEndTime=Number(v.currentTime)||recordStartTime; recordBlob=new Blob(recordChunks,{type:recorder.mimeType||'audio/webm'}); if(recordUrl) URL.revokeObjectURL(recordUrl); recordUrl=URL.createObjectURL(recordBlob); $("reviewAudio").src=recordUrl; $("reviewAudio").classList.remove('hidden'); const note={start:recordStartTime,end:Math.max(recordEndTime,recordStartTime+0.1),blob:recordBlob,url:recordUrl,text:''}; reviewNotes.push(note); renderReviewNotes(); $("recordPlay").disabled=false; $("recordTranscribe").disabled=false; setRecordStatus(`Voice note saved for ${fmtTime(note.start)} → ${fmtTime(note.end)}.`,true); stream.getTracks().forEach(t=>t.stop()); }; recorder.start(250); $("recordStart").disabled=true; $("recordStop").disabled=false; $("recordTranscribe").disabled=true; v.muted=$("recordMuteVideo").checked; if(v.paused) await v.play().catch(()=>{}); const started=performance.now(); clearInterval(recordTimerId); recordTimerId=setInterval(()=>$("recTimer").textContent=formatDuration(performance.now()-started),250); setRecordStatus('🔴 Recording… explain the English meaning naturally.',true); } function stopReviewRecording(){ if(!recorder) return; clearInterval(recordTimerId); recordTimerId=null; $("recTimer").textContent='00:00'; recorder.stop(); $("recordStart").disabled=false; $("recordStop").disabled=true; } async function transcribeReviewBlob(blob){ const apiKey=$("key").value.trim(); if(!apiKey) throw new Error('Enter the Gemini API key first.'); if(blob.size>20*1024*1024) throw new Error('That voice note is over 20 MB. Keep reviewer notes shorter.'); const b64=await new Promise((resolve,reject)=>{const r=new FileReader(); r.onload=()=>resolve(String(r.result).split(',')[1]||''); r.onerror=reject; r.readAsDataURL(blob);}); const mime=blob.type||'audio/webm'; const prompt='Transcribe this reviewer voice note exactly into clear English. The speaker is a human reviewer explaining the English meaning of a Himba/OtjiHimba section. Do not add facts, summarize, or reinterpret. Return only the English transcription.'; const r=await fetch('https://generativelanguage.googleapis.com/v1beta/models/gemini-3.5-transcribe:generateContent',{method:'POST',headers:{'x-goog-api-key':apiKey,'Content-Type':'application/json'},body:JSON.stringify({contents:[{parts:[{text:prompt},{inlineData:{mimeType:mime,data:b64}}]}]})}); const t=await r.text(); if(!r.ok) throw new Error(`Voice-note transcription failed (${r.status}). ${t.slice(0,400)}`); const data=JSON.parse(t); return (data?.candidates?.[0]?.content?.parts||[]).filter(p=>p.text).map(p=>p.text).join('\n').trim(); } async function transcribeReviewNote(index){ const n=reviewNotes[index]; if(!n?.blob) return; try{setRecordStatus(`Transcribing reviewer note ${index+1}…`); n.text=await transcribeReviewBlob(n.blob); renderReviewNotes(); setRecordStatus(`Reviewer note ${index+1} transcribed. Check the text before using it in the SRT.`,true);}catch(e){setRecordStatus(e?.message||String(e),false,true);} } function setClipStatus(msg,good=false,err=false){ const el=$('clipStatus'); if(!el) return; el.textContent=msg; el.className='status '+(good?'good':err?'err':''); } let activeDictation=null; function speechCtor(){return window.SpeechRecognition||window.webkitSpeechRecognition||null;} function stopDictation(){ if(activeDictation?.recognition){try{activeDictation.recognition.stop();}catch(_){} } if(activeDictation?.button){activeDictation.button.classList.remove('recording');activeDictation.button.textContent='🎤 Speak';} activeDictation=null; } function toggleDictation(textarea,button){ const R=speechCtor(); if(!R){alert('Voice typing is not supported in this browser. Please use Chrome on Android or type normally.');return;} if(activeDictation?.textarea===textarea){stopDictation();return;} if(activeDictation) stopDictation(); const recognition=new R(); recognition.lang='en-US'; recognition.continuous=true; recognition.interimResults=true; const original=textarea.value.trim(); let committed=original; let lastInterim=''; recognition.onstart=()=>{activeDictation={recognition,textarea,button};button.classList.add('recording');button.textContent='⏹ Stop';textarea.focus();}; recognition.onresult=e=>{ let interim=''; for(let k=e.resultIndex;k{ if(e.error!=='no-speech'&&e.error!=='aborted'){ button.textContent='🎤 Speak'; button.classList.remove('recording'); if(activeDictation?.textarea===textarea) activeDictation=null; alert('Voice typing error: '+e.error+'. You can continue typing normally.'); } }; recognition.onend=()=>{ // Chrome can end recognition after a pause. Keep the committed text and reset the button. if(activeDictation?.textarea===textarea){ button.classList.remove('recording'); button.textContent='🎤 Speak'; activeDictation=null; } }; try{recognition.start();}catch(e){alert('Could not start voice typing. Please tap the microphone again.');} } function createClipQueue(){ const v=$('reviewVideo'); if(!v || !Number.isFinite(v.duration) || !v.duration){ setClipStatus('Load a video first so its duration can be read.',false,true); return; } const len=Number($('clipLength').value)||15; clipQueue=[]; activeClipIndex=-1; for(let start=0; startn.clipIndex===i); } function languageBadge(lang){ const l=String(lang||'Unknown'); const map={Italian:'🇮🇹 Italian',Himba:'🟤 Himba',Herero:'🟣 Herero',English:'🇬🇧 English',Unknown:'❔ Unknown'}; return map[l]||('🌍 '+l); } function renderClipQueue(){ const box=$('clipQueue'); if(!box) return; if(!clipQueue.length){ box.innerHTML=''; return; } box.innerHTML=clipQueue.map((c,i)=>{ const lang=c.language||'Unknown'; const auto=String(c.english||'').trim(); const needsHuman=(lang==='Himba'||lang==='Herero'); const shown=auto || clipNoteFor(i)?.text || ''; return `
Segment ${i+1}${languageBadge(lang)}
${fmtTime(c.start)} → ${fmtTime(c.end)}
${needsHuman?'Human translator: translate this Himba/Herero speech.':(lang==='Unknown'?'Human review: language/meaning could not be established reliably.':'AI translation is available; review it before export.')}
${needsHuman?'':''}
${auto?'🇮🇹 Automatically translated — review before final export.':needsHuman?'🟤 Waiting for human translation.':'No translation required.'}
`; }).join(''); box.querySelectorAll('.playClip').forEach(b=>b.onclick=()=>playClip(Number(b.dataset.i))); box.querySelectorAll('.pauseClip').forEach(b=>b.onclick=()=>pauseClip(Number(b.dataset.i))); box.querySelectorAll('.clipTranslation').forEach(t=>{ const i=Number(t.dataset.i); const c=clipQueue[i]; t.value=(c.english||clipNoteFor(i)?.text||''); t.oninput=()=>{ if(c.english && c.language==='Italian') return; const idx=reviewNotes.findIndex(n=>n.clipIndex===i); const text=t.value; if(c.language!=='Italian') c.english=text; if(idx>=0) reviewNotes[idx].text=text; else if(text.trim()) reviewNotes.push({clipIndex:i,start:c.start,end:c.end,text}); const pill=t.closest('.cliprow')?.querySelector('.pill'); if(pill && (c.language==='Himba'||c.language==='Herero')) pill.textContent=languageBadge(c.language)+(text.trim()?' · ✓':''); saveWorkspaceSoon(); }; }); box.querySelectorAll('.clipDictate').forEach(b=>b.onclick=()=>{const ta=box.querySelector('.clipTranslation[data-i="'+b.dataset.i+'"]');if(ta) toggleDictation(ta,b);}); } async function playClip(i){ const v=$('reviewVideo'), c=clipQueue[i]; if(!v||!c) return; activeClipIndex=i; renderClipQueue(); v.currentTime=c.start; v.muted=false; try{ await v.play(); }catch(e){ setClipStatus('Tap Play on the video once, then try again.',false,true); return; } const stop=()=>{ if(v.currentTime>=c.end-0.03){ v.pause(); v.removeEventListener('timeupdate',stop); } }; v.addEventListener('timeupdate',stop); setClipStatus(`Playing Clip ${i+1}. You can pause it, replay it, then type the English translation in the box.`); } function pauseClip(i){ const v=$('reviewVideo'); if(!v) return; v.pause(); activeClipIndex=i; renderClipQueue(); setClipStatus(`Clip ${i+1} paused. Continue listening or type the English translation in its box.`); } async function recordClipTranslation(i){ const v=$('reviewVideo'), c=clipQueue[i]; if(!v||!c) return; activeClipIndex=i; renderClipQueue(); v.pause(); v.currentTime=c.start; try{ if(!navigator.mediaDevices?.getUserMedia) throw new Error('Microphone recording is not available in this browser.'); const stream=await navigator.mediaDevices.getUserMedia({audio:{echoCancellation:true,noiseSuppression:true,autoGainControl:true}}); const types=['audio/webm;codecs=opus','audio/webm','audio/mp4']; const mime=types.find(x=>MediaRecorder.isTypeSupported(x))||''; recorder=new MediaRecorder(stream,mime?{mimeType:mime}:undefined); recordChunks=[]; recorder.ondataavailable=e=>{if(e.data?.size) recordChunks.push(e.data);}; recorder.onstop=()=>{ const blob=new Blob(recordChunks,{type:recorder.mimeType||'audio/webm'}), url=URL.createObjectURL(blob); const idx=reviewNotes.findIndex(n=>n.clipIndex===i); if(idx>=0 && reviewNotes[idx].url) URL.revokeObjectURL(reviewNotes[idx].url); const note={clipIndex:i,start:c.start,end:c.end,blob,url,text:''}; if(idx>=0) reviewNotes[idx]=note; else reviewNotes.push(note); stream.getTracks().forEach(t=>t.stop()); renderReviewNotes(); renderClipQueue(); setClipStatus(`Clip ${i+1} recorded. Transcribe it or type the English meaning, then continue.`,true); }; recorder.start(250); $('recordStart').disabled=true; $('recordStop').disabled=false; v.muted=$('recordMuteVideo').checked; await v.play().catch(()=>{}); const started=performance.now(); clearInterval(recordTimerId); recordTimerId=setInterval(()=>$('recTimer').textContent=formatDuration(performance.now()-started),250); const stopAt=()=>{ if(v.currentTime>=c.end-0.03 && recorder?.state==='recording'){ v.pause(); v.removeEventListener('timeupdate',stopAt); stopReviewRecording(); } }; v.addEventListener('timeupdate',stopAt); setClipStatus(`🔴 Recording Clip ${i+1}. Explain the English meaning while the clip plays. Use headphones.`,true); }catch(e){ setClipStatus(e?.message||String(e),false,true); } } let packClips=[]; function setPackStatus(msg,good=false,err=false){const el=$('packStatus');if(!el)return;el.textContent=msg;el.className='status '+(good?'good':err?'err':'');} function parseTimeToken(x){ x=String(x||'').trim(); if(!x)return null; if(/^\d+(?:\.\d+)?$/.test(x)) return Number(x); const a=x.replace(/,/g,'.').split(':').map(Number); if(a.some(n=>!Number.isFinite(n))) return null; if(a.length===3)return a[0]*3600+a[1]*60+a[2]; if(a.length===2)return a[0]*60+a[1]; return null; } function parseTimesFromName(name){ const base=name.replace(/\.[^.]+$/,''); const m=base.match(/(\d{1,2}(?:[:._-]\d{2}){1,2})\s*(?:to|_|-|–|—)\s*(\d{1,2}(?:[:._-]\d{2}){1,2})/i); if(!m)return null; const norm=x=>x.replace(/[._-]/g,':'); const start=parseTimeToken(norm(m[1])), end=parseTimeToken(norm(m[2])); return start!=null&&end!=null&&end>start?{start,end}:null; } async function addPackFiles(files){ const arr=Array.from(files||[]); if(!arr.length)return; for(const file of arr){ const type=file.type||''; if(!type.startsWith('video/')&&!/\.(mp4|mov|m4v|webm|avi|mkv|3gp|mpeg|mpg|m2ts|mts|ts)$/i.test(file.name))continue; const url=URL.createObjectURL(file); const item={id:crypto.randomUUID?crypto.randomUUID():String(Date.now()+Math.random()),file,url,start:null,end:null,duration:null}; const parsed=parseTimesFromName(file.name); if(parsed){item.start=parsed.start;item.end=parsed.end;} await new Promise(resolve=>{const v=document.createElement('video');v.preload='metadata';v.src=url;v.onloadedmetadata=()=>{item.duration=Number(v.duration)||0;if(item.start==null){const prev=packClips[packClips.length-1];item.start=prev&&Number.isFinite(prev.end)?prev.end:0;}if(item.end==null)item.end=item.start+item.duration;resolve();};v.onerror=()=>{item.duration=0;if(item.start==null)item.start=0;if(item.end==null)item.end=0;resolve();};}); packClips.push(item); } renderPack(); setPackStatus(`${packClips.length} clip${packClips.length===1?'':'s'} ready. Check the original timestamps, then build the translator pack.`,true); } function renderPack(){ const box=$('packList');if(!box)return; if(!packClips.length){box.innerHTML='';return;} box.innerHTML=packClips.map((c,i)=>`
Clip ${i+1}
${escapeHtml(c.file.name)}
${c.duration?c.duration.toFixed(1)+' sec':''}
`).join(''); box.querySelectorAll('.packtime').forEach(el=>el.oninput=()=>{const c=packClips[Number(el.dataset.i)];c[el.dataset.k]=Number(el.value);}); box.querySelectorAll('.removePack').forEach(el=>el.onclick=()=>{const i=Number(el.dataset.i);if(!packClips[i])return;if(!confirm(`Delete Clip ${i+1}?`))return;URL.revokeObjectURL(packClips[i].url);packClips.splice(i,1);renderPack();setPackStatus(`${packClips.length} clips remaining in pack.`,true);}); } function sortPack(){packClips.sort((a,b)=>(a.start||0)-(b.start||0));renderPack();setPackStatus('Clips sorted by original start time.',true);} function clearPack(){if(!packClips.length)return;if(!confirm('Delete all clips from this pack?'))return;packClips.forEach(c=>URL.revokeObjectURL(c.url));packClips=[];renderPack();setPackStatus('All clips deleted.');} function translatorHtml(){return `Translator Clip Pack
Himba → English Translator v21
Watch each clip as video, use Play/Pause and replay as needed, then enter the complete English translation in the white box beside the video, or tap 🎤 Speak to dictate it. Smart auto-correction checks spelling, grammar and rushed typing while you work. When all required clips are translated, download one final SRT with the original timestamps.
🎧 Recommended: listen to the clip first, then use 🎙️ Voice note and speak the full English meaning. The browser will turn your speech into text automatically when supported.