add webapp
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
from dash import Dash, dcc, html, dash_table, Input, Output, State, callback
|
||||||
|
|
||||||
|
import base64
|
||||||
|
from autotranscript.app.qtfaststart import process
|
||||||
|
from autotranscript import AutoTranscribe
|
||||||
|
import io
|
||||||
|
import subprocess as sp
|
||||||
|
import numpy as np
|
||||||
|
from autotranscript.audio import SAMPLE_RATE
|
||||||
|
|
||||||
|
# Setup auto-transcript
|
||||||
|
autot = AutoTranscribe() # whisper_model="tiny", whisper_kwargs={"local" : False}
|
||||||
|
|
||||||
|
# Setup FFmpeg
|
||||||
|
PROBLEMATIC_FILE_TYPES : tuple = "mov","mp4","m4a","3gp","3g2","mj2"
|
||||||
|
|
||||||
|
|
||||||
|
# Setup Dash
|
||||||
|
external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
|
||||||
|
|
||||||
|
app = Dash(__name__, external_stylesheets=external_stylesheets)
|
||||||
|
|
||||||
|
app.layout = html.Div([
|
||||||
|
dcc.Upload(
|
||||||
|
id='upload-data',
|
||||||
|
children=html.Div([
|
||||||
|
'Drag and Drop or ',
|
||||||
|
html.A('Select Files')
|
||||||
|
]),
|
||||||
|
style={
|
||||||
|
'width': '100%',
|
||||||
|
'height': '60px',
|
||||||
|
'lineHeight': '60px',
|
||||||
|
'borderWidth': '1px',
|
||||||
|
'borderStyle': 'dashed',
|
||||||
|
'borderRadius': '5px',
|
||||||
|
'textAlign': 'center',
|
||||||
|
'margin': '10px'
|
||||||
|
},
|
||||||
|
# Allow multiple files to be uploaded
|
||||||
|
multiple=True
|
||||||
|
),
|
||||||
|
html.Div(id='output-data-upload'),
|
||||||
|
])
|
||||||
|
|
||||||
|
def parse_contents(contents, filename, date):
|
||||||
|
content_type, content_string = contents.split(',')
|
||||||
|
|
||||||
|
decoded = base64.b64decode(content_string)
|
||||||
|
file = io.BytesIO(decoded).read()
|
||||||
|
|
||||||
|
if filename.endswith(PROBLEMATIC_FILE_TYPES):
|
||||||
|
# mp4 and other files need to be processed with qtfaststart
|
||||||
|
# since theire metadata is at the end of the file
|
||||||
|
# and we need it at the beginning
|
||||||
|
file = process(file)
|
||||||
|
|
||||||
|
cmd = [
|
||||||
|
"ffmpeg",
|
||||||
|
"-nostdin",
|
||||||
|
"-threads", "0",
|
||||||
|
"-i",'pipe:',
|
||||||
|
"-f", "s16le",
|
||||||
|
'-hide_banner',
|
||||||
|
'-loglevel', 'error',
|
||||||
|
"-c", "copy",
|
||||||
|
"-vn",
|
||||||
|
"-ac", "1",
|
||||||
|
"-acodec", "pcm_s16le",
|
||||||
|
"-ar", str(SAMPLE_RATE),
|
||||||
|
"-"
|
||||||
|
]
|
||||||
|
|
||||||
|
proc = sp.Popen(cmd, stdout=sp.PIPE, stdin=sp.PIPE)
|
||||||
|
|
||||||
|
out = proc.communicate(input=file)[0]
|
||||||
|
out = np.frombuffer(out, np.int16).flatten().astype(np.float32) / 32768.0
|
||||||
|
out = np.array([out, SAMPLE_RATE])
|
||||||
|
|
||||||
|
transcript = str(autot.transcribe(out))
|
||||||
|
|
||||||
|
return html.Div([
|
||||||
|
html.H5(f"File Name: {filename} \n" \
|
||||||
|
"Transcript: \n"
|
||||||
|
),
|
||||||
|
html.P(transcript)
|
||||||
|
])
|
||||||
|
|
||||||
|
@callback(Output('output-data-upload', 'children'),
|
||||||
|
Input('upload-data', 'contents'),
|
||||||
|
State('upload-data', 'filename'),
|
||||||
|
State('upload-data', 'last_modified'))
|
||||||
|
def update_output(list_of_contents, list_of_names, list_of_dates):
|
||||||
|
if list_of_contents is not None:
|
||||||
|
children = [
|
||||||
|
parse_contents(c, n, d) for c, n, d in
|
||||||
|
zip(list_of_contents, list_of_names, list_of_dates)]
|
||||||
|
return children
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
app.run_server()
|
||||||
@@ -0,0 +1,319 @@
|
|||||||
|
"""
|
||||||
|
This file contains a modified version of qtfaststart by qtfaststart
|
||||||
|
https://github.com/danielgtaylor/qtfaststart/tree/master
|
||||||
|
|
||||||
|
All credit goes to the original author.
|
||||||
|
Copyright (C) 2008 - 2013 Daniel G. Taylor <dan@programmer-art.org>
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this
|
||||||
|
software and associated documentation files (the "Software"),
|
||||||
|
to deal in the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||||
|
Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies
|
||||||
|
or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
|
||||||
|
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||||
|
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
|
||||||
|
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
|
||||||
|
ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
||||||
|
IN THE SOFTWARE.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import collections
|
||||||
|
import io
|
||||||
|
|
||||||
|
# define error classes
|
||||||
|
class FastStartException(Exception):
|
||||||
|
"""
|
||||||
|
Raised when something bad happens during processing.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class FastStartSetupError(FastStartException):
|
||||||
|
"""
|
||||||
|
Rasised when asked to process a file that does not need processing
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class MalformedFileError(FastStartException):
|
||||||
|
"""
|
||||||
|
Raised when the input file is setup in an unexpected way
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
class UnsupportedFormatError(FastStartException):
|
||||||
|
"""
|
||||||
|
Raised when a movie file is recognized as a format not supported.
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
# define constants
|
||||||
|
CHUNK_SIZE = 8192
|
||||||
|
|
||||||
|
log = logging.getLogger("qtfaststart")
|
||||||
|
|
||||||
|
# Older versions of Python require this to be defined
|
||||||
|
if not hasattr(os, 'SEEK_CUR'):
|
||||||
|
os.SEEK_CUR = 1
|
||||||
|
|
||||||
|
Atom = collections.namedtuple('Atom', 'name position size')
|
||||||
|
|
||||||
|
def read_atom(datastream):
|
||||||
|
"""
|
||||||
|
Read an atom and return a tuple of (size, type) where size is the size
|
||||||
|
in bytes (including the 8 bytes already read) and type is a "fourcc"
|
||||||
|
like "ftyp" or "moov".
|
||||||
|
"""
|
||||||
|
size, type = struct.unpack(">L4s", datastream.read(8))
|
||||||
|
type = type.decode('ascii')
|
||||||
|
return size, type
|
||||||
|
|
||||||
|
|
||||||
|
def _read_atom_ex(datastream):
|
||||||
|
"""
|
||||||
|
Read an Atom from datastream
|
||||||
|
"""
|
||||||
|
pos = datastream.tell()
|
||||||
|
atom_size, atom_type = read_atom(datastream)
|
||||||
|
if atom_size == 1:
|
||||||
|
atom_size, = struct.unpack(">Q", datastream.read(8))
|
||||||
|
return Atom(atom_type, pos, atom_size)
|
||||||
|
|
||||||
|
|
||||||
|
def get_index(datastream):
|
||||||
|
"""
|
||||||
|
Return an index of top level atoms, their absolute byte-position in the
|
||||||
|
file and their size in a list:
|
||||||
|
|
||||||
|
index = [
|
||||||
|
("ftyp", 0, 24),
|
||||||
|
("moov", 25, 2658),
|
||||||
|
("free", 2683, 8),
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
The tuple elements will be in the order that they appear in the file.
|
||||||
|
"""
|
||||||
|
log.debug("Getting index of top level atoms...")
|
||||||
|
|
||||||
|
index = list(_read_atoms(datastream))
|
||||||
|
_ensure_valid_index(index)
|
||||||
|
|
||||||
|
return index
|
||||||
|
|
||||||
|
|
||||||
|
def _read_atoms(datastream):
|
||||||
|
"""
|
||||||
|
Read atoms until an error occurs
|
||||||
|
"""
|
||||||
|
while datastream:
|
||||||
|
try:
|
||||||
|
atom = _read_atom_ex(datastream)
|
||||||
|
log.debug("%s: %s" % (atom.name, atom.size))
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
|
yield atom
|
||||||
|
|
||||||
|
if atom.size == 0:
|
||||||
|
if atom.name == "mdat":
|
||||||
|
# Some files may end in mdat with no size set, which generally
|
||||||
|
# means to seek to the end of the file. We can just stop indexing
|
||||||
|
# as no more entries will be found!
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Weird, but just continue to try to find more atoms
|
||||||
|
continue
|
||||||
|
|
||||||
|
datastream.seek(atom.position + atom.size)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_valid_index(index):
|
||||||
|
"""
|
||||||
|
Ensure the minimum viable atoms are present in the index.
|
||||||
|
|
||||||
|
Raise FastStartException if not.
|
||||||
|
"""
|
||||||
|
top_level_atoms = set([item.name for item in index])
|
||||||
|
for key in ["moov", "mdat"]:
|
||||||
|
if key not in top_level_atoms:
|
||||||
|
log.error("%s atom not found, is this a valid MOV/MP4 file?" % key)
|
||||||
|
raise FastStartException()
|
||||||
|
|
||||||
|
|
||||||
|
def find_atoms(size, datastream):
|
||||||
|
"""
|
||||||
|
Compatibilty interface for _find_atoms_ex
|
||||||
|
"""
|
||||||
|
fake_parent = Atom('fake', datastream.tell()-8, size+8)
|
||||||
|
for atom in _find_atoms_ex(fake_parent, datastream):
|
||||||
|
yield atom.name
|
||||||
|
|
||||||
|
|
||||||
|
def _find_atoms_ex(parent_atom, datastream):
|
||||||
|
"""
|
||||||
|
Yield either "stco" or "co64" Atoms from datastream.
|
||||||
|
datastream will be 8 bytes into the stco or co64 atom when the value
|
||||||
|
is yielded.
|
||||||
|
|
||||||
|
It is assumed that datastream will be at the end of the atom after
|
||||||
|
the value has been yielded and processed.
|
||||||
|
|
||||||
|
parent_atom is the parent atom, a 'moov' or other ancestor of CO
|
||||||
|
atoms in the datastream.
|
||||||
|
"""
|
||||||
|
stop = parent_atom.position + parent_atom.size
|
||||||
|
|
||||||
|
while datastream.tell() < stop:
|
||||||
|
try:
|
||||||
|
atom = _read_atom_ex(datastream)
|
||||||
|
except:
|
||||||
|
log.exception("Error reading next atom!")
|
||||||
|
raise FastStartException()
|
||||||
|
|
||||||
|
if atom.name in ["trak", "mdia", "minf", "stbl"]:
|
||||||
|
# Known ancestor atom of stco or co64, search within it!
|
||||||
|
for res in _find_atoms_ex(atom, datastream):
|
||||||
|
yield res
|
||||||
|
elif atom.name in ["stco", "co64"]:
|
||||||
|
yield atom
|
||||||
|
else:
|
||||||
|
# Ignore this atom, seek to the end of it.
|
||||||
|
datastream.seek(atom.position + atom.size)
|
||||||
|
|
||||||
|
|
||||||
|
def process(infilename, limit=float('inf')):
|
||||||
|
"""
|
||||||
|
Convert a Quicktime/MP4 file for streaming by moving the metadata to
|
||||||
|
the front of the file. This method writes a new file.
|
||||||
|
|
||||||
|
If limit is set to something other than zero it will be used as the
|
||||||
|
number of bytes to write of the atoms following the moov atom. This
|
||||||
|
is very useful to create a small sample of a file with full headers,
|
||||||
|
which can then be used in bug reports and such.
|
||||||
|
"""
|
||||||
|
if isinstance(infilename, str):
|
||||||
|
datastream = open(infilename, "rb")
|
||||||
|
elif isinstance(infilename, bytes):
|
||||||
|
datastream = io.BytesIO(infilename)
|
||||||
|
else:
|
||||||
|
raise TypeError("infilename must be a filename, bytes or file-like object")
|
||||||
|
# Get the top level atom index
|
||||||
|
index = get_index(datastream)
|
||||||
|
|
||||||
|
mdat_pos = 999999
|
||||||
|
free_size = 0
|
||||||
|
|
||||||
|
# Make sure moov occurs AFTER mdat, otherwise no need to run!
|
||||||
|
for atom in index:
|
||||||
|
# The atoms are guaranteed to exist from get_index above!
|
||||||
|
if atom.name == "moov":
|
||||||
|
moov_atom = atom
|
||||||
|
moov_pos = atom.position
|
||||||
|
elif atom.name == "mdat":
|
||||||
|
mdat_pos = atom.position
|
||||||
|
elif atom.name == "free" and atom.position < mdat_pos:
|
||||||
|
# This free atom is before the mdat!
|
||||||
|
free_size += atom.size
|
||||||
|
log.info("Removing free atom at %d (%d bytes)" % (atom.position, atom.size))
|
||||||
|
elif atom.name == "\x00\x00\x00\x00" and atom.position < mdat_pos:
|
||||||
|
# This is some strange zero atom with incorrect size
|
||||||
|
free_size += 8
|
||||||
|
log.info("Removing strange zero atom at %s (8 bytes)" % atom.position)
|
||||||
|
|
||||||
|
# Offset to shift positions
|
||||||
|
offset = moov_atom.size - free_size
|
||||||
|
|
||||||
|
if moov_pos < mdat_pos:
|
||||||
|
# moov appears to be in the proper place, don't shift by moov size
|
||||||
|
offset -= moov_atom.size
|
||||||
|
if not free_size:
|
||||||
|
# No free atoms and moov is correct, we are done!
|
||||||
|
log.error("This file appears to already be setup for streaming!")
|
||||||
|
# Stupid hack to retrun the non-processed file:
|
||||||
|
if isinstance(infilename, str):
|
||||||
|
return open(infilename, "rb").read()
|
||||||
|
elif isinstance(infilename, bytes):
|
||||||
|
return io.BytesIO(infilename).read()
|
||||||
|
|
||||||
|
# Read and fix moov
|
||||||
|
moov = _patch_moov(datastream, moov_atom, offset)
|
||||||
|
|
||||||
|
log.info("Writing output...")
|
||||||
|
outfile = b''
|
||||||
|
|
||||||
|
# Write ftype
|
||||||
|
for atom in index:
|
||||||
|
if atom.name == "ftyp":
|
||||||
|
log.debug("Writing ftyp... (%d bytes)" % atom.size)
|
||||||
|
datastream.seek(atom.position)
|
||||||
|
outfile += datastream.read(atom.size)
|
||||||
|
|
||||||
|
# Write moov
|
||||||
|
_bytes = moov.getvalue()
|
||||||
|
log.debug("Writing moov... (%d bytes)" % len(_bytes))
|
||||||
|
outfile += _bytes
|
||||||
|
|
||||||
|
# Write the rest
|
||||||
|
atoms = [item for item in index if item.name not in ["ftyp", "moov", "free"]]
|
||||||
|
for atom in atoms:
|
||||||
|
log.debug("Writing %s... (%d bytes)" % (atom.name, atom.size))
|
||||||
|
datastream.seek(atom.position)
|
||||||
|
|
||||||
|
# for compatability, allow '0' to mean no limit
|
||||||
|
cur_limit = limit or float('inf')
|
||||||
|
cur_limit = min(cur_limit, atom.size)
|
||||||
|
|
||||||
|
for chunk in get_chunks(datastream, CHUNK_SIZE, cur_limit):
|
||||||
|
outfile += chunk
|
||||||
|
|
||||||
|
return outfile
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_moov(datastream, atom, offset):
|
||||||
|
datastream.seek(atom.position)
|
||||||
|
moov = io.BytesIO(datastream.read(atom.size))
|
||||||
|
|
||||||
|
# reload the atom from the fixed stream
|
||||||
|
atom = _read_atom_ex(moov)
|
||||||
|
|
||||||
|
for atom in _find_atoms_ex(atom, moov):
|
||||||
|
# Read either 32-bit or 64-bit offsets
|
||||||
|
ctype, csize = dict(
|
||||||
|
stco=('L', 4),
|
||||||
|
co64=('Q', 8),
|
||||||
|
)[atom.name]
|
||||||
|
|
||||||
|
# Get number of entries
|
||||||
|
version, entry_count = struct.unpack(">2L", moov.read(8))
|
||||||
|
|
||||||
|
log.info("Patching %s with %d entries" % (atom.name, entry_count))
|
||||||
|
|
||||||
|
entries_pos = moov.tell()
|
||||||
|
|
||||||
|
struct_fmt = ">%(entry_count)s%(ctype)s" % vars()
|
||||||
|
|
||||||
|
# Read entries
|
||||||
|
entries = struct.unpack(struct_fmt, moov.read(csize * entry_count))
|
||||||
|
|
||||||
|
# Patch and write entries
|
||||||
|
offset_entries = [entry + offset for entry in entries]
|
||||||
|
moov.seek(entries_pos)
|
||||||
|
moov.write(struct.pack(struct_fmt, *offset_entries))
|
||||||
|
return moov
|
||||||
|
|
||||||
|
def get_chunks(stream, chunk_size, limit):
|
||||||
|
remaining = limit
|
||||||
|
while remaining:
|
||||||
|
chunk = stream.read(min(remaining, chunk_size))
|
||||||
|
if not chunk:
|
||||||
|
return
|
||||||
|
remaining -= len(chunk)
|
||||||
|
yield chunk
|
||||||
Reference in New Issue
Block a user