Anchor-annotator 0.8.1__py3-none-any.whl → 0.9.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
anchor/plot.py CHANGED
@@ -11,8 +11,16 @@ import numpy as np
11
11
  import pyqtgraph as pg
12
12
  import sqlalchemy
13
13
  from Bio import pairwise2
14
- from montreal_forced_aligner.data import CtmInterval
15
- from montreal_forced_aligner.db import Speaker, Utterance
14
+ from montreal_forced_aligner.db import (
15
+ PhoneInterval,
16
+ Pronunciation,
17
+ ReferencePhoneInterval,
18
+ ReferenceWordInterval,
19
+ Speaker,
20
+ Utterance,
21
+ Word,
22
+ WordInterval,
23
+ )
16
24
  from montreal_forced_aligner.dictionary.mixins import (
17
25
  DEFAULT_PUNCTUATION,
18
26
  DEFAULT_WORD_BREAK_MARKERS,
@@ -188,15 +196,18 @@ class UtteranceClusterView(pg.PlotWidget):
188
196
  pen=pg.mkPen(self.settings.value(self.settings.MAIN_TEXT_COLOR)),
189
197
  labelTextColor=self.settings.value(self.settings.MAIN_TEXT_COLOR),
190
198
  )
199
+ self.pen_colormap = pg.ColorMap(None, [0.0, 1.0])
200
+ self.pens = []
191
201
  self.legend_item.changeCluster.connect(self.change_cluster)
192
202
  self.legend_item.setParentItem(self.getPlotItem())
193
203
  self.legend_item.setFont(self.settings.font)
194
204
  self.selected_indices = set()
195
205
  # self.addItem(self.legend_item)
196
- self.selected_pen = pg.mkPen(self.settings.value(self.settings.MAIN_TEXT_COLOR), width=2)
206
+ self.selected_pen = pg.mkPen(
207
+ self.settings.value(self.settings.PRIMARY_LIGHT_COLOR), width=2
208
+ )
197
209
  self.updated_pen = pg.mkPen(self.settings.value(self.settings.MAIN_TEXT_COLOR), width=2)
198
210
  self.hover_pen = pg.mkPen(self.settings.value(self.settings.ACCENT_LIGHT_COLOR), width=2)
199
- self.base_pen = pg.mkPen(self.settings.value(self.settings.PRIMARY_DARK_COLOR), width=1)
200
211
  self.selection_timer = QtCore.QTimer()
201
212
  self.selection_timer.setInterval(300)
202
213
  self.selection_timer.timeout.connect(self.send_selection_update)
@@ -253,6 +264,8 @@ class UtteranceClusterView(pg.PlotWidget):
253
264
  if ev.button() == QtCore.Qt.MouseButton.LeftButton:
254
265
  utterance_id = int(self.speaker_model.utterance_ids[index])
255
266
  utterance = self.corpus_model.session.query(Utterance).get(utterance_id)
267
+ if utterance is None:
268
+ return
256
269
  self.selection_model.set_current_file(
257
270
  utterance.file_id,
258
271
  utterance.begin,
@@ -260,6 +273,7 @@ class UtteranceClusterView(pg.PlotWidget):
260
273
  utterance.id,
261
274
  utterance.speaker_id,
262
275
  force_update=True,
276
+ single_utterance=False,
263
277
  )
264
278
  else:
265
279
  brush_indices = list(self.brushes.keys())
@@ -301,6 +315,7 @@ class UtteranceClusterView(pg.PlotWidget):
301
315
  self.brushes[x if x in self.speaker_model.current_speakers else -1]
302
316
  for x in self.speaker_model.cluster_labels
303
317
  ]
318
+ self.pens = [pg.mkPen(x, width=2) for x in self.speaker_model.distances]
304
319
  xmin, xmax = np.min(self.speaker_model.mds[:, 0]), np.max(self.speaker_model.mds[:, 0])
305
320
  ymin, ymax = np.min(self.speaker_model.mds[:, 1]), np.max(self.speaker_model.mds[:, 1])
306
321
  xrange = xmax - xmin
@@ -328,7 +343,7 @@ class UtteranceClusterView(pg.PlotWidget):
328
343
  pos=self.speaker_model.mds,
329
344
  size=10,
330
345
  brush=brushes,
331
- pen=self.base_pen,
346
+ pen=self.pens,
332
347
  hoverPen=self.hover_pen,
333
348
  hoverable=True,
334
349
  )
@@ -360,7 +375,7 @@ class UtteranceClusterView(pg.PlotWidget):
360
375
  elif i in self.updated_indices:
361
376
  pens.append(self.updated_pen)
362
377
  else:
363
- pens.append(self.base_pen)
378
+ pens.append(self.pens[i])
364
379
  self.scatter_item.setPen(pens)
365
380
 
366
381
 
@@ -528,6 +543,7 @@ class UtteranceView(QtWidgets.QWidget):
528
543
  t.set_models(corpus_model, selection_model, dictionary_model)
529
544
  self.audio_plot.set_models(self.selection_model)
530
545
  self.selection_model.viewChanged.connect(self.update_plot)
546
+ self.selection_model.searchTermChanged.connect(self.set_search_term)
531
547
  # self.corpus_model.utteranceTextUpdated.connect(self.refresh_utterance_text)
532
548
  self.selection_model.resetView.connect(self.reset_plot)
533
549
  self.file_model.utterancesReady.connect(self.finalize_loading_utterances)
@@ -558,7 +574,6 @@ class UtteranceView(QtWidgets.QWidget):
558
574
  if self.file_model.file is None:
559
575
  return
560
576
  scroll_to = None
561
-
562
577
  self.speaker_tiers = {}
563
578
  self.speaker_tier_items = {}
564
579
  self.speaker_tier_layout.clear()
@@ -683,25 +698,18 @@ class UtteranceView(QtWidgets.QWidget):
683
698
  self.extra_tiers["Normalized text"] = "normalized_text"
684
699
  self.extra_tiers["Transcription"] = "transcription_text"
685
700
  if self.corpus_model.has_alignments and "Words" not in self.extra_tiers:
686
- self.extra_tiers["Words"] = "aligned_word_intervals"
687
- self.extra_tiers["Phones"] = "aligned_phone_intervals"
688
- if self.corpus_model.has_reference_alignments and "Reference" not in self.extra_tiers:
689
- self.extra_tiers["Reference"] = "reference_phone_intervals"
690
- if (
691
- self.corpus_model.has_transcribed_alignments
692
- and "Transcription" not in self.extra_tiers
693
- ):
694
- self.extra_tiers["Transcribed words"] = "transcribed_word_intervals"
695
- self.extra_tiers["Transcribed phones"] = "transcribed_phone_intervals"
701
+ self.extra_tiers["Words"] = "word_intervals"
702
+ self.extra_tiers["Phones"] = "phone_intervals"
696
703
  if (
697
- self.corpus_model.has_per_speaker_transcribed_alignments
698
- and "Transcription" not in self.extra_tiers
704
+ self.corpus_model.has_reference_alignments
705
+ and "Reference phones" not in self.extra_tiers
699
706
  ):
700
- self.extra_tiers["Transcribed words"] = "per_speaker_transcribed_word_intervals"
701
- self.extra_tiers["Transcribed phones"] = "per_speaker_transcribed_phone_intervals"
707
+ self.extra_tiers["Reference words"] = "reference_word_intervals"
708
+ self.extra_tiers["Reference phones"] = "reference_phone_intervals"
702
709
 
703
- def set_search_term(self):
704
- term = self.corpus_model.text_filter
710
+ def set_search_term(self, term: TextFilterQuery = None):
711
+ if term is None:
712
+ term = self.corpus_model.text_filter
705
713
  if not term:
706
714
  return
707
715
  self.search_term = term
@@ -766,6 +774,7 @@ class UtteranceView(QtWidgets.QWidget):
766
774
 
767
775
  class UtteranceLine(pg.InfiniteLine):
768
776
  hoverChanged = QtCore.Signal(object)
777
+ snapModeChanged = QtCore.Signal(object)
769
778
 
770
779
  def __init__(
771
780
  self, *args, movingPen=None, view_min=None, view_max=None, initial=True, **kwargs
@@ -798,6 +807,7 @@ class UtteranceLine(pg.InfiniteLine):
798
807
 
799
808
  def mouseDragEvent(self, ev):
800
809
  if self.movable and ev.button() == QtCore.Qt.MouseButton.LeftButton:
810
+ self.snapModeChanged.emit(ev.modifiers() & QtCore.Qt.KeyboardModifier.ControlModifier)
801
811
  if ev.isStart() and (
802
812
  (self.initial and self.pos().x() - self.mapToParent(ev.buttonDownPos()).x() < 0)
803
813
  or (
@@ -1224,7 +1234,7 @@ class IntervalTextRegion(pg.GraphicsObject):
1224
1234
 
1225
1235
  def __init__(
1226
1236
  self,
1227
- interval: CtmInterval,
1237
+ interval,
1228
1238
  color,
1229
1239
  top_point,
1230
1240
  height,
@@ -1544,7 +1554,7 @@ class MfaRegion(pg.LinearRegionItem):
1544
1554
 
1545
1555
  def __init__(
1546
1556
  self,
1547
- item: CtmInterval,
1557
+ item,
1548
1558
  corpus_model: CorpusModel,
1549
1559
  file_model: FileUtterancesModel,
1550
1560
  dictionary_model: typing.Optional[DictionaryTableModel],
@@ -1702,41 +1712,244 @@ class MfaRegion(pg.LinearRegionItem):
1702
1712
  return br
1703
1713
 
1704
1714
 
1715
+ class IntervalLine(pg.InfiniteLine):
1716
+ hoverChanged = QtCore.Signal(object, object)
1717
+
1718
+ def __init__(
1719
+ self,
1720
+ pos,
1721
+ index=None,
1722
+ index_count=None,
1723
+ pen=None,
1724
+ movingPen=None,
1725
+ hoverPen=None,
1726
+ bottom_point: float = 0,
1727
+ top_point: float = 1,
1728
+ bound_min=None,
1729
+ bound_max=None,
1730
+ movable=True,
1731
+ ):
1732
+ super().__init__(
1733
+ pos,
1734
+ angle=90,
1735
+ span=(bottom_point, top_point),
1736
+ pen=pen,
1737
+ hoverPen=hoverPen,
1738
+ movable=movable,
1739
+ )
1740
+ self.index = index
1741
+ self.index_count = index_count
1742
+ self.initial = index <= 0
1743
+ self.final = index >= index_count - 1
1744
+ self.bound_min = bound_min
1745
+ self.bound_max = bound_max
1746
+ self.movingPen = movingPen
1747
+ self.bounding_width = 0.1
1748
+ if self.movable:
1749
+ self.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
1750
+
1751
+ def setMouseHover(self, hover):
1752
+ if hover and self.movable:
1753
+ self.setCursor(QtCore.Qt.CursorShape.SizeHorCursor)
1754
+ elif self.movable:
1755
+ self.setCursor(QtCore.Qt.CursorShape.ArrowCursor)
1756
+ self.hoverChanged.emit(hover, self)
1757
+ super().setMouseHover(hover)
1758
+
1759
+ def _computeBoundingRect(self):
1760
+ # br = UIGraphicsItem.boundingRect(self)
1761
+ vr = self.viewRect() # bounds of containing ViewBox mapped to local coords.
1762
+ if vr is None:
1763
+ return QtCore.QRectF()
1764
+
1765
+ # add a 4-pixel radius around the line for mouse interaction.
1766
+ px = self.pixelLength(
1767
+ direction=pg.Point(1, 0), ortho=True
1768
+ ) # get pixel length orthogonal to the line
1769
+ if px is None:
1770
+ px = 0
1771
+ pw = max(self.pen.width() / 2, self.hoverPen.width() / 2)
1772
+ w = max(self.bounding_width, self._maxMarkerSize + pw) + 5
1773
+ w = w * px
1774
+ br = QtCore.QRectF(vr)
1775
+ br.setBottom(-w)
1776
+ br.setTop(w)
1777
+
1778
+ left = self.span[0]
1779
+ right = self.span[1]
1780
+
1781
+ br.setLeft(left)
1782
+ br.setRight(right)
1783
+ br = br.normalized()
1784
+
1785
+ vs = self.getViewBox().size()
1786
+
1787
+ if self._bounds != br or self._lastViewSize != vs:
1788
+ self._bounds = br
1789
+ self._lastViewSize = vs
1790
+ self.prepareGeometryChange()
1791
+
1792
+ self._endPoints = (left, right)
1793
+ self._lastViewRect = vr
1794
+
1795
+ return self._bounds
1796
+
1797
+ def hoverEvent(self, ev):
1798
+ if (
1799
+ (not ev.isExit())
1800
+ and self.movable
1801
+ # and (
1802
+ # (self.initial and self.pos().x() - self.mapToParent(ev.pos()).x() < 0)
1803
+ # or (not self.initial and self.pos().x() - self.mapToParent(ev.pos()).x() > 0)
1804
+ # )
1805
+ and ev.acceptDrags(QtCore.Qt.MouseButton.LeftButton)
1806
+ ):
1807
+ self.setMouseHover(True)
1808
+ else:
1809
+ self.setMouseHover(False)
1810
+
1811
+ def mouseDragEvent(self, ev):
1812
+ if self.movable and ev.button() == QtCore.Qt.MouseButton.LeftButton:
1813
+ if ev.isStart():
1814
+ self.moving = True
1815
+ self._boundingRect = None
1816
+ self.currentPen = self.movingPen
1817
+ self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos())
1818
+ self.startPosition = self.pos()
1819
+ ev.accept()
1820
+
1821
+ if not self.moving:
1822
+ return
1823
+ p = self.cursorOffset + self.mapToParent(ev.pos())
1824
+ p.setY(self.startPosition.y())
1825
+ if p.x() >= self.bound_max - 0.01:
1826
+ p.setX(self.bound_max - 0.01)
1827
+ if p.x() <= self.bound_min + 0.01:
1828
+ p.setX(self.bound_min + 0.01)
1829
+ self.setPos(p)
1830
+ self.sigDragged.emit(self)
1831
+ if ev.isFinish():
1832
+ self.currentPen = self.pen
1833
+ self.moving = False
1834
+ self.sigPositionChangeFinished.emit(self)
1835
+
1836
+
1705
1837
  class IntervalTier(pg.GraphicsObject):
1706
1838
  highlightRequested = QtCore.Signal(object)
1707
1839
 
1708
1840
  def __init__(
1709
1841
  self,
1710
- parent,
1711
- utterance: CtmInterval,
1712
- intervals: typing.List[CtmInterval],
1842
+ parent: UtteranceRegion,
1843
+ utterance: Utterance,
1844
+ intervals: typing.List[typing.Union[PhoneInterval, WordInterval]],
1713
1845
  selection_model: FileSelectionModel,
1714
- top_point,
1715
- bottom_point,
1716
- word=False,
1846
+ top_point: float,
1847
+ bottom_point: float,
1848
+ lookup: typing.Optional[str] = None,
1849
+ movable: bool = False,
1717
1850
  ):
1718
1851
  super().__init__()
1719
1852
  self.setParentItem(parent)
1720
1853
  self.intervals = intervals
1721
- self.word = word
1854
+ self.lookup = lookup
1855
+ self.settings = AnchorSettings()
1856
+ self.plot_theme = self.settings.plot_theme
1857
+ self.anchor = pg.Point((0.5, 0.5))
1858
+ self.plot_text_font = self.settings.font
1859
+ self.movable = movable
1860
+
1861
+ self.background_color = self.plot_theme.background_color
1862
+ self.hover_line_color = self.plot_theme.hover_line_color
1863
+ self.moving_line_color = self.plot_theme.moving_line_color
1864
+ self.break_line_color = self.plot_theme.break_line_color
1865
+ self.text_color = self.plot_theme.text_color
1866
+ self.selected_interval_color = self.plot_theme.selected_interval_color
1867
+ self.highlight_interval_color = self.plot_theme.break_line_color
1868
+ self.highlight_text_color = self.plot_theme.background_color
1869
+
1870
+ self.top_point = top_point
1871
+ self.bottom_point = bottom_point
1872
+ self.selection_model = selection_model
1873
+ self.utterance = utterance
1874
+ self.lines = []
1875
+ self.selected = []
1876
+
1877
+ self._boundingRectCache = None
1878
+ self._cached_pixel_size = None
1879
+
1880
+ self.hoverPen = pg.mkPen(self.hover_line_color, width=3)
1881
+ self.movingPen = pg.mkPen(
1882
+ self.moving_line_color, width=3, style=QtCore.Qt.PenStyle.DashLine
1883
+ )
1884
+ self.border_pen = pg.mkPen(self.break_line_color, width=3)
1885
+ self.border_pen.setCapStyle(QtCore.Qt.PenCapStyle.FlatCap)
1886
+ self.text_pen = pg.mkPen(self.text_color)
1887
+ self.text_brush = pg.mkBrush(self.text_color)
1888
+ self.highlight_text_pen = pg.mkPen(self.highlight_text_color)
1889
+ self.highlight_text_brush = pg.mkBrush(self.highlight_text_color)
1890
+ self.search_term = None
1891
+ self.search_regex = None
1892
+ self.update_intervals(self.utterance)
1893
+
1894
+ def refresh_boundaries(self, interval_id, new_time):
1895
+ for index, interval in enumerate(self.intervals):
1896
+ if interval.id == interval_id:
1897
+ try:
1898
+ self.lines[index].setPos(new_time)
1899
+ except IndexError:
1900
+ pass
1901
+ break
1902
+ self.refresh_tier()
1903
+
1904
+ def update_intervals(self, utterance):
1905
+ self.intervals = sorted(
1906
+ getattr(utterance, self.lookup, self.intervals), key=lambda x: x.begin
1907
+ )
1908
+ for line in self.lines:
1909
+ if line.scene() is not None:
1910
+ line.scene().removeItem(line)
1911
+ self.lines = []
1912
+ bound_min = self.utterance.begin
1913
+ for i, interval in enumerate(self.intervals):
1914
+ if i == 0:
1915
+ continue
1916
+
1917
+ line = IntervalLine(
1918
+ interval.begin,
1919
+ index=i - 1,
1920
+ index_count=len(self.intervals) - 1,
1921
+ bound_min=bound_min,
1922
+ bound_max=interval.end,
1923
+ bottom_point=self.bottom_point,
1924
+ top_point=self.top_point,
1925
+ pen=self.border_pen,
1926
+ movingPen=self.movingPen,
1927
+ hoverPen=self.hoverPen,
1928
+ movable=self.movable,
1929
+ )
1930
+ line.setZValue(30)
1931
+ line.setParentItem(self)
1932
+ # line.sigPositionChanged.connect(self._lineMoved)
1933
+ self.lines.append(line)
1934
+ bound_min = interval.begin
1935
+ self.refresh_tier()
1936
+
1937
+ def refresh_tier(self):
1938
+ self.regenerate_text_boxes()
1939
+ self.update()
1940
+
1941
+ def regenerate_text_boxes(self):
1722
1942
  self.array = pg.Qt.internals.PrimitiveArray(QtCore.QRectF, 4)
1723
1943
  self.selected_array = pg.Qt.internals.PrimitiveArray(QtCore.QRectF, 4)
1724
1944
  self.array.resize(len(self.intervals))
1725
1945
  self.selected = []
1726
- self.settings = AnchorSettings()
1727
- self.plot_theme = self.settings.plot_theme
1728
1946
  memory = self.array.ndarray()
1729
- self.anchor = pg.Point((0.5, 0.5))
1730
- if self.word:
1731
- self.plot_text_font = self.settings.font
1732
- else:
1733
- self.plot_text_font = self.settings.small_font
1734
1947
 
1735
1948
  fm = QtGui.QFontMetrics(self.plot_text_font)
1736
- for i, interval in enumerate(intervals):
1949
+ for i, interval in enumerate(self.intervals):
1737
1950
  memory[i, 0] = interval.begin
1738
1951
  memory[i, 2] = interval.end - interval.begin
1739
- if interval.label not in self.parentItem().painter_path_cache[self.word]:
1952
+ if interval.label not in self.parentItem().painter_path_cache:
1740
1953
  symbol = QtGui.QPainterPath()
1741
1954
 
1742
1955
  symbol.addText(0, 0, self.plot_text_font, interval.label)
@@ -1747,46 +1960,28 @@ class IntervalTier(pg.GraphicsObject):
1747
1960
 
1748
1961
  # translating
1749
1962
  tr.translate(-br.x() - br.width() / 2.0, fm.height() / 2.0)
1750
- self.parentItem().painter_path_cache[self.word][interval.label] = tr.map(symbol)
1751
-
1752
- memory[:, 1] = bottom_point
1753
- memory[:, 3] = top_point - bottom_point
1754
- self.top_point = top_point
1755
- self.bottom_point = bottom_point
1756
- self.selection_model = selection_model
1757
- self.utterance = utterance
1963
+ self.parentItem().painter_path_cache[interval.label] = tr.map(symbol)
1758
1964
 
1759
- self.background_color = self.plot_theme.background_color
1760
- self.hover_line_color = self.plot_theme.hover_line_color
1761
- self.moving_line_color = self.plot_theme.moving_line_color
1762
-
1763
- self.break_line_color = self.plot_theme.break_line_color
1764
- self.text_color = self.plot_theme.text_color
1765
- self.selected_interval_color = self.plot_theme.selected_interval_color
1766
- self.highlight_interval_color = self.plot_theme.break_line_color
1767
- self.highlight_text_color = self.plot_theme.background_color
1768
- self.text_pen = pg.mkPen(self.text_color)
1769
- self.text_brush = pg.mkBrush(self.text_color)
1770
- self.highlight_text_pen = pg.mkPen(self.highlight_text_color)
1771
- self.highlight_text_brush = pg.mkBrush(self.highlight_text_color)
1772
- self.border_pen = pg.mkPen(self.break_line_color, width=1)
1773
- self.border_pen.setCapStyle(QtCore.Qt.PenCapStyle.FlatCap)
1774
- self.search_term = None
1775
- self.search_regex = None
1965
+ memory[:, 1] = self.bottom_point
1966
+ memory[:, 3] = self.top_point - self.bottom_point
1776
1967
 
1777
1968
  def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
1778
1969
  if e.button() == QtCore.Qt.MouseButton.LeftButton:
1970
+ if any(line.mouseHovering for line in self.lines):
1971
+ e.ignore()
1972
+ return
1779
1973
  time = e.pos().x()
1974
+
1975
+ margin = 21 * self._cached_pixel_size[0]
1976
+ if time <= self.utterance.begin + margin or time >= self.utterance.end - margin:
1977
+ e.ignore()
1978
+ return
1780
1979
  memory = self.array.ndarray()
1781
1980
  if memory.shape[0] > 0:
1782
1981
  index = np.searchsorted(memory[:, 0], time) - 1
1783
- self.selection_model.select_audio(
1784
- self.intervals[index].begin, self.intervals[index].end
1785
- )
1786
- if self.word:
1787
- self.highlightRequested.emit(
1788
- TextFilterQuery(self.intervals[index].label, word=True)
1789
- )
1982
+ interval = self.intervals[index]
1983
+ self.selection_model.select_audio(interval.begin, interval.end)
1984
+ self.highlightRequested.emit(TextFilterQuery(interval.label, word=True))
1790
1985
  e.accept()
1791
1986
  return
1792
1987
 
@@ -1814,9 +2009,10 @@ class IntervalTier(pg.GraphicsObject):
1814
2009
  vb = self.getViewBox()
1815
2010
  px = vb.viewPixelSize()
1816
2011
  inst = self.array.instances()
2012
+ br = self.boundingRect()
1817
2013
  painter.save()
1818
2014
  painter.setPen(self.border_pen)
1819
- painter.drawRects(inst)
2015
+ painter.drawRect(br)
1820
2016
  painter.restore()
1821
2017
  total_time = self.selection_model.max_time - self.selection_model.min_time
1822
2018
  if self.selected:
@@ -1847,29 +2043,338 @@ class IntervalTier(pg.GraphicsObject):
1847
2043
  painter.setPen(text_pen)
1848
2044
  painter.setBrush(text_brush)
1849
2045
  painter.translate(x, (self.top_point + self.bottom_point) / 2)
1850
- path = self.parentItem().painter_path_cache[self.word][interval.label]
2046
+ path = self.parentItem().painter_path_cache[interval.label]
1851
2047
  painter.scale(px[0], -px[1])
1852
2048
  painter.drawPath(path)
1853
2049
  painter.restore()
1854
2050
 
1855
2051
  def boundingRect(self):
1856
- return QtCore.QRectF(
2052
+ br = QtCore.QRectF(
1857
2053
  self.utterance.begin,
1858
2054
  self.bottom_point,
1859
2055
  self.utterance.end - self.utterance.begin,
1860
2056
  abs(self.top_point - self.bottom_point),
1861
2057
  )
2058
+ vb = self.getViewBox()
2059
+ self._cached_pixel_size = vb.viewPixelSize()
2060
+ if self._boundingRectCache != br:
2061
+ self._boundingRectCache = br
2062
+ self.prepareGeometryChange()
2063
+ return br
2064
+
2065
+
2066
+ class WordIntervalTier(IntervalTier):
2067
+ wordPronunciationChanged = QtCore.Signal(object, object)
2068
+ wordChanged = QtCore.Signal(object, object)
2069
+
2070
+ def __init__(
2071
+ self,
2072
+ parent,
2073
+ utterance: Utterance,
2074
+ intervals: typing.List[WordInterval],
2075
+ selection_model: FileSelectionModel,
2076
+ top_point,
2077
+ bottom_point,
2078
+ lookup=None,
2079
+ ):
2080
+ super().__init__(
2081
+ parent, utterance, intervals, selection_model, top_point, bottom_point, lookup=lookup
2082
+ )
2083
+
2084
+ def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
2085
+ if e.button() == QtCore.Qt.MouseButton.RightButton:
2086
+ time = e.pos().x()
2087
+ memory = self.array.ndarray()
2088
+ if memory.shape[0] > 0:
2089
+ index = np.searchsorted(memory[:, 0], time) - 1
2090
+ interval = self.intervals[index]
2091
+ self.selection_model.select_audio(interval.begin, interval.end)
2092
+ self.highlightRequested.emit(TextFilterQuery(interval.label, word=True))
2093
+ menu = self.construct_context_menu(interval)
2094
+ menu.exec_(e.screenPos())
2095
+ e.accept()
2096
+ return
2097
+
2098
+ return super().mousePressEvent(e)
2099
+
2100
+ def construct_context_menu(
2101
+ self, word_interval: typing.Union[WordInterval, ReferenceWordInterval]
2102
+ ):
2103
+ menu = QtWidgets.QMenu()
2104
+ a = QtGui.QAction(menu)
2105
+ a.setText("Change word...")
2106
+ a.triggered.connect(lambda triggered, x=word_interval: self.change_word(x))
2107
+ menu.addAction(a)
2108
+ if isinstance(word_interval, WordInterval):
2109
+ change_pronunciation_menu = QtWidgets.QMenu("Change pronunciation")
2110
+ parent: UtteranceRegion = self.parentItem()
2111
+ pronunciations = (
2112
+ parent.corpus_model.session.query(Pronunciation)
2113
+ .filter(
2114
+ Pronunciation.word_id == word_interval.word_id,
2115
+ )
2116
+ .all()
2117
+ )
2118
+ for pron in pronunciations:
2119
+ if pron.id == word_interval.pronunciation_id:
2120
+ continue
2121
+ a = QtGui.QAction(menu)
2122
+ a.setText(pron.pronunciation)
2123
+ a.triggered.connect(
2124
+ lambda triggered, x=word_interval, y=pron: self.update_pronunciation(x, y)
2125
+ )
2126
+ change_pronunciation_menu.addAction(a)
2127
+ menu.addMenu(change_pronunciation_menu)
2128
+ change_pronunciation_menu.setStyleSheet(self.settings.menu_style_sheet)
2129
+ menu.setStyleSheet(self.settings.menu_style_sheet)
2130
+ return menu
2131
+
2132
+ def change_word(self, word_interval: typing.Union[WordInterval, ReferenceWordInterval]):
2133
+ from anchor.widgets import WordQueryDialog
2134
+
2135
+ parent: UtteranceRegion = self.parentItem()
2136
+
2137
+ dialog = WordQueryDialog(parent.corpus_model)
2138
+ if dialog.exec_():
2139
+ word = dialog.word_dropdown.current_text()
2140
+ self.wordChanged.emit(word_interval, word)
2141
+
2142
+ def update_pronunciation(
2143
+ self,
2144
+ word_interval: typing.Union[WordInterval, ReferenceWordInterval],
2145
+ pronunciation: Pronunciation,
2146
+ ):
2147
+ self.wordPronunciationChanged.emit(word_interval, pronunciation)
2148
+
2149
+
2150
+ class PhoneIntervalTier(IntervalTier):
2151
+ draggingLine = QtCore.Signal(object)
2152
+ lineDragFinished = QtCore.Signal(object)
2153
+ phoneBoundaryChanged = QtCore.Signal(object, object, object)
2154
+ phoneIntervalChanged = QtCore.Signal(object, object)
2155
+ phoneIntervalInserted = QtCore.Signal(object, object, object, object, object, object)
2156
+ phoneIntervalDeleted = QtCore.Signal(object, object, object, object)
2157
+ deleteReferenceAlignments = QtCore.Signal()
2158
+
2159
+ def __init__(
2160
+ self,
2161
+ parent,
2162
+ utterance: Utterance,
2163
+ intervals: typing.List[PhoneInterval, ReferencePhoneInterval],
2164
+ selection_model: FileSelectionModel,
2165
+ top_point,
2166
+ bottom_point,
2167
+ lookup=None,
2168
+ ):
2169
+ super().__init__(
2170
+ parent,
2171
+ utterance,
2172
+ intervals,
2173
+ selection_model,
2174
+ top_point,
2175
+ bottom_point,
2176
+ lookup=lookup,
2177
+ movable=True,
2178
+ )
2179
+
2180
+ def update_intervals(self, utterance):
2181
+ self.intervals = sorted(
2182
+ getattr(utterance, self.lookup, self.intervals), key=lambda x: x.begin
2183
+ )
2184
+ for line in self.lines:
2185
+ if line.scene() is not None:
2186
+ line.scene().removeItem(line)
2187
+ self.lines = []
2188
+ bound_min = self.utterance.begin
2189
+ for i, interval in enumerate(self.intervals):
2190
+ if i == 0:
2191
+ continue
2192
+
2193
+ line = IntervalLine(
2194
+ interval.begin,
2195
+ index=i - 1,
2196
+ index_count=len(self.intervals) - 1,
2197
+ bound_min=bound_min,
2198
+ bound_max=interval.end,
2199
+ bottom_point=self.bottom_point,
2200
+ top_point=self.top_point,
2201
+ pen=self.border_pen,
2202
+ movingPen=self.movingPen,
2203
+ hoverPen=self.hoverPen,
2204
+ movable=self.movable,
2205
+ )
2206
+ line.setZValue(30)
2207
+ line.setParentItem(self)
2208
+ line.sigPositionChangeFinished.connect(self.lineMoveFinished)
2209
+ # line.sigPositionChanged.connect(self._lineMoved)
2210
+ self.lines.append(line)
2211
+ bound_min = interval.begin
2212
+ line.sigPositionChanged.connect(self.draggingLine.emit)
2213
+ line.sigPositionChangeFinished.connect(self.lineDragFinished.emit)
2214
+ line.hoverChanged.connect(self.update_hover)
2215
+ self.refresh_tier()
2216
+
2217
+ def update_hover(self, hovered, time):
2218
+ if hovered:
2219
+ self.draggingLine.emit(time)
2220
+ else:
2221
+ self.lineDragFinished.emit(time)
2222
+
2223
+ def lineMoveFinished(self):
2224
+ sender: IntervalLine = self.sender()
2225
+ self.phoneBoundaryChanged.emit(
2226
+ self.intervals[sender.index], self.intervals[sender.index + 1], sender.pos().x()
2227
+ )
2228
+ if sender.index != 0:
2229
+ self.lines[sender.index - 1].bound_max = sender.pos().x()
2230
+ if sender.index != len(self.lines) - 1:
2231
+ self.lines[sender.index + 1].bound_min = sender.pos().x()
2232
+ self.regenerate_text_boxes()
2233
+ self.update()
2234
+
2235
+ def mousePressEvent(self, e: QtGui.QMouseEvent) -> None:
2236
+ if e.button() == QtCore.Qt.MouseButton.RightButton:
2237
+ time = e.pos().x()
2238
+ memory = self.array.ndarray()
2239
+ if memory.shape[0] > 0:
2240
+ index = np.searchsorted(memory[:, 0], time) - 1
2241
+ interval = self.intervals[index]
2242
+ self.selection_model.select_audio(interval.begin, interval.end)
2243
+ self.highlightRequested.emit(TextFilterQuery(interval.label, word=True))
2244
+ initial = (time - interval.begin) < (interval.end - time)
2245
+ menu = self.construct_context_menu(index, interval, initial)
2246
+ menu.exec_(e.screenPos())
2247
+ e.accept()
2248
+ return
2249
+
2250
+ return super().mousePressEvent(e)
2251
+
2252
+ def update_phone(self, phone_interval, phone):
2253
+ self.phoneIntervalChanged.emit(phone_interval, phone)
2254
+
2255
+ def insert_phone_interval(self, index: int, initial: bool):
2256
+ previous_interval = None
2257
+ following_interval = None
2258
+ if initial:
2259
+ following_interval = self.intervals[index]
2260
+ word_interval_id = following_interval.word_interval_id
2261
+ if index > 0:
2262
+ previous_interval = self.intervals[index - 1]
2263
+ begin = following_interval.begin
2264
+ end = (following_interval.begin + following_interval.end) / 2
2265
+ else:
2266
+ previous_interval = self.intervals[index]
2267
+ word_interval_id = previous_interval.word_interval_id
2268
+ if index < len(self.intervals) - 1:
2269
+ following_interval = self.intervals[index + 1]
2270
+ begin = (previous_interval.begin + previous_interval.end) / 2
2271
+ end = previous_interval.end
2272
+ self.phoneIntervalInserted.emit(
2273
+ previous_interval, following_interval, word_interval_id, begin, end, self.lookup
2274
+ )
2275
+
2276
+ def insert_silence_interval(self, index: int, initial: bool):
2277
+ previous_interval = None
2278
+ following_interval = None
2279
+ if initial:
2280
+ following_interval = self.intervals[index]
2281
+ if index > 0:
2282
+ previous_interval = self.intervals[index - 1]
2283
+ begin = following_interval.begin
2284
+ end = (following_interval.begin + following_interval.end) / 2
2285
+ else:
2286
+ previous_interval = self.intervals[index]
2287
+ if index < len(self.intervals) - 1:
2288
+ following_interval = self.intervals[index + 1]
2289
+ begin = (previous_interval.begin + previous_interval.end) / 2
2290
+ end = previous_interval.end
2291
+ self.phoneIntervalInserted.emit(
2292
+ previous_interval, following_interval, None, begin, end, self.lookup
2293
+ )
2294
+
2295
+ def delete_phone_interval(self, index: int, initial: bool):
2296
+ previous_interval = None
2297
+ interval = self.intervals[index]
2298
+ following_interval = None
2299
+ if index > 0:
2300
+ previous_interval = self.intervals[index - 1]
2301
+ if index < len(self.intervals) - 1:
2302
+ following_interval = self.intervals[index + 1]
2303
+ if initial:
2304
+ time_point = interval.end
2305
+ else:
2306
+ time_point = interval.begin
2307
+ self.phoneIntervalDeleted.emit(interval, previous_interval, following_interval, time_point)
2308
+
2309
+ def delete_reference(self):
2310
+ self.deleteReferenceAlignments.emit()
2311
+
2312
+ def construct_context_menu(
2313
+ self,
2314
+ index,
2315
+ phone_interval: typing.Union[PhoneInterval, ReferencePhoneInterval],
2316
+ initial=True,
2317
+ ):
2318
+ menu = QtWidgets.QMenu()
2319
+ change_phone_menu = QtWidgets.QMenu("Change phone")
2320
+ for phone_label, phone in sorted(
2321
+ self.parentItem().corpus_model.phones.items(), key=lambda x: x[0]
2322
+ ):
2323
+ if phone_label == phone_interval.label:
2324
+ continue
2325
+ a = QtGui.QAction(menu)
2326
+ a.setText(phone_label)
2327
+ a.triggered.connect(
2328
+ lambda triggered, x=phone_interval, y=phone: self.update_phone(x, y)
2329
+ )
2330
+ change_phone_menu.addAction(a)
2331
+ menu.addMenu(change_phone_menu)
2332
+ a = QtGui.QAction(menu)
2333
+ a.setText("Insert silence/word")
2334
+ a.triggered.connect(
2335
+ lambda triggered, x=index, y=initial: self.insert_silence_interval(x, y)
2336
+ )
2337
+ menu.addAction(a)
2338
+
2339
+ a = QtGui.QAction(menu)
2340
+ a.setText("Insert interval")
2341
+ a.triggered.connect(lambda triggered, x=index, y=initial: self.insert_phone_interval(x, y))
2342
+ menu.addAction(a)
2343
+
2344
+ a = QtGui.QAction(menu)
2345
+ a.setText("Delete interval")
2346
+ a.triggered.connect(lambda triggered, x=index, y=initial: self.delete_phone_interval(x, y))
2347
+ menu.addAction(a)
2348
+ if self.lookup.startswith("reference"):
2349
+ a = QtGui.QAction(menu)
2350
+ a.setText("Delete reference alignments")
2351
+ a.triggered.connect(self.delete_reference)
2352
+ menu.addAction(a)
2353
+ change_phone_menu.setStyleSheet(self.settings.menu_style_sheet)
2354
+ menu.setStyleSheet(self.settings.menu_style_sheet)
2355
+ return menu
1862
2356
 
1863
2357
 
1864
2358
  class UtteranceRegion(MfaRegion):
2359
+ phoneBoundaryChanged = QtCore.Signal(object, object, object, object)
2360
+ phoneIntervalChanged = QtCore.Signal(object, object, object)
2361
+ wordPronunciationChanged = QtCore.Signal(object, object, object)
2362
+ wordChanged = QtCore.Signal(object, object, object)
2363
+ phoneIntervalInserted = QtCore.Signal(object, object, object, object, object)
2364
+ phoneIntervalDeleted = QtCore.Signal(object, object, object, object, object)
2365
+ deleteReferenceAlignments = QtCore.Signal(object)
1865
2366
  lookUpWord = QtCore.Signal(object)
1866
2367
  createWord = QtCore.Signal(object)
1867
2368
  transcribeRequested = QtCore.Signal(object)
2369
+ draggingLine = QtCore.Signal(object)
2370
+ lineDragFinished = QtCore.Signal(object)
2371
+ wordBoundariesChanged = QtCore.Signal(object, object)
2372
+ phoneTiersChanged = QtCore.Signal(object)
1868
2373
 
1869
2374
  def __init__(
1870
2375
  self,
1871
2376
  parent,
1872
- utterance: workers.UtteranceData,
2377
+ utterance: Utterance,
1873
2378
  corpus_model: CorpusModel,
1874
2379
  file_model: FileUtterancesModel,
1875
2380
  dictionary_model: DictionaryTableModel,
@@ -1931,15 +2436,18 @@ class UtteranceRegion(MfaRegion):
1931
2436
  **lineKwds,
1932
2437
  ),
1933
2438
  ]
1934
-
2439
+ self.snap_mode = False
2440
+ self.initial_line_moving = False
1935
2441
  for line in self.lines:
1936
2442
  line.setZValue(30)
1937
2443
  line.setParentItem(self)
1938
2444
  line.sigPositionChangeFinished.connect(self.lineMoveFinished)
2445
+ line.hoverChanged.connect(self.popup)
2446
+ line.sigPositionChanged.connect(self.draggingLine.emit)
2447
+ line.sigPositionChangeFinished.connect(self.lineDragFinished.emit)
2448
+ line.snapModeChanged.connect(self.update_snap_mode)
1939
2449
  self.lines[0].sigPositionChanged.connect(self._line0Moved)
1940
2450
  self.lines[1].sigPositionChanged.connect(self._line1Moved)
1941
- self.lines[0].hoverChanged.connect(self.popup)
1942
- self.lines[1].hoverChanged.connect(self.popup)
1943
2451
 
1944
2452
  self.corpus_model.utteranceTextUpdated.connect(self.update_text_from_model)
1945
2453
  self.original_text = self.item.text
@@ -1955,7 +2463,7 @@ class UtteranceRegion(MfaRegion):
1955
2463
  search_term=search_term,
1956
2464
  speaker_id=utterance.speaker_id,
1957
2465
  )
1958
-
2466
+ self._painter_path_cache = {}
1959
2467
  self.text_edit = self.text_item.text_edit
1960
2468
  self.text_edit.gainedFocus.connect(self.select_self)
1961
2469
  self.text_edit.menuRequested.connect(self.generate_text_edit_menu)
@@ -1972,6 +2480,7 @@ class UtteranceRegion(MfaRegion):
1972
2480
  self.normalized_text = None
1973
2481
  self.transcription_text = None
1974
2482
  i = -1
2483
+ self.file_model.phoneTierChanged.connect(self.update_phone_tiers)
1975
2484
  for tier_name, lookup in self.extra_tiers.items():
1976
2485
  if not visible_tiers[tier_name]:
1977
2486
  continue
@@ -2013,9 +2522,10 @@ class UtteranceRegion(MfaRegion):
2013
2522
  self.transcription_text.text_edit.menuRequested.connect(
2014
2523
  self.generate_text_edit_menu
2015
2524
  )
2016
- self.normalized_text.text_edit.textChanged.connect(
2017
- self.update_transcription_highlight
2018
- )
2525
+ if self.normalized_text is not None:
2526
+ self.normalized_text.text_edit.textChanged.connect(
2527
+ self.update_transcription_highlight
2528
+ )
2019
2529
  self.transcription_text.text_edit.textChanged.connect(
2020
2530
  self.update_transcription_highlight
2021
2531
  )
@@ -2040,35 +2550,51 @@ class UtteranceRegion(MfaRegion):
2040
2550
  )
2041
2551
  cmap.linearize()
2042
2552
  if "phone_intervals" in lookup:
2043
- for interval in intervals:
2044
- if interval.confidence is None:
2045
- continue
2046
- if min_confidence is None or interval.confidence < min_confidence:
2047
- min_confidence = interval.confidence
2048
- if max_confidence is None or interval.confidence > max_confidence:
2049
- max_confidence = interval.confidence
2050
- interval_tier = IntervalTier(
2553
+ if lookup != "reference_phone_intervals":
2554
+ for interval in intervals:
2555
+ if interval.confidence is None:
2556
+ continue
2557
+ if min_confidence is None or interval.confidence < min_confidence:
2558
+ min_confidence = interval.confidence
2559
+ if max_confidence is None or interval.confidence > max_confidence:
2560
+ max_confidence = interval.confidence
2561
+ interval_tier = PhoneIntervalTier(
2051
2562
  self,
2052
2563
  self.item,
2053
2564
  intervals,
2054
2565
  self.selection_model,
2055
2566
  top_point=tier_top_point,
2056
2567
  bottom_point=tier_bottom_point,
2057
- word=False,
2568
+ lookup=lookup,
2058
2569
  )
2570
+ interval_tier.draggingLine.connect(self.draggingLine.emit)
2571
+ interval_tier.lineDragFinished.connect(self.lineDragFinished.emit)
2572
+ interval_tier.phoneBoundaryChanged.connect(self.change_phone_boundaries)
2573
+ interval_tier.phoneIntervalChanged.connect(self.change_phone_interval)
2574
+ interval_tier.phoneIntervalDeleted.connect(self.delete_phone_interval)
2575
+ interval_tier.phoneIntervalInserted.connect(self.insert_phone_interval)
2576
+ self.phoneTiersChanged.connect(interval_tier.update_intervals)
2577
+ if lookup.startswith("reference"):
2578
+ interval_tier.deleteReferenceAlignments.connect(
2579
+ self.delete_reference_alignments
2580
+ )
2059
2581
  self.extra_tier_intervals[tier_name].append(interval_tier)
2060
2582
 
2061
2583
  elif "word_intervals" in lookup:
2062
- interval_tier = IntervalTier(
2584
+ interval_tier = WordIntervalTier(
2063
2585
  self,
2064
2586
  self.item,
2065
2587
  intervals,
2066
2588
  self.selection_model,
2067
2589
  top_point=tier_top_point,
2068
2590
  bottom_point=tier_bottom_point,
2069
- word=True,
2591
+ lookup=lookup,
2070
2592
  )
2593
+ self.wordBoundariesChanged.connect(interval_tier.refresh_boundaries)
2071
2594
  interval_tier.highlightRequested.connect(self.set_search_term)
2595
+ interval_tier.wordPronunciationChanged.connect(self.change_word_pronunciation)
2596
+ interval_tier.wordChanged.connect(self.change_word)
2597
+ self.phoneTiersChanged.connect(interval_tier.update_intervals)
2072
2598
  if self.transcription_text is not None:
2073
2599
  interval_tier.highlightRequested.connect(
2074
2600
  self.transcription_text.highlighter.setSearchTerm
@@ -2105,6 +2631,25 @@ class UtteranceRegion(MfaRegion):
2105
2631
  self.show()
2106
2632
  self.available_speakers = available_speakers
2107
2633
 
2634
+ def update_snap_mode(self, snap_mode):
2635
+ self.snap_mode = snap_mode
2636
+
2637
+ def _line0Moved(self):
2638
+ self.lineMoved(0)
2639
+ self.initial_line_moving = True
2640
+
2641
+ def _line1Moved(self):
2642
+ self.lineMoved(1)
2643
+ self.initial_line_moving = False
2644
+
2645
+ def delete_reference_alignments(self):
2646
+ self.deleteReferenceAlignments.emit(self.item)
2647
+
2648
+ def update_phone_tiers(self, utterance):
2649
+ if utterance.id != self.item.id:
2650
+ return
2651
+ self.phoneTiersChanged.emit(utterance)
2652
+
2108
2653
  def update_transcription_highlight(self):
2109
2654
  if self.item.normalized_text and self.item.transcription_text:
2110
2655
  alignment = pairwise2.align.globalms(
@@ -2131,6 +2676,8 @@ class UtteranceRegion(MfaRegion):
2131
2676
 
2132
2677
  @property
2133
2678
  def painter_path_cache(self):
2679
+ if self.parentItem() is None:
2680
+ return self._painter_path_cache
2134
2681
  return self.parentItem().painter_path_cache
2135
2682
 
2136
2683
  def update_edit_fields(self):
@@ -2140,6 +2687,7 @@ class UtteranceRegion(MfaRegion):
2140
2687
  self.normalized_text.text_item.update_times(begin, end)
2141
2688
  if self.transcription_text is not None:
2142
2689
  self.transcription_text.text_item.update_times(begin, end)
2690
+ self.phoneTiersChanged.emit(self.item)
2143
2691
 
2144
2692
  def change_editing(self, editable: bool):
2145
2693
  self.lines[0].movable = editable
@@ -2390,6 +2938,126 @@ class UtteranceRegion(MfaRegion):
2390
2938
  self.text_edit.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter)
2391
2939
  self.update()
2392
2940
 
2941
+ def change_phone_boundaries(
2942
+ self, first_phone_interval, second_phone_interval, new_time: float
2943
+ ):
2944
+ self.phoneBoundaryChanged.emit(
2945
+ self.item, first_phone_interval, second_phone_interval, new_time
2946
+ )
2947
+ if first_phone_interval.word_interval_id != second_phone_interval.word_interval_id:
2948
+ self.wordBoundariesChanged.emit(first_phone_interval.word_interval_id, new_time)
2949
+
2950
+ def change_phone_interval(self, phone_interval, new_phone_id):
2951
+ self.phoneIntervalChanged.emit(self.item, phone_interval, new_phone_id)
2952
+
2953
+ def change_word(self, word_interval, word):
2954
+ self.wordChanged.emit(self.item, word_interval, word)
2955
+
2956
+ def change_word_pronunciation(self, word_interval, pronunciation):
2957
+ self.wordPronunciationChanged.emit(self.item, word_interval, pronunciation)
2958
+
2959
+ def delete_phone_interval(self, interval, previous_interval, following_interval, time_point):
2960
+ self.phoneIntervalDeleted.emit(
2961
+ self.item, interval, previous_interval, following_interval, time_point
2962
+ )
2963
+ if (
2964
+ previous_interval is None
2965
+ or following_interval is None
2966
+ or previous_interval.word_interval_id != following_interval.word_interval_id
2967
+ ):
2968
+ self.wordBoundariesChanged.emit(previous_interval.word_interval_id, time_point)
2969
+
2970
+ def insert_phone_interval(
2971
+ self,
2972
+ previous_interval,
2973
+ following_interval,
2974
+ word_interval_id,
2975
+ begin=None,
2976
+ end=None,
2977
+ lookup="phone_intervals",
2978
+ ):
2979
+ if begin is None:
2980
+ begin = (
2981
+ (previous_interval.begin + previous_interval.end) / 2
2982
+ if previous_interval is not None
2983
+ else self.item.begin
2984
+ )
2985
+ if end is None:
2986
+ end = (
2987
+ (following_interval.begin + following_interval.end) / 2
2988
+ if following_interval is not None
2989
+ else self.item.end
2990
+ )
2991
+ inserting_word_interval = word_interval_id is None
2992
+ word_interval = None
2993
+ if inserting_word_interval:
2994
+ previous_word_interval_id = (
2995
+ previous_interval.word_interval_id if previous_interval is not None else None
2996
+ )
2997
+ following_word_interval_id = (
2998
+ following_interval.word_interval_id if following_interval is not None else None
2999
+ )
3000
+ at_word_boundary = previous_word_interval_id != following_word_interval_id
3001
+ if not at_word_boundary:
3002
+ return
3003
+ if not lookup.startswith("reference"):
3004
+ word_interval_id = self.corpus_model.corpus.get_next_primary_key(WordInterval)
3005
+ word = self.corpus_model.corpus.session.get(Word, 1)
3006
+ word_interval = WordInterval(
3007
+ id=word_interval_id,
3008
+ word=word,
3009
+ begin=begin,
3010
+ end=end,
3011
+ )
3012
+ else:
3013
+ word_interval_id = self.corpus_model.corpus.get_next_primary_key(
3014
+ ReferenceWordInterval
3015
+ )
3016
+ word = self.corpus_model.corpus.session.get(Word, 1)
3017
+ word_interval = ReferenceWordInterval(
3018
+ id=word_interval_id,
3019
+ word=word,
3020
+ begin=begin,
3021
+ end=end,
3022
+ )
3023
+ elif not lookup.startswith("reference"):
3024
+ for x in self.item.word_intervals:
3025
+ if x.id == word_interval_id:
3026
+ word_interval = x
3027
+ break
3028
+ else:
3029
+ for x in self.item.reference_word_intervals:
3030
+ if x.id == word_interval_id:
3031
+ word_interval = x
3032
+ break
3033
+ if not lookup.startswith("reference"):
3034
+ next_pk = self.corpus_model.corpus.get_next_primary_key(PhoneInterval)
3035
+ phone_interval = PhoneInterval(
3036
+ id=next_pk,
3037
+ phone=self.corpus_model.phones["sil"],
3038
+ begin=begin,
3039
+ end=end,
3040
+ word_interval=word_interval,
3041
+ word_interval_id=word_interval_id,
3042
+ )
3043
+ else:
3044
+ next_pk = self.corpus_model.corpus.get_next_primary_key(ReferencePhoneInterval)
3045
+ phone_interval = ReferencePhoneInterval(
3046
+ id=next_pk,
3047
+ phone=self.corpus_model.phones["sil"],
3048
+ begin=begin,
3049
+ end=end,
3050
+ word_interval=word_interval,
3051
+ word_interval_id=word_interval_id,
3052
+ )
3053
+ self.phoneIntervalInserted.emit(
3054
+ self.item,
3055
+ phone_interval,
3056
+ previous_interval,
3057
+ following_interval,
3058
+ word_interval if inserting_word_interval else None,
3059
+ )
3060
+
2393
3061
  def save_changes(self):
2394
3062
  text = self.text_edit.toPlainText()
2395
3063
  if self.original_text == text:
@@ -2830,7 +3498,7 @@ class SpeakerTier(pg.GraphicsObject):
2830
3498
  self.selection_model.selectionChanged.connect(self.update_select)
2831
3499
  self.selection_model.model().utterancesReady.connect(self.refresh)
2832
3500
  self.available_speakers = {}
2833
- self.painter_path_cache = {True: {}, False: {}}
3501
+ self.painter_path_cache = {}
2834
3502
 
2835
3503
  def wheelEvent(self, ev):
2836
3504
  self.receivedWheelEvent.emit(ev)
@@ -2953,10 +3621,8 @@ class SpeakerTier(pg.GraphicsObject):
2953
3621
  )
2954
3622
  reg.sigRegionChanged.connect(self.check_utterance_bounds)
2955
3623
  reg.sigRegionChangeFinished.connect(self.update_utterance)
2956
- reg.lines[0].sigPositionChanged.connect(self.draggingLine.emit)
2957
- reg.lines[0].sigPositionChangeFinished.connect(self.lineDragFinished.emit)
2958
- reg.lines[1].sigPositionChanged.connect(self.draggingLine.emit)
2959
- reg.lines[0].sigPositionChangeFinished.connect(self.lineDragFinished.emit)
3624
+ reg.draggingLine.connect(self.draggingLine.emit)
3625
+ reg.sigRegionChangeFinished.connect(self.lineDragFinished.emit)
2960
3626
  reg.undoRequested.connect(self.corpus_model.undoRequested.emit)
2961
3627
  reg.undoRequested.connect(self.corpus_model.undoRequested.emit)
2962
3628
  reg.redoRequested.connect(self.corpus_model.redoRequested.emit)
@@ -2964,12 +3630,58 @@ class SpeakerTier(pg.GraphicsObject):
2964
3630
  reg.audioSelected.connect(self.selection_model.select_audio)
2965
3631
  reg.viewRequested.connect(self.selection_model.set_view_times)
2966
3632
  reg.textEdited.connect(self.update_utterance_text)
3633
+ reg.phoneBoundaryChanged.connect(self.update_phone_boundaries)
3634
+ reg.phoneIntervalChanged.connect(self.update_phone_interval)
3635
+ reg.wordPronunciationChanged.connect(self.update_word_pronunciation)
3636
+ reg.wordChanged.connect(self.update_word)
3637
+ reg.phoneIntervalInserted.connect(self.insert_phone_interval)
3638
+ reg.phoneIntervalDeleted.connect(self.delete_phone_interval)
3639
+ reg.deleteReferenceAlignments.connect(self.delete_reference_alignments)
2967
3640
  reg.transcribeRequested.connect(self.corpus_model.transcribeRequested.emit)
2968
3641
  reg.selectRequested.connect(self.selection_model.update_select)
2969
3642
  self.visible_utterances[u.id] = reg
2970
3643
 
2971
3644
  self.show()
2972
3645
 
3646
+ def delete_reference_alignments(self, utterance: Utterance):
3647
+ self.selection_model.model().delete_reference_alignments(utterance)
3648
+
3649
+ def update_phone_boundaries(
3650
+ self, utterance: Utterance, first_phone_interval, second_phone_interval, new_time: float
3651
+ ):
3652
+ self.selection_model.model().update_phone_boundaries(
3653
+ utterance, first_phone_interval, second_phone_interval, new_time
3654
+ )
3655
+
3656
+ def update_phone_interval(self, utterance: Utterance, phone_interval, phone_id):
3657
+ self.selection_model.model().update_phone_interval(utterance, phone_interval, phone_id)
3658
+
3659
+ def update_word_pronunciation(
3660
+ self, utterance: Utterance, word_interval: WordInterval, pronunciation: Pronunciation
3661
+ ):
3662
+ self.selection_model.model().update_word_pronunciation(
3663
+ utterance, word_interval, pronunciation
3664
+ )
3665
+
3666
+ def update_word(
3667
+ self, utterance: Utterance, word_interval: WordInterval, word: typing.Union[Word, str]
3668
+ ):
3669
+ self.selection_model.model().update_word(utterance, word_interval, word)
3670
+
3671
+ def insert_phone_interval(
3672
+ self, utterance: Utterance, interval, previous_interval, following_interval, word_interval
3673
+ ):
3674
+ self.selection_model.model().insert_phone_interval(
3675
+ utterance, interval, previous_interval, following_interval, word_interval
3676
+ )
3677
+
3678
+ def delete_phone_interval(
3679
+ self, utterance: Utterance, interval, previous_interval, following_interval, time_point
3680
+ ):
3681
+ self.selection_model.model().delete_phone_interval(
3682
+ utterance, interval, previous_interval, following_interval, time_point
3683
+ )
3684
+
2973
3685
  def update_utterance_text(self, utterance, new_text):
2974
3686
  self.selection_model.model().update_utterance_text(utterance, text=new_text)
2975
3687
 
@@ -2982,7 +3694,7 @@ class SpeakerTier(pg.GraphicsObject):
2982
3694
  r.setSelected(False)
2983
3695
 
2984
3696
  def check_utterance_bounds(self):
2985
- reg = self.sender()
3697
+ reg: UtteranceRegion = self.sender()
2986
3698
  with QtCore.QSignalBlocker(reg):
2987
3699
  beg, end = reg.getRegion()
2988
3700
  if self.settings.right_to_left:
@@ -3005,16 +3717,35 @@ class SpeakerTier(pg.GraphicsObject):
3005
3717
  ):
3006
3718
  reg.setRegion([beg, self.selection_model.model().file.duration])
3007
3719
  return
3008
- for r in self.visible_utterances.values():
3720
+ prev_r = None
3721
+ for r in sorted(self.visible_utterances.values(), key=lambda x: x.item_min):
3009
3722
  if r.item.id == reg.item.id:
3723
+ if reg.initial_line_moving and reg.snap_mode and prev_r is not None:
3724
+ other_begin, other_end = prev_r.getRegion()
3725
+ prev_r.setRegion([other_begin, beg])
3726
+ break
3010
3727
  continue
3011
3728
  other_begin, other_end = r.getRegion()
3012
3729
  if other_begin <= beg < other_end or beg <= other_begin < other_end < end:
3013
- reg.setRegion([other_end, end])
3730
+ if reg.initial_line_moving and reg.snap_mode:
3731
+ r.setRegion([other_begin, beg])
3732
+ else:
3733
+ reg.setRegion([other_end, end])
3014
3734
  break
3015
3735
  if other_begin < end <= other_end or end > other_begin > other_end > beg:
3016
- reg.setRegion([beg, other_begin])
3736
+ if (
3737
+ False
3738
+ and not reg.initial_line_moving
3739
+ and reg.snap_mode
3740
+ and prev_r is not None
3741
+ and prev_r.item.id == reg.item.id
3742
+ ):
3743
+ r.setRegion([end, other_end])
3744
+ else:
3745
+ reg.setRegion([beg, other_begin])
3017
3746
  break
3747
+ prev_r = r
3748
+
3018
3749
  reg.update_edit_fields()
3019
3750
  reg.select_self()
3020
3751
  reg.update()