gpt-image-playground deploy commited on
Commit
da92fb7
·
1 Parent(s): 704e84f

Deploy 5d089e0 to Docker Space

Browse files
README.md CHANGED
@@ -12,7 +12,7 @@ app_port: 4783
12
  本地 AI 图片创作工作台,面向中文内容运营、设计草图和自动化生图流程。支持 `gpt-image-2`、OpenAI 兼容图片接口、文生图、图生图、遮罩编辑、批量任务、历史复用、费用追踪和 Agent API。
13
 
14
  <p align="center">
15
- <img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/912aa682eb019912a0298072cc7a67a11040d502/readme-images/interface.jpg?v=20260608-07b596b" alt="GPT Image Playground 界面" width="900"/>
16
  </p>
17
 
18
  ## 快速开始
@@ -83,13 +83,13 @@ start-windows.bat
83
  遮罩编辑示例:
84
 
85
  <p align="center">
86
- <img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/912aa682eb019912a0298072cc7a67a11040d502/readme-images/mask-creation.jpg?v=20260608-07b596b" alt="遮罩创建" width="900"/>
87
  </p>
88
 
89
  历史与费用示例:
90
 
91
  <p align="center">
92
- <img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/912aa682eb019912a0298072cc7a67a11040d502/readme-images/history.jpg?v=20260608-07b596b" alt="历史面板" width="900"/>
93
  </p>
94
 
95
  ## 配置
 
12
  本地 AI 图片创作工作台,面向中文内容运营、设计草图和自动化生图流程。支持 `gpt-image-2`、OpenAI 兼容图片接口、文生图、图生图、遮罩编辑、批量任务、历史复用、费用追踪和 Agent API。
13
 
14
  <p align="center">
15
+ <img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/5d089e0e43da0a0478aa6c7f29ffd8e5dc181543/readme-images/interface.jpg?v=20260608-07b596b" alt="GPT Image Playground 界面" width="900"/>
16
  </p>
17
 
18
  ## 快速开始
 
83
  遮罩编辑示例:
84
 
85
  <p align="center">
86
+ <img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/5d089e0e43da0a0478aa6c7f29ffd8e5dc181543/readme-images/mask-creation.jpg?v=20260608-07b596b" alt="遮罩创建" width="900"/>
87
  </p>
88
 
89
  历史与费用示例:
90
 
91
  <p align="center">
92
+ <img src="https://raw.githubusercontent.com/MisonL/gpt-image-playground-customer/5d089e0e43da0a0478aa6c7f29ffd8e5dc181543/readme-images/history.jpg?v=20260608-07b596b" alt="历史面板" width="900"/>
93
  </p>
94
 
95
  ## 配置
public/hf-space-deploy-marker.json CHANGED
@@ -1,6 +1,6 @@
1
  {
2
  "schema_version": 1,
3
- "local_sha": "912aa682eb019912a0298072cc7a67a11040d502",
4
- "created_at": "2026-07-23T06:16:09.836Z",
5
- "deploy_id": "9af7705f-34b0-4058-8c85-5a29c56ada87"
6
  }
 
1
  {
2
  "schema_version": 1,
3
+ "local_sha": "5d089e0e43da0a0478aa6c7f29ffd8e5dc181543",
4
+ "created_at": "2026-07-23T09:45:05.334Z",
5
+ "deploy_id": "a598c41b-0709-4241-9127-e1aa7744e48b"
6
  }
scripts/agent-skill-scripts.test.mjs CHANGED
@@ -2353,7 +2353,7 @@ describe('Agent skill script argument validation', () => {
2353
  );
2354
  });
2355
 
2356
- it('omits page SSE streaming strategy unless explicitly requested', async () => {
2357
  let pageRequestBody = '';
2358
  await withServer(
2359
  async (request, response) => {
@@ -2399,7 +2399,7 @@ describe('Agent skill script argument validation', () => {
2399
 
2400
  assert.equal(result.status, 0);
2401
  assert.equal(result.stderr.trim(), '');
2402
- assert.match(pageRequestBody, /name="stream"\r?\n\r?\ntrue/);
2403
  assert.doesNotMatch(pageRequestBody, /name="image_streaming_strategy"/);
2404
  }
2405
  );
 
2353
  );
2354
  });
2355
 
2356
+ it('omits implicit page streaming controls unless explicitly requested', async () => {
2357
  let pageRequestBody = '';
2358
  await withServer(
2359
  async (request, response) => {
 
2399
 
2400
  assert.equal(result.status, 0);
2401
  assert.equal(result.stderr.trim(), '');
2402
+ assert.doesNotMatch(pageRequestBody, /name="stream"/);
2403
  assert.doesNotMatch(pageRequestBody, /name="image_streaming_strategy"/);
2404
  }
2405
  );
scripts/page-form-streaming.test.mjs ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import assert from 'node:assert/strict';
2
+ import { spawn } from 'node:child_process';
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { createServer } from 'node:http';
5
+ import { tmpdir } from 'node:os';
6
+ import { join } from 'node:path';
7
+ import { describe, it } from 'node:test';
8
+ import { fileURLToPath } from 'node:url';
9
+
10
+ const repoRoot = fileURLToPath(new URL('..', import.meta.url));
11
+ const skillScriptsRoot = join(repoRoot, 'skills/gpt-image-playground-agent/scripts');
12
+
13
+ describe('Page form streaming defaults', () => {
14
+ it('does not force stream=true for page generate JSON responses', async () => {
15
+ let requestBody = '';
16
+ await withServer(
17
+ async (request, response) => {
18
+ if (request.url === '/api/agent/capabilities') {
19
+ writePageCapabilities(response);
20
+ return;
21
+ }
22
+ if (request.url === '/api/images') {
23
+ requestBody = await readRequestText(request);
24
+ writePageImageResult(response, 'generate.webp', 'generate-page-json-key');
25
+ return;
26
+ }
27
+ writeMissing(response);
28
+ },
29
+ async (baseUrl) => {
30
+ const result = await runSkillScript(
31
+ 'generate-image.mjs',
32
+ [
33
+ '--allow-billable',
34
+ '--page-sse',
35
+ '--format',
36
+ 'webp',
37
+ '--idempotency-key',
38
+ 'generate-page-json-key',
39
+ 'generate prompt'
40
+ ],
41
+ { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
42
+ );
43
+
44
+ assert.equal(result.status, 0);
45
+ assert.equal(result.stderr.trim(), '');
46
+ const body = JSON.parse(result.stdout);
47
+ assert.equal(body.images[0].filename, 'generate.webp');
48
+ assert.equal(body.images[0].absolute_path, `${baseUrl}/api/image/generate.webp`);
49
+ assert.doesNotMatch(requestBody, /name="stream"/);
50
+ }
51
+ );
52
+ });
53
+
54
+ it('does not force stream=true for default page edit JSON responses', async () => {
55
+ const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-page-form-edit-'));
56
+ try {
57
+ const imagePath = join(tempRoot, 'source.png');
58
+ writeFileSync(imagePath, fakePngBuffer(2, 1));
59
+ let requestBody = '';
60
+
61
+ await withServer(
62
+ async (request, response) => {
63
+ if (request.url === '/api/agent/capabilities') {
64
+ writePageCapabilities(response);
65
+ return;
66
+ }
67
+ if (request.url === '/api/images') {
68
+ requestBody = await readRequestText(request);
69
+ writePageImageResult(response, 'edit.webp', 'edit-page-json-key');
70
+ return;
71
+ }
72
+ writeMissing(response);
73
+ },
74
+ async (baseUrl) => {
75
+ const result = await runSkillScript(
76
+ 'edit-image.mjs',
77
+ [
78
+ '--allow-billable',
79
+ '--idempotency-key',
80
+ 'edit-page-json-key',
81
+ imagePath,
82
+ 'edit prompt'
83
+ ],
84
+ { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
85
+ );
86
+
87
+ assert.equal(result.status, 0);
88
+ assert.equal(result.stderr.trim(), '');
89
+ const body = JSON.parse(result.stdout);
90
+ assert.equal(body.images[0].filename, 'edit.webp');
91
+ assert.equal(body.images[0].absolute_path, `${baseUrl}/api/image/edit.webp`);
92
+ assert.doesNotMatch(requestBody, /name="stream"/);
93
+ }
94
+ );
95
+ } finally {
96
+ rmSync(tempRoot, { recursive: true, force: true });
97
+ }
98
+ });
99
+
100
+ it('does not force stream=true for batch page edit JSON responses', async () => {
101
+ const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-page-form-batch-'));
102
+ try {
103
+ const imagePath = join(tempRoot, 'source.png');
104
+ const inputPath = join(tempRoot, 'tasks.jsonl');
105
+ writeFileSync(imagePath, fakePngBuffer(2, 1));
106
+ writeFileSync(
107
+ inputPath,
108
+ JSON.stringify({
109
+ id: 'batch-page-json',
110
+ mode: 'edit',
111
+ prompt: 'batch edit prompt',
112
+ image_path: imagePath,
113
+ size: '1024x1024',
114
+ idempotency_key: 'batch-page-json-key'
115
+ })
116
+ );
117
+ let requestBody = '';
118
+
119
+ await withServer(
120
+ async (request, response) => {
121
+ if (request.url === '/api/agent/capabilities') {
122
+ writePageCapabilities(response);
123
+ return;
124
+ }
125
+ if (request.url === '/api/images') {
126
+ requestBody = await readRequestText(request);
127
+ writePageImageResult(response, 'batch.webp', 'batch-page-json-key');
128
+ return;
129
+ }
130
+ writeMissing(response);
131
+ },
132
+ async (baseUrl) => {
133
+ const result = await runSkillScript(
134
+ 'batch-images.mjs',
135
+ ['--allow-billable', '--input', inputPath],
136
+ { GPT_IMAGE_PLAYGROUND_URL: baseUrl }
137
+ );
138
+
139
+ assert.equal(result.status, 0);
140
+ assert.equal(result.stderr.trim(), '');
141
+ const body = JSON.parse(result.stdout);
142
+ assert.equal(body.results[0].status, 'succeeded');
143
+ assert.equal(body.results[0].response.images[0].filename, 'batch.webp');
144
+ assert.equal(
145
+ body.results[0].response.images[0].absolute_path,
146
+ `${baseUrl}/api/image/batch.webp`
147
+ );
148
+ assert.doesNotMatch(requestBody, /name="stream"/);
149
+ }
150
+ );
151
+ } finally {
152
+ rmSync(tempRoot, { recursive: true, force: true });
153
+ }
154
+ });
155
+ });
156
+
157
+ function writePageCapabilities(response) {
158
+ response.writeHead(200, { 'content-type': 'application/json' });
159
+ response.end(
160
+ JSON.stringify({
161
+ agent_streaming: {
162
+ page_sse: {
163
+ supported: true,
164
+ endpoint: '/api/images',
165
+ auth: { required: false, schemes: [], form_field: 'passwordHash' }
166
+ }
167
+ },
168
+ agent_jobs: { supported: true, mode: 'job_polling' }
169
+ })
170
+ );
171
+ }
172
+
173
+ function writePageImageResult(response, filename, clientRequestId) {
174
+ response.writeHead(200, { 'content-type': 'application/json' });
175
+ response.end(
176
+ JSON.stringify({
177
+ images: [
178
+ {
179
+ filename,
180
+ path: `/api/image/${filename}`,
181
+ output_format: 'webp',
182
+ clientRequestId
183
+ }
184
+ ],
185
+ clientRequestId
186
+ })
187
+ );
188
+ }
189
+
190
+ function writeMissing(response) {
191
+ response.writeHead(404, { 'content-type': 'application/json' });
192
+ response.end(JSON.stringify({ error: 'missing' }));
193
+ }
194
+
195
+ function withServer(handler, run) {
196
+ return new Promise((resolve, reject) => {
197
+ const server = createServer((request, response) => {
198
+ Promise.resolve(handler(request, response)).catch(reject);
199
+ });
200
+ server.listen(0, '127.0.0.1', async () => {
201
+ const address = server.address();
202
+ try {
203
+ assert.equal(typeof address, 'object');
204
+ assert.ok(address);
205
+ await run(`http://127.0.0.1:${address.port}`);
206
+ resolve();
207
+ } catch (error) {
208
+ reject(error);
209
+ } finally {
210
+ server.close();
211
+ }
212
+ });
213
+ });
214
+ }
215
+
216
+ function runSkillScript(filename, args, env) {
217
+ return new Promise((resolve, reject) => {
218
+ const child = spawn(process.execPath, [join(skillScriptsRoot, filename), ...args], {
219
+ cwd: repoRoot,
220
+ env: buildTestEnvironment(env),
221
+ stdio: ['ignore', 'pipe', 'pipe']
222
+ });
223
+ let stdout = '';
224
+ let stderr = '';
225
+ const timeout = setTimeout(() => child.kill('SIGTERM'), 30_000);
226
+ child.stdout.setEncoding('utf8');
227
+ child.stderr.setEncoding('utf8');
228
+ child.stdout.on('data', (chunk) => {
229
+ stdout += chunk;
230
+ });
231
+ child.stderr.on('data', (chunk) => {
232
+ stderr += chunk;
233
+ });
234
+ child.once('error', (error) => {
235
+ clearTimeout(timeout);
236
+ reject(error);
237
+ });
238
+ child.once('close', (status, signal) => {
239
+ clearTimeout(timeout);
240
+ resolve({ status, signal, stdout, stderr });
241
+ });
242
+ });
243
+ }
244
+
245
+ function buildTestEnvironment(overrides) {
246
+ const names = ['HOME', 'PATH', 'SystemRoot', 'TEMP', 'TMP', 'TMPDIR', 'USERPROFILE'];
247
+ const environment = { GPT_IMAGE_AGENT_LOAD_ENV_FILE: '0' };
248
+ for (const name of names) {
249
+ if (process.env[name] !== undefined) environment[name] = process.env[name];
250
+ }
251
+ return { ...environment, ...overrides };
252
+ }
253
+
254
+ function readRequestText(request) {
255
+ return new Promise((resolve, reject) => {
256
+ let body = '';
257
+ request.setEncoding('utf8');
258
+ request.on('data', (chunk) => {
259
+ body += chunk;
260
+ });
261
+ request.on('end', () => resolve(body));
262
+ request.on('error', reject);
263
+ });
264
+ }
265
+
266
+ function fakePngBuffer(width, height) {
267
+ const buffer = Buffer.alloc(24);
268
+ buffer[0] = 0x89;
269
+ buffer.write('PNG', 1, 'ascii');
270
+ buffer[4] = 0x0d;
271
+ buffer[5] = 0x0a;
272
+ buffer[6] = 0x1a;
273
+ buffer[7] = 0x0a;
274
+ buffer.writeUInt32BE(13, 8);
275
+ buffer.write('IHDR', 12, 'ascii');
276
+ buffer.writeUInt32BE(width, 16);
277
+ buffer.writeUInt32BE(height, 20);
278
+ return buffer;
279
+ }
skills/gpt-image-playground-agent/scripts/batch-images.mjs CHANGED
@@ -1116,8 +1116,7 @@ function buildPageSseRequestPreview(task) {
1116
  size: raw.size || (task.mode === 'generate' ? '1024x1024' : 'auto'),
1117
  quality: raw.quality || (task.mode === 'generate' ? 'high' : 'auto'),
1118
  response_mode: readResponseMode(raw),
1119
- clientRequestId: task.idempotencyKey,
1120
- stream: 'true'
1121
  };
1122
  if (raw.n !== undefined) preview.n = readConfiguredPositiveInteger(raw.n, `${task.id}.n`, 1);
1123
  if (raw.stream_mode) preview.stream_mode = String(raw.stream_mode);
@@ -1421,7 +1420,6 @@ function buildPageSseTaskFormData(task) {
1421
  formData.append('quality', raw.quality || (task.mode === 'generate' ? 'high' : 'auto'));
1422
  formData.append('response_mode', readResponseMode(raw));
1423
  formData.append('clientRequestId', task.idempotencyKey);
1424
- formData.append('stream', 'true');
1425
  if (raw.n !== undefined) formData.append('n', String(readConfiguredPositiveInteger(raw.n, `${task.id}.n`, 1)));
1426
  if (raw.stream_mode) formData.append('stream_mode', String(raw.stream_mode));
1427
  if (raw.streaming_strategy) formData.append('image_streaming_strategy', String(raw.streaming_strategy));
 
1116
  size: raw.size || (task.mode === 'generate' ? '1024x1024' : 'auto'),
1117
  quality: raw.quality || (task.mode === 'generate' ? 'high' : 'auto'),
1118
  response_mode: readResponseMode(raw),
1119
+ clientRequestId: task.idempotencyKey
 
1120
  };
1121
  if (raw.n !== undefined) preview.n = readConfiguredPositiveInteger(raw.n, `${task.id}.n`, 1);
1122
  if (raw.stream_mode) preview.stream_mode = String(raw.stream_mode);
 
1420
  formData.append('quality', raw.quality || (task.mode === 'generate' ? 'high' : 'auto'));
1421
  formData.append('response_mode', readResponseMode(raw));
1422
  formData.append('clientRequestId', task.idempotencyKey);
 
1423
  if (raw.n !== undefined) formData.append('n', String(readConfiguredPositiveInteger(raw.n, `${task.id}.n`, 1)));
1424
  if (raw.stream_mode) formData.append('stream_mode', String(raw.stream_mode));
1425
  if (raw.streaming_strategy) formData.append('image_streaming_strategy', String(raw.streaming_strategy));
skills/gpt-image-playground-agent/scripts/edit-image.mjs CHANGED
@@ -660,7 +660,6 @@ function buildPageSseFormData() {
660
  formData.append('quality', options.quality);
661
  formData.append('response_mode', options.responseMode);
662
  formData.append('clientRequestId', idempotencyKey);
663
- formData.append('stream', 'true');
664
  formData.append('output_format', readOutputFormat(options));
665
  if (readOutputCompression(options) !== undefined) {
666
  formData.append('output_compression', String(readOutputCompression(options)));
 
660
  formData.append('quality', options.quality);
661
  formData.append('response_mode', options.responseMode);
662
  formData.append('clientRequestId', idempotencyKey);
 
663
  formData.append('output_format', readOutputFormat(options));
664
  if (readOutputCompression(options) !== undefined) {
665
  formData.append('output_compression', String(readOutputCompression(options)));
skills/gpt-image-playground-agent/scripts/generate-image.mjs CHANGED
@@ -835,7 +835,6 @@ function buildPageSseFormData() {
835
  formData.append('output_format', requestBody.output_format);
836
  formData.append('response_mode', requestBody.response_mode);
837
  formData.append('clientRequestId', idempotencyKey);
838
- formData.append('stream', 'true');
839
  if (requestBody.stream_mode) formData.append('stream_mode', requestBody.stream_mode);
840
  formData.append('partial_images', String(requestBody.partial_images ?? pageSsePartialImages));
841
  if (requestBody.image_backend)
@@ -1510,7 +1509,6 @@ async function checkPageSseContract() {
1510
  formData.append('quality', requestBody.quality);
1511
  formData.append('output_format', requestBody.output_format);
1512
  formData.append('response_mode', requestBody.response_mode);
1513
- formData.append('stream', 'true');
1514
  formData.append('clientRequestId', 'x'.repeat(pageSseClientRequestIdMaxLength + 1));
1515
  if (passwordHash) formData.append('passwordHash', passwordHash);
1516
  const { response, text } = await fetchJson(`${baseUrl}${PAGE_SSE_ENDPOINT}`, {
 
835
  formData.append('output_format', requestBody.output_format);
836
  formData.append('response_mode', requestBody.response_mode);
837
  formData.append('clientRequestId', idempotencyKey);
 
838
  if (requestBody.stream_mode) formData.append('stream_mode', requestBody.stream_mode);
839
  formData.append('partial_images', String(requestBody.partial_images ?? pageSsePartialImages));
840
  if (requestBody.image_backend)
 
1509
  formData.append('quality', requestBody.quality);
1510
  formData.append('output_format', requestBody.output_format);
1511
  formData.append('response_mode', requestBody.response_mode);
 
1512
  formData.append('clientRequestId', 'x'.repeat(pageSseClientRequestIdMaxLength + 1));
1513
  if (passwordHash) formData.append('passwordHash', passwordHash);
1514
  const { response, text } = await fetchJson(`${baseUrl}${PAGE_SSE_ENDPOINT}`, {
src/app/api/deploy-marker/route.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { NextResponse } from 'next/server';
2
 
3
- const deployMarker = {"schema_version":1,"local_sha":"912aa682eb019912a0298072cc7a67a11040d502","created_at":"2026-07-23T06:16:09.836Z","deploy_id":"9af7705f-34b0-4058-8c85-5a29c56ada87"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6
 
 
1
  import { NextResponse } from 'next/server';
2
 
3
+ const deployMarker = {"schema_version":1,"local_sha":"5d089e0e43da0a0478aa6c7f29ffd8e5dc181543","created_at":"2026-07-23T09:45:05.334Z","deploy_id":"a598c41b-0709-4241-9127-e1aa7744e48b"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6