Spaces:
Running
Running
<template> | |
<v-app> | |
<v-app-bar app dark color="primary"> | |
<v-toolbar-title>{{i18n.title}}</v-toolbar-title> | |
<v-spacer></v-spacer> | |
<v-select style="max-width: 10rem" v-model="locale_name" @change="set_locale(locale_name)" :items="['English','Chinese']" prepend-icon="mdi-web" hide-details></v-select> | |
</v-app-bar> | |
<v-main> | |
<v-row class="mx-3"> | |
<div class="col-12 offset-0 col-sm-8 offset-sm-2 col-md-8 offset-md-2 col-lg-6 offset-lg-3 col-xl-6 offset-xl-3 mt-8"> | |
<v-textarea | |
v-model="prompt_title" | |
:label="i18n.input_title_label" | |
:disabled="loading" | |
rows="1" | |
prepend-icon="mdi-comment" | |
:hint="i18n.input_title_hint" | |
no-resize | |
></v-textarea> | |
<v-textarea | |
v-model="prompt_text" | |
:label="i18n.input_text_label" | |
:disabled="loading" | |
rows="2" | |
prepend-icon="mdi-comment" | |
:hint="i18n.input_text_hint" | |
no-resize | |
></v-textarea> | |
<v-row> | |
<v-checkbox class="col-6" v-model="show_romaji" :disabled="loading" @change="show_type=show_romaji?0:1" :label="i18n.show_romaji"></v-checkbox> | |
<v-scroll-x-transition> | |
<v-select class="col-6" v-if="show_romaji" v-model="romaji_system" :disabled="loading" :items="romaji_systems" :hint="i18n.romaji_system"></v-select> | |
</v-scroll-x-transition> | |
</v-row> | |
<v-row class="mt-n8"> | |
<v-checkbox class="col-6" v-model="use_trans" :disabled="loading" @change="show_type=use_trans?1:0" :label="i18n.use_translate"></v-checkbox> | |
<v-scroll-x-transition> | |
<v-select class="col-6" v-if="use_trans" v-model="trans_target_lang" :disabled="loading" :items="google_supported_languages" :hint="i18n.target_lang"></v-select> | |
</v-scroll-x-transition> | |
</v-row> | |
<v-btn color="primary" class="mb-10" elevation="5" @click="do_gen()" :loading="loading" text block large tile>{{i18n.generate}}</v-btn> | |
<v-expand-transition> | |
<v-simple-table v-if="lyric.length !== 0" class="py-5" dense> | |
<template v-slot:default> | |
<thead> | |
<tr> | |
<th>{{i18n.lyric}}<v-btn class="ml-2" elevation="0" @click="copy_lyric('s')" fab x-small><v-icon>mdi-content-copy</v-icon></v-btn></th> | |
<v-scroll-x-transition> | |
<th v-if="use_trans||show_romaji"> | |
<span v-if="show_romaji&&!use_trans">{{i18n.romaji}}</span> | |
<span v-if="!show_romaji&&use_trans">{{i18n.translation}}</span> | |
<v-btn v-if="use_trans&&show_romaji" class="mr-n2" elevation="0" @click="show_type=show_type===0?1:0" text small> | |
<span v-if="show_romaji" :class="{'grey--text':show_type===1}">{{i18n.romaji_s}}</span>/<span v-if="use_trans" :class="{'grey--text':show_type===0}">{{i18n.translation_s}}</span> | |
</v-btn> | |
<v-btn class="ml-2" elevation="0" @click="copy_lyric(show_type===0?'r':'t')" fab x-small><v-icon>mdi-content-copy</v-icon></v-btn> | |
</th> | |
</v-scroll-x-transition> | |
</tr> | |
</thead> | |
<tbody style="word-break: break-word; word-wrap:break-word;"> | |
<tr v-for="(item, idx) in lyric" :key="idx" @mouseenter="hover=idx" @mouseleave="hover=-1"> | |
<td :class="{'text-h6': idx===0, 'text-center': !item.is_lyric}"> | |
{{item.s}} | |
<v-scroll-x-transition v-if="tts_support"> | |
<v-btn v-if="!tts_loading&&item.s!==''&&(speaking===idx||(speaking===-1&&hover===idx))" :class="{'mr-n9':true,'blue--text':speaking===idx}" icon small @click="ja_tts(idx)"> | |
<v-icon>mdi-volume-high</v-icon> | |
</v-btn> | |
</v-scroll-x-transition > | |
<v-progress-circular v-if="tts_support&&speaking===idx&&tts_loading" class="mr-n9" indeterminate size="20" width="2" color="primary"></v-progress-circular> | |
</td> | |
<v-scroll-x-transition> | |
<td v-if="use_trans||show_romaji" :class="{'text-center': !item.is_lyric}">{{show_type===0?item.r:item.t}}</td> | |
</v-scroll-x-transition> | |
</tr> | |
</tbody> | |
</template> | |
</v-simple-table> | |
</v-expand-transition> | |
</div> | |
</v-row> | |
<v-snackbar dark v-model="snackbar" :timeout="2000"> | |
{{ snackbar_msg }} | |
<template v-slot:action="{ attrs }"> | |
<v-btn color="blue" text v-bind="attrs" @click="snackbar = false"> | |
Close | |
</v-btn> | |
</template> | |
</v-snackbar> | |
</v-main> | |
<v-footer> | |
<v-row class="ma-0"> | |
<div class="text-center caption col-12 pa-0">{{i18n.footer}}</div> | |
<div class="text-center caption col-12 pa-0">© 2022 SkyTNT</div> | |
</v-row> | |
</v-footer> | |
</v-app> | |
</template> | |
<style lang="scss"> | |
</style> | |
<script> | |
import axios from 'axios' | |
if (!String.prototype.trim) { | |
String.prototype.trim = function () { | |
return this.triml().trimr(); | |
} | |
String.prototype.triml = function () { | |
return this.replace(/^[\s\n\t]+/g, ""); | |
} | |
String.prototype.trimr = function () { | |
return this.replace(/[\s\n\t]+$/g, ""); | |
} | |
} | |
function isMobile(){ | |
return /(iPhone|iPad|iPod|iOS|Android|Windows Phone)/i.test(navigator.userAgent); | |
} | |
export default { | |
name: "App", | |
data:()=>({ | |
locale_name: "English", | |
i18n:{}, | |
use_trans: false, | |
google_supported_languages:['af', 'sq', 'am', 'ar', 'hy', 'az', 'eu', 'be', 'bn', 'bs', 'bg', 'my', 'ca', 'ca', 'ceb', 'co', 'cs', 'da', 'nl', 'nl', 'en', 'eo', 'et', 'fi', 'fr', 'fy', 'ka', 'de', 'gd', 'gd', 'ga', 'gl', 'el', 'gu', 'ht', 'ht', 'ha', 'haw', 'he', 'hi', 'hr', 'hu', 'ig', 'is', 'id', 'it', 'jw', 'ja', 'kn', 'kk', 'km', 'ky', 'ky', 'ko', 'ku', 'lo', 'la', 'lv', 'lt', 'lb', 'lb', 'mk', 'ml', 'mi', 'mr', 'ms', 'mg', 'mt', 'mn', 'ne', 'no', 'ny', 'ny', 'ny', 'or', 'pa', 'pa', 'fa', 'pl', 'pt', 'ps', 'ps', 'ro', 'ro', 'ro', 'ru', 'si', 'si', 'sk', 'sl', 'sm', 'sn', 'sd', 'so', 'st', 'es', 'es', 'sr', 'su', 'sw', 'sv', 'ta', 'te', 'tg', 'tl', 'th', 'tr', 'ug', 'ug', 'uk', 'ur', 'uz', 'vi', 'cy', 'xh', 'yi', 'yo', 'zu', 'zh-CN', 'zh-TW'], | |
trans_target_lang:"zh-CN", | |
show_romaji:false, | |
show_type:0, | |
romaji_systems:["hepburn", "kunrei", "nippon"], | |
romaji_system:"hepburn", | |
prompt_title:"桜", | |
prompt_text:"", | |
loading:false, | |
lyric:[], | |
tts_cache:{},//tts缓存 | |
tts_loading:false, | |
tts_support:false, | |
speaking:-1,//正在tts的行数 | |
hover:-1,//鼠标悬停的行数 | |
snackbar:false, | |
snackbar_msg:"" | |
}), | |
mounted() { | |
this.locale_name = (navigator.language ||navigator.userLanguage)==="zh-CN"?"Chinese":"English" | |
if(this.locale_name==="English") | |
this.trans_target_lang="en"; | |
this.set_locale(this.locale_name); | |
this.loads(); | |
}, | |
methods:{ | |
set_locale(name){ | |
let locale = name==="English"?"en":"zh"; | |
let langs={ | |
en:{ | |
title:"Japanese Lyric Generator", | |
loading:"Loading", | |
input_title_label:"Title (can be empty)", | |
input_text_label:"Beginning (can be empty)", | |
input_title_hint:"Generate lyric for a given title", | |
input_text_hint:"The generator continues with this text", | |
show_romaji:"Show romaji", | |
romaji_system:"Romaji system", | |
use_translate:"Translate", | |
target_lang:"Target language", | |
generate:"Generate!", | |
lyric:"Lyric", | |
romaji:"Romaji", | |
romaji_s:"R", | |
translation:"Translation", | |
translation_s:"T", | |
footer:"The generated content can be used as you like, please share this site if you like.", | |
copy_successful:"Copy successful", | |
error:"Error", | |
error_network:"Network error", | |
error_failed_load_kuromoji:"Failed to load romaji converter", | |
error_failed_covert_romaji:"Failed to convert lyric to romaji", | |
error_failed_translate:"Failed to translate lyric", | |
error_copy:"Copy failed, browser does not support" | |
}, | |
zh:{ | |
title:"日语歌词生成器", | |
loading:"加载中", | |
input_title_label:"歌词标题(可不填)", | |
input_text_label:"歌词开头(可不填)", | |
input_title_hint:"给定标题生成歌词", | |
input_text_hint:"生成器以这段文本进行续写", | |
show_romaji:"显示罗马音", | |
romaji_system:"罗马音系统", | |
use_translate:"是否进行翻译", | |
target_lang:"目标语言", | |
generate:"生成!", | |
lyric:"歌词", | |
romaji:"罗马音", | |
romaji_s:"音", | |
translation:"翻译", | |
translation_s:"译", | |
footer:"生成的内容可以随意使用,喜欢的话就请分享本网站", | |
copy_successful:"复制成功", | |
error:"错误", | |
error_network:"网络错误", | |
error_failed_load_kuromoji:"罗马音转换器加载失败", | |
error_failed_covert_romaji:"转换罗马音错误", | |
error_failed_translate:"翻译出错", | |
error_copy:"复制失败,浏览器不支持" | |
} | |
}; | |
this.i18n = langs[locale]; | |
}, | |
async loads(){ | |
if(!isMobile()){ | |
eval((await axios.get('https://l2d.fab.moe/js/autoload.js')).data); | |
} | |
}, | |
async do_gen(){ | |
let prompt_title = this.prompt_title.trim(); | |
let prompt_text = this.prompt_text.trim(); | |
this.loading = true; | |
this.lyric = []; | |
this.tts_cache = {}; | |
try { | |
let result = await axios.post("gen", | |
{title:prompt_title, text:prompt_text}) | |
let gen_text = `${result.data.title}\n\n${result.data.lyric}` | |
let lines = gen_text.split('\n') | |
for (let i in lines) { | |
let line = lines[i].trim().replace(/ /g, "\u3000") | |
this.lyric.push({s: line, r: "", t: "", is_lyric: line!=="---end---"}) | |
} | |
this.lyric[0].is_lyric = false; //title | |
try { | |
let romajis = (await axios.post("romaji",{system:this.romaji_system, text:gen_text})).data.romaji | |
romajis = romajis.split('\n') | |
for (let i in romajis) { | |
this.lyric[i].r = romajis[i] | |
} | |
}catch (e) { | |
this.show_snackbar(this.i18n.error_failed_covert_romaji+":" + e.message) | |
} | |
if (this.use_trans) { | |
let trans = await this.google_translate(gen_text); | |
if (trans !== "") { | |
trans = trans.split("\n") | |
for (let i in trans) { | |
this.lyric[i].t = trans[i].trim().replace(/ /g, "\u3000") | |
} | |
} | |
} | |
this.loading = false; | |
} catch (e) { | |
console.log(e) | |
this.show_snackbar(this.i18n.error+":" + e.message) | |
this.loading = false; | |
} | |
}, | |
async google_translate(text){ | |
let params = {client: "gtx", dt: "t", sl: "ja", tl: this.trans_target_lang, q: text}; | |
try { | |
let result = await axios.get("https://translate.googleapis.com/translate_a/single",{ | |
params:params | |
}); | |
let out = ""; | |
let lines = result.data[0]; | |
for(let i in lines) | |
out += lines[i][0]; | |
return out; | |
}catch(e){ | |
this.show_snackbar(this.i18n.error_failed_translate+":"+e.message); | |
} | |
return ""; | |
}, | |
async ja_tts(line){ | |
let text = this.lyric[line].s; | |
if(this.speaking !== -1||text === ""){ | |
return; | |
} | |
this.speaking = line; | |
try{ | |
let context = new AudioContext(); | |
let gain = context.createGain(); | |
gain.gain.value=2; | |
if(this.tts_cache[text] === undefined){ | |
this.tts_loading=true; | |
let audio_query = (await axios.post("https://voicevox-skytnt.cloud.okteto.net/audio_query",null, | |
{params:{speaker:4,text:text}})).data; | |
let wave = (await axios.post("https://voicevox-skytnt.cloud.okteto.net/synthesis",audio_query, | |
{params:{speaker:4},responseType:"arraybuffer"})).data; | |
this.tts_loading=false; | |
this.tts_cache[text] = await context.decodeAudioData(wave); | |
} | |
let source = context.createBufferSource(); | |
source.buffer = this.tts_cache[text]; | |
source.loop = false; | |
source.connect(gain); | |
gain.connect(context.destination); | |
await new Promise((resolve) => { | |
source.onended = () => {resolve()} | |
source.start(0);//立即播放 | |
}); | |
}catch (e) { | |
this.show_snackbar(this.i18n.error+":"+e.message) | |
} | |
this.speaking = -1; | |
this.tts_loading=false;//防止网络出错导致tts_loading为true | |
}, | |
copy_lyric(attr_name){ | |
let text = "" | |
for(let i in this.lyric){ | |
text += this.lyric[i][attr_name] + "\n" | |
} | |
text = text.trim() | |
this.copy(text) | |
}, | |
async copy(text){ | |
try { | |
await navigator.clipboard.writeText(text) | |
this.show_snackbar(this.i18n.copy_successful) | |
}catch (e) { | |
let input = document.createElement('textarea') | |
input.style.position = 'fixed' | |
input.style.top = '-10000px' | |
input.style.zIndex = '-999' | |
document.body.appendChild(input) | |
console.log('input', input) | |
input.value = text | |
input.focus() | |
input.select() | |
try { | |
let result = document.execCommand('copy') | |
if (!result || result === 'unsuccessful') { | |
this.show_snackbar(this.i18n.error_copy) | |
} else { | |
this.show_snackbar(this.i18n.copy_successful) | |
} | |
} catch (e) { | |
this.show_snackbar(this.i18n.error_copy) | |
}finally { | |
document.body.removeChild(input) | |
} | |
} | |
}, | |
show_snackbar(msg){ | |
this.snackbar=true; | |
this.snackbar_msg=msg; | |
} | |
} | |
} | |
</script> | |