diff --git a/subsync/cache.py b/subsync/cache.py
new file mode 100644
index 0000000..27308dc
--- /dev/null
+++ b/subsync/cache.py
@@ -0,0 +1,26 @@
+from subsync.settings import settings
+
+
+class WordsCache(object):
+ def __init__(self):
+ self.init(None)
+
+ def mkId(self, stream):
+ if stream:
+ return (stream.path, stream.no, settings().minWordProb, settings().minWordLen)
+
+ def init(self, id):
+ self.id = self.mkId(id)
+ self.data = []
+ self.progress = []
+
+ def isValid(self, id):
+ return self.mkId(id) == self.id
+
+ def isEmpty(self):
+ return not (self.id or self.data or self.progress)
+
+ def clear(self):
+ self.id = None
+ self.data = []
+ self.progress = None
diff --git a/subsync/gui/layout/settingswin.fbp b/subsync/gui/layout/settingswin.fbp
index e4bdfcf..bc9a394 100644
--- a/subsync/gui/layout/settingswin.fbp
+++ b/subsync/gui/layout/settingswin.fbp
@@ -1138,6 +1138,275 @@
+
+
+ 5
+ wxALL|wxALIGN_CENTER_VERTICAL|wxEXPAND
+ 0
+
+ 1
+ 1
+ 1
+ 1
+
+
+
+
+
+
+
+ 1
+ 0
+ 1
+ 1
+
+ 1
+ 0
+ Dock
+ 0
+ Left
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ use RAM cache for references
+
+ 0
+
+
+ 0
+
+ 1
+ m_refsCache
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 5
+ wxEXPAND
+ 1
+
+ 0
+ protected
+ 0
+
+
+
+ 5
+ wxALL|wxALIGN_CENTER_VERTICAL
+ 0
+
+ 1
+ 1
+ 1
+ 1
+
+
+
+
+
+
+
+ 1
+ 0
+ 1
+
+ 1
+ 0
+ 0
+ Dock
+ 0
+ Left
+ 1
+
+ 1
+
+ 0
+ 0
+ wxID_ANY
+ Clear cache
+
+ 0
+
+
+ 0
+
+ 1
+ m_buttonClearCache
+ 1
+
+
+ protected
+ 1
+
+ Resizable
+ 1
+
+
+ ; forward_declare
+ 0
+
+
+ wxFILTER_NONE
+ wxDefaultValidator
+
+
+
+
+ onButtonClearCache
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subsync/gui/layout/settingswin.py b/subsync/gui/layout/settingswin.py
index e6ad476..950813e 100644
--- a/subsync/gui/layout/settingswin.py
+++ b/subsync/gui/layout/settingswin.py
@@ -81,6 +81,20 @@ def __init__( self, parent ):
self.m_askForUpdate.SetValue(True)
fgSizer4.Add( self.m_askForUpdate, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL|wx.EXPAND, 5 )
+ self.staticText5 = wx.StaticText( self.m_panelGeneral, wx.ID_ANY, _(u"Cache:"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.staticText5.Wrap( -1 )
+ fgSizer4.Add( self.staticText5, 0, wx.ALL|wx.ALIGN_RIGHT|wx.ALIGN_CENTER_VERTICAL, 5 )
+
+ self.m_refsCache = wx.CheckBox( self.m_panelGeneral, wx.ID_ANY, _(u"use RAM cache for references"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ self.m_refsCache.SetValue(True)
+ fgSizer4.Add( self.m_refsCache, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL|wx.EXPAND, 5 )
+
+
+ fgSizer4.Add( ( 0, 0), 1, wx.EXPAND, 5 )
+
+ self.m_buttonClearCache = wx.Button( self.m_panelGeneral, wx.ID_ANY, _(u"Clear cache"), wx.DefaultPosition, wx.DefaultSize, 0 )
+ fgSizer4.Add( self.m_buttonClearCache, 0, wx.ALL|wx.ALIGN_CENTER_VERTICAL, 5 )
+
self.m_panelGeneral.SetSizer( fgSizer4 )
self.m_panelGeneral.Layout()
@@ -256,6 +270,7 @@ def __init__( self, parent ):
self.Centre( wx.BOTH )
# Connect Events
+ self.m_buttonClearCache.Bind( wx.EVT_BUTTON, self.onButtonClearCache )
self.m_checkAutoJobsNo.Bind( wx.EVT_CHECKBOX, self.onCheckAutoJobsNoCheck )
self.m_checkLogToFile.Bind( wx.EVT_CHECKBOX, self.onCheckLogToFileCheck )
self.m_buttonLogFileSelect.Bind( wx.EVT_BUTTON, self.onButtonLogFileSelectClick )
@@ -266,6 +281,9 @@ def __del__( self ):
# Virtual event handlers, overide them in your derived class
+ def onButtonClearCache( self, event ):
+ event.Skip()
+
def onCheckAutoJobsNoCheck( self, event ):
event.Skip()
diff --git a/subsync/gui/mainwin.py b/subsync/gui/mainwin.py
index 66f2e05..68b0756 100644
--- a/subsync/gui/mainwin.py
+++ b/subsync/gui/mainwin.py
@@ -6,6 +6,7 @@
from subsync.gui.aboutwin import AboutWin
from subsync.gui.errorwin import error_dlg
from subsync import assets
+from subsync import cache
from subsync import img
from subsync import thread
from subsync import config
@@ -64,6 +65,8 @@ def __init__(self, parent, subs=None, refs=None):
self.Fit()
self.Layout()
+ self.refsCache = cache.WordsCache()
+
self.selfUpdater = None
assets.init(self.assetsUpdated)
@@ -81,13 +84,16 @@ def onButtonMenuClick(self, event):
self.PopupMenu(self.m_menu)
def onMenuItemSettingsClick(self, event):
- with SettingsWin(self, settings()) as dlg:
+ with SettingsWin(self, settings(), self.refsCache) as dlg:
if dlg.ShowModal() == wx.ID_OK:
newSettings = dlg.getSettings()
if settings() != newSettings:
self.changeSettings(newSettings)
def changeSettings(self, newSettings):
+ if not settings().refsCache:
+ self.refsCache.clear()
+
if settings().logLevel != newSettings.logLevel:
loggercfg.setLevel(newSettings.logLevel)
@@ -130,8 +136,12 @@ def onButtonCloseClick(self, event):
@error_dlg
def onButtonStartClick(self, event):
- settings().save()
- self.start()
+ try:
+ settings().save()
+ self.start()
+ except:
+ self.refsCache.clear()
+ raise
def start(self, listener=None):
self.validateSelection()
@@ -140,7 +150,9 @@ def start(self, listener=None):
if self.validateAssets():
sub = self.m_panelSub.stream
ref = self.m_panelRef.stream
- with SyncWin(self, sub, ref, listener) as dlg:
+ cache = self.refsCache if settings().refsCache else None
+
+ with SyncWin(self, sub, ref, cache, listener) as dlg:
dlg.ShowModal()
if listener:
diff --git a/subsync/gui/settingswin.py b/subsync/gui/settingswin.py
index 3ef92c5..ef87765 100644
--- a/subsync/gui/settingswin.py
+++ b/subsync/gui/settingswin.py
@@ -7,7 +7,7 @@
class SettingsWin(subsync.gui.layout.settingswin.SettingsWin):
- def __init__(self, parent, settings):
+ def __init__(self, parent, settings, cache=None):
super().__init__(parent)
self.m_outputCharEnc.SetString(0, _('same as input subtitles'))
@@ -18,6 +18,9 @@ def __init__(self, parent, settings):
self.setSettings(settings)
+ self.cache = cache
+ self.m_buttonClearCache.Enable(cache and not cache.isEmpty())
+
def setSettings(self, settings):
self.settings = Settings(settings)
@@ -100,3 +103,8 @@ def onButtonRestoreDefaultsClick(self, event):
if dlg.ShowModal() == wx.ID_YES:
self.setSettings(Settings())
+ def onButtonClearCache(self, event):
+ if self.cache:
+ self.cache.clear()
+ self.m_buttonClearCache.Disable()
+
diff --git a/subsync/gui/syncwin.py b/subsync/gui/syncwin.py
index 8b350b0..46f28aa 100644
--- a/subsync/gui/syncwin.py
+++ b/subsync/gui/syncwin.py
@@ -19,7 +19,7 @@
class SyncWin(subsync.gui.layout.syncwin.SyncWin):
- def __init__(self, parent, subs, refs, listener=None):
+ def __init__(self, parent, subs, refs, refsCache=None, listener=None):
super().__init__(parent)
self.m_buttonDebugMenu.SetLabel(u'\u22ee') # 2630
@@ -49,7 +49,7 @@ def __init__(self, parent, subs, refs, listener=None):
self.Bind(wx.EVT_TIMER, self.onUpdateTimerTick, self.updateTimer)
with busydlg.BusyDlg(_('Loading, please wait...')):
- self.sync = synchro.Synchronizer(self, self.subs, self.refs)
+ self.sync = synchro.Synchronizer(self, self.subs, self.refs, refsCache)
self.sync.start()
self.isRunning = True
diff --git a/subsync/pipeline.py b/subsync/pipeline.py
index 94805d4..0b01eb0 100644
--- a/subsync/pipeline.py
+++ b/subsync/pipeline.py
@@ -17,14 +17,22 @@ def __init__(self, stream):
self.duration = self.demux.getDuration()
self.timeWindow = [0, self.duration]
+ self.done = False
+
def destroy(self):
self.extractor.connectEosCallback(None)
self.extractor.connectErrorCallback(None)
self.extractor.stop()
- def selectTimeWindow(self, *window):
- self.timeWindow = window
- self.extractor.selectTimeWindow(*window)
+ def selectTimeWindow(self, begin, end, start=None):
+ if end > self.duration:
+ end = self.duration
+ if begin > end:
+ begin = end
+ self.timeWindow = (begin, end)
+ if start == None:
+ start = begin
+ self.extractor.selectTimeWindow(start, end)
def start(self, threadName=None):
self.extractor.start(threadName=threadName)
@@ -40,14 +48,21 @@ def getProgress(self):
pos = self.demux.getPosition()
if math.isclose(pos, 0.0):
pos = self.timeWindow[0]
- return (pos - self.timeWindow[0]) / (self.timeWindow[1] - self.timeWindow[0])
+ if self.timeWindow[1] > self.timeWindow[0]:
+ return (pos - self.timeWindow[0]) / (self.timeWindow[1] - self.timeWindow[0])
- def connectEosCallback(self, cb, dst=None):
- self.extractor.connectEosCallback(cb)
+ def getPosition(self):
+ return max(self.timeWindow[0], min(self.demux.getPosition(), self.timeWindow[1]))
- def connectErrorCallback(self, cb, dst=None):
- self.extractor.connectErrorCallback(cb)
+ def connectEosCallback(self, cb):
+ def eos(*args, **kw):
+ self.done = True
+ cb()
+ self.extractor.connectEosCallback(eos if cb else None)
+
+ def connectErrorCallback(self, cb):
+ self.extractor.connectErrorCallback(cb)
class SubtitlePipeline(BasePipeline):
def __init__(self, stream):
@@ -127,18 +142,30 @@ def createProducerPipeline(stream):
raise Error(_('Not supported stream type'), type=stream.type)
-def createProducerPipelines(stream, no):
+def createProducerPipelines(stream, no=None, timeWindows=None):
+ if timeWindows != None:
+ no = len(timeWindows)
+
pipes = []
for i in range(no):
p = createProducerPipeline(stream)
pipes.append(p)
if p.duration:
- partTime = p.duration / no
- begin = i * partTime
- end = begin + partTime
+ if timeWindows != None:
+ start, begin, end = timeWindows[i]
+
+ else:
+ partTime = p.duration / no
+ begin = i * partTime
+ end = begin + partTime
+ start = None
+
logger.info('job %i/%i time window set to %.2f - %.2f', i+1, no, begin, end)
- p.selectTimeWindow(begin, end)
+ if start != None:
+ logger.info('job %i/%i starting position set to %.2f', i+1, no, start)
+
+ p.selectTimeWindow(begin, end, start)
else:
logger.warn('cannot get duration - using single pipeline')
diff --git a/subsync/settings.py b/subsync/settings.py
index 2b69738..507b9b9 100644
--- a/subsync/settings.py
+++ b/subsync/settings.py
@@ -21,6 +21,7 @@ def __init__(self, settings=None, **kw):
self.minCorrelation = 0.9999
self.minWordsSim = 0.6
self.lastdir = ''
+ self.refsCache = True
self.autoUpdate = True
self.askForUpdate = True
self.debugOptions = False
diff --git a/subsync/synchro.py b/subsync/synchro.py
index b24898e..c6e6a67 100644
--- a/subsync/synchro.py
+++ b/subsync/synchro.py
@@ -4,6 +4,7 @@
from subsync.settings import settings
from subsync import dictionary
from subsync import encdetect
+from subsync import utils
import threading
import multiprocessing
@@ -20,10 +21,11 @@ def getJobsNo():
class Synchronizer(object):
- def __init__(self, listener, subs, refs):
+ def __init__(self, listener, subs, refs, refsCache=None):
self.listener = listener
self.subs = subs
self.refs = refs
+ self.refsCache = refsCache
self.fps = refs.stream().frameRate
if self.fps == None:
@@ -60,14 +62,33 @@ def __init__(self, listener, subs, refs):
else:
self.subPipeline.connectWordsCallback(self.correlator.pushSubWord)
- self.refPipelines = pipeline.createProducerPipelines(refs, getJobsNo())
+ if refsCache and refsCache.isValid(self.refs):
+ logger.info('restoring cached reference words (%i)', len(refsCache.data))
+
+ for word, time in refsCache.data:
+ self.correlator.pushRefWord(word, time)
+
+ self.refPipelines = pipeline.createProducerPipelines(refs, timeWindows=refsCache.progress)
+
+ else:
+ if refsCache:
+ refsCache.init(refs)
+
+ self.refPipelines = pipeline.createProducerPipelines(refs, no=getJobsNo())
+
for p in self.refPipelines:
p.connectEosCallback(self.onRefEos)
p.connectErrorCallback(self.onRefError)
- p.connectWordsCallback(self.correlator.pushRefWord)
+ p.connectWordsCallback(self.onRefWord)
self.pipelines = [ self.subPipeline ] + self.refPipelines
+ def onRefWord(self, word, time):
+ self.correlator.pushRefWord(word, time)
+
+ if self.refsCache and self.refsCache.id:
+ self.refsCache.data.append((word, time))
+
def destroy(self):
self.stop()
self.correlator.connectStatsCallback(None)
@@ -90,6 +111,11 @@ def stop(self):
for p in self.pipelines:
p.stop()
+ if self.refsCache and self.refsCache.id:
+ self.refsCache.progress = [ (p.getPosition(), *p.timeWindow)
+ for p in self.refPipelines
+ if not p.done and p.getPosition() < p.timeWindow[1] ]
+
def isRunning(self):
if self.correlator.isRunning() and not self.correlator.isDone():
return True