|
const express = require("express"); |
|
const cors = require("cors"); |
|
const opentype = require("opentype.js"); |
|
const makerjs = require("makerjs"); |
|
const wawoff2 = require("wawoff2"); |
|
const xml2js = require("xml2js"); |
|
const axios = require("axios"); |
|
const fs = require("fs").promises; |
|
const os = require("os"); |
|
const path = require("path"); |
|
const getImageOutline = require("image-outline"); |
|
const Tesseract = require("tesseract.js"); |
|
const app = express(); |
|
|
|
const FormData = require("form-data"); |
|
|
|
class Kolors { |
|
constructor() { |
|
this.commonHeaders = { |
|
accept: "*/*", |
|
"accept-language": "en-US,en;q=0.9", |
|
priority: "u=1, i", |
|
referer: "https://kwai-kolors-kolors.hf.space/?__theme=dark", |
|
origin: "https://kwai-kolors-kolors.hf.space", |
|
}; |
|
} |
|
|
|
async processRequest(method, url, headers = {}, data = null, files = null) { |
|
const config = { |
|
method, |
|
url, |
|
headers, |
|
data, |
|
responseType: "text", |
|
}; |
|
|
|
if (files) { |
|
const form = new FormData(); |
|
for (const [name, file] of Object.entries(files)) { |
|
form.append(name, fs.createReadStream(file.path), { |
|
filename: file.name, |
|
contentType: "image/webp", |
|
}); |
|
} |
|
config.data = form; |
|
config.headers = { |
|
...headers, |
|
...form.getHeaders(), |
|
}; |
|
} else if (data) { |
|
config.data = data; |
|
} |
|
|
|
try { |
|
const response = await axios(config); |
|
console.log(response.status); |
|
console.log(response.data); |
|
return response.data; |
|
} catch (error) { |
|
console.error("Error in processRequest:", error); |
|
throw error; |
|
} |
|
} |
|
|
|
async uploadImage(imagePath) { |
|
const url = |
|
"https://kwai-kolors-kolors.hf.space/upload?upload_id=uppaw4kwm5"; |
|
const headers = { |
|
...this.commonHeaders, |
|
"content-type": "multipart/form-data", |
|
}; |
|
|
|
const responseText = await this.processRequest("post", url, headers, null, { |
|
image: { |
|
path: imagePath, |
|
name: "image.webp", |
|
}, |
|
}); |
|
const filePath = responseText.replace(/[\[\]"\\\n]/g, ""); |
|
return filePath; |
|
} |
|
|
|
async getJwtToken() { |
|
const generateTimestamp = () => |
|
encodeURIComponent(new Date().toISOString()); |
|
const url = `https://huggingface.co/api/spaces/Kwai-Kolors/Kolors/jwt?expiration=${generateTimestamp()}`; |
|
const responseText = await this.processRequest( |
|
"get", |
|
url, |
|
this.commonHeaders |
|
); |
|
const responseJson = JSON.parse(responseText); |
|
return responseJson.token; |
|
} |
|
|
|
async getQueueData(sessionHash) { |
|
const url = `https://kwai-kolors-kolors.hf.space/queue/data?session_hash=${sessionHash}`; |
|
const headers = { |
|
...this.commonHeaders, |
|
accept: "text/event-stream", |
|
"content-type": "application/json", |
|
}; |
|
|
|
try { |
|
const response = await axios.get(url, { |
|
headers, |
|
responseType: "stream", |
|
}); |
|
return new Promise((resolve, reject) => { |
|
let timeoutId = setTimeout(() => { |
|
response.data.destroy(); |
|
resolve(sessionHash); |
|
}, 3000); |
|
|
|
response.data.on("data", (chunk) => { |
|
const lines = chunk |
|
.toString() |
|
.split("\n") |
|
.filter((line) => line.startsWith("data: ")); |
|
lines.forEach((line) => { |
|
console.log(line); |
|
const eventData = line.substring(6); |
|
const eventJson = JSON.parse(eventData); |
|
if (eventJson.msg === "process_completed") { |
|
clearTimeout(timeoutId); |
|
const outputData = eventJson.output?.data || []; |
|
if (outputData.length > 0) { |
|
const fileInfo = outputData[0]; |
|
resolve(fileInfo.url); |
|
response.data.destroy(); |
|
} |
|
} |
|
}); |
|
}); |
|
|
|
response.data.on("end", () => { |
|
clearTimeout(timeoutId); |
|
reject(new Error("Stream ended without completion")); |
|
response.data.destroy(); |
|
}); |
|
|
|
response.data.on("error", (err) => { |
|
clearTimeout(timeoutId); |
|
reject(err); |
|
response.data.destroy(); |
|
}); |
|
|
|
response.data.on("close", () => { |
|
console.log("Stream closed"); |
|
}); |
|
}); |
|
} catch (error) { |
|
console.error("Error in getQueueData:", error); |
|
throw error; |
|
} |
|
} |
|
|
|
async joinQueue(sessionHash, fileUrl, jwtToken, prompt) { |
|
const url = "https://kwai-kolors-kolors.hf.space/queue/join?__theme=dark"; |
|
const headers = { |
|
...this.commonHeaders, |
|
"content-type": "application/json", |
|
"x-zerogpu-token": jwtToken, |
|
origin: "https://kwai-kolors-kolors.hf.space", |
|
}; |
|
|
|
const data = { |
|
data: [ |
|
prompt, |
|
{ |
|
path: fileUrl, |
|
url: `https://kwai-kolors-kolors.hf.space/file=${fileUrl}`, |
|
orig_name: "image.webp", |
|
size: 172602, |
|
mime_type: "image/webp", |
|
meta: { |
|
_type: "gradio.FileData", |
|
}, |
|
}, |
|
0.3, |
|
"", |
|
0, |
|
true, |
|
1024, |
|
1536, |
|
5, |
|
25, |
|
], |
|
event_data: null, |
|
fn_index: 2, |
|
trigger_id: 26, |
|
session_hash: sessionHash, |
|
}; |
|
|
|
const responseText = await this.processRequest("post", url, headers, data); |
|
return responseText; |
|
} |
|
|
|
generateSessionHash() { |
|
const length = 11; |
|
const charset = |
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; |
|
let result = ""; |
|
for (let i = 0; i < length; i++) { |
|
const randomIndex = Math.floor(Math.random() * charset.length); |
|
result += charset[randomIndex]; |
|
} |
|
return result; |
|
} |
|
} |
|
app.use(cors()); |
|
app.use(express.json()); |
|
app.use(express.urlencoded({ extended: true })); |
|
|
|
|
|
function polygonToSVGPath(polygon) { |
|
if (!polygon || polygon.length === 0) { |
|
return ""; |
|
} |
|
|
|
|
|
let pathString = `M${polygon[0].x},${polygon[0].y}`; |
|
for (let i = 1; i < polygon.length; i++) { |
|
pathString += ` L${polygon[i].x},${polygon[i].y}`; |
|
} |
|
pathString += " Z"; |
|
|
|
return pathString; |
|
} |
|
|
|
function handleRequest(err, loadedFont, config) { |
|
const [ |
|
text, |
|
fontName, |
|
size, |
|
union, |
|
filled, |
|
kerning, |
|
separate, |
|
bezierAccuracy, |
|
units, |
|
fill, |
|
stroke, |
|
strokeWidth, |
|
strokeNonScaling, |
|
fillRule, |
|
individualLetters, |
|
res, |
|
] = config; |
|
|
|
if (err) { |
|
console.error("Font could not be loaded:", err); |
|
return res.status(500).json({ error: err.message }); |
|
} |
|
|
|
if (individualLetters) { |
|
|
|
const individualSVGs = {}; |
|
for (let i = 0; i < text.length; i++) { |
|
const letter = text[i]; |
|
const result = callMakerjs( |
|
loadedFont, |
|
letter, |
|
size, |
|
union, |
|
filled, |
|
kerning, |
|
separate, |
|
bezierAccuracy, |
|
units, |
|
fill, |
|
stroke, |
|
strokeWidth, |
|
strokeNonScaling, |
|
fillRule |
|
); |
|
let temp; |
|
xml2js.parseString( |
|
result.svg, |
|
{ |
|
explicitArray: false, |
|
}, |
|
function (err, result) { |
|
if (!err) { |
|
|
|
temp = result; |
|
} else { |
|
console.error(err); |
|
} |
|
} |
|
); |
|
|
|
individualSVGs[letter] = temp; |
|
} |
|
res.status(200).json(individualSVGs); |
|
} else { |
|
|
|
const result = callMakerjs( |
|
loadedFont, |
|
text, |
|
size, |
|
union, |
|
filled, |
|
kerning, |
|
separate, |
|
bezierAccuracy, |
|
units, |
|
fill, |
|
stroke, |
|
strokeWidth, |
|
strokeNonScaling, |
|
fillRule |
|
); |
|
res.status(200).send(result.svg); |
|
} |
|
} |
|
|
|
async function downloadAndSaveFont(fontUrl) { |
|
const tempDir = path.join(os.tmpdir(), "downloaded-or-converted-fonts"); |
|
|
|
try { |
|
await fs.access(tempDir); |
|
} catch (error) { |
|
await fs.mkdir(tempDir, { recursive: true }); |
|
} |
|
|
|
try { |
|
const response = await axios.get(fontUrl, { |
|
responseType: "arraybuffer", |
|
headers: { |
|
"Accept-Encoding": "identity", |
|
}, |
|
}); |
|
|
|
let filename = |
|
response.headers["content-disposition"]?.match( |
|
/filename=['"]?(.+)['"]?/ |
|
)?.[1] || new URL(fontUrl).pathname.split("/").pop(); |
|
|
|
const fontPath = path.join(tempDir, filename); |
|
await fs.writeFile(fontPath, response.data); |
|
|
|
|
|
if (filename.endsWith(".woff2")) { |
|
try { |
|
const fontBuffer = await fs.readFile(fontPath); |
|
const decompressedBuffer = await wawoff2.decompress(fontBuffer); |
|
const ttfFilename = filename.replace(".woff2", ".ttf"); |
|
const ttfFontPath = path.join(tempDir, ttfFilename); |
|
await fs.writeFile(ttfFontPath, decompressedBuffer); |
|
console.log("Font decompressed and saved successfully."); |
|
return ttfFontPath; |
|
} catch (decompressionError) { |
|
console.error("Error decompressing the font:", decompressionError); |
|
throw decompressionError; |
|
} |
|
} |
|
|
|
return fontPath; |
|
} catch (error) { |
|
console.error("Error processing the font:", error); |
|
throw error; |
|
} |
|
} |
|
|
|
function callMakerjs( |
|
font, |
|
text, |
|
size, |
|
union, |
|
filled, |
|
kerning, |
|
separate, |
|
bezierAccuracy, |
|
units, |
|
fill, |
|
stroke, |
|
strokeWidth, |
|
strokeNonScaling, |
|
fillRule |
|
) { |
|
|
|
var textModel = new makerjs.models.Text( |
|
font, |
|
text, |
|
size, |
|
union, |
|
false, |
|
bezierAccuracy, |
|
{ kerning } |
|
); |
|
|
|
if (separate) { |
|
for (var i in textModel.models) { |
|
textModel.models[i].layer = i; |
|
} |
|
} |
|
|
|
var svg = makerjs.exporter.toSVG(textModel, { |
|
fill: filled ? fill : undefined, |
|
stroke: stroke ? stroke : undefined, |
|
strokeWidth: strokeWidth ? strokeWidth : undefined, |
|
fillRule: fillRule ? fillRule : undefined, |
|
scalingStroke: !strokeNonScaling, |
|
}); |
|
|
|
var dxf = makerjs.exporter.toDXF(textModel, { |
|
units: units, |
|
usePOLYLINE: true, |
|
}); |
|
|
|
return { svg, dxf }; |
|
} |
|
|
|
|
|
async function downloadImage(url, dest) { |
|
const writer = await fs.open(dest, "w"); |
|
const response = await axios.get(url, { responseType: "stream" }); |
|
|
|
await new Promise((resolve, reject) => { |
|
response.data.pipe(writer.createWriteStream()); |
|
response.data.on("end", resolve); |
|
response.data.on("error", reject); |
|
}); |
|
|
|
await writer.close(); |
|
} |
|
|
|
|
|
app.post("/highlight", express.json(), async (req, res) => { |
|
const { imageUrl, searchTerms } = req.body; |
|
|
|
if (!imageUrl || !Array.isArray(searchTerms) || searchTerms.length === 0) { |
|
return res |
|
.status(400) |
|
.json({ error: "imageUrl and searchTerms are required" }); |
|
} |
|
|
|
try { |
|
|
|
const tempImagePath = path.join(os.tmpdir(), "temp_image.jpg"); |
|
await downloadImage(imageUrl, tempImagePath); |
|
|
|
|
|
const { |
|
data: { text, words }, |
|
} = await Tesseract.recognize(tempImagePath, "eng", { |
|
logger: (info) => console.log(info), |
|
}); |
|
|
|
const highlights = []; |
|
|
|
|
|
searchTerms.forEach((term) => { |
|
const termWords = term.toLowerCase().split(" "); |
|
const termLen = termWords.length; |
|
|
|
let wordIndex = 0; |
|
|
|
words.forEach((wordObj, i) => { |
|
const word = wordObj.text?.toLowerCase(); |
|
if (!word) return; |
|
|
|
if (word === termWords[wordIndex]) { |
|
wordIndex++; |
|
|
|
|
|
if (wordIndex === termLen) { |
|
wordIndex = 0; |
|
|
|
|
|
const xStart = words[i - termLen + 1].bbox.x0; |
|
const yStart = words[i - termLen + 1].bbox.y0; |
|
const xEnd = words[i].bbox.x1; |
|
const yEnd = words[i].bbox.y1; |
|
|
|
highlights.push({ |
|
text: term, |
|
bbox: { x0: xStart, y0: yStart, x1: xEnd, y1: yEnd }, |
|
}); |
|
} |
|
} else { |
|
wordIndex = 0; |
|
} |
|
}); |
|
}); |
|
|
|
|
|
await fs.unlink(tempImagePath); |
|
|
|
|
|
return res.json({ searchTerms, highlights }); |
|
} catch (error) { |
|
console.error(error); |
|
return res |
|
.status(500) |
|
.json({ error: "An error occurred while processing the image." }); |
|
} |
|
}); |
|
|
|
app.get("/process", async (req, res) => { |
|
const kolors = new Kolors(); |
|
try { |
|
const fileUrl = |
|
"/tmp/gradio/a8afacda04e05001682bb475f128b24002ace7b7/e41b87fb-4cc3-43cd-a6e6-f3dbb08c2399.webp"; |
|
const prompt = |
|
req.query.prompt || |
|
"Anna, 破旧衣服, 大声喊叫的愤怒脸, 嘴巴张大, 全身, 贫民窟背景, 仙女教母, 时尚TikTok风格衣服, 出现在闪亮和点赞的云中, 脸上带着炫酷的表情, 魔法棒, 变成华丽舞会礼服, 鱼网袜和蕾丝项圈, 震惊表情, 这幅艺术作品致敬了传奇的弗兰克·弗拉泽塔,展示了Loish van Baarle的独特风格和Boris Vallejo的动态笔触。这幅杰作向著名艺术家Ross Tran、Greg Tocchini、Tom Bagshaw和Steve Henderson的才华致敬,创造了一个引人入胜且迷人的场景。"; |
|
const sessionHash = req.query.session_hash || kolors.generateSessionHash(); |
|
|
|
const jwtToken = await kolors.getJwtToken(); |
|
console.log("JWT Token:", jwtToken); |
|
|
|
|
|
const queueResponse = await kolors.joinQueue( |
|
sessionHash, |
|
fileUrl, |
|
jwtToken, |
|
prompt |
|
); |
|
console.log("Queue Response:", queueResponse); |
|
|
|
res.send({ sessionHash }); |
|
} catch (error) { |
|
console.error("Error:", error); |
|
res.status(500).send("An error occurred"); |
|
} |
|
}); |
|
|
|
app.get("/poll", async (req, res) => { |
|
const kolors = new Kolors(); |
|
try { |
|
const sessionHash = req.query.session_hash; |
|
if (!sessionHash) { |
|
return res.status(400).send("session_hash is required"); |
|
} |
|
|
|
const finalUrl = await kolors.getQueueData(sessionHash); |
|
console.log("Final URL:", finalUrl); |
|
|
|
res.send({ finalUrl }); |
|
} catch (error) { |
|
console.error("Error:", error); |
|
res.status(500).send("An error occurred"); |
|
} |
|
}); |
|
|
|
app.post("/generateSVGPath", async (req, res) => { |
|
|
|
const { |
|
text, |
|
size = 72, |
|
union = false, |
|
filled = true, |
|
kerning = true, |
|
separate = false, |
|
bezierAccuracy = 2, |
|
units = "mm", |
|
fill = "black", |
|
stroke = "none", |
|
strokeWidth = "1", |
|
strokeNonScaling = false, |
|
fillRule = "nonzero", |
|
fontUrl, |
|
individualLetters = false, |
|
font = "Roobert-Regular.ttf", |
|
} = req.body; |
|
|
|
const config = [ |
|
text, |
|
font, |
|
size, |
|
union, |
|
filled, |
|
kerning, |
|
separate, |
|
bezierAccuracy, |
|
units, |
|
fill, |
|
stroke, |
|
strokeWidth, |
|
strokeNonScaling, |
|
fillRule, |
|
individualLetters, |
|
res, |
|
]; |
|
|
|
const fontPath = fontUrl |
|
? await downloadAndSaveFont(fontUrl) |
|
: path.join(__dirname, "public", "fonts", font); |
|
console.log(fontPath); |
|
try { |
|
await fs.access(fontPath); |
|
console.log("File exists:", fontPath); |
|
} catch (error) { |
|
console.log(fontPath); |
|
res.status(500).json({ error: error.message }); |
|
|
|
} |
|
opentype.load(fontPath, (err, loadedFont) => { |
|
handleRequest(err, loadedFont, config); |
|
}); |
|
}); |
|
|
|
app.post("/generateSVGPathWithGoogleFont", async (req, res) => { |
|
|
|
const { |
|
text, |
|
fontName = "Open Sans", |
|
size = 72, |
|
union = false, |
|
filled = true, |
|
kerning = true, |
|
separate = false, |
|
bezierAccuracy = 2, |
|
units = "mm", |
|
fill = "black", |
|
stroke = "none", |
|
strokeWidth = "1", |
|
strokeNonScaling = false, |
|
fillRule = "nonzero", |
|
individualLetters = false, |
|
} = req.body; |
|
const config = [ |
|
text, |
|
fontName, |
|
size, |
|
union, |
|
filled, |
|
kerning, |
|
separate, |
|
bezierAccuracy, |
|
units, |
|
fill, |
|
stroke, |
|
strokeWidth, |
|
strokeNonScaling, |
|
fillRule, |
|
individualLetters, |
|
res, |
|
]; |
|
const apiKey = "AIzaSyAOES8EmKhuJEnsn9kS1XKBpxxp-TgN8Jc"; |
|
|
|
try { |
|
|
|
const response = await axios.get( |
|
`https://www.googleapis.com/webfonts/v1/webfonts?key=${apiKey}` |
|
); |
|
const fonts = response.data.items; |
|
|
|
|
|
const fontDetails = fonts.find((f) => f.family === fontName); |
|
if (!fontDetails) { |
|
return res.status(404).send("Font not found"); |
|
} |
|
|
|
|
|
let fontUrl = fontDetails.files.regular; |
|
fontUrl = fontUrl.replace("http", "https"); |
|
const fontPath = await downloadAndSaveFont(fontUrl); |
|
|
|
opentype.load(fontPath, (err, loadedFont) => { |
|
handleRequest(err, loadedFont, config); |
|
}); |
|
} catch (error) { |
|
console.error("Error:", error); |
|
res.status(500).json({ error: error.message }); |
|
} |
|
}); |
|
|
|
app.post("/predictions", async (req, res) => { |
|
const { input, path } = req.body; |
|
const headers = { |
|
"Content-Type": "application/json", |
|
|
|
}; |
|
const data = { |
|
input: input, |
|
is_training: false, |
|
create_model: "0", |
|
stream: false, |
|
}; |
|
try { |
|
const response = await axios.post( |
|
`https://replicate.com/api/${path}/predictions`, |
|
data, |
|
{ headers } |
|
); |
|
return res.json(response.data); |
|
} catch (error) { |
|
console.error("Error:", error); |
|
return res.status(500).json({ error: error.message }); |
|
} |
|
}); |
|
|
|
|
|
app.post("/vectorize", (req, res) => { |
|
const { imageUrl } = req.body; |
|
|
|
|
|
|
|
getImageOutline(imageUrl, function (err, polygon) { |
|
if (err) { |
|
console.error("Error:", err); |
|
return res.status(500).json({ error: "Error vectorizing image" }); |
|
} |
|
|
|
|
|
const svgPath = polygonToSVGPath(polygon); |
|
|
|
|
|
res.json({ svgPath }); |
|
}); |
|
}); |
|
|
|
const PORT = process.env.PORT || 3000; |
|
app.listen(PORT, () => { |
|
console.log(`Server running on port ${PORT}`); |
|
}); |
|
|