How do I transcribe an Instagram video on an iPhone?
There is nothing to install and nothing to sign up for: the page is a web page, it runs in Safari like any other, and the video goes in through one file picker — the same picker as any upload form, except that nothing is ever uploaded, because the decoding and the recognition both happen inside the tab. What a phone changes is not the route but the ceiling. A three-minute Reel recorded at 48 kHz in stereo becomes a 69,120,000-byte decoded audio buffer plus an 11,520,000-byte 16,000 Hz mono copy for the model, so 80,640,000 bytes sit in one tab at one moment before a single word comes back — and the first run also fetches a 79.7 MB speech model. Those two numbers, not the length of the Reel, are what decide whether a given iPhone finishes a given video. The dashed box that says “or drop one here” is wired to 4 drag events that a touch screen cannot fire.
The rest of this page is that answer taken apart in units you can check rather than reassurances you have to trust: the 12 tokens in the file input’s accept list and the 2 of them that are wildcards, the 4 drag listeners that are dead weight on a touch screen, the 384,000 bytes a second that one second of 48 kHz stereo costs against the 64,000 a second the model actually reads, the 278,082,062 bytes the one measured ten-minute run held at its peak, the 0 occurrences of a service worker in either file that would let a second visit skip the 79.7 MB download, and the 0.419 seconds of recognition this machine spent per second of audio — a rate that is this machine’s, not an iPhone’s.
There is no app, and there is no upload either
The whole tool is two files served from one static host: index.html at 17,965 bytes and app.js at 22,349 bytes. Neither is wrapped in a native shell, there is no App Store listing to look for, and the App Store is not where this lives. On an iPhone the page arrives exactly as it does on a laptop, and it talks to 2 hosts and no more: cdn.jsdelivr.net for the recognition library, and huggingface.co for the speech model and for the generation_config.json the language picker is built from. Those are 1 dynamic import and 1 explicit fetch in the 22,349 bytes, plus the weight files the library fetches for itself. Your video is not part of any of them.
- 0 service workers, so nothing installs and nothing caches on your behalf. The string
serviceWorkerappears 0 times in the 22,349 bytes of app.js and 0 times in the 17,965 bytes of index.html, and there is no manifest link in the head of any page on the site. A home-screen shortcut would be a bookmark, not an app: it would still fetch the 79.7 MB model through the browser’s own HTTP cache, which is the only thing standing between a second visit and a second download. - The video is read with
file.arrayBuffer(), which is a local read. That call appears 1 time in the 22,349 bytes and it is the only place the file is touched; the bytes go straight intodecodeAudioData(), which appears 1 time, and never into afetchbody or aFormData. The page’s own header comment says the file “is never part of any request”, and the 1 explicitfetchin the file asks huggingface.co for a 3,832-bytegeneration_config.jsonwhile the 1 dynamicimportpulls the library from cdn.jsdelivr.net. - What a phone has to trust is the same thing a laptop does. There is no account, no quota counter and no server-side timer, because there is no server: the 22,349 bytes of app.js are the entire program, and the only state that survives a reload is whatever the browser cached. Nothing counts how many videos you have done.
The only door on a phone is the file picker
On a laptop there are two ways in. On an iPhone there is one. The control is an <input type="file"> with id="file" that is visually hidden by the .sr class, and the thing you actually tap is a <label for="file"> drawn as the dashed box. A label forwards the tap to its input, so the picker opens; that is the entire interaction, and it is 1 tap before the system sheet appears.
- 12 tokens in the accept list, 2 of which are wildcards.
acceptisvideo/*,audio/*plus 10 extensions —.mp4,.mov,.m4v,.webm,.mkvfor video and.mp3,.m4a,.wav,.ogg,.flacfor audio, so 5 video extensions and 5 audio ones. Becausevideo/*already covers every one of those 5 video extensions, 5 of the 12 tokens are redundant on any browser that honours wildcards — they are there for the ones that do not. .movis on the list, which is the format an iPhone camera writes. A clip recorded on the phone and saved to Photos arrives as a MOV, and the page’s own error comment records that “MP4, MOV, WebM and M4A all worked when they were tried on this page” — 4 container formats tried, 4 accepted. That line sits in the comment at lines 207–229 of the 22,349-byte file, next to the three failures it cannot distinguish.captureappears 0 times andmultipleappears 0 times. With nocaptureattribute the tap opens the system sheet rather than jumping to the camera, so the video has to exist already — in Photos or in Files — before the page can see it. With nomultiplethe picker hands back 1 file, which matchesfiles[0]in the change handler: there is no multi-select on a phone and no queue behind it.- The visible sub-line names 6 formats; the input accepts 10. It reads “or drop one here — MP4, MOV, WebM, MP3, M4A, WAV”, so 6 of the 10 extensions are advertised and
.m4v,.mkv,.oggand.flacare not, even though all 4 of them are in the accept list. A file that fails to appear in the picker is more likely to be a Photos-library question than a format question.
The drop box is 4 listeners a touch screen cannot fire
The dashed box is a drop zone as well as a button, and the code that makes it one is wireDropZone(): 4 event types — dragenter, dragover, dragleave, drop — each with preventDefault() and stopPropagation(), plus a depth counter that exists because dragenter and dragleave also fire for children. All of it is correct, and on an iPhone none of it can run.
- 0 of the 4 events fire without a pointer. A drag gesture needs a mouse or a trackpad holding a button down while something moves; on a touch screen a finger that moves scrolls the page instead. The handler still waits for
e.dataTransfer.files[0], and with no drag source that object is never populated. The listeners are not broken — they are simply unreachable, which is worse, because nothing reports it. - The sentence under the button promises a gesture the device does not have. “or drop one here” is 4 words of instruction that an iPhone cannot follow, sitting directly above the only control that works. This page cannot fix that from here without editing index.html, so it is written down instead: on a phone, tap the box and use the sheet.
- An iPad is a different question, and this page has not measured it. iPadOS can drag a file from another app into Safari, so the 4 listeners may well be live there. The distinction matters because the answer “drag and drop does not work” is true of an iPhone and untested on an iPad, and writing it as a general rule would be a guess dressed as a finding.
What a minute of audio costs the tab
This is the part that decides whether your phone finishes. Web Audio hands back an AudioBuffer of 32-bit floats, so the cost of one second is sample rate × channels × 4 bytes. At the rate a phone camera records — 48 kHz, stereo — that is 384,000 bytes a second. The model does not read that buffer; it reads a second one, rendered down to 16,000 Hz mono at OfflineAudioContext(1, frames, 16000), which is 64,000 bytes a second. The ratio is exactly 6, and both buffers exist at the same time, because toPcm() holds decoded while startRendering() produces the second one.
- One minute: 23,040,000 + 3,840,000 = 26,880,000 bytes. That is 960,000 frames of 16 kHz mono and 420 seconds’ worth of 16 kHz audio in the render alone — the render is not the small part people assume. The model download that follows is 2.965 times this figure, which is the first hint that on a phone the download and the buffer compete for the same memory budget.
- Ninety seconds: 34,560,000 + 5,760,000 = 40,320,000 bytes. That is 1,440,000 frames, and it is the length where the two buffers stop being a rounding error: 40 MB of audio for a Reel that is a few megabytes on disk. At 30 seconds a chunk, 90 seconds is 4 windows rather than 3, because the last window only starts at 60 seconds and has to be padded out.
- Three minutes: 69,120,000 + 11,520,000 = 80,640,000 bytes. That is 2,880,000 frames and 9 windows, and it is 940,000 bytes more than the 79.7 MB model itself. A three-minute Reel, in other words, costs the tab slightly more in audio than the model costs in weights — and unlike the model, the audio is allocated fresh on every single run.
- Ten minutes, measured: 278,082,062 bytes at the peak. The one full-length run recorded on this machine took a 9,147,662-byte file, decoded it to a 230,515,200-byte buffer, rendered 9,604,800 frames to 38,419,200 bytes, and held all three at once. The file you picked is 3.97% of the buffer it becomes, which is why “it is only a 9 MB video” is not a memory argument.
toPcm() keeps both because decoded is still referenced while startRendering() runs. 60 seconds is 23,040,000 + 3,840,000 = 26,880,000 bytes; 90 seconds is 34,560,000 + 5,760,000 = 40,320,000; 180 seconds is 69,120,000 + 11,520,000 = 80,640,000, which is 940,000 bytes past the 79.7 MB model marked by the dashed line. The 10-minute run recorded on this machine peaked at 278,082,062 bytes once the 9,147,662-byte file is counted alongside its 230,515,200-byte decode and 38,419,200-byte render. The 48 kHz stereo source rate is an assumption about what a phone camera records, not a measurement of your file; a mono 44.1 kHz recording would be 176,400 bytes a second instead of 384,000.The model is the other thing a phone pays for
The recognition library is loaded from a CDN and told allowLocalModels = false, so the model weights always come over the network rather than from anything bundled with the page. The progress code records what that costs, measured in this browser: the total climbs as the library discovers files, “26.0 MB at the first sample and 79.7 MB by the third”. On a laptop on wifi that is a wait. On a phone on cellular it is also 79.7 MB of somebody’s data allowance, once per fresh cache.
- The two checkpoints are 76,894,612 and 40,844,231 bytes of weights. Those are the q8 files fetched from Hugging Face for
whisper-baseandwhisper-tiny, measured on this machine; the ratio is 1.8826, so choosing the smaller download saves 36,050,381 bytes. That is the single largest lever a phone user has, and it is one dropdown. - 79,700,000 bytes is 207.55 seconds of 48 kHz stereo source. Divided by 384,000 that is 207.55 s, and divided by the 64,000 the model actually reads it is 1,245.3125 s — 20.76 minutes of 16 kHz mono audio. The download is bigger than most of the videos anyone brings to it, which is the reason it happens once and only after a file has been decoded successfully.
- Loads are serialised on purpose, and the comment says why.
loadQueuechains every load so 2 pipelines can never be alive together: “two pipelines at once on a phone is how a tab runs out of memory”. Switching the dropdown from one model to the other therefore waits for the first load to settle rather than running 2 downloads side by side — the comment next to it records the case that forced this, “a run that should have cost 44 MB cost 118 MB”. - The download starts at the first file, not at page load. That ordering is deliberate and documented in
start(): a visitor who never picks a file, or who changes their mind about the model, is not charged for a model they are not using. On a phone this is the difference between opening the page costing 0 bytes and opening it costing 79.7 MB.
Three Safari behaviours the source settles
Phones raise questions that a laptop never does, and three of them can be answered from the file rather than from a guess about how mobile browsers behave. The pattern in all three is that the code already contains a fallback, and knowing which fallback is on which path is what tells you whether a failure on iOS is a bug or an expected branch.
- The live
AudioContextis only a decoder, so iOS’s suspended state does not block anything.toPcm()creates oneAudioContextand uses it for exactly 1 thing,decodeAudioData(); it appears 1 time in the 22,349 bytes, and the resampling is done by a separateOfflineAudioContext. There are 0 calls toresume()and 0 tostart()on the live context — because on iOS a context created after anawaitstarts suspended, and this code never needs it running. The context is closed in afinallyblock, so 1 decoder is opened and closed per run. - The language list degrades to raw codes on an old iOS, not to an error. The picker is filled from the model’s own
generation_config.json, which lists 99 languages, and the display names come fromIntl.DisplayNamesinside atry. Where that constructor is missing, thecatchleavesdisplayNamesnull andlanguageName()returns the raw code, so the menu showseninstead of English. Nothing breaks; 99 options still exist and the value sent to the model is the same code either way. - Copy has 2 routes and the second one is the one phones take.
navigator.clipboard.writeTextappears 1 time inside atry; thecatchbuilds atextarea, selects it and callsdocument.execCommand('copy'), which appears 1 time.setSelectionRangeappears 0 times, so the selection is whateverselect()produces — this page has not measured that on an iPhone, and says so rather than promising it works.
How long it takes, and why this page will not promise a number
The one full-length run recorded here took 600.3 seconds of audio and 251.5 seconds of recognition, which is 0.419 seconds of work per second of audio on this machine. That number belongs to this machine. The same wasm binary on an iPhone is a different CPU with a different thermal budget, and no run has been measured on one, so the honest form of the answer is the formula rather than a figure: seconds of recognition ≈ seconds of audio × the rate your device achieves, and the rate is the only unknown.
- 0.419 × 90 = 37.7 seconds, if a phone ran at this machine’s rate. It will not, in either direction, and that is exactly why the page gives the arithmetic instead of the result. What does carry across devices is the shape: the work grows with the length of the audio, not with the size of the file, so a heavily compressed Reel and a lightly compressed one of the same duration cost the same.
- The work is counted in 30-second windows, and 90 seconds is 4 of them.
chunk_length_s: 30is 480,000 samples at 16 kHz, consecutive windows are 320,000 samples apart, so a 90-second Reel is 4 windows, 3 minutes is 9, and 10 minutes is 30. Overlap is why the counts are not 3, 6 and 20: each window carries 160,000 samples of context that the next one repeats, and 1.489 times the audio is processed for a 10-minute file. - A shorter cut is cheaper twice. Halving the length halves the windows and halves the buffers, so a 90-second cut of a 3-minute Reel is 4 windows and 40,320,000 bytes instead of 9 windows and 80,640,000. If a run is going to fail on a phone, it fails at the allocation, before any window is processed — which is why trying a shorter cut first is the fastest way to find out whether the file or the tab is the problem.
When the tab reloads in the middle
A browser tab has no swap file. When the memory budget runs out the tab is discarded and reloaded, and everything the run was holding goes with it: RESULT is a variable in memory, the transcript panel is a DOM node, and neither is written anywhere until you press a download button. On a phone that reload is not an edge case; it is the most likely way a long video ends.
- Re-picking the same file works, because the input is cleared.
acceptFile()sets$('file').value = ''before starting the run — 1 line that exists so choosing the same file twice still fires achangeevent. Without it a second attempt on the same video would be silently ignored, because the input’s value had not changed and the browser had nothing to report. - The smaller model is 36,050,381 bytes cheaper and it is one dropdown. Switching to
whisper-tinydrops the weights from 76,894,612 to 40,844,231 bytes. It does not shrink the audio buffers — those are 26,880,000 bytes a minute regardless of model — so on a phone the order to try is: shorter cut first, smaller model second, because only the first one touches the part that grows with the video. - Download the transcript before you do anything else with the tab. The 4 exits — copy,
.txt,.srt,.vtt— all read from the same in-memory object, anddownload()builds a blob URL and clicks an anchor with adownloadattribute, then revokes the URL 30,000 ms later. Where that file lands is the operating system’s decision, not this page’s; on an iPhone it goes wherever the system puts a downloaded file, and this page has not measured that.
Is there an Instagram transcription app for iPhone?
Not from this site, and this site does not need one. The tool is 2 files — index.html at 17,965 bytes and app.js at 22,349 bytes — served as a normal web page, so Safari is the whole installation. An app would have to ship the same 79.7 MB of model weights inside a bundle and ask for the same audio buffers; the browser route gets the model over the network once and asks the tab for 26,880,000 bytes per minute of audio, which is a smaller ask than a native app that keeps a model resident.
Does it upload my video from my phone?
No. The file is read with file.arrayBuffer() — 1 call in the 22,349 bytes — and handed straight to decodeAudioData(), which appears 1 time; there is no FormData and no request that carries the file. The 2 network destinations the page has are the recognition library on cdn.jsdelivr.net and the model on huggingface.co, and neither one receives any part of your video. The page’s own header comment states it in one line: the file “is never part of any request”.
Why can’t I drop a video into the box on my phone?
Because a drag gesture needs a pointer that can hold something while it moves, and a finger on a touch screen scrolls the page instead. The drop zone is wired to 4 events — dragenter, dragover, dragleave, drop — and on an iPhone none of the 4 can fire, so the handler that reads e.dataTransfer.files[0] never gets an object to read. The label inside the same box still works: tap it, and the system sheet offers Photos and Files.
Can I record with the camera straight into the page?
No, and that is a deliberate omission rather than a bug. capture appears 0 times on the file input, so tapping it opens the normal picker instead of jumping to the camera; there is also 0 use of getUserMedia anywhere in the 22,349 bytes. The route is: record in the camera app, save to Photos, then tap the box — 3 steps, and the clip is a MOV, which is one of the 10 extensions in the accept list and one of the 4 container formats the code comment records as working.
What is the longest Instagram video an iPhone can transcribe?
There is no fixed number, and any page that gives you one is guessing. The ceiling is the tab’s memory, and the arithmetic is: available bytes − the file − 384,000 per second of 48 kHz stereo source − 64,000 per second for the 16 kHz render. On this desktop browser a valid 20-minute WAV — 76,800,000 bytes as 16 kHz mono — was rejected as “more than the tab holds”, which is the only ceiling that has actually been observed here; no iPhone has been measured, so this page reports the formula and the one refusal it has, not a promise about your phone.
Where does the .srt file go on an iPhone?
The page does not choose. download() creates a blob URL, sets a.download = 'transcript.srt', clicks the anchor and revokes the URL 30,000 ms later; the destination is whatever the operating system does with a download. The filename is fixed for all 4 exits — transcript.txt, transcript.srt, transcript.vtt — and the file is written as text/plain;charset=utf-8 in every case, so a second download of the same kind replaces the first unless the system renames it.
Last checked: 1 November 2026. The source counts were read on this machine from the deployed files: app.js is 22,349 bytes and index.html 17,965, with serviceWorker, capture, multiple, resume(), getUserMedia and setSelectionRange each 0 times, arrayBuffer, decodeAudioData, getChannelData and navigator.clipboard 1 time each, and 4 drag event types wired in wireDropZone(). The accept list is 12 tokens — 2 wildcards, 5 video extensions, 5 audio extensions — counted from the attribute itself. All byte figures are arithmetic on constants in app.js and on the Web Audio specification: a 32-bit float sample, one channel for the render, so 48 kHz stereo is 384,000 bytes a second and 16 kHz mono is 64,000; 60, 90 and 180 seconds were multiplied out and printed here, as were 230,515,200 + 38,419,200 + 9,147,662 = 278,082,062 and 79,700,000 ÷ 384,000 = 207.55. The window counts follow from chunk_length_s: 30 — 480,000 samples, 320,000 apart — and were printed as 4, 9 and 30. The 76,894,612 and 40,844,231 weight sizes were read from Hugging Face for the two q8 checkpoints. The 26.0 MB, 79.7 MB, “44 MB cost 118 MB”, the 20-minute WAV rejection, the “MP4, MOV, WebM and M4A all worked” line and the “two pipelines at once on a phone” sentence are quoted from the comments in app.js, which record what was measured in this browser. The 251.5 seconds and 0.419 per second come from the 600.3-second run recorded on 27 October 2026 on this Windows machine. No part of this page was measured on an iPhone: the iOS behaviour described here is read off the code path and the specification, and is marked as such wherever it appears.
If the file itself is the problem rather than the phone, the five failures page sorts out which of them you have. If the wait is the problem, the long Reel page has the measured 10-minute run in full, and the length limit page covers the 20-minute ceiling quoted above. The SRT and VTT page covers what the 4 exits actually produce, the no-login page covers what the browser needs in order to read a file at all, and the timestamps page covers the 12 characters this tool puts in front of every line.