Game line names across languages; RSubtitle; editor in three columns

This commit is contained in:
2026-10-10 02:14:06 +02:00
parent f29af40b37
commit ea3e44000a
40 changed files with 54018 additions and 53559 deletions

View File

@@ -4,7 +4,7 @@ import os
import numpy as np
from PySide6.QtCore import Qt, QTimer, Signal
from PySide6.QtGui import QFont
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDoubleSpinBox, QFileDialog, QGridLayout, QGroupBox,
from PySide6.QtWidgets import (QCheckBox, QComboBox, QDoubleSpinBox, QFileDialog, QGridLayout, QGroupBox, QSplitter,
QHBoxLayout, QLabel, QMessageBox, QProgressBar, QPushButton, QSlider,
QVBoxLayout, QWidget)
@@ -60,9 +60,11 @@ class EditorPanel(QWidget):
return w
def _build(self):
"""Header on top; below, side by side (resizable): LISTEN | REPLACE (with the apply buttons) | SUBTITLE."""
self.setObjectName("EditorPanel")
lay = QVBoxLayout(self)
lay.setContentsMargins(10, 8, 10, 8)
lay.setContentsMargins(10, 6, 10, 6)
lay.setSpacing(4)
self.title = QLabel()
self.title.setObjectName("EditorTitle")
self.title.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
@@ -71,18 +73,21 @@ class EditorPanel(QWidget):
f.setBold(True)
f.setLetterSpacing(QFont.SpacingType.PercentageSpacing, 104)
self.title.setFont(f)
lay.addWidget(self.title)
self.state = QLabel()
self.state.setWordWrap(True)
head = QHBoxLayout()
head.addWidget(self.title, 1)
head.addWidget(self.state, 0, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
lay.addLayout(head)
self.meta = QLabel()
self.meta.setWordWrap(True)
self.meta.setObjectName("Muted")
self.state = QLabel()
self.state.setWordWrap(True)
lay.addWidget(self.meta)
lay.addWidget(self.state)
# listen ----------------------------------------------------------------------------------
g = QGroupBox("LISTEN")
gl = QGridLayout(g)
gl.setVerticalSpacing(4)
self.wf_orig = self._wave("orig")
self.wf_inst = self._wave("inst")
self.b_play_orig = self._btn("Original", "play", lambda: self.play("orig"), "Play the original (Space)", icons.STEEL)
@@ -92,8 +97,6 @@ class EditorPanel(QWidget):
icons.ACCENT)
self.cb_loop = self._btn("Loop", "loop", None, "Repeat playback")
self.cb_loop.setCheckable(True)
self.vol_icon = QLabel()
self.vol_icon.setPixmap(icons.icon("play", icons.MUTED).pixmap(14, 14))
self.sl_vol = QSlider(Qt.Orientation.Horizontal)
self.sl_vol.setRange(0, 100)
self.sl_vol.setValue(80)
@@ -103,39 +106,46 @@ class EditorPanel(QWidget):
self.lbl_time = QLabel("0:00.00")
self.lbl_time.setObjectName("Muted")
gl.addWidget(QLabel("Original"), 0, 0)
gl.addWidget(self.wf_orig, 0, 1, 1, 6)
gl.addWidget(self.wf_orig, 0, 1)
self.lbl_inst = QLabel("Installed")
gl.addWidget(self.lbl_inst, 1, 0)
gl.addWidget(self.wf_inst, 1, 1, 1, 6)
gl.addWidget(self.wf_inst, 1, 1)
row = QHBoxLayout()
for w in (self.b_play_orig, self.b_play_inst, self.b_stop, self.b_ab, self.cb_loop, QLabel(" Volume"),
self.sl_vol, self.lbl_time):
for w in (self.b_play_orig, self.b_play_inst, self.b_stop):
row.addWidget(w)
row.addStretch(1)
gl.addLayout(row, 2, 0, 1, 7)
gl.addLayout(row, 2, 0, 1, 2)
row = QHBoxLayout()
for w in (self.b_ab, self.cb_loop, QLabel("Volume"), self.sl_vol, self.lbl_time):
row.addWidget(w)
row.addStretch(1)
gl.addLayout(row, 3, 0, 1, 2)
gl.setColumnStretch(1, 1)
lay.addWidget(g)
gl.setRowStretch(4, 1)
self.wf_orig.seekRequested.connect(lambda t: self.play("orig", t))
self.wf_inst.seekRequested.connect(lambda t: self.play("inst", t))
listen = g
# replace ---------------------------------------------------------------------------------
g = QGroupBox("REPLACE")
rl = QVBoxLayout(g)
rl.setSpacing(4)
src = QHBoxLayout()
self.b_load = self._btn("Load file…", "open", self.load_file_dialog,
"WAV, FLAC, OGG, OPUS, MP3, AIFF (M4A/WMA need ffmpeg installed) (Ctrl+O)")
self.cmb_in = QComboBox()
self.cmb_in.setMinimumWidth(180)
self.cmb_in.setMinimumWidth(110)
self.cmb_in.setMaximumWidth(200)
self.cmb_in.setToolTip("Recording device")
self.b_rec = self._btn("Record", "record", self.toggle_record, "Record from the selected input")
self.meter = QProgressBar()
self.meter.setRange(0, 100)
self.meter.setTextVisible(False)
self.meter.setMaximumWidth(120)
self.meter.setMaximumWidth(60)
self.meter.setToolTip("Input level")
self.lbl_rec = QLabel("")
self.lbl_rec.setObjectName("Muted")
for w in (self.b_load, QLabel(" or"), self.cmb_in, self.b_rec, self.meter, self.lbl_rec):
for w in (self.b_load, self.cmb_in, self.b_rec, self.meter, self.lbl_rec):
src.addWidget(w)
src.addStretch(1)
rl.addLayout(src)
@@ -144,71 +154,82 @@ class EditorPanel(QWidget):
self.wf_new.seekRequested.connect(lambda t: self.play("new", t))
self.wf_new.selectionChanged.connect(self._selection)
rl.addWidget(self.wf_new)
proc = QHBoxLayout()
self.sp_a = QDoubleSpinBox()
self.sp_b = QDoubleSpinBox()
for s_ in (self.sp_a, self.sp_b):
s_.setDecimals(2)
s_.setSuffix(" s")
s_.setRange(0, 36000)
s_.setMaximumWidth(92)
s_.valueChanged.connect(self._changed)
self.sp_gain = QDoubleSpinBox()
self.sp_gain.setRange(-40, 40)
self.sp_gain.setSuffix(" dB")
self.sp_gain.setMaximumWidth(92)
self.sp_gain.valueChanged.connect(self._changed)
self.cb_norm = QCheckBox("Normalize")
self.cb_match = QCheckBox("Match original loudness")
self.cb_match = QCheckBox("Match loudness")
self.cb_match.setToolTip("Match the loudness of the original sound")
self.cb_norm.toggled.connect(self._changed)
self.cb_match.toggled.connect(self._changed)
self.b_silence = self._btn("Trim silence", None, self.trim_silence, "Cut silence at the start and end")
self.b_play_new = self._btn("Replacement", "play", lambda: self.play("new"), "Play the processed replacement",
icons.ACCENT)
for w in (QLabel("Keep from"), self.sp_a, QLabel("to"), self.sp_b, self.b_silence, QLabel(" Gain"),
self.sp_gain, self.cb_norm, self.cb_match, self.b_play_new):
proc = QHBoxLayout()
for w in (QLabel("Keep"), self.sp_a, QLabel("to"), self.sp_b, self.b_silence, self.b_play_new):
proc.addWidget(w)
proc.addStretch(1)
rl.addLayout(proc)
proc = QHBoxLayout()
for w in (QLabel("Gain"), self.sp_gain, self.cb_norm, self.cb_match):
proc.addWidget(w)
proc.addStretch(1)
rl.addLayout(proc)
info = QHBoxLayout()
self.lbl_new = QLabel()
self.lbl_new.setWordWrap(True)
self.lbl_new.setObjectName("Muted")
self.lbl_new.setWordWrap(True)
self.warn_icon = QLabel()
self.warn_icon.setPixmap(icons.icon("warning", icons.ACCENT).pixmap(18, 18))
self.warn_icon.hide()
self.lbl_warn = QLabel()
self.lbl_warn.setWordWrap(True)
self.lbl_warn.setStyleSheet("color: %s;" % icons.ACCENT)
self.lbl_new.setWordWrap(False)
info.addWidget(self.lbl_new, 0, Qt.AlignmentFlag.AlignTop)
info.addSpacing(12)
info.addWidget(self.lbl_new, 1, Qt.AlignmentFlag.AlignTop)
info.addWidget(self.warn_icon, 0, Qt.AlignmentFlag.AlignTop)
info.addWidget(self.lbl_warn, 1)
rl.addLayout(info)
lay.addWidget(g)
# apply -----------------------------------------------------------------------------------
# apply (belongs to the replacement) ------------------------------------------------------
act = QHBoxLayout()
self.b_apply = self._btn("Apply to game", "apply", self.apply,
"Encode the replacement and install it in the game files (Ctrl+S)", "#ffffff")
self.b_apply.setObjectName("Primary")
self.b_revert = self._btn("Revert to original", "revert", self.revert, "Put the game's original back")
self.b_export = self._btn("Save as WAV…", "save", self.export_wav,
self.b_revert = self._btn("Revert", "revert", self.revert, "Put the game's original back")
self.b_export = self._btn("Save WAV…", "save", self.export_wav,
"Save the original (or the replacement) as a WAV file")
self.busy = Spinner(16)
self.lbl_busy = QLabel()
for w in (self.b_apply, self.b_revert, self.b_export, self.busy, self.lbl_busy):
act.addWidget(w)
act.addStretch(1)
lay.addLayout(act)
rl.addStretch(1)
rl.addLayout(act)
replace = g
# subtitle of this sound (edited side by side with the audio) -------------------------------
from .textpanel import SubtitleBox
gs = QGroupBox("Subtitle")
gs = QGroupBox("SUBTITLE")
gsl = QVBoxLayout(gs)
self.subtitle = SubtitleBox(self.ctx, gs)
gsl.addWidget(self.subtitle)
lay.addWidget(gs)
lay.addStretch(1)
gsl.addStretch(1)
self.columns = QSplitter(Qt.Orientation.Horizontal)
self.columns.setChildrenCollapsible(False)
for w, stretch in ((listen, 3), (replace, 4), (gs, 3)):
self.columns.addWidget(w)
self.columns.setStretchFactor(self.columns.count() - 1, stretch)
lay.addWidget(self.columns, 1)
self.recorder.level.connect(lambda v: self.meter.setValue(int(min(1.0, v) * 100)))
self.recorder.elapsed.connect(lambda t: self.lbl_rec.setText("recording %s" % fmt_s(t)))

View File

@@ -209,8 +209,14 @@ class MainWindow(QMainWindow):
scroll.setWidget(self.editor)
scroll.setWidgetResizable(True)
# sidebar | sound list on top, the editor below across the whole width
hs = QSplitter(Qt.Orientation.Horizontal)
hs.addWidget(side)
hs.addWidget(self.table)
hs.setStretchFactor(1, 1)
hs.setSizes([SIDEBAR_MAX, 100000])
self.vsplit = QSplitter(Qt.Orientation.Vertical)
self.vsplit.addWidget(self.table)
self.vsplit.addWidget(hs)
self.vsplit.addWidget(scroll)
self.vsplit.setStretchFactor(0, 1) # extra height goes to the list; the editor keeps its fitted height
self.vsplit.setStretchFactor(1, 0)
@@ -219,15 +225,10 @@ class MainWindow(QMainWindow):
self._user_sized = False # once the divider is dragged, keep the user's choice
self.vsplit.splitterMoved.connect(lambda *_: setattr(self, "_user_sized", True))
self.editor.installEventFilter(self)
hs = QSplitter(Qt.Orientation.Horizontal)
hs.addWidget(side)
hs.addWidget(self.vsplit)
hs.setStretchFactor(1, 1)
hs.setSizes([SIDEBAR_MAX, 100000])
# sections: sounds, subtitles, menu text
self.tabs = QTabWidget()
self.tabs.setDocumentMode(True)
self.tabs.addTab(hs, icons.icon("wave"), "Sounds")
self.tabs.addTab(self.vsplit, icons.icon("wave"), "Sounds")
self.subs = TextPanel(self, "subs")
self.menu = TextPanel(self, "menu")
self.tabs.addTab(self.subs, icons.icon("ab"), "Subtitles")

View File

@@ -143,18 +143,25 @@ class SubtitleBox(QWidget):
self.lbl = QLabel()
self.lbl.setObjectName("Muted")
row = QHBoxLayout()
self.b_apply = QPushButton(icons.icon("apply"), "Apply subtitle")
self.b_revert = QPushButton(icons.icon("revert"), "Revert subtitle")
self.b_open = QPushButton(icons.icon("search"), "Show in Subtitles")
self.b_apply = QPushButton(icons.icon("apply"), "Apply")
self.b_revert = QPushButton(icons.icon("revert"), "Revert")
self.b_open = QPushButton(icons.icon("search"), "Show in list")
self.b_apply.setToolTip("Write this subtitle into the game (every copy)")
self.b_revert.setToolTip("Put the original subtitle back")
self.b_open.setToolTip("Open this subtitle in the Subtitles tab")
self.b_apply.clicked.connect(self.apply)
self.b_revert.clicked.connect(self.revert)
self.b_open.clicked.connect(lambda: self.keys and self.open_in_list.emit(self.keys[0]))
for b in (self.b_apply, self.b_revert, self.b_open):
for b in (self.b_apply, self.b_revert):
row.addWidget(b)
row.addStretch(1)
self.row = row
row2 = QHBoxLayout()
row2.addWidget(self.b_open)
row2.addStretch(1)
self.lbl.setWordWrap(True)
self.lay.addRow(self.lbl)
self.lay.addRow(row)
self.lay.addRow(row2)
def set_sound(self, s):
for e in self.edits:
@@ -171,6 +178,8 @@ class SubtitleBox(QWidget):
for i, ln in enumerate(lines):
k = subtitles.key(ln.lang, ln.speech, ln.line)
e = QLineEdit(tx.records().get(k, {}).get("applied") or ln.text)
e.setMinimumWidth(80)
e.setCursorPosition(0) # long subtitles: show their beginning
e.setToolTip("Original: %s\nRoom: %d characters" % (ln.text, ln.room))
e.textChanged.connect(self._check)
self.lay.insertRow(i, "Line %d" % (i + 1) if len(lines) > 1 else "Subtitle", e)