ok these are the individual intermediate steps that take place during translation. good to know but i don't know what to do with it.rommmcek wrote: ↑04 Mar 2020, 06:05I extracted additional translation data:Code: Select all
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases. ; #Warn ; Enable warnings to assist with detecting common errors. SendMode Input ; Recommended for new scripts due to its superior speed and reliability. SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory. MsgBox, % GoogleTranslate("今日の天気はとても良いです") MsgBox, % GoogleTranslate("Hello, World!",, "ru") MsgBox, % GoogleTranslate("Привет, Mир!",, "de") MsgBox, % GoogleTranslate("salinisazione",, "fr") MsgBox, % GoogleTranslate("belugar", "fr", "es") MsgBox, % GoogleTranslate("ቴዎድሮስ አድሓኖም ገብረኢየሱስ", , "fr") MsgBox, % GoogleTranslate("cork", , "de") MsgBox, % GoogleTranslate("morcego", "sl", "de") res := GoogleTranslate("今日",, "en") MsgBox, % res MsgBox, % RegExReplace(res, "s)\R\+.+") MsgBox % GoogleTranslate("string", , "de") ExitApp Return Esc::ExitApp GoogleTranslate(str, from := "auto", to := "en") { static JS := CreateScriptObj(), _ := JS.( GetJScript() ) := JS.("delete ActiveXObject; delete GetObject;") json := SendRequest(JS, str, to, from, proxy := "") oJSON := JS.("(" . json . ")") step1:= oJSON[0][0][8][0][0][1], step2:= oJSON[0][0][8][1][0][1] ; by RRR if step1||step2 ; by RRR inter:= (step1? "step1: " step1: "") (step2? "`r`nstep2: " step2: "") ; by RRR loop, % lln:=oJSON[8][0].length ; by RRR lng1.= oJSON[8][0][A_Index-1] (A_Index<lln? ", ": "") ; by RRR loop, % lln:=oJSON[8][0].length ; by RRR lng2.= oJSON[8][3][A_Index-1] (A_Index<lln? ", ": "") ; by RRR if !IsObject(oJSON[1]) { Loop % oJSON[0].length trans .= oJSON[0][A_Index - 1][0] trans := (inter? inter "`n`n": "") oJSON[2] "`n" lng1 "`n" lng2 "`n`n" trans ; by RRR } else { ; else block by RRR loop, % ojson[1].length { trans.= "♦ " ojson[1][cW:=A_Index-1][0] "`n" Loop, % ojson[1][cW][2].length { Loop, % (ln:=ojson[1][cW][2][tW:=A_Index-1][1].length) tr.= ojson[1][cW][2][tW][1][A_Index-1] (A_Index<ln? ", ": "") trans.= " " ((ar:=ojson[1][cW][2][A_Index-1][4])? ar " ": "") ojson[1][cW][2][A_Index-1][0] " ► " tr "`n", tr:= "" } } trans:= (inter? inter "`n`n": "") oJSON[2] "`n" lng1 "`n" lng2 "`n`n" oJSON[0][0][0] "`n+`n" trans } Return trans } SendRequest(JS, str, tl, sl, proxy) { ComObjError(false) http := ComObjCreate("WinHttp.WinHttpRequest.5.1") ( proxy && http.SetProxy(2, proxy) ) http.open( "POST", "https://translate.google.com/translate_a/single?client=webapp&sl=" . sl . "&tl=" . tl . "&hl=" . sl ; <------- sl in place of tl by RRR . "&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&ie=UTF-8&oe=UTF-8&otf=0&ssel=0&tsel=0&pc=1&kc=1" . "&tk=" . JS.("tk").(str), 1 ) http.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") http.SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0") http.send("q=" . URIEncode(str)) http.WaitForResponse(-1) Return http.responsetext } URIEncode(str, encoding := "UTF-8") { VarSetCapacity(var, StrPut(str, encoding)) StrPut(str, &var, encoding) While code := NumGet(Var, A_Index - 1, "UChar") { bool := (code > 0x7F || code < 0x30 || code = 0x3D) UrlStr .= bool ? "%" . Format("{:02X}", code) : Chr(code) } Return UrlStr } GetJScript() { script = ( var TKK = ((function() { var a = 561666268; var b = 1526272306; return 406398 + '.' + (a + b); })()); function b(a, b) { for (var d = 0; d < b.length - 2; d += 3) { var c = b.charAt(d + 2), c = "a" <= c ? c.charCodeAt(0) - 87 : Number(c), c = "+" == b.charAt(d + 1) ? a >>> c : a << c; a = "+" == b.charAt(d) ? a + c & 4294967295 : a ^ c } return a } function tk(a) { for (var e = TKK.split("."), h = Number(e[0]) || 0, g = [], d = 0, f = 0; f < a.length; f++) { var c = a.charCodeAt(f); 128 > c ? g[d++] = c : (2048 > c ? g[d++] = c >> 6 | 192 : (55296 == (c & 64512) && f + 1 < a.length && 56320 == (a.charCodeAt(f + 1) & 64512) ? (c = 65536 + ((c & 1023) << 10) + (a.charCodeAt(++f) & 1023), g[d++] = c >> 18 | 240, g[d++] = c >> 12 & 63 | 128) : g[d++] = c >> 12 | 224, g[d++] = c >> 6 & 63 | 128), g[d++] = c & 63 | 128) } a = h; for (d = 0; d < g.length; d++) a += g[d], a = b(a, "+-a^+6"); a = b(a, "+-3^+b+-f"); a ^= Number(e[1]) || 0; 0 > a && (a = (a & 2147483647) + 2147483648); a `%= 1E6; return a.toString() + "." + (a ^ h) } ) Return script } CreateScriptObj() { static doc doc := ComObjCreate("htmlfile") doc.write("<meta http-equiv='X-UA-Compatible' content='IE=9'>") Return ObjBindMethod(doc.parentWindow, "eval") }
Using Google Translate to automate text translation
-
- Posts: 223
- Joined: 06 Jun 2017, 11:38
Re: Using Google Translate to automate text translation
All important data are now extracted. Since for some translations the data can be abondant MsgBox is no longer suitable for display.
F1 or Ctrl + 5 - copy marked text and traslate it. Esc - hide Gui, F2 - show hidden Gui, #Esc - Exit
Edit: Added Phonetic transcription and "Did you mean..."@teadrinker do you have a hint how to trigger pronunciation if its possible at all?
F1 or Ctrl + 5 - copy marked text and traslate it. Esc - hide Gui, F2 - show hidden Gui, #Esc - Exit
Edit: Added Phonetic transcription and "Did you mean..."
Code: Select all
#NoEnv ; Recommended for performance and compatibility with future AutoHotkey releases.
; #Warn ; Enable warnings to assist with detecting common errors.
SendMode Input ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir% ; Ensures a consistent starting directory.
#SingleInstance, Force
DetectHiddenWindows, On
SoundBeep, 2500
F1::
^5::
While Clipboard && A_Index<10 {
Clipboard:= ""
Sleep, 50
}
While !Clipboard && A_Index<5 {
Sleep, 100
SendInput, ^c
}
if !Clipboard {
MsgBox,,, No copied text to translate!, 2.5
Return
}
r:= GoogleTranslate(c:= RTrim(Clipboard, "`n`r"), , "de")
loop, 7
if StrLen(r[A_Index])>(l:=10000)
r[A_Index]:= SubStr(r[A_Index], 1, l)
;[int, lng, Main, trn, syn, def, exp]
d:= (r.1? r.1 "`n": "") (r.2? r.2 "`n`n": "") r.3 (r.4||r.5||r.6||r.7? "`n`n": "")
. (r.4? r.4 (r.5||r.6||r.7? "`n`n": ""): "") (r.5? r.5 (r.6||r.7? "`n`n": ""): "")
. (r.6? r.6 (r.7? "`n`n": ""): "") (r.7? r.7: ""), ww:=wh:=g:= 0
Gosub doGui
gui show, Hide
WinGetPos,,, ww,, ahk_id %hGui%
ww:= ww>A_ScreenWidth*.8? A_ScreenWidth*.8: ww
Gosub doGui
gui show, Hide
WinGetPos,,,, wh, ahk_id %hGui%
wh>=A_ScreenHeight*.9? (wh:=A_ScreenHeight*.9, ww+=Ceil(15*A_ScreenDPI/96) ): ""
Gosub doGui
gui Show
SendMessage, 0xB1, 0, 0,, ahk_id %hEdt%
Return
doGui:
gui, Destroy
gui, Margin, 0, 0
;gui font, s12
gui color,, BEC8BE
gui, % "+hWndhGui -DPIScale " (++g<3? "-Caption": "")
gui add, Edit, % (ww? "w" ww: "") (wh? " h" wh: "") (wh<A_ScreenHeight*.9? " -VScroll": "") " +hwndhEdt", % d
GroupAdd, TrGui, % "ahk_id " hGui
Return
#IfWinActive, ahk_group TrGui
GuiClose:
Esc:: Gui, Hide
#IfWinActive
#Esc::
KeyWait, Esc
KeyWait, LWin
SoundBeep, 1200
ExitApp
F2:: Gui, show
GoogleTranslate(str, from := "auto", to := "en") { ;function by RRR based on teadrinkers GoogleTranslate()
static JS := CreateScriptObj(), _ := JS.( GetJScript() ) := JS.("delete ActiveXObject; delete GetObject;")
json := SendRequest(JS, str, to, from, proxy := "")
oJSON := JS.("(" . json . ")")
step1:= oJSON[0][0][8][0][0][1], step2:= oJSON[0][0][8][1][0][1]
int:= (step1? "1: " step1: "") (step2? " 2: " step2: "")
loop, % lln:=oJSON[8][0].length
lng1.= oJSON[8][0][A_Index-1] (A_Index<lln? ", ": "")
loop, % lln:=oJSON[8][0].length
lng2.= oJSON[8][3][A_Index-1] (A_Index<lln? ", ": "")
lng := "a: " oJSON[2] " b: " lng1 " c: " lng2
Loop % oJSON[0].length
Src.= oJSON[0][A_Index-1][1] ((Prn:= ojson[0][1][3])&&A_Index=1? "`n[" Prn "]": "")
Src!=(Dym:= ojson[7][1])? Src.= (Dym? "`n►►►Did you mean: ▌" Dym "?": "")
: Src.= (Dym? "`n►►►Showing result for ▌" Dym "`n►►►Translate instead ▌" (Src:=Str): "")
Loop % oJSON[0].length
mTr .= oJSON[0][A_Index-1][0]
Main:= Src "`n----------------------`n" mTr
loop, % ojson[1].length
{
trn.= "♦ " ojson[1][cW:=A_Index-1][0] "`n"
Loop, % ojson[1][cW][2].length
{
Loop, % ln:=ojson[1][cW][2][tW:=A_Index-1][1].length
tr.= ojson[1][cW][2][tW][1][A_Index-1] (A_Index<ln? ", ": "")
;. (Mod(A_Index, 5)=0&&A_Index<ln? "`n ": "")
trn.= " ▪ " ((ar:=ojson[1][cW][2][A_Index-1][4])? ar " ": "")
. ojson[1][cW][2][A_Index-1][0] " ► " tr "`n", tr:= ""
}
} trn:= (trn? "▬ Translations of ►" oJSON[1][0][3] "◄`n" RTrim(trn, "`n"): "")
loop, % ojson[11].length
{
syn.= "♦ " ojson[11][cW:=A_Index-1][0] " ►" ojson[11][cW][2] "◄`n"
Loop, % ojson[11][cw].length
Loop, % ojson[11][cW][cN:=A_Index].length
Loop, % ojson[11][cW][cN][sW:=A_Index-1].length
Loop, % ln:=ojson[11][cW][cN][sW][tW:=A_Index-1].length
syn.= (A_Index=1? " ▪ ": "") ojson[11][cW][cN][sW][tW][A_Index-1] (A_Index<ln? ", ": "`n")
;. (Mod(A_Index, 5)=0&&A_Index<ln? "`n ": "")
} syn:= (syn? "▬ Synonims`n" RTrim(syn, "`n"): "")
loop, % ojson[12].length
{
def.= "♦ " ojson[12][cW:=A_Index-1][0] " ►" ojson[12][cW][2] "◄`n"
loop, % ojson[12][cW][1].length
qu:= ojson[12][0][1][A_Index-1][2]
, def.= " ▪ " ojson[12][cW][1][A_Index-1][0] (qu? "`n """ qu """" "`n": "`n")
} def:= (def? "▬ Definitions`n" RTrim(def, "`n "): "")
loop, % ln:=ojson[13][0].length
exp.= " ♦ " ojson[13][0][A_Index-1][0] (A_Index<ln? "`n": "")
exp:= (exp? "▬ Examples`n" StrReplace(StrReplace(exp, "<b>", "▌"), "</b>"): "")
Return [int, lng, Main, trn, syn, def, exp]
}
SendRequest(JS, str, tl, sl, proxy) {
ComObjError(false)
http := ComObjCreate("WinHttp.WinHttpRequest.5.1")
¬:=( proxy && http.SetProxy(2, proxy) )
http.open( "POST", "https://translate.google.com/translate_a/single?client=webapp&sl="
. sl . "&tl=" . tl . "&hl=" . sl ; sl in place of tl by RRR
. "&dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t&ie=UTF-8&oe=UTF-8&otf=0&ssel=0&tsel=0&pc=1&kc=1"
. "&tk=" . JS.("tk").(str), 1 )
http.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded;charset=utf-8")
http.SetRequestHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0")
http.send("q=" . URIEncode(str))
http.WaitForResponse(-1)
Return http.responsetext
}
URIEncode(str, encoding := "UTF-8") {
VarSetCapacity(var, StrPut(str, encoding))
StrPut(str, &var, encoding)
While code := NumGet(Var, A_Index - 1, "UChar") {
bool := (code > 0x7F || code < 0x30 || code = 0x3D)
UrlStr .= bool ? "%" . Format("{:02X}", code) : Chr(code)
}
Return UrlStr
}
GetJScript()
{
script =
(
var TKK = ((function() {
var a = 561666268;
var b = 1526272306;
return 406398 + '.' + (a + b);
})());
function b(a, b) {
for (var d = 0; d < b.length - 2; d += 3) {
var c = b.charAt(d + 2),
c = "a" <= c ? c.charCodeAt(0) - 87 : Number(c),
c = "+" == b.charAt(d + 1) ? a >>> c : a << c;
a = "+" == b.charAt(d) ? a + c & 4294967295 : a ^ c
}
return a
}
function tk(a) {
for (var e = TKK.split("."), h = Number(e[0]) || 0, g = [], d = 0, f = 0; f < a.length; f++) {
var c = a.charCodeAt(f);
128 > c ? g[d++] = c : (2048 > c ? g[d++] = c >> 6 | 192 : (55296 == (c & 64512) && f + 1 < a.length && 56320 == (a.charCodeAt(f + 1) & 64512) ?
(c = 65536 + ((c & 1023) << 10) + (a.charCodeAt(++f) & 1023), g[d++] = c >> 18 | 240,
g[d++] = c >> 12 & 63 | 128) : g[d++] = c >> 12 | 224, g[d++] = c >> 6 & 63 | 128), g[d++] = c & 63 | 128)
}
a = h;
for (d = 0; d < g.length; d++) a += g[d], a = b(a, "+-a^+6");
a = b(a, "+-3^+b+-f");
a ^= Number(e[1]) || 0;
0 > a && (a = (a & 2147483647) + 2147483648);
a `%= 1E6;
return a.toString() + "." + (a ^ h)
}
)
Return script
}
CreateScriptObj() {
static doc
doc := ComObjCreate("htmlfile")
doc.write("<meta http-equiv='X-UA-Compatible' content='IE=9'>")
Return ObjBindMethod(doc.parentWindow, "eval")
}
Last edited by rommmcek on 11 Mar 2020, 05:14, edited 7 times in total.
-
- Posts: 4347
- Joined: 29 Mar 2015, 09:41
- Contact:
Re: Using Google Translate to automate text translation
@rommmcek
You can get an mp3 file from Google like this:
You can get an mp3 file from Google like this:
Code: Select all
MsgBox, % GetAudioFromGoogle("Hello World", "en", A_Desktop . "\test.mp3")
GetAudioFromGoogle(text, lng, mp3filePath) {
url := CreateUrl(text, lng)
data := SendRequest(url)
if SubStr(data, 1, 6) = "Error!"
Return data
Return SaveDataToFile(data, mp3filePath)
}
CreateUrl(text, lng) {
JS := CreateScriptObj(), JS.( GetJScript() )
url := "https://translate.google.ru/translate_tts?ie=UTF-8&tl="
. lng . "&total=1&idx=0&client=t&prev=input&textlen="
. StrLen(text) . "&tk=" . JS.("tk").(text) . "&q=" . URIEncode(text)
Return url
}
SendRequest(url) {
whr := ComObjCreate("Msxml2.XMLHTTP.6.0")
whr.Open("GET", url, false)
whr.Send()
if (whr.Status != 200)
Return "Error! Status: " . whr.Status . "`n`n" . whr.responseBody
Return whr.responseBody
}
SaveDataToFile(data, filePath) {
stream := ComObjCreate("ADODB.Stream")
stream.type := 1 ; Binary data
stream.Open
stream.Write(data)
stream.SaveToFile(filePath, 2)
stream.Close
Return true
}
GetJScript()
{
script =
(
var TKK = ((function() {
var a = 561666268;
var b = 1526272306;
return 406398 + '.' + (a + b);
})());
function b(a, b) {
for (var d = 0; d < b.length - 2; d += 3) {
var c = b.charAt(d + 2),
c = "a" <= c ? c.charCodeAt(0) - 87 : Number(c),
c = "+" == b.charAt(d + 1) ? a >>> c : a << c;
a = "+" == b.charAt(d) ? a + c & 4294967295 : a ^ c
}
return a
}
function tk(a) {
for (var e = TKK.split("."), h = Number(e[0]) || 0, g = [], d = 0, f = 0; f < a.length; f++) {
var c = a.charCodeAt(f);
128 > c ? g[d++] = c : (2048 > c ? g[d++] = c >> 6 | 192 : (55296 == (c & 64512) && f + 1 < a.length && 56320 == (a.charCodeAt(f + 1) & 64512) ?
(c = 65536 + ((c & 1023) << 10) + (a.charCodeAt(++f) & 1023), g[d++] = c >> 18 | 240,
g[d++] = c >> 12 & 63 | 128) : g[d++] = c >> 12 | 224, g[d++] = c >> 6 & 63 | 128), g[d++] = c & 63 | 128)
}
a = h;
for (d = 0; d < g.length; d++) a += g[d], a = b(a, "+-a^+6");
a = b(a, "+-3^+b+-f");
a ^= Number(e[1]) || 0;
0 > a && (a = (a & 2147483647) + 2147483648);
a `%= 1E6;
return a.toString() + "." + (a ^ h)
}
)
Return script
}
URIEncode(str, encoding := "UTF-8") {
VarSetCapacity(var, StrPut(str, encoding))
StrPut(str, &var, encoding)
While code := NumGet(Var, A_Index - 1, "UChar") {
bool := (code > 0x7F || code < 0x30 || code = 0x3D)
UrlStr .= bool ? "%" . Format("{:02X}", code) : Chr(code)
}
Return UrlStr
}
CreateScriptObj() {
static doc
doc := ComObjCreate("htmlfile")
doc.write("<meta http-equiv='X-UA-Compatible' content='IE=9'>")
Return ObjBindMethod(doc.parentWindow, "eval")
}
Re: Using Google Translate to automate text translation
You are amazing! Thank you very much! Maybe is now time for a tiny repayment!
Re: Using Google Translate to automate text translation
thank you teadrinker, I'm an idiot
how translate this english text to portuguese ?
( I hear english text in audio file )
how translate this english text to portuguese ?
( I hear english text in audio file )
Code: Select all
f1:= a_desktop . "\test.mp3"
lng:="pt"
text:="Good evening , how are you ?"
aa:=GetAudioFromGoogle(text, lng, F1)
global aa
runwait,%f1%
filedelete,%f1%
exitapp
;----------------------------------------------------------------------------
GetAudioFromGoogle(text,lng, mp3filePath) {
url := CreateUrl(text,lng)
data := SendRequest(url)
if SubStr(data, 1, 6) = "Error!"
Return data
Return SaveDataToFile(data, mp3filePath)
}
CreateUrl(text, lng) {
JS := CreateScriptObj(), JS.( GetJScript() )
url := "https://translate.google.ru/translate_tts?ie=UTF-8&tl="
. lng . "&total=1&idx=0&client=t&prev=input&textlen="
. StrLen(text) . "&tk=" . JS.("tk").(text) . "&q=" . URIEncode(text)
Return url
}
SendRequest(url) {
whr := ComObjCreate("Msxml2.XMLHTTP.6.0")
whr.Open("GET", url, false)
whr.Send()
if (whr.Status != 200)
Return "Error! Status: " . whr.Status . "`n`n" . whr.responseBody
Return whr.responseBody
}
SaveDataToFile(data, filePath) {
stream := ComObjCreate("ADODB.Stream")
stream.type := 1 ; Binary data
stream.Open
stream.Write(data)
stream.SaveToFile(filePath, 2)
stream.Close
Return true
}
GetJScript()
{
script =
(
var TKK = ((function() {
var a = 561666268;
var b = 1526272306;
return 406398 + '.' + (a + b);
})());
function b(a, b) {
for (var d = 0; d < b.length - 2; d += 3) {
var c = b.charAt(d + 2),
c = "a" <= c ? c.charCodeAt(0) - 87 : Number(c),
c = "+" == b.charAt(d + 1) ? a >>> c : a << c;
a = "+" == b.charAt(d) ? a + c & 4294967295 : a ^ c
}
return a
}
function tk(a) {
for (var e = TKK.split("."), h = Number(e[0]) || 0, g = [], d = 0, f = 0; f < a.length; f++) {
var c = a.charCodeAt(f);
128 > c ? g[d++] = c : (2048 > c ? g[d++] = c >> 6 | 192 : (55296 == (c & 64512) && f + 1 < a.length && 56320 == (a.charCodeAt(f + 1) & 64512) ?
(c = 65536 + ((c & 1023) << 10) + (a.charCodeAt(++f) & 1023), g[d++] = c >> 18 | 240,
g[d++] = c >> 12 & 63 | 128) : g[d++] = c >> 12 | 224, g[d++] = c >> 6 & 63 | 128), g[d++] = c & 63 | 128)
}
a = h;
for (d = 0; d < g.length; d++) a += g[d], a = b(a, "+-a^+6");
a = b(a, "+-3^+b+-f");
a ^= Number(e[1]) || 0;
0 > a && (a = (a & 2147483647) + 2147483648);
a `%= 1E6;
return a.toString() + "." + (a ^ h)
}
)
Return script
}
URIEncode(str, encoding := "UTF-8") {
VarSetCapacity(var, StrPut(str, encoding))
StrPut(str, &var, encoding)
While code := NumGet(Var, A_Index - 1, "UChar") {
bool := (code > 0x7F || code < 0x30 || code = 0x3D)
UrlStr .= bool ? "%" . Format("{:02X}", code) : Chr(code)
}
Return UrlStr
}
CreateScriptObj() {
static doc
doc := ComObjCreate("htmlfile")
doc.write("<meta http-equiv='X-UA-Compatible' content='IE=9'>")
Return ObjBindMethod(doc.parentWindow, "eval")
}
-
- Posts: 223
- Joined: 06 Jun 2017, 11:38
Re: Using Google Translate to automate text translation
@garry Translate before you create the audio
Code: Select all
f1 := a_desktop . "\test.mp3"
lng := "pt"
text := GoogleTranslate("Good evening , how are you ?",, lng)
aa := GetAudioFromGoogle(text, lng, F1)
return
Re: Using Google Translate to automate text translation
thank you... I've lost the overview ...
-
- Posts: 4347
- Joined: 29 Mar 2015, 09:41
- Contact:
Re: Using Google Translate to automate text translation
Tried to play mp3 from memory. It turned out a bit cumbersome.
Code: Select all
; https://docs.microsoft.com/en-us/windows/win32/medfound/player-cpp
#NoEnv
SetBatchLines, -1
text := "not`r`nsignificant """
if !pIStream := GetMp3StreamFromGoogle(text, "en") {
MsgBox, Failed to create stream!
ExitApp
}
Session := new MediaSession()
Session.InitializeWithStream(pIStream)
ObjRelease(pIStream)
Session.Start()
Return
$F1:: Session.Start()
$F2:: Session.Pause()
$F3:: Session.Stop()
Esc:: ExitApp
class MediaSession {
__New() {
static MF_VERSION := 0x2, MFSTARTUP_NOSOCKET := 0x1
DllCall("Mfplat\MFStartup", "UInt", MF_VERSION, "UInt", MFSTARTUP_NOSOCKET, "UInt")
this.MFSession := IMFMediaSession.Create()
}
__Delete() {
this.Topology := ""
this.PresentationDescriptor := ""
this.MediaSource := ""
this.MFSession.Shutdown()
this.MFSession := ""
DllCall("Mfplat\MFShutdown")
}
InitializeWithStream(pIStream, fileType := "mp3") {
static MF_RESOLUTION_MEDIASOURCE := 1
SourceResolver := IMFSourceResolver.Create()
ByteStream := IMFByteStream.CreateFromStream(pIStream)
this.MediaSource := SourceResolver.CreateObjectFromByteStream( ByteStream.ptr, "file." . fileType
, MF_RESOLUTION_MEDIASOURCE, 0, MF_OBJECT_TYPE )
SourceResolver := ByteStream := ""
this.PresentationDescriptor := this.MediaSource.CreatePresentationDescriptor()
this.Topology := IMFTopology.Create()
descriptorCount := this.PresentationDescriptor.GetStreamDescriptorCount()
Loop % descriptorCount
this._AddBranchToPartialTopology(A_Index - 1)
this.MFSession.SetTopology(0, this.Topology.ptr)
}
Start(atBeginning := true) {
VarSetCapacity(GUID_NULL, 16, 0)
VarSetCapacity(PROPVARIANT, 8 + A_PtrSize*2, 0)
state := this.GetState()
if atBeginning
NumPut(VT_I8 := 0x14, PROPVARIANT, "UShort")
this.MFSession.Start(&GUID_NULL, &PROPVARIANT)
}
Pause() {
state := this.GetState()
if (state != 1)
this.Start(false)
else
this.MFSession.Pause()
}
Stop() {
this.MFSession.Stop()
}
GetState() {
Clock := this.MFSession.GetClock()
Return Clock.GetState()
}
_AddBranchToPartialTopology(i) {
static MFMediaType_Audio := "{73647561-0000-0010-8000-00AA00389B71}"
StreamDescriptor := this.PresentationDescriptor.GetStreamDescriptorByIndex(i, selected)
if selected {
MediaTypeHandler := StreamDescriptor.GetMediaTypeHandler()
VarSetCapacity(GUID, 16, 0)
MediaTypeHandler.GetMajorType(GUID)
if StringFromGUID(GUID) = MFMediaType_Audio
MFActivate := IMFActivate.Create()
SourceTopologyNode := this._AddSourceNode(StreamDescriptor)
OutputTopologyNode := this._AddOutputNode(MFActivate)
SourceTopologyNode.ConnectOutput(0, OutputTopologyNode.ptr, 0)
}
}
_AddSourceNode(StreamDescriptor) {
static MF_TOPOLOGY_SOURCESTREAM_NODE := 1
, MF_TOPONODE_SOURCE := "{835C58EC-E075-4BC7-BCBA-4DE000DF9AE6}"
, MF_TOPONODE_PRESENTATION_DESCRIPTOR := "{835C58ED-E075-4BC7-BCBA-4DE000DF9AE6}"
, MF_TOPONODE_STREAM_DESCRIPTOR := "{835C58EE-E075-4BC7-BCBA-4DE000DF9AE6}"
SourceTopologyNode := IMFTopologyNode.Create(MF_TOPOLOGY_SOURCESTREAM_NODE)
SourceTopologyNode.SetUnknown( CLSIDFromString(MF_TOPONODE_SOURCE , GUID), this.MediaSource.ptr )
SourceTopologyNode.SetUnknown( CLSIDFromString(MF_TOPONODE_PRESENTATION_DESCRIPTOR, GUID), this.PresentationDescriptor.ptr )
SourceTopologyNode.SetUnknown( CLSIDFromString(MF_TOPONODE_STREAM_DESCRIPTOR , GUID), StreamDescriptor.ptr )
this.Topology.AddNode(SourceTopologyNode.ptr)
Return SourceTopologyNode
}
_AddOutputNode(MFActivate) {
static MF_TOPOLOGY_OUTPUT_NODE := 0
, MF_TOPONODE_STREAMID := "{14932F9B-9087-4BB4-8412-5167145CBE04}"
, MF_TOPONODE_NOSHUTDOWN_ON_REMOVE := "{14932F9C-9087-4BB4-8412-5167145CBE04}"
OutputTopologyNode := IMFTopologyNode.Create(MF_TOPOLOGY_OUTPUT_NODE)
OutputTopologyNode.SetObject(MFActivate.ptr)
OutputTopologyNode.SetUINT32( CLSIDFromString(MF_TOPONODE_STREAMID , GUID), 0 )
OutputTopologyNode.SetUINT32( CLSIDFromString(MF_TOPONODE_NOSHUTDOWN_ON_REMOVE, GUID), 0 )
this.Topology.AddNode(OutputTopologyNode.ptr)
Return OutputTopologyNode
}
}
class IMFMediaSession extends InterfaceBase {
Create() {
DllCall("Mf\MFCreateMediaSession", "Ptr", 0, "PtrP", pIMFMediaSession)
Return new IMFMediaSession(pIMFMediaSession)
}
SetTopology(flags, pIMFTopology) {
hr := DllCall(this.VTable(7), "Ptr", this.ptr, "UInt", flags, "Ptr", pIMFTopology)
this.IsError("IMFMediaSession::SetTopology", hr)
}
Start(pGUID, pVariant) {
hr := DllCall(this.VTable(9), "Ptr", this.ptr, "Ptr", pGUID, "Ptr", pVariant)
this.IsError("IMFMediaSession::Start", hr)
}
Pause() {
hr := DllCall(this.VTable(10), "Ptr", this.ptr)
this.IsError("IMFMediaSession::Pause", hr)
}
Stop() {
hr := DllCall(this.VTable(11), "Ptr", this.ptr)
this.IsError("IMFMediaSession::Stop", hr)
}
Shutdown() {
hr := DllCall(this.VTable(13), "Ptr", this.ptr)
this.IsError("IMFMediaSession::Shutdown", hr)
}
GetClock() {
hr := DllCall(this.VTable(14), "Ptr", this.ptr, "PtrP", pIMFClock)
this.IsError("IMFMediaSession::GetClock", hr)
Return new IMFClock(pIMFClock)
}
}
class IMFClock extends InterfaceBase {
GetState() {
hr := DllCall(this.VTable(6), "Ptr", this.ptr, "UInt", 0, "UIntP", state)
this.IsError("IMFClock::GetState", hr)
Return state
}
}
class IMFSourceResolver extends InterfaceBase {
Create() {
DllCall("Mfplat\MFCreateSourceResolver", "PtrP", pIMFSourceResolver)
Return new IMFSourceResolver(pIMFSourceResolver)
}
CreateObjectFromByteStream(pIMFByteStream, URL, flags, pIPropertyStore, ByRef MF_OBJECT_TYPE) {
static IID_IMFMediaSource := "{279A808D-AEC7-40C8-9C6B-A6B492C78A66}"
hr := DllCall(this.VTable(4), "Ptr", this.ptr, "Ptr", pIMFByteStream, "WStr", URL, "UInt", flags
, "Ptr", pIPropertyStore, "UIntP", MF_OBJECT_TYPE, "PtrP", pIUnknown)
this.IsError("IMFSourceResolver::CreateObjectFromByteStream", hr)
pIMFMediaSource := ComObjQuery(pIUnknown, IID_IMFMediaSource)
ObjRelease(pIUnknown)
Return new IMFMediaSource(pIMFMediaSource)
}
}
class IMFByteStream extends InterfaceBase {
CreateFromStream(pIStream) {
hr := DllCall("Mfplat\MFCreateMFByteStreamOnStream", "Ptr", pIStream, "PtrP", pIMFByteStream)
IMFByteStream.IsError("IMFByteStream::MFCreateMFByteStreamOnStream", hr)
Return new IMFByteStream(pIMFByteStream)
}
}
class IMFMediaSource extends InterfaceBase {
CreatePresentationDescriptor() {
hr := DllCall(this.VTable(8), "Ptr", this.ptr, "PtrP", pIMFPresentationDescriptor)
this.IsError("IMFMediaSource::CreatePresentationDescriptor", hr)
Return new IMFPresentationDescriptor(pIMFPresentationDescriptor)
}
}
class IMFPresentationDescriptor extends InterfaceBase {
GetStreamDescriptorCount() {
hr := DllCall(this.VTable(33), "Ptr", this.ptr, "UIntP", descriptorCount)
this.IsError("IMFPresentationDescriptor::GetStreamDescriptorCount", hr)
Return descriptorCount
}
GetStreamDescriptorByIndex(idx, ByRef selected) {
hr := DllCall(this.VTable(34), "Ptr", this.ptr, "UInt", idx, "UIntP", selected, "PtrP", pIMFStreamDescriptor)
this.IsError("IMFPresentationDescriptor::GetStreamDescriptorByIndex", hr)
Return new IMFStreamDescriptor(pIMFStreamDescriptor)
}
}
class IMFTopology extends InterfaceBase {
Create() {
DllCall("Mf\MFCreateTopology", "PtrP", pIMFTopology)
Return new IMFTopology(pIMFTopology)
}
AddNode(pNode) {
hr := DllCall(this.VTable(34), "Ptr", this.ptr, "Ptr", pNode)
this.IsError("IMFTopology::AddNode", hr)
}
}
class IMFStreamDescriptor extends InterfaceBase {
GetMediaTypeHandler() {
hr := DllCall(this.VTable(34), "Ptr", this.ptr, "PtrP", pIMFMediaTypeHandler)
this.IsError("IMFStreamDescriptor::GetMediaTypeHandler", hr)
Return new IMFMediaTypeHandler(pIMFMediaTypeHandler)
}
}
class IMFMediaTypeHandler extends InterfaceBase {
GetMajorType(ByRef GUID) {
hr := DllCall(this.VTable(8), "Ptr", this.ptr, "Ptr", &GUID)
this.IsError("IMFMediaTypeHandler::GetMajorType", hr)
}
}
class IMFActivate extends InterfaceBase {
Create() {
hr := DllCall("Mf\MFCreateAudioRendererActivate", "PtrP", pIMFActivate)
Return new IMFActivate(pIMFActivate)
}
}
class IMFTopologyNode extends InterfaceBase {
Create(type) {
hr := DllCall("Mf\MFCreateTopologyNode", "UInt", type, "PtrP", pIMFTopologyNode)
IMFTopologyNode.IsError("IMFTopologyNode::Create", hr)
Return new IMFTopologyNode(pIMFTopologyNode)
}
SetUINT32(riid, value) {
hr := DllCall(this.VTable(21), "Ptr", this.ptr, "Ptr", riid, "UInt", value)
this.IsError("IMFTopologyNode::SetUINT32", hr)
}
SetUnknown(riid, pi) {
hr := DllCall(this.VTable(27), "Ptr", this.ptr, "Ptr", riid, "Ptr", pi)
this.IsError("IMFTopologyNode::SetUnknown", hr)
}
SetObject(pi) {
hr := DllCall(this.VTable(33), "Ptr", this.ptr, "Ptr", pi)
this.IsError("IMFTopologyNode::SetObject", hr)
}
ConnectOutput(OutputIndex, pDownstreamNode, dwInputIndexOnDownstreamNode) {
hr := DllCall(this.VTable(40), "Ptr", this.ptr, "UInt", OutputIndex
, "Ptr", pDownstreamNode, "UInt", dwInputIndexOnDownstreamNode)
this.IsError("IMFTopologyNode::ConnectOutput", hr)
}
}
class InterfaceBase {
__New(ptr) {
this.ptr := ptr
}
__Delete() {
ObjRelease(this.ptr)
}
VTable(idx) {
Return NumGet(NumGet(this.ptr + 0) + A_PtrSize*idx)
}
IsError(method, result, exc := true) {
if (result = 0)
Return 0
this.error := method . " failed. Result: " . ( result = "" ? "No result" : Format("{:#x}", result & 0xFFFFFFFF) )
. "`nErrorLevel: " . ErrorLevel
if !exc
Return this.error
throw Exception(this.error)
}
}
StringFromGUID(ByRef VarOrAddress) {
pGuid := IsByRef(VarOrAddress) ? &VarOrAddress : VarOrAddress
VarSetCapacity(sGuid, 78) ; (38 + 1) * 2
if !DllCall("ole32\StringFromGUID2", "Ptr", pGuid, "Ptr", &sGuid, "Int", 39)
throw Exception("Invalid GUID", -1, Format("<at {1:p}>", pGuid))
return StrGet(&sGuid, "UTF-16")
}
CLSIDFromString(IID, ByRef CLSID) {
VarSetCapacity(CLSID, 16, 0)
if res := DllCall("ole32\CLSIDFromString", "WStr", IID, "Ptr", &CLSID, "UInt")
throw Exception("CLSIDFromString failed. Error: " . Format("{:#x}", res))
Return &CLSID
}
GetMp3StreamFromGoogle(text, lng) {
static hHeap := DllCall("GetProcessHeap", "Ptr")
, flags := (HEAP_NO_SERIALIZE := 0x1) | (HEAP_ZERO_MEMORY := 0x8)
if ( (text := Trim(text, " `t`n`r")) = "" ) {
MsgBox, String is empty
Return
}
chunks := [], text .= ".", pos := 1
Loop {
for k, v in ["\.", "!", "\?", ";", ",", ":", "\(", "\)", " ", "$"]
RegExMatch(SubStr(text, pos, 200), "sO).+" . v . "+", m)
until m.Len
chunks.Push(m[0])
pos += m.Len
} until pos > StrLen(text)
last := chunks.Pop()
chunks.Push( SubStr(last, 1, -1) )
size := offset := 0
for k, v in chunks {
url := CreateUrl(v, lng)
arr := SendRequest(url)
pData := NumGet(ComObjValue(arr) + 8 + A_PtrSize)
length := arr.MaxIndex() + 1
size += length
if (A_Index = 1)
pHeap := DllCall("HeapAlloc", "Ptr", hHeap, "UInt", flags, "Ptr", size, "Ptr")
else
pHeap := DllCall("HeapReAlloc", "Ptr", hHeap, "UInt", flags, "Ptr", pHeap, "Ptr", size, "Ptr")
DllCall("RtlMoveMemory", "Ptr", pHeap + offset, "Ptr", pData, "Ptr", length)
offset := size
}
pIStream := DllCall("Shlwapi\SHCreateMemStream", "Ptr", pHeap, "UInt", offset, "Ptr")
DllCall("HeapFree", "Ptr", hHeap, "UInt", 0, "Ptr", pHeap)
Return pIStream
}
CreateUrl(text, lng) {
JS := GetJS(), JS.( GetJScript() )
url := "https://translate.google.ru/translate_tts?ie=UTF-8&tl="
. lng . "&total=1&idx=0&client=t&prev=input&textlen="
. StrLen(text) . "&tk=" . JS.("tk").(text) . "&q=" . URIEncode(text)
Return url
}
SendRequest(url) {
whr := ComObjCreate("Msxml2.XMLHTTP.6.0")
whr.Open("GET", url, false)
whr.Send()
if (whr.Status != 200)
Return "Error! Status: " . whr.Status . "`n`n" . whr.responseBody
Return whr.responseBody
}
GetJS() {
static doc := ComObjCreate("htmlfile")
, __ := doc.write("<meta http-equiv='X-UA-Compatible' content='IE=9'>")
, JS := ObjBindMethod(doc.parentWindow, "eval")
Return JS
}
URIEncode(str, encoding := "UTF-8") {
VarSetCapacity(var, StrPut(str, encoding))
StrPut(str, &var, encoding)
While code := NumGet(Var, A_Index - 1, "UChar") {
bool := (code > 0x7F || code < 0x30 || code = 0x3D)
UrlStr .= bool ? "%" . Format("{:02X}", code) : Chr(code)
}
Return UrlStr
}
GetJScript()
{
script =
(
var TKK = ((function() {
var a = 561666268;
var b = 1526272306;
return 406398 + '.' + (a + b);
})());
function b(a, b) {
for (var d = 0; d < b.length - 2; d += 3) {
var c = b.charAt(d + 2),
c = "a" <= c ? c.charCodeAt(0) - 87 : Number(c),
c = "+" == b.charAt(d + 1) ? a >>> c : a << c;
a = "+" == b.charAt(d) ? a + c & 4294967295 : a ^ c
}
return a
}
function tk(a) {
for (var e = TKK.split("."), h = Number(e[0]) || 0, g = [], d = 0, f = 0; f < a.length; f++) {
var c = a.charCodeAt(f);
128 > c ? g[d++] = c : (2048 > c ? g[d++] = c >> 6 | 192 : (55296 == (c & 64512) && f + 1 < a.length && 56320 == (a.charCodeAt(f + 1) & 64512) ?
(c = 65536 + ((c & 1023) << 10) + (a.charCodeAt(++f) & 1023), g[d++] = c >> 18 | 240,
g[d++] = c >> 12 & 63 | 128) : g[d++] = c >> 12 | 224, g[d++] = c >> 6 & 63 | 128), g[d++] = c & 63 | 128)
}
a = h;
for (d = 0; d < g.length; d++) a += g[d], a = b(a, "+-a^+6");
a = b(a, "+-3^+b+-f");
a ^= Number(e[1]) || 0;
0 > a && (a = (a & 2147483647) + 2147483648);
a `%= 1E6;
return a.toString() + "." + (a ^ h)
}
)
Return script
}
Last edited by teadrinker on 12 Mar 2020, 04:05, edited 7 times in total.
Re: Using Google Translate to automate text translation
thank you teadrinker, works fine , I'll never understand, just few basic , like if ... then ... goto
Re: Using Google Translate to automate text translation
Hi teadrinker!
Thank you very much again! You fixed the bug for long chunks of text (present in mp3 code - which I never noticed) too.
I think neither original Google translator site nor your code manage chunks for optimal pronunciation (always tending to get them as big as possible up to 200 characters).
I'm not very familiar with RegEx, but I propose using While StartPos := RegExMatch(Text, ".+?(\.|;|!|\?)", Found, StartPos) + StrLen(Found) (didn't noticed any difference if $ sign is present or not), but there is much less chance to break outside of listed punctuations, hence less chance for "hesitation" in the middle of the sentence.
Besides I think there is a tiny bug in your second while loop for "chunks". It should brake at comma (as Google traslator does), instead it breaks in the middle of the sentence for the given example:
Don't know how to fix it concisely!
Added Phonetic transcription and "Did you mean..." to my previous code!
Thank you very much again! You fixed the bug for long chunks of text (present in mp3 code - which I never noticed) too.
I think neither original Google translator site nor your code manage chunks for optimal pronunciation (always tending to get them as big as possible up to 200 characters).
I'm not very familiar with RegEx, but I propose using While StartPos := RegExMatch(Text, ".+?(\.|;|!|\?)", Found, StartPos) + StrLen(Found) (didn't noticed any difference if $ sign is present or not), but there is much less chance to break outside of listed punctuations, hence less chance for "hesitation" in the middle of the sentence.
Besides I think there is a tiny bug in your second while loop for "chunks". It should brake at comma (as Google traslator does), instead it breaks in the middle of the sentence for the given example:
Code: Select all
...well as
any manner...
Added Phonetic transcription and "Did you mean..." to my previous code!
-
- Posts: 4347
- Joined: 29 Mar 2015, 09:41
- Contact:
Re: Using Google Translate to automate text translation
@rommmcek
I've added some improvements, try it now, please.
I've added some improvements, try it now, please.
Re: Using Google Translate to automate text translation
Very good "chunks" code!
Ha, ha, you append a period, if not present, to make it working (at least now I understand (condition && expression) - where can I read this in docs?).
For the given text example: Better then Google translator!
You didn't follow my observations entirely though. If you replace all semicolons with e.g. exclamation marks then you drop to Google level again. I suggest for k, v in ["\.", "!", "\?", ":", ";", ",", " "] to reduce that (maybe still some punctuation to add Wikipedia [][!"#$%&'()*+,./:;<=>?@\^_`|{}~-]). Btw., I changed the order(not sure if your code use order of appearance). Edit: Sure it take precedence! Awesome!
All in all, not so important, but instructive anyway!
Ha, ha, you append a period, if not present, to make it working (at least now I understand (condition && expression) - where can I read this in docs?).
For the given text example: Better then Google translator!
You didn't follow my observations entirely though. If you replace all semicolons with e.g. exclamation marks then you drop to Google level again. I suggest for k, v in ["\.", "!", "\?", ":", ";", ",", " "] to reduce that (maybe still some punctuation to add Wikipedia [][!"#$%&'()*+,./:;<=>?@\^_`|{}~-]). Btw., I changed the order
All in all, not so important, but instructive anyway!
Last edited by rommmcek on 09 Mar 2020, 16:45, edited 2 times in total.
-
- Posts: 4347
- Joined: 29 Mar 2015, 09:41
- Contact:
-
- Posts: 4347
- Joined: 29 Mar 2015, 09:41
- Contact:
Re: Using Google Translate to automate text translation
Thanks! But it works! However I hate it a bit, because it triggers an AutoGUI highlight bug. Nevertheless I have a cure for it: ¬:=(condition && expression). And no, it's not some new kind of emoji (it could be any variable name)!
Edit: Nested example due to Ternary bad practice:
P.s.: Was personally involved in that discussion, but "your way" didn't remember because didn't understand nada!
Edit: Nested example due to Ternary bad practice:
Code: Select all
(("can you do it" != "" && a := " Yes you can!") && r:= "It worked!")
MsgBox % "Can you do it? " a " and " r
Re: Using Google Translate to automate text translation
Newest chunking code seems to have serious bug. E.g. not
significant is strangely pronounced. Apparently adding option s cures the problem so I use RegExMatch(SubStr(text, pos, 200), "sO).+" . v, m) for now.
Edit: Besides appending period at the end of the text chunk could be unfortunate if at the end is at least one white space, causing it to be pronounced literally (dot). Working solution would be to Trim it: chunks.Push( RTrim( m[0], ".") ), but then I got the idea to append an inert character, unlikely to appear in any text and then Trim it afterwards:
P.s.: Sorry for my ignorance, didn't find option O in docs. On stackoverflow I read o (minuscule) makes RegEx perform only once...
Edit2. Sign + (plus) anywhere in text causes error (not the case in previous code): Error: IMFSourceResolver::CreateObjectFromByteStream failed. Result: 0xc00d36c4
ErrorLevel: 0
---> 315: Throw,Exception(this.error)
significant is strangely pronounced. Apparently adding option s cures the problem so I use RegExMatch(SubStr(text, pos, 200), "sO).+" . v, m) for now.
Edit: Besides appending period at the end of the text chunk could be unfortunate if at the end is at least one white space, causing it to be pronounced literally (dot). Working solution would be to Trim it: chunks.Push( RTrim( m[0], ".") ), but then I got the idea to append an inert character, unlikely to appear in any text and then Trim it afterwards:
Code: Select all
chunks := [], Text.="ǁ", pos := 1
Loop {
for k, v in ["ǁ", "\.", "!", "\?", ";", ",", ":", "\(", "\)", " ", "$"]
RegExMatch(SubStr(text, pos, 200), "sO).+" v, m)
until m.Len
¬:= ( m[0] != "" && chunks.Push( RTrim(m[0], "ǁ") ) ), pos += m.Len
} until pos > StrLen(text)
Edit2. Sign + (plus) anywhere in text causes error (not the case in previous code): Error: IMFSourceResolver::CreateObjectFromByteStream failed. Result: 0xc00d36c4
ErrorLevel: 0
---> 315: Throw,Exception(this.error)
Re: Using Google Translate to automate text translation
When the entry text is not what Google translate would expect and Google is pretty sure what it should be then alert the user with the message: "Did you mean: ….?".
If Google is not so sure the alert is: Showing result for …. / Translate instead ….
I think I solved this now! Updated my previous code!
If Google is not so sure the alert is: Showing result for …. / Translate instead ….
I think I solved this now! Updated my previous code!
-
- Posts: 4347
- Joined: 29 Mar 2015, 09:41
- Contact:
Re: Using Google Translate to automate text translation
@rommmcek
Thanks for testing!
Thanks for testing!
Fixed.
Added
Now all spaces at the end are trimmed.
Last edited by teadrinker on 11 Mar 2020, 14:44, edited 1 time in total.
-
- Posts: 4347
- Joined: 29 Mar 2015, 09:41
- Contact:
Return to “Scripts and Functions (v1)”
Who is online
Users browsing this forum: No registered users and 78 guests