mirror of
https://github.com/zotero/zotero.git
synced 2026-10-10 03:28:10 +00:00
Read Aloud: Support highlight granularity customization & timestamps
This commit is contained in:
parent
ccbeab61e2
commit
5a785c4e0f
6 changed files with 83 additions and 9 deletions
|
|
@ -222,6 +222,23 @@
|
|||
</vbox>
|
||||
</groupbox>
|
||||
|
||||
<groupbox id="zotero-prefpane-read-aloud-groupbox" aria-labelledby="preferences-read-aloud-title">
|
||||
<label><html:h2 id="preferences-read-aloud-title" data-l10n-id="preferences-read-aloud-title"/></label>
|
||||
|
||||
<hbox align="center">
|
||||
<label data-l10n-id="preferences-read-aloud-highlight-granularity" control="read-aloud-highlight-granularity-menulist"/>
|
||||
<menulist id="read-aloud-highlight-granularity-menulist"
|
||||
preference="extensions.zotero.reader.readAloud.highlightGranularity"
|
||||
native="true">
|
||||
<menupopup>
|
||||
<menuitem data-l10n-id="preferences-read-aloud-highlight-granularity-paragraph" value="paragraph"/>
|
||||
<menuitem data-l10n-id="preferences-read-aloud-highlight-granularity-sentence" value="sentence"/>
|
||||
<menuitem data-l10n-id="preferences-read-aloud-highlight-granularity-word" value="word"/>
|
||||
</menupopup>
|
||||
</menulist>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
||||
<groupbox id="zotero-prefpane-note-groupbox" aria-labelledby="preferences-note-title">
|
||||
<label><html:h2 id="preferences-note-title" data-l10n-id="preferences-note-title"/></label>
|
||||
<checkbox id="open-note-in-new-window"
|
||||
|
|
|
|||
|
|
@ -264,6 +264,7 @@ class ReaderInstance {
|
|||
readAloudVoices: this._getReadAloudVoices(),
|
||||
readAloudEnabledVoices: await this._getReadAloudEnabledVoices(),
|
||||
readAloudRemoteInterface: this._getReadAloudRemoteInterface(this._iframeWindow),
|
||||
readAloudHighlightGranularity: Zotero.Prefs.get('reader.readAloud.highlightGranularity'),
|
||||
getSDTPack: this._createGetSDTPack(this._iframeWindow),
|
||||
loggedIn: Zotero.Sync.Runner.enabled,
|
||||
onOpenContextMenu: () => {
|
||||
|
|
@ -662,6 +663,7 @@ class ReaderInstance {
|
|||
Zotero.Prefs.registerObserver('reader.autoDisableTool.text', this._handleAutoDisableToolPrefChange),
|
||||
Zotero.Prefs.registerObserver('reader.autoDisableTool.image', this._handleAutoDisableToolPrefChange),
|
||||
Zotero.Prefs.registerObserver('reader.readAloudVoices', this._handleReadAloudVoicesPrefChange),
|
||||
Zotero.Prefs.registerObserver('reader.readAloud.highlightGranularity', this._handleReadAloudHighlightGranularityChange),
|
||||
];
|
||||
|
||||
return true;
|
||||
|
|
@ -1223,6 +1225,12 @@ class ReaderInstance {
|
|||
this._internalReader.setReadAloudVoices(Cu.cloneInto(this._getReadAloudVoices(), this._iframeWindow));
|
||||
};
|
||||
|
||||
_handleReadAloudHighlightGranularityChange = () => {
|
||||
this._internalReader.setReadAloudHighlightGranularity(
|
||||
Zotero.Prefs.get('reader.readAloud.highlightGranularity')
|
||||
);
|
||||
};
|
||||
|
||||
_handleReadAloudEnabledVoicesChange = async (voices) => {
|
||||
if (!voices) {
|
||||
voices = await this._getReadAloudEnabledVoices();
|
||||
|
|
@ -1729,7 +1737,19 @@ class ReaderInstance {
|
|||
cache = await audioCache;
|
||||
let cached = await cache.match(cacheURL);
|
||||
if (cached) {
|
||||
resolve(Cu.cloneInto({ audio: await cached.blob() }, targetWindow));
|
||||
// Word-level timestamps are cached in a response header
|
||||
// alongside the audio (see below)
|
||||
let timestamps;
|
||||
let timestampsHeader = cached.headers.get('X-Zotero-Timestamps');
|
||||
if (timestampsHeader) {
|
||||
try {
|
||||
timestamps = JSON.parse(timestampsHeader);
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
}
|
||||
}
|
||||
resolve(Cu.cloneInto({ audio: await cached.blob(), timestamps }, targetWindow));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
@ -1743,7 +1763,13 @@ class ReaderInstance {
|
|||
// returned with Cache-Control: no-store)
|
||||
if (result.audio && cache && !result.noStore) {
|
||||
try {
|
||||
await cache.put(cacheURL, new Response(result.audio));
|
||||
// Put word-level timestamps in a header so they
|
||||
// get cached with the audio response
|
||||
let headers = {};
|
||||
if (result.timestamps) {
|
||||
headers['X-Zotero-Timestamps'] = JSON.stringify(result.timestamps);
|
||||
}
|
||||
await cache.put(cacheURL, new Response(result.audio, { headers }));
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
|
|
@ -1775,7 +1801,7 @@ class ReaderInstance {
|
|||
// cacheVersion is included so that a server-side version bump changes the
|
||||
// key, causing old entries to miss and the correct audio to be re-fetched.
|
||||
_getReadAloudCacheURL(segment, voice) {
|
||||
let params = { voice: voice.id, text: segment.text, cacheVersion: voice.cacheVersion };
|
||||
let params = { voice: voice.id, text: segment.text, cacheVersion: voice.cacheVersion, timestamps: 1 };
|
||||
return 'https://read-aloud.zotero.invalid/audio?' + new URLSearchParams(params);
|
||||
}
|
||||
|
||||
|
|
@ -1797,8 +1823,10 @@ class ReaderInstance {
|
|||
}
|
||||
let cache = await audioCache;
|
||||
for (let request of await cache.keys()) {
|
||||
let version = new URL(request.url).searchParams.get('cacheVersion');
|
||||
if (!validVersions.has(version)) {
|
||||
let params = new URL(request.url).searchParams;
|
||||
// Drop entries stored in the pre-timestamp format, plus those
|
||||
// whose cacheVersion is no longer offered by the server
|
||||
if (params.get('timestamps') !== '1' || !validVersions.has(params.get('cacheVersion'))) {
|
||||
await cache.delete(request);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -672,13 +672,16 @@ Zotero.Sync.APIClient.prototype = {
|
|||
let method;
|
||||
let url;
|
||||
let options = {
|
||||
responseType: "blob",
|
||||
// Could be audio bytes or a JSON { audioURL, timestamps } object
|
||||
// Inspect the Content-Type before parsing
|
||||
responseType: "arraybuffer",
|
||||
errorDelayMax: 8000,
|
||||
};
|
||||
if (segment === 'sample') {
|
||||
method = "GET";
|
||||
let params = new URLSearchParams();
|
||||
params.set('voice', voiceID);
|
||||
params.set('timestamps', '1');
|
||||
url = this.baseURL + "tts/sample?" + params;
|
||||
options.noAPIKey = true;
|
||||
}
|
||||
|
|
@ -691,20 +694,36 @@ Zotero.Sync.APIClient.prototype = {
|
|||
options.body = JSON.stringify({
|
||||
voice: voiceID,
|
||||
text: segment.text,
|
||||
timestamps: 1,
|
||||
});
|
||||
}
|
||||
try {
|
||||
let xmlhttp = await this.makeRequest(method, url, options);
|
||||
let contentType = xmlhttp.getResponseHeader('Content-Type') || '';
|
||||
let cacheControl = xmlhttp.getResponseHeader('Cache-Control') || '';
|
||||
let noStore = /(?:^|,)\s*no-store\s*(?:,|$)/i.test(cacheControl);
|
||||
return { audio: xmlhttp.response, noStore };
|
||||
if (contentType.includes('application/json')) {
|
||||
let json = JSON.parse(new TextDecoder().decode(xmlhttp.response));
|
||||
let audioResponse = await Zotero.HTTP.request('GET', json.audioURL, {
|
||||
responseType: "blob",
|
||||
errorDelayMax: 8000,
|
||||
});
|
||||
return { audio: audioResponse.response, timestamps: json.timestamps, noStore };
|
||||
}
|
||||
return { audio: new Blob([xmlhttp.response], { type: contentType }), noStore };
|
||||
}
|
||||
catch (e) {
|
||||
Zotero.logError(e);
|
||||
|
||||
let error;
|
||||
if (e instanceof Zotero.HTTP.UnexpectedStatusException && e.status === 402) {
|
||||
let body = await e.xmlhttp.response?.text();
|
||||
let body = null;
|
||||
try {
|
||||
body = new TextDecoder().decode(e.xmlhttp.response);
|
||||
}
|
||||
catch {
|
||||
// Ignore
|
||||
}
|
||||
error = body === 'daily_limit_exceeded' ? 'daily-limit-exceeded' : 'quota-exceeded';
|
||||
}
|
||||
else if (e instanceof Zotero.HTTP.BrowserOfflineException) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,15 @@ preferences-reader-ebook-font = Ebook font:
|
|||
preferences-reader-ebook-hyphenate =
|
||||
.label = Enable automatic hyphenation
|
||||
|
||||
preferences-read-aloud-title = Read Aloud
|
||||
preferences-read-aloud-highlight-granularity = While reading, highlight:
|
||||
preferences-read-aloud-highlight-granularity-paragraph =
|
||||
.label = Paragraphs
|
||||
preferences-read-aloud-highlight-granularity-sentence =
|
||||
.label = Sentences
|
||||
preferences-read-aloud-highlight-granularity-word =
|
||||
.label = Words
|
||||
|
||||
preferences-note-title = Notes
|
||||
preferences-note-open-in-new-window =
|
||||
.label = Open notes in new windows instead of tabs
|
||||
|
|
|
|||
|
|
@ -249,6 +249,7 @@ pref("extensions.zotero.reader.autoDisableTool.note", true);
|
|||
pref("extensions.zotero.reader.autoDisableTool.text", true);
|
||||
pref("extensions.zotero.reader.autoDisableTool.image", true);
|
||||
pref("extensions.zotero.reader.lastSidebarTab", "annotations");
|
||||
pref("extensions.zotero.reader.readAloud.highlightGranularity", "sentence");
|
||||
|
||||
// Set color scheme to auto by default
|
||||
pref("browser.theme.toolbar-theme", 2);
|
||||
|
|
|
|||
2
reader
2
reader
|
|
@ -1 +1 @@
|
|||
Subproject commit 8cb29632079c26a920d08590643d7242c72c1986
|
||||
Subproject commit c12c65e3f01414ae244f6102da4028c700cf6584
|
||||
Loading…
Add table
Reference in a new issue