forked from Shinonome/dots-hyprland
screen translator: more error handling & refetch token when expired
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
pragma ComponentBehavior: Bound
|
||||||
|
import QtQuick
|
||||||
|
import Quickshell
|
||||||
|
import qs.modules.common.functions
|
||||||
|
import qs.modules.common.utils
|
||||||
|
import qs.services
|
||||||
|
import qs.modules.common
|
||||||
|
import ".."
|
||||||
|
|
||||||
|
NestableObject {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Done, Preparing, Processing, Error
|
||||||
|
}
|
||||||
|
|
||||||
|
signal finished()
|
||||||
|
signal error(message: string)
|
||||||
|
property int errorCode
|
||||||
|
property string errorMessage: ""
|
||||||
|
property var outputData
|
||||||
|
property var state: GCloudApi.State.Done
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
root.state = GCloudApi.State.Done;
|
||||||
|
root.errorMessage = "";
|
||||||
|
root.outputData = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleApiOutput(out: string): bool {
|
||||||
|
try {
|
||||||
|
root.outputData = JSON.parse(out);
|
||||||
|
if (outputData.error) {
|
||||||
|
print("API error: " + JSON.stringify(outputData.error, null, 2))
|
||||||
|
root.state = GCloudApi.State.Error;
|
||||||
|
root.errorCode = outputData.error.code;
|
||||||
|
root.errorMessage = outputData.error.message;
|
||||||
|
root.error(outputData.error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
root.finished();
|
||||||
|
root.state = GCloudApi.State.Done;
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
print("Failed to parse API response: " + e + "\n" + out)
|
||||||
|
root.state = GCloudApi.State.Error;
|
||||||
|
root.errorMessage = "Failed to parse API response";
|
||||||
|
root.error(root.errorMessage);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,17 +5,9 @@ import qs.modules.common.utils
|
|||||||
import qs.services
|
import qs.services
|
||||||
import ".."
|
import ".."
|
||||||
|
|
||||||
NestableObject {
|
GCloudApi {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
enum State {
|
|
||||||
Done, Preparing, Processing
|
|
||||||
}
|
|
||||||
|
|
||||||
signal finished()
|
|
||||||
property var outputData
|
|
||||||
property var state: GCloudTranslate.State.Done
|
|
||||||
|
|
||||||
property list<string> pendingStrings
|
property list<string> pendingStrings
|
||||||
property bool setupReady: false
|
property bool setupReady: false
|
||||||
readonly property bool preparationReady: GoogleCloud.tokenReady && setupReady
|
readonly property bool preparationReady: GoogleCloud.tokenReady && setupReady
|
||||||
@@ -24,13 +16,13 @@ NestableObject {
|
|||||||
GoogleCloud.load();
|
GoogleCloud.load();
|
||||||
root.setupReady = false;
|
root.setupReady = false;
|
||||||
root.pendingStrings = strings;
|
root.pendingStrings = strings;
|
||||||
root.state = GCloudTranslate.State.Preparing;
|
root.state = GCloudApi.State.Preparing;
|
||||||
root.setupReady = true;
|
root.setupReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onPreparationReadyChanged: {
|
onPreparationReadyChanged: {
|
||||||
if (!preparationReady) return;
|
if (!preparationReady) return;
|
||||||
root.state = GCloudTranslate.State.Processing;
|
root.state = GCloudApi.State.Processing;
|
||||||
|
|
||||||
const targetLang = Translation.languageCode;
|
const targetLang = Translation.languageCode;
|
||||||
const payload = {
|
const payload = {
|
||||||
@@ -53,11 +45,7 @@ NestableObject {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
seq.push(((out) => {
|
seq.push(((out) => {
|
||||||
// print(out)
|
root.handleApiOutput(out);
|
||||||
root.outputData = JSON.parse(out);
|
|
||||||
root.pendingStrings = [];
|
|
||||||
root.finished();
|
|
||||||
root.state = GCloudTranslate.State.Done;
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
multiproc.runSequence(seq);
|
multiproc.runSequence(seq);
|
||||||
|
|||||||
@@ -7,18 +7,9 @@ import qs.services
|
|||||||
import qs.modules.common
|
import qs.modules.common
|
||||||
import ".."
|
import ".."
|
||||||
|
|
||||||
NestableObject {
|
GCloudApi {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
enum State {
|
|
||||||
Done, Uploading, Processing, Error
|
|
||||||
}
|
|
||||||
|
|
||||||
signal finished()
|
|
||||||
signal error()
|
|
||||||
property var outputData
|
|
||||||
property var state: GCloudVision.State.Done
|
|
||||||
|
|
||||||
readonly property string imageBase64FilePath: `${Directories.screenshotTemp}/vision_base64.txt`
|
readonly property string imageBase64FilePath: `${Directories.screenshotTemp}/vision_base64.txt`
|
||||||
readonly property string payloadFilePath: `${Directories.screenshotTemp}/vision_payload.json`
|
readonly property string payloadFilePath: `${Directories.screenshotTemp}/vision_payload.json`
|
||||||
property string uploadEndpoint: "https://uguu.se/upload"
|
property string uploadEndpoint: "https://uguu.se/upload"
|
||||||
@@ -28,7 +19,8 @@ NestableObject {
|
|||||||
readonly property bool preparationReady: tokenReady && onlineImageReady
|
readonly property bool preparationReady: tokenReady && onlineImageReady
|
||||||
|
|
||||||
function annotateImage(imageUri: string) {
|
function annotateImage(imageUri: string) {
|
||||||
root.state = GCloudVision.State.Uploading;
|
resetState();
|
||||||
|
root.state = GCloudApi.State.Preparing;
|
||||||
root.onlineImageReady = false
|
root.onlineImageReady = false
|
||||||
GoogleCloud.load();
|
GoogleCloud.load();
|
||||||
|
|
||||||
@@ -49,11 +41,11 @@ NestableObject {
|
|||||||
onPreparationReadyChanged: {
|
onPreparationReadyChanged: {
|
||||||
if (!preparationReady) return;
|
if (!preparationReady) return;
|
||||||
if (GoogleCloud.tokenError || GoogleCloud.keyError) {
|
if (GoogleCloud.tokenError || GoogleCloud.keyError) {
|
||||||
root.state = GCloudVision.State.Error;
|
root.state = GCloudApi.State.Error;
|
||||||
root.error();
|
root.error(Translation.tr("Set your Google Cloud service account key"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
root.state = GCloudVision.State.Processing;
|
root.state = GCloudApi.State.Processing;
|
||||||
var seq = []; // command sequence
|
var seq = []; // command sequence
|
||||||
|
|
||||||
// Construct the JSON payload using jq to read from the base64 file
|
// Construct the JSON payload using jq to read from the base64 file
|
||||||
@@ -75,9 +67,7 @@ https://vision.googleapis.com/v1/images:annotate \
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
seq.push((out) => {
|
seq.push((out) => {
|
||||||
root.outputData = JSON.parse(out);
|
root.handleApiOutput(out);
|
||||||
root.finished();
|
|
||||||
root.state = GCloudVision.State.Done;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
lookMultiproc.runSequence(seq);
|
lookMultiproc.runSequence(seq);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
property bool error: false
|
property bool error: false
|
||||||
|
property string errorMessage: ""
|
||||||
function showError() {
|
function showError() {
|
||||||
error = true;
|
error = true;
|
||||||
}
|
}
|
||||||
@@ -45,13 +46,17 @@ Item {
|
|||||||
cloudVision.annotateImage(screenshotPath);
|
cloudVision.annotateImage(screenshotPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function reattemptAsNeeded() {
|
||||||
|
if (root.visionParagraphs == [] && GoogleCloud.tokenReady && !GoogleCloud.tokenError) {
|
||||||
|
root.error = false;
|
||||||
|
cloudVision.annotateImage(root.screenshotPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: GoogleCloud
|
target: GoogleCloud
|
||||||
function onTokenChanged() {
|
function onTokenReadyChanged() {
|
||||||
if (GoogleCloud.tokenReady && !GoogleCloud.tokenError) {
|
root.reattemptAsNeeded();
|
||||||
root.error = false;
|
|
||||||
cloudVision.annotateImage(root.screenshotPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,15 +81,15 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: {
|
text: {
|
||||||
if (cloudVision.state == GCloudVision.State.Uploading)
|
if (cloudVision.state == GCloudApi.State.Preparing)
|
||||||
return Translation.tr("Uploading image");
|
return Translation.tr("Uploading image");
|
||||||
else if (cloudVision.state == GCloudVision.State.Processing)
|
else if (cloudVision.state == GCloudApi.State.Processing)
|
||||||
return Translation.tr("Reading image");
|
return Translation.tr("Reading image");
|
||||||
else if (cloudVision.state == GCloudVision.State.Error)
|
else if (cloudVision.state == GCloudApi.State.Error)
|
||||||
return Translation.tr("Error");
|
return Translation.tr("Error");
|
||||||
else if (cloudTrans.state == GCloudTranslate.State.Preparing)
|
else if (cloudTrans.state == GCloudApi.State.Preparing)
|
||||||
return Translation.tr("Getting ready to translate");
|
return Translation.tr("Getting ready to translate");
|
||||||
else if (cloudTrans.state == GCloudTranslate.State.Processing)
|
else if (cloudTrans.state == GCloudApi.State.Processing)
|
||||||
return Translation.tr("Translating");
|
return Translation.tr("Translating");
|
||||||
else
|
else
|
||||||
return " ";
|
return " ";
|
||||||
@@ -111,19 +116,19 @@ Item {
|
|||||||
}
|
}
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.min(root.windowWidth / 2, 800) * root.scaleFactor
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
textFormat: Text.MarkdownText
|
textFormat: Text.MarkdownText
|
||||||
text: `**${Translation.tr("Screen Translator")}**\n\n${Translation.tr("Set your Google Cloud service account key")}\n\n__[${Translation.tr("See how on the wiki")}](${root.wikiLink})__`
|
wrapMode: Text.Wrap
|
||||||
|
text: `**${Translation.tr("Screen Translator")}**\n\n${root.errorMessage}\n\n__[${Translation.tr("See setup instructions on the wiki")}](${root.wikiLink})__`
|
||||||
font.pixelSize: Appearance.font.pixelSize.small * root.scaleFactor
|
font.pixelSize: Appearance.font.pixelSize.small * root.scaleFactor
|
||||||
color: root.textColor
|
color: root.textColor
|
||||||
MouseArea {
|
onLinkActivated: (link) => {
|
||||||
anchors.fill: parent
|
Qt.openUrlExternally(link)
|
||||||
cursorShape: Qt.PointingHandCursor
|
GlobalStates.screenTranslatorOpen = false
|
||||||
onClicked: {
|
|
||||||
Qt.openUrlExternally(root.wikiLink)
|
|
||||||
GlobalStates.screenTranslatorOpen = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PointingHandLinkHover {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -132,10 +137,16 @@ Item {
|
|||||||
id: gcr
|
id: gcr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleError(msg) {
|
||||||
|
if (msg?.length > 0) root.errorMessage = msg;
|
||||||
|
else root.errorMessage = Translation.tr("Set your Google Cloud service account key");
|
||||||
|
root.showError();
|
||||||
|
}
|
||||||
|
|
||||||
GCloudVision {
|
GCloudVision {
|
||||||
id: cloudVision
|
id: cloudVision
|
||||||
onError: {
|
onError: (msg) => {
|
||||||
root.showError();
|
root.handleError(msg);
|
||||||
}
|
}
|
||||||
onFinished: {
|
onFinished: {
|
||||||
gcr.initializeWithData(outputData);
|
gcr.initializeWithData(outputData);
|
||||||
@@ -149,6 +160,9 @@ Item {
|
|||||||
|
|
||||||
GCloudTranslate {
|
GCloudTranslate {
|
||||||
id: cloudTrans
|
id: cloudTrans
|
||||||
|
onError: (msg) => {
|
||||||
|
root.handleError(msg);
|
||||||
|
}
|
||||||
onFinished: {
|
onFinished: {
|
||||||
var values = outputData.translations.map(translation => translation.translatedText);
|
var values = outputData.translations.map(translation => translation.translatedText);
|
||||||
const keys = root.translationKeys;
|
const keys = root.translationKeys;
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
|
|
||||||
property var keyContent: ({})
|
property var keyContent: ({})
|
||||||
property string keyProjectId: keyContent.project_id
|
property string keyProjectId: keyContent?.project_id
|
||||||
property bool keyError: false
|
property bool keyError: false
|
||||||
property bool keyReady: false
|
property bool keyReady: false
|
||||||
property string token: ""
|
property string token: ""
|
||||||
|
property date tokenExpiry
|
||||||
property bool tokenError: false
|
property bool tokenError: false
|
||||||
property bool tokenReady: false
|
property bool tokenReady: false
|
||||||
readonly property string projectId: keyProjectId
|
readonly property string projectId: keyProjectId
|
||||||
@@ -21,12 +22,27 @@ Singleton {
|
|||||||
readonly property string tokenForKeyScriptPath: Quickshell.shellPath("services/gCloud/token-from-key-venv.sh")
|
readonly property string tokenForKeyScriptPath: Quickshell.shellPath("services/gCloud/token-from-key-venv.sh")
|
||||||
|
|
||||||
function load() {
|
function load() {
|
||||||
// Dummy for init
|
// Init load will be handled by Component.onCompleted
|
||||||
|
if (!tokenReady) return;
|
||||||
|
// We just reload if key expired
|
||||||
|
if (new Date() >= root.tokenExpiry) {
|
||||||
|
root.tokenReady = false;
|
||||||
|
root.keyReady = false;
|
||||||
|
loadKeyIfPossible();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function unready() {
|
||||||
|
root.keyReady = false;
|
||||||
|
root.tokenReady = false;
|
||||||
|
root.keyError = false;
|
||||||
|
root.tokenError = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setKeyJson(str: string): bool {
|
function setKeyJson(str: string): bool {
|
||||||
try {
|
try {
|
||||||
var keyData = JSON.parse(str)
|
var keyData = JSON.parse(str)
|
||||||
|
root.unready();
|
||||||
KeyringStorage.setNestedField(["googleCloud", "serviceAccountKey"], keyData);
|
KeyringStorage.setNestedField(["googleCloud", "serviceAccountKey"], keyData);
|
||||||
return true;
|
return true;
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
@@ -41,20 +57,26 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
tokenProc.runSequence([(() => { // prep token fetcher
|
tokenProc.runSequence([(() => { // prep token fetcher
|
||||||
tokenProc.environment.SERVICE_KEY_CONTENT = JSON.stringify(root.keyContent);
|
tokenProc.environment.SERVICE_KEY_CONTENT = JSON.stringify(root.keyContent);
|
||||||
tokenProc.command = [ //
|
tokenProc.command = [ //
|
||||||
"bash", "-c" //
|
"bash", "-c" //
|
||||||
, `${tokenForKeyScriptPath} "$SERVICE_KEY_CONTENT"`];
|
, `${tokenForKeyScriptPath} "$SERVICE_KEY_CONTENT"`];
|
||||||
}), [] // run token fetcher
|
}), //
|
||||||
, (out => {
|
[], // run token fetcher
|
||||||
if (out.startsWith("Error")) {
|
((out) => {
|
||||||
root.tokenError = true;
|
try {
|
||||||
} else {
|
const data = JSON.parse(out)
|
||||||
root.tokenError = false;
|
root.token = data.token
|
||||||
root.token = out.trim();
|
// Js wants millis instead of seconds
|
||||||
}
|
root.tokenExpiry = new Date(data.expiry * 1000)
|
||||||
root.tokenReady = true;
|
root.tokenError = false;
|
||||||
})]);
|
} catch(e) {
|
||||||
|
root.tokenError = true;
|
||||||
|
print("[GoogleCloud] Failed to parse token response: " + e + "\n" + out)
|
||||||
|
}
|
||||||
|
root.tokenReady = true;
|
||||||
|
}
|
||||||
|
)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadKeyIfPossible() {
|
function loadKeyIfPossible() {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
import calendar
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import google.auth.transport.requests
|
import google.auth.transport.requests
|
||||||
@@ -16,8 +17,15 @@ def get_token(json_str):
|
|||||||
# Refresh to get the access token
|
# Refresh to get the access token
|
||||||
request = google.auth.transport.requests.Request()
|
request = google.auth.transport.requests.Request()
|
||||||
scoped_creds.refresh(request)
|
scoped_creds.refresh(request)
|
||||||
|
|
||||||
|
token = scoped_creds.token
|
||||||
|
expiry = int(calendar.timegm(scoped_creds.expiry.utctimetuple()))
|
||||||
|
|
||||||
print(scoped_creds.token)
|
print(json.dumps({
|
||||||
|
"token": token,
|
||||||
|
"expiry": expiry
|
||||||
|
}))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
sys.stderr.write(f"Error: {str(e)}\n")
|
sys.stderr.write(f"Error: {str(e)}\n")
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user