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