File indexing completed on 2024-04-21 04:51:25
0001 /* 0002 SPDX-FileCopyrightText: 2020 Jean-Baptiste Mardelle 0003 SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0004 */ 0005 0006 #include "textbasededit.h" 0007 #include "bin/bin.h" 0008 #include "bin/projectclip.h" 0009 #include "bin/projectitemmodel.h" 0010 #include "bin/projectsubclip.h" 0011 #include "core.h" 0012 #include "kdenlivesettings.h" 0013 #include "mainwindow.h" 0014 #include "monitor/monitor.h" 0015 #include "timeline2/view/timelinecontroller.h" 0016 #include "timeline2/view/timelinewidget.h" 0017 #include "widgets/timecodedisplay.h" 0018 #include <profiles/profilemodel.hpp> 0019 0020 #include "utils/KMessageBox_KdenliveCompat.h" 0021 #include <KLocalizedString> 0022 #include <KMessageBox> 0023 #include <KUrlRequesterDialog> 0024 #include <QAbstractTextDocumentLayout> 0025 #include <QEvent> 0026 #include <QFontDatabase> 0027 #include <QJsonArray> 0028 #include <QJsonObject> 0029 #include <QJsonParseError> 0030 #include <QKeyEvent> 0031 #include <QMenu> 0032 #include <QPainter> 0033 #include <QScrollBar> 0034 #include <QTextBlock> 0035 #include <QTextDocumentFragment> 0036 #include <QToolButton> 0037 0038 #include <memory> 0039 0040 VideoTextEdit::VideoTextEdit(QWidget *parent) 0041 : QTextEdit(parent) 0042 { 0043 setMouseTracking(true); 0044 setReadOnly(true); 0045 // setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); 0046 lineNumberArea = new LineNumberArea(this); 0047 connect(this, &VideoTextEdit::cursorPositionChanged, [this]() { lineNumberArea->update(); }); 0048 connect(verticalScrollBar(), &QScrollBar::valueChanged, this, [this]() { lineNumberArea->update(); }); 0049 QRect rect = this->contentsRect(); 0050 setViewportMargins(lineNumberAreaWidth(), 0, 0, 0); 0051 lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height()); 0052 0053 bookmarkAction = new QAction(QIcon::fromTheme(QStringLiteral("bookmark-new")), i18n("Add marker"), this); 0054 bookmarkAction->setEnabled(false); 0055 deleteAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Delete selection"), this); 0056 deleteAction->setEnabled(false); 0057 } 0058 0059 void VideoTextEdit::repaintLines() 0060 { 0061 lineNumberArea->update(); 0062 } 0063 0064 void VideoTextEdit::cleanup() 0065 { 0066 speechZones.clear(); 0067 cutZones.clear(); 0068 m_hoveredBlock = -1; 0069 clear(); 0070 document()->setDefaultStyleSheet(QString("a {text-decoration:none;color:%1}").arg(palette().text().color().name())); 0071 setCurrentFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); 0072 } 0073 0074 const QString VideoTextEdit::selectionStartAnchor(QTextCursor &cursor, int start, int max) 0075 { 0076 if (start == -1) { 0077 start = cursor.selectionStart(); 0078 } 0079 if (max == -1) { 0080 max = cursor.selectionEnd(); 0081 } 0082 cursor.setPosition(start); 0083 cursor.select(QTextCursor::WordUnderCursor); 0084 while (cursor.selectedText().isEmpty() && start < max) { 0085 start++; 0086 cursor.setPosition(start); 0087 cursor.select(QTextCursor::WordUnderCursor); 0088 } 0089 int selStart = cursor.selectionStart(); 0090 int selEnd = cursor.selectionEnd(); 0091 cursor.setPosition(selStart + (selEnd - selStart) / 2); 0092 return anchorAt(cursorRect(cursor).center()); 0093 } 0094 0095 const QString VideoTextEdit::selectionEndAnchor(QTextCursor &cursor, int end, int min) 0096 { 0097 qDebug() << "==== TESTING SELECTION END ANCHOR FROM: " << end << " , MIN: " << min; 0098 if (end == -1) { 0099 end = cursor.selectionEnd(); 0100 } 0101 if (min == -1) { 0102 min = cursor.selectionStart(); 0103 } 0104 cursor.setPosition(end); 0105 cursor.select(QTextCursor::WordUnderCursor); 0106 while (cursor.selectedText().isEmpty() && end > min) { 0107 end--; 0108 cursor.setPosition(end); 0109 cursor.select(QTextCursor::WordUnderCursor); 0110 } 0111 qDebug() << "==== TESTING SELECTION END ANCHOR FROM: " << end << " , WORD: " << cursor.selectedText(); 0112 int selStart = cursor.selectionStart(); 0113 int selEnd = cursor.selectionEnd(); 0114 cursor.setPosition(selStart + (selEnd - selStart) / 2); 0115 qDebug() << "==== END POS SELECTION FOR: " << cursor.selectedText() << " = " << anchorAt(cursorRect(cursor).center()); 0116 QString anch = anchorAt(cursorRect(cursor).center()); 0117 double endMs = anch.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble(); 0118 qDebug() << "==== GOT LAST FRAME: " << GenTime(endMs).frames(25); 0119 return anchorAt(cursorRect(cursor).center()); 0120 } 0121 0122 void VideoTextEdit::processCutZones(const QList<QPoint> &loadZones) 0123 { 0124 // Remove all outside load zones 0125 qDebug() << "=== LOADING CUT ZONES: " << loadZones << "\n........................"; 0126 QTextCursor curs = textCursor(); 0127 curs.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); 0128 qDebug() << "===== GOT DOCUMENT END: " << curs.position(); 0129 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 0130 double fps = pCore->getCurrentFps(); 0131 while (!curs.atEnd()) { 0132 qDebug() << "=== CURSOR POS: " << curs.position(); 0133 QString anchorStart = selectionStartAnchor(curs, curs.position(), document()->characterCount()); 0134 int startPos = GenTime(anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble()).frames(fps); 0135 int endPos = GenTime(anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble()).frames(fps); 0136 bool isInZones = false; 0137 for (auto &p : loadZones) { 0138 if ((startPos >= p.x() && startPos <= p.y()) || (endPos >= p.x() && endPos <= p.y())) { 0139 isInZones = true; 0140 break; 0141 } 0142 } 0143 if (!isInZones) { 0144 // Delete current word 0145 qDebug() << "=== DELETING WORD: " << curs.selectedText(); 0146 curs.select(QTextCursor::WordUnderCursor); 0147 curs.removeSelectedText(); 0148 if (document()->characterAt(curs.position() - 1) == QLatin1Char(' ')) { 0149 // Remove trailing space 0150 curs.deleteChar(); 0151 } else { 0152 if (!curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor)) { 0153 break; 0154 } 0155 } 0156 } else { 0157 curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor); 0158 if (!curs.movePosition(QTextCursor::NextWord, QTextCursor::MoveAnchor)) { 0159 break; 0160 } 0161 qDebug() << "=== WORD INSIDE, POS: " << curs.position(); 0162 } 0163 qDebug() << "=== MOVED CURSOR POS: " << curs.position(); 0164 } 0165 } 0166 0167 void VideoTextEdit::rebuildZones() 0168 { 0169 speechZones.clear(); 0170 m_selectedBlocks.clear(); 0171 QTextCursor curs = textCursor(); 0172 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 0173 for (int i = 0; i < document()->blockCount(); ++i) { 0174 int start = curs.position() + 1; 0175 QString anchorStart = selectionStartAnchor(curs, start, document()->characterCount()); 0176 // qDebug()<<"=== START ANCHOR: "<<anchorStart<<" AT POS: "<<curs.position(); 0177 curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor); 0178 int end = curs.position() - 1; 0179 QString anchorEnd = selectionEndAnchor(curs, end, start); 0180 if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) { 0181 double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble(); 0182 double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble(); 0183 speechZones << QPair<double, double>(startMs, endMs); 0184 } 0185 curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor); 0186 } 0187 repaintLines(); 0188 } 0189 0190 int VideoTextEdit::lineNumberAreaWidth() 0191 { 0192 int space = 3 + fontMetrics().horizontalAdvance(QLatin1Char('9')) * 11; 0193 return space; 0194 } 0195 0196 QVector<QPoint> VideoTextEdit::processedZones(const QVector<QPoint> &sourceZones) 0197 { 0198 if (cutZones.isEmpty()) { 0199 return sourceZones; 0200 } 0201 QVector<QPoint> resultZones; 0202 QVector<QPoint> processingZones = sourceZones; 0203 for (auto &cut : cutZones) { 0204 for (auto &zone : processingZones) { 0205 if (cut.x() > zone.y() || cut.y() < zone.x()) { 0206 // Cut is outside zone, keep it as is 0207 resultZones << zone; 0208 } else if (cut.y() >= zone.y()) { 0209 // Only keep the start of this zone 0210 resultZones << QPoint(zone.x(), cut.x()); 0211 } else if (cut.x() <= zone.x()) { 0212 // Only keep the end of this zone 0213 resultZones << QPoint(cut.y(), zone.y()); 0214 } else { 0215 // Cut is in the middle of this zone 0216 resultZones << QPoint(zone.x(), cut.x()); 0217 resultZones << QPoint(cut.y(), zone.y()); 0218 } 0219 } 0220 processingZones = resultZones; 0221 resultZones.clear(); 0222 } 0223 return processingZones; 0224 } 0225 0226 QVector<QPoint> VideoTextEdit::getInsertZones() 0227 { 0228 if (m_selectedBlocks.isEmpty()) { 0229 // return text selection, not blocks 0230 QTextCursor cursor = textCursor(); 0231 QString anchorStart; 0232 QString anchorEnd; 0233 if (!cursor.selectedText().isEmpty()) { 0234 qDebug() << "=== EXPORTING SELECTION"; 0235 int start = cursor.selectionStart(); 0236 int end = cursor.selectionEnd() - 1; 0237 anchorStart = selectionStartAnchor(cursor, start, end); 0238 anchorEnd = selectionEndAnchor(cursor, end, start); 0239 } else { 0240 // Return full text 0241 cursor.movePosition(QTextCursor::End, QTextCursor::MoveAnchor); 0242 int end = cursor.position() - 1; 0243 cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 0244 int start = cursor.position(); 0245 anchorStart = selectionStartAnchor(cursor, start, end); 0246 anchorEnd = selectionEndAnchor(cursor, end, start); 0247 } 0248 if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) { 0249 double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble(); 0250 double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble(); 0251 qDebug() << "=== GOT EXPORT MAIN ZONE: " << GenTime(startMs).frames(pCore->getCurrentFps()) << " - " 0252 << GenTime(endMs).frames(pCore->getCurrentFps()); 0253 QPoint originalZone(QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps()))); 0254 return processedZones({originalZone}); 0255 } 0256 return {}; 0257 } 0258 QVector<QPoint> zones; 0259 int zoneStart = -1; 0260 int zoneEnd = -1; 0261 int currentEnd = -1; 0262 int currentStart = -1; 0263 qDebug() << "=== FROM BLOCKS: " << m_selectedBlocks; 0264 for (auto &bk : m_selectedBlocks) { 0265 QPair<double, double> z = speechZones.at(bk); 0266 currentStart = GenTime(z.first).frames(pCore->getCurrentFps()); 0267 currentEnd = GenTime(z.second).frames(pCore->getCurrentFps()); 0268 if (zoneStart < 0) { 0269 zoneStart = currentStart; 0270 } else if (currentStart - zoneEnd > 1) { 0271 // Insert last zone 0272 zones << QPoint(zoneStart, zoneEnd); 0273 zoneStart = currentStart; 0274 } 0275 zoneEnd = currentEnd; 0276 } 0277 qDebug() << "=== INSERT LAST: " << currentStart << "-" << currentEnd; 0278 zones << QPoint(currentStart, currentEnd); 0279 0280 qDebug() << "=== GOT RESULTING ZONES: " << zones; 0281 return processedZones(zones); 0282 } 0283 0284 QVector<QPoint> VideoTextEdit::fullExport() 0285 { 0286 // Loop through all blocks 0287 QVector<QPoint> zones; 0288 int currentEnd = -1; 0289 int currentStart = -1; 0290 for (int i = 0; i < document()->blockCount(); ++i) { 0291 QTextBlock block = document()->findBlockByNumber(i); 0292 QTextCursor curs(block); 0293 int start = curs.position() + 1; 0294 QString anchorStart = selectionStartAnchor(curs, start, document()->characterCount()); 0295 // qDebug()<<"=== START ANCHOR: "<<anchorStart<<" AT POS: "<<curs.position(); 0296 curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor); 0297 int end = curs.position() - 1; 0298 QString anchorEnd = selectionEndAnchor(curs, end, start); 0299 if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) { 0300 double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble(); 0301 double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble(); 0302 currentStart = GenTime(startMs).frames(pCore->getCurrentFps()); 0303 currentEnd = GenTime(endMs).frames(pCore->getCurrentFps()); 0304 zones << QPoint(currentStart, currentEnd); 0305 } 0306 } 0307 return processedZones(zones); 0308 } 0309 0310 void VideoTextEdit::slotRemoveSilence() 0311 { 0312 for (int i = 0; i < document()->blockCount(); ++i) { 0313 QTextBlock block = document()->findBlockByNumber(i); 0314 if (block.text() == i18n("No speech")) { 0315 QTextCursor curs(block); 0316 curs.select(QTextCursor::BlockUnderCursor); 0317 curs.removeSelectedText(); 0318 curs.deleteChar(); 0319 i--; 0320 continue; 0321 } 0322 } 0323 rebuildZones(); 0324 } 0325 0326 void VideoTextEdit::updateLineNumberArea(const QRect &rect, int dy) 0327 { 0328 if (dy) 0329 lineNumberArea->scroll(0, dy); 0330 else 0331 lineNumberArea->update(0, rect.y(), lineNumberArea->width(), rect.height()); 0332 } 0333 0334 void VideoTextEdit::resizeEvent(QResizeEvent *e) 0335 { 0336 QTextEdit::resizeEvent(e); 0337 QRect cr = contentsRect(); 0338 lineNumberArea->setGeometry(QRect(cr.left(), cr.top(), lineNumberAreaWidth(), cr.height())); 0339 } 0340 0341 void VideoTextEdit::keyPressEvent(QKeyEvent *e) 0342 { 0343 QTextEdit::keyPressEvent(e); 0344 } 0345 0346 void VideoTextEdit::checkHoverBlock(int yPos) 0347 { 0348 QTextCursor curs = QTextCursor(this->document()); 0349 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 0350 0351 m_hoveredBlock = -1; 0352 for (int i = 0; i < this->document()->blockCount(); ++i) { 0353 QTextBlock block = curs.block(); 0354 QRect r2 = this->document()->documentLayout()->blockBoundingRect(block).translated(0, 0 - (this->verticalScrollBar()->sliderPosition())).toRect(); 0355 if (yPos < r2.x()) { 0356 break; 0357 } 0358 if (yPos > r2.x() && yPos < r2.bottom()) { 0359 m_hoveredBlock = i; 0360 break; 0361 } 0362 curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor); 0363 } 0364 setCursor(m_hoveredBlock == -1 ? Qt::ArrowCursor : Qt::PointingHandCursor); 0365 lineNumberArea->update(); 0366 } 0367 0368 void VideoTextEdit::blockClicked(Qt::KeyboardModifiers modifiers, bool play) 0369 { 0370 if (m_hoveredBlock > -1 && m_hoveredBlock < speechZones.count()) { 0371 if (m_selectedBlocks.contains(m_hoveredBlock)) { 0372 if (modifiers & Qt::ControlModifier) { 0373 // remove from selection on ctrl+click an already selected block 0374 m_selectedBlocks.removeAll(m_hoveredBlock); 0375 } else { 0376 m_selectedBlocks = {m_hoveredBlock}; 0377 lineNumberArea->update(); 0378 } 0379 } else { 0380 // Add to selection 0381 if (modifiers & Qt::ControlModifier) { 0382 m_selectedBlocks << m_hoveredBlock; 0383 } else if (modifiers & Qt::ShiftModifier) { 0384 if (m_lastClickedBlock > -1) { 0385 for (int i = qMin(m_lastClickedBlock, m_hoveredBlock); i <= qMax(m_lastClickedBlock, m_hoveredBlock); i++) { 0386 if (!m_selectedBlocks.contains(i)) { 0387 m_selectedBlocks << i; 0388 } 0389 } 0390 } else { 0391 m_selectedBlocks = {m_hoveredBlock}; 0392 } 0393 } else { 0394 m_selectedBlocks = {m_hoveredBlock}; 0395 } 0396 } 0397 if (m_hoveredBlock >= 0) { 0398 m_lastClickedBlock = m_hoveredBlock; 0399 } 0400 0401 // Find continuous block selection 0402 int startBlock = m_hoveredBlock; 0403 int endBlock = m_hoveredBlock; 0404 while (m_selectedBlocks.contains(startBlock)) { 0405 startBlock--; 0406 } 0407 if (!m_selectedBlocks.contains(startBlock)) { 0408 startBlock++; 0409 } 0410 while (m_selectedBlocks.contains(endBlock)) { 0411 endBlock++; 0412 } 0413 if (!m_selectedBlocks.contains(endBlock)) { 0414 endBlock--; 0415 } 0416 QPair<double, double> zone = {speechZones.at(startBlock).first, speechZones.at(endBlock).second}; 0417 double startMs = zone.first; 0418 double endMs = zone.second; 0419 pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(GenTime(startMs).frames(pCore->getCurrentFps())); 0420 pCore->getMonitor(Kdenlive::ClipMonitor) 0421 ->slotLoadClipZone(QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps()))); 0422 QTextCursor cursor = textCursor(); 0423 cursor.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 0424 cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, m_hoveredBlock); 0425 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 0426 setTextCursor(cursor); 0427 if (play) { 0428 pCore->getMonitor(Kdenlive::ClipMonitor)->slotPlayZone(); 0429 } 0430 } 0431 } 0432 0433 int VideoTextEdit::getFirstVisibleBlockId() 0434 { 0435 // Detect the first block for which bounding rect - once 0436 // translated in absolute coordinates - is contained 0437 // by the editor's text area 0438 0439 // Costly way of doing but since 0440 // "blockBoundingGeometry(...)" doesn't exist 0441 // for "QTextEdit"... 0442 0443 QTextCursor curs = QTextCursor(this->document()); 0444 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 0445 for (int i = 0; i < this->document()->blockCount(); ++i) { 0446 QTextBlock block = curs.block(); 0447 0448 QRect r1 = this->viewport()->geometry(); 0449 QRect r2 = 0450 this->document()->documentLayout()->blockBoundingRect(block).translated(r1.x(), r1.y() - (this->verticalScrollBar()->sliderPosition())).toRect(); 0451 0452 if (r1.contains(r2, true)) { 0453 return i; 0454 } 0455 0456 curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor); 0457 } 0458 return 0; 0459 } 0460 0461 void VideoTextEdit::lineNumberAreaPaintEvent(QPaintEvent *event) 0462 { 0463 this->verticalScrollBar()->setSliderPosition(this->verticalScrollBar()->sliderPosition()); 0464 0465 QPainter painter(lineNumberArea); 0466 painter.fillRect(event->rect(), palette().alternateBase().color()); 0467 int blockNumber = this->getFirstVisibleBlockId(); 0468 0469 QTextBlock block = this->document()->findBlockByNumber(blockNumber); 0470 QTextBlock prev_block = (blockNumber > 0) ? this->document()->findBlockByNumber(blockNumber - 1) : block; 0471 int translate_y = (blockNumber > 0) ? -this->verticalScrollBar()->sliderPosition() : 0; 0472 0473 int top = this->viewport()->geometry().top(); 0474 0475 // Adjust text position according to the previous "non entirely visible" block 0476 // if applicable. Also takes in consideration the document's margin offset. 0477 int additional_margin; 0478 if (blockNumber == 0) 0479 // Simply adjust to document's margin 0480 additional_margin = int(this->document()->documentMargin()) - 1 - this->verticalScrollBar()->sliderPosition(); 0481 else 0482 // Getting the height of the visible part of the previous "non entirely visible" block 0483 additional_margin = int( 0484 this->document()->documentLayout()->blockBoundingRect(prev_block).translated(0, translate_y).intersected(this->viewport()->geometry()).height()); 0485 0486 // Shift the starting point 0487 top += additional_margin; 0488 0489 int bottom = top + int(this->document()->documentLayout()->blockBoundingRect(block).height()); 0490 0491 QColor col_2 = palette().link().color(); 0492 QColor col_1 = palette().highlightedText().color(); 0493 QColor col_0 = palette().text().color(); 0494 0495 // Draw the numbers (displaying the current line number in green) 0496 while (block.isValid() && top <= event->rect().bottom()) { 0497 if (blockNumber >= speechZones.count()) { 0498 break; 0499 } 0500 if (block.isVisible() && bottom >= event->rect().top()) { 0501 if (m_selectedBlocks.contains(blockNumber)) { 0502 painter.fillRect(QRect(0, top, lineNumberArea->width(), bottom - top), palette().highlight().color()); 0503 painter.setPen(col_1); 0504 } else { 0505 painter.setPen((this->textCursor().blockNumber() == blockNumber) ? col_2 : col_0); 0506 } 0507 QString number = pCore->timecode().getDisplayTimecode(GenTime(speechZones[blockNumber].first), false); 0508 painter.drawText(-5, top, lineNumberArea->width(), fontMetrics().height(), Qt::AlignRight, number); 0509 } 0510 painter.setPen(palette().dark().color()); 0511 painter.drawLine(0, bottom, width(), bottom); 0512 block = block.next(); 0513 top = bottom; 0514 bottom = top + int(this->document()->documentLayout()->blockBoundingRect(block).height()); 0515 ++blockNumber; 0516 } 0517 } 0518 0519 void VideoTextEdit::contextMenuEvent(QContextMenuEvent *event) 0520 { 0521 QMenu *menu = createStandardContextMenu(); 0522 menu->addAction(bookmarkAction); 0523 menu->addAction(deleteAction); 0524 menu->exec(event->globalPos()); 0525 delete menu; 0526 } 0527 0528 void VideoTextEdit::mousePressEvent(QMouseEvent *e) 0529 { 0530 if (e->buttons() & Qt::LeftButton) { 0531 QTextCursor current = textCursor(); 0532 QTextCursor cursor = cursorForPosition(e->pos()); 0533 int pos = cursor.position(); 0534 qDebug() << "=== CLICKED AT: " << pos << ", SEL: " << current.selectionStart() << "-" << current.selectionEnd(); 0535 if (pos > current.selectionStart() && pos < current.selectionEnd()) { 0536 // Clicked in selection 0537 e->ignore(); 0538 qDebug() << "=== IGNORING MOUSE CLICK"; 0539 return; 0540 } else { 0541 QTextEdit::mousePressEvent(e); 0542 const QString link = anchorAt(e->pos()); 0543 if (!link.isEmpty()) { 0544 // Clicked on a word 0545 cursor.setPosition(pos + 1, QTextCursor::KeepAnchor); 0546 double startMs = link.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble(); 0547 pCore->getMonitor(Kdenlive::ClipMonitor)->requestSeek(GenTime(startMs).frames(pCore->getCurrentFps())); 0548 } 0549 } 0550 setTextCursor(cursor); 0551 } else { 0552 QTextEdit::mousePressEvent(e); 0553 } 0554 } 0555 0556 void VideoTextEdit::mouseReleaseEvent(QMouseEvent *e) 0557 { 0558 QTextEdit::mouseReleaseEvent(e); 0559 if (e->button() == Qt::LeftButton) { 0560 QTextCursor cursor = textCursor(); 0561 if (!cursor.selectedText().isEmpty()) { 0562 // We have a selection, ensure full word is selected 0563 int start = cursor.selectionStart(); 0564 int end = cursor.selectionEnd(); 0565 if (document()->characterAt(end - 1) == QLatin1Char(' ')) { 0566 // Selection ends with a space 0567 end--; 0568 } 0569 QTextBlock bk = cursor.block(); 0570 if (bk.text().simplified() == i18n("No speech")) { 0571 // This is a silence block, select all 0572 cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::MoveAnchor); 0573 cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); 0574 } else { 0575 cursor.setPosition(start); 0576 cursor.movePosition(QTextCursor::StartOfWord, QTextCursor::MoveAnchor); 0577 cursor.setPosition(end, QTextCursor::KeepAnchor); 0578 cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); 0579 } 0580 setTextCursor(cursor); 0581 } 0582 if (!m_selectedBlocks.isEmpty()) { 0583 m_selectedBlocks.clear(); 0584 repaintLines(); 0585 } 0586 } else { 0587 qDebug() << "==== NO LEFT CLICK!"; 0588 } 0589 } 0590 0591 void VideoTextEdit::mouseMoveEvent(QMouseEvent *e) 0592 { 0593 QTextEdit::mouseMoveEvent(e); 0594 if (e->buttons() & Qt::LeftButton) { 0595 /*QTextCursor cursor = textCursor(); 0596 cursor.movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); 0597 setTextCursor(cursor);*/ 0598 } else { 0599 const QString link = anchorAt(e->pos()); 0600 viewport()->setCursor(link.isEmpty() ? Qt::ArrowCursor : Qt::PointingHandCursor); 0601 } 0602 } 0603 0604 void VideoTextEdit::wheelEvent(QWheelEvent *e) 0605 { 0606 QTextEdit::wheelEvent(e); 0607 } 0608 0609 TextBasedEdit::TextBasedEdit(QWidget *parent) 0610 : QWidget(parent) 0611 , m_stt(nullptr) 0612 { 0613 setFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); 0614 setupUi(this); 0615 setFocusPolicy(Qt::StrongFocus); 0616 connect(pCore.get(), &Core::speechEngineChanged, this, &TextBasedEdit::updateEngine); 0617 0618 // Settings menu 0619 QMenu *menu = new QMenu(this); 0620 m_translateAction = new QAction(i18n("Translate to English"), this); 0621 m_translateAction->setCheckable(true); 0622 menu->addAction(m_translateAction); 0623 QAction *configAction = new QAction(i18n("Configure Speech Recognition"), this); 0624 menu->addAction(configAction); 0625 button_config->setMenu(menu); 0626 button_config->setIcon(QIcon::fromTheme(QStringLiteral("application-menu"))); 0627 connect(m_translateAction, &QAction::triggered, [](bool enabled) { KdenliveSettings::setWhisperTranslate(enabled); }); 0628 connect(configAction, &QAction::triggered, []() { pCore->window()->slotShowPreferencePage(Kdenlive::PageSpeech); }); 0629 connect(menu, &QMenu::aboutToShow, [this]() { 0630 m_translateAction->setChecked(KdenliveSettings::whisperTranslate()); 0631 m_translateAction->setEnabled(KdenliveSettings::speechEngine() == QLatin1String("whisper")); 0632 }); 0633 0634 m_voskConfig = new QAction(i18n("Configure"), this); 0635 connect(m_voskConfig, &QAction::triggered, []() { pCore->window()->slotShowPreferencePage(Kdenlive::PageSpeech); }); 0636 0637 // Visual text editor 0638 auto *l = new QVBoxLayout; 0639 l->setContentsMargins(0, 0, 0, 0); 0640 m_visualEditor = new VideoTextEdit(this); 0641 m_visualEditor->installEventFilter(this); 0642 l->addWidget(m_visualEditor); 0643 text_frame->setLayout(l); 0644 m_document.setDefaultFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); 0645 // m_document = m_visualEditor->document(); 0646 // m_document.setDefaultFont(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)); 0647 m_visualEditor->setDocument(&m_document); 0648 connect(&m_document, &QTextDocument::blockCountChanged, this, [this](int ct) { 0649 m_visualEditor->repaintLines(); 0650 qDebug() << "++++++++++++++++++++\n\nGOT BLOCKS: " << ct << "\n\n+++++++++++++++++++++"; 0651 }); 0652 0653 QMenu *insertMenu = new QMenu(this); 0654 QAction *createSequence = new QAction(QIcon::fromTheme(QStringLiteral("list-add")), i18n("Create new sequence with edit"), this); 0655 QAction *insertSelection = new QAction(QIcon::fromTheme(QStringLiteral("timeline-insert")), i18n("Insert selection in timeline"), this); 0656 QAction *saveAsPlaylist = new QAction(QIcon::fromTheme(QStringLiteral("document-save-as")), i18n("Save edited text in a playlist file"), this); 0657 insertMenu->addAction(createSequence); 0658 insertMenu->addAction(insertSelection); 0659 insertMenu->addSeparator(); 0660 insertMenu->addAction(saveAsPlaylist); 0661 button_insert->setMenu(insertMenu); 0662 button_insert->setDefaultAction(createSequence); 0663 button_insert->setToolTip(i18n("Create new sequence with text edit ")); 0664 0665 connect(createSequence, &QAction::triggered, this, &TextBasedEdit::createSequence); 0666 connect(insertSelection, &QAction::triggered, this, &TextBasedEdit::insertToTimeline); 0667 connect(saveAsPlaylist, &QAction::triggered, this, [&]() { previewPlaylist(true); }); 0668 insertSelection->setEnabled(false); 0669 0670 connect(m_visualEditor, &VideoTextEdit::selectionChanged, this, [this, insertSelection]() { 0671 bool hasSelection = m_visualEditor->textCursor().selectedText().simplified().isEmpty() == false; 0672 m_visualEditor->bookmarkAction->setEnabled(hasSelection); 0673 m_visualEditor->deleteAction->setEnabled(hasSelection); 0674 insertSelection->setEnabled(hasSelection); 0675 }); 0676 0677 button_start->setEnabled(false); 0678 connect(button_start, &QPushButton::clicked, this, &TextBasedEdit::startRecognition); 0679 frame_progress->setVisible(false); 0680 connect(button_abort, &QToolButton::clicked, this, [this]() { 0681 if (m_speechJob && m_speechJob->state() == QProcess::Running) { 0682 m_speechJob->kill(); 0683 } else if (m_tCodeJob && m_tCodeJob->state() == QProcess::Running) { 0684 m_tCodeJob->kill(); 0685 } 0686 }); 0687 language_box->setToolTip(i18n("Speech model")); 0688 speech_language->setToolTip(i18n("Speech language")); 0689 connect(pCore.get(), &Core::voskModelUpdate, this, [&](const QStringList &models) { 0690 if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) { 0691 return; 0692 } 0693 language_box->clear(); 0694 language_box->addItems(models); 0695 0696 if (models.isEmpty()) { 0697 showMessage(i18n("Please install speech recognition models"), KMessageWidget::Information, m_voskConfig); 0698 } else { 0699 if (!KdenliveSettings::vosk_text_model().isEmpty() && models.contains(KdenliveSettings::vosk_text_model())) { 0700 int ix = language_box->findText(KdenliveSettings::vosk_text_model()); 0701 if (ix > -1) { 0702 language_box->setCurrentIndex(ix); 0703 } 0704 } 0705 } 0706 }); 0707 connect(language_box, static_cast<void (QComboBox::*)(int)>(&QComboBox::activated), this, [this]() { 0708 if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) { 0709 const QString modelName = language_box->currentData().toString(); 0710 speech_language->setEnabled(!modelName.endsWith(QLatin1String(".en"))); 0711 KdenliveSettings::setWhisperModel(modelName); 0712 } else { 0713 KdenliveSettings::setVosk_text_model(language_box->currentText()); 0714 } 0715 }); 0716 info_message->hide(); 0717 updateEngine(); 0718 0719 m_logAction = new QAction(i18n("Show log"), this); 0720 connect(m_logAction, &QAction::triggered, this, [this]() { KMessageBox::error(this, m_errorString, i18n("Detailed log")); }); 0721 0722 speech_zone->setChecked(KdenliveSettings::speech_zone()); 0723 connect(speech_zone, &QCheckBox::stateChanged, [](int state) { KdenliveSettings::setSpeech_zone(state == Qt::Checked); }); 0724 button_delete->setDefaultAction(m_visualEditor->deleteAction); 0725 button_delete->setToolTip(i18n("Delete selected text")); 0726 connect(m_visualEditor->deleteAction, &QAction::triggered, this, &TextBasedEdit::deleteItem); 0727 0728 button_bookmark->setDefaultAction(m_visualEditor->bookmarkAction); 0729 button_bookmark->setToolTip(i18n("Add marker for current selection")); 0730 connect(m_visualEditor->bookmarkAction, &QAction::triggered, this, &TextBasedEdit::addBookmark); 0731 0732 // Zoom 0733 QAction *zoomIn = new QAction(QIcon::fromTheme(QStringLiteral("zoom-in")), i18n("Zoom In"), this); 0734 connect(zoomIn, &QAction::triggered, this, &TextBasedEdit::slotZoomIn); 0735 QAction *zoomOut = new QAction(QIcon::fromTheme(QStringLiteral("zoom-out")), i18n("Zoom Out"), this); 0736 connect(zoomOut, &QAction::triggered, this, &TextBasedEdit::slotZoomOut); 0737 QAction *removeSilence = new QAction(QIcon::fromTheme(QStringLiteral("edit-delete")), i18n("Remove non speech zones"), this); 0738 connect(removeSilence, &QAction::triggered, m_visualEditor, &VideoTextEdit::slotRemoveSilence); 0739 // Build menu 0740 QMenu *extraMenu = new QMenu(this); 0741 extraMenu->addAction(zoomIn); 0742 extraMenu->addAction(zoomOut); 0743 extraMenu->addSeparator(); 0744 extraMenu->addAction(removeSilence); 0745 subMenu->setMenu(extraMenu); 0746 0747 // Message Timer 0748 m_hideTimer.setSingleShot(true); 0749 m_hideTimer.setInterval(5000); 0750 connect(&m_hideTimer, &QTimer::timeout, info_message, &KMessageWidget::animatedHide); 0751 0752 // Search stuff 0753 search_frame->setVisible(false); 0754 connect(button_search, &QToolButton::toggled, this, [&](bool toggled) { 0755 search_frame->setVisible(toggled); 0756 search_line->setFocus(); 0757 }); 0758 connect(search_line, &QLineEdit::textChanged, this, [this](const QString &searchText) { 0759 QPalette palette = this->palette(); 0760 QColor col = palette.color(QPalette::Base); 0761 if (searchText.length() > 2) { 0762 bool found = m_visualEditor->find(searchText); 0763 if (found) { 0764 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5))); 0765 palette.setColor(QPalette::Base, col); 0766 QTextCursor cur = m_visualEditor->textCursor(); 0767 cur.select(QTextCursor::WordUnderCursor); 0768 m_visualEditor->setTextCursor(cur); 0769 } else { 0770 // Loop over, abort 0771 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5))); 0772 palette.setColor(QPalette::Base, col); 0773 } 0774 } 0775 search_line->setPalette(palette); 0776 }); 0777 connect(search_next, &QToolButton::clicked, this, [this]() { 0778 const QString searchText = search_line->text(); 0779 QPalette palette = this->palette(); 0780 QColor col = palette.color(QPalette::Base); 0781 if (searchText.length() > 2) { 0782 bool found = m_visualEditor->find(searchText); 0783 if (found) { 0784 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5))); 0785 palette.setColor(QPalette::Base, col); 0786 QTextCursor cur = m_visualEditor->textCursor(); 0787 cur.select(QTextCursor::WordUnderCursor); 0788 m_visualEditor->setTextCursor(cur); 0789 } else { 0790 // Loop over, abort 0791 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5))); 0792 palette.setColor(QPalette::Base, col); 0793 } 0794 } 0795 search_line->setPalette(palette); 0796 }); 0797 connect(search_prev, &QToolButton::clicked, this, [this]() { 0798 const QString searchText = search_line->text(); 0799 QPalette palette = this->palette(); 0800 QColor col = palette.color(QPalette::Base); 0801 if (searchText.length() > 2) { 0802 bool found = m_visualEditor->find(searchText, QTextDocument::FindBackward); 0803 if (found) { 0804 col.setGreen(qMin(255, static_cast<int>(col.green() * 1.5))); 0805 palette.setColor(QPalette::Base, col); 0806 QTextCursor cur = m_visualEditor->textCursor(); 0807 cur.select(QTextCursor::WordUnderCursor); 0808 m_visualEditor->setTextCursor(cur); 0809 } else { 0810 // Loop over, abort 0811 col.setRed(qMin(255, static_cast<int>(col.red() * 1.5))); 0812 palette.setColor(QPalette::Base, col); 0813 } 0814 } 0815 search_line->setPalette(palette); 0816 }); 0817 } 0818 0819 void TextBasedEdit::slotZoomIn() 0820 { 0821 QTextCursor cursor = m_visualEditor->textCursor(); 0822 m_visualEditor->selectAll(); 0823 qreal fontSize = QFontInfo(m_visualEditor->currentFont()).pointSizeF() * 1.2; 0824 KdenliveSettings::setSubtitleEditFontSize(fontSize); 0825 m_visualEditor->setFontPointSize(KdenliveSettings::subtitleEditFontSize()); 0826 m_visualEditor->setTextCursor(cursor); 0827 } 0828 0829 void TextBasedEdit::slotZoomOut() 0830 { 0831 QTextCursor cursor = m_visualEditor->textCursor(); 0832 m_visualEditor->selectAll(); 0833 qreal fontSize = QFontInfo(m_visualEditor->currentFont()).pointSizeF() / 1.2; 0834 fontSize = qMax(fontSize, QFontInfo(QFontDatabase::systemFont(QFontDatabase::SmallestReadableFont)).pointSizeF()); 0835 KdenliveSettings::setSubtitleEditFontSize(fontSize); 0836 m_visualEditor->setFontPointSize(KdenliveSettings::subtitleEditFontSize()); 0837 m_visualEditor->setTextCursor(cursor); 0838 } 0839 0840 void TextBasedEdit::applyFontSize() 0841 { 0842 if (KdenliveSettings::subtitleEditFontSize() > 0) { 0843 QTextCursor cursor = m_visualEditor->textCursor(); 0844 m_visualEditor->selectAll(); 0845 m_visualEditor->setFontPointSize(KdenliveSettings::subtitleEditFontSize()); 0846 m_visualEditor->setTextCursor(cursor); 0847 } 0848 } 0849 0850 void TextBasedEdit::updateEngine() 0851 { 0852 delete m_stt; 0853 if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) { 0854 m_stt = new SpeechToText(SpeechToText::EngineType::EngineWhisper, this); 0855 language_box->clear(); 0856 QList<std::pair<QString, QString>> whisperModels = m_stt->whisperModels(); 0857 for (auto &w : whisperModels) { 0858 language_box->addItem(w.first, w.second); 0859 } 0860 int ix = language_box->findData(KdenliveSettings::whisperModel()); 0861 if (ix > -1) { 0862 language_box->setCurrentIndex(ix); 0863 } 0864 if (speech_language->count() == 0) { 0865 // Fill whisper languages 0866 QMap<QString, QString> languages = m_stt->whisperLanguages(); 0867 QMapIterator<QString, QString> j(languages); 0868 while (j.hasNext()) { 0869 j.next(); 0870 speech_language->addItem(j.key(), j.value()); 0871 } 0872 int ix = speech_language->findData(KdenliveSettings::whisperLanguage()); 0873 if (ix > -1) { 0874 speech_language->setCurrentIndex(ix); 0875 } 0876 } 0877 speech_language->setEnabled(!KdenliveSettings::whisperModel().endsWith(QLatin1String(".en"))); 0878 speech_language->setVisible(true); 0879 } else { 0880 // VOSK 0881 speech_language->setVisible(false); 0882 m_stt = new SpeechToText(SpeechToText::EngineType::EngineVosk, this); 0883 language_box->clear(); 0884 m_stt->parseVoskDictionaries(); 0885 } 0886 } 0887 0888 TextBasedEdit::~TextBasedEdit() 0889 { 0890 if (m_speechJob && m_speechJob->state() == QProcess::Running) { 0891 m_speechJob->kill(); 0892 m_speechJob->waitForFinished(); 0893 } 0894 } 0895 0896 bool TextBasedEdit::eventFilter(QObject *obj, QEvent *event) 0897 { 0898 if (event->type() == QEvent::KeyPress) { 0899 qDebug() << "==== FOT TXTEDIT EVENT FILTER: " << static_cast<QKeyEvent *>(event)->key(); 0900 } 0901 /*if(obj == m_visualEditor && event->type() == QEvent::KeyPress) 0902 { 0903 QKeyEvent *keyEvent = static_cast <QKeyEvent*> (event); 0904 if (keyEvent->key() != Qt::Key_Left && keyEvent->key() != Qt::Key_Up && keyEvent->key() != Qt::Key_Right && keyEvent->key() != Qt::Key_Down) { 0905 parentWidget()->setFocus(); 0906 return true; 0907 } 0908 }*/ 0909 return QObject::eventFilter(obj, event); 0910 } 0911 0912 void TextBasedEdit::startRecognition() 0913 { 0914 if (m_speechJob && m_speechJob->state() != QProcess::NotRunning) { 0915 if (KMessageBox::questionTwoActions( 0916 this, i18n("Another recognition job is already running. It will be aborted in favor of the new job. Do you want to proceed?"), {}, 0917 KStandardGuiItem::cont(), KStandardGuiItem::cancel()) != KMessageBox::PrimaryAction) { 0918 return; 0919 } 0920 } 0921 info_message->hide(); 0922 m_errorString.clear(); 0923 m_visualEditor->cleanup(); 0924 // m_visualEditor->insertHtml(QStringLiteral("<body>")); 0925 m_stt->checkDependencies(false); 0926 QString modelDirectory; 0927 QString language; 0928 QString modelName; 0929 if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) { 0930 // Whisper engine 0931 if (!m_stt->checkSetup() || !m_stt->missingDependencies({QStringLiteral("openai-whisper")}).isEmpty()) { 0932 showMessage(i18n("Please configure speech to text."), KMessageWidget::Warning, m_voskConfig); 0933 return; 0934 } 0935 modelName = language_box->currentData().toString(); 0936 language = speech_language->isEnabled() && !speech_language->currentData().isNull() 0937 ? QStringLiteral("language=%1").arg(speech_language->currentData().toString()) 0938 : QString(); 0939 if (KdenliveSettings::whisperDisableFP16()) { 0940 language.append(QStringLiteral(" fp16=False")); 0941 } 0942 } else { 0943 // VOSK engine 0944 if (!m_stt->checkSetup() || !m_stt->missingDependencies({QStringLiteral("vosk")}).isEmpty()) { 0945 showMessage(i18n("Please configure speech to text."), KMessageWidget::Warning, m_voskConfig); 0946 return; 0947 } 0948 // Start python script 0949 modelName = language_box->currentText(); 0950 if (modelName.isEmpty()) { 0951 showMessage(i18n("Please install a language model."), KMessageWidget::Warning, m_voskConfig); 0952 return; 0953 } 0954 modelDirectory = m_stt->voskModelPath(); 0955 } 0956 m_binId = pCore->getMonitor(Kdenlive::ClipMonitor)->activeClipId(); 0957 std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId); 0958 if (clip == nullptr) { 0959 showMessage(i18n("Select a clip with audio in Project Bin."), KMessageWidget::Information); 0960 return; 0961 } 0962 0963 m_speechJob = std::make_unique<QProcess>(this); 0964 showMessage(i18n("Starting speech recognition"), KMessageWidget::Information); 0965 qApp->processEvents(); 0966 0967 m_sourceUrl.clear(); 0968 QString clipName; 0969 m_clipOffset = 0; 0970 m_lastPosition = 0; 0971 double endPos = 0; 0972 bool hasAudio = false; 0973 if (clip->itemType() == AbstractProjectItem::ClipItem) { 0974 std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip); 0975 if (clipItem) { 0976 m_sourceUrl = clipItem->url(); 0977 clipName = clipItem->clipName(); 0978 hasAudio = clipItem->hasAudio(); 0979 if (speech_zone->isChecked()) { 0980 // Analyse clip zone only 0981 QPoint zone = clipItem->zone(); 0982 m_lastPosition = zone.x(); 0983 m_clipOffset = GenTime(zone.x(), pCore->getCurrentFps()).seconds(); 0984 m_clipDuration = GenTime(zone.y() - zone.x(), pCore->getCurrentFps()).seconds(); 0985 endPos = m_clipDuration; 0986 } else { 0987 m_clipDuration = clipItem->duration().seconds(); 0988 } 0989 } 0990 } else if (clip->itemType() == AbstractProjectItem::SubClipItem) { 0991 std::shared_ptr<ProjectSubClip> clipItem = std::static_pointer_cast<ProjectSubClip>(clip); 0992 if (clipItem) { 0993 auto master = clipItem->getMasterClip(); 0994 m_sourceUrl = master->url(); 0995 hasAudio = master->hasAudio(); 0996 clipName = master->clipName(); 0997 QPoint zone = clipItem->zone(); 0998 m_lastPosition = zone.x(); 0999 m_clipOffset = GenTime(zone.x(), pCore->getCurrentFps()).seconds(); 1000 m_clipDuration = GenTime(zone.y() - zone.x(), pCore->getCurrentFps()).seconds(); 1001 endPos = m_clipDuration; 1002 } 1003 } 1004 if (m_sourceUrl.isEmpty() || !hasAudio) { 1005 showMessage(i18n("Select a clip with audio for speech recognition."), KMessageWidget::Information); 1006 return; 1007 } 1008 clipNameLabel->setText(clipName); 1009 if (clip->clipType() == ClipType::Playlist) { 1010 // We need to extract audio first 1011 m_playlistWav.remove(); 1012 m_playlistWav.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.wav"))); 1013 if (!m_playlistWav.open()) { 1014 showMessage(i18n("Cannot create temporary file."), KMessageWidget::Warning); 1015 return; 1016 } 1017 m_playlistWav.close(); 1018 1019 showMessage(i18n("Extracting audio for %1.", clipName), KMessageWidget::Information); 1020 qApp->processEvents(); 1021 m_tCodeJob = std::make_unique<QProcess>(this); 1022 connect(m_tCodeJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, 1023 [this, modelName, language, clipName, modelDirectory, endPos](int code, QProcess::ExitStatus status) { 1024 Q_UNUSED(code) 1025 qDebug() << "++++++++++++++++++++++ TCODE JOB FINISHED\n"; 1026 if (status == QProcess::CrashExit) { 1027 showMessage(i18n("Audio extract failed."), KMessageWidget::Warning); 1028 speech_progress->setValue(0); 1029 frame_progress->setVisible(false); 1030 m_playlistWav.remove(); 1031 return; 1032 } 1033 showMessage(i18n("Starting speech recognition on %1.", clipName), KMessageWidget::Information); 1034 qApp->processEvents(); 1035 connect(m_speechJob.get(), &QProcess::readyReadStandardError, this, &TextBasedEdit::slotProcessSpeechError); 1036 connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessSpeech); 1037 connect(m_speechJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, 1038 [this](int code, QProcess::ExitStatus status) { 1039 m_playlistWav.remove(); 1040 slotProcessSpeechStatus(code, status); 1041 }); 1042 qDebug() << "::: STARTING SPEECH: " << modelDirectory << " / " << modelName << " / " << language; 1043 if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) { 1044 // Whisper 1045 connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessWhisperSpeech); 1046 if (speech_zone->isChecked()) { 1047 m_tmpCutWav.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.wav"))); 1048 if (!m_tmpCutWav.open()) { 1049 showMessage(i18n("Cannot create temporary file."), KMessageWidget::Warning); 1050 return; 1051 } 1052 m_tmpCutWav.close(); 1053 m_speechJob->start(m_stt->pythonExec(), 1054 {m_stt->speechScript(), m_playlistWav.fileName(), modelName, KdenliveSettings::whisperDevice(), 1055 KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe"), language, 1056 QString::number(m_clipOffset), QString::number(endPos), m_tmpCutWav.fileName()}); 1057 } else { 1058 m_speechJob->start(m_stt->pythonExec(), 1059 {m_stt->speechScript(), m_playlistWav.fileName(), modelName, KdenliveSettings::whisperDevice(), 1060 KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe"), language}); 1061 } 1062 } else { 1063 m_speechJob->start(m_stt->pythonExec(), {m_stt->speechScript(), modelDirectory, modelName, m_playlistWav.fileName(), 1064 QString::number(m_clipOffset), QString::number(endPos)}); 1065 } 1066 speech_progress->setValue(0); 1067 frame_progress->setVisible(true); 1068 }); 1069 connect(m_tCodeJob.get(), &QProcess::readyReadStandardOutput, this, [this]() { 1070 QString saveData = QString::fromUtf8(m_tCodeJob->readAllStandardOutput()); 1071 qDebug() << "+GOT OUTPUT: " << saveData; 1072 saveData = saveData.section(QStringLiteral("percentage:"), 1).simplified(); 1073 int percent = saveData.section(QLatin1Char(' '), 0, 0).toInt(); 1074 speech_progress->setValue(percent); 1075 }); 1076 m_tCodeJob->start(KdenliveSettings::meltpath(), 1077 {QStringLiteral("-progress"), m_sourceUrl, QStringLiteral("-consumer"), QString("avformat:%1").arg(m_playlistWav.fileName()), 1078 QStringLiteral("vn=1"), QStringLiteral("ar=16000")}); 1079 speech_progress->setValue(0); 1080 frame_progress->setVisible(true); 1081 } else { 1082 showMessage(i18n("Starting speech recognition on %1.", clipName), KMessageWidget::Information); 1083 qApp->processEvents(); 1084 connect(m_speechJob.get(), &QProcess::readyReadStandardError, this, &TextBasedEdit::slotProcessSpeechError); 1085 connect(m_speechJob.get(), static_cast<void (QProcess::*)(int, QProcess::ExitStatus)>(&QProcess::finished), this, 1086 &TextBasedEdit::slotProcessSpeechStatus); 1087 button_insert->setEnabled(false); 1088 if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) { 1089 // Whisper 1090 qDebug() << "=== STARTING Whisper reco: " << m_stt->speechScript() << " / " << language_box->currentData() << " / " 1091 << KdenliveSettings::whisperDevice() << " / " 1092 << (KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe")) << " / " << m_sourceUrl 1093 << ", START: " << m_clipOffset << ", DUR: " << endPos << " / " << language; 1094 connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessWhisperSpeech); 1095 if (speech_zone->isChecked()) { 1096 m_tmpCutWav.setFileTemplate(QDir::temp().absoluteFilePath(QStringLiteral("kdenlive-XXXXXX.wav"))); 1097 if (!m_tmpCutWav.open()) { 1098 showMessage(i18n("Cannot create temporary file."), KMessageWidget::Warning); 1099 return; 1100 } 1101 m_tmpCutWav.close(); 1102 m_speechJob->start(m_stt->pythonExec(), {m_stt->speechScript(), m_sourceUrl, modelName, KdenliveSettings::whisperDevice(), 1103 KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe"), 1104 language, QString::number(m_clipOffset), QString::number(endPos), m_tmpCutWav.fileName()}); 1105 } else { 1106 m_speechJob->start(m_stt->pythonExec(), 1107 {m_stt->speechScript(), m_sourceUrl, modelName, KdenliveSettings::whisperDevice(), 1108 KdenliveSettings::whisperTranslate() ? QStringLiteral("translate") : QStringLiteral("transcribe"), language}); 1109 } 1110 } else { 1111 // VOSK 1112 qDebug() << "=== STARTING RECO: " << m_stt->speechScript() << " / " << modelDirectory << " / " << modelName << " / " << m_sourceUrl 1113 << ", START: " << m_clipOffset << ", DUR: " << endPos; 1114 connect(m_speechJob.get(), &QProcess::readyReadStandardOutput, this, &TextBasedEdit::slotProcessSpeech); 1115 m_speechJob->start(m_stt->pythonExec(), 1116 {m_stt->speechScript(), modelDirectory, modelName, m_sourceUrl, QString::number(m_clipOffset), QString::number(endPos)}); 1117 } 1118 speech_progress->setValue(0); 1119 frame_progress->setVisible(true); 1120 } 1121 } 1122 1123 void TextBasedEdit::slotProcessSpeechStatus(int, QProcess::ExitStatus status) 1124 { 1125 m_tmpCutWav.remove(); 1126 if (status == QProcess::CrashExit) { 1127 showMessage(i18n("Speech recognition aborted."), KMessageWidget::Warning, m_errorString.isEmpty() ? nullptr : m_logAction); 1128 } else if (m_visualEditor->toPlainText().isEmpty()) { 1129 if (m_errorString.contains(QStringLiteral("ModuleNotFoundError"))) { 1130 showMessage(i18n("Error, please check the speech to text configuration."), KMessageWidget::Warning, m_voskConfig); 1131 } else { 1132 showMessage(i18n("No speech detected."), KMessageWidget::Information, m_errorString.isEmpty() ? nullptr : m_logAction); 1133 } 1134 } else { 1135 // Last empty object - no speech detected 1136 if (KdenliveSettings::speechEngine() != QLatin1String("whisper")) { 1137 // VOSK 1138 GenTime silenceStart(m_lastPosition + 1, pCore->getCurrentFps()); 1139 if (silenceStart.seconds() < m_clipDuration + m_clipOffset) { 1140 m_visualEditor->moveCursor(QTextCursor::End); 1141 QTextCursor cursor = m_visualEditor->textCursor(); 1142 QTextCharFormat fmt = cursor.charFormat(); 1143 fmt.setAnchorHref(QString("%1#%2:%3").arg(m_binId).arg(silenceStart.seconds()).arg(GenTime(m_clipDuration + m_clipOffset).seconds())); 1144 fmt.setAnchor(true); 1145 cursor.insertText(i18n("No speech"), fmt); 1146 m_visualEditor->textCursor().insertBlock(cursor.blockFormat()); 1147 m_visualEditor->speechZones << QPair<double, double>(silenceStart.seconds(), GenTime(m_clipDuration + m_clipOffset).seconds()); 1148 m_visualEditor->repaintLines(); 1149 } 1150 } 1151 1152 button_insert->setEnabled(true); 1153 showMessage(i18n("Speech recognition finished."), KMessageWidget::Positive); 1154 // Store speech analysis in clip properties 1155 std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId); 1156 if (clip) { 1157 std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip); 1158 QString oldSpeech; 1159 if (clipItem) { 1160 oldSpeech = clipItem->getProducerProperty(QStringLiteral("kdenlive:speech")); 1161 } 1162 QMap<QString, QString> oldProperties; 1163 oldProperties.insert(QStringLiteral("kdenlive:speech"), oldSpeech); 1164 QMap<QString, QString> properties; 1165 properties.insert(QStringLiteral("kdenlive:speech"), m_visualEditor->toHtml()); 1166 pCore->bin()->slotEditClipCommand(m_binId, oldProperties, properties); 1167 } 1168 } 1169 QTextCursor cur = m_visualEditor->textCursor(); 1170 cur.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 1171 m_visualEditor->setTextCursor(cur); 1172 frame_progress->setVisible(false); 1173 applyFontSize(); 1174 } 1175 1176 void TextBasedEdit::slotProcessSpeechError() 1177 { 1178 const QString log = QString::fromUtf8(m_speechJob->readAllStandardError()); 1179 if (KdenliveSettings::speechEngine() == QLatin1String("whisper")) { 1180 if (log.contains(QStringLiteral("%|"))) { 1181 int prog = log.section(QLatin1Char('%'), 0, 0).toInt(); 1182 speech_progress->setValue(prog); 1183 } 1184 } 1185 m_errorString.append(log); 1186 } 1187 1188 void TextBasedEdit::slotProcessWhisperSpeech() 1189 { 1190 const QString saveData = QString::fromUtf8(m_speechJob->readAllStandardOutput()); 1191 QStringList sentences = saveData.split(QLatin1Char('\n'), Qt::SkipEmptyParts); 1192 QString sentenceTimings = sentences.takeFirst(); 1193 if (!sentenceTimings.startsWith(QLatin1Char('['))) { 1194 // This is not a timing output 1195 if (sentenceTimings.startsWith(QStringLiteral("Detected "))) { 1196 showMessage(sentenceTimings, KMessageWidget::Information); 1197 } 1198 return; 1199 } 1200 QPair<double, double> sentenceZone; 1201 sentenceZone.first = sentenceTimings.section(QLatin1Char('['), 1).section(QLatin1Char('>'), 0, 0).toDouble() + m_clipOffset; 1202 sentenceZone.second = sentenceTimings.section(QLatin1Char('>'), 1).section(QLatin1Char(']'), 0, 0).toDouble() + m_clipOffset; 1203 QTextCursor cursor = m_visualEditor->textCursor(); 1204 QTextCharFormat fmt = cursor.charFormat(); 1205 QPair<double, double> wordZone; 1206 GenTime sentenceStart(sentenceZone.first); 1207 if (sentenceStart.frames(pCore->getCurrentFps()) > m_lastPosition + 1) { 1208 // Insert space 1209 GenTime silenceStart(m_lastPosition, pCore->getCurrentFps()); 1210 m_visualEditor->moveCursor(QTextCursor::End); 1211 fmt.setAnchorHref(QString("%1#%2:%3") 1212 .arg(m_binId) 1213 .arg(silenceStart.seconds()) 1214 .arg(GenTime(sentenceStart.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds())); 1215 fmt.setAnchor(true); 1216 cursor.insertText(i18n("No speech"), fmt); 1217 fmt.setAnchor(false); 1218 m_visualEditor->textCursor().insertBlock(cursor.blockFormat()); 1219 m_visualEditor->speechZones << QPair<double, double>(silenceStart.seconds(), 1220 GenTime(sentenceStart.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds()); 1221 } 1222 for (auto &s : sentences) { 1223 wordZone.first = s.section(QLatin1Char('['), 1).section(QLatin1Char('>'), 0, 0).toDouble() + m_clipOffset; 1224 wordZone.second = s.section(QLatin1Char('>'), 1).section(QLatin1Char(']'), 0, 0).toDouble() + m_clipOffset; 1225 const QString text = s.section(QLatin1Char(']'), 1); 1226 if (text.isEmpty()) { 1227 // new section, insert 1228 m_visualEditor->textCursor().insertBlock(cursor.blockFormat()); 1229 m_visualEditor->speechZones << sentenceZone; 1230 GenTime lastSentence(sentenceZone.second); 1231 GenTime nextSentence(wordZone.first); 1232 if (nextSentence.frames(pCore->getCurrentFps()) > lastSentence.frames(pCore->getCurrentFps()) + 1) { 1233 // Insert space 1234 m_visualEditor->moveCursor(QTextCursor::End); 1235 fmt.setAnchorHref(QString("%1#%2:%3") 1236 .arg(m_binId) 1237 .arg(lastSentence.seconds()) 1238 .arg(GenTime(nextSentence.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds())); 1239 fmt.setAnchor(true); 1240 cursor.insertText(i18n("No speech"), fmt); 1241 fmt.setAnchor(false); 1242 m_visualEditor->textCursor().insertBlock(cursor.blockFormat()); 1243 m_visualEditor->speechZones << QPair<double, double>( 1244 lastSentence.seconds(), GenTime(nextSentence.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds()); 1245 } 1246 sentenceZone = wordZone; 1247 continue; 1248 } 1249 fmt.setAnchor(true); 1250 fmt.setAnchorHref(QString("%1#%2:%3").arg(m_binId).arg(wordZone.first).arg(wordZone.second)); 1251 cursor.insertText(text, fmt); 1252 fmt.setAnchor(false); 1253 cursor.insertText(QStringLiteral(" "), fmt); 1254 } 1255 m_visualEditor->textCursor().insertBlock(cursor.blockFormat()); 1256 m_visualEditor->speechZones << sentenceZone; 1257 if (sentenceZone.second < m_clipOffset + m_clipDuration) { 1258 } 1259 m_visualEditor->repaintLines(); 1260 qDebug() << "::: " << saveData; 1261 } 1262 1263 void TextBasedEdit::slotProcessSpeech() 1264 { 1265 QString saveData = QString::fromUtf8(m_speechJob->readAllStandardOutput()); 1266 qDebug() << "=== GOT DATA:\n" << saveData; 1267 QJsonParseError error; 1268 auto loadDoc = QJsonDocument::fromJson(saveData.toUtf8(), &error); 1269 qDebug() << "===JSON ERROR: " << error.errorString(); 1270 QTextCursor cursor = m_visualEditor->textCursor(); 1271 QTextCharFormat fmt = cursor.charFormat(); 1272 // fmt.setForeground(palette().text().color()); 1273 if (loadDoc.isObject()) { 1274 QJsonObject obj = loadDoc.object(); 1275 if (!obj.isEmpty()) { 1276 // QString itemText = obj["text"].toString(); 1277 bool textFound = false; 1278 QPair<double, double> sentenceZone; 1279 if (obj["result"].isArray()) { 1280 QJsonArray obj2 = obj["result"].toArray(); 1281 1282 // Get start time for first word 1283 QJsonValue val = obj2.first(); 1284 if (val.isObject() && val.toObject().keys().contains("start")) { 1285 double ms = val.toObject().value("start").toDouble() + m_clipOffset; 1286 GenTime startPos(ms); 1287 sentenceZone.first = ms; 1288 if (startPos.frames(pCore->getCurrentFps()) > m_lastPosition + 1) { 1289 // Insert space 1290 GenTime silenceStart(m_lastPosition, pCore->getCurrentFps()); 1291 m_visualEditor->moveCursor(QTextCursor::End); 1292 fmt.setAnchorHref(QString("%1#%2:%3") 1293 .arg(m_binId) 1294 .arg(silenceStart.seconds()) 1295 .arg(GenTime(startPos.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds())); 1296 fmt.setAnchor(true); 1297 cursor.insertText(i18n("No speech"), fmt); 1298 m_visualEditor->textCursor().insertBlock(cursor.blockFormat()); 1299 m_visualEditor->speechZones << QPair<double, double>( 1300 silenceStart.seconds(), GenTime(startPos.frames(pCore->getCurrentFps()) - 1, pCore->getCurrentFps()).seconds()); 1301 } 1302 val = obj2.last(); 1303 if (val.isObject() && val.toObject().keys().contains("end")) { 1304 ms = val.toObject().value("end").toDouble() + m_clipOffset; 1305 sentenceZone.second = ms; 1306 m_lastPosition = GenTime(ms).frames(pCore->getCurrentFps()); 1307 if (m_clipDuration > 0.) { 1308 speech_progress->setValue(static_cast<int>(100 * ms / (+m_clipOffset + m_clipDuration))); 1309 } 1310 } 1311 } 1312 // Store words with their start/end time 1313 for (const QJsonValue &v : obj2) { 1314 textFound = true; 1315 fmt.setAnchor(true); 1316 fmt.setAnchorHref(QString("%1#%2:%3") 1317 .arg(m_binId) 1318 .arg(v.toObject().value("start").toDouble() + m_clipOffset) 1319 .arg(v.toObject().value("end").toDouble() + m_clipOffset)); 1320 cursor.insertText(v.toObject().value("word").toString(), fmt); 1321 fmt.setAnchor(false); 1322 cursor.insertText(QStringLiteral(" "), fmt); 1323 } 1324 } else { 1325 // Last empty object - no speech detected 1326 } 1327 if (textFound) { 1328 if (sentenceZone.second < m_clipOffset + m_clipDuration) { 1329 m_visualEditor->textCursor().insertBlock(cursor.blockFormat()); 1330 } 1331 m_visualEditor->speechZones << sentenceZone; 1332 } 1333 } 1334 } else if (loadDoc.isEmpty()) { 1335 qDebug() << "==== EMPTY OBJECT DOC"; 1336 } 1337 qDebug() << "==== GOT BLOCKS: " << m_document.blockCount(); 1338 qDebug() << "=== LINES: " << m_document.firstBlock().lineCount(); 1339 m_visualEditor->repaintLines(); 1340 } 1341 1342 void TextBasedEdit::deleteItem() 1343 { 1344 QTextCursor cursor = m_visualEditor->textCursor(); 1345 int start = cursor.selectionStart(); 1346 int end = cursor.selectionEnd(); 1347 if (end > start) { 1348 QTextDocumentFragment fragment = cursor.selection(); 1349 QString anchorStart = m_visualEditor->selectionStartAnchor(cursor, start, end); 1350 cursor.setPosition(end); 1351 bool blockEnd = cursor.atBlockEnd(); 1352 cursor = m_visualEditor->textCursor(); 1353 QString anchorEnd = m_visualEditor->selectionEndAnchor(cursor, end, start); 1354 qDebug() << "=== FINAL END CUT: " << end; 1355 qDebug() << "=== GOT END ANCHOR: " << cursor.selectedText() << " = " << anchorEnd; 1356 if (!anchorStart.isEmpty() && !anchorEnd.isEmpty()) { 1357 double startMs = anchorStart.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble(); 1358 double endMs = anchorEnd.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble(); 1359 if (startMs < endMs) { 1360 Fun redo = [this, start, end, startMs, endMs, blockEnd]() { 1361 QTextCursor tCursor = m_visualEditor->textCursor(); 1362 tCursor.setPosition(start); 1363 tCursor.setPosition(end, QTextCursor::KeepAnchor); 1364 m_visualEditor->cutZones << QPoint(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps())); 1365 tCursor.removeSelectedText(); 1366 if (blockEnd) { 1367 tCursor.deleteChar(); 1368 } 1369 // Reset selection and rebuild line numbers 1370 m_visualEditor->rebuildZones(); 1371 previewPlaylist(false); 1372 return true; 1373 }; 1374 Fun undo = [this, start, fr = fragment, startMs, endMs]() { 1375 qDebug() << "::: PASTING FRAGMENT: " << fr.toPlainText(); 1376 QTextCursor tCursor = m_visualEditor->textCursor(); 1377 QPoint p(GenTime(startMs).frames(pCore->getCurrentFps()), GenTime(endMs).frames(pCore->getCurrentFps())); 1378 int ix = m_visualEditor->cutZones.indexOf(p); 1379 if (ix > -1) { 1380 m_visualEditor->cutZones.remove(ix); 1381 } 1382 tCursor.setPosition(start); 1383 tCursor.insertFragment(fr); 1384 // Reset selection and rebuild line numbers 1385 m_visualEditor->rebuildZones(); 1386 previewPlaylist(false); 1387 return true; 1388 }; 1389 redo(); 1390 pCore->pushUndo(undo, redo, i18nc("@action", "Edit clip text")); 1391 } 1392 } 1393 } else { 1394 QTextCursor curs = m_visualEditor->textCursor(); 1395 curs.movePosition(QTextCursor::Start, QTextCursor::MoveAnchor); 1396 for (int i = 0; i < m_document.blockCount(); ++i) { 1397 int blockStart = curs.position(); 1398 curs.movePosition(QTextCursor::EndOfBlock, QTextCursor::MoveAnchor); 1399 int blockEnd = curs.position(); 1400 if (blockStart == blockEnd) { 1401 // Empty block, delete 1402 curs.select(QTextCursor::BlockUnderCursor); 1403 curs.removeSelectedText(); 1404 curs.deleteChar(); 1405 } 1406 curs.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor); 1407 } 1408 } 1409 } 1410 1411 void TextBasedEdit::insertToTimeline() 1412 { 1413 QVector<QPoint> zones = m_visualEditor->getInsertZones(); 1414 if (zones.isEmpty()) { 1415 return; 1416 } 1417 Fun undo = []() { return true; }; 1418 Fun redo = []() { return true; }; 1419 for (auto &zone : zones) { 1420 pCore->window()->getCurrentTimeline()->controller()->insertZone(m_binId, zone, false, undo, redo); 1421 } 1422 pCore->pushUndo(undo, redo, i18nc("@action", "Create sequence clip")); 1423 } 1424 1425 void TextBasedEdit::previewPlaylist(bool createNew) 1426 { 1427 QVector<QPoint> zones = m_visualEditor->getInsertZones(); 1428 if (zones.isEmpty()) { 1429 showMessage(i18n("No text to export"), KMessageWidget::Information); 1430 return; 1431 } 1432 std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId); 1433 std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip); 1434 QString sourcePath = clipItem->url(); 1435 QMap<QString, QString> properties; 1436 properties.insert(QStringLiteral("kdenlive:baseid"), m_binId); 1437 QStringList playZones; 1438 for (const auto &p : qAsConst(zones)) { 1439 playZones << QString("%1:%2").arg(p.x()).arg(p.y()); 1440 } 1441 properties.insert(QStringLiteral("kdenlive:cutzones"), playZones.join(QLatin1Char(';'))); 1442 if (createNew) { 1443 int ix = 1; 1444 m_playlist = QString("%1-cut%2.kdenlive").arg(sourcePath).arg(ix); 1445 while (QFile::exists(m_playlist)) { 1446 ix++; 1447 m_playlist = QString("%1-cut%2.kdenlive").arg(sourcePath).arg(ix); 1448 } 1449 QUrl url = KUrlRequesterDialog::getUrl(QUrl::fromLocalFile(m_playlist), this, i18n("Enter new playlist path")); 1450 if (url.isEmpty()) { 1451 return; 1452 } 1453 m_playlist = url.toLocalFile(); 1454 } 1455 if (!m_playlist.isEmpty()) { 1456 pCore->bin()->savePlaylist(m_binId, m_playlist, zones, properties, createNew); 1457 clipNameLabel->setText(QFileInfo(m_playlist).fileName()); 1458 } 1459 } 1460 1461 void TextBasedEdit::createSequence() 1462 { 1463 QVector<QPoint> zones = m_visualEditor->fullExport(); 1464 if (zones.isEmpty()) { 1465 showMessage(i18n("No text to export"), KMessageWidget::Information); 1466 return; 1467 } 1468 QVector<QPoint> mergedZones; 1469 int max = zones.count(); 1470 int current = 0; 1471 int referenceStart = zones.at(current).x(); 1472 int currentEnd = zones.at(current).y(); 1473 int nextStart = 0; 1474 current++; 1475 while (current < max) { 1476 nextStart = zones.at(current).x(); 1477 if (nextStart == currentEnd || nextStart == currentEnd + 1) { 1478 // Contiguous zones 1479 currentEnd = zones.at(current).y(); 1480 current++; 1481 continue; 1482 } 1483 // Insert zone 1484 mergedZones << QPoint(referenceStart, currentEnd); 1485 referenceStart = zones.at(current).x(); 1486 currentEnd = zones.at(current).y(); 1487 current++; 1488 } 1489 Fun undo = []() { return true; }; 1490 Fun redo = []() { return true; }; 1491 // Create new timeline sequence 1492 std::shared_ptr<AbstractProjectItem> clip = pCore->projectItemModel()->getItemByBinId(m_binId); 1493 std::shared_ptr<ProjectClip> clipItem = std::static_pointer_cast<ProjectClip>(clip); 1494 const QString sequenceId = pCore->bin()->buildSequenceClipWithUndo(undo, redo, -1, -1, clipItem->clipName()); 1495 if (sequenceId == QLatin1String("-1")) { 1496 // Aborting 1497 return; 1498 } 1499 for (const auto &p : qAsConst(zones)) { 1500 if (p.y() > p.x()) { 1501 pCore->window()->getCurrentTimeline()->controller()->insertZone(m_binId, p, false, undo, redo); 1502 } 1503 } 1504 pCore->pushUndo(undo, redo, i18nc("@action", "Create sequence clip")); 1505 } 1506 1507 void TextBasedEdit::showMessage(const QString &text, KMessageWidget::MessageType type, QAction *action) 1508 { 1509 if (m_currentMessageAction != nullptr && (action == nullptr || action != m_currentMessageAction)) { 1510 info_message->removeAction(m_currentMessageAction); 1511 m_currentMessageAction = action; 1512 if (m_currentMessageAction) { 1513 info_message->addAction(m_currentMessageAction); 1514 } 1515 } else if (action) { 1516 m_currentMessageAction = action; 1517 info_message->addAction(m_currentMessageAction); 1518 } 1519 1520 if (info_message->isVisible()) { 1521 m_hideTimer.stop(); 1522 } 1523 info_message->setMessageType(type); 1524 info_message->setText(text); 1525 info_message->animatedShow(); 1526 if (type != KMessageWidget::Error && m_currentMessageAction == nullptr) { 1527 m_hideTimer.start(); 1528 } 1529 } 1530 1531 void TextBasedEdit::openClip(std::shared_ptr<ProjectClip> clip) 1532 { 1533 if (m_speechJob && m_speechJob->state() == QProcess::Running) { 1534 // TODO: ask for job cancellation 1535 return; 1536 } 1537 if (clip && clip->isValid() && clip->hasAudio()) { 1538 qDebug() << "====== OPENING CLIP: " << clip->clipName(); 1539 QString refId = clip->getProducerProperty(QStringLiteral("kdenlive:baseid")); 1540 if (!refId.isEmpty() && refId == m_refId) { 1541 // We opened a resulting playlist, do not clear text edit 1542 // TODO: this is broken. We should try reading the kdenlive:speech data from the sequence xml 1543 return; 1544 } 1545 if (!m_visualEditor->toPlainText().isEmpty()) { 1546 m_visualEditor->cleanup(); 1547 } 1548 QString speech; 1549 QList<QPoint> cutZones; 1550 m_binId = refId.isEmpty() ? clip->binId() : refId; 1551 if (!refId.isEmpty()) { 1552 // this is a clip playlist with a bin reference, fetch it 1553 m_refId = refId; 1554 std::shared_ptr<ProjectClip> refClip = pCore->bin()->getBinClip(refId); 1555 if (refClip) { 1556 speech = refClip->getProducerProperty(QStringLiteral("kdenlive:speech")); 1557 clipNameLabel->setText(refClip->clipName()); 1558 } 1559 QStringList zones = clip->getProducerProperty("kdenlive:cutzones").split(QLatin1Char(';')); 1560 for (const QString &z : qAsConst(zones)) { 1561 cutZones << QPoint(z.section(QLatin1Char(':'), 0, 0).toInt(), z.section(QLatin1Char(':'), 1, 1).toInt()); 1562 } 1563 } else { 1564 m_refId.clear(); 1565 speech = clip->getProducerProperty(QStringLiteral("kdenlive:speech")); 1566 clipNameLabel->setText(clip->clipName()); 1567 } 1568 if (speech.isEmpty()) { 1569 // Nothing else to do 1570 button_insert->setEnabled(false); 1571 button_start->setEnabled(true); 1572 return; 1573 } 1574 m_visualEditor->insertHtml(speech); 1575 if (!cutZones.isEmpty()) { 1576 m_visualEditor->processCutZones(cutZones); 1577 } 1578 m_visualEditor->rebuildZones(); 1579 button_insert->setEnabled(true); 1580 button_start->setEnabled(true); 1581 } else { 1582 button_start->setEnabled(false); 1583 clipNameLabel->clear(); 1584 m_visualEditor->cleanup(); 1585 } 1586 applyFontSize(); 1587 } 1588 1589 void TextBasedEdit::addBookmark() 1590 { 1591 std::shared_ptr<ProjectClip> clip = pCore->bin()->getBinClip(m_binId); 1592 if (clip) { 1593 QString txt = m_visualEditor->textCursor().selectedText(); 1594 QTextCursor cursor = m_visualEditor->textCursor(); 1595 QString startAnchor = m_visualEditor->selectionStartAnchor(cursor, -1, -1); 1596 cursor = m_visualEditor->textCursor(); 1597 QString endAnchor = m_visualEditor->selectionEndAnchor(cursor, -1, -1); 1598 if (startAnchor.isEmpty()) { 1599 showMessage(i18n("No timecode found in selection"), KMessageWidget::Information); 1600 return; 1601 } 1602 double ms = startAnchor.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 0, 0).toDouble(); 1603 int startPos = GenTime(ms).frames(pCore->getCurrentFps()); 1604 ms = endAnchor.section(QLatin1Char('#'), 1).section(QLatin1Char(':'), 1, 1).toDouble(); 1605 int endPos = GenTime(ms).frames(pCore->getCurrentFps()); 1606 int monitorPos = pCore->getMonitor(Kdenlive::ClipMonitor)->position(); 1607 qDebug() << "==== GOT MARKER: " << txt << ", FOR POS: " << startPos << "-" << endPos << ", MON: " << monitorPos; 1608 if (monitorPos > startPos && monitorPos < endPos) { 1609 // Monitor seek is on the selection, use the current frame 1610 pCore->bin()->addClipMarker(m_binId, {monitorPos}, {txt}); 1611 } else { 1612 pCore->bin()->addClipMarker(m_binId, {startPos}, {txt}); 1613 } 1614 } else { 1615 qDebug() << "==== NO CLIP FOR " << m_binId; 1616 } 1617 }