daydreamer-json commited on
Commit
f9976a4
·
verified ·
1 Parent(s): 518bc25

Add APNG to GIF logic

Browse files
Dockerfile CHANGED
@@ -23,7 +23,8 @@ COPY . .
23
  # ポートを公開(Hugging Face Spacesではapp_port設定と合わせる)
24
  EXPOSE 3000
25
 
26
- RUN bun install --production \
 
27
  && bun pm cache rm \
28
  && chmod -R 777 /app
29
 
 
23
  # ポートを公開(Hugging Face Spacesではapp_port設定と合わせる)
24
  EXPOSE 3000
25
 
26
+ RUN apk add --no-cache ffmpeg \
27
+ && bun install --production \
28
  && bun pm cache rm \
29
  && chmod -R 777 /app
30
 
src/routes/download/emoji.ts CHANGED
@@ -1,5 +1,7 @@
1
  import { Hono } from 'hono';
2
  import ky from 'ky';
 
 
3
  import { logger } from '../../utils/logger';
4
  import { loadConfig } from '../../utils/config';
5
 
@@ -76,6 +78,7 @@ app.get('/single/:productId/:iconIndex', async (c) => {
76
  const iconIndex: number = parseInt(c.req.param('iconIndex') || '-1');
77
  const deviceType: DeviceType = (c.req.query('device_type') || 'ios') as DeviceType;
78
  const isStaticFlag: boolean = c.req.query('is_static') === 'true' || false;
 
79
  // const variantSize: number = parseInt(c.req.query('size') || '2');
80
 
81
  if (!/^[0-9a-f]+$/.test(productId)) return c.text('Invalid productId', 400);
@@ -95,6 +98,42 @@ app.get('/single/:productId/:iconIndex', async (c) => {
95
  },
96
  );
97
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  const allowedHeaders = ['Content-Type', 'ETag', 'Cache-Control', 'Last-Modified'];
99
  const filteredHeaders = new Headers();
100
  allowedHeaders.forEach((header) => {
@@ -102,12 +141,6 @@ app.get('/single/:productId/:iconIndex', async (c) => {
102
  filteredHeaders.set(header, response.headers.get(header)!);
103
  }
104
  });
105
- // filteredHeaders.set(
106
- // 'Content-Disposition',
107
- // `attachment; filename="${productId}_${String(iconIndex).padStart(3, '0')}_${deviceType}_${
108
- // isStaticFlag ? '_static' : ''
109
- // }.png"`,
110
- // );
111
  filteredHeaders.set('X-Origin-Date', response.headers.get('Date')!);
112
 
113
  return new Response(response.body, {
 
1
  import { Hono } from 'hono';
2
  import ky from 'ky';
3
+ import fs from 'fs';
4
+ import { exec } from 'child_process';
5
  import { logger } from '../../utils/logger';
6
  import { loadConfig } from '../../utils/config';
7
 
 
78
  const iconIndex: number = parseInt(c.req.param('iconIndex') || '-1');
79
  const deviceType: DeviceType = (c.req.query('device_type') || 'ios') as DeviceType;
80
  const isStaticFlag: boolean = c.req.query('is_static') === 'true' || false;
81
+ const gifFlag: boolean = c.req.query('gif') === 'true';
82
  // const variantSize: number = parseInt(c.req.query('size') || '2');
83
 
84
  if (!/^[0-9a-f]+$/.test(productId)) return c.text('Invalid productId', 400);
 
98
  },
99
  );
100
 
101
+ // GIF変換が必要な場合(アニメーションの場合のみ)
102
+ if (gifFlag && !isStaticFlag) {
103
+ const buffer = await response.arrayBuffer();
104
+ const prefix = Math.random().toString(36).substring(2);
105
+ const inputPath = `tmp_${prefix}_input.png`;
106
+ const outputPath = `tmp_${prefix}_output.gif`;
107
+
108
+ fs.writeFileSync(inputPath, Buffer.from(buffer));
109
+
110
+ // FFmpegでAPNGをGIFに変換
111
+ await new Promise<void>((resolve, reject) => {
112
+ exec(
113
+ `ffmpeg -y -i ${inputPath} -loop 0 -filter_complex "[0:v] split [a][b];[a] palettegen [p];[b][p] paletteuse=dither=floyd_steinberg" ${outputPath}`,
114
+ (error) => {
115
+ if (error) reject(error);
116
+ else resolve();
117
+ },
118
+ );
119
+ });
120
+
121
+ const gifBuffer = fs.readFileSync(outputPath);
122
+
123
+ // 一時ファイルをクリーンアップ
124
+ fs.unlink(inputPath, () => {});
125
+ fs.unlink(outputPath, () => {});
126
+
127
+ const filteredHeaders = new Headers();
128
+ filteredHeaders.set('Content-Type', 'image/gif');
129
+ filteredHeaders.set('X-Origin-Date', response.headers.get('Date')!);
130
+
131
+ return new Response(gifBuffer, {
132
+ status: 200,
133
+ headers: filteredHeaders,
134
+ });
135
+ }
136
+
137
  const allowedHeaders = ['Content-Type', 'ETag', 'Cache-Control', 'Last-Modified'];
138
  const filteredHeaders = new Headers();
139
  allowedHeaders.forEach((header) => {
 
141
  filteredHeaders.set(header, response.headers.get(header)!);
142
  }
143
  });
 
 
 
 
 
 
144
  filteredHeaders.set('X-Origin-Date', response.headers.get('Date')!);
145
 
146
  return new Response(response.body, {
src/routes/download/sticker.ts CHANGED
@@ -1,5 +1,7 @@
1
  import { Hono } from 'hono';
2
  import ky from 'ky';
 
 
3
  import { logger } from '../../utils/logger';
4
  import { loadConfig } from '../../utils/config';
5
 
@@ -69,6 +71,7 @@ app.get('/single/:stickerId', async (c) => {
69
  const deviceType: DeviceType = (c.req.query('device_type') || 'ios') as DeviceType;
70
  const isStaticFlag: boolean = c.req.query('is_static') === 'true' || false;
71
  const variantSize: number = parseInt(c.req.query('size') || '2');
 
72
 
73
  if (stickerId === -1) return c.text('Invalid stickerId', 400);
74
  if (!validDeviceTypes.includes(deviceType as DeviceType)) return c.text('Invalid device_type', 400);
@@ -86,6 +89,43 @@ app.get('/single/:stickerId', async (c) => {
86
  },
87
  );
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  const allowedHeaders = ['Content-Type', 'ETag', 'Cache-Control', 'Last-Modified'];
90
  const filteredHeaders = new Headers();
91
  allowedHeaders.forEach((header) => {
 
1
  import { Hono } from 'hono';
2
  import ky from 'ky';
3
+ import fs from 'fs';
4
+ import { exec } from 'child_process';
5
  import { logger } from '../../utils/logger';
6
  import { loadConfig } from '../../utils/config';
7
 
 
71
  const deviceType: DeviceType = (c.req.query('device_type') || 'ios') as DeviceType;
72
  const isStaticFlag: boolean = c.req.query('is_static') === 'true' || false;
73
  const variantSize: number = parseInt(c.req.query('size') || '2');
74
+ const gifFlag: boolean = c.req.query('gif') === 'true';
75
 
76
  if (stickerId === -1) return c.text('Invalid stickerId', 400);
77
  if (!validDeviceTypes.includes(deviceType as DeviceType)) return c.text('Invalid device_type', 400);
 
89
  },
90
  );
91
 
92
+ // GIF変換が必要な場合(アニメーションの場合のみ)
93
+ if (gifFlag && !isStaticFlag) {
94
+ const buffer = await response.arrayBuffer();
95
+ const prefix = Math.random().toString(36).substring(2);
96
+ const inputPath = `tmp_${prefix}_input.png`;
97
+ const outputPath = `tmp_${prefix}_output.gif`;
98
+
99
+ fs.writeFileSync(inputPath, Buffer.from(buffer));
100
+
101
+ // FFmpegでAPNGをGIFに変換
102
+ await new Promise<void>((resolve, reject) => {
103
+ exec(
104
+ `ffmpeg -y -i ${inputPath} -loop 0 -filter_complex "[0:v] split [a][b];[a] palettegen [p];[b][p] paletteuse=dither=floyd_steinberg" ${outputPath}`,
105
+ (error) => {
106
+ if (error) reject(error);
107
+ else resolve();
108
+ },
109
+ );
110
+ });
111
+
112
+ const gifBuffer = fs.readFileSync(outputPath);
113
+
114
+ // 一時ファイルをクリーンアップ
115
+ fs.unlink(inputPath, () => {});
116
+ fs.unlink(outputPath, () => {});
117
+
118
+ const filteredHeaders = new Headers();
119
+ filteredHeaders.set('Content-Type', 'image/gif');
120
+ filteredHeaders.set('X-Origin-Date', response.headers.get('Date')!);
121
+
122
+ return new Response(gifBuffer, {
123
+ status: 200,
124
+ headers: filteredHeaders,
125
+ });
126
+ }
127
+
128
+ // 通常の場合
129
  const allowedHeaders = ['Content-Type', 'ETag', 'Cache-Control', 'Last-Modified'];
130
  const filteredHeaders = new Headers();
131
  allowedHeaders.forEach((header) => {