Initial commit: media-to-text app
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
# Development Rules
|
||||||
|
|
||||||
|
## Testing Requirements
|
||||||
|
- ALWAYS test code before claiming it's done
|
||||||
|
- Verify file uploads work with actual test files
|
||||||
|
- Check browser console for errors during testing
|
||||||
|
- Never say "done" without confirming functionality
|
||||||
|
- Run test scripts first before presenting solutions
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
- MP4 video files may fail with AudioContext.decodeAudioData()
|
||||||
|
- Browser can't decode all video codecs to audio
|
||||||
|
- MediaRecorder approach for video extraction produces empty/unusable audio
|
||||||
|
- Solution: AudioContext.decodeAudioData works directly on many video formats
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(lsof:*)",
|
||||||
|
"Bash(curl:*)",
|
||||||
|
"Bash(pkill:*)",
|
||||||
|
"Bash(/tmp/create_speech.py:*)",
|
||||||
|
"Bash(__NEW_LINE_96afbd91fece9431__ python3 /tmp/create_speech.py)",
|
||||||
|
"Bash(brew install:*)",
|
||||||
|
"Bash(docker logout:*)",
|
||||||
|
"Bash(docker-compose up:*)",
|
||||||
|
"Bash(~/.docker/config.json << 'EOF'\n{\n \"cliPluginsExtraDirs\": [\n \"/opt/homebrew/lib/docker/cli-plugins\"\n ]\n}\nEOF)",
|
||||||
|
"Bash(colima start:*)",
|
||||||
|
"Bash(colima stop:*)",
|
||||||
|
"Bash(docker-compose logs:*)",
|
||||||
|
"Bash(docker-compose down:*)",
|
||||||
|
"Bash(ls:*)",
|
||||||
|
"WebFetch(domain:unpkg.com)",
|
||||||
|
"WebFetch(domain:esm.sh)",
|
||||||
|
"WebFetch(domain:cdn.jsdelivr.net)",
|
||||||
|
"WebSearch",
|
||||||
|
"WebFetch(domain:cdn.skypack.dev)",
|
||||||
|
"WebFetch(domain:app.unpkg.com)",
|
||||||
|
"WebFetch(domain:www.jsdelivr.com)",
|
||||||
|
"WebFetch(domain:ffmpegwasm.netlify.app)",
|
||||||
|
"Bash(find:*)",
|
||||||
|
"Bash(chmod:*)",
|
||||||
|
"Bash(du:*)",
|
||||||
|
"Bash(git init:*)",
|
||||||
|
"Bash(git add:*)",
|
||||||
|
"Bash(git commit:*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
*.tmp
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{var e={454:e=>{function t(e){return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}))}t.keys=()=>[],t.resolve=t,t.id=454,e.exports=t}},t={};function r(o){var s=t[o];if(void 0!==s)return s.exports;var a=t[o]={exports:{}};return e[o](a,a.exports,r),a.exports}return r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e="https://unpkg.com/@ffmpeg/core@0.12.9/dist/umd/ffmpeg-core.js";var t;!function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const o=new Error("unknown message type"),s=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),a=(new Error("called FFmpeg.terminate()"),new Error("failed to import ffmpeg-core.js"));let n;self.onmessage=async({data:{id:E,type:c,data:i}})=>{const p=[];let f;try{if(c!==t.LOAD&&!n)throw s;switch(c){case t.LOAD:f=await(async({coreURL:o,wasmURL:s,workerURL:E})=>{const c=!n;try{o||(o=e),importScripts(o)}catch{if(o&&o!==e||(o=e.replace("/umd/","/esm/")),self.createFFmpegCore=(await r(454)(o)).default,!self.createFFmpegCore)throw a}const i=o,p=s||o.replace(/.js$/g,".wasm"),f=E||o.replace(/.js$/g,".worker.js");return n=await self.createFFmpegCore({mainScriptUrlOrBlob:`${i}#${btoa(JSON.stringify({wasmURL:p,workerURL:f}))}`}),n.setLogger((e=>self.postMessage({type:t.LOG,data:e}))),n.setProgress((e=>self.postMessage({type:t.PROGRESS,data:e}))),c})(i);break;case t.EXEC:f=(({args:e,timeout:t=-1})=>{n.setTimeout(t),n.exec(...e);const r=n.ret;return n.reset(),r})(i);break;case t.FFPROBE:f=(({args:e,timeout:t=-1})=>{n.setTimeout(t),n.ffprobe(...e);const r=n.ret;return n.reset(),r})(i);break;case t.WRITE_FILE:f=(({path:e,data:t})=>(n.FS.writeFile(e,t),!0))(i);break;case t.READ_FILE:f=(({path:e,encoding:t})=>n.FS.readFile(e,{encoding:t}))(i);break;case t.DELETE_FILE:f=(({path:e})=>(n.FS.unlink(e),!0))(i);break;case t.RENAME:f=(({oldPath:e,newPath:t})=>(n.FS.rename(e,t),!0))(i);break;case t.CREATE_DIR:f=(({path:e})=>(n.FS.mkdir(e),!0))(i);break;case t.LIST_DIR:f=(({path:e})=>{const t=n.FS.readdir(e),r=[];for(const o of t){const t=n.FS.stat(`${e}/${o}`),s=n.FS.isDir(t.mode);r.push({name:o,isDir:s})}return r})(i);break;case t.DELETE_DIR:f=(({path:e})=>(n.FS.rmdir(e),!0))(i);break;case t.MOUNT:f=(({fsType:e,options:t,mountPoint:r})=>{const o=e,s=n.FS.filesystems[o];return!!s&&(n.FS.mount(s,t,r),!0)})(i);break;case t.UNMOUNT:f=(({mountPoint:e})=>(n.FS.unmount(e),!0))(i);break;default:throw o}}catch(e){return void self.postMessage({id:E,type:t.ERROR,data:e.toString()})}f instanceof Uint8Array&&p.push(f.buffer),self.postMessage({id:E,type:c,data:f},p)}})(),{}})()));
|
||||||
|
//# sourceMappingURL=814.ffmpeg.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,58 @@
|
|||||||
|
# Media to Text - Development Rules
|
||||||
|
|
||||||
|
## CRITICAL RULE: Never Upload Full Video Files
|
||||||
|
|
||||||
|
**Status**: MANDATORY - NEVER VIOLATE
|
||||||
|
|
||||||
|
### Rule Definition
|
||||||
|
- **NEVER** allow users to upload full video files (MP4, AVI, MOV, MKV, FLV, WMV, WEBM) to the server
|
||||||
|
- **ALWAYS** extract audio on the browser BEFORE uploading
|
||||||
|
- Only audio files (MP3, WAV, M4A, etc.) should be uploaded to the server
|
||||||
|
|
||||||
|
### Why This Rule Exists
|
||||||
|
1. **Bandwidth Efficiency**: Video files are 10-100x larger than extracted audio
|
||||||
|
2. **User Experience**: Faster uploads = better UX
|
||||||
|
3. **Server Resources**: Reduces server storage and processing load
|
||||||
|
4. **Cost**: Minimizes data transfer costs
|
||||||
|
|
||||||
|
### Implementation Requirements
|
||||||
|
- Browser-side audio extraction is MANDATORY for video files
|
||||||
|
- Use ffmpeg.wasm or equivalent WebAssembly solution
|
||||||
|
- If browser extraction fails, show error message - DO NOT fall back to uploading full video
|
||||||
|
- Server-side extraction is NOT acceptable as a workaround
|
||||||
|
|
||||||
|
### Current Status
|
||||||
|
- ❌ Browser extraction via ffmpeg.wasm: BLOCKED (CORS/CORB issues)
|
||||||
|
- ⚠️ Temporary workaround: Guide users to extract audio locally using ffmpeg CLI
|
||||||
|
- 🔄 TODO: Find working ffmpeg.wasm CDN or alternative solution
|
||||||
|
|
||||||
|
### Acceptable Solutions (In Priority Order)
|
||||||
|
1. ffmpeg.wasm from working CDN (no CORS/CORB issues)
|
||||||
|
2. Local ffmpeg.wasm with complete wasm binaries
|
||||||
|
3. Alternative WebAssembly video processing library
|
||||||
|
4. User-extracted audio files only (with clear instructions)
|
||||||
|
|
||||||
|
### NOT Acceptable
|
||||||
|
- ❌ Uploading full video files to server
|
||||||
|
- ❌ Server-side extraction as primary solution
|
||||||
|
- ❌ Removing this rule without explicit approval
|
||||||
|
|
||||||
|
## RULE: Show Model Download Progress
|
||||||
|
|
||||||
|
**Status**: MANDATORY
|
||||||
|
|
||||||
|
### Rule Definition
|
||||||
|
- **ALWAYS** show download progress when loading Whisper models
|
||||||
|
- Display percentage and/or file size being downloaded
|
||||||
|
- Models can be 150MB-500MB+ and take significant time to download
|
||||||
|
|
||||||
|
### Why This Rule Exists
|
||||||
|
1. **User Experience**: Users need to know the app isn't frozen
|
||||||
|
2. **Transparency**: Large downloads should be visible
|
||||||
|
3. **Expectations**: Users can estimate wait time
|
||||||
|
|
||||||
|
### Implementation Requirements
|
||||||
|
- Use progress callback from transformers.js pipeline
|
||||||
|
- Display progress percentage in status element
|
||||||
|
- Show "Downloading model: X%" or similar message
|
||||||
|
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.13-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y ffmpeg && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
CMD ["python", "server.py"]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Media to Text Converter
|
||||||
|
|
||||||
|
Convert audio files to text locally or in the browser.
|
||||||
|
|
||||||
|
## Browser Usage
|
||||||
|
|
||||||
|
Open `index.html` in Chrome or Edge:
|
||||||
|
- **Upload Audio**: Select an audio file (MP3, WAV, M4A, etc.) for transcription
|
||||||
|
- **Live Recording**: Click "Start Recording" to record and transcribe live audio
|
||||||
|
|
||||||
|
## Local Usage (Python)
|
||||||
|
|
||||||
|
For transcribing audio files using Python.
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Transcribe a file:
|
||||||
|
```bash
|
||||||
|
python transcribe.py audio.mp3
|
||||||
|
```
|
||||||
|
|
||||||
|
## Server Mode
|
||||||
|
|
||||||
|
Run Flask server for API access:
|
||||||
|
```bash
|
||||||
|
python server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Server runs on http://127.0.0.1:5000
|
||||||
|
|
||||||
|
**Note**: Server only accepts audio files (not video) per project rules.
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { pipeline, env } from 'https://cdn.jsdelivr.net/npm/@xenova/transformers@2.17.2';
|
||||||
|
import * as OpenCC from 'https://cdn.jsdelivr.net/npm/opencc-js@1.0.5/+esm';
|
||||||
|
|
||||||
|
env.allowLocalModels = false;
|
||||||
|
|
||||||
|
let progressCallback = (progress) => {
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
if (progress.status === 'progress') {
|
||||||
|
status.textContent = `Downloading model... ${progress.file}`;
|
||||||
|
} else if (progress.status === 'done') {
|
||||||
|
status.textContent = 'Model loaded';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
env.progress_callback = progressCallback;
|
||||||
|
|
||||||
|
const languageSelect = document.getElementById('language');
|
||||||
|
const recordBtn = document.getElementById('record-btn');
|
||||||
|
const stopBtn = document.getElementById('stop-btn');
|
||||||
|
const fileInput = document.getElementById('file-input');
|
||||||
|
const uploadBtn = document.getElementById('upload-btn');
|
||||||
|
const output = document.getElementById('output');
|
||||||
|
const status = document.getElementById('status');
|
||||||
|
const copyBtn = document.getElementById('copy-btn');
|
||||||
|
|
||||||
|
let recognition;
|
||||||
|
let transcriber = null;
|
||||||
|
let currentModel = null;
|
||||||
|
|
||||||
|
const getModelForLanguage = () => 'Xenova/whisper-small';
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', () => {
|
||||||
|
uploadBtn.disabled = !fileInput.files.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const extractAudio = async (file) => {
|
||||||
|
status.textContent = `Reading file... (${(file.size / 1024 / 1024).toFixed(1)} MB)`;
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
status.textContent = 'Decoding audio... (page may be briefly unresponsive)';
|
||||||
|
await new Promise(r => setTimeout(r, 100));
|
||||||
|
const audioContext = new AudioContext({ sampleRate: 16000 });
|
||||||
|
const audioBuffer = await new Promise((resolve, reject) => {
|
||||||
|
audioContext.decodeAudioData(arrayBuffer, resolve, reject);
|
||||||
|
});
|
||||||
|
status.textContent = `Audio decoded (${Math.round(audioBuffer.duration)}s)`;
|
||||||
|
await audioContext.close();
|
||||||
|
return audioBuffer.getChannelData(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
uploadBtn.addEventListener('click', async () => {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
uploadBtn.disabled = true;
|
||||||
|
output.textContent = '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const selectedLang = languageSelect.value;
|
||||||
|
const modelName = getModelForLanguage(selectedLang);
|
||||||
|
|
||||||
|
if (!transcriber || currentModel !== modelName) {
|
||||||
|
status.textContent = 'Downloading model...';
|
||||||
|
transcriber = await pipeline('automatic-speech-recognition', modelName);
|
||||||
|
currentModel = modelName;
|
||||||
|
}
|
||||||
|
|
||||||
|
status.textContent = 'Processing audio...';
|
||||||
|
const audio = await extractAudio(file);
|
||||||
|
|
||||||
|
status.textContent = 'Transcribing...';
|
||||||
|
const langCode = selectedLang.split('-')[0];
|
||||||
|
const result = await transcriber(audio, {
|
||||||
|
language: langCode,
|
||||||
|
chunk_length_s: 30,
|
||||||
|
stride_length_s: 5,
|
||||||
|
return_timestamps: false
|
||||||
|
});
|
||||||
|
let text = result.text;
|
||||||
|
if (selectedLang === 'zh-CN') {
|
||||||
|
const converter = OpenCC.Converter({ from: 'tw', to: 'cn' });
|
||||||
|
text = converter(text);
|
||||||
|
}
|
||||||
|
output.textContent = text;
|
||||||
|
copyBtn.style.display = 'block';
|
||||||
|
status.textContent = 'Complete';
|
||||||
|
} catch (err) {
|
||||||
|
status.textContent = `Error: ${err.message}`;
|
||||||
|
} finally {
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
recordBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await navigator.mediaDevices.getUserMedia({ audio: true });
|
||||||
|
|
||||||
|
recognition = new webkitSpeechRecognition();
|
||||||
|
recognition.continuous = true;
|
||||||
|
recognition.interimResults = true;
|
||||||
|
recognition.lang = languageSelect.value;
|
||||||
|
|
||||||
|
let transcript = '';
|
||||||
|
|
||||||
|
recognition.onresult = (event) => {
|
||||||
|
let interim = '';
|
||||||
|
for (let i = event.resultIndex; i < event.results.length; i++) {
|
||||||
|
if (event.results[i].isFinal) {
|
||||||
|
transcript += event.results[i][0].transcript + ' ';
|
||||||
|
} else {
|
||||||
|
interim += event.results[i][0].transcript;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
output.textContent = transcript + interim;
|
||||||
|
copyBtn.style.display = 'block';
|
||||||
|
};
|
||||||
|
|
||||||
|
recognition.start();
|
||||||
|
recordBtn.disabled = true;
|
||||||
|
stopBtn.disabled = false;
|
||||||
|
status.textContent = 'Recording...';
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
status.textContent = 'Microphone access denied';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
stopBtn.addEventListener('click', () => {
|
||||||
|
if (recognition) {
|
||||||
|
recognition.stop();
|
||||||
|
recordBtn.disabled = false;
|
||||||
|
stopBtn.disabled = true;
|
||||||
|
status.textContent = 'Recording stopped';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
copyBtn.addEventListener('click', () => {
|
||||||
|
navigator.clipboard.writeText(output.textContent);
|
||||||
|
status.textContent = 'Copied to clipboard';
|
||||||
|
});
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Cleanup script for media-to-text project
|
||||||
|
# Removes unused ffmpeg files (~18.3MB)
|
||||||
|
|
||||||
|
echo "🧹 Cleaning up unused files..."
|
||||||
|
|
||||||
|
# Remove unused ffmpeg directories and files
|
||||||
|
rm -rf ffmpeg/ ffmpeg-local/ ffmpeg-wasm/
|
||||||
|
rm -f 814.ffmpeg.js 814.ffmpeg.js.map
|
||||||
|
rm -f ffmpeg.js ffmpeg.js.map
|
||||||
|
rm -f ffmpeg-core.js ffmpeg-core.wasm ffmpeg-core.worker.js
|
||||||
|
|
||||||
|
echo "✅ Cleanup complete!"
|
||||||
|
echo ""
|
||||||
|
echo "Removed:"
|
||||||
|
echo " - ffmpeg/ directory"
|
||||||
|
echo " - ffmpeg-local/ directory"
|
||||||
|
echo " - ffmpeg-wasm/ directory"
|
||||||
|
echo " - Root-level ffmpeg files"
|
||||||
|
echo ""
|
||||||
|
echo "Space saved: ~18.3MB"
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
volumes:
|
||||||
|
- ./:/app
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
var threadInfoStruct=0;var selfThreadId=0;var parentThreadId=0;var Module={};function threadPrintErr(){var text=Array.prototype.slice.call(arguments).join(" ");console.error(text)}function threadAlert(){var text=Array.prototype.slice.call(arguments).join(" ");postMessage({cmd:"alert",text:text,threadId:selfThreadId})}var err=threadPrintErr;this.alert=threadAlert;Module["instantiateWasm"]=function(info,receiveInstance){var instance=new WebAssembly.Instance(Module["wasmModule"],info);Module["wasmModule"]=null;receiveInstance(instance);return instance.exports};this.onmessage=function(e){try{if(e.data.cmd==="load"){Module["DYNAMIC_BASE"]=e.data.DYNAMIC_BASE;Module["DYNAMICTOP_PTR"]=e.data.DYNAMICTOP_PTR;Module["wasmModule"]=e.data.wasmModule;Module["wasmMemory"]=e.data.wasmMemory;Module["buffer"]=Module["wasmMemory"].buffer;Module["ENVIRONMENT_IS_PTHREAD"]=true;if(typeof e.data.urlOrBlob==="string"){importScripts(e.data.urlOrBlob)}else{var objectUrl=URL.createObjectURL(e.data.urlOrBlob);importScripts(objectUrl);URL.revokeObjectURL(objectUrl)}postMessage({"cmd":"loaded"})}else if(e.data.cmd==="objectTransfer"){Module["PThread"].receiveObjectTransfer(e.data)}else if(e.data.cmd==="run"){Module["__performance_now_clock_drift"]=performance.now()-e.data.time;threadInfoStruct=e.data.threadInfoStruct;Module["__register_pthread_ptr"](threadInfoStruct,0,0);selfThreadId=e.data.selfThreadId;parentThreadId=e.data.parentThreadId;var max=e.data.stackBase;var top=e.data.stackBase+e.data.stackSize;Module["establishStackSpace"](top,max);Module["_emscripten_tls_init"]();Module["PThread"].receiveObjectTransfer(e.data);Module["PThread"].setThreadStatus(Module["_pthread_self"](),1);try{var result=Module["dynCall_ii"](e.data.start_routine,e.data.arg);if(!Module["getNoExitRuntime"]())Module["PThread"].threadExit(result)}catch(ex){if(ex==="Canceled!"){Module["PThread"].threadCancel()}else if(ex!="unwind"){Atomics.store(Module["HEAPU32"],threadInfoStruct+4>>2,ex instanceof Module["ExitStatus"]?ex.status:-2);Atomics.store(Module["HEAPU32"],threadInfoStruct+0>>2,1);Module["_emscripten_futex_wake"](threadInfoStruct+0,2147483647);if(!(ex instanceof Module["ExitStatus"]))throw ex}}}else if(e.data.cmd==="cancel"){if(threadInfoStruct){Module["PThread"].threadCancel()}}else if(e.data.target==="setimmediate"){}else if(e.data.cmd==="processThreadQueue"){if(threadInfoStruct){Module["_emscripten_current_thread_process_queued_calls"]()}}else{err("worker.js received unknown command "+e.data.cmd);err(e.data)}}catch(ex){err("worker.js onmessage() captured an uncaught exception: "+ex);if(ex.stack)err(ex.stack);throw ex}};if(typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string"){self={location:{href:__filename}};var onmessage=this.onmessage;var nodeWorkerThreads=require("worker_threads");Worker=nodeWorkerThreads.Worker;var parentPort=nodeWorkerThreads.parentPort;parentPort.on("message",function(data){onmessage({data:data})});var nodeFS=require("fs");var nodeRead=function(filename){return nodeFS.readFileSync(filename,"utf8")};function globalEval(x){global.require=require;global.Module=Module;eval.call(null,x)}importScripts=function(f){globalEval(nodeRead(f))};postMessage=function(msg){parentPort.postMessage(msg)};if(typeof performance==="undefined"){performance={now:function(){return Date.now()}}}}
|
||||||
Vendored
+162
@@ -0,0 +1,162 @@
|
|||||||
|
import { FSNode, FFMessageLoadConfig, OK, IsFirst, LogEventCallback, ProgressEventCallback, FileData, FFFSType, FFFSMountOptions, FFFSPath } from "./types.js";
|
||||||
|
type FFMessageOptions = {
|
||||||
|
signal?: AbortSignal;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Provides APIs to interact with ffmpeg web worker.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export declare class FFmpeg {
|
||||||
|
#private;
|
||||||
|
loaded: boolean;
|
||||||
|
/**
|
||||||
|
* Listen to log or prgress events from `ffmpeg.exec()`.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* ffmpeg.on("log", ({ type, message }) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* ffmpeg.on("progress", ({ progress, time }) => {
|
||||||
|
* // ...
|
||||||
|
* })
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* - log includes output to stdout and stderr.
|
||||||
|
* - The progress events are accurate only when the length of
|
||||||
|
* input and output video/audio file are the same.
|
||||||
|
*
|
||||||
|
* @category FFmpeg
|
||||||
|
*/
|
||||||
|
on(event: "log", callback: LogEventCallback): void;
|
||||||
|
on(event: "progress", callback: ProgressEventCallback): void;
|
||||||
|
/**
|
||||||
|
* Unlisten to log or progress events from `ffmpeg.exec()`.
|
||||||
|
*
|
||||||
|
* @category FFmpeg
|
||||||
|
*/
|
||||||
|
off(event: "log", callback: LogEventCallback): void;
|
||||||
|
off(event: "progress", callback: ProgressEventCallback): void;
|
||||||
|
/**
|
||||||
|
* Loads ffmpeg-core inside web worker. It is required to call this method first
|
||||||
|
* as it initializes WebAssembly and other essential variables.
|
||||||
|
*
|
||||||
|
* @category FFmpeg
|
||||||
|
* @returns `true` if ffmpeg core is loaded for the first time.
|
||||||
|
*/
|
||||||
|
load: ({ classWorkerURL, ...config }?: FFMessageLoadConfig, { signal }?: FFMessageOptions) => Promise<IsFirst>;
|
||||||
|
/**
|
||||||
|
* Execute ffmpeg command.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* To avoid common I/O issues, ["-nostdin", "-y"] are prepended to the args
|
||||||
|
* by default.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* await ffmpeg.load();
|
||||||
|
* await ffmpeg.writeFile("video.avi", ...);
|
||||||
|
* // ffmpeg -i video.avi video.mp4
|
||||||
|
* await ffmpeg.exec(["-i", "video.avi", "video.mp4"]);
|
||||||
|
* const data = ffmpeg.readFile("video.mp4");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @returns `0` if no error, `!= 0` if timeout (1) or error.
|
||||||
|
* @category FFmpeg
|
||||||
|
*/
|
||||||
|
exec: (args: string[], timeout?: number, { signal }?: FFMessageOptions) => Promise<number>;
|
||||||
|
/**
|
||||||
|
* Execute ffprobe command.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* await ffmpeg.load();
|
||||||
|
* await ffmpeg.writeFile("video.avi", ...);
|
||||||
|
* // Getting duration of a video in seconds: ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 video.avi -o output.txt
|
||||||
|
* await ffmpeg.ffprobe(["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", "video.avi", "-o", "output.txt"]);
|
||||||
|
* const data = ffmpeg.readFile("output.txt");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @returns `0` if no error, `!= 0` if timeout (1) or error.
|
||||||
|
* @category FFmpeg
|
||||||
|
*/
|
||||||
|
ffprobe: (args: string[], timeout?: number, { signal }?: FFMessageOptions) => Promise<number>;
|
||||||
|
/**
|
||||||
|
* Terminate all ongoing API calls and terminate web worker.
|
||||||
|
* `FFmpeg.load()` must be called again before calling any other APIs.
|
||||||
|
*
|
||||||
|
* @category FFmpeg
|
||||||
|
*/
|
||||||
|
terminate: () => void;
|
||||||
|
/**
|
||||||
|
* Write data to ffmpeg.wasm.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* await ffmpeg.load();
|
||||||
|
* await ffmpeg.writeFile("video.avi", await fetchFile("../video.avi"));
|
||||||
|
* await ffmpeg.writeFile("text.txt", "hello world");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
writeFile: (path: string, data: FileData, { signal }?: FFMessageOptions) => Promise<OK>;
|
||||||
|
mount: (fsType: FFFSType, options: FFFSMountOptions, mountPoint: FFFSPath) => Promise<OK>;
|
||||||
|
unmount: (mountPoint: FFFSPath) => Promise<OK>;
|
||||||
|
/**
|
||||||
|
* Read data from ffmpeg.wasm.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* await ffmpeg.load();
|
||||||
|
* const data = await ffmpeg.readFile("video.mp4");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
readFile: (path: string, encoding?: string, { signal }?: FFMessageOptions) => Promise<FileData>;
|
||||||
|
/**
|
||||||
|
* Delete a file.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
deleteFile: (path: string, { signal }?: FFMessageOptions) => Promise<OK>;
|
||||||
|
/**
|
||||||
|
* Rename a file or directory.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
rename: (oldPath: string, newPath: string, { signal }?: FFMessageOptions) => Promise<OK>;
|
||||||
|
/**
|
||||||
|
* Create a directory.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
createDir: (path: string, { signal }?: FFMessageOptions) => Promise<OK>;
|
||||||
|
/**
|
||||||
|
* List directory contents.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
listDir: (path: string, { signal }?: FFMessageOptions) => Promise<FSNode[]>;
|
||||||
|
/**
|
||||||
|
* Delete an empty directory.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
deleteDir: (path: string, { signal }?: FFMessageOptions) => Promise<OK>;
|
||||||
|
}
|
||||||
|
export {};
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
import { FFMessageType } from "./const.js";
|
||||||
|
import { getMessageID } from "./utils.js";
|
||||||
|
import { ERROR_TERMINATED, ERROR_NOT_LOADED } from "./errors.js";
|
||||||
|
/**
|
||||||
|
* Provides APIs to interact with ffmpeg web worker.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export class FFmpeg {
|
||||||
|
#worker = null;
|
||||||
|
/**
|
||||||
|
* #resolves and #rejects tracks Promise resolves and rejects to
|
||||||
|
* be called when we receive message from web worker.
|
||||||
|
*/
|
||||||
|
#resolves = {};
|
||||||
|
#rejects = {};
|
||||||
|
#logEventCallbacks = [];
|
||||||
|
#progressEventCallbacks = [];
|
||||||
|
loaded = false;
|
||||||
|
/**
|
||||||
|
* register worker message event handlers.
|
||||||
|
*/
|
||||||
|
#registerHandlers = () => {
|
||||||
|
if (this.#worker) {
|
||||||
|
this.#worker.onmessage = ({ data: { id, type, data }, }) => {
|
||||||
|
switch (type) {
|
||||||
|
case FFMessageType.LOAD:
|
||||||
|
this.loaded = true;
|
||||||
|
this.#resolves[id](data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.MOUNT:
|
||||||
|
case FFMessageType.UNMOUNT:
|
||||||
|
case FFMessageType.EXEC:
|
||||||
|
case FFMessageType.FFPROBE:
|
||||||
|
case FFMessageType.WRITE_FILE:
|
||||||
|
case FFMessageType.READ_FILE:
|
||||||
|
case FFMessageType.DELETE_FILE:
|
||||||
|
case FFMessageType.RENAME:
|
||||||
|
case FFMessageType.CREATE_DIR:
|
||||||
|
case FFMessageType.LIST_DIR:
|
||||||
|
case FFMessageType.DELETE_DIR:
|
||||||
|
this.#resolves[id](data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.LOG:
|
||||||
|
this.#logEventCallbacks.forEach((f) => f(data));
|
||||||
|
break;
|
||||||
|
case FFMessageType.PROGRESS:
|
||||||
|
this.#progressEventCallbacks.forEach((f) => f(data));
|
||||||
|
break;
|
||||||
|
case FFMessageType.ERROR:
|
||||||
|
this.#rejects[id](data);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
delete this.#resolves[id];
|
||||||
|
delete this.#rejects[id];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Generic function to send messages to web worker.
|
||||||
|
*/
|
||||||
|
#send = ({ type, data }, trans = [], signal) => {
|
||||||
|
if (!this.#worker) {
|
||||||
|
return Promise.reject(ERROR_NOT_LOADED);
|
||||||
|
}
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const id = getMessageID();
|
||||||
|
this.#worker && this.#worker.postMessage({ id, type, data }, trans);
|
||||||
|
this.#resolves[id] = resolve;
|
||||||
|
this.#rejects[id] = reject;
|
||||||
|
signal?.addEventListener("abort", () => {
|
||||||
|
reject(new DOMException(`Message # ${id} was aborted`, "AbortError"));
|
||||||
|
}, { once: true });
|
||||||
|
});
|
||||||
|
};
|
||||||
|
on(event, callback) {
|
||||||
|
if (event === "log") {
|
||||||
|
this.#logEventCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
else if (event === "progress") {
|
||||||
|
this.#progressEventCallbacks.push(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
off(event, callback) {
|
||||||
|
if (event === "log") {
|
||||||
|
this.#logEventCallbacks = this.#logEventCallbacks.filter((f) => f !== callback);
|
||||||
|
}
|
||||||
|
else if (event === "progress") {
|
||||||
|
this.#progressEventCallbacks = this.#progressEventCallbacks.filter((f) => f !== callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Loads ffmpeg-core inside web worker. It is required to call this method first
|
||||||
|
* as it initializes WebAssembly and other essential variables.
|
||||||
|
*
|
||||||
|
* @category FFmpeg
|
||||||
|
* @returns `true` if ffmpeg core is loaded for the first time.
|
||||||
|
*/
|
||||||
|
load = ({ classWorkerURL, ...config } = {}, { signal } = {}) => {
|
||||||
|
if (!this.#worker) {
|
||||||
|
this.#worker = classWorkerURL ?
|
||||||
|
new Worker(new URL(classWorkerURL, import.meta.url), {
|
||||||
|
type: "module",
|
||||||
|
}) :
|
||||||
|
// We need to duplicated the code here to enable webpack
|
||||||
|
// to bundle worekr.js here.
|
||||||
|
new Worker(new URL("./worker.js", import.meta.url), {
|
||||||
|
type: "module",
|
||||||
|
});
|
||||||
|
this.#registerHandlers();
|
||||||
|
}
|
||||||
|
return this.#send({
|
||||||
|
type: FFMessageType.LOAD,
|
||||||
|
data: config,
|
||||||
|
}, undefined, signal);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Execute ffmpeg command.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* To avoid common I/O issues, ["-nostdin", "-y"] are prepended to the args
|
||||||
|
* by default.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* await ffmpeg.load();
|
||||||
|
* await ffmpeg.writeFile("video.avi", ...);
|
||||||
|
* // ffmpeg -i video.avi video.mp4
|
||||||
|
* await ffmpeg.exec(["-i", "video.avi", "video.mp4"]);
|
||||||
|
* const data = ffmpeg.readFile("video.mp4");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @returns `0` if no error, `!= 0` if timeout (1) or error.
|
||||||
|
* @category FFmpeg
|
||||||
|
*/
|
||||||
|
exec = (
|
||||||
|
/** ffmpeg command line args */
|
||||||
|
args,
|
||||||
|
/**
|
||||||
|
* milliseconds to wait before stopping the command execution.
|
||||||
|
*
|
||||||
|
* @defaultValue -1
|
||||||
|
*/
|
||||||
|
timeout = -1, { signal } = {}) => this.#send({
|
||||||
|
type: FFMessageType.EXEC,
|
||||||
|
data: { args, timeout },
|
||||||
|
}, undefined, signal);
|
||||||
|
/**
|
||||||
|
* Execute ffprobe command.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* await ffmpeg.load();
|
||||||
|
* await ffmpeg.writeFile("video.avi", ...);
|
||||||
|
* // Getting duration of a video in seconds: ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 video.avi -o output.txt
|
||||||
|
* await ffmpeg.ffprobe(["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", "video.avi", "-o", "output.txt"]);
|
||||||
|
* const data = ffmpeg.readFile("output.txt");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @returns `0` if no error, `!= 0` if timeout (1) or error.
|
||||||
|
* @category FFmpeg
|
||||||
|
*/
|
||||||
|
ffprobe = (
|
||||||
|
/** ffprobe command line args */
|
||||||
|
args,
|
||||||
|
/**
|
||||||
|
* milliseconds to wait before stopping the command execution.
|
||||||
|
*
|
||||||
|
* @defaultValue -1
|
||||||
|
*/
|
||||||
|
timeout = -1, { signal } = {}) => this.#send({
|
||||||
|
type: FFMessageType.FFPROBE,
|
||||||
|
data: { args, timeout },
|
||||||
|
}, undefined, signal);
|
||||||
|
/**
|
||||||
|
* Terminate all ongoing API calls and terminate web worker.
|
||||||
|
* `FFmpeg.load()` must be called again before calling any other APIs.
|
||||||
|
*
|
||||||
|
* @category FFmpeg
|
||||||
|
*/
|
||||||
|
terminate = () => {
|
||||||
|
const ids = Object.keys(this.#rejects);
|
||||||
|
// rejects all incomplete Promises.
|
||||||
|
for (const id of ids) {
|
||||||
|
this.#rejects[id](ERROR_TERMINATED);
|
||||||
|
delete this.#rejects[id];
|
||||||
|
delete this.#resolves[id];
|
||||||
|
}
|
||||||
|
if (this.#worker) {
|
||||||
|
this.#worker.terminate();
|
||||||
|
this.#worker = null;
|
||||||
|
this.loaded = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Write data to ffmpeg.wasm.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* await ffmpeg.load();
|
||||||
|
* await ffmpeg.writeFile("video.avi", await fetchFile("../video.avi"));
|
||||||
|
* await ffmpeg.writeFile("text.txt", "hello world");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
writeFile = (path, data, { signal } = {}) => {
|
||||||
|
const trans = [];
|
||||||
|
if (data instanceof Uint8Array) {
|
||||||
|
trans.push(data.buffer);
|
||||||
|
}
|
||||||
|
return this.#send({
|
||||||
|
type: FFMessageType.WRITE_FILE,
|
||||||
|
data: { path, data },
|
||||||
|
}, trans, signal);
|
||||||
|
};
|
||||||
|
mount = (fsType, options, mountPoint) => {
|
||||||
|
const trans = [];
|
||||||
|
return this.#send({
|
||||||
|
type: FFMessageType.MOUNT,
|
||||||
|
data: { fsType, options, mountPoint },
|
||||||
|
}, trans);
|
||||||
|
};
|
||||||
|
unmount = (mountPoint) => {
|
||||||
|
const trans = [];
|
||||||
|
return this.#send({
|
||||||
|
type: FFMessageType.UNMOUNT,
|
||||||
|
data: { mountPoint },
|
||||||
|
}, trans);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Read data from ffmpeg.wasm.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const ffmpeg = new FFmpeg();
|
||||||
|
* await ffmpeg.load();
|
||||||
|
* const data = await ffmpeg.readFile("video.mp4");
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
readFile = (path,
|
||||||
|
/**
|
||||||
|
* File content encoding, supports two encodings:
|
||||||
|
* - utf8: read file as text file, return data in string type.
|
||||||
|
* - binary: read file as binary file, return data in Uint8Array type.
|
||||||
|
*
|
||||||
|
* @defaultValue binary
|
||||||
|
*/
|
||||||
|
encoding = "binary", { signal } = {}) => this.#send({
|
||||||
|
type: FFMessageType.READ_FILE,
|
||||||
|
data: { path, encoding },
|
||||||
|
}, undefined, signal);
|
||||||
|
/**
|
||||||
|
* Delete a file.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
deleteFile = (path, { signal } = {}) => this.#send({
|
||||||
|
type: FFMessageType.DELETE_FILE,
|
||||||
|
data: { path },
|
||||||
|
}, undefined, signal);
|
||||||
|
/**
|
||||||
|
* Rename a file or directory.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
rename = (oldPath, newPath, { signal } = {}) => this.#send({
|
||||||
|
type: FFMessageType.RENAME,
|
||||||
|
data: { oldPath, newPath },
|
||||||
|
}, undefined, signal);
|
||||||
|
/**
|
||||||
|
* Create a directory.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
createDir = (path, { signal } = {}) => this.#send({
|
||||||
|
type: FFMessageType.CREATE_DIR,
|
||||||
|
data: { path },
|
||||||
|
}, undefined, signal);
|
||||||
|
/**
|
||||||
|
* List directory contents.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
listDir = (path, { signal } = {}) => this.#send({
|
||||||
|
type: FFMessageType.LIST_DIR,
|
||||||
|
data: { path },
|
||||||
|
}, undefined, signal);
|
||||||
|
/**
|
||||||
|
* Delete an empty directory.
|
||||||
|
*
|
||||||
|
* @category File System
|
||||||
|
*/
|
||||||
|
deleteDir = (path, { signal } = {}) => this.#send({
|
||||||
|
type: FFMessageType.DELETE_DIR,
|
||||||
|
data: { path },
|
||||||
|
}, undefined, signal);
|
||||||
|
}
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
export declare const MIME_TYPE_JAVASCRIPT = "text/javascript";
|
||||||
|
export declare const MIME_TYPE_WASM = "application/wasm";
|
||||||
|
export declare const CORE_VERSION = "0.12.9";
|
||||||
|
export declare const CORE_URL: string;
|
||||||
|
export declare enum FFMessageType {
|
||||||
|
LOAD = "LOAD",
|
||||||
|
EXEC = "EXEC",
|
||||||
|
FFPROBE = "FFPROBE",
|
||||||
|
WRITE_FILE = "WRITE_FILE",
|
||||||
|
READ_FILE = "READ_FILE",
|
||||||
|
DELETE_FILE = "DELETE_FILE",
|
||||||
|
RENAME = "RENAME",
|
||||||
|
CREATE_DIR = "CREATE_DIR",
|
||||||
|
LIST_DIR = "LIST_DIR",
|
||||||
|
DELETE_DIR = "DELETE_DIR",
|
||||||
|
ERROR = "ERROR",
|
||||||
|
DOWNLOAD = "DOWNLOAD",
|
||||||
|
PROGRESS = "PROGRESS",
|
||||||
|
LOG = "LOG",
|
||||||
|
MOUNT = "MOUNT",
|
||||||
|
UNMOUNT = "UNMOUNT"
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export const MIME_TYPE_JAVASCRIPT = "text/javascript";
|
||||||
|
export const MIME_TYPE_WASM = "application/wasm";
|
||||||
|
export const CORE_VERSION = "0.12.9";
|
||||||
|
export const CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;
|
||||||
|
export var FFMessageType;
|
||||||
|
(function (FFMessageType) {
|
||||||
|
FFMessageType["LOAD"] = "LOAD";
|
||||||
|
FFMessageType["EXEC"] = "EXEC";
|
||||||
|
FFMessageType["FFPROBE"] = "FFPROBE";
|
||||||
|
FFMessageType["WRITE_FILE"] = "WRITE_FILE";
|
||||||
|
FFMessageType["READ_FILE"] = "READ_FILE";
|
||||||
|
FFMessageType["DELETE_FILE"] = "DELETE_FILE";
|
||||||
|
FFMessageType["RENAME"] = "RENAME";
|
||||||
|
FFMessageType["CREATE_DIR"] = "CREATE_DIR";
|
||||||
|
FFMessageType["LIST_DIR"] = "LIST_DIR";
|
||||||
|
FFMessageType["DELETE_DIR"] = "DELETE_DIR";
|
||||||
|
FFMessageType["ERROR"] = "ERROR";
|
||||||
|
FFMessageType["DOWNLOAD"] = "DOWNLOAD";
|
||||||
|
FFMessageType["PROGRESS"] = "PROGRESS";
|
||||||
|
FFMessageType["LOG"] = "LOG";
|
||||||
|
FFMessageType["MOUNT"] = "MOUNT";
|
||||||
|
FFMessageType["UNMOUNT"] = "UNMOUNT";
|
||||||
|
})(FFMessageType || (FFMessageType = {}));
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export declare class FFmpeg {
|
||||||
|
constructor();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// File to be imported in node enviroments
|
||||||
|
export class FFmpeg {
|
||||||
|
constructor() {
|
||||||
|
throw new Error("ffmpeg.wasm does not support nodejs");
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
export declare const ERROR_UNKNOWN_MESSAGE_TYPE: Error;
|
||||||
|
export declare const ERROR_NOT_LOADED: Error;
|
||||||
|
export declare const ERROR_TERMINATED: Error;
|
||||||
|
export declare const ERROR_IMPORT_FAILURE: Error;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export const ERROR_UNKNOWN_MESSAGE_TYPE = new Error("unknown message type");
|
||||||
|
export const ERROR_NOT_LOADED = new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first");
|
||||||
|
export const ERROR_TERMINATED = new Error("called FFmpeg.terminate()");
|
||||||
|
export const ERROR_IMPORT_FAILURE = new Error("failed to import ffmpeg-core.js");
|
||||||
Vendored
+2
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./classes.js";
|
||||||
|
export * from "./types.js";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from "./classes.js";
|
||||||
|
export * from "./types.js";
|
||||||
Vendored
+131
@@ -0,0 +1,131 @@
|
|||||||
|
export type FFFSPath = string;
|
||||||
|
/**
|
||||||
|
* ffmpeg-core loading configuration.
|
||||||
|
*/
|
||||||
|
export interface FFMessageLoadConfig {
|
||||||
|
/**
|
||||||
|
* `ffmpeg-core.js` URL.
|
||||||
|
*
|
||||||
|
* @defaultValue `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;
|
||||||
|
*/
|
||||||
|
coreURL?: string;
|
||||||
|
/**
|
||||||
|
* `ffmpeg-core.wasm` URL.
|
||||||
|
*
|
||||||
|
* @defaultValue `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.wasm`;
|
||||||
|
*/
|
||||||
|
wasmURL?: string;
|
||||||
|
/**
|
||||||
|
* `ffmpeg-core.worker.js` URL. This worker is spawned when using multithread version of ffmpeg-core.
|
||||||
|
*
|
||||||
|
* @ref: https://ffmpegwasm.netlify.app/docs/overview#architecture
|
||||||
|
* @defaultValue `https://unpkg.com/@ffmpeg/core-mt@${CORE_VERSION}/dist/umd/ffmpeg-core.worker.js`;
|
||||||
|
*/
|
||||||
|
workerURL?: string;
|
||||||
|
/**
|
||||||
|
* `ffmpeg.worker.js` URL. This worker is spawned when FFmpeg.load() is called, it is an essential worker and usually you don't need to update this config.
|
||||||
|
*
|
||||||
|
* @ref: https://ffmpegwasm.netlify.app/docs/overview#architecture
|
||||||
|
* @defaultValue `./worker.js`
|
||||||
|
*/
|
||||||
|
classWorkerURL?: string;
|
||||||
|
}
|
||||||
|
export interface FFMessageExecData {
|
||||||
|
args: string[];
|
||||||
|
timeout?: number;
|
||||||
|
}
|
||||||
|
export interface FFMessageWriteFileData {
|
||||||
|
path: FFFSPath;
|
||||||
|
data: FileData;
|
||||||
|
}
|
||||||
|
export interface FFMessageReadFileData {
|
||||||
|
path: FFFSPath;
|
||||||
|
encoding: string;
|
||||||
|
}
|
||||||
|
export interface FFMessageDeleteFileData {
|
||||||
|
path: FFFSPath;
|
||||||
|
}
|
||||||
|
export interface FFMessageRenameData {
|
||||||
|
oldPath: FFFSPath;
|
||||||
|
newPath: FFFSPath;
|
||||||
|
}
|
||||||
|
export interface FFMessageCreateDirData {
|
||||||
|
path: FFFSPath;
|
||||||
|
}
|
||||||
|
export interface FFMessageListDirData {
|
||||||
|
path: FFFSPath;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @remarks
|
||||||
|
* Only deletes empty directory.
|
||||||
|
*/
|
||||||
|
export interface FFMessageDeleteDirData {
|
||||||
|
path: FFFSPath;
|
||||||
|
}
|
||||||
|
export declare enum FFFSType {
|
||||||
|
MEMFS = "MEMFS",
|
||||||
|
NODEFS = "NODEFS",
|
||||||
|
NODERAWFS = "NODERAWFS",
|
||||||
|
IDBFS = "IDBFS",
|
||||||
|
WORKERFS = "WORKERFS",
|
||||||
|
PROXYFS = "PROXYFS"
|
||||||
|
}
|
||||||
|
export type WorkerFSFileEntry = File;
|
||||||
|
export interface WorkerFSBlobEntry {
|
||||||
|
name: string;
|
||||||
|
data: Blob;
|
||||||
|
}
|
||||||
|
export interface WorkerFSMountData {
|
||||||
|
blobs?: WorkerFSBlobEntry[];
|
||||||
|
files?: WorkerFSFileEntry[];
|
||||||
|
}
|
||||||
|
export type FFFSMountOptions = WorkerFSMountData;
|
||||||
|
export interface FFMessageMountData {
|
||||||
|
fsType: FFFSType;
|
||||||
|
options: FFFSMountOptions;
|
||||||
|
mountPoint: FFFSPath;
|
||||||
|
}
|
||||||
|
export interface FFMessageUnmountData {
|
||||||
|
mountPoint: FFFSPath;
|
||||||
|
}
|
||||||
|
export type FFMessageData = FFMessageLoadConfig | FFMessageExecData | FFMessageWriteFileData | FFMessageReadFileData | FFMessageDeleteFileData | FFMessageRenameData | FFMessageCreateDirData | FFMessageListDirData | FFMessageDeleteDirData | FFMessageMountData | FFMessageUnmountData;
|
||||||
|
export interface Message {
|
||||||
|
type: string;
|
||||||
|
data?: FFMessageData;
|
||||||
|
}
|
||||||
|
export interface FFMessage extends Message {
|
||||||
|
id: number;
|
||||||
|
}
|
||||||
|
export interface FFMessageEvent extends MessageEvent {
|
||||||
|
data: FFMessage;
|
||||||
|
}
|
||||||
|
export interface LogEvent {
|
||||||
|
type: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
export interface ProgressEvent {
|
||||||
|
progress: number;
|
||||||
|
time: number;
|
||||||
|
}
|
||||||
|
export type ExitCode = number;
|
||||||
|
export type ErrorMessage = string;
|
||||||
|
export type FileData = Uint8Array | string;
|
||||||
|
export type IsFirst = boolean;
|
||||||
|
export type OK = boolean;
|
||||||
|
export interface FSNode {
|
||||||
|
name: string;
|
||||||
|
isDir: boolean;
|
||||||
|
}
|
||||||
|
export type CallbackData = FileData | ExitCode | ErrorMessage | LogEvent | ProgressEvent | IsFirst | OK | Error | FSNode[] | undefined;
|
||||||
|
export interface Callbacks {
|
||||||
|
[id: number | string]: (data: CallbackData) => void;
|
||||||
|
}
|
||||||
|
export type LogEventCallback = (event: LogEvent) => void;
|
||||||
|
export type ProgressEventCallback = (event: ProgressEvent) => void;
|
||||||
|
export interface FFMessageEventCallback {
|
||||||
|
data: {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
data: CallbackData;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export var FFFSType;
|
||||||
|
(function (FFFSType) {
|
||||||
|
FFFSType["MEMFS"] = "MEMFS";
|
||||||
|
FFFSType["NODEFS"] = "NODEFS";
|
||||||
|
FFFSType["NODERAWFS"] = "NODERAWFS";
|
||||||
|
FFFSType["IDBFS"] = "IDBFS";
|
||||||
|
FFFSType["WORKERFS"] = "WORKERFS";
|
||||||
|
FFFSType["PROXYFS"] = "PROXYFS";
|
||||||
|
})(FFFSType || (FFFSType = {}));
|
||||||
Vendored
+4
@@ -0,0 +1,4 @@
|
|||||||
|
/**
|
||||||
|
* Generate an unique message ID.
|
||||||
|
*/
|
||||||
|
export declare const getMessageID: () => number;
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* Generate an unique message ID.
|
||||||
|
*/
|
||||||
|
export const getMessageID = (() => {
|
||||||
|
let messageID = 0;
|
||||||
|
return () => messageID++;
|
||||||
|
})();
|
||||||
Vendored
+9
@@ -0,0 +1,9 @@
|
|||||||
|
/// <reference no-default-lib="true"/>
|
||||||
|
/// <reference lib="esnext" />
|
||||||
|
/// <reference lib="webworker" />
|
||||||
|
import type { FFmpegCoreModuleFactory } from "@ffmpeg/types";
|
||||||
|
declare global {
|
||||||
|
interface WorkerGlobalScope {
|
||||||
|
createFFmpegCore: FFmpegCoreModuleFactory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/// <reference no-default-lib="true" />
|
||||||
|
/// <reference lib="esnext" />
|
||||||
|
/// <reference lib="webworker" />
|
||||||
|
import { CORE_URL, FFMessageType } from "./const.js";
|
||||||
|
import { ERROR_UNKNOWN_MESSAGE_TYPE, ERROR_NOT_LOADED, ERROR_IMPORT_FAILURE, } from "./errors.js";
|
||||||
|
let ffmpeg;
|
||||||
|
const load = async ({ coreURL: _coreURL, wasmURL: _wasmURL, workerURL: _workerURL, }) => {
|
||||||
|
const first = !ffmpeg;
|
||||||
|
try {
|
||||||
|
if (!_coreURL)
|
||||||
|
_coreURL = CORE_URL;
|
||||||
|
// when web worker type is `classic`.
|
||||||
|
importScripts(_coreURL);
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
if (!_coreURL || _coreURL === CORE_URL)
|
||||||
|
_coreURL = CORE_URL.replace('/umd/', '/esm/');
|
||||||
|
// when web worker type is `module`.
|
||||||
|
self.createFFmpegCore = (await import(
|
||||||
|
/* @vite-ignore */ _coreURL)).default;
|
||||||
|
if (!self.createFFmpegCore) {
|
||||||
|
throw ERROR_IMPORT_FAILURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const coreURL = _coreURL;
|
||||||
|
const wasmURL = _wasmURL ? _wasmURL : _coreURL.replace(/.js$/g, ".wasm");
|
||||||
|
const workerURL = _workerURL
|
||||||
|
? _workerURL
|
||||||
|
: _coreURL.replace(/.js$/g, ".worker.js");
|
||||||
|
ffmpeg = await self.createFFmpegCore({
|
||||||
|
// Fix `Overload resolution failed.` when using multi-threaded ffmpeg-core.
|
||||||
|
// Encoded wasmURL and workerURL in the URL as a hack to fix locateFile issue.
|
||||||
|
mainScriptUrlOrBlob: `${coreURL}#${btoa(JSON.stringify({ wasmURL, workerURL }))}`,
|
||||||
|
});
|
||||||
|
ffmpeg.setLogger((data) => self.postMessage({ type: FFMessageType.LOG, data }));
|
||||||
|
ffmpeg.setProgress((data) => self.postMessage({
|
||||||
|
type: FFMessageType.PROGRESS,
|
||||||
|
data,
|
||||||
|
}));
|
||||||
|
return first;
|
||||||
|
};
|
||||||
|
const exec = ({ args, timeout = -1 }) => {
|
||||||
|
ffmpeg.setTimeout(timeout);
|
||||||
|
ffmpeg.exec(...args);
|
||||||
|
const ret = ffmpeg.ret;
|
||||||
|
ffmpeg.reset();
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
const ffprobe = ({ args, timeout = -1 }) => {
|
||||||
|
ffmpeg.setTimeout(timeout);
|
||||||
|
ffmpeg.ffprobe(...args);
|
||||||
|
const ret = ffmpeg.ret;
|
||||||
|
ffmpeg.reset();
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
|
const writeFile = ({ path, data }) => {
|
||||||
|
ffmpeg.FS.writeFile(path, data);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const readFile = ({ path, encoding }) => ffmpeg.FS.readFile(path, { encoding });
|
||||||
|
// TODO: check if deletion works.
|
||||||
|
const deleteFile = ({ path }) => {
|
||||||
|
ffmpeg.FS.unlink(path);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const rename = ({ oldPath, newPath }) => {
|
||||||
|
ffmpeg.FS.rename(oldPath, newPath);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
// TODO: check if creation works.
|
||||||
|
const createDir = ({ path }) => {
|
||||||
|
ffmpeg.FS.mkdir(path);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const listDir = ({ path }) => {
|
||||||
|
const names = ffmpeg.FS.readdir(path);
|
||||||
|
const nodes = [];
|
||||||
|
for (const name of names) {
|
||||||
|
const stat = ffmpeg.FS.stat(`${path}/${name}`);
|
||||||
|
const isDir = ffmpeg.FS.isDir(stat.mode);
|
||||||
|
nodes.push({ name, isDir });
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
};
|
||||||
|
// TODO: check if deletion works.
|
||||||
|
const deleteDir = ({ path }) => {
|
||||||
|
ffmpeg.FS.rmdir(path);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const mount = ({ fsType, options, mountPoint }) => {
|
||||||
|
const str = fsType;
|
||||||
|
const fs = ffmpeg.FS.filesystems[str];
|
||||||
|
if (!fs)
|
||||||
|
return false;
|
||||||
|
ffmpeg.FS.mount(fs, options, mountPoint);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
const unmount = ({ mountPoint }) => {
|
||||||
|
ffmpeg.FS.unmount(mountPoint);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
self.onmessage = async ({ data: { id, type, data: _data }, }) => {
|
||||||
|
const trans = [];
|
||||||
|
let data;
|
||||||
|
try {
|
||||||
|
if (type !== FFMessageType.LOAD && !ffmpeg)
|
||||||
|
throw ERROR_NOT_LOADED; // eslint-disable-line
|
||||||
|
switch (type) {
|
||||||
|
case FFMessageType.LOAD:
|
||||||
|
data = await load(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.EXEC:
|
||||||
|
data = exec(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.FFPROBE:
|
||||||
|
data = ffprobe(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.WRITE_FILE:
|
||||||
|
data = writeFile(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.READ_FILE:
|
||||||
|
data = readFile(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.DELETE_FILE:
|
||||||
|
data = deleteFile(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.RENAME:
|
||||||
|
data = rename(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.CREATE_DIR:
|
||||||
|
data = createDir(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.LIST_DIR:
|
||||||
|
data = listDir(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.DELETE_DIR:
|
||||||
|
data = deleteDir(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.MOUNT:
|
||||||
|
data = mount(_data);
|
||||||
|
break;
|
||||||
|
case FFMessageType.UNMOUNT:
|
||||||
|
data = unmount(_data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw ERROR_UNKNOWN_MESSAGE_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
self.postMessage({
|
||||||
|
id,
|
||||||
|
type: FFMessageType.ERROR,
|
||||||
|
data: e.toString(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data instanceof Uint8Array) {
|
||||||
|
trans.push(data.buffer);
|
||||||
|
}
|
||||||
|
self.postMessage({ id, type, data }, trans);
|
||||||
|
};
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{var e={454:e=>{function t(e){return Promise.resolve().then((()=>{var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}))}t.keys=()=>[],t.resolve=t,t.id=454,e.exports=t}},t={};function r(o){var s=t[o];if(void 0!==s)return s.exports;var a=t[o]={exports:{}};return e[o](a,a.exports,r),a.exports}return r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e="https://unpkg.com/@ffmpeg/core@0.12.9/dist/umd/ffmpeg-core.js";var t;!function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const o=new Error("unknown message type"),s=new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first"),a=(new Error("called FFmpeg.terminate()"),new Error("failed to import ffmpeg-core.js"));let n;self.onmessage=async({data:{id:E,type:c,data:i}})=>{const p=[];let f;try{if(c!==t.LOAD&&!n)throw s;switch(c){case t.LOAD:f=await(async({coreURL:o,wasmURL:s,workerURL:E})=>{const c=!n;try{o||(o=e),importScripts(o)}catch{if(o&&o!==e||(o=e.replace("/umd/","/esm/")),self.createFFmpegCore=(await r(454)(o)).default,!self.createFFmpegCore)throw a}const i=o,p=s||o.replace(/.js$/g,".wasm"),f=E||o.replace(/.js$/g,".worker.js");return n=await self.createFFmpegCore({mainScriptUrlOrBlob:`${i}#${btoa(JSON.stringify({wasmURL:p,workerURL:f}))}`}),n.setLogger((e=>self.postMessage({type:t.LOG,data:e}))),n.setProgress((e=>self.postMessage({type:t.PROGRESS,data:e}))),c})(i);break;case t.EXEC:f=(({args:e,timeout:t=-1})=>{n.setTimeout(t),n.exec(...e);const r=n.ret;return n.reset(),r})(i);break;case t.FFPROBE:f=(({args:e,timeout:t=-1})=>{n.setTimeout(t),n.ffprobe(...e);const r=n.ret;return n.reset(),r})(i);break;case t.WRITE_FILE:f=(({path:e,data:t})=>(n.FS.writeFile(e,t),!0))(i);break;case t.READ_FILE:f=(({path:e,encoding:t})=>n.FS.readFile(e,{encoding:t}))(i);break;case t.DELETE_FILE:f=(({path:e})=>(n.FS.unlink(e),!0))(i);break;case t.RENAME:f=(({oldPath:e,newPath:t})=>(n.FS.rename(e,t),!0))(i);break;case t.CREATE_DIR:f=(({path:e})=>(n.FS.mkdir(e),!0))(i);break;case t.LIST_DIR:f=(({path:e})=>{const t=n.FS.readdir(e),r=[];for(const o of t){const t=n.FS.stat(`${e}/${o}`),s=n.FS.isDir(t.mode);r.push({name:o,isDir:s})}return r})(i);break;case t.DELETE_DIR:f=(({path:e})=>(n.FS.rmdir(e),!0))(i);break;case t.MOUNT:f=(({fsType:e,options:t,mountPoint:r})=>{const o=e,s=n.FS.filesystems[o];return!!s&&(n.FS.mount(s,t,r),!0)})(i);break;case t.UNMOUNT:f=(({mountPoint:e})=>(n.FS.unmount(e),!0))(i);break;default:throw o}}catch(e){return void self.postMessage({id:E,type:t.ERROR,data:e.toString()})}f instanceof Uint8Array&&p.push(f.buffer),self.postMessage({id:E,type:c,data:f},p)}})(),{}})()));
|
||||||
|
//# sourceMappingURL=814.ffmpeg.js.map
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,2 @@
|
|||||||
|
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{"use strict";var e={m:{},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},u:e=>e+".ffmpeg.js"};e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var s=e.g.document;if(!t&&s&&(s.currentScript&&(t=s.currentScript.src),!t)){var r=s.getElementsByTagName("script");if(r.length)for(var a=r.length-1;a>-1&&!t;)t=r[a--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.b=document.baseURI||self.location.href;var t,s={};e.r(s),e.d(s,{FFFSType:()=>n,FFmpeg:()=>i}),function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const r=(()=>{let e=0;return()=>e++})(),a=(new Error("unknown message type"),new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first")),o=new Error("called FFmpeg.terminate()");new Error("failed to import ffmpeg-core.js");class i{#e=null;#t={};#s={};#r=[];#a=[];loaded=!1;#o=()=>{this.#e&&(this.#e.onmessage=({data:{id:e,type:s,data:r}})=>{switch(s){case t.LOAD:this.loaded=!0,this.#t[e](r);break;case t.MOUNT:case t.UNMOUNT:case t.EXEC:case t.FFPROBE:case t.WRITE_FILE:case t.READ_FILE:case t.DELETE_FILE:case t.RENAME:case t.CREATE_DIR:case t.LIST_DIR:case t.DELETE_DIR:this.#t[e](r);break;case t.LOG:this.#r.forEach((e=>e(r)));break;case t.PROGRESS:this.#a.forEach((e=>e(r)));break;case t.ERROR:this.#s[e](r)}delete this.#t[e],delete this.#s[e]})};#i=({type:e,data:t},s=[],o)=>this.#e?new Promise(((a,i)=>{const n=r();this.#e&&this.#e.postMessage({id:n,type:e,data:t},s),this.#t[n]=a,this.#s[n]=i,o?.addEventListener("abort",(()=>{i(new DOMException(`Message # ${n} was aborted`,"AbortError"))}),{once:!0})})):Promise.reject(a);on(e,t){"log"===e?this.#r.push(t):"progress"===e&&this.#a.push(t)}off(e,t){"log"===e?this.#r=this.#r.filter((e=>e!==t)):"progress"===e&&(this.#a=this.#a.filter((e=>e!==t)))}load=({classWorkerURL:s,...r}={},{signal:a}={})=>(this.#e||(this.#e=s?new Worker(new URL(s,"file:///Users/focus/Projects/ffmpeg.wasm/packages/ffmpeg/dist/esm/classes.js"),{type:"module"}):new Worker(new URL(e.p+e.u(814),e.b),{type:void 0}),this.#o()),this.#i({type:t.LOAD,data:r},void 0,a));exec=(e,s=-1,{signal:r}={})=>this.#i({type:t.EXEC,data:{args:e,timeout:s}},void 0,r);ffprobe=(e,s=-1,{signal:r}={})=>this.#i({type:t.FFPROBE,data:{args:e,timeout:s}},void 0,r);terminate=()=>{const e=Object.keys(this.#s);for(const t of e)this.#s[t](o),delete this.#s[t],delete this.#t[t];this.#e&&(this.#e.terminate(),this.#e=null,this.loaded=!1)};writeFile=(e,s,{signal:r}={})=>{const a=[];return s instanceof Uint8Array&&a.push(s.buffer),this.#i({type:t.WRITE_FILE,data:{path:e,data:s}},a,r)};mount=(e,s,r)=>this.#i({type:t.MOUNT,data:{fsType:e,options:s,mountPoint:r}},[]);unmount=e=>this.#i({type:t.UNMOUNT,data:{mountPoint:e}},[]);readFile=(e,s="binary",{signal:r}={})=>this.#i({type:t.READ_FILE,data:{path:e,encoding:s}},void 0,r);deleteFile=(e,{signal:s}={})=>this.#i({type:t.DELETE_FILE,data:{path:e}},void 0,s);rename=(e,s,{signal:r}={})=>this.#i({type:t.RENAME,data:{oldPath:e,newPath:s}},void 0,r);createDir=(e,{signal:s}={})=>this.#i({type:t.CREATE_DIR,data:{path:e}},void 0,s);listDir=(e,{signal:s}={})=>this.#i({type:t.LIST_DIR,data:{path:e}},void 0,s);deleteDir=(e,{signal:s}={})=>this.#i({type:t.DELETE_DIR,data:{path:e}},void 0,s)}var n;return function(e){e.MEMFS="MEMFS",e.NODEFS="NODEFS",e.NODERAWFS="NODERAWFS",e.IDBFS="IDBFS",e.WORKERFS="WORKERFS",e.PROXYFS="PROXYFS"}(n||(n={})),s})()));
|
||||||
|
//# sourceMappingURL=ffmpeg.js.map
|
||||||
File diff suppressed because one or more lines are too long
Generated
+18
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "ffmpeg-wasm",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@ffmpeg/core-wasm": "^0.7.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ffmpeg/core-wasm": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg/core-wasm/-/core-wasm-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-TZOsaGPFC4DxGwWenW+an1c08WC70xJoZitbzEBg7YYeLwK+qeCRsvgc9gMHBmYViP9AqMm35wOAiOnwtdnQCw==",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@ffmpeg/core-wasm": "^0.7.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FFmpegWASM=t():e.FFmpegWASM=t()}(self,(()=>(()=>{"use strict";var e={m:{},d:(t,s)=>{for(var r in s)e.o(s,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:s[r]})},u:e=>e+".ffmpeg.js"};e.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),e.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),e.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{var t;e.g.importScripts&&(t=e.g.location+"");var s=e.g.document;if(!t&&s&&(s.currentScript&&(t=s.currentScript.src),!t)){var r=s.getElementsByTagName("script");if(r.length)for(var a=r.length-1;a>-1&&!t;)t=r[a--].src}if(!t)throw new Error("Automatic publicPath is not supported in this browser");t=t.replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),e.p=t})(),e.b=document.baseURI||self.location.href;var t,s={};e.r(s),e.d(s,{FFFSType:()=>n,FFmpeg:()=>i}),function(e){e.LOAD="LOAD",e.EXEC="EXEC",e.FFPROBE="FFPROBE",e.WRITE_FILE="WRITE_FILE",e.READ_FILE="READ_FILE",e.DELETE_FILE="DELETE_FILE",e.RENAME="RENAME",e.CREATE_DIR="CREATE_DIR",e.LIST_DIR="LIST_DIR",e.DELETE_DIR="DELETE_DIR",e.ERROR="ERROR",e.DOWNLOAD="DOWNLOAD",e.PROGRESS="PROGRESS",e.LOG="LOG",e.MOUNT="MOUNT",e.UNMOUNT="UNMOUNT"}(t||(t={}));const r=(()=>{let e=0;return()=>e++})(),a=(new Error("unknown message type"),new Error("ffmpeg is not loaded, call `await ffmpeg.load()` first")),o=new Error("called FFmpeg.terminate()");new Error("failed to import ffmpeg-core.js");class i{#e=null;#t={};#s={};#r=[];#a=[];loaded=!1;#o=()=>{this.#e&&(this.#e.onmessage=({data:{id:e,type:s,data:r}})=>{switch(s){case t.LOAD:this.loaded=!0,this.#t[e](r);break;case t.MOUNT:case t.UNMOUNT:case t.EXEC:case t.FFPROBE:case t.WRITE_FILE:case t.READ_FILE:case t.DELETE_FILE:case t.RENAME:case t.CREATE_DIR:case t.LIST_DIR:case t.DELETE_DIR:this.#t[e](r);break;case t.LOG:this.#r.forEach((e=>e(r)));break;case t.PROGRESS:this.#a.forEach((e=>e(r)));break;case t.ERROR:this.#s[e](r)}delete this.#t[e],delete this.#s[e]})};#i=({type:e,data:t},s=[],o)=>this.#e?new Promise(((a,i)=>{const n=r();this.#e&&this.#e.postMessage({id:n,type:e,data:t},s),this.#t[n]=a,this.#s[n]=i,o?.addEventListener("abort",(()=>{i(new DOMException(`Message # ${n} was aborted`,"AbortError"))}),{once:!0})})):Promise.reject(a);on(e,t){"log"===e?this.#r.push(t):"progress"===e&&this.#a.push(t)}off(e,t){"log"===e?this.#r=this.#r.filter((e=>e!==t)):"progress"===e&&(this.#a=this.#a.filter((e=>e!==t)))}load=({classWorkerURL:s,...r}={},{signal:a}={})=>(this.#e||(this.#e=s?new Worker(new URL(s,"file:///Users/focus/Projects/ffmpeg.wasm/packages/ffmpeg/dist/esm/classes.js"),{type:"module"}):new Worker(new URL(e.p+e.u(814),e.b),{type:void 0}),this.#o()),this.#i({type:t.LOAD,data:r},void 0,a));exec=(e,s=-1,{signal:r}={})=>this.#i({type:t.EXEC,data:{args:e,timeout:s}},void 0,r);ffprobe=(e,s=-1,{signal:r}={})=>this.#i({type:t.FFPROBE,data:{args:e,timeout:s}},void 0,r);terminate=()=>{const e=Object.keys(this.#s);for(const t of e)this.#s[t](o),delete this.#s[t],delete this.#t[t];this.#e&&(this.#e.terminate(),this.#e=null,this.loaded=!1)};writeFile=(e,s,{signal:r}={})=>{const a=[];return s instanceof Uint8Array&&a.push(s.buffer),this.#i({type:t.WRITE_FILE,data:{path:e,data:s}},a,r)};mount=(e,s,r)=>this.#i({type:t.MOUNT,data:{fsType:e,options:s,mountPoint:r}},[]);unmount=e=>this.#i({type:t.UNMOUNT,data:{mountPoint:e}},[]);readFile=(e,s="binary",{signal:r}={})=>this.#i({type:t.READ_FILE,data:{path:e,encoding:s}},void 0,r);deleteFile=(e,{signal:s}={})=>this.#i({type:t.DELETE_FILE,data:{path:e}},void 0,s);rename=(e,s,{signal:r}={})=>this.#i({type:t.RENAME,data:{oldPath:e,newPath:s}},void 0,r);createDir=(e,{signal:s}={})=>this.#i({type:t.CREATE_DIR,data:{path:e}},void 0,s);listDir=(e,{signal:s}={})=>this.#i({type:t.LIST_DIR,data:{path:e}},void 0,s);deleteDir=(e,{signal:s}={})=>this.#i({type:t.DELETE_DIR,data:{path:e}},void 0,s)}var n;return function(e){e.MEMFS="MEMFS",e.NODEFS="NODEFS",e.NODERAWFS="NODERAWFS",e.IDBFS="IDBFS",e.WORKERFS="WORKERFS",e.PROXYFS="PROXYFS"}(n||(n={})),s})()));
|
||||||
|
//# sourceMappingURL=ffmpeg.js.map
|
||||||
File diff suppressed because one or more lines are too long
Generated
+33
@@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"name": "ffmpeg",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@ffmpeg/ffmpeg": "^0.12.15"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ffmpeg/ffmpeg": {
|
||||||
|
"version": "0.12.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.15.tgz",
|
||||||
|
"integrity": "sha512-1C8Obr4GsN3xw+/1Ww6PFM84wSQAGsdoTuTWPOj2OizsRDLT4CXTaVjPhkw6ARyDus1B9X/L2LiXHqYYsGnRFw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@ffmpeg/types": "^0.12.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.x"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@ffmpeg/types": {
|
||||||
|
"version": "0.12.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@ffmpeg/types/-/types-0.12.4.tgz",
|
||||||
|
"integrity": "sha512-k9vJQNBGTxE5AhYDtOYR5rO5fKsspbg51gbcwtbkw2lCdoIILzklulcjJfIDwrtn7XhDeF2M+THwJ2FGrLeV6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.x"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@ffmpeg/ffmpeg": "^0.12.15"
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Media to Text</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: system-ui; max-width: 800px; margin: 50px auto; padding: 20px; }
|
||||||
|
button { padding: 10px 20px; margin: 10px 5px; cursor: pointer; }
|
||||||
|
#output { white-space: pre-wrap; border: 1px solid #ccc; padding: 15px; margin: 20px 0; min-height: 100px; }
|
||||||
|
#status { color: #666; margin: 10px 0; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Media to Text Converter</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<select id="language">
|
||||||
|
<option value="en-US">English</option>
|
||||||
|
<option value="es-ES">Spanish</option>
|
||||||
|
<option value="fr-FR">French</option>
|
||||||
|
<option value="de-DE">German</option>
|
||||||
|
<option value="it-IT">Italian</option>
|
||||||
|
<option value="pt-BR">Portuguese</option>
|
||||||
|
<option value="ru-RU">Russian</option>
|
||||||
|
<option value="ja-JP">Japanese</option>
|
||||||
|
<option value="ko-KR">Korean</option>
|
||||||
|
<option value="zh-CN">Chinese (Simplified)</option>
|
||||||
|
<option value="zh-TW">Chinese (Traditional)</option>
|
||||||
|
<option value="ar-SA">Arabic</option>
|
||||||
|
<option value="hi-IN">Hindi</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0; padding: 20px; border: 2px dashed #ccc; border-radius: 8px;">
|
||||||
|
<h3>Upload Audio File</h3>
|
||||||
|
<p style="color: #666; font-size: 14px;">Upload audio file for transcription (processed in browser)</p>
|
||||||
|
<input type="file" id="file-input" accept="audio/*">
|
||||||
|
<button id="upload-btn" disabled>Convert to Text</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin: 20px 0; padding: 20px; border: 2px solid #ccc; border-radius: 8px;">
|
||||||
|
<h3>Live Recording</h3>
|
||||||
|
<button id="record-btn">Start Recording</button>
|
||||||
|
<button id="stop-btn" disabled>Stop Recording</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
<div id="output"></div>
|
||||||
|
<button id="copy-btn" style="display:none">Copy Text</button>
|
||||||
|
|
||||||
|
<script type="module" src="app.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
flask
|
||||||
|
flask-cors
|
||||||
|
openai-whisper
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from flask import Flask, request, jsonify, send_from_directory
|
||||||
|
from flask_cors import CORS
|
||||||
|
import whisper
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
app = Flask(__name__, static_folder='.', static_url_path='')
|
||||||
|
CORS(app)
|
||||||
|
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB max (audio only)
|
||||||
|
|
||||||
|
@app.after_request
|
||||||
|
def set_headers(response):
|
||||||
|
response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
|
||||||
|
response.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'
|
||||||
|
return response
|
||||||
|
|
||||||
|
print("Loading Whisper model...")
|
||||||
|
model = whisper.load_model("base")
|
||||||
|
print("Model loaded!")
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
response = send_from_directory('.', 'index.html')
|
||||||
|
response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
|
||||||
|
response.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route('/<path:filename>')
|
||||||
|
def static_files(filename):
|
||||||
|
response = send_from_directory('.', filename)
|
||||||
|
response.headers['Cross-Origin-Opener-Policy'] = 'same-origin'
|
||||||
|
response.headers['Cross-Origin-Embedder-Policy'] = 'require-corp'
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route('/transcribe', methods=['POST'])
|
||||||
|
def transcribe():
|
||||||
|
try:
|
||||||
|
if 'file' not in request.files:
|
||||||
|
return jsonify({'error': 'No file provided'}), 400
|
||||||
|
|
||||||
|
file = request.files['file']
|
||||||
|
language = request.form.get('language', None)
|
||||||
|
|
||||||
|
if language:
|
||||||
|
language = language.split('-')[0].lower()
|
||||||
|
|
||||||
|
if file.filename == '':
|
||||||
|
return jsonify({'error': 'No file selected'}), 400
|
||||||
|
|
||||||
|
# Enforce audio-only uploads (per DEVELOPMENT_RULES.md)
|
||||||
|
ext = os.path.splitext(file.filename)[1].lower()
|
||||||
|
video_exts = {'.mp4', '.avi', '.mov', '.mkv', '.flv', '.wmv', '.webm'}
|
||||||
|
if ext in video_exts:
|
||||||
|
return jsonify({'error': 'Video files not allowed. Extract audio first.'}), 400
|
||||||
|
|
||||||
|
with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.filename)[1]) as tmp:
|
||||||
|
filepath = tmp.name
|
||||||
|
file.save(filepath)
|
||||||
|
|
||||||
|
result = model.transcribe(filepath, language=language)
|
||||||
|
os.remove(filepath)
|
||||||
|
|
||||||
|
return jsonify({'text': result['text']})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run(debug=False, port=5000, host='127.0.0.1')
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import sys
|
||||||
|
import whisper
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python transcribe.py <audio_or_video_file> [language]")
|
||||||
|
print("Example: python transcribe.py audio.mp3 es")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
model = whisper.load_model("base")
|
||||||
|
language = sys.argv[2] if len(sys.argv) > 2 else None
|
||||||
|
result = model.transcribe(sys.argv[1], language=language)
|
||||||
|
print(result["text"])
|
||||||
Reference in New Issue
Block a user