Merge branch 'master' of https://github.com/x4base/fast-srt-subtitle into x4base-master
This commit is contained in:
145
index.html
145
index.html
@@ -1,108 +1,47 @@
|
||||
<html>
|
||||
<title>Fast and Dirty Captioner</title>
|
||||
<title>Fast and Dirty Captioner</title>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<script src="p5.min.js"></script>
|
||||
<script src="p5.sound.min.js"></script>
|
||||
<script src="sketch.js"></script>
|
||||
<script>
|
||||
(function(a, b) {
|
||||
if ("function" == typeof define && define.amd) define([], b);
|
||||
else if ("undefined" != typeof exports) b();
|
||||
else {
|
||||
b(), a.FileSaver = {
|
||||
exports: {}
|
||||
}.exports
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
||||
<style>
|
||||
.video {
|
||||
width: 960px;
|
||||
}
|
||||
})(this, function() {
|
||||
"use strict";
|
||||
|
||||
function b(a, b) {
|
||||
return "undefined" == typeof b ? b = {
|
||||
autoBom: !1
|
||||
} : "object" != typeof b && (console.warn("Deprecated: Expected third argument to be a object"), b = {
|
||||
autoBom: !b
|
||||
}), b.autoBom && /^\s*(?:text\/\S*|application\/xml|\S*\/\S*\+xml)\s*;.*charset\s*=\s*utf-8/i.test(a.type) ? new Blob(["\uFEFF", a], {
|
||||
type: a.type
|
||||
}) : a
|
||||
}
|
||||
|
||||
function c(b, c, d) {
|
||||
var e = new XMLHttpRequest;
|
||||
e.open("GET", b), e.responseType = "blob", e.onload = function() {
|
||||
a(e.response, c, d)
|
||||
}, e.onerror = function() {
|
||||
console.error("could not download file")
|
||||
}, e.send()
|
||||
}
|
||||
|
||||
function d(a) {
|
||||
var b = new XMLHttpRequest;
|
||||
b.open("HEAD", a, !1);
|
||||
try {
|
||||
b.send()
|
||||
} catch (a) {}
|
||||
return 200 <= b.status && 299 >= b.status
|
||||
}
|
||||
|
||||
function e(a) {
|
||||
try {
|
||||
a.dispatchEvent(new MouseEvent("click"))
|
||||
} catch (c) {
|
||||
var b = document.createEvent("MouseEvents");
|
||||
b.initMouseEvent("click", !0, !0, window, 0, 0, 0, 80, 20, !1, !1, !1, !1, 0, null), a.dispatchEvent(b)
|
||||
}
|
||||
}
|
||||
var f = "object" == typeof window && window.window === window ? window : "object" == typeof self && self.self === self ? self : "object" == typeof global && global.global === global ? global : void 0,
|
||||
a = f.saveAs || ("object" != typeof window || window !== f ? function() {} : "download" in HTMLAnchorElement.prototype ? function(b, g, h) {
|
||||
var i = f.URL || f.webkitURL,
|
||||
j = document.createElement("a");
|
||||
g = g || b.name || "download", j.download = g, j.rel = "noopener", "string" == typeof b ? (j.href = b, j.origin === location.origin ? e(j) : d(j.href) ? c(b, g, h) : e(j, j.target = "_blank")) : (j.href = i.createObjectURL(b),
|
||||
setTimeout(function() {
|
||||
i.revokeObjectURL(j.href)
|
||||
}, 4E4), setTimeout(function() {
|
||||
e(j)
|
||||
}, 0))
|
||||
} : "msSaveOrOpenBlob" in navigator ? function(f, g, h) {
|
||||
if (g = g || f.name || "download", "string" != typeof f) navigator.msSaveOrOpenBlob(b(f, h), g);
|
||||
else if (d(f)) c(f, g, h);
|
||||
else {
|
||||
var i = document.createElement("a");
|
||||
i.href = f, i.target = "_blank", setTimeout(function() {
|
||||
e(i)
|
||||
})
|
||||
}
|
||||
} : function(a, b, d, e) {
|
||||
if (e = e || open("", "_blank"), e && (e.document.title = e.document.body.innerText = "downloading..."), "string" == typeof a) return c(a, b, d);
|
||||
var g = "application/octet-stream" === a.type,
|
||||
h = /constructor/i.test(f.HTMLElement) || f.safari,
|
||||
i = /CriOS\/[\d]+/.test(navigator.userAgent);
|
||||
if ((i || g && h) && "undefined" != typeof FileReader) {
|
||||
var j = new FileReader;
|
||||
j.onloadend = function() {
|
||||
var a = j.result;
|
||||
a = i ? a : a.replace(/^data:[^;]*;/, "data:attachment/file;"), e ? e.location.href = a : location = a, e = null
|
||||
}, j.readAsDataURL(a)
|
||||
} else {
|
||||
var k = f.URL || f.webkitURL,
|
||||
l = k.createObjectURL(a);
|
||||
e ? e.location = l : location.href = l, e = null, setTimeout(function() {
|
||||
k.revokeObjectURL(l)
|
||||
}, 4E4)
|
||||
}
|
||||
});
|
||||
f.saveAs = a.saveAs = a, "undefined" != typeof module && (module.exports = a)
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>用 Node 開 HTTP Server,影片檔名取 video.mp4,字幕文字檔 subs.txt(每句分行),放在同一資料夾。</p>
|
||||
<p>K: 下一行開始 | L: 這一行提前結束 | I: 前捲一行 | O: 後捲一行 | U: 倒帶 3 秒 | P: 前進 3 秒 | Q: 製作 SRT 檔</p>
|
||||
<p>K: Next Line | L: This Line Ends Early | I: Scroll Back | O: Scroll Forward | U: Rewind | P: Forward | Q: Make SRT File</p>
|
||||
<p id="status">Test Text.</p>
|
||||
<textarea id="textArea" rows="10" cols="80">預設的字。</textarea>
|
||||
</body>
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<p>選取字幕檔以及影片檔案開始製作 SRT!</p>
|
||||
<label>
|
||||
<span>選擇字幕檔案:</span>
|
||||
<input
|
||||
id="srtFile"
|
||||
type="file"
|
||||
name="srtFile"
|
||||
placeholder="點擊上傳字幕檔"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<span>選擇影片檔案:</span>
|
||||
<input
|
||||
id="videoFile"
|
||||
type="file"
|
||||
name="videoFile"
|
||||
placeholder="點擊上傳影片檔"
|
||||
/>
|
||||
</label>
|
||||
<p>
|
||||
K: 下一行開始 | L: 這一行提前結束 | I: 前捲一行 | O: 後捲一行 | U: 倒帶 3
|
||||
秒 | P: 前進 3 秒 | Q: 製作 SRT 檔
|
||||
</p>
|
||||
<p>
|
||||
K: Next Line | L: This Line Ends Early | I: Scroll Back | O: Scroll
|
||||
Forward | U: Rewind | P: Forward | Q: Make SRT File
|
||||
</p>
|
||||
<p id="status">Test Text.</p>
|
||||
<textarea id="textArea" rows="10" cols="80">預設的字。</textarea>
|
||||
<video class="video" id="video" controls></video>
|
||||
</body>
|
||||
<script src="main.js"></script>
|
||||
</html>
|
||||
|
151
main.js
Normal file
151
main.js
Normal file
@@ -0,0 +1,151 @@
|
||||
const SRT_ID = 'srtFile';
|
||||
const VIDEO_ID = 'videoFile';
|
||||
|
||||
const srtInput = document.querySelector('#srtFile');
|
||||
const videoInput = document.querySelector('#videoFile');
|
||||
const video = document.querySelector('#video');
|
||||
const textArea = document.querySelector('#textArea');
|
||||
const status = document.querySelector('#status');
|
||||
const reactTime = 0.4;
|
||||
let subTexts = [];
|
||||
let currentStamping = 0;
|
||||
let lines = [];
|
||||
|
||||
function clamp(num) {
|
||||
return Math.max(num, 0);
|
||||
}
|
||||
|
||||
const keyMap = {
|
||||
'k': video => {
|
||||
if (currentStamping >= lines.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
lines[currentStamping + 1][0] = clamp(video.currentTime - reactTime);
|
||||
lines[currentStamping][1] =
|
||||
lines[currentStamping][1] > video.currentTime - reactTime ||
|
||||
lines[currentStamping][1] === null
|
||||
? clamp(video.currentTime - 0.03 - reactTime)
|
||||
: null;
|
||||
currentStamping += 1;
|
||||
},
|
||||
'l': video => {
|
||||
lines[currentStamping] = [
|
||||
lines[currentStamping][0],
|
||||
video.currentTime - reactTime
|
||||
];
|
||||
},
|
||||
'i': () => {
|
||||
currentStamping -= 1;
|
||||
},
|
||||
'o': () => {
|
||||
currentStamping += 1;
|
||||
},
|
||||
'u': () => (video.currentTime -= 3),
|
||||
'p': () => (video.currentTime += 3),
|
||||
'q': () => makeSRT()
|
||||
};
|
||||
|
||||
function getCurrentStatus() {
|
||||
return `Stamping Line ${currentStamping} | Playhead: ${video.currentTime}`;
|
||||
}
|
||||
|
||||
function execHotkey(keyMap) {
|
||||
document.addEventListener('keypress', function(e) {
|
||||
const execFn = keyMap[e.key.toLowerCase()];
|
||||
if (typeof execFn === 'function') {
|
||||
execFn(video);
|
||||
updateContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateContent() {
|
||||
const head = '** 目前 ---> ';
|
||||
|
||||
const content = subTexts
|
||||
.slice(currentStamping, currentStamping + 5)
|
||||
.map((text, i) => {
|
||||
const [timeStart, timeEnd] = lines[currentStamping + i];
|
||||
return `${i === 0 ? head : ''}${text} | ${timeStart} --> ${timeEnd}`;
|
||||
})
|
||||
.join('\n');
|
||||
|
||||
textArea.value = content;
|
||||
}
|
||||
|
||||
function handleFileUpload(e) {
|
||||
if (e.target.files !== null) {
|
||||
const reader = new FileReader();
|
||||
const file = e.target.files[0];
|
||||
|
||||
/*
|
||||
if it's srt file, fill text area with srt content
|
||||
if it's video, load it into video tag
|
||||
*/
|
||||
reader.onload = function() {
|
||||
if (e.target.id === SRT_ID) {
|
||||
subTexts = reader.result.split('\n');
|
||||
subTexts.forEach((_, i) => (lines[i] = [null, null]));
|
||||
lines[0][0] = 0;
|
||||
|
||||
updateContent();
|
||||
|
||||
execHotkey(keyMap);
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = function() {
|
||||
alert('無法讀取檔案!');
|
||||
};
|
||||
|
||||
if (e.target.id === SRT_ID) {
|
||||
reader.readAsText(file);
|
||||
} else {
|
||||
video.src = URL.createObjectURL(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
videoInput.addEventListener('change', handleFileUpload);
|
||||
srtInput.addEventListener('change', handleFileUpload);
|
||||
|
||||
video.addEventListener('timeupdate', function(e) {
|
||||
status.textContent = getCurrentStatus();
|
||||
});
|
||||
|
||||
function makeSRT() {
|
||||
srt = '';
|
||||
for (let i = 0; i < subTexts.length; i++) {
|
||||
// line number
|
||||
srt += i + 1 + '\n';
|
||||
// line time
|
||||
let sh, sm, ss, sms;
|
||||
let eh, em, es, ems;
|
||||
const [timeStart, timeEnd] = lines[i];
|
||||
const leftPad = str => `${str}`.padStart(2, '0');
|
||||
sh = leftPad(Math.floor(timeStart / 3600));
|
||||
sm = leftPad(Math.floor((timeStart % 3600) / 60));
|
||||
ss = leftPad(Math.floor(timeStart % 60));
|
||||
sms = leftPad(Math.floor((timeStart * 1000) % 1000));
|
||||
eh = leftPad(Math.floor(timeEnd / 3600));
|
||||
em = leftPad(Math.floor((timeEnd % 3600) / 60));
|
||||
es = leftPad(Math.floor(timeEnd % 60));
|
||||
ems = leftPad(Math.floor((timeEnd * 1000) % 1000));
|
||||
|
||||
srt += `${sh}:${sm}:${ss},${sms} --> ${eh}:${em}:${es},${ems}\n`;
|
||||
srt += subTexts[i];
|
||||
srt += '\n\n';
|
||||
}
|
||||
console.log(srt);
|
||||
let blob = new Blob([srt], {
|
||||
type: 'text/plain;charset=utf-8'
|
||||
});
|
||||
const a = document.createElement('a');
|
||||
const file = new Blob([srt], { type: 'text/plain;charset=utf-8' });
|
||||
a.href = URL.createObjectURL(file);
|
||||
a.download = 'srt.txt';
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
a.remove();
|
||||
}
|
28
p5.sound.min.js
vendored
28
p5.sound.min.js
vendored
File diff suppressed because one or more lines are too long
118
sketch.js
118
sketch.js
@@ -1,118 +0,0 @@
|
||||
let curr; // current time
|
||||
let sta; // status text
|
||||
let status; // = select("#status");
|
||||
let tAreaText; //
|
||||
let tArea;
|
||||
let vidFile = "./video.mp4";
|
||||
let subFile = "./subs.txt";
|
||||
let subText; // load from text file
|
||||
let lineStartTime = [];
|
||||
let lineEndTime = [];
|
||||
let reactTime = 0.4;
|
||||
|
||||
let currentStamping = 0;
|
||||
let srt = "";
|
||||
|
||||
function preload() {
|
||||
vid = createVideo(vidFile);
|
||||
vid.size(640, 320);
|
||||
subText = loadStrings(subFile);
|
||||
}
|
||||
|
||||
function setup() {
|
||||
noCanvas();
|
||||
status = select("#status");
|
||||
tArea = select("#textArea");
|
||||
|
||||
for (let i = 0; i < subText.length; i++) {
|
||||
lineStartTime[i] = null;
|
||||
lineEndTime[i] = null;
|
||||
|
||||
//init all timestamps to 0
|
||||
}
|
||||
lineStartTime[0] = 0;
|
||||
|
||||
}
|
||||
|
||||
function draw() {
|
||||
curr = vid.elt.currentTime;
|
||||
sta = `Stamping Line ${currentStamping} | Playhead: ${curr}`;
|
||||
tAreaText = "";
|
||||
for (let i = 0; i < 5; i++) {
|
||||
if (i == 0) {
|
||||
tAreaText += "** 目前 ---> "
|
||||
}
|
||||
tAreaText += `${subText[currentStamping+i]} | ${lineStartTime[currentStamping+i]} --> ${lineEndTime[currentStamping+i]}` + String.fromCharCode(13, 10);
|
||||
}
|
||||
status.html(sta);
|
||||
tArea.html(tAreaText);
|
||||
}
|
||||
|
||||
function keyPressed() {
|
||||
if (keyCode === 75) { // K
|
||||
// 按左鍵
|
||||
// set line start time to current time
|
||||
lineStartTime[currentStamping + 1] = vid.elt.currentTime - reactTime;
|
||||
if (lineStartTime[currentStamping + 1] < 0){
|
||||
lineStartTime[currentStamping + 1] = 0;
|
||||
}
|
||||
// set prev line's end time, if prev end time > currentTime;
|
||||
if (lineEndTime[currentStamping] > vid.elt.currentTime - reactTime || lineEndTime[currentStamping] == null) {
|
||||
lineEndTime[currentStamping] = vid.elt.currentTime - 0.05 - reactTime;
|
||||
if (lineEndTime[currentStamping] < 0){
|
||||
lineEndTime[currentStamping] = 0;
|
||||
}
|
||||
}
|
||||
currentStamping++;
|
||||
} else if (keyCode === 76) { // L
|
||||
lineEndTime[currentStamping] = vid.elt.currentTime - reactTime;
|
||||
} else if (keyCode === 73) { // I
|
||||
currentStamping--;
|
||||
} else if (keyCode === 79) { // O
|
||||
currentStamping++;
|
||||
} else if (keyCode === 81) {
|
||||
// Q : Make SRT
|
||||
makeSRT();
|
||||
} else if (keyCode === 85) { // U
|
||||
vid.elt.currentTime -= 2;
|
||||
|
||||
} else if (keyCode === 80) { // P
|
||||
vid.elt.currentTime += 2;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function pad(num, size) {
|
||||
var s = "000000000" + num;
|
||||
return s.substr(s.length-size);
|
||||
}
|
||||
|
||||
function makeSRT() {
|
||||
srt = "";
|
||||
for (let i = 0; i < subText.length; i++) {
|
||||
// line number
|
||||
srt += (i + 1) + "\n";
|
||||
// line time
|
||||
let sh, sm, ss, sms;
|
||||
let eh, em, es, ems;
|
||||
sh = floor(lineStartTime[i] / 3600);
|
||||
sm = floor((lineStartTime[i] % 3600) / 60);
|
||||
ss = floor(lineStartTime[i] % 60);
|
||||
sms = floor((lineStartTime[i] * 1000) % 1000);
|
||||
sms = pad(sms, 3);
|
||||
eh = floor(lineEndTime[i] / 3600);
|
||||
em = floor((lineEndTime[i] % 3600) / 60);
|
||||
es = floor(lineEndTime[i] % 60);
|
||||
ems = floor((lineEndTime[i] * 1000) % 1000);
|
||||
ems = pad(ems, 3);
|
||||
|
||||
srt += `${sh}:${sm}:${ss},${sms} --> ${eh}:${em}:${es},${ems}\n`
|
||||
srt += subText[i];
|
||||
srt += "\n\n"
|
||||
}
|
||||
console.log(srt);
|
||||
let blob = new Blob([srt], {
|
||||
type: "text/plain;charset=utf-8"
|
||||
});
|
||||
saveAs(blob, 'srt.txt');
|
||||
}
|
@@ -1 +0,0 @@
|
||||
http-server -p 1234 -o
|
Reference in New Issue
Block a user