Spaces:
Sleeping
Sleeping
| <template> | |
| <v-app style="overflow: scroll"> | |
| <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-btn href="https://github.com/SkyTNT/gpt2-japanese-lyric" icon><v-icon>mdi-github</v-icon></v-btn> | |
| <v-btn href="https://fab.moe" icon><v-icon>mdi-home</v-icon></v-btn> | |
| </v-app-bar> | |
| <v-main> | |
| <v-container v-if="loading_kuroshiro" style="height:100%;" fluid> | |
| <div class="py-2 d-flex flex-column justify-center align-center" style="height:100%;"> | |
| <v-progress-circular color="blue" indeterminate></v-progress-circular> | |
| <label class="py-4 text">{{i18n.loading}}</label> | |
| </div> | |
| </v-container> | |
| <v-row v-if="!loading_kuroshiro" 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(0)" :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' | |
| import Kuroshiro from "kuroshiro"; | |
| import KuromojiAnalyzer from "kuroshiro-analyzer-kuromoji"; | |
| 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:["nippon","passport","hepburn"], | |
| romaji_system:"hepburn", | |
| prompt_title:"桜", | |
| prompt_text:"", | |
| loading_kuroshiro:true, | |
| loading:false, | |
| lyric:[], | |
| tts_cache:{},//tts缓存 | |
| tts_loading:false, | |
| tts_support:false, | |
| speaking:-1,//正在tts的行数 | |
| hover:-1,//鼠标悬停的行数 | |
| snackbar:false, | |
| snackbar_msg:"", | |
| kuroshiro: new Kuroshiro() | |
| }), | |
| 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(){ | |
| await this.load_kuroshiro(); | |
| if(!isMobile()){ | |
| eval((await axios.get('https://l2d.fab.moe/js/autoload.js')).data); | |
| } | |
| }, | |
| async load_kuroshiro(){ | |
| try { | |
| await this.kuroshiro.init(new KuromojiAnalyzer({dictPath:"dict/"})); | |
| }catch (e) { | |
| this.show_snackbar(this.i18n.error_failed_load_kuromoji+":"+e.message); | |
| console.log(e); | |
| } | |
| this.loading_kuroshiro=false; | |
| console.log("kuroshiro loaded"); | |
| }, | |
| 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 this.kuroshiro.convert(gen_text, { to: "romaji" ,mode:"spaced",romajiSystem:this.romaji_system}) | |
| 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) | |
| if(e.message==="Network Error"||e.response === undefined){ | |
| this.show_snackbar(this.i18n.error_network) | |
| this.loading = false; | |
| }else { | |
| this.show_snackbar(this.i18n.error+":" + e.response.data.msg) | |
| 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> | |