mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Merge 986b8871f4 into ff93139cce
This commit is contained in:
commit
e10ac2b835
2 changed files with 142 additions and 0 deletions
|
|
@ -617,7 +617,99 @@ Zotero.Sync.APIClient.prototype = {
|
|||
},
|
||||
|
||||
|
||||
// Hidden prefs for Read Aloud custom endpoints:
|
||||
// extensions.zotero.reader.readAloud.customEndpoint
|
||||
// "{mode}=[url]". Empty/invalid falls back to the normal Zotero API
|
||||
// endpoint. Only "openai" is currently supported as a mode, where the
|
||||
// server must implement OpenAI's /audio/speech
|
||||
// (e.g. https://api.openai.com/v1).
|
||||
// extensions.zotero.reader.readAloud.customEndpoint.voices
|
||||
// Comma-separated voice IDs (openai mode only). Defaults to the
|
||||
// OpenAI TTS voice list.
|
||||
// extensions.zotero.reader.readAloud.customEndpoint.apiKey
|
||||
// Sent as "Authorization: Bearer <key>" (openai mode only).
|
||||
// extensions.zotero.reader.readAloud.customEndpoint.model
|
||||
// Model name sent in the request body (openai mode only).
|
||||
_getReadAloudCustomEndpoint() {
|
||||
let pref = Zotero.Prefs.get('reader.readAloud.customEndpoint');
|
||||
if (!pref || typeof pref !== 'string') return null;
|
||||
let match = pref.match(/^(\w+)=(.+)$/);
|
||||
if (!match) return null;
|
||||
let [, mode, url] = match;
|
||||
if (mode !== 'openai') {
|
||||
Zotero.debug(`Unsupported customEndpoint mode '${mode}'`);
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line no-new
|
||||
new URL(url);
|
||||
}
|
||||
catch {
|
||||
Zotero.debug('Invalid customEndpoint pref value: ' + pref);
|
||||
return null;
|
||||
}
|
||||
if (!url.endsWith('/')) {
|
||||
url += '/';
|
||||
}
|
||||
let voicesPref = Zotero.Prefs.get('reader.readAloud.customEndpoint.voices');
|
||||
let voices = typeof voicesPref === 'string'
|
||||
? voicesPref.split(',').map(s => s.trim()).filter(Boolean)
|
||||
: null;
|
||||
let apiKey = Zotero.Prefs.get('reader.readAloud.customEndpoint.apiKey');
|
||||
if (typeof apiKey !== 'string' || !apiKey) {
|
||||
apiKey = null;
|
||||
}
|
||||
let model = Zotero.Prefs.get('reader.readAloud.customEndpoint.model');
|
||||
if (typeof model !== 'string' || !model) {
|
||||
model = 'gpt-4o-mini-tts';
|
||||
}
|
||||
return { mode, url, voices, apiKey, model };
|
||||
},
|
||||
|
||||
|
||||
async getReadAloudVoices() {
|
||||
let custom = this._getReadAloudCustomEndpoint();
|
||||
if (custom?.mode === 'openai') {
|
||||
// Fall back to OpenAI's voice IDs
|
||||
let voiceIDs = custom.voices ?? [
|
||||
'alloy',
|
||||
'ash',
|
||||
'ballad',
|
||||
'coral',
|
||||
'echo',
|
||||
'fable',
|
||||
'nova',
|
||||
'onyx',
|
||||
'sage',
|
||||
'shimmer',
|
||||
'verse',
|
||||
];
|
||||
let voices = Object.fromEntries(voiceIDs.map(id => [id, { label: id }]));
|
||||
// Derive a cache version from the endpoint and model so that
|
||||
// changing either one invalidates previously cached audio
|
||||
let cacheVersion = parseInt(
|
||||
Zotero.Utilities.Internal.md5(custom.url + '|' + custom.model).slice(0, 8),
|
||||
16
|
||||
);
|
||||
// OpenAI's TTS endpoint auto-detects language from the input
|
||||
// text, so use the wildcard locale.
|
||||
// The reader treats '*' as matching any language.
|
||||
return {
|
||||
voices: {
|
||||
standard: [{
|
||||
creditsPerMinute: 0,
|
||||
segmentGranularity: 'sentence',
|
||||
cacheVersion,
|
||||
voices,
|
||||
locales: { '*': voiceIDs },
|
||||
}],
|
||||
},
|
||||
standardCreditsRemaining: null,
|
||||
premiumCreditsRemaining: null,
|
||||
devMode: false,
|
||||
};
|
||||
}
|
||||
|
||||
let url = this.baseURL + "tts/voices";
|
||||
let params = new URLSearchParams();
|
||||
params.set("lang", Services.locale.appLocaleAsBCP47);
|
||||
|
|
@ -669,6 +761,47 @@ Zotero.Sync.APIClient.prototype = {
|
|||
|
||||
|
||||
async getReadAloudAudio(segment, voiceID) {
|
||||
let custom = this._getReadAloudCustomEndpoint();
|
||||
if (custom?.mode === 'openai') {
|
||||
let text = segment === 'sample'
|
||||
? "Isn't it nice to have a computer that will talk to you?"
|
||||
: segment.text;
|
||||
let headers = { "Content-Type": "application/json" };
|
||||
if (custom.apiKey) {
|
||||
headers.Authorization = `Bearer ${custom.apiKey}`;
|
||||
}
|
||||
try {
|
||||
let xmlhttp = await Zotero.HTTP.request("POST", custom.url + "audio/speech", {
|
||||
responseType: "blob",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
model: custom.model,
|
||||
voice: voiceID,
|
||||
input: text,
|
||||
// eslint-disable-next-line camelcase
|
||||
response_format: 'mp3',
|
||||
}),
|
||||
errorDelayMax: 8000,
|
||||
});
|
||||
return { audio: xmlhttp.response };
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
|
||||
let error;
|
||||
if (e instanceof Zotero.HTTP.BrowserOfflineException) {
|
||||
error = 'network';
|
||||
}
|
||||
else {
|
||||
error = 'unknown';
|
||||
}
|
||||
return {
|
||||
audio: null,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let method;
|
||||
let url;
|
||||
let options = {
|
||||
|
|
@ -741,6 +874,10 @@ Zotero.Sync.APIClient.prototype = {
|
|||
|
||||
|
||||
async getReadAloudCreditsRemaining() {
|
||||
let custom = this._getReadAloudCustomEndpoint();
|
||||
if (custom?.mode === 'openai') {
|
||||
return { standardCreditsRemaining: null, premiumCreditsRemaining: null };
|
||||
}
|
||||
let uri = this.baseURL + "tts/credits";
|
||||
try {
|
||||
let xmlhttp = await this.makeRequest("GET", uri, {
|
||||
|
|
@ -761,6 +898,10 @@ Zotero.Sync.APIClient.prototype = {
|
|||
|
||||
|
||||
async resetReadAloudCredits() {
|
||||
let custom = this._getReadAloudCustomEndpoint();
|
||||
if (custom?.mode === 'openai') {
|
||||
return { standardCreditsRemaining: null, premiumCreditsRemaining: null };
|
||||
}
|
||||
let uri = this.baseURL + "tts/reset";
|
||||
try {
|
||||
let xmlhttp = await this.makeRequest("POST", uri, {
|
||||
|
|
|
|||
|
|
@ -251,6 +251,7 @@ pref("extensions.zotero.reader.autoDisableTool.image", true);
|
|||
pref("extensions.zotero.reader.lastSidebarTab", "annotations");
|
||||
pref("extensions.zotero.reader.popupPositions", "{}");
|
||||
pref("extensions.zotero.reader.readAloud.highlightGranularity", "sentence");
|
||||
pref("extensions.zotero.reader.readAloud.customEndpoint.model", "gpt-4o-mini-tts");
|
||||
|
||||
// Set color scheme to auto by default
|
||||
pref("browser.theme.toolbar-theme", 2);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue