Spaces:
Sleeping
Sleeping
<html> | |
<head> | |
<title>Poetry Camera</title> | |
<meta charset="UTF-8"> | |
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> | |
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/index.css"> | |
<link rel="stylesheet" href="css/index.css"> | |
</head> | |
<body> | |
<div id="app" class="client-area"> | |
<div :style="appContainer"> | |
<div> | |
<div><b>Poetry Camera</b></div> | |
<div class="follow"> | |
<a class="github-button" href="https://github.com/ufownl" aria-label="Follow @ufownl on GitHub">Follow @ufownl</a> | |
</div> | |
</div> | |
<div v-if="vlm"> | |
<el-upload class="image-uploader" :file-list="selectedFiles" :limit="1" :show-file-list="false" :auto-upload="false" accept="image/*" :on-change="selectImage" :disabled="working"> | |
<img v-if="image" class="image" :src="imageUrl" /> | |
<img v-else class="image-uploader-icon" src="img/logo.jpg" /> | |
</el-upload> | |
<el-form class="params" :model="form" inline> | |
<el-form-item class="params-item"> | |
<el-select class="lang-sel" v-model="form.lang" :disabled="working"> | |
<el-option label="English" value="en"></el-option> | |
<el-option label="Chinese" value="zh"></el-option> | |
</el-select> | |
</el-form-item> | |
<el-form-item class="params-item"> | |
<el-input v-model="form.style" placeholder="Style" maxlength="32" :style="styleInput" :disabled="working" show-word-limit /> | |
</el-form-item> | |
<el-form-item class="params-end"> | |
<el-button class="req-btn" type="primary" @click="request" :disabled="!image || !form.style.trim() || working">{{ reqBtnText }}</el-button> | |
</el-form-item> | |
</el-form> | |
<el-steps :active="statusId" finish-status="success" align-center> | |
<el-step title="Viewing Picture" icon="View"></el-step> | |
<el-step title="Thinking Hard" icon="Cpu"></el-step> | |
<el-step title="Writing Poem" icon="EditPen"></el-step> | |
</el-steps> | |
<el-card v-if="poem" class="poem-container" shadow="never"> | |
<pre :style="poemStyle">{{ poem }}</pre> | |
<div v-if="statusId === 3" class="copy-container"> | |
<el-link id="copy" type="primary" :data-clipboard-text="poem">Click Here to Copy!</el-link> | |
</div> | |
</el-card> | |
<div v-else> | |
<el-empty v-if="statusId < 0" :description="statusDesc"></el-empty> | |
<div v-else class="status-container"> | |
<el-skeleton v-loading="true" element-loading-background="rgba(255, 255, 255, 0.4)" :element-loading-text="statusDesc" :rows="5" animated /> | |
</div> | |
</div> | |
</div> | |
<div v-else> | |
<el-empty description="Loading model and initializing..." /> | |
</div> | |
</div> | |
</div> | |
</body> | |
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/index.full.min.js"></script> | |
<script src="https://unpkg.com/@element-plus/[email protected]/dist/index.iife.min.js"></script> | |
<script src="https://unpkg.com/[email protected]/dist/clipboard.min.js"></script> | |
<script async defer src="https://buttons.github.io/buttons.js"></script> | |
<script> | |
const enPoemStyle = { margin: '0px', fontSize: '14px' } | |
const zhPoemStyle = { margin: '18px', fontSize: '18px' } | |
const statusText = [ | |
'Take a picture and start creation!', | |
'AI poet is viewing your picture...', | |
'AI poet is thinking hard about the poem...', | |
'AI poet is writing the poem...' | |
] | |
const wsUrl = window.location.origin.replace('http', 'ws') + '/cgemma/session' | |
const app = Vue.createApp({ | |
data() { | |
return { | |
clientHeight: 0, | |
clientWidth: 0, | |
cb: undefined, | |
ws: undefined, | |
vlm: undefined, | |
selectedFiles: [], | |
image: undefined, | |
form: { | |
lang: 'en', | |
style: 'Emily Dickinson' | |
}, | |
lastForm: {}, | |
statusId: -1, | |
working: false, | |
poem: '', | |
poemStyle: enPoemStyle | |
} | |
}, | |
computed: { | |
appContainer() { | |
const margin = this.clientWidth > 640 ? (this.clientWidth - 640) / 2 : 0 | |
return { | |
marginLeft: margin + 'px', | |
marginRight: margin + 'px', | |
padding: '8px' | |
} | |
}, | |
imageUrl() { | |
return URL.createObjectURL(this.image) | |
}, | |
styleInput() { | |
const appWidth = this.clientWidth > 640 ? 640 - 16 : this.clientWidth - 16 | |
return { | |
width: appWidth - 180 + 'px' | |
} | |
}, | |
reqBtnText() { | |
return this.statusId < 0 ? 'Create' : 'Retry' | |
}, | |
statusDesc() { | |
return statusText[this.statusId + 1] | |
} | |
}, | |
watch: { | |
'form.lang': function(value) { | |
if (value === 'en') { | |
this.form.style = 'Emily Dickinson' | |
} else if (value === 'zh') { | |
this.form.style = '海子' | |
} | |
} | |
}, | |
mounted() { | |
this.clientHeight = this.$el.clientHeight | |
this.clientWidth = this.$el.clientWidth | |
window.onresize = () => { | |
this.clientHeight = this.$el.clientHeight | |
this.clientWidth = this.$el.clientWidth | |
} | |
this.cb = new ClipboardJS('#copy') | |
this.cb.on('success', () => { | |
this.$message.success('Copied! Now you can share it with your friends!') | |
}) | |
const ws = new WebSocket(wsUrl) | |
ws.onerror = evt => { | |
this.$message.error('WebSocket Connection Error') | |
this.ws = undefined | |
} | |
ws.onclose = evt => { | |
this.$message.error('WebSocket Connection Closed') | |
this.ws = undefined | |
} | |
ws.onopen = evt => { | |
ws.onmessage = evt => { | |
this.handleMessage(JSON.parse(evt.data)) | |
} | |
this.ws = ws | |
this.keepalive() | |
} | |
}, | |
methods: { | |
selectImage(file) { | |
this.selectedFiles = [] | |
this.image = undefined | |
if (file.size > this.vlm.max_file_size) { | |
this.$message.error('Image size cannot exceed ' + this.vlm.max_file_size / (1024 * 1024) + 'MB!') | |
return | |
} | |
const reader = new FileReader() | |
reader.onload = (e) => { | |
this.image = new Blob([e.target.result], { type: file.raw.type }) | |
this.statusId = -1 | |
} | |
reader.readAsArrayBuffer(file.raw) | |
}, | |
keepalive() { | |
setTimeout(() => { | |
if (this.ws) { | |
this.ws.send(JSON.stringify({ op: 'keepalive' })) | |
this.keepalive() | |
} | |
}, 30000) | |
}, | |
handleMessage(msg) { | |
if (msg.op === 'init') { | |
this.vlm = msg.vlm | |
} else if (msg.op === 'status') { | |
this.statusId = msg.id | |
} else if (msg.op === 'stream') { | |
if (msg.token) { | |
this.poem += msg.token | |
} else { | |
this.statusId += 1 | |
this.working = false | |
} | |
} | |
}, | |
request() { | |
if (!this.ws) { | |
this.$message.error('Not connected! Please refresh the page to reconnect!') | |
return | |
} | |
this.working = true | |
this.poem = '' | |
this.poemStyle = this.form.lang === 'zh' ? zhPoemStyle : enPoemStyle | |
if (this.statusId < 0) { | |
this.image.arrayBuffer().then(buf => { | |
const arr = new Uint8Array(buf) | |
let bin = '' | |
arr.forEach(v => { | |
bin += String.fromCharCode(v) | |
}) | |
this.ws.send(JSON.stringify({ op: 'create', image: btoa(bin), ...this.form })) | |
}) | |
} else { | |
if (Object.entries(this.form).every(x => x[1] === this.lastForm[x[0]])) { | |
this.ws.send(JSON.stringify({ op: 'retry' })) | |
} else { | |
this.ws.send(JSON.stringify({ op: 'retry', ...this.form })) | |
} | |
} | |
this.lastForm = { ...this.form } | |
} | |
} | |
}) | |
app.use(ElementPlus) | |
for (const [name, comp] of Object.entries(ElementPlusIconsVue)) { | |
app.component(name, comp) | |
} | |
app.mount('#app') | |
</script> | |