Skip to content

Commit

Permalink
piper
Browse files Browse the repository at this point in the history
  • Loading branch information
ken107 committed Mar 12, 2024
1 parent decbf35 commit 3cfebb1
Show file tree
Hide file tree
Showing 11 changed files with 393 additions and 30 deletions.
6 changes: 6 additions & 0 deletions _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
"options_voicegroup_offline": {
"message": "Offline Voices"
},
"options_voicegroup_piper": {
"message": "Piper Voices"
},
"options_voicegroup_standard": {
"message": "Standard Voices"
},
Expand All @@ -29,6 +32,9 @@
"options_enable_custom_voices": {
"message": "Enable Custom Voices"
},
"options_enable_piper_voices": {
"message": "Manage Piper Voices"
},
"options_rate_label": {
"message": "Speed"
},
Expand Down
26 changes: 25 additions & 1 deletion js/defaults.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ function lazy(get) {
return () => value || (value = get())
}

function immediate(get) {
return get()
}

function getQueryString() {
return location.search ? parseQueryString(location.search) : {};
}
Expand Down Expand Up @@ -148,7 +152,7 @@ function clearState(key) {
*/
function getVoices(opts) {
if (!opts) opts = {}
return getSettings(["awsCreds", "gcpCreds", "openaiCreds", "azureCreds"])
return getSettings(["awsCreds", "gcpCreds", "openaiCreds", "azureCreds", "piperVoices"])
.then(function(settings) {
return Promise.all([
browserTtsEngine.getVoices(),
Expand All @@ -166,6 +170,7 @@ function getVoices(opts) {
phoneTtsEngine.getVoices(),
settings.openaiCreds ? openaiTtsEngine.getVoices() : [],
settings.azureCreds ? azureTtsEngine.getVoices() : [],
settings.piperVoices || [],
])
})
.then(function(arr) {
Expand Down Expand Up @@ -233,6 +238,10 @@ function isAzure(voice) {
return /^Azure /.test(voice.voiceName);
}

function isPiperVoice(voice) {
return /^Piper /.test(voice.voiceName)
}

function isUseMyPhone(voice) {
return voice.isUseMyPhone == true
}
Expand Down Expand Up @@ -964,6 +973,21 @@ function escapeXml(unsafe) {
})
}

function makePlaybackControl(initialState) {
const subject = new rxjs.BehaviorSubject(initialState)
return {
getState() {
return subject.getValue()
},
setState(state) {
subject.next(state)
},
wait(condition) {
return rxjs.firstValueFrom(subject.pipe(rxjs.filter(condition)))
}
}
}

var languageTable = (function() {
const nameFromCode = new Map([
['af', 'Afrikaans'],
Expand Down
13 changes: 12 additions & 1 deletion js/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ var handlers = {
seek: seek,
reportIssue: reportIssue,
authWavenet: authWavenet,
managePiperVoices,
}

registerMessageListener("serviceWorker", handlers)
Expand Down Expand Up @@ -360,6 +361,15 @@ async function openPdfViewer(tabId, pdfUrl) {
await new Promise(f => handlers.pdfViewerCheckIn = f)
}

async function managePiperVoices() {
const result = await sendToPlayer({method: "managePiperVoices"}).catch(err => false)
if (result != "OK") {
if (result == "POPOUT") await sendToPlayer({method: "close"})
await injectPlayer()
await sendToPlayer({method: "managePiperVoices"})
}
}



async function contentScriptAlreadyInjected(tab, frameId) {
Expand Down Expand Up @@ -400,8 +410,9 @@ async function injectContentScript(tab, frameId, extraScripts) {
}

async function injectPlayer(tab) {
const settings = await getSettings(["useEmbeddedPlayer", "piperVoices"])
const promise = new Promise(f => handlers.playerCheckIn = f)
if ((await getSettings()).useEmbeddedPlayer) {
if (tab && settings.useEmbeddedPlayer && (settings.piperVoices || []).length == 0) {
try {
if (tab.incognito) {
//https://developer.chrome.com/docs/extensions/mv3/manifest/incognito/
Expand Down
71 changes: 71 additions & 0 deletions js/messaging.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,74 @@ function errorToJson(err) {
return err
}
}



function makeDispatcher(myAddress, handlers) {
const pendingRequests = new Map();
return {
waitForResponse(requestId) {
let pending = pendingRequests.get(requestId);
if (!pending)
pendingRequests.set(requestId, pending = makePending());
return pending.promise;
},
dispatch(message, sender, sendResponse) {
switch (message.type) {
case "request": return handleRequest(message, sender, sendResponse);
case "notification": return handleNotification(message, sender);
case "response": return handleResponse(message);
}
},
updateHandlers(newHandlers) {
handlers = newHandlers;
}
};
function makePending() {
const pending = {};
pending.promise = new Promise((fulfill, reject) => {
pending.fulfill = fulfill;
pending.reject = reject;
});
return pending;
}
function handleRequest(req, sender, sendResponse) {
if (req.to == myAddress) {
if (handlers[req.method]) {
Promise.resolve()
.then(() => handlers[req.method](req.args, sender))
.then(result => sendResponse({ type: "response", id: req.id, result, error: undefined }), error => sendResponse({ type: "response", id: req.id, result: undefined, error }));
//let caller know that sendResponse will be called asynchronously
return true;
}
else {
console.error("No handler for method", req);
}
}
}
function handleNotification(ntf, sender) {
if (ntf.to == myAddress) {
if (handlers[ntf.method]) {
Promise.resolve()
.then(() => handlers[ntf.method](ntf.args, sender))
.catch(error => console.error("Failed to handle notification", ntf, error));
}
else {
console.error("No handler for method", ntf);
}
}
}
function handleResponse(res) {
const pending = pendingRequests.get(res.id);
if (pending) {
pendingRequests.delete(res.id);
if (res.error)
pending.reject(res.error);
else
pending.fulfill(res.result);
}
else {
console.error("Stray response", res);
}
}
}
21 changes: 20 additions & 1 deletion js/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
var voiceName = $(this).val();
if (voiceName == "@custom") location.href = "custom-voices.html";
else if (voiceName == "@premium") brapi.tabs.create({url: "premium-voices.html"});
else if (voiceName == "@piper") bgPageInvoke("managePiperVoices").catch(console.error)
else updateSettings({voiceName})
});
$("#languages-edit-button")
Expand Down Expand Up @@ -282,14 +283,16 @@

//group by standard/premium
var groups = Object.assign({
piper: [],
offline: [],
premium: [],
standard: [],
},
voices.groupBy(function(voice) {
if (isPiperVoice(voice)) return "piper"
if (isOfflineVoice(voice)) return "offline"
if (isPremiumVoice(voice)) return "premium";
else return "standard";
return "standard"
}))
for (var name in groups) groups[name].sort(voiceSorter);

Expand All @@ -304,6 +307,22 @@
.appendTo(offline)
}

//create piper group
$("<optgroup>").appendTo("#voices")
const piper = $("<optgroup>")
.attr("label", brapi.i18n.getMessage("options_voicegroup_piper"))
.appendTo("#voices")
for (const voice of groups.piper) {
$("<option>")
.val(voice.voiceName)
.text(voice.voiceName)
.appendTo(piper)
}
$("<option>")
.val("@piper")
.text(brapi.i18n.getMessage("options_enable_piper_voices"))
.appendTo(piper)

//create the standard optgroup
$("<optgroup>").appendTo($("#voices"))
var standard = $("<optgroup>")
Expand Down
65 changes: 65 additions & 0 deletions js/player.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,43 @@

const isEmbedded = top != self
var queryString = new URLSearchParams(location.search)
var activeDoc;
var playbackError = null;
var closeTabTimer = queryString.has("autoclose") && startTimer(5*60*1000, closePlayer)


const piperSubject = new rxjs.Subject()
const piperObservable = rxjs.defer(() => {
createPiperFrame()
return piperSubject
})
.pipe(
rxjs.shareReplay({bufferSize: 1, refCount: false})
)
const piperCallbacks = new rxjs.Subject()
const domDispatcher = makeDispatcher("piper-host", {
advertiseVoices({voices}, sender) {
updateSettings({piperVoices: voices})
piperSubject.next(sender)
},
onSentence: args => piperCallbacks.next({type: "sentence", ...args}),
onParagraph: args => piperCallbacks.next({type: "paragraph", ...args}),
onEnd: args => piperCallbacks.next({type: "end", ...args}),
onError: args => piperCallbacks.next({type: "error", ...args}),
})
window.addEventListener("message", event => {
const send = message => event.source.postMessage(message, {targetOrigin: event.origin})
const sender = {
sendRequest(method, args) {
const id = String(Math.random())
send({to: "piper-service", type: "request", id, method, args})
return domDispatcher.waitForResponse(id)
}
}
domDispatcher.dispatch(event.data, sender, send)
})


var messageHandlers = {
playText: playText,
playTab: playTab,
Expand All @@ -19,6 +52,7 @@ var messageHandlers = {
shouldPlaySilence: shouldPlaySilence.bind({}),
startPairing: () => phoneTtsEngine.startPairing(),
isPaired: () => phoneTtsEngine.isPaired(),
managePiperVoices,
}

registerMessageListener("player", messageHandlers)
Expand Down Expand Up @@ -280,3 +314,34 @@ async function shouldPlaySilence(providerId) {
}
}
}

function managePiperVoices() {
if (isEmbedded) {
return "POPOUT"
}
else {
rxjs.firstValueFrom(piperObservable)
.catch(console.error)
brapi.tabs.getCurrent()
.then(tab => Promise.all([
brapi.windows.update(tab.windowId, {focused: true}),
brapi.tabs.update(tab.id, {active: true})
]))
.catch(console.error)
return "OK"
}
}

function createPiperFrame() {
const f = document.createElement("iframe")
f.id = "piper-frame"
f.src = "http://localhost:8080/"
f.allow = "cross-origin-isolated"
f.style.position = "absolute"
f.style.left =
f.style.top = "0"
f.style.width =
f.style.height = "100%"
f.style.borderWidth = "0"
document.body.appendChild(f)
}
Loading

0 comments on commit 3cfebb1

Please sign in to comment.