feat(youtube): add YouTube browser window and Whisper subtitle source

- Add `subminer youtube`/`yt` and tray "Browse YouTube" window that plays or queues picked videos in mpv
- Add `youtube.subtitleSource` (`youtube` | `whisper`) to transcribe YouTube audio via subtitle generation
- Allow the subtitle generation modal on YouTube videos
- Pick the real Japanese track over machine-translated ones and fall back to yt-dlp on HTTP 429
- Remove legacy launcher youtubeSubgen/whisper/AI args and `--youtube-mode`
- Fix EPIPE crash when the launching terminal is closed with Ctrl+C
- Thin media timing review handles with orange original-boundary bars
- Update docs and change fragments
This commit is contained in:
2026-09-28 20:46:11 -07:00
105 changed files with 2297 additions and 623 deletions
-6
View File
@@ -393,12 +393,6 @@
<span class="media-timing-review-original-boundary is-start"></span>
<span class="media-timing-review-original-boundary is-end"></span>
</div>
<span class="media-timing-review-original-label is-start" aria-hidden="true">
Line start
</span>
<span class="media-timing-review-original-label is-end" aria-hidden="true">
Line end
</span>
<div
id="mediaTimingReviewSelectedRange"
class="media-timing-review-selected-range"
+2 -2
View File
@@ -449,8 +449,8 @@ export function createMediaTimingReviewModal(
}
/**
* Moves an untouched clip end back to where the line's dialogue ends. The Line end
* rail keeps marking the subtitle timing, and Reset restores it.
* Moves an untouched clip end back to where the line's dialogue ends. The orange
* end bar keeps marking the subtitle timing, and Reset restores it.
*/
function trimTrailingSilence(peaks: readonly number[]): void {
if (!payload || !trailingTrimPending || previewPlaying || previewRequest.isInFlight()) {
+3 -2
View File
@@ -88,7 +88,7 @@ export function createSubtitleGenerationModal(
const vad = snapshot ? describeGenerationVad(snapshot.vad) : null;
const tools = snapshot ? describeGenerationTools(snapshot.tools) : null;
const readyMessage = !snapshot?.mediaPath
? 'Open local media to generate subtitles.'
? 'Open a local file or a YouTube video to generate subtitles.'
: !tools?.ready
? 'Install the missing tools or set their paths in Settings, then click Check again.'
: !model?.ready
@@ -96,7 +96,8 @@ export function createSubtitleGenerationModal(
: !vad?.ready
? 'Download the speech detection model or uncheck Focus on spoken dialogue.'
: 'Ready when you are.';
dom.media.textContent = snapshot?.mediaPath ?? 'Open a local media file in the player first.';
dom.media.textContent =
snapshot?.mediaPath ?? 'Open a local file or a YouTube video in the player first.';
dom.tools.textContent = tools?.text ?? 'Checking local tools...';
dom.model.textContent = model?.text ?? 'Checking local models...';
dom.modelPicker.classList.toggle('hidden', !snapshot || Boolean(snapshot.externalModelPath));
+20 -47
View File
@@ -1763,34 +1763,6 @@ body:focus-visible,
right: 0;
}
.media-timing-review-original-label {
position: absolute;
z-index: 7;
padding: 3px 5px;
border: 1px solid color-mix(in srgb, var(--ctp-crust) 35%, transparent);
border-radius: 4px;
background: var(--ctp-peach);
box-shadow: 0 2px 6px color-mix(in srgb, var(--ctp-crust) 55%, transparent);
color: var(--ctp-crust);
font-size: 8px;
font-weight: 800;
letter-spacing: 0.06em;
line-height: 1;
pointer-events: none;
white-space: nowrap;
text-transform: uppercase;
}
.media-timing-review-original-label.is-start {
top: 5px;
left: calc(var(--original-start) + 7px);
}
.media-timing-review-original-label.is-end {
right: calc(100% - var(--original-end) + 7px);
bottom: 5px;
}
.media-timing-review-track.is-loading::after {
position: absolute;
z-index: 2;
@@ -1856,61 +1828,62 @@ body:focus-visible,
}
}
/* A thin line centered on the clip edge, so the line itself is the trim boundary. */
.media-timing-review-handle {
position: absolute;
z-index: 6;
top: 0;
bottom: 0;
width: 14px;
border: 1px solid var(--ctp-teal);
background: linear-gradient(
180deg,
var(--ctp-teal),
color-mix(in srgb, var(--ctp-teal) 74%, var(--ctp-crust))
);
width: 3px;
background: var(--ctp-teal);
cursor: ew-resize;
touch-action: none;
transform: translateX(-50%);
transition: box-shadow 130ms ease;
}
/* Widens the grab area past the visible bracket without moving the clip edge. */
/* Widens the grab area past the visible line without moving the clip edge. */
.media-timing-review-handle::before {
position: absolute;
inset: 0 -7px;
inset: 0 -8px;
content: '';
}
/* Grip knob, symmetric about the line so it never reads as a separate edge. */
.media-timing-review-handle::after {
position: absolute;
top: 50%;
left: 50%;
width: 6px;
height: 18px;
width: 7px;
height: 16px;
content: '';
border-inline: 1px solid color-mix(in srgb, var(--ctp-crust) 55%, transparent);
border: 1px solid color-mix(in srgb, var(--ctp-crust) 55%, transparent);
border-radius: 4px;
background: var(--ctp-teal);
transform: translate(-50%, -50%);
pointer-events: none;
}
.media-timing-review-handle-start {
left: var(--selection-start);
border-radius: 5px 0 0 5px;
}
.media-timing-review-handle-end {
left: var(--selection-end);
border-radius: 0 5px 5px 0;
transform: translateX(-100%);
}
.media-timing-review-handle:hover {
box-shadow: 0 0 16px color-mix(in srgb, var(--ctp-teal) 60%, transparent);
box-shadow: 0 0 10px color-mix(in srgb, var(--ctp-teal) 70%, transparent);
}
.media-timing-review-handle:focus-visible {
outline: 2px solid transparent;
box-shadow:
inset 0 0 0 2px var(--ctp-yellow),
0 0 16px color-mix(in srgb, var(--ctp-yellow) 55%, transparent);
background: var(--ctp-yellow);
box-shadow: 0 0 12px color-mix(in srgb, var(--ctp-yellow) 60%, transparent);
}
.media-timing-review-handle:focus-visible::after {
background: var(--ctp-yellow);
}
.media-timing-review-expand-row span {