Skip to content

Commit 114840b

Browse files
chore: release v0.1.17
1 parent c62a244 commit 114840b

10 files changed

Lines changed: 154 additions & 24 deletions

File tree

electron/main.mjs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ let setAssistOverlayWindowFn = null;
6060
let refreshAssistOverlayStateFn = null;
6161
let setYouTubeWindowFn = null;
6262
let computeBackendCache = null;
63+
let computeBackendCacheMeta = {
64+
lastProbeAtMs: 0,
65+
lastProbeFailed: false,
66+
lastProbeReason: null,
67+
lastProbeSource: null,
68+
};
69+
const COMPUTE_BACKEND_REPROBE_COOLDOWN_MS = 10 * 60 * 1000;
6370
let batchNotificationTimer = null;
6471

6572
process.on('unhandledRejection', (reason) => {
@@ -900,6 +907,20 @@ async function detectComputeBackend(opts = {}) {
900907
} else {
901908
const reason = smoke?.data?.reason || smoke?.error || 'cuda_smoke_failed';
902909
logWarn('CUDA smoke test failed', { reason });
910+
if (smoke?.data) {
911+
logWarn('CUDA smoke test diagnostics', {
912+
deviceCount: smoke.data.cuda_device_count ?? null,
913+
deviceCountRaw: smoke.data.cuda_device_count_raw ?? null,
914+
deviceQueryError: smoke.data.cuda_device_query_error || undefined,
915+
supportedComputeTypes: smoke.data.supported_compute_types || [],
916+
supportedComputeTypesError: smoke.data.supported_compute_types_error || undefined,
917+
});
918+
}
919+
if (reason === 'no_cuda_device') {
920+
logWarn('GPU detected but CUDA device count = 0', {
921+
deviceCount: smoke?.data?.cuda_device_count ?? null,
922+
});
923+
}
903924
}
904925
if (smoke?.data?.inference_test) {
905926
logInfo('CUDA inference smoke test', {
@@ -947,24 +968,62 @@ async function detectComputeBackend(opts = {}) {
947968
};
948969
}
949970

971+
function shouldSkipComputeBackendProbe(opts = {}) {
972+
if (!computeBackendCache) return false;
973+
const forceRefresh = Boolean(opts.forceRefresh);
974+
if (!forceRefresh) return true;
975+
const userInitiated = Boolean(opts.userInitiated);
976+
if (userInitiated) return false;
977+
if (computeBackendCacheMeta?.lastProbeFailed) return true;
978+
const lastProbeAtMs = Number(computeBackendCacheMeta?.lastProbeAtMs || 0);
979+
if (lastProbeAtMs && Date.now() - lastProbeAtMs < COMPUTE_BACKEND_REPROBE_COOLDOWN_MS) {
980+
return true;
981+
}
982+
return false;
983+
}
984+
985+
function recordComputeBackendProbe(result, opts = {}, error = null) {
986+
const failed = Boolean(error) || !Boolean(result?.availableGpu);
987+
const reason =
988+
(result?.details && result.details.cuda_smoke && result.details.cuda_smoke.reason)
989+
|| result?.error
990+
|| (typeof error === 'string' ? error : (error ? String(error) : null));
991+
computeBackendCacheMeta = {
992+
lastProbeAtMs: Date.now(),
993+
lastProbeFailed: failed,
994+
lastProbeReason: reason || null,
995+
lastProbeSource: typeof opts.source === 'string' ? opts.source : null,
996+
};
997+
}
998+
950999
async function getComputeBackend(opts = {}) {
951-
if (computeBackendCache && !opts.forceRefresh) return computeBackendCache;
1000+
const logFn = typeof opts.log === 'function' ? opts.log : null;
1001+
const logInfo = (message, keys = {}) => logFn && logFn('INFO', 'compute', message, keys);
1002+
if (shouldSkipComputeBackendProbe(opts)) {
1003+
logInfo('Compute backend probe skipped', {
1004+
reason: computeBackendCacheMeta?.lastProbeReason || 'cached',
1005+
source: computeBackendCacheMeta?.lastProbeSource || 'unknown',
1006+
});
1007+
return computeBackendCache;
1008+
}
9521009
try {
9531010
computeBackendCache = await detectComputeBackend(opts);
1011+
recordComputeBackendProbe(computeBackendCache, opts, null);
9541012
} catch (e) {
9551013
computeBackendCache = {
9561014
availableGpu: false,
9571015
details: {},
9581016
error: String(e),
9591017
pythonPath: getPipelinePythonPath(),
9601018
};
1019+
recordComputeBackendProbe(computeBackendCache, opts, e);
9611020
}
9621021
return computeBackendCache;
9631022
}
9641023

9651024
async function refreshComputeBackend() {
9661025
computeBackendCache = null;
967-
return await getComputeBackend({ forceRefresh: true });
1026+
return await getComputeBackend({ forceRefresh: true, userInitiated: true, source: 'developer_refresh' });
9681027
}
9691028

9701029
/** Returns the configured outputs base directory (absolute). Ensures it exists. */
@@ -3621,7 +3680,7 @@ ipcMain.handle('pipeline:runPipeline', async (_event, payload) => {
36213680
effectiveDevice = 'cpu';
36223681
} else {
36233682
try {
3624-
computeInfo = await getComputeBackend({ forceRefresh: true, log });
3683+
computeInfo = await getComputeBackend({ forceRefresh: true, log, source: 'pipeline' });
36253684
} catch (e) {
36263685
computeInfo = null;
36273686
fallbackReason = String(e);

electron/secrets.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,34 @@ function extractLegacyGoogleOAuthClient(data) {
170170
return { clientId, ...(clientSecret ? { clientSecret } : {}) };
171171
}
172172

173+
function resolveBundledGoogleOAuthClientPath() {
174+
const candidates = [];
175+
if (app?.isPackaged && process.resourcesPath) {
176+
candidates.push(path.join(process.resourcesPath, 'assets', 'oauth', 'google_oauth_client.json'));
177+
candidates.push(path.join(process.resourcesPath, 'oauth', 'google_oauth_client.json'));
178+
}
179+
const appPath = typeof app?.getAppPath === 'function' ? app.getAppPath() : '';
180+
if (appPath) {
181+
candidates.push(path.join(appPath, 'assets', 'oauth', 'google_oauth_client.json'));
182+
}
183+
candidates.push(path.join(process.cwd(), 'assets', 'oauth', 'google_oauth_client.json'));
184+
for (const candidate of candidates) {
185+
if (candidate && fs.existsSync(candidate)) return candidate;
186+
}
187+
return null;
188+
}
189+
190+
export async function getBundledGoogleOAuthClient({ log } = {}) {
191+
const logger = typeof log === 'function' ? log : null;
192+
const filePath = resolveBundledGoogleOAuthClientPath();
193+
if (!filePath) return null;
194+
const data = safeReadJson(filePath, null);
195+
const parsed = extractLegacyGoogleOAuthClient(data);
196+
if (!parsed?.clientId) return null;
197+
if (logger) logger('[secrets] loaded bundled Google OAuth client');
198+
return { ...parsed, source: 'bundled', filePath };
199+
}
200+
173201
async function migrateLegacyGoogleOAuthClient({ log } = {}) {
174202
const logger = typeof log === 'function' ? log : console.log;
175203
const userDataDir = app.getPath('userData');

electron/youtube.mjs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ import crypto from 'node:crypto';
44
import fs from 'node:fs';
55
import path from 'node:path';
66
import { google } from 'googleapis';
7-
import { getGoogleOAuthClientWithFallback, getYouTubeTokens, redactSecrets, setYouTubeTokens } from './secrets.mjs';
7+
import {
8+
getBundledGoogleOAuthClient,
9+
getGoogleOAuthClientWithFallback,
10+
getYouTubeTokens,
11+
redactSecrets,
12+
setYouTubeTokens,
13+
} from './secrets.mjs';
814

915
const { app, shell } = electron;
1016

@@ -36,6 +42,8 @@ function parseTags(input) {
3642
let cachedTokens = null;
3743
let tokensLoaded = false;
3844

45+
const OAUTH_CLIENT_MISSING_CODE = 'OAUTH_CLIENT_MISSING';
46+
3947
async function loadTokensFromKeytar() {
4048
if (tokensLoaded) return cachedTokens;
4149
cachedTokens = await getYouTubeTokens();
@@ -85,9 +93,17 @@ export async function loadClientCredentials() {
8593
throw new Error('Missing Google OAuth client secret. Re-enter credentials in app settings.');
8694
}
8795

88-
throw new Error(
89-
'Google OAuth client is not configured on this device. Open Settings to add credentials or set GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET.',
90-
);
96+
const bundled = await getBundledGoogleOAuthClient();
97+
if (bundled?.clientId && bundled?.clientSecret) {
98+
if (!bundled.clientId.includes('.apps.googleusercontent.com') && !bundled.clientId.includes('@')) {
99+
throw new Error(
100+
`Invalid Client ID format. Expected format: numbers-letters.apps.googleusercontent.com. Got: ${bundled.clientId.slice(0, 50)}...`,
101+
);
102+
}
103+
return { clientId: bundled.clientId, clientSecret: bundled.clientSecret, source: 'bundled' };
104+
}
105+
106+
throw new Error(OAUTH_CLIENT_MISSING_CODE);
91107
}
92108

93109
export async function isConnected() {

electron/youtube_ipc.mjs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,17 @@ export function initYouTubeIpc(ipcMain, { appRoot, getOutputsDir, logToPipeline
9090
const fileExists = fs.existsSync(credsPath);
9191
const sourceLabel =
9292
creds?.source === 'env'
93-
? 'environment variables'
93+
? 'local override (environment variables)'
9494
: creds?.source === 'legacy'
9595
? 'legacy file (migrated to OS keychain)'
96-
: 'OS keychain';
96+
: creds?.source === 'bundled'
97+
? 'bundled app client'
98+
: 'OS keychain';
9799

98100
return {
99101
ok: true,
100102
hasCredentials: true,
103+
source: creds?.source || null,
101104
clientIdPrefix: creds.clientId.slice(0, 30) + '...',
102105
clientIdValid: creds.clientId.includes('.apps.googleusercontent.com'),
103106
filePath: credsPath,
@@ -113,7 +116,8 @@ export function initYouTubeIpc(ipcMain, { appRoot, getOutputsDir, logToPipeline
113116

114117
const errorMessage = redactSecrets(String(e?.message || e));
115118
const missingMessage =
116-
'Google OAuth client is not configured on this device. Open Settings > Developer mode to add credentials.';
119+
'YouTube connection is not available yet on this device. Please try again or contact support.';
120+
const isMissingClient = errorMessage.includes('OAUTH_CLIENT_MISSING');
117121

118122
return {
119123
ok: false,
@@ -123,7 +127,7 @@ export function initYouTubeIpc(ipcMain, { appRoot, getOutputsDir, logToPipeline
123127
fileExists,
124128
message: fileExists
125129
? `Credentials found but invalid: ${errorMessage}`
126-
: errorMessage || missingMessage,
130+
: (isMissingClient ? missingMessage : (errorMessage || missingMessage)),
127131
};
128132
}
129133
});

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "yt_uploader_app",
33
"private": true,
4-
"version": "0.1.16",
4+
"version": "0.1.17",
55
"type": "module",
66
"main": "electron/main.mjs",
77
"scripts": {

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9840,7 +9840,7 @@ const add = async () => {
98409840
} catch (e: any) {
98419841
console.error(e);
98429842
const msg = String(e?.message || e || t('youtubeConnectFailedDefault'));
9843-
if (msg.includes('Missing Google OAuth credentials')) {
9843+
if (msg.includes('OAUTH_CLIENT_MISSING')) {
98449844
setSnack(t('oauthCredentialsMissing'));
98459845
} else if (msg.includes('invalid_client') || msg.includes('401')) {
98469846
setSnack(t('oauthInvalidClientHelp'));

src/components/DeveloperModeDialog.tsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,6 @@ export default function DeveloperModeDialog({
159159
}
160160
}, [open, loadOutputsPath, loadDeveloperOptions]);
161161

162-
const handleComputeBackendPreferenceChange = (value: 'auto' | 'prefer_gpu' | 'force_cpu') => {
163-
setComputeBackendPreference(value);
164-
window.api?.setDeveloperOptions?.({ computeBackendPreference: value });
165-
};
166-
167162
const handleComputeBackendRefresh = async () => {
168163
try {
169164
setComputeBackendLoading(true);
@@ -178,6 +173,12 @@ export default function DeveloperModeDialog({
178173
}
179174
};
180175

176+
const handleComputeBackendPreferenceChange = (value: 'auto' | 'prefer_gpu' | 'force_cpu') => {
177+
setComputeBackendPreference(value);
178+
window.api?.setDeveloperOptions?.({ computeBackendPreference: value });
179+
handleComputeBackendRefresh();
180+
};
181+
181182
const savePythonPath = async (value: string) => {
182183
const trimmed = value.trim();
183184
setPythonPath(trimmed);
@@ -394,7 +395,7 @@ export default function DeveloperModeDialog({
394395
missing_cublas: 'missing cuBLAS DLL',
395396
missing_cudart: 'missing CUDA runtime DLL',
396397
missing_dll: 'missing CUDA DLL',
397-
no_cuda_device: 'no CUDA device',
398+
no_cuda_device: 'GPU detected, but no usable CUDA device from the current driver/runtime. Falling back to CPU. Update NVIDIA driver for CUDA 12 support.',
398399
cuda_no_float16: 'float16 not supported',
399400
cuda_unavailable: 'CUDA unavailable',
400401
cuda_probe_failed: 'CUDA probe failed',

src/i18n/locales/en.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,7 @@
598598
"credentialsValidationFailedWithMessage": "⚠️ {{message}}",
599599
"youtubeConnectedSuccess": "YouTube connected successfully!",
600600
"youtubeConnectFailedDefault": "YouTube connect failed",
601-
"oauthCredentialsMissing": "⚠️ Google OAuth client is not configured on this device. Open Settings > Developer mode to add credentials.",
601+
"oauthCredentialsMissing": "⚠️ YouTube connection is not available yet on this device. Please try again or contact support.",
602602
"oauthInvalidClientHelp": "❌ Error 401: invalid_client. Check: 1) OAuth client type = \"Desktop app\" (NOT \"Web application\"), 2) Client ID/Secret copied fully, 3) YouTube Data API v3 enabled, 4) OAuth consent screen configured. See GOOGLE_OAUTH_SETUP.md",
603603
"youtubeConnectFailedWithMessage": "YouTube connect failed: {{message}}",
604604
"retryingPipelineCount": "Retrying pipeline for {{count}} item(s)",

src/i18n/locales/ro.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@
534534
"credentialsValidationFailedWithMessage": "⚠️ {{message}}",
535535
"youtubeConnectedSuccess": "YouTube conectat cu succes!",
536536
"youtubeConnectFailedDefault": "Conectarea YouTube a eșuat",
537-
"oauthCredentialsMissing": "⚠️ Clientul Google OAuth nu este configurat pe acest dispozitiv. Deschide Setări > Developer mode pentru a adăuga credențialele.",
537+
"oauthCredentialsMissing": "⚠️ Conectarea YouTube nu este disponibilă încă pe acest dispozitiv. Încearcă din nou sau contactează suportul.",
538538
"oauthInvalidClientHelp": "❌ Eroare 401: invalid_client. Verifică: 1) Tip client OAuth = „Desktop app” (NU „Web application”), 2) Client ID/Secret copiate complet, 3) YouTube Data API v3 activat, 4) Ecranul de consimțământ OAuth configurat. Vezi GOOGLE_OAUTH_SETUP.md",
539539
"youtubeConnectFailedWithMessage": "Conectarea YouTube a eșuat: {{message}}",
540540
"retryingPipelineCount": "Se reîncearcă pipeline pentru {{count}} element(e)",

yt_pipeline/tools/cuda_smoke_test.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,11 @@ def main() -> int:
4343
"platform": platform.platform(),
4444
"ctranslate2_version": None,
4545
"cuda_device_count": 0,
46+
"cuda_device_count_raw": None,
47+
"cuda_device_query_error": None,
4648
"supported_compute_types": [],
49+
"supported_compute_types_error": None,
50+
"cuda_visible_devices": os.getenv("CUDA_VISIBLE_DEVICES"),
4751
"dll_check": {
4852
"status": "skipped",
4953
"missing": [],
@@ -72,14 +76,32 @@ def main() -> int:
7276
return _print_and_exit(result, started)
7377

7478
result["ctranslate2_version"] = getattr(ctranslate2, "__version__", None)
75-
count = int(ctranslate2.get_cuda_device_count() or 0)
79+
try:
80+
raw_count = ctranslate2.get_cuda_device_count()
81+
except Exception as exc:
82+
msg = f"{type(exc).__name__}: {exc}"
83+
result["cuda_device_query_error"] = msg
84+
result["error"] = msg
85+
result["reason"] = _classify_error(msg)
86+
return _print_and_exit(result, started)
87+
result["cuda_device_count_raw"] = raw_count
88+
count = int(raw_count or 0)
7689
result["cuda_device_count"] = count
90+
91+
try:
92+
supported = _safe_list(ctranslate2.get_supported_compute_types("cuda"))
93+
except Exception as exc:
94+
msg = f"{type(exc).__name__}: {exc}"
95+
result["supported_compute_types_error"] = msg
96+
supported = []
97+
result["supported_compute_types"] = supported
98+
7799
if count <= 0:
100+
if not result.get("error"):
101+
result["error"] = "CUDA device count returned 0"
78102
result["reason"] = "no_cuda_device"
79103
return _print_and_exit(result, started)
80104

81-
supported = _safe_list(ctranslate2.get_supported_compute_types("cuda"))
82-
result["supported_compute_types"] = supported
83105
if "float16" not in [s.lower() for s in supported]:
84106
result["reason"] = "cuda_no_float16"
85107
return _print_and_exit(result, started)

0 commit comments

Comments
 (0)