Kaynağa Gözat

add fetch-meta from doubao

guoziyun 1 yıl önce
ebeveyn
işleme
e291ba6215

+ 10 - 10
config/meta.js

@@ -7,11 +7,11 @@ let homePageTile = {
 }
 
 let homePageDescription = {
-  zh: '免费的、海量的线条底图,等着您来给它们涂上颜色。不需要复杂的操作,只需要根据数字的指引,即可完成一幅幅精美绝伦的作品,让你感受关于构图和色彩的艺术气息...',
-  en: `Free and a vast number of line drawings are waiting for you to color them. There's no need for complicated operations. You just need to follow the number guides, and then you can complete one exquisite work after another, allowing you to feel the artistic charm of composition and colors...`,
-  es: 'Hay una gran colección gratuita de dibujos a lápiz esperando a que les des color. No es necesario realizar operaciones complicadas. Simplemente sigue las indicaciones de los números, colorea por números y podrás completar una y otra obra impresionante, lo que te permitirá sentir la esencia artística de la composición y los colores...',
-  pt: 'Uma vasta coleção gratuita de desenhos a lápis está esperando por você para dar cor a eles. Não é necessário realizar operações complicadas. Basta seguir as indicações dos números, colorir por números e você poderá completar uma obra maravilhosa após outra, permitindo que você sinta a essência artística da composição e das cores...',
-  ja: '無料でたくさんの線画が、あなたが色を塗るのを待っています。複雑な操作は必要ありません。ただ数字の案内に従って、数字に対応した色で塗るだけで、見事な作品を次々に完成させることができ、構図と色彩の芸術的な本質を感じることができます...',
+  zh: 'art.pcoloring.com 是涂画家的乐园,海量原创的、免费的填色图,等着您来给它们涂上颜色。如果你愿意,你可以把它们下载并打印出来,根据自己的喜好着色。当然你也可以在线进行着色游戏,不需要复杂的操作,只需要根据数字的指引,即可完成一幅幅精美绝伦的作品,让你感受关于构图和色彩的艺术气息...',
+  en: `art.pcoloring.com is a paradise for coloring enthusiasts, offering a vast collection of original and free coloring pages waiting for you to bring them to life with your colors. If you prefer, you can download and print them out to color according to your own preferences. Alternatively, you can enjoy online coloring games without any complicated steps—just follow the number guide to complete stunning works of art, immersing yourself in the artistry of composition and color.`,
+  es: 'art.pcoloring.com es un paraíso para los amantes del coloreado, que ofrece una gran variedad de páginas para colorear originales y gratuitas esperando a que les des vida con tus colores. Si lo prefieres, puedes descargarlas e imprimirlas para colorearlas según tus gustos. También puedes disfrutar de juegos de coloreado en línea sin pasos complicados: solo tienes que seguir la guía de números para completar increíbles obras de arte y sumergirte en el arte de la composición y el color.',
+  pt: 'art.pcoloring.com é um paraíso para os entusiastas de colorir, oferecendo uma enorme coleção de páginas para colorir originais e gratuitas esperando por você para dar-lhes vida com suas cores. Se preferir, você pode baixar e imprimir para colorir de acordo com suas preferências. Você também pode desfrutar de jogos de colorir online sem nenhuma etapa complicada - basta seguir o guia de números para completar obras de arte deslumbrantes, mergulhando na arte da composição e da cor.',
+  ja: 'art.pcoloring.com は涂色愛好家の楽園で、あなたの色で命を吹き込むことを待っているオリジナルかつ無料の涂色ページが山ほどあります。好きなように涂色したい場合は、ダウンロードして印刷することもできます。また、複雑な操作は必要なく、数字のガイドに従うだけで見事なアート作品を完成させることができるオンライン涂色ゲームも楽しめます。構図と色彩の芸術的世界に浸ってください...',
 }
 
 let categoryTitle = {
@@ -160,11 +160,11 @@ let detailDescription = {
 }
 
 let playTitle = {
-  zh: '数字填色游戏',
-  en: 'PLAY COLOR BY NUMBER',
-  es: 'Jugar al juego de colorear por números',
-  pt: 'Jogar o jogo de colorir por números',
-  ja: '数字に従った塗り絵ゲームを遊ぶ',
+  zh: '在线数字填色书 - art.pcoloring.com',
+  en: 'Online Coloring Books and Coloring Pages - art.pcoloring.com',
+  es: 'Libros de colorear en línea y páginas de colorear - art.pcoloring.com',
+  pt: 'Livros de colorir online e páginas de colorir - art.pcoloring.com',
+  ja: 'オンライン塗り絵の本と塗り絵のページ - art.pcoloring.com',
 }
 
 let playDescription = {

+ 54 - 18
routes/index.js

@@ -15,7 +15,7 @@ const { getListBuilder } = require('../libs/pager');
 const CACHE_PREFIX = "art_v1";
 const CACHE_EXPIRES = 60; // 60s刷新一次
 
-const artSelect = 'name title desc width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId desc name';
+const artSelect = 'name title desc width height date publishTime tags lastMod mystery hasSpecial useSpecialThumb publishVersion lock pageId';
 
 
 router.get('/', (req, res, next) => {
@@ -498,7 +498,6 @@ router.get('/:lang/coloring-page/:str', function (req, res, next) {
     organizeDetail(doc, lang);
 
     // find relate
-    // 算法: 排除掉主流的tag,用剩下的tag去检索,取最多12条记录
     let tags = [...doc.tags];
     let cates = categories.map(e => e.id);
     tags = tags.filter(e => !cates.includes(e));
@@ -518,7 +517,8 @@ router.get('/:lang/coloring-page/:str', function (req, res, next) {
 
 
     let data = {
-      title: `${doc.name.replace(/[_]+/g, '-')}`,
+      // title: `${doc.name.replace(/[_]+/g, '-')}`,
+      title: `${doc.title}`,
       description: `${doc.desc}`,
       detail: doc,
       data: result.data,
@@ -557,24 +557,26 @@ router.get('/:lang/detail/:id', function (req, res, next) {
     organizeDetail(doc, lang);
 
     // find relate
-    // 算法: 排除掉主流的tag,用剩下的tag去检索,取最多12条记录
     let tags = [...doc.tags];
     let cates = categories.map(e => e.id);
     tags = tags.filter(e => !cates.includes(e));
-    let baseSort = { publishTime: 'desc' };
 
-    let relates = await models.Art
-      .find({ tags: { $in: tags }, status: 9000 })
-      .select(artSelect)
-      .sort(baseSort)
-      .limit(12)
-      .lean()
-      .exec();
-    organizeData(relates, lang);
+    let query = {
+      page: req.query.page,
+      length: req.query.length,
+      search: req.query.search,
+      orderBy: 'publishTime',
+      order: 'desc',
+      base: { status: 9000 },
+      filters: { tags },
+    }
 
+    let result = await getListBuilder(query, models.Art);
+    organizeData(result.data, lang);
 
     let data = {
-      title: `${doc.name.replace(/[_]+/g, '-')}`,
+      // title: `${doc.name.replace(/[_]+/g, '-')}`,
+      title: `${doc.title}`,
       description: `${doc.desc}`,
       data: doc,
       translate,
@@ -649,9 +651,24 @@ const organizeData = (data, lang) => {
       doc.thumb = `${host}/thumbs/v2/${str}/480/${doc._id}.jpeg`;
     }
 
-    doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip`
+    doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip`;
 
-    let utf8name = encodeURIComponent(doc.name.replace(/[\s_]+/g, '-'));
+    let utf8name = encodeURIComponent(doc.name.replace(/[\s_]+/g, '-')).toLowerCase();
+    if (doc.title) { // 如果有英文title,就用英文title作为url,而不用原来的name字段,name字段没有多语
+      try {
+        let titleJson = JSON.parse(doc.title);
+        if (titleJson && titleJson.en) {
+          utf8name = encodeURIComponent(titleJson.en.replace(/[\s_]+/g, '-')).toLowerCase();
+        }
+        if (titleJson && titleJson[lang]) {
+          doc.title = titleJson[lang];
+        }
+      } catch (e) {
+        console.error(e.message);
+      }
+    } else {
+      doc.title = doc.name;
+    }
 
 
     doc.uri = `${lang}/coloring-page/${utf8name}-${doc._id}`;
@@ -660,6 +677,7 @@ const organizeData = (data, lang) => {
     delete doc.useSpecialThumb;
     delete doc.publishVersion;
     delete doc.pageId;
+    delete doc.desc;
   })
 }
 
@@ -686,8 +704,26 @@ const organizeDetail = (doc, lang) => {
 
   doc.zip = `${host}/zips/v2/number_mini/${version}/${doc._id}.zip`
 
-  doc.desc = translate.descTest[lang];
-  doc.title = translate.titleTest[lang];
+  // doc.title = translate.titleTest[lang];
+  // doc.desc = translate.descTest[lang];
+  if (doc.title) {
+    try {
+      let titleJson = JSON.parse(doc.title);
+      doc.title = titleJson && titleJson[lang] ? titleJson[lang] : doc.name;
+    } catch (e) {
+      console.error(e.message);
+    }
+  } else {
+    doc.title = doc.name;
+  }
+  if (doc.desc) {
+    try {
+      let descJson = JSON.parse(doc.desc);
+      doc.desc = descJson && descJson[lang] ? descJson[lang] : '';
+    } catch (e) {
+      console.error(e.message);
+    }
+  }
   doc.publishTime = format(new Date(doc.publishTime), 'yyyy/MM/dd');
 
   let utf8name = encodeURIComponent(doc.name.replace(/[\s_]+/g, '-'));

+ 0 - 22
service/cron-jobs/ai-seo.js

@@ -1,22 +0,0 @@
-const { format, subMinutes } = require('date-fns');
-const models = require('../../models');
-const utils = require('../../libs/utils');
-
-async function run() {
-  let now = new Date();
-
-  // 删选出所有已经ready并且还没有title的图
-  let query = { status: { $gte: 7000 }, $or: [{ title: { $exists: false } }, { title: null }] };
-  let docs = await models.Art.find(query);
-
-
-
-}
-
-
-
-module.exports = { run }
-
-
-if (require.main == module) {
-}

+ 28 - 0
service/cron-jobs/doubao.json

@@ -0,0 +1,28 @@
+{
+  "choices": [
+    {
+      "finish_reason": "stop",
+      "index": 0,
+      "logprobs": null,
+      "message": {
+        "content": "{\n    \"title\": {\n        \"zh\": \"可爱的猫狗同框\",\n        \"en\": \"Cute Cats and Dogs Together\",\n        \"es\": \"Gatitos y perritos lindos juntos\",\n        \"pt\": \"Gatos e cães lindos juntos\",\n        \"ja\": \"かわいい猫と犬の同時登場\"\n    },\n    \"copy\": {\n        \"zh\": \"画面中可爱的小猫和小狗坐在一起,背景是秋日的风景,给人一种温馨的感觉。\",\n        \"en\": \"The cute kittens and puppies sitting together in the picture, with a autumn scenery in the background, giving people a warm feeling.\",\n        \"es\": \"Los lindos gatitos y perritos sentados juntos en la imagen, con un paisaje otoñal en el fondo, dan a las personas un sentido cálido.\",\n        \"pt\": \"Os gatos e cães lindos sentados juntos na imagem, com um cenário de outono no fundo, transmitem uma sensação agradável às pessoas.\",\n        \"ja\": \"絵に描かれたかわいい猫と犬が一緒に座っており、背景は秋の風景で、温かみを感じさせる。\"\n    }\n}",
+        "role": "assistant"
+      }
+    }
+  ],
+  "created": 1738763701,
+  "id": "021738763695777bfd17bc2326779ae07cf71165f8ec0235b5aa0",
+  "model": "doubao-vision-lite-32k-241015",
+  "object": "chat.completion",
+  "usage": {
+    "completion_tokens": 288,
+    "prompt_tokens": 209,
+    "total_tokens": 497,
+    "prompt_tokens_details": {
+      "cached_tokens": 0
+    },
+    "completion_tokens_details": {
+      "reasoning_tokens": 0
+    }
+  }
+}

+ 149 - 0
service/cron-jobs/fetch-meta.js

@@ -0,0 +1,149 @@
+const models = require('../../models');
+const fetch = require('node-fetch');
+const config = require('../../config/app');
+
+/**
+ * 
+curl --location 'https://ark.cn-beijing.volces.com/api/v3/chat/completions' \
+--header 'Authorization: Bearer fb8942c2-fe94-4092-80fc-233e252f7090' \
+--header 'Content-Type: application/json' \
+--data '{
+        "model": "ep-20250206115552-7qg5c",
+        "messages": [
+        {"role": "user", "content": [
+            {
+                "type":"text",
+                "text": "根据图片生成多语言的标题和200字以内的文案描述, 支持语言中文(zh)、英语(en)、西班牙语(es)、葡萄牙语(pt)、日语(ja),以json格式输出,形如: title: {zh:, en:}, copy: {zh: en:}"
+            },
+            {
+                "type": "image_url",
+                "image_url": {
+                    "url": "https://color.jccytech.cn/thumbs/v2/work/320/67a241674f9d65537938e36d.png"
+                }
+            }
+        ]}
+    ]
+}'
+*/
+
+let apiKey = require('process').env.ARK_API_KEY;
+const url = "https://ark.cn-beijing.volces.com/api/v3/chat/completions";
+let headers = {
+  'Authorization': `Bearer ${apiKey}`,
+  'Content-Type': 'application/json'
+}
+
+/**
+ * 从豆包获取图片的标题和文案描述
+ * @param {*} imageUrl 
+ */
+async function fetchMetaFromDoubao(imageUrl) {
+  let data = {
+    "model": "ep-20250206115552-7qg5c", // Doubao-1.5...ion-pro-32k 当前最新,贵,响应慢,效果好
+    // "model": "ep-20250204231910-4phb8", // Doubao-vision-lite-32k  便宜点,相应速度快
+    "messages": [
+      {
+        "role": "user", "content": [
+          {
+            "type": "text",
+            "text": "根据图片生成多语言的标题和描述文案(文案200字左右,尽量不要出现诸如画面中、这幅画这样的废话), 支持语言中文(zh)、英语(en)、西班牙语(es)、葡萄牙语(pt)、日语(ja),以json格式输出,形如: title: {zh:, en:}, copy: {zh: en:}"
+          },
+          {
+            "type": "image_url",
+            "image_url": {
+              "url": `${imageUrl}`
+            }
+          }
+        ]
+      }
+    ]
+  }
+
+  const jsonData = JSON.stringify(data);
+
+  const response = await fetch(url, { method: 'POST', headers, body: jsonData });
+
+  let responseJson = await response.json();
+
+  console.log(responseJson);
+
+  return responseJson.choices[0].message.content;
+}
+
+
+async function run() {
+
+  let done = 0;
+  let duration = 0;
+  let hour, minute, second;
+  let start = Date.now();
+
+
+  // 筛选出所有已经ready并且还没有title的图
+  let query = { status: { $gte: 9000 }, $or: [{ title: { $exists: false } }, { title: null }] };
+  let docs = await models.Art.find(query).sort({ publishTime: 'desc' });
+
+  let total = docs.length;
+  console.log('total:', total);
+
+  for (let doc of docs) {
+    let thumbUrl = `${config.resHost}/thumbs/v2/work/320/${doc._id}.png`;
+    if (doc.hasSpecial) {
+      thumbUrl = `${config.resHost}/thumbs/v2/special/320/${doc._id}.png`;
+    }
+
+    console.time(doc._id);
+
+    try {
+      let metaInfo = await fetchMetaFromDoubao(thumbUrl);
+      console.log(metaInfo);
+      let metaInfoJson = JSON.parse(metaInfo);
+      let titleJson = metaInfoJson.title;
+      let descJson = metaInfoJson.copy;
+      let title = JSON.stringify(titleJson);
+      let desc = JSON.stringify(descJson);
+
+      doc.title = title;
+      doc.desc = desc;
+      await doc.save();
+
+    } catch (e) {
+      console.error(e.message);
+    }
+
+    console.timeEnd(doc._id);
+
+    done++;
+    duration = (Date.now() - start) / 1000;
+    hour = (Math.floor(duration / 60 / 60)).toString().padStart(2, '0');
+    minute = (Math.floor(duration / 60) % 60).toString().padStart(2, '0');
+    second = (Math.floor(duration) % 60).toString().padStart(2, '0');
+
+    console.log('progress: ' + Math.floor((100 * done / total)) + '% used time: ' + hour + ':' + minute + ':' + second);
+
+  }
+
+
+}
+
+
+
+async function test() {
+  let metaInfo = await fetchMetaFromDoubao("https://color.jccytech.cn/thumbs/v2/work/640/67a254ec4f9d65537938e5c5.png");
+  console.log(metaInfo);
+  let metaInfoJson = JSON.parse(metaInfo);
+  let titleJson = metaInfoJson.title;
+  let descJson = metaInfoJson.copy;
+  let title = JSON.stringify(titleJson);
+  let desc = JSON.stringify(descJson);
+  console.log(title);
+  console.log(desc);
+}
+
+
+module.exports = { run }
+
+
+if (require.main == module) {
+  run();
+}

+ 1 - 1
service/cron-jobs/index.js

@@ -1,7 +1,7 @@
 const cron = require('node-cron');
 
 const settings = [
-  ['ai-seo', '* * * * *', require('./ai-seo')],  // 每个小时跑一遍,ai自动生成标题和文案
+  ['fetch-meta', '* * * * *', require('./fetch-meta')],  // 每个小时跑一遍,ai自动生成标题和文案
 ]
 
 

+ 1 - 1
views/album.ejs

@@ -23,7 +23,7 @@
     <div class="content">
       <div class="image-grid">
         <% data.contents.forEach(item=> { %>
-          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>"></a>
+          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
           <% }); %>
       </div>
     </div>

+ 1 - 1
views/category.ejs

@@ -30,7 +30,7 @@
     <div class="content">
       <div class="image-grid">
         <% data.forEach(item=> { %>
-          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>"></a>
+          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
           <% }); %>
       </div>
     </div>

+ 7 - 1
views/common-meta.ejs

@@ -2,4 +2,10 @@
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <link rel="icon" href="/assets/icon/favicon.ico" type="image/x-icon">
   <title><%= title %></title>
-  <meta name="description" content="<%= description %>" />
+  <meta name="description" content="<%= description %>">
+  <meta property="og:title" content="<%= title %>">
+  <meta property="og:site_name" content="Art Number Coloring Page">
+  <meta property="og:image" content="https://www.art.pcoloring.com/assets/svg/logo.svg">
+  <meta property="og:description" content="<%= description %>">
+  <meta property="og:type" content="website">
+  <meta property="og:url" content="https://www.art.pcoloring.com/">

+ 1 - 1
views/designer.ejs

@@ -23,7 +23,7 @@
     <div class="content">
       <div class="image-grid">
         <% data.forEach(item=> { %>
-          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>"></a>
+          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
           <% }); %>
       </div>
     </div>

+ 1 - 1
views/detail.ejs

@@ -38,7 +38,7 @@
     <div class="content" style="margin-bottom: 40px;">
         <div class="image-grid">
             <% relates.forEach(item=> { %>
-            <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>"></a>
+            <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
             <% }); %>
         </div>
     </div>

+ 1 - 1
views/hot-section.ejs

@@ -8,7 +8,7 @@
   <div class="content">
     <div class="image-grid">
         <% recommend.forEach(item => { %>
-            <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>" ></a>
+            <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>" ></a>
         <% }); %>
     </div>
   </div>

+ 1 - 1
views/latest-section.ejs

@@ -11,7 +11,7 @@
   <div class="content">
     <div class="image-grid">
       <% latest.forEach(item=> { %>
-        <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>"></a>
+        <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
         <% }); %>
     </div>
   </div>

+ 1 - 1
views/search.ejs

@@ -20,7 +20,7 @@
     <div class="content">
       <div class="image-grid">
         <% data.forEach(item=> { %>
-          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>"></a>
+          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
           <% }); %>
       </div>
     </div>

+ 1 - 1
views/special-section.ejs

@@ -8,7 +8,7 @@
   <div class="content">
     <div class="image-grid">
         <% special.forEach(item => { %>
-            <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>" ></a>
+            <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>" ></a>
         <% }); %>
     </div>
   </div>

+ 1 - 1
views/tag.ejs

@@ -26,7 +26,7 @@
     <div class="content">
       <div class="image-grid">
         <% data.forEach(item=> { %>
-          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.name %>"></a>
+          <a href="/<%= item.uri %>"><img src="<%= item.thumb %>" alt="<%= item.title %>"></a>
           <% }); %>
       </div>
     </div>